Puppet integration tests in (about) seven minutes

While puppet-lint and rspec-puppet (thanks to Tim Sharpe) will help ensure your Puppet code is both clean and produces what you’d expect in the compiled catalog there are times when you’ll want to go further than unit testing with rspec-puppet and do some basic integration tests to ensure the system ends up in the desired state. In this post, with the assumption that you have Docker installed, I’ll show a simple way to run basic integration tests against a Puppet module. Hopefully in about seven minutes.

In order to collect all the shinies we’ll use Docker, in conjunction with Hashicorps Packer, as our platform and we’ll write our unit tests in json and run them under a Go based test framework. If you want to read along without copy and pasting you can find the code in the Basic Puppet integration testing with Docker and Packer repo.

Packer is “a tool for creating machine and container images for multiple platforms from a single source configuration.” In our case we’ll use it to create a Docker image, install Puppet and our testing tools and then run the test case inside the container to confirm that everything did as we expected.

If you’re on Linux, and installing Packer by hand, it’s as simple as:

mkdir /tmp/puppet-and-packer
cd /tmp/puppet-and-packer/

wget -O packer.zip https://releases.hashicorp.com/packer/0.8.6/packer_0.8.6_linux_amd64.zip
unzip packer.zip

./packer --version
0.8.6

Software installed we’ll be good devopsy people and write our tests first. For our simple example all we’ll do with puppet is install the strace package so we’ll write a simple Goss test to verify it was installed in the container. You can read more about Testing with Goss on this very site.

cat goss.json

{
    "package": {
        "strace": {
            "installed": true
        }
    }
}

Now we have our test cases we can write our tiny puppet module:

mkdir -p modules/strace/manifests
cat modules/strace/manifests/init.pp

class strace {

  package { 'strace': ensure => 'present', }

}

and the site.pp that uses it

cat site.pp
include strace

Tiny examples created we’ll get to the meat of the configuration, our Packer file.

{
  "builders": [{
    "type": "docker",
    "image": "ubuntu",
    "commit": "true"
 }],
 "provisioners": [{
   "type": "shell",
   "inline": [
     "apt-get update",
     "apt-get install puppet curl -y",
     "curl -L https://github.com/aelsabbahy/goss/releases/download/v0.0.22/goss-linux-amd64 > /usr/local/bin/goss && chmod +x /usr/local/bin/goss"
     ]
  }, {
    "type": "file",
    "source": "goss.json",
    "destination": "/tmp/goss.json"
  }, {
    "type": "puppet-masterless",
    "manifest_file": "site.pp",
    "module_paths": [ "modules" ]
  }, {
    "type": "shell",
    "inline": [
      "/usr/local/bin/goss -g /tmp/goss.json validate --format documentation"
    ]
  }]
}

As an aside we see why JSON is a bad format for application config. You can’t include comments in native JSON. In our example we’ll use two of Packers three main sections, builders and provisioners. Builders create and generate images from for various platforms, in our case Docker. Provisioners install and configure software inside our images. In the above code we’re using three different types.

  • inline shell to install our dependencies, such as Puppet and Goss
  • file to copy our tests in to the image
  • puppet-masterless to upload our code and run Puppet over it.

With all the parts in place we can now create a container and have it run our tests.

# and now we build the image and run tests inside the container
./packer validate strace-testing.json
./packer build strace-testing.json

==> docker: Creating a temporary directory for sharing data...
==> docker: Pulling Docker image: ubuntu
...
    # puppet output
    docker: Notice: Compiled catalog for 5a68ef0b80b1.udlabs.priv in environment production in 0.10 seconds
    docker: Info: Applying configuration version '1454970724'
    docker: Notice: /Stage[main]/Strace/Package[strace]/ensure: ensure changed 'purged' to 'present'
    docker: Info: Creating state file /var/lib/puppet/state/state.yaml
    docker: Notice: Finished catalog run in 8.59 seconds
...
    # goss output
==> docker: Provisioning with shell script: /tmp/packer-shell193374970
    docker: Package: strace: installed: matches expectation: [true]
    docker:
    docker:
    docker: Total Duration: 0.009s
    docker: Count: 1, Failed: 0
==> docker: Committing the container

I’ve heavily snipped Packers output and only shown the parts of relevance to this post. You can see the usual Puppet output, here showing the installation of the strace package

docker: Notice: /Stage[main]/Strace/Package[strace]/ensure: ensure changed 'purged' to 'present'

followed by the goss tests running and confirm all is as expected

docker: Package: strace: installed: matches expectation: [true]
docker: Count: 1, Failed: 0

It’s trivially simple to replace Goss in these examples with InSpec, ServerSpec or TestInfra if you’re more comfortable with those tools. I chose Goss for this post as it’s very simple to install and has no dependency chain. Something that’s very relevant when you’ve only given yourself seven minutes to show people a new, and hopefully useful, technique.

As a closing note, you’ll want to occasionally clean up the old Docker images this testing creates.