Forge Home

collections

Provide a means to work with collections of data - creating resources from lists of data, or creating files from multiple fragments

248 downloads

248 latest version

4.7 quality score

We run a couple of automated
scans to help you access a
module's quality. Each module is
given a score based on how well
the author has formatted their
code and documentation and
modules are also checked for
malware using VirusTotal.

Please note, the information below
is for guidance only and neither of
these methods should be considered
an endorsement by Puppet.

Version information

  • 1.0.0 (latest)
released Mar 26th 2024
This version is compatible with:
  • Puppet Enterprise 2023.8.x, 2023.7.x, 2023.6.x, 2023.5.x, 2023.4.x, 2023.3.x, 2023.2.x, 2023.1.x, 2023.0.x, 2021.7.x, 2021.6.x, 2021.5.x, 2021.4.x, 2021.3.x, 2021.2.x, 2021.1.x, 2021.0.x
  • Puppet >= 7.0.0 < 9.0.0
  • , , , , , , , ,

Start using this module

  • r10k or Code Manager
  • Bolt
  • Manual installation
  • Direct download

Add this module to your Puppetfile:

mod 'puppet-collections', '1.0.0'
Learn more about managing modules with a Puppetfile

Add this module to your Bolt project:

bolt module add puppet-collections
Learn more about using this module with an existing project

Manually install this module globally with Puppet module tool:

puppet module install puppet-collections --version 1.0.0

Direct download is not typically how you would use a Puppet module to manage your infrastructure, but you may want to download the module in order to inspect the code.

Download

Documentation

puppet/collections — version 1.0.0 Mar 26th 2024

collections

Table of Contents

  1. Description
  2. Setup - The basics of getting started with collections
  3. Usage - Configuration options and additional functionality
  4. A simple example
  5. Creating a file
  6. Concat
  7. YAML
  8. JSON
  9. Testing
  10. How collections works
  11. Limitations - OS compatibility, etc.
  12. Development - Guide for contributing to the module

Description

Collections is a generic iterator written in (nearly) pure puppet. You can declare an iterator stack, add items to the stack as you process, then define a set of actions that will be called with those items. Some examples from my own codebase using it are:

  • Build a file with multiple modules contributing fragments, and be able to write a simple test for the contents of it. This functionality owes a debt to richardc's datacat module, but is hopefully much easier to work with.

  • Declare a collection for 'all users' and 'admin users'. You're now able to have a module declare 'Okay, create a resource of this type for every user'

  • In combination with a fact listing 'extra' IPs per instance, remove any IPs that aren't defined in the current puppet run cleanly.

Setup

Collections uses the Ruby deep_merge gem which can recursively merge both hashes and arrays.

Usage

A simple example

Here's an example of how you could use Collections to allow modules to easily define additional functionality for admin users:

First, in a module handling your users, create some collections:

  # During initialisation
  collections::create { 'all-users': }
  collections::create { 'admin-users': }

Then in a class that handles creation of users:

  $configured_users.each |$name, $user| {
    # ... Configure the user

    collections::append { "user ${name}":
      target => 'all-users',
      data   => {
        $name => $user,
      },
    }

    if $is_admin_user {
      collections::append { "${name} is an admin":
        target => 'admin-users',
        data   => {
          $name => $user,
        },
      }
    }
  }

You can now add dependent resources to the collections. For example, if you install a database server, you might want every admin user to have access to the database.

In your database configuration module, you can now define a type to give admin access:

define database::admin_user (
  String[1] $target, # The name of the collection (in case of reuse)
  Any $item, # The item passed in. In this example a hash: { $name => $user }
) {
  # ... configure this user as an admin
}

And add the following to the class that installs the database server:

  collections::register_action { 'Admin users get database access':
    target   => 'admin-users',
    resource => 'database::admin_user',
  }

Creating a file

A common use case is to allow multiple actors to contribute to a file. A small suite of convenience functions have been added to Collections for this use case. These are built on top of collections::create, collections::append, and so on.

To create a file, first declare it with collections::file. The following example uses the built-in YAML template, which will take all the items in the collection, merge them in order and write the result to the file as YAML:

  collections::file { '/path/to/file.yaml':
    collector => 'app-config-file',
    template  => 'collections/yaml.epp',
    file      => {
      owner => 'root',
      group => 'root',
      mode  => '0640',
    },
    data      => {
      config => {
        user => 'nobody',
      },
    },
  }

(data passed above is optional, a first item for the collection).

You can then add data to the file using collections::append:

  collections::append { 'App: Set chroot options':
    target => 'app-config-file',
    data   => {
      config => {
        use_chroot => true,
        chroot_dir => '/var/spool/app/chroot',
      },
    },
  }

Concat

Template name: collections/concat.epp (or .erb)

This allows for joining individual content blocks together, with some inbuilt ordering. It expects the data key to be a hash containing two items:

  • order - An Integer used as a primary sort key for the items. Default: 1000
  • content - A string to write to the file.

Sorting is by the order key first, then by definition order. You can omit the order key entirely if you wish to use only Puppet's resource ordering.

Example:

collections::append { 'Append to a concat file':
  target => 'a-collection-using-the-concat-template',
  data   => {
    order   => 100,
    content => 'Some string content for the file',
  },
}

YAML

Template name: collections/yaml.epp (or .erb)

Takes any sequence of data items and sequentially merges them together with deep_merge, then converts the result to YAML and writes it to a file.

JSON

Template name: collections/json.epp (or .erb)

Takes any sequence of data items and sequentially merges them together with deep_merge, then converts the result to JSON and writes it to a file.

Testing

One of the core design goals for this module was to be able to have distributed actions without impacting the ability to test. Because the core 'engine' in the module is standard Puppet resource execution and ordering, there are no special tricks or techniques. If you use collection::file to create a file, you can then test for a file resource with the expected content field, just as if you created it directly.

How collections works

The core mechanic that allows collections to work is declaring resources with the correct structure and initial data, then appending to them using resource references. This is quite tricky to get right, and while the actual code in collections is quite small, the structure is vital.

Order of processing

Within a collection, the order of processing is:

  • Gather items
  • Run all executors (resources that are instantiated with the complete list of items as a single parameter)
  • Run all actions (resources that are instantiated once for each item)
  • Complete

If you ever need to take particular actions at specific times within this processing, you can add constraints on Collections::Checkpoint resources:

  • `Collections::Checkpoint["collection::${name}::before-executors"]
  • `Collections::Checkpoint["collection::${name}::after-executors"]
  • `Collections::Checkpoint["collection::${name}::before-actions"]
  • `Collections::Checkpoint["collection::${name}::after-actions"]
  • `Collections::Checkpoint["collection::${name}::completed"]

A simplified explanation

To simplify the explanation, we will only cover how items are added to the collection and processed by it.

To create a collection you define a collection::create resource:

collections::create { 'example': }

This results in the following chain of resources and constraints:


# Created by the user
collections::create { 'example': }

# Created by collections::create
collections::iterator { 'example':
  items => []
}
Collections::Append <|target == 'example'|> -> Collections::Commit['example']

# Created by collections::iterator
collections::iterator { 'example':
  items => []
}

When collections::append is used to add an item, it runs the following:

Collections::Commit <|title=='example'|> {
  items +> [ $new_item ]
}

This is a deeper structure than may be expected, but it is required to function - in particular, the resource constraint that declares all appends must complete before the commit only works when it is outside the commit resource.

Limitations

Development