Serverspec comes ready to go with many useful resource types. Sometimes, however, you may find yourself in a situation, where you need something special. Let’s see how it’s done.
Not enough
This happened to me while initially writing tests for Nginx hardening. For all configuration checks, these tests looked for values inside /etc/nginx/nginx.conf
:
describe file( nginx_conf ) do
its(:content) { should match(/^\s*server_tokens off;$/) }
end
...
With that out of the way, I started the implementation in Chef. Since hardening is designed as an overlay on top of existing cookbooks, we chose miketheman/nginx
as the base provider.
This led to a crucial discovery: Some values were configured inside nginx.conf
, while others were just not available (as seen in the template).
For Nginx, an easy and clean solution is to create a configuration file inside conf.d
and add all remaining values there. This leads to a split in configuration parameters between both configuration files, which must be reflected serverspec.
Some tests now didn’t target nginx.conf
anymore, but the hardening configuration conf.d/90.hardening.conf
:
describe file( hardening_conf ) do
its(:content) { should match(/^\s*more_clear_headers 'Server';$/) }
end
The obvious issue with this decision popped up during the implementation in Puppet. Here, a different provider for nginx was used: jfryman/nginx
. Its nginx.conf
had other values configured than the Chef equivalent (as seen in the template). Unfortunately, the serverspec tests were expecting parameters in nginx.conf
, that had now moved to conf.d
. There was no way to check both with the same set of tests anymore.
The ideal solution would have been to move all configuration values to the custom configuration file in conf.d
. However, this would have broken the overlay design. A user may still configure a parameter that ends up in both nginx.conf
and conf.d
.
Extending Serverspec
I needed a resource provider, that could check nginx configuration in multiple files at once:
conf_paths = [ nginx_conf, hardening_conf ]
describe multi_file( conf_paths )
its(:content) { should match(/^\s*more_clear_headers 'Server';$/) }
end
First, add a new file for the provider. Your directory structure may look like this:
.
└── serverspec
├── nginx_spec.rb
├── spec_helper.rb
└── type
└── multi_file.rb
The type provider extends Serverspec::Type
by adding a method multi_file
. This is the method you call when writing your tests. Additionally, we create a class that holds the object which is tested. This class includes methods that expose values for matchers (e.g. :content
) and must be extended from Serverspec::Type::Base
.
module Serverspec
module Type
class MultiFile < Base
def initialize(paths)
@paths = paths
end
def content
@paths.map{|x| @runner.get_file_content(x) }.join("\n")
end
end
def multi_file(paths)
MultiFile.new(paths)
end
end
end
include Serverspec::Type
Additionally, you can create endpoints for checks like:
describe multi_file( conf_paths )
it { should be_valid }
end
Simply add a method valid?
to your object:
class MultiFile < Base
...
def valid?
# check if the files are valid
end
end
A great source of inspiration is serverspec’s own collection of types.
Finally, don’t forget to include this file, preferably in your spec_helper.rb
:
...
# additional requirements
require 'type/multi_file'
Now, :content
provides the content of all files combined. Using the existing matchers, it’s easy to write the tests.
Final thoughts
Our serverspec tests in the hardening project are not limited to just verifying Chef and Puppet runs. We also want them to check if a system has a valid configuration (compliance checks). Without custom resource types, this is not possible, as you sometimes cannot expect a system to be configured in a certain way. This includes configurations for Apache, MySQL, or even Sysctl, which all feature a directory structure where additional configuration files are applied.
It’s important to remember what these resource types provide, and what they don’t cover. Stick to rspec and serverspec matchers for checking values themselves. Use resource types to get your values exposed to these matchers.
Happy testing!