Writing custom functions in the Puppet language

You can write simple custom functions in the Puppet language, to transform data and construct values. A function can optionally take one or more parameters as arguments. A function returns a calculated value from its final expression.

Note: While many functions can be written in the Puppet language, it doesn’t support all of the same features as pure Ruby. For information about writing Ruby functions, which can perform more complex work, see Writing functions in Ruby. For information about iterative functions, which can be invoked by, but not written exclusively with, Puppet code, see Writing iterative functions.

Syntax of functions

function <MODULE NAME>::<NAME>(<PARAMETER LIST>) >> <RETURN TYPE> {
  ... body of function ...
  final expression, which is the returned value of the function
}

The general form of a function written in Puppet language is:

  • The keyword function.

  • The namespace of the function. This must match the name of the module the function is contained in.

  • The namespace separator, a double colon ::

  • The name of the function.

  • An optional parameter list, which consists of:

    • An opening parenthesis (

    • A comma-separated list of parameters (for example, String $myparam = "default value"). Each parameter consists of:

      • An optional data type, which restricts the allowed values for the parameter (defaults to Any).

      • A variable name to represent the parameter, including the $ prefix.

      • An optional equals sign = and default value, which must match the data type, if one was specified.

    • An optional trailing comma after the last parameter.

    • A closing parenthesis )

  • An optional return type, which consists of:

    • Two greater-than signs >>

    • A data type that matches every value the function could return.

  • An opening curly brace {

  • A block of Puppet code, ending with an expression whose value is returned.

  • A closing curly brace }

For example:
function apache::bool2http(Variant[String, Boolean] $arg) >> String {
  case $arg {
    false, undef, /(?i:false)/ : { 'Off' }
    true, /(?i:true)/          : { 'On' }
    default               : { "$arg" }
  }
}

Order and optional parameters

Puppet passes arguments by parameter position. This means that the order of parameters is important. Parameter names do not affect the order in which they are passed.

If a parameter has a default value, then it’s optional to pass a value for it when you're calling the function. If the caller doesn’t pass in an argument for that parameter, the function uses the default value. However, because parameters are passed by position, when you write the function, you must list optional parameters after all required parameters. If you put a required parameter after an optional one, it causes an evaluation error.

Variables in default parameters values

If you reference a variable as a default value for a parameter, Puppet starts looking for that variable at top scope. For example, if you use $fqdn as a variable, but then call the function from a class that overrides $fqdn, the parameter’s default value is the value from top scope, not the value from the class. You can reference qualified variable names in a function default value, but compilation fails if that class isn't declared by the time the function is called.

The extra arguments parameter

You can specify that a function's last parameter is an extra arguments parameter. The extra arguments parameter collects an unlimited number of extra arguments into an array. This is useful when you don’t know in advance how many arguments the caller provides.

To specify that the parameter must collect extra arguments, start its name with an asterisk *, for example *$others. The asterisk is valid only for the last parameter.

Tip: An extra argument's parameter is always optional.
The value of an extra argument’s parameter is an array, containing every argument in excess of the earlier parameters. You can give it a default value, which has some automatic array wrapping for convenience:
  • If the provided default is a non-array value, the real default is a single-element array containing that value.

  • If the provided default is an array, the real default is that array.

If there are no extra arguments and there is no default value, it's an empty array.

An extra arguments parameter can also have a data type. Puppet uses this data type to validate the elements of the array. That is, if you specify a data type of String, the real data type of the extra arguments parameter is Array[String].

Return types

Between the parameter list and the function body, you can use >> and a data type to specify the type of the values the function returns. For example, this function only returns strings:
function apache::bool2http(Variant[String, Boolean] $arg) >> String {
  ...
}
The return type serves two purposes:
  • Documentation. Puppet Strings includes information about the return value of a function.

  • Insurance. If something goes wrong and your function returns the wrong type (such as undef when a string is expected), it fails early with an informative error instead of allowing compilation to continue with an incorrect value.

The function body

In the function body, put the code required to compute the return value you want, given the arguments passed in. Avoid declaring resources in the body of your function. If you want to create resources based on inputs, use defined types instead.

The final expression in the function body determines the value that the function returns when called. Most conditional expressions in the Puppet language have values that work in a similar way, so you can use an if statement or a case statement as the final expression to give different values based on different numbers or types of inputs. In the following example, the case statement serves as both the body of the function, and its final expression.

function apache::bool2http(Variant[String, Boolean] $arg) >> String {
  case $arg {
    false, undef, /(?i:false)/ : { 'Off' }
    true, /(?i:true)/          : { 'On' }
    default               : { "$arg" }
  }
}

Locations

Store the functions you write in a module's functions folder, which is a top-level directory (a sibling of manifests and lib). Define only one function per file, and name the file to match the name of the function being defined. Puppet is automatically aware of functions in a valid module and autoloads them by name.

Avoid storing functions in the main manifest. Functions in the main manifest override any function of the same name in all modules (except built-in functions).

Names

Give your function a name that clearly reveals what it does. For more information about names, including restrictions and reserved words, see Puppet naming conventions.

Related topics: Arrays, Classes, Data types, Conditional expressions, Defined types, Namespaces and autoloading, Variables.

Calling a function

A call to a custom function behaves the same as a call to any built-in Puppet function, and resolves to the function's returned value.

After a function is written and available in a module where the autoloader can find it, you can call that function, either from a Puppet manifest that lists the containing module as a dependency, or from your main manifest.

Any arguments you pass to the function are mapped to the parameters defined in the function’s definition. You must pass arguments for the mandatory parameters, but you can choose whether you want to pass in arguments for optional parameters.

Functions are autoloaded and are available to other modules unless those modules have specified dependencies. If a module has a list of dependencies in its metadata.json file, only custom functions from those specific dependencies are loaded.

Related topics: namespaces and autoloading, module metadata, main manifest directory

Complex example of a function

The following code example is a re-written version of a Ruby function from the postgresql module into Puppet code. This function translates the IPv4 and IPv6 Access Control Lists (ACLs) format into a resource suitable for create_resources. In this case, the filename would be acls_to_resource_hash.pp, and it would be saved in a folder named functions in the top-level directory of the postgresql module.
function postgresql::acls_to_resource_hash(Array $acls, String $id, Integer $offset) {

  $func_name = "postgresql::acls_to_resources_hash()"

  # The final hash is constructed as an array of individual hashes
  # (using the map function), the result of that
  # gets merged at the end (using reduce).
  #
  $resources = $acls.map |$index, $acl| {
    $parts = $acl.split('\s+')
    unless $parts =~ Array[Data, 4] {
      fail("${func_name}: acl line $index does not have enough parts")
    }

    # build each entry in the final hash
    $resource = { "postgresql class generated rule ${id} ${index}" =>
      # The first part is the same for all entries
      {
        'type'     => $parts[0],
        'database' => $parts[1],
        'user'     => $parts[2],
        'order'    => sprintf("'%03d'", $offset + $index)
      }
      # The rest depends on if first part is 'local',
      # the length of the parts, and the value in $parts[4].
      # Using a deep matching case expression is a good way
      # to untangle if-then-else spaghetti.
      #
      # The conditional part is merged with the common part
      # using '+' and the case expression results in a hash
      #
      +
      case [$parts[0], $parts, $parts[4]] {

        ['local', Array[Data, 5], default] : {
          { 'auth_method' => $parts[3],
            'auth_option' => $parts[4, -1].join(" ")
          }
        }

        ['local', default, default] : {
          { 'auth_method' => $parts[3] }
        }

        [default, Array[Data, 7], /^\d/] : {
          { 'address'     => "${parts[3]} ${parts[4]}",
            'auth_method' => $parts[5],
            'auth_option' => $parts[6, -1].join(" ")
          }
        }

        [default, default, /^\d/] : {
          { 'address'     => "${parts[3]} ${parts[4]}",
            'auth_method' => $parts[5]
          }
        }

        [default, Array[Data, 6], default] : {
          { 'address'     => $parts[3],
            'auth_method' => $parts[4],
            'auth_option' => $parts[5, -1].join(" ")
          }
        }

        [default, default, default] : {
          { 'address'     => $parts[3],
            'auth_method' => $parts[4]
          }
        }
      }
    }
    $resource
  }
  # Merge the individual resource hashes into one
  $resources.reduce({}) |$result, $resource| { $result + $resource }
}