This document describes puppet's Ruby DSL for defining resource types, aka custom types. Examples are taken from the puppetlabs-aws
module and core puppet.
The second part of the document describes issues that cause puppet resource types to leak across environments (lack of environment isolation).
Defines a new puppet resource type and registers the type using the specified symbolic name. For example, to define a resource type named ec2_instance
:
Puppet::Type.newtype(:ec2_instance) do
# definition goes here
end
The newtype
method creates a new class Puppet::Type::Ec2_instance
, which subclasses Puppet::Type
. The block passed to the newtype
method defines methods and variables on the Puppet::Type::Ec2_instance
class.
Defines a method for retrieving the class for a resource type, for example, when writing a provider for the ec2_instance
type:
Puppet::Type.type(:ec2_instance).provide(:v2, ...) do
# provider definition
end
Within the Puppet::Type.newtype
block, the following DSL methods are often called, and are evaluated in the context of the Puppet::Type::<name>
class:
Defines the documentation string for the type, which is output when running puppet describe <type>
. The doc string is usually set via the Puppet::Type
class variable, though there is also a Puppet::Type.doc=
class method.
Puppet::Type.newtype(:ec2_instance) do
@doc = 'A type representing an AWS VPC'
end
The type must specify a parameter to uniquely identify the resource. Conceptually this is referred to as the namevar. The type can specify multiple namevar parameters, which is referred to a composite namevar. For example, the identity of a package
resource is based on the name of the package and provider, because you can have different types of packages with the same name, e.g. rpm and gem.
Puppet will implicitly treat any parameter named name
as a namevar. A parameter can explicitly specify that it is a namevar, described in the Parameter & Properties section below.
Provides a way for setting attributes from the title. For example, the file
resource sets its path
namevar based on its title
, stripping trailing slashes in the process. So if title
is /foo/bar//
, then path
will be /foo/bar
.
Puppet::Type.newtype(:file) do
def self.title_patterns
[ [ /^(.*?)\/*\Z/m, [ [ :path ] ] ] ]
end
end
A type must define a title_patterns
method when using composite namevars so that puppet knows how to decompose the title
into its constituent namevars, e.g. see java-ks, websphere.
Creates an ensure
property with acceptable values of present
and absent
, each of which invoke the provider's create
and destroy
methods, respectively:
Puppet::Type.newtype(:ec2_instance) do
ensurable
end
Ensurable can also take a block, and in that case, the type should define the allowed set of values. For example, the package
type defines allowed values as:
Puppet::Type.newtype(:package) do
ensurable do
newvalue(:present)
newvalue(:absent)
newvalue(:purged)
newvalue(:held)
newvalue(:latest)
newvalue(/./)
end
end
Defines a new property for a type. Puppet will ensure the resource's current state (as retrieved by the provider) matches the property's desired state (as expressed in the manifest).
Puppet::Type.newtype(:ec2_instance) do
newproperty(:region) do
end
end
Defines a new parameter for the type. Puppet does not enforce state for a parameter. Instead parameters specify additional information about how the provider should ensure the resource and its properties are in the correct state.
Puppet::Type.newtype(:ec2_instance) do
newparam(:user_data) do
end
end
Defines a new metaparameter for a type. Puppet has several built-in metaparameters described below. It is uncommon for types to define new metaparams as these generally require puppet core changes to be useful, e.g. noop
, tag
, schedule
, etc.
Performs per-resource validation at catalog application time. This method is often used to validate related parameters and properties for a single resource, e.g. mutually exclusive properties or if one property requires another. One concrete example is the file
resource requires the source
parameter to be set if the recurse
parameter is set to remote
:
Puppet::Type.newtype(:file) do
validate do
self.fail "You cannot specify a remote recursion without a source" if !self[:source] && self[:recurse] == :remote
end
end
Allows a type to define automatic relationships (before
, subscribe
, require
, notify
) between each instance of the type and instances of a different type. For example, the exec
type will autorequire any file resource whose path
matches the exec command
. Note the relationship will only be added to the catalog if puppet is managing both ends of the relationship. That way puppet will automatically create the file containing the command to execute, before trying to executing it, regardless of manifest order.
autorequire(:file) do
reqs = []
self[:command].scan(file_regex) { |str|
reqs << str
}
reqs
end
Autorequires are by far the most common, though recently puppet added support for autobefore
, autonotify
, and autosubscribe
.
The agent will call the pre_run_check
method for each Puppet::Type
instance in the agent's catalog. This provides an opportunity for a resource to perform consistency checks/validation against other resources in the catalog. It differs from the validate
method, since it is called later during catalog application, and can rely on the catalog having all generated resources.
Defines a feature for the type, which allows puppet to perform additional validation on the agent at catalog application time based on the selected provider. For example the service
type defines an enableable
feature:
Puppet::Type.newtype(:service) do
feature :enableable
end
And the type specifies that the enable
property can only be managed if the selected provider supports the enableable
feature. This validation check is performed at catalog application time, once the provider has been resolved on the agent.
Puppet::Type.newtype(:service) do
newproperty(:enable, :required_features => :enableable) do
end
end
An array of features can also be specified, e.g.
newproperty(:enable, :required_features => [:green, :blue]) do
end
A provider indicates it supports the feature using has_feature
:
Puppet::Type.type(:service).provide :launchd, :parent => :base do
has_feature :enableable
end
A type can also restrict allowed parameter/property values based on provider features. For example, the enable
property can only be set to mask
if the provider is maskable
:
Puppet::Type.newtype(:service) do
feature :maskable
newproperty(:enable, :required_features => :enableable) do
...
newvalue(:mask, :event => :service_disabled, :required_features => :maskable) do
provider.mask
end
end
end
Since puppet resource types are defined in ruby, you can mixin additional functionality. The AWS module uses this to create subclasses for different types of Route53 DNS records:
Puppet::Type.newtype(:route53_a_record) do
extend PuppetX::Puppetlabs::Route53Record
@doc = 'Type representing a Route53 DNS record.'
create_properties_and_params()
end
where the base module defines common parameters and properties for all Route53Records
:
module PuppetX
module Puppetlabs
module Route53Record
def create_properties_and_params
ensurable
newproperty(:zone) do
end
end
end
end
end
The following DSL methods are commonly used for individual parameters and properties within the body of newparam
and newproperty
respectively:
Description of the property, output when running puppet describe <type>
:
Puppet::Type.newtype(:ec2_instance) do
newproperty(:region) do
desc 'The region in which to launch the instance.'
end
end
Specifies that the parameter is the namevar
(aka identity) for the resource:
Puppet::Type.newtype(:ec2_instance) do
newparam(:name) do
isnamevar
end
end
As mentioned earlier, if a type specifies a parameter named :name
, it will automatically be the namevar, so the call to isnamevar
is redundant, but is explicit.
Alternatively, you can pass an option when calling newparam:
Puppet::Type.newtype(:ec2_instance) do
newparam(:name, :namevar => true) do
end
end
Note that the namevar is necessarily a parameter, and not a property, since changing the name identifies a different resource as opposed to changing the name of an existing resource.
Defining a parameter as the namevar also means it is required. Normally the namevar is automatically set to be the same as the title, unless the type overrides the title_patterns
method, e.g. for composite namevars.
Validates an individual parameter value at catalog application time. The value to validate is yielded to the block:
Puppet::Type.newtype(:ec2_instance) do
newproperty(:region) do
validate do |value|
fail 'region should not contain spaces' if value =~ /\s/
end
end
end
Puppet parameter and properties can be multi-valued. For example, the ec2_instance
can be given a list of security_groups
, and each value in the list will be yielded to the validate
method:
Puppet::Type.newtype(:ec2_instance) do
newproperty(:security_groups, ...) do
validate do |value|
fail 'security_groups should be ...' if value !~ /something/
end
end
end
Normalizes the desired (aka should
) property value at catalog application time. Puppet will compare the normalized value against the current
value the provider returns to determine if the property is insync or not. There is also an unmunge
method, less commonly used. For example, the ec2_autoscalinggroup
defines a minimum number of instances in the group, where the value is munged from a string to an integer:
Puppet::Type.newtype(:ec2_autoscalinggroup) do
newproperty(:min_size) do
munge do |value|
value.to_i
end
end
end
Often times the logic for validation and munging is the same, e.g. try to convert a value into an integer. As a result, the validate
logic is omitted, and validation is performed during munge
.
Defines an enumeration of values for a parameter or property. For example the ec2_instance
type uses the plural form newvalues
to define an enumeration of instance_initiated_shutdown_behavior
values:
Puppet::Type.newtype(:ec2_instance) do
newparam(:instance_initiated_shutdown_behavior) do
desc 'Whether the instance stops or terminates when you initiate shutdown from the instance.'
newvalues(:stop, :terminate)
end
end
The singular form newvalue
defines one possible value and takes a block. At catalog application time, if puppet determines the property is not insync, it will call the block to "sync" that resource's property. It's common to define multiple newvalue
blocks, where each value's block calls an appropriate provider method.
Puppet::Type.newtype(:ec2_instance) do
newproperty(:ensure) do
newvalue(:running) do
provider.create unless provider.running?
end
newvalue(:stopped) do
provider.stop unless provider.stopped?
end
end
end
The newvalue
and newvalues
methods can also be passed a regex. Puppet will compare explicit symbols/strings first, and if there are no matches, compare regex's. For example, the package
type defines:
Puppet::Type.newtype(:package) do
newproperty(:ensure) do
newvalue(:present)
newvalue(:absent)
...
newvalue(/./)
end
end
The last regex is used to match version strings, e.g ensure => '1.2.3'
Aliases a value to be the same as an existing value. For example, the package type aliases installed
to be the same as present
, because it's more natural to declare that a package is installed
:
Puppet::Type.newtype(:ec2_instance) do
newproperty(:ensure) do
newvalue(:present)
aliasvalue(:installed, :present)
end
end
Defines the default value for a parameter. If a value is specified, then defaultto
creates a default
method with a block that always returns the specified value.
Puppet::Type.newtype(:ec2_instance) do
newparam(:instance_initiated_shutdown_behavior) do
newvalues(:stop, :terminate)
defaultto :stop
end
end
If a block is specified, then the block is called to retrieve the default value at catalog application time. For example, the filebucket
type defines a server
parameter:
Puppet::Type.newtype(:filebucket) do
newparam(:server) do
desc "The server providing the remote filebucket service."
defaultto { Puppet[:server] }
end
end
The default value of Puppet[:server]
works for all agents, because the default value is resolved at catalog application time, not compilation time.
Overrides log messages for a property at catalog application time. Common examples are convert an id to a human readable name, sorting multi-valued attributes, redacting passwords, etc.
Puppet::Type.newtype(:user) do
newproperty(:password) do
def is_to_s(currentvalue)
return '[old password hash redacted]'
end
end
end
The following options are passed to the newparam
and newproperty
methods to modify their behavior. These are largely hacks because puppet's predefined parameter/properties types are not well-specified or complete. For example, there isn't a Puppet::Parameter::Integer
class.
By default, if multiple desired (aka should
) values are specified in a manifest, puppet will make sure the current (aka is
) value matches at least one of the desired values. Alternatively, you can specify array_matching => all
, and puppet will ensure that the array of current values match the desired values.
By default, the insync?
comparison is sensitive to order and duplicate values, so sometimes a type will override the insync?
method. For example, security groups are compared set-wise, which ignores duplicates.
Puppet::Type.newtype(:ec2_instance) do
newproperty(:security_groups, :array_matching => :all) do
desc 'The security groups to associate the instance.'
def insync?(is)
is.to_set == should.to_set
end
end
end
Specifies a parent class that the newly defined property/parameter should extend. For example, the ec2_instance
type defines a tags
property that inherits from the PuppetX::Property::AwsTag
class:
require_relative '../../puppet_x/puppetlabs/property/tag.rb'
Puppet::Type.newtype(:ec2_instance) do
newproperty(:tags, :parent => PuppetX::Property::AwsTag) do
desc 'The tags for the instance.'
end
end
The PuppetX::Property::AwsTag
class is defined in helper code, so the type must require it using a relative path. The helper code defines logic for validating aws tags and how log messages are written:
module PuppetX
module Property
class AwsTag < Puppet::Property
def format_tags(value)
Hash[value.sort]
end
[:should_to_s, :is_to_s].each { |method|
alias_method method, :format_tags
}
validate do |value|
fail 'tags should be a Hash' unless value.is_a?(Hash)
end
end
end
end
Meta-programs a predicate method for the parameter/property. For example, the file type defines a force
parameter:
newparam(:force, :boolean => true, :parent => Puppet::Parameter::Boolean) do
end
which defines a Puppet::Type::File#force?
method. It's not clear who calls this method, but the pattern of specifying both :boolean => true, :parent => Puppet::Parameter::Boolean
is copy/pasted everywhere.
irb(main):008:0> Puppet::Type.type(:file).new(:path => '/tmp/foo', :force => true).force?
=> true
When the master compiles a catalog for the first time, it loads puppet resource types from that request's environment-specific modulepath. If the master then compiles a catalog using a different environment, it will use whatever type information was loaded from the previous environment. We refer to this as puppet types leaking across environments.
In the future, we want to isolate types within an environment, so that you can deploy different module versions in different environments, e.g. dev, qa, prod, but still get consistent results in any given environment.
This section describes issues with the way puppet resource types are defined and loaded, which prevent environment isolation.
-
The main issue is that the
Puppet::Type.newtype
method defines a class that is global in the ruby runtime. So you can't have two different versions of the type in the same ruby runtime. -
Types often require helper code, like the
AwsTag
example, and that has the same issue as above. -
Types use various tricks to require helper code in order to support when running on the master and when running
puppet apply
. For example:require_relative '../../puppet_x/puppetlabs/property/tag.rb'
require Pathname.new(__FILE__).dirname + '../' + 'puppet_x/puppetlabs/powershell_version'
begin require "puppet_x/puppetlabs/registry" rescue LoadError => detail require Pathname.new(__FILE__).dirname + "../../" + "puppet_x/puppetlabs/registry" end
-
The
Puppet::Type.newtype
method adds aprovider
parameter to the type and loads all providers for the type, even when the type is loaded on the master. A comment in the code says it is to determine the default provider, but that is no longer true. We only determine the default provider when the type is loaded on the agent, and we resolve provider confines and suitability. -
The
title_patterns
method is called during compilation and causes attribute values to be set on the resource that is serialized in the catalog. For example givenfile { '/foo/bar//': ensure => file }
, the catalog will contain:
{
"type": "File",
"title": "/foo/bar//",
"parameters": {
"path": "/foo/bar",
"ensure": "file"
}
},
There has been confusion over time about exactly what parts of a type are evaluated on the master vs agent. Part of the confusion is because the master will apply multiple settings catalogs for file/directory based-settings, e.g. ssldir
. This ensures the files exist and have the correct permissions. However, it causes the master's ruby process to behave like an agent, so all of the validate, munge, etc methods for Puppet::Type::File
are evaluated on the master. Also when the master is running as root, the same applies to the Puppet::Type::User
and Puppet::Type::Group
types so that the master can manage permissions for those file/directories. But generally speaking, the master does not call the validate, munge, etc methods.