Configuring Travis CI on your Puppet Controlrepo

Continuous Integration is an important technique used in modern software development. For every change, a CI system runs a suite of tests to ensure the whole system – not just the changed portion – still “works”, or more specifically, still passes the defined tests. We are going to look at Travis CI, a cloud-based Continuous Integration service that you can connect to your GitHub repositories. This is valuable because it’s free (for “best effort” access; there are paid plans as well.) and helps you guarantee that code you check in will work with Puppet. This isn’t a substitute or replacement for rspec-puppet, this is another layer of testing that improves the quality of our work.

There are plenty of other CI systems out there – Jenkins and Bamboo are popular – but that would involve setting up the CI system as well as configuring our repo to use CI. Please feel free to investigate these CI systems, but they’ll remain beyond the scope of this blog for the time being. Please share any guides you may have in the comments, though!

Travis CI works by spinning up a VM or docker instance, cloning our git repo (using tokenized authentication), and running the command(s) we provide. Each entry in our test matrix will run on a separate node, so we can test different OSes or Ruby or Puppet versions to our heart’s content. The results of the matrix are visible through GitHub and show us red if any test failed and green if all tests passed. We’ll look at some details of how this works as we set up Travis CI.

From a workflow perspective, you’ll continue to create branches on your controlrepo and submit PRs. The only additional step is that when a PR is ready for review, you’ll want to wait for Travis CI to complete first. If it’s red, investigate the failure and remediate it. Don’t review code until everything is green because it won’t work anyway. This will mostly be a time saver, unless you’re watching your CI run which of course makes it slower!

Register for Travis CI

Travis CI works very simply through the use of a .travis.yml file in the root of your repository plus some registration options at Travis CI. Register for public repos at https://travis-ci.org/getting_started and private repos at https://magnum.travis-ci.com/getting_started. Once you’re registered, click on your avatar in the top right and choose Account Settings (URL: https://travis-ci.org/profile/<your github ID>). Find your controlrepo and move the slider to the green check box. Click on the gear and decide whether you want every push to run a CI build or just Pull Requests. I suggest using only PRs for now, especially if you may need a pay plan – the first 100 builds are free, best to limit those during setup.

travis ci fig 0

Add Travis CI configuration to your repository

Now we need to add some Travis CI configuration to our repository. We need to update or create a few files: .travis.yml, Gemfile, dist/profile/Rakefile, .gitignore. If we have a dist/profile/Gemfile, we can git mv that to the root of our repository, as we won’t need it in dist/profile anymore. We’ll work with the Travis CI config last, starting with our more familiar files first.

I originally used my own or severely modified files, such as a Rakefil without the rubocop portion. In hindsight, I found the files from puppet-module-skeleton (.gitignore, GemfileRakefile.travis.yml) to be more useful in the long term. Copy the files into the appropriate location and modify as needed.

The .gitignore file needs these three entries (below), in addition to any existing entries. The new entries are based around the use of bundler by ourselves and Travis CI to install ruby gem dependencies. The lock file tracks what dependencies have been made available, vendor is the directory users will store those dependencies in, and .bundle is the directory bundler uses for its own management. If these were present in the repo, bundler would be confused:

Gemfile.lock
vendor
.bundle

Next, let’s look at the Gemfile. This is where we describe the ruby gem dependencies for the project. You can learn more about bundler, puppet and rspec dependencies in this CodingBee tutorial. This sample Gemfile is a great base for Puppet code but can always be enhanced. This ensures that the gems we need for rspec-puppet and puppet-lint are present. Remember, this Gemfile needs to be present at the root of your repository, not in dist/profile.

source 'https://rubygems.org'

group :development, :test do
  gem 'json', :require => false
  gem 'metadata-json-lint', :require => false
  gem 'puppetlabs_spec_helper', :require => false
  gem 'puppet-lint', :require => false
  gem 'rake', :require => false
  gem 'rspec-puppet', :require => false
end

if puppetversion = ENV['PUPPET_GEM_VERSION']
  gem 'puppet', puppetversion, :require => false
else
  gem 'puppet', :require => false
end

# vim:ft=ruby

Next, we need a Rakefile. This is similar to a Makefile, but for Ruby (and rake instead of make), and something we’ve been using for a while without actually calling it out. When we run rake spec, we’re using the spec target in the Rakefile. You can use the Rakefile that you get with puppet module generate, or you can use the one from Gareth’s puppet-module-skeleton, or anything in between. As a side effect, this gets us up to the latest version of the best practices rspec-puppet setup!

require 'rubygems'
require 'bundler/setup'

require 'puppetlabs_spec_helper/rake_tasks'
require 'puppet/version'
require 'puppet/vendor/semantic/lib/semantic' unless Puppet.version.to_f < 3.6
require 'puppet-lint/tasks/puppet-lint'
require 'puppet-syntax/tasks/puppet-syntax'
require 'metadata-json-lint/rake_task'

# These gems aren't always present, for instance
# on Travis with --without development
begin
  require 'puppet_blacksmith/rake_tasks'
rescue LoadError # rubocop:disable Lint/HandleExceptions
end

exclude_paths = [
  "bundle/**/*",
  "pkg/**/*",
  "vendor/**/*",
  "spec/**/*.pp",
]

Rake::Task[:lint].clear

PuppetLint.configuration.relative = true
PuppetLint.configuration.disable_80chars
PuppetLint.configuration.disable_arrow_alignment
PuppetLint.configuration.disable_class_inherits_from_params_class
PuppetLint.configuration.disable_class_parameter_defaults
PuppetLint.configuration.fail_on_warnings = true

PuppetLint::RakeTask.new :lint do |config|
  config.ignore_paths = exclude_paths
end

PuppetSyntax.exclude_paths = exclude_paths

desc "Run acceptance tests"
RSpec::Core::RakeTask.new(:acceptance) do |t|
  t.pattern = 'spec/acceptance'
end

desc "Populate CONTRIBUTORS file"
task :contributors do
  system("git log --format='%aN' | sort -u > CONTRIBUTORS")
end

desc "Run syntax, lint, and spec tests."
task :test => [
  :metadata_lint,
  :syntax,
  :lint,
  :spec,
]

With the Gemfile and Rakefile in place, we’re going to need to use bundler ourselves rather than simply calling rspec-puppet, so this is a good time to test our setup. You may need to set the environment variable PUPPET_GEM_VERSION if your code and modules will not work with the latest puppet version. I haven’t ensured that all the code is future parser compatible, so I am falling back to Puppet v3.7.3:

[rnelson0@build02 controlrepo:travisci±]$ PUPPET_GEM_VERSION=3.7.3 bundle install --path vendor --without system_tests
Resolving dependencies...
Using rake 10.4.2
Using CFPropertyList 2.2.8
Using diff-lcs 1.2.5
Using facter 2.4.4
Using json_pure 1.8.2
Using hiera 1.3.4
Using json 1.8.3
Using metaclass 0.0.4
Using spdx-licenses 1.0.0
Using metadata-json-lint 0.0.11
Using mocha 1.1.0
Using puppet 3.7.3
Using puppet-lint 1.1.0
Using puppet-syntax 2.0.0
Using rspec-support 3.3.0
Using rspec-core 3.3.2
Using rspec-expectations 3.3.1
Using rspec-mocks 3.3.2
Using rspec 3.3.0
Using rspec-puppet 2.2.0
Using puppetlabs_spec_helper 0.10.3
Using bundler 1.7.6
Your bundle is complete!
Gems in the group system_tests were not installed.
It was installed into ./vendor
[rnelson0@build02 controlrepo:travisci±]$ cd dist/profile
[rnelson0@build02 profile:travisci±]$ bundle exec rake test
ruby -c lib/facter/puppet_role.rb
Syntax OK
---> syntax:manifests
---> syntax:templates
---> syntax:hiera:yaml
metadata-json-lint metadata.json
HEAD is now at 710b3d5 Merge pull request #268 from mhaskel/1.2.0-prep
/usr/local/rvm/rubies/ruby-1.9.3-p551/bin/ruby -I/home/rnelson0/git/puppetinabox/controlrepo/vendor/ruby/1.9.1/gems/rspec-core-3.3.2/lib:/home/rnelson0/git/puppetinabox/controlrepo/vendor/ruby/1.9.1/gems/rspec-support-3.3.0/lib /home/rnelson0/git/puppetinabox/controlrepo/vendor/ruby/1.9.1/gems/rspec-core-3.3.2/exe/rspec --pattern spec/\{classes,defines,unit,functions,hosts,integration\}/\*\*/\*_spec.rb --color

profile::apache
  with defaults for all parameters
    should contain Class[profile::apache]
    should contain Package[httpd]
    should contain User[apache]

profile::base
  with defaults for all parameters
    should contain Class[profile::base]
    should contain Class[profile::linuxfw]
    should contain Class[ssh::server]
    should contain Class[ssh::client]
    should contain Class[ntp]
    should contain Ssh_authorized_key[testkey]
    should contain Yumrepo[lab]
    should contain Exec[shosts.equiv]
    should contain Class[sudo]
    should contain Sudo::Conf[wheel]
    should contain Local_user[testuser]

profile::build
  with defaults for all parameters
    should contain Class[profile::build]
    should contain Class[rvm]
    should contain Rvm_system_ruby[ruby-1.9.3-p511]
    should contain Rvm_gem[test] with require => "Rvm_system_ruby[ruby-1.9.3-p511]"
  using puppet enterprise
    should not contain Class[rvm]

profile::dhcp
  with defaults for all parameters
    should contain Class[profile::dhcp]
    should contain Package[dhcp]
    should contain Dhcp::Server::Subnet[10.0.0.0]
    should contain Dhcp::Server::Host[sample]

profile::dns
  with defaults for all parameters
    should contain Class[profile::dns]
    should contain Package[bind]
    should contain Bind::Server::Conf[/etc/named.conf]
    should contain Bind::Server::File[named.test]

profile::hiera
  with defaults for all parameters
    should contain Class[profile::hiera]
    should contain Package[hiera]
    should contain Package[hiera-puppet]

profile::linuxfw::post
  with defaults for all parameters
    should contain Class[profile::linuxfw::post]
    should contain Firewall[998 input reject]
    should contain Firewall[999 forward reject]

profile::linuxfw::pre
  with defaults for all parameters
    should contain Class[profile::linuxfw::pre]
    should contain Firewall[000 accept related established rules]
    should contain Firewall[001 accept all icmp]
    should contain Firewall[002 accept all to lo interface]
    should contain Firewall[003 accept ssh connections]

profile::linuxfw
  with defaults for all parameters
    should contain Class[profile::linuxfw]
    should contain Class[profile::linuxfw::pre]
    should contain Class[profile::linuxfw::post]
    should contain Class[firewall]

profile::mysql::client
  with defaults for all parameters
    should contain Class[profile::mysql::client]
    should contain Class[mysql::client]

profile::mysql::server
  with defaults for all parameters
    should contain Class[profile::mysql::server]
    should contain Package[policycoreutils-python]
    should contain Exec[set-mysql-selinux-context]
    should contain Lvm::Volume[lv_mysql] with pv => "/dev/sda3", fstype => "ext4" and size => "40G"
    should contain File[/data] with ensure => "directory"
    should contain Mount[/data] with ensure => "mounted", name => "/data", device => "/dev/mapper/vg_mysql-lv_mysql", fstype => "ext4" and atboot => "true"
    should contain File[/data/mysql] with ensure => "directory"
    should contain Exec[enforce-mysql-selinux-context]
    should contain Class[mysql::server]
    should contain Class[mysql::server::backup]
    should contain Firewall[100 MySQL inbound] with dport => "3306", proto => "tcp" and action => "accept"
    should contain Mysql_user[someuser@localhost]
    should contain Mysql_grant[someuser@localhost/somedb.*]
    should contain Mysql_database[somedb] with charset => "utf8"

profile::phpmyadmin
  with dbpass and cname
    should contain Class[profile::phpmyadmin]
    should contain Package[phpMyAdmin]
    should contain Selboolean[httpd_can_network_connect_db]
    should contain Selboolean[httpd_can_network_connect]
    should contain Certs::Vhost[phpmyadmin.example.com]
    should contain Apache::Vhost[phpmyadmin.example.com]
    should contain Class[apache::mod::php]
    should contain File[/etc/httpd/conf.d/phpMyAdmin.conf] with ensure => "absent"
    should contain File[/etc/phpMyAdmin] with ensure => "directory" and mode => "0755"
    should contain File[config.inc.php] with mode => "0644"

profile::puppetdb
  with defaults for all parameters
    should contain Class[profile::puppetdb]
    should contain Class[puppetdb]
    should contain Class[puppetdb::master::config]

profile::rcfiles::bash
  with defaults for all parameters
    should contain Class[profile::rcfiles::bash]
    should contain File[/etc/bashrc.puppet]
    should contain File_line[bashrc_skel_puppet_source]

profile::rcfiles::vim
  with defaults for all parameters
    should contain Class[profile::rcfiles::vim]
    should contain Package[vim-enhanced]
    should contain Vcsrepo[/usr/share/vim/puppet]
    should contain File_line[vimrc_runtimepath]
    should contain File_line[vimrc_indent]
    should contain File_line[vimrc_shiftwidth]

profile::tftp
  with defaults for all parameters
    should contain Class[profile::tftp]
    should contain Package[tftpd-hpa]
    should contain Tftp::File[pxelinux.0]
    should contain Firewall[100 tftp requests] with dport => "69", proto => "udp" and action => "accept"

profile::yumrepo
  with defaults for all parameters
    should contain Class[profile::yumrepo]
    should contain File[/repodir]
    should contain Createrepo[testrepo]
    should contain Apache::Vhost[yum.example.com]

Finished in 11.91 seconds (files took 0.55361 seconds to load)
88 examples, 0 failures

You should not see any real differences between your previous rspec results and your current results. If you do, check to make sure you used the right version of the puppet gem with bundle exec gem list. Puppet 4’s future parser can wreck havoc on older code and you’ll often see this with failures against the spec fixtures. Here’s what happens when rnelson0/local_user is parsed by Puppet 4:

  10) profile::base with defaults for all parameters should contain Sudo::Conf[wheel]
      Failure/Error: it { is_expected.to contain_sudo__conf('wheel') }
      Puppet::PreformattedError:
        Evaluation Error: Error while evaluating a Function Call, 0 is not a string.  It looks to be a Fixnum at /home/rnelson0/git/
puppetinabox/controlrepo/dist/profile/spec/fixtures/modules/local_user/manifests/init.pp:50:5 on node build02.nelson.va

If it’s not your module, you’re SOL. If it is your module, we’ll look at adding Travis CI to our modules soon enough. For today, just re-run bundle install with a prepend of the right puppet version.

Of course, if there are some issues in your controlrepo code itself, not the fixtures, and you can fix it easily, go ahead and get it done. Continue when the rake tests all complete without errors.

Finally, let’s create a .travis.yml with the tests we want to run and our support matrix:

---
sudo: false
language: ruby
bundler_args: --without system_tests
script: "cd dist/profile && bundle exec rake test"
matrix:
  fast_finish: true
  include:
  - rvm: 1.9.3
    env: PUPPET_GEM_VERSION="~> 3.0"
  - rvm: 2.1.6
    env: PUPPET_GEM_VERSION="~> 3.0"
  #- rvm: 2.1.6
  #  env: PUPPET_GEM_VERSION="~> 3.0" FUTURE_PARSER="yes"
  #- rvm: 2.1.6
  #  env: PUPPET_GEM_VERSION="~> 4.0" STRICT_VARIABLES="yes"
notifications:
  email: false

The script statement is the commands we ran for testing. You can update this as necessary. Remember that Travis CI will start in the root of your repo. If you have other modules you want to test, chain on some cd commands for those directories and tests.

The matrix setting is where we pair Ruby versions with Puppet versions. Use whatever pairings are appropriate for you. I’ve commented out the future parser versions since my code definitely does not work with Puppet 4 yet, but I certainly intend to turn it on sometime. I also don’t care about Ruby 1.8.7 or Puppet older than 3.x. This part is up to you, do whatever makes sense. There’s also a good config file in puppet-module-skeleton, unsurprisingly.

The rest of the file is defaults I’ve seen used elsewhere. Standing on the shoulders of giants and all that. You can find more details on the configuration file at Travis CI’s documentation center.

Test your changes

Next, commit your changes, push them upstream, and create a PR. Within a few seconds, you’ll see the Travis CI integration kick off. If everything went well, you’ll see green as soon as all your builds complete!

travis ci fig 1

The PR I used to create this article is PR50. If you click details next to the Travis CI build, you can dive deeper. In that build, you can then dive into a matrix entry. That’s as deep as you go, this isn’t Inception. If a test fails, drill down into the specific build and check the results. You’ll find the error that resulted from the script command most of the time, but sometimes you’ll find that Travis CI had an issue. If that happens, you can go back to the build view and click the Restart Build button, a clockwise arrow icon in the upper-right area of the screen, to try again.

Once your PR status shows green for all the checks, it’s time to do your normal code review and then merge the pull request. Remember, don’t merge anything that’s red!

Enabling Travis CI on a new module

We got the hard one out of the way, so this is really simple: Install puppet-module-skeleton, generate your new module with puppet module generate, and your new module is ready out of the box to use with Travis CI! Just go into settings on Travis CI and enable it for the repo. Voila!

Summary

By enabling Travis CI, we’ve added some simple but valuable continuous integration checks to our puppet controlrepo. By setting up our test matrix, we can easily test our code in a number of environments and get rapid feedback before we even begin the code review. Hopefully this helps you with your puppet coding. We’ll look at adding CI to our modules soon!

5 thoughts on “Configuring Travis CI on your Puppet Controlrepo

  1. Pingback: Configuring Travis CI on a Puppet Module Repo | rnelson0
  2. Pingback: Modern rspec-puppet practices | rnelson0
  3. Pingback: #Puppetinabox moving to v4 in 2016 | rnelson0
  4. Pingback: Puppet 4 Lessons Learned | rnelson0
  5. Pingback: Getting started with Jenkins and Puppet | rnelson0

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s