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

  1. Inspect the path: if it is a directory that is no longer needed, remove or relocate it before the write
  2. Fix the writing code to target a file path that cannot collide with a directory (different name or subdirectory structure)
  3. 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

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


AI-assisted analysis of puppetlabs/puppet@e227c27540 (2026-08-21). Data as JSON: /api/errors/620f3e2fbae1b231. Report an issue: GitHub.