puppetlabs/puppet · error · Puppet::Error

Could not destroy %{json} %{request}: %{detail}

Error message

Could not destroy %{json} %{request}: %{detail}

What it means

The JSON terminus destroy calls Puppet::FileSystem.unlink on the stored file. Errno::ENOENT is swallowed and 1 is returned to emulate success, but any other error - EACCES/EPERM, EISDIR, read-only filesystem, EROFS - is re-raised as Puppet::Error 'Could not destroy json <key>: <original message>' with the original backtrace preserved.

Source

Thrown at lib/puppet/indirector/json.rb:28

class Puppet::Indirector::JSON < Puppet::Indirector::Terminus
  def find(request)
    load_json_from_file(path(request.key), request.key)
  end

  def save(request)
    filename = path(request.key)
    FileUtils.mkdir_p(File.dirname(filename))

    Puppet::FileSystem.replace_file(filename, 0o660) { |f| f.print to_json(request.instance).force_encoding(Encoding::BINARY) }
  rescue TypeError => detail
    Puppet.log_exception(detail, _("Could not save %{json} %{request}: %{detail}") % { json: name, request: request.key, detail: detail })
  end

  def destroy(request)
    Puppet::FileSystem.unlink(path(request.key))
  rescue => detail
    unless detail.is_a? Errno::ENOENT
      raise Puppet::Error, _("Could not destroy %{json} %{request}: %{detail}") % { json: name, request: request.key, detail: detail }, detail.backtrace
    end

    1 # emulate success...
  end

  def search(request)
    Dir.glob(path(request.key)).collect do |file|
      load_json_from_file(file, request.key)
    end
  end

  # Return the path to a given node's file.
  def path(name, ext = '.json')
    if name =~ Puppet::Indirector::BadNameRegexp then
      Puppet.crit(_("directory traversal detected in %{json}: %{name}") % { json: self.class, name: name.inspect })
      raise ArgumentError, _("invalid key")
    end

View on GitHub (pinned to e227c27540)

Solutions

  1. Read the <detail> suffix; it names the real errno (permission denied, read-only file system, ...).
  2. Fix ownership: chown -R <serviceuser>:<servicegroup> "$(puppet config print server_datadir)" (and client_datadir on agents where relevant).
  3. Remount writable or relocate the datadir if the filesystem is read-only.
  4. If SELinux is enforcing, check audit logs and run restorecon on the datadir.
  5. Remove a stray directory-at-path manually if a path-type clash caused the error.

Example fix

# before: service user cannot unlink root-owned store file
# Could not destroy json node.example.com: Permission denied @ unlink_internal

# after
chown -R puppet:puppet /opt/puppetlabs/server/data/puppet
systemctl restart puppetserver
Defensive patterns

Strategy: try-catch

Validate before calling

path = File.join(store_dir, indirection_name, key + '.json')
raise ArgumentError, 'not removable by this user' unless File.writable?(File.dirname(path)) && (File.writable?(path) || !File.exist?(path))

Try / catch

begin
  indirection.destroy(key)
rescue Puppet::Error => e
  Puppet.err('destroy failed: ' + e.message) # operator follow-up: fix ownership
end

Prevention

When it happens

Trigger: Destroying an entry in a json-backed indirection store when server_datadir (or client_datadir) files are owned by root while the service runs as puppet; the datadir lives on a read-only mount; the path points at a directory instead of a file.

Common situations: Master data dirs chowned incorrectly after package upgrades; runs mixed between root and the service user; SELinux denials; containerized puppetservers with read-only persistent volumes.

Related errors


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