Puppet custom type validation woes

Since I’ve just lost a full day to troubleshooting this issue, I’m documenting it in case it hits anyone else. In at least puppet versions 4.7.0 and earlier, global type validation cannot be used to ensure the presence of paramters without breaking puppet resource.

Simplified example type:

Puppet::Type.newtype(:entropy_mask) do
  @desc = "Mask packages in Entropy"

  ensurable

  newparam(:name) do
    desc "Unique name for this mask"
  end

  newproperty(:package) do
    desc "Name of the package being masked"
  end

  validate do
    # This will break for `puppet resource`
    raise(ArgumentError, "Package is required") if self[:package].nil?
  end
end

This works fine to validate that in a puppet manifest the `package` parameter is provided, but not when puppet resource interrogates the state of the existing system due to the way the object is constructed.

Puppet calls the `provider.instances` to obtain a list of the resources on the system managed by that provider. In the example above, the provider was a child of ParsedFile and took care of parsing the contents of /etc/entropy/packages/package.mask, splitting the lines into the various properties including package

Puppet then tries to create an instance of the Type/resource for each object retrieved by the provider. It does so by instantiating an instance of the type, but passing in the namevar and provider only. It then attempts to iterate through all the provider properties and set them on the resource one by one. The problem is that validation happens on the call to new() and so the required properties have not yet been set.

Here’s the code from lib/puppet/type.rb from puppet 4.7.0, with irrelevant bits stripped out and comments added by me:

def self.instances
  # Iterate through all providers for this type
  providers_by_source.collect do |provider|
    # Iterate through all instances managed by this provider
    provider.instances.collect do |instance|
      # Instantiate the resource using just the namevar and provider
      result = new(:name => instance.name, :provider => instance)
      # Oops, type.validate() got called here, but not all properties
      # have been set yet</code>

      # Now iterate through all properties on the provider and set
      # them on the resource
      properties.each { |name| result.newattr(name) }

      # And add this to the list of resources to return
      result
    end
  end.flatten.compact
end

Of course, once the problem is understood, finding out that someone else already discovered this 2 years ago becomes much easier. Here’s the upstream bug report: https://tickets.puppetlabs.com/browse/PUP-3732

puppet-sabayon

I’ve just uploaded my first puppet module to the forge, optiz0r-sabayon, which improves support for the Sabayon Linux distribution in puppet.

This does the following things:

  • Overrides the operatingsystem fact for Sabayon hosts
  • Adds a provider for entropy package manager, and sets this as the default for Sabayon
  • Adds a shim service provider, that marks systemd as the default for Sabayon hosts
  • Adds an enman_repo type which manages installation of Sabayon Community Repositories onto a Sabayon host
  • Adds entropy_mask and entropy_unmask types, which manage package masks and unmasks

I’ll add more features as and when I need them. In the meantime, pull requests welcome!