puppetlabs/puppet · error · Errno::EISDIR
Is a directory: %{directory}
Error message
Is a directory: %{directory} What it means
On JRuby (puppetserver), File.rename happily replaces a destination directory instead of failing like MRI Ruby. Puppet::FileSystem::JRuby#replace_file pre-checks directory?(path) and raises Errno::EISDIR with the path so both Rubies behave identically: atomic file replacement never silently deletes a directory.
Source
Thrown at lib/puppet/file_system/jruby.rb:20
require_relative '../../puppet/file_system/posix'
class Puppet::FileSystem::JRuby < Puppet::FileSystem::Posix
def unlink(*paths)
File.unlink(*paths)
rescue Errno::ENOENT
# JRuby raises ENOENT if the path doesn't exist or the parent directory
# doesn't allow execute/traverse. If it's the former, `stat` will raise
# ENOENT, if it's the later, it'll raise EACCES
# See https://github.com/jruby/jruby/issues/5617
stat(*paths)
end
def replace_file(path, mode = nil, &block)
# MRI Ruby rename checks if destination is a directory and raises, while
# JRuby removes the directory and replaces the file.
if directory?(path)
raise Errno::EISDIR, _("Is a directory: %{directory}") % { directory: path }
end
super
end
end
View on GitHub (pinned to e227c27540)
Solutions
- Inspect the path: if it is a directory that is no longer needed, remove or relocate it before the write
- Fix the writing code to target a file path that cannot collide with a directory (different name or subdirectory structure)
- If the directory is required, change the design: write the file elsewhere and reference both explicitly
Example fix
# before
Puppet::FileSystem.replace_file('/etc/app/state', 0o644) { |f| f.write(data) }
# raises Errno::EISDIR on JRuby because /etc/app/state is a directory
# after
require 'fileutils'
FileUtils.rm_r('/etc/app/state') if File.directory?('/etc/app/state')
Puppet::FileSystem.replace_file('/etc/app/state', 0o644) { |f| f.write(data) } Defensive patterns
Strategy: validation
Validate before calling
if Puppet::FileSystem.directory?(path)
raise ArgumentError, "refusing to replace_file a directory: #{path}"
end
Puppet::FileSystem.replace_file(path, 0o644) { |f| f.write(data) } Type guard
def writable_file_path?(path) !Puppet::FileSystem.exist?(path) || Puppet::FileSystem.file?(path) end
Try / catch
begin
Puppet::FileSystem.replace_file(path, 0o644) { |f| f.write(data) }
rescue Errno::EISDIR
raise "#{path} is a directory; refusing to delete it automatically"
end Prevention
- Never let file and directory resources target the same path
- On shared code paths, test the JRuby behavior (puppetserver) not just MRI
- Clean up leftover directories after changing a path's intended type
When it happens
Trigger: Calling Puppet::FileSystem.replace_file (directly or via Puppet APIs that write files atomically) where the destination path is an existing directory, under puppetserver/JRuby; usually a path collision between a planned file and an existing directory.
Common situations: A manifest or CA/inventory code path writing to a path that a previous run or package created as a directory; storing state under a path that is also a mount point; leftover directories from restored backups.
Related errors
- Is a directory: %{directory}
- Is a directory: %{directory}
- Timeout waiting for exclusive lock on %{path}
- Operation not permitted
- ReplaceFile(#{target}, #{source})
AI-assisted analysis of puppetlabs/puppet@e227c27540 (2026-08-21).
Data as JSON: /api/errors/620f3e2fbae1b231.
Report an issue: GitHub.