diff --git a/lib/tmpdir.rb b/lib/tmpdir.rb index f78fd72..e5cafed 100644 --- a/lib/tmpdir.rb +++ b/lib/tmpdir.rb @@ -35,6 +35,14 @@ def self.tmpdir when !File.writable?(dir) # We call File.writable?, not stat.writable?, because you can't tell if a dir is actually # writable just from stat; OS mechanisms other than user/group/world bits can affect this. + # + # However, in some container environments (e.g. Kubernetes with read-only root filesystem + # and emptyDir volumes), File.writable? may return false even though the directory is + # actually writable via mounted volumes. Fall back to stat.writable? in that case. + if stat.writable? + warn "#{name}: File.writable? reports not writable but file mode bits suggest writable, using anyway: #{dir}" + break dir + end warn "#{name} is not writable: #{dir}" when stat.world_writable? && !stat.sticky? warn "#{name} is world-writable: #{dir}" diff --git a/test/test_tmpdir.rb b/test/test_tmpdir.rb index c91fc33..357fbad 100644 --- a/test/test_tmpdir.rb +++ b/test/test_tmpdir.rb @@ -59,6 +59,59 @@ def test_tmpdir_not_empty_parent end end + def test_writable_fallback_to_stat + omit "no meaning on this platform" if /mswin|mingw/ =~ RUBY_PLATFORM + Dir.mktmpdir do |tmpdir| + envs = %w[TMPDIR TMP TEMP] + oldenv = envs.each_with_object({}) {|v, h| h[v] = ENV.delete(v)} + begin + ENV[envs[0]] = tmpdir + + # Stub File.writable? to return false for our tmpdir + # This simulates container environments where access(2) returns false + # even though the directory is actually writable + original_writable = File.method(:writable?) + File.define_singleton_method(:writable?) do |path| + if path == tmpdir + false + else + original_writable.call(path) + end + end + + # Should fall back to stat.writable? and succeed with a warning + assert_equal(tmpdir, assert_warn(/File\.writable\? reports not writable but file mode bits suggest writable/) { Dir.tmpdir }) + + ensure + # Restore original File.writable? + File.define_singleton_method(:writable?, original_writable) if original_writable + ENV.update(oldenv) + end + end + end + + def test_writable_fallback_both_fail + omit "no meaning on this platform" if /mswin|mingw/ =~ RUBY_PLATFORM + omit "root can write to any directory" if Process.uid == 0 + Dir.mktmpdir do |tmpdir| + envs = %w[TMPDIR TMP TEMP] + oldenv = envs.each_with_object({}) {|v, h| h[v] = ENV.delete(v)} + begin + ENV[envs[0]] = tmpdir + + # Make directory not writable (both File.writable? and stat.writable? will return false) + File.chmod(0555, tmpdir) + + # Should reject the directory with "not writable" warning + assert_not_equal(tmpdir, assert_warn(/is not writable/) { Dir.tmpdir }) + + ensure + File.chmod(0755, tmpdir) + ENV.update(oldenv) + end + end + end + def test_no_homedir bug7547 = '[ruby-core:50793]' home, ENV["HOME"] = ENV["HOME"], nil