Classes and defined types

Classes and defined types should follow scope and organization guidelines.

Separate files

Put all classes and resource type definitions (defined types) as separate files in the manifests directory of the module. Each file in the manifest directory should contain nothing other than the class or resource type definition.

Good: etc/puppetlabs/puppet/modules/apache/manifests/init.pp:

class apache { }
Good: etc/puppetlabs/puppet/modules/apache/manifests/ssl.pp:
class apache::ssl { }
Good: etc/puppetlabs/puppet/modules/apache/manifests/virtual_host.pp:
define apache::virtual_host () { }

Separating classes and defined types into separate files is functionally identical to declaring them in init.pp, but has the benefit of highlighting the structure of the module and making the function and structure more legible.

When a resource or include statement is placed outside of a class, node definition, or defined type, it is included in all catalogs. This can have undesired effects and is not always easy to detect.

Good: manifests/init.pp:

# class ntp
class ntp {
  ntp::install
}
# end of file
Bad: manifests/init.pp:
class ntp {
  #...
}
ntp::install

Internal organization of classes and defined types

Structure classes and defined types to accomplish one task.

Documentation comments for Puppet Strings should be included for each class or defined type. If used, documentation comments must precede the name of the element. For complete documentation recommendations, see the Modules section.

Put the lines of code in the following order:

  1. First line: Name of class or type.

  2. Following lines, if applicable: Define parameters. Parameters should be typed.

  3. Next lines: Includes and validation come after parameters are defined. Includes may come before or after validation, but should be grouped separately, with all includes and requires in one group and all validations in another. Validations should validate any parameters and fail catalog compilation if any parameters are invalid. See puppetlabs-ntp for an example.

  4. Next lines, if applicable: Should declare local variables and perform variable munging.

  5. Next lines: Should declare resource defaults.

  6. Next lines: Should override resources if necessary.

The following example follows the recommended style.

In init.pp:

  • The myservice class installs packages, ensures the state of myservice, and creates a tempfile with given content. If the tempfile contains digits, they are filtered out.

  • @param service_ensure the wanted state of services.

  • @param package_list the list of packages to install, at least one must be given, or an error of unsupported OS is raised.

  • @param tempfile_contents the text to be included in the tempfile, all digits are filtered out if present.

    class myservice (
      Enum['running', 'stopped'] $service_ensure,
      String                     $tempfile_contents,
      Optional[Array[String[1]]] $package_list = undef,
    ) {

  • Rather than just saying that there was a type mismatch for $package_list, this example includes an additional assertion with an improved error message. The list can be "not given", or have an empty list of packages to install. An assertion is made that the list is an array of at least one String, and that the String is at least one character long.

      assert_type(Array[String[1], 1], $package_list) |$expected, $actual| {
        fail("Module ${module_name} does not support ${facts['os']['name']} as the list of packages is of type ${actual}")
      }
    
      package { $package_list:
        ensure => present,
      }
    
      file { "/tmp/${variable}":
        ensure   => present,
        contents => regsubst($tempfile_contents, '\d', '', 'G'),
        owner    => '0',
        group    => '0',
        mode     => '0644',
      }
    
      service { 'myservice':
        ensure    => $service_ensure,
        hasstatus => true,
      }
     
      Package[$package_list] -> Service['myservice']
    }

In hiera.yaml: The default values can be merged if you want to extend with additional packages. If not, use default_hierarchy instead of hierarchy.

---
version: 5
defaults:
  data_hash: yaml_data
 
hierarchy:
- name: 'Per Operating System'
  path: "os/%{os.name}.yaml"
- name: 'Common'
  path: 'common.yaml'
In data/common.yaml:
myservice::service_ensure: running
In data/os/centos.yaml:
myservice::package_list:
- 'myservice-centos-package'

Public and private

Split your module into public and private classes and defined types where possible. Public classes or defined types should contain the parts of the module meant to be configured or customized by the user, while private classes should contain things you do not expect the user to change via parameters. Separating into public and private classes or defined types helps build reusable and readable code.

Help indicate to the user which classes are which by making sure all public classes have complete comments and denoting public and private classes in your documentation. Use the documentation tags “@api private” and “@api public” to make this clear. For complete documentation recommendations, see the Modules section.

Chaining arrow syntax

Most of the time, use relationship metaparameters rather than chaining arrows. When you have many interdependent or order-specific items, chaining syntax may be used. A chain operator should appear on the same line as its right-hand operand. Chaining arrows must be used left to right.

Good: Points left to right:

Package['httpd'] -> Service['httpd']
Good: On the line of the right-hand operand:
Package['httpd']
-> Service['httpd']
Bad: Arrows are not all pointing to the right:
Service['httpd'] <- Package['httpd']
Bad: Must be on the right-hand operand's line:
Package['httpd'] ->
Service['httpd']

Nested classes or defined types

Don't define classes and defined resource types within other classes or defined types. Declare them as close to node scope as possible. If you have a class or defined type which requires another class or defined type, put graceful failures in place if those required classes or defined types are not declared elsewhere.

Bad:

class apache {
  class ssl { ... }
}
Bad:
class apache {
  define config() { ... }
}

Display order of parameters

In parameterized class and defined resource type definitions, you can list required parameters before optional parameters (that is, parameters with defaults). Required parameters are parameters that are not set to anything, including undef. For example, parameters such as passwords or IP addresses might not have reasonable default values.

You can also group related parameters, order them alphabetically, or in the order you encounter them in the code. How you order parameters is personal preference.

Note that treating a parameter like a namevar and defaulting it to $title or $name does not make it a required parameter. It should still be listed following the order recommended here.

Good:

class dhcp (
  $dnsdomain,
  $nameservers,
  $default_lease_time = 3600,
  $max_lease_time     = 86400,
) {}
Bad:
class ntp (
  $options   = "iburst",
  $servers,
  $multicast = false,
) {}

Parameter defaults

Adding default values to the parameters in classes and defined types makes your module easier to use. Use Hiera data in your module to set parameter defaults. See Defining classes for details about setting parameter defaults with Hiera data. In simple cases, you can also specify the default values directly in the class or defined type.

Be sure to declare the data type of parameters, as this provides automatic type assertions.

Good: Parameter defaults set in the class with references to Hiera data:

class my_module (
  String $source,
  String $config,
) {
  # body of class
}
A hiera.yaml in the root of the module sets the hierarchy for assigning defaults:
---
version: 5
default_hierarchy: 
- name: 'defaults'
  path: 'defaults.yaml'
  data_hash: yaml_data
And the file data/defaults.yaml specifies the actual default values:
my_module::source: 'default source value'
my_module::config: 'default config value'

This example places the values in the defaults hierarchy, which means that the defaults are not merged into overriding values. To merge the defaults into those values, change the default_hierarchy to hierarchy.

If you are maintaining old code created prior to Puppet 4.9, you might encounter the use of a params.pp pattern. This pattern makes maintenance and troubleshooting difficult — refactor such code to use the Hiera data-in-modules pattern instead. See Adding Hiera data to a module for a detailed example showing how to replace params.pp with data.

Bad: params.pp

class my_module ( 
  String $source = $my_module::params::source,
  String $config = $my_module::params::config,
) inherits my_module::params {
  # body of class
}

Exported resources

Exported resources should be opt-in rather than opt-out. Your module should not be written to use exported resources to function by default unless it is expressly required.

When using exported resources, name the property collect_exported.

Exported resources should be exported and collected selectively using a search expression, ideally allowing user-defined tags as parameters so tags can be used to selectively collect by environment or custom fact.

Good:

define haproxy::frontend (
  $ports            = undef,
  $ipaddress        = [$::ipaddress],
  $bind             = undef,
  $mode             = undef,
  $collect_exported = false,
  $options          = {
    'option' => [
      'tcplog',
    ],
  },
) {
  # body of define
}

Parameter indentation and alignment

Parameters to classes or defined types must be uniformly indented in two spaces from the title. The equals sign should be aligned.

Good:

class profile::myclass (
  $var1    = 'default',
  $var2    = 'something else',
  $another = 'another default value',
) {

}
Good:
class ntp (
  Boolean                        $broadcastclient = false,
  Optional[Stdlib::Absolutepath] $config_dir      = undef,
  Enum['running', 'stopped']     $service_ensure  = 'running',
  String                         $package_ensure  = 'present',
  # ...
) {
# ...
}

Bad: Too many level of indentation:

class profile::myclass (
      $var1    = 'default',
      $var2    = 'something else',
      $another = 'another default value',
) {

}
Bad: No indentation:
class profile::myclass (
$var1    = 'default',
$var2    = 'something else',
$another = 'another default value',
) {

}
Bad: Misaligned equals sign:
class profile::myclass (
  $var1 = 'default',
  $var2  = 'something else',
  $another = 'another default value',
) {

}

Class inheritance

In addition to scope and organization, there are some additional guidelines for handling classes in your module.

Don't use class inheritance; use data binding instead of params.pp pattern. Inheritance is used only for params.pp, which is not recommended in Puppet 4.

If you use inheritance for maintaining older modules, do not use it across module namespaces. To satisfy cross-module dependencies in a more portable way, include statements or relationship declarations. Only use class inheritance for myclass::params parameter defaults. Accomplish other use cases by adding parameters or conditional logic.

Good:

class ssh { ... }

class ssh::client inherits ssh { ... }

class ssh::server inherits ssh { ... }
Bad:
class ssh inherits server { ... }

class ssh::client inherits workstation { ... }

class wordpress inherits apache { ... }

Public modules

When declaring classes in publicly available modules, use include, contain, or require rather than class resource declaration. This avoids duplicate class declarations and vendor lock-in.

Type signatures

We recommend always using type signatures for class and defined type parameters. Keep the parameters and = signs aligned.

When dealing with very long type signatures, you can define type aliases and use short definitions. Good naming of aliases can also serve as documentation, making your code easier to read and understand. Or, if necessary, you can turn the 140 line character limit off. For more information on type signatures, see the Type data type.

Related information