collections
Version information
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
Add this module to your Puppetfile:
mod 'puppet-collections', '1.0.0'
Learn more about managing modules with a PuppetfileDocumentation
collections
Table of Contents
- Description
- Setup - The basics of getting started with collections
- Usage - Configuration options and additional functionality
- A simple example
- Creating a file
- Concat
- YAML
- JSON
- Testing
- How collections works
- Limitations - OS compatibility, etc.
- 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: 1000content
- 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
Reference
Table of Contents
Defined types
Public Defined types
collections::append
: Append an item of any type to a collection.collections::create
: Create a new collectioncollections::debug_executor
: Collections debugging - print the set of items passed incollections::file
: Create a file which allows many resources to contribute content.collections::register_action
: Register a defined type to be run for each item in a collection.collections::register_executor
: Register a defined type to be run once, for all items in a collection.collections::tap
: Print the items received for debugging
Private Defined types
collections::checkpoint
: An empty resource recording stages within a collectioncollections::commit
: The resource that calls actions and executors upon a collectioncollections::file::writer
: Write collected data to a filecollections::iterator
: Wrapper resource to allow predictable resource ordering
Functions
collections::deep_merge
: Wrapper for the Ruby deep_merge gem
Defined types
collections::append
Append an item of any type to a collection.
Examples
collections::append { 'User foo is an admin':
target => 'users::admin-users',
data => {
username => 'foo',
uid => 1001,
},
}
Parameters
The following parameters are available in the collections::append
defined type:
target
Data type: String[1]
The target collection, created by collection::create
data
Data type: Any
The item to be added to the target collection
collections::create
Create a collection and the unerlying collection::iterator
resource which powers it.
Examples
collections::create { 'collection-name':
}
collections::create { 'Operations for all users':
target => 'users::all',
defaults => {
parent => '/home'
}
}
Parameters
The following parameters are available in the collections::create
defined type:
target
Data type: String[1]
The name of this collection. You must pass this target name to all resources that act upon or work with this collection.
Default value: $title
defaults
Data type: Hash[String, Any]
Parameters that will be used as defaults for any resources generated by this collection. When a resource is created, these will be passed in as default parameters.
Default value: {}
initial_items
Data type: Array[Any]
A set of items to begine the collection with. This should be an array of data that could
be added one at a time with collection::append
.
Default value: []
collections::debug_executor
This provides an example of an executor and possibly a useful debugging tool.
In normal use, you would never create a resource of this type manually, it
would instead be created by collections::register_executor
.
- See also
- collections::register_executor
Examples
collections::register_executor { 'Debug: Print all items added to the collection':
target => 'an-exiting-collection',
resource => 'collections::debug_executor',
}
Parameters
The following parameters are available in the collections::debug_executor
defined type:
target
Data type: String[1]
Passed in by collections::commit
when creating this resource. It indicates the
name of the collection that it was spawned from, to allow any
items
Data type: Array[Any]
Passed in by collections::commit
when creating this resource. It contains an
array of all the items added to this collection using collections::append
collections::file
This is a convenience function that uses a collection to handle the problem
of allowing portions of a file to be defined in many places. The file will
be generate from a template, with data collated from any number of
collection::file::fragment
resources.
EPP templates will receive two parameters:
Any $data
- The contents of alldata
parameters from the collection merged- 'Array $items
- An array containing each
data` parameter from the collection, in order.
ERB templates can access these two variables as @data
and @items
.
As a convenience, a small set of templates are predefined within the collections module to suit a few use cases:
collections/concat.epp
(Also .erb)
This template allows constructing a file from multiple content blocks, with
ordering based upon an optional order
key. Fragments should contain a
content
key. Order will default to 1000 if it is not supplied.
collections/yaml.epp
(Also .erb) This template will output the collected data as a YAML document
collections/json.epp
(Also .erb) This template will output the collected data as a JSON document
Examples
## Create a file from multiple string content blocks, with ordering
collections::file { '/etc/motd':
collector => 'motd',
template => 'collections/concat.epp',
data => {
order => 1,
content => file('my-module/motd-header'),
},
file => {
owner => 'root',
group => 'root',
mode => '0444',
},
}
collections::file::fragment { 'Add an unauthorised access warning':
target => 'motd',
data => {
order => 2,
content => file('my-module/motd-unathorised-warning'),
},
}
## Create a yaml file using multiple merged values
collections::file { '/etc/service/config.yaml':
collector => 'service-config',
template => 'collections/yaml.epp',
file => {
owner => 'root',
group => 'service',
mode => '0640',
},
}
collections::file::fragment { 'Set the user and group':
target => 'service-config',
data => {
user => 'serviceuser',
group => 'serviceuser',
},
}
Parameters
The following parameters are available in the collections::file
defined type:
collector
Data type: String[1]
The name of this collection
template
Data type: Optional[String[3]]
The name of a template to use. This should be a String in the form of modulename/filename
.
Either this or template_body
(the actual template code) must be passed.
Default value: undef
template_body
Data type: Optional[String]
The template code to use (content of a template file, rather than a path).
Either this or template
(a path to a template in a module) must be passed.
Default value: undef
template_type
Data type: Enum['epp','erb','auto']
The template language used. The default is auto
, which will default to 'erb'
unless a filename template is passed that ends in .epp
.
Default value: 'auto'
data
Data type: Any
Optional. Initial data for the collection.
If provided, a collection::append
resource named <collectionname>::auto-initial-data
will
be created.
Default value: undef
file
Data type: Hash[String, Any]
Parameters to pass to the file
resource which will be created by this collection.
Most values will be passed through, but source
or content
will be removed
(As allowing them would be ambiguous) and ensure
will only be allowed if set to
absent
, file
or present
.
Default value: {}
merge_options
Data type: Hash[String,Variant[Boolean,String]]
Default: { keep_array_duplicates => true } (for compatability with datacat)
Options to pass to the Ruby deep_merge
gem. See the options reference for details.
Default value: {}
reverse_merge_order
Data type: Boolean
Default: false Give merge priority to items later in the set
Default value: false
collections::register_action
In the collections module, an action
is a defined_type that will be called
for each item added to the collection, passed in as the item
parameter
Examples
collections::create { 'admin_users': }
collections::register_action { 'Admin users get database access':
target => admin_users,
resource => 'database::add_admin_access',
parameters => {
require => Service['database']
}
}
collections::append { 'Alice is an admin user':
target => admin_users,
data => {
user => 'Alice',
uid => 1001,
home => '/home/alice'
}
}
collections::append { 'Bob is an admin user':
target => admin_users,
data => {
user => 'Bob',
uid => 1002,
home => '/home/bob'
}
}
# Will result in:
database::add_admin_access { 'admin_users::1',
target => admin_users,
item => {
user => 'Alice',
uid => 1001,
home => '/home/alice'
},
require => Service['database']
}
database::add_admin_access { 'admin_users::2',
target => admin_users,
item => {
user => 'Bob',
uid => 1002,
home => '/home/bob'
},
require => Service['database']
}
Parameters
The following parameters are available in the collections::register_action
defined type:
target
Data type: String[1]
The name of the collection to configure
resource
Data type: String[1]
The name of a defined_type that will be created once per item
parameters
Data type: Hash[String,Any]
Optional parameters that will be passed to the action resource when created
Default value: {}
collections::register_executor
In the collections module, an executor
is a defined_type that will be called
once, with all items in the collection passed to it as the items
parameter
Examples
collections::create { 'admin_users': }
collections::register_executor { 'Admin users get database access':
target => admin_users,
resource => 'database::add_admin_access',
parameters => {
require => Service['database']
}
}
collections::append { 'Alice is an admin user':
target => admin_users,
data => {
user => 'Alice',
uid => 1001,
home => '/home/alice'
}
}
collections::append { 'Bob is an admin user':
target => admin_users,
data => {
user => 'Bob',
uid => 1002,
home => '/home/bob'
}
}
# Will result in:
database::add_admin_access { 'admin_users::executor',
target => admin_users,
items => [
{
user => 'Alice',
uid => 1001,
home => '/home/alice',
},
{
user => 'Bob',
uid => 1002,
home => '/home/bob',
},
},
require => Service['database']
}
Parameters
The following parameters are available in the collections::register_executor
defined type:
target
Data type: String[1]
The name of the collection to configure
resource
Data type: String[1]
The name of a defined_type
parameters
Data type: Hash[String,Any]
Optional parameters that will be passed to the action resource when created
Default value: {}
collections::tap
This is a debugging tool and example action. It creates a notify resource for the item passed in to it.
Examples
collections::create { 'foo': }
collections::append { 'foo A':
target => 'foo',
data => 'A'
}
collections::append { 'foo B':
target => 'foo',
data => 'B'
}
collections::register_action { 'Debug: Print the items'
# Will result in:
collections::tap { 'foo:1':
target => 'foo',
item => 'A'
}
collections::tap { 'foo:2':
target => 'foo',
item => 'B'
}
Parameters
The following parameters are available in the collections::tap
defined type:
target
Data type: String[1]
Passed in by collections::commit
when creating this resource. It indicates the
name of the collection that it was spawned from, to allow any
item
Data type: Any
Passed in by collections::commit
when creating this resource. It contains an
one item from the collection.
Functions
collections::deep_merge
Type: Ruby 4.x API
Returns a copy of dest with source merged into it
collections::deep_merge(Any $dest, Any $source, Optional[Hash[String, Variant[Boolean,String]]] $options)
Returns a copy of dest with source merged into it
Returns: Any
dest
Data type: Any
The destination object, which will be overridden by the contents of source
source
Data type: Any
The source object, which will override the contents of dest
options
Data type: Optional[Hash[String, Variant[Boolean,String]]]
Options for the deep_merge method.
Changelog
All notable changes to this project will be documented in this file. Each new release typically also includes the latest modulesync defaults. These should not affect the functionality of the module.
v1.0.0 (2024-03-26)
Implemented enhancements:
- Add EPP support and
template_type
param #10 (ccooke) - Add template_body param to collections::file, improve tests #4 (ccooke)
Fixed bugs:
Merged pull requests:
- README: fix typo #3 (kenyon)
- Delete .sync.yml in prep for the move to voxpupuli #2 (ccooke)
- V0.1.2 #1 (ccooke)
* This Changelog was automatically generated by github_changelog_generator
Dependencies
- puppetlabs-stdlib (>= 4.13.1)
MIT License Copyright (c) 2024 C. Cooke Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.