© Stuart Preston 2016

Stuart Preston, Using Chef with Microsoft Azure, 10.1007/978-1-4842-1476-3_6

6. Integrating Quality Tooling into the Chef Development Life Cycle

Stuart Preston

(1)London, UK

The Chef ecosystem is fortunate to have many tools available to it to help in the quest for quality. This chapter introduces some of the tools that are publicly available to help. Eventually we will be using these tools as part of a Continuous Delivery pipeline, and it is important to get an understanding of how each tool can be executed individually.

In this chapter we’re going to take a tour around some of the most popular code analysis and testing tools that are distributed with the ChefDK. First we’ll have a look at cookbook linting using tools such as Rubocop and FoodCritic, before turning our attention to the different types of unit and integration testing tools out there.

As the purpose of this chapter is to introduce the tools, we’ll show you one or two working examples for each tool, focusing on how they are used in the context of Chef and Azure and then provide some further resources for you to go and explore at your own pace.

Cookbook Linting

Lintingis the process of checking source code for problems before execution. Two tools have emerged as the most popular in this area: Rubocop and FoodCritic. Rubocop is a static code analyzer that focuses on Ruby code style errors, FoodCritic focuses on common errors in Chef recipes, and both are powerful tools when used as part of a development workflow. Writing code in a consistent manner makes it easier for other team members to read your code and extend it confidently. It also reduces the amount of difference from commit to commit in your source code repository, making it easier to review.

It takes just a few seconds to run Rubocop and FoodCritic, so there’s no reason not to run both regularly against your cookbooks as part of your development process. These tools also return standard return codes meaning they are very suitable for use in automated pipelines. We’ll be covering this in chapter 8.

Note

Rubocop and FoodCritic are installed with the Chef Development Kit (ChefDK) by default. See chapter 1 if you have not installed ChefDK yet.

Using RuboCop

Most of the code that is authored when working with Chef is written as Ruby. Just about every file within a cookbook (with few exceptions) is a Ruby file. So we can use Rubocop to do the following:

  • Enforce style conventions and best practices

  • Evaluate the code in a cookbook against metrics such as “line length” and “function size”

  • Help every member of a team to author similarly structured code

  • Establish uniformity of source code

  • Set expectations for fellow (and future) project contributors

The Rubocop ruleset was borne out of the Ruby Style Guide (see https://github.com/bbatsov/ruby-style-guide ), which is a community-maintained set of guidelines that attempts to define a good set of conventions and principles.

Each rule in Rubocop may be enabled and disabled, either at a global level by configuring a .rubocop.yml file at the root of your project or by adding special comments to each Ruby file that exclude offenses from being counted. Let’s give it a go in a new cookbook.

Running Rubocop against an Entire Repository

In this example, we’re going to run Rubocop against a new repository to verify the output is as expected, with no errors.

First of all, let’s create a new repository using chef generate app:

PS C:UsersStuartPreston> chef generate app chefazure-ch06              

This should generate an output similar to the following, and I have shortened the (lengthy) output:

Compiling Cookbooks...
Recipe: code_generator::app
  * directory[C:/Users/StuartPreston/chefazure-ch06] action create
    - create new directory C:/Users/StuartPreston/chefazure-ch06
  * template[C:/Users/StuartPreston/chefazure-ch06/.kitchen.yml] action create
    - create new file C:/Users/StuartPreston/chefazure-ch06/.kitchen.yml


    [... files are created here ...]
  * execute[initialize-git] action run
    - execute git init .
  * cookbook_file[C:/Users/StuartPreston/chefazure-ch06/.gitignore] action create
    - create new file C:/Users/StuartPreston/chefazure-ch06/.gitignore
    - update content in file C:/Users/StuartPreston/chefazure-ch06/.gitignore from none to 33d469
    (diff output suppressed by config)

Now let’s move into our repo directory:

PS C:UsersStuartPreston> cd chefazure-ch06              

and execute Rubocop with no other command-line options:

PS C:UsersStuartPrestonchefazure-ch06> rubocop                
Inspecting 7 files
.......


7 files inspected, no offenses detected

As you can see from the output we have no offenses detected. That’s a good start. Let’s add some code to a recipe and see if Rubocop detects anything wrong.

Detecting and Correcting Rubocop Violations

Let’s open up our code editor (in my case Visual Studio Code):

PS C:UsersStuartPrestonchefazure-ch06> code .              

Now navigate to cookbookschefazure-ch06 ecipes and open up the default.rb file. Let’s add a simple log resource to our recipe as follows:

#
# Cookbook Name:: chefazure-ch06
# Recipe:: default
#
# Copyright (c) 2016 The Authors, All Rights Reserved.


log "Hello, World!"

The code should look similar to the code shown in Figure 6-1. Save the file.

A346707_1_En_6_Fig1_HTML.jpg
Figure 6-1. Adding a log resource to the default recipe in our cookbook

Now we’re going to run Rubocop again and see what results we get:

PS C:UsersStuartPrestonchefazure-ch06> rubocop                
Inspecting 7 files
..C....


Offenses:

cookbooks/chefazure-ch06/recipes/default.rb:7:5: C: Prefer single-quoted strings when you don't need string interpolation or special symbols.
log "Hello, World!"
    ^^^^^^^^^^^^^^^
cookbooks/chefazure-ch06/recipes/default.rb:7:20: C: Final newline missing.
log "Hello, World!"


7 files inspected, 2 offenses detected

As we can see from the output, two style offenses have been detected in the code from adding one seemingly innocuous line of code. Impressive! We can now correct them and try it again. We can make the changes manually ourselves or use the (experimental) autocorrect feature of Rubocop.

Note

Some text editors will automatically add a final newline to your file if you forget, so you may only see one offense here!

Rubocop Autocorrect

While there are some Rubocop violations that have multiple possible fixes, Rubocop does a good job of correcting code automatically by simply typing rubocop -a.

Autocorrect works best when there is only one solution to the problem; otherwise it will leave the offense alone. Let’s run rubocop -a against our code and see what happens:

PS C:UsersStuartPrestonchefazure-ch06> rubocop -a                
Inspecting 7 files
..C....


Offenses:

cookbooks/chefazure-ch06/recipes/default.rb:7:5: C: [Corrected] Prefer single-quoted strings when you don't need string interpolation or special symbols.
log "Hello, World!"
    ^^^^^^^^^^^^^^^
cookbooks/chefazure-ch06/recipes/default.rb:7:20: C: [Corrected] Final newline missing.
log "Hello, World!"


7 files inspected, 2 offenses detected, 2 offenses corrected

We can see in our example we now have corrected two offenses. This can also be seen in Figure 6-2. Our quoted string was changed to single quotes, and a new line was added at the end of the file for consistency.

A346707_1_En_6_Fig2_HTML.jpg
Figure 6-2. Our corrected recipe after being run through Rubocop in autocorrect mode

Suppressing Rubocop Offenses

While it would be great if we could use and enforce the default Rubocop rules for all our projects without modification, sometimes you will break so many rules that you want to defer fixing them until a later point, or perhaps you have a rule that simply doesn’t make sense for your project.

If you have a legitimate reason to suppress an offense there are a few ways to accomplish this:

1. You may generate a .rubocop_todo.yml file from your failing tests, with the intent of fixing them later.

2. You may add blanket exclusions to your .rubocop.yml file.

3. You may add per-line exclusions as a comment in each file.

4. You may exclude sections of files.

5. You may exclude entire files.

Generating a todo file

A Rubocop todo file is simply a partial configuration file that can be included in your Rubocop configuration. As you gradually solve the problems you remove the line from the todo file until there’s none left.

Let’s change our default.rb file so it triggers our first warning again:

#
# Cookbook Name:: chefazure-ch06
# Recipe:: default
#
# Copyright (c) 2016 The Authors, All Rights Reserved.


log "Hello, World!"

Now let’s generate a rubocop-todo.yml file by using rubocop --auto-gen-config:

PS C:UsersStuartPrestonchefazure-ch06> rubocop --auto-gen-config                  
Inspecting 7 files
..C....


Offenses:

cookbooks/chefazure-ch06/recipes/default.rb:7:5: C: Prefer single-quoted strings when you don't need string interpolation or special symbols.
log "Hello, World!"
    ^^^^^^^^^^^^^^^


7 files inspected, 1 offense detected
Created .rubocop_todo.yml.
Run `rubocop --config .rubocop_todo.yml`, or
add inherit_from: .rubocop_todo.yml in a .rubocop.yml file.

If we inspect the contents of the .rubocop_todo.yml file we can see the exclusions created for the specific ‘cops’ (Rubocop tests) that we are no longer interested in testing for:

PS C:UsersStuartPrestonchefazure-ch06> cat .rubocop_todo.yml                  
# This configuration was generated by
# `rubocop --auto-gen-config`
# on 2015-11-05 16:18:50 +0000 using RuboCop version 0.34.2.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again.


# Offense count: 1
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
Style/StringLiterals:
  Enabled: false

To take on the new configuration, you can either specify rubocop -c .rubocop_todo.yml from the command line, or (the preferred approach) is to create a new file called .rubocop.yml and insert the following line:

inherit_from: .rubocop_todo.yml

Now when you execute rubocop without any parameters it will pick up the .rubocop_todo.yml file and suppress any defined rules there:

PS C:UsersStuartPrestonchefazure-ch06> rubocop                  
Inspecting 7 files
.......


7 files inspected, no offenses detected
Adding Blanket Exclusions

As we mentioned before, the .rubocop.yml file is where we apply any configuration that is global to the whole repository. So let’s replace our .rubocop.yml file with the contents of the .rubocop_todo.yml file, as it already has some rules in it:

PS C:UsersStuartPrestonchefazure-ch06> cp .rubocop_todo.yml .rubocop.yml                  
PS C:UsersStuartPrestonchefazure-ch06> rubocop
Inspecting 7 files
.......


7 files inspected, no offenses detected

Ok so what’s the difference? Although the same outcome is achieved, you would place rules you wanted to suppress permanently for the whole team in the .rubocop.yml file and rules that you eventually want to fix in the .rubocop_todo.yml file.

Finally let’s have a look at how to exclude cops from running on a per-line basis.

Adding Per-line Exclusions

To exclude cops from running on a per-line basis, we need to add a Ruby comment to that line.

First, let’s remove our .rubocop.yml to bring back an error that needs correcting, and run Rubocop again to detect our mistake. We can use the -D parameter to see the full cop name, which we’ll need to use to specify our exclusion.

PS C:UsersStuartPrestonchefazure-ch06> rm .rubocop.yml                  
PS C:UsersStuartPrestonchefazure-ch06> rubocop -D
Inspecting 7 files
..C....


Offenses:

cookbooks/chefazure-ch06/recipes/default.rb:7:5: C: Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.
log "Hello, World!"
    ^^^^^^^^^^^^^^^


7 files inspected, 1 offense detected

We can take the cop name “Style/StringLiterals” and put that in a comment against our code with a rubocop:disable directive, as shown in Figure 6-3. The resulting code should look like this:

A346707_1_En_6_Fig3_HTML.jpg
Figure 6-3. Suppressing a rubocop rule at line level
#
# Cookbook Name:: chefazure-ch06
# Recipe:: default
#
# Copyright (c) 2016 The Authors, All Rights Reserved.


log "Hello, World!" # rubocop:disable Style/StringLiterals

Now when you execute rubocop -D again, there should be no offenses detected:

PS C:UsersStuartPrestonchefazure-ch06> rubocop -D                  
Inspecting 7 files
.......


7 files inspected, no offenses detected
Suppressing Specific Rules Per Section

A per-section Rubocop suppression is achieved by surrounding the code with a comment to disable and then enable the rule. Here’s an example:

#
# Cookbook Name:: chefazure-ch06
# Recipe:: default
#
# Copyright (c) 2016 The Authors, All Rights Reserved.


# rubocop:disable Style/StringLiterals
log "Hello, World!"
# rubocop:enable Style/StringLiterals

When we run rubocop now, our section is excluded from the listed rules, and we get the output shown below:

PS C:UsersStuartPrestonchefazure-ch06> rubocop                  
Inspecting 7 files
.......


7 files inspected, no offenses detected
Suppressing All Rules in a Section

Finally, you can override Rubocop from running any rules against a section of code by using the all directive. Here’s an example:

#
# Cookbook Name:: chefazure-ch06
# Recipe:: default
#
# Copyright (c) 2015 The Authors, All Rights Reserved.


# rubocop:disable all
log "Hello, World!"
# rubocop:enable all

When we run rubocop now, all rules in that section are suppressed from execution:

PS C:UsersStuartPrestonchefazure-ch06> rubocop                  
Inspecting 7 files
.......


7 files inspected, no offenses detected

Rubocop Options

Rubocop can be executed with a range of options; these are all documented in Table 6-1, you can also get the list by running rubocop -h:

Table 6-1. Rubocop command-line options

Command-line Argument

Description

-v/--version

Displays the current version and exits.

-V/--verbose-version

Displays the current version plus the version of Parser and Ruby.

-L/--list-target-files

List all files Rubocop will inspect.

-F/--fail-fast

Inspects in modification time order and stops after first file with offenses.

-C/--cache

Store and reuse results for faster operation.

-d/--debug

Displays some extra debug output.

-D/--display-cop-names

Displays cop names in offense messages.

-c/--config

Run with specified config file.

-f/--format

Choose a formatter.

-o/--out

Write output to a file instead of STDOUT.

-r/--require

Require Ruby file.

-R/--rails

Run extra Rails cops.

-l/--lint

Run only lint cops.

-a/--auto-correct

Autocorrect certain offenses. Note: Experimental - use with caution.

--only

Run only the specified cop(s) and/or cops in the specified departments.

--except

Run all cops enabled by configuration except the specified cop(s) and/or departments.

--auto-gen-config

Generate a configuration file acting as a TODO list.

--exclude-limit

Limit how many individual files --auto-gen-config can list in Exclude parameters, default is 15.

--show-cops

Shows available cops and their configuration.

--fail-level

Minimum severity for exit with error code. Full severity name or uppercase initial can be given. Normally, auto-corrected offenses are ignored. Use A or autocorrect if you'd like them to trigger failure.

-s/--stdin

Pipe source from STDIN. This is useful for editor integration.

PS C:UsersStuartPreston> rubocop -h              

That covers the basics of Rubocop. Where possible try to maintain zero violations in your project or distribute your .rubocop.yml files with your repo so that it is clear what is acceptable to the project to be ignored or suppressed. Let’s move on to some of the other quality tools within the ChefDK.

Using FoodCritic

FoodCritic is a static code analyzer that checks for what it considers to be poor cookbook authoring practices when using the Chef language. Compiling, converging, and executing real cookbook tests take time, and tools such as FoodCritic help us ‘fail fast’ before any code has been executed. It can also flag problems that would cause your Chef Client run to fail.

FoodCritic Rules

There are currently 58 default FoodCritic rules that are executed against the specified cookbooks. A complete list can be generated by using the command foodcritic -l:

PS C:UsersStuartPrestonchefazure-ch06> foodcritic -l              
FC001: Use strings in preference to symbols to access node attributes
FC002: Avoid string interpolation where not required
FC003: Check whether you are running with chef server before using server-specific features
FC004: Use a service resource to start and stop services
FC005: Avoid repetition of resource declarations
FC006: Mode should be quoted or fully specified when setting file permissions
FC007: Ensure recipe dependencies are reflected in cookbook metadata
FC008: Generated cookbook metadata needs updating
FC009: Resource attribute not recognised
FC010: Invalid search syntax
FC011: Missing README in markdown format
FC012: Use Markdown for README rather than RDoc
FC013: Use file_cache_path rather than hard-coding tmp paths
FC014: Consider extracting long ruby_block to library
FC015: Consider converting definition to a LWRP
FC016: LWRP does not declare a default action
FC017: LWRP does not notify when updated
FC018: LWRP uses deprecated notification syntax
FC019: Access node attributes in a consistent manner
FC021: Resource condition in provider may not behave as expected
FC022: Resource condition within loop may not behave as expected
FC023: Prefer conditional attributes
FC024: Consider adding platform equivalents
FC025: Prefer chef_gem to compile-time gem install
FC026: Conditional execution block attribute contains only string
FC027: Resource sets internal attribute
FC028: Incorrect #platform? Usage
FC029: No leading cookbook name in recipe metadata
FC030: Cookbook contains debugger breakpoints
FC031: Cookbook without metadata file
FC032: Invalid notification timing
FC033: Missing template
FC034: Unused template variables
FC037: Invalid notification action
FC038: Invalid resource action
FC039: Node method cannot be accessed with key
FC040: Execute resource used to run git commands
FC041: Execute resource used to run curl or wget commands
FC042: Prefer include_recipe to require_recipe
FC043: Prefer new notification syntax
FC044: Avoid bare attribute keys
FC045: Consider setting cookbook name in metadata
FC046: Attribute assignment uses assign unless nil
FC047: Attribute assignment does not specify precedence
FC048: Prefer Mixlib::ShellOut
FC049: Role name does not match containing file name
FC050: Name includes invalid characters
FC051: Template partials loop indefinitely
FC052: Metadata uses the unimplemented "suggests" keyword
FC053: Metadata uses the unimplemented "recommends" keyword

Each FoodCritic rule is documented at http://foodcritic.io - for example Figure 6-4 shows the first rule that is tested against your cookbook: FC001.

A346707_1_En_6_Fig4_HTML.jpg
Figure 6-4. FoodCritic rule explanation as seen at http://www.foodcritic.io/#FC001

To see FoodCritic in action, let’s go back to our recipe cookbooks/chefazure-ch06/default.rb in our code editor and modify it as follows so that when executed it would log three messages as part of the Chef Client run:

#
# Cookbook Name:: chefazure-ch06
# Recipe:: default
#
# Copyright (c) 2015 The Authors, All Rights Reserved.


log 'Hello, Adam!' do
  level :info
end


log 'Hello, Alan!' do
  level :info
end


log 'Hello, Ross!' do
  level :info
end

Now let’s see what FoodCritic identifies about our cookbook:

C:UsersStuartPrestonchefazure-ch06> foodcritic cookbooks/chefazure-ch06                
FC005: Avoid repetition of resource declarations: cookbooks/chefazure-ch06/recipes/default.rb:7
FC011: Missing README in markdown format: cookbooks/chefazure-ch06/README.md:1

We can see that we have triggered the rule FC005: Avoid repetition of resource declarations. If we look up this rule (as seen in Figure 6-5), we can see the problem and a possible solution is presented to us.

A346707_1_En_6_Fig5_HTML.jpg
Figure 6-5. FoodCritic rule explanation as seen at http://foodcritic.io/#FC005

We can now go back to our recipe and change it to match the desired style:

#
# Cookbook Name:: chefazure-ch06
# Recipe:: default
#
# Copyright (c) 2015 The Authors, All Rights Reserved.


%w(Adam Alan Ross).each do |friend|
  log "Hello, #{friend}!" do
    level :info
  end
end
Note

On a Windows machine, the path to the cookbook must be passed in with forward slashes (e.g., cookbooks/chefazure-ch06) rather than backslashes (e.g., cookbookschefazure-ch06).

We can now retest and ensure we do not get any FC005 matches returned:

PS C:UsersStuartPrestonchefazure-ch06> foodcritic cookbooks/chefazure-ch06                

We are left with one further warning that we will suppress in the next section:

FC011: Missing README in markdown format: cookbooks/chefazure-ch06/README.md:1

Suppressing FoodCritic Messages

Rules in FoodCritic are identified by a tag, which takes the format FC + number: for example, FC001. To exclude rules with specific tags, the -t option is used with a ∼ in front of the tag name. For example, if we wished to exclude the tag FC011: Missing README in markdown format we would specify the following command at the command line:

PS C:UsersStuartPrestonchefazure-ch06> foodcritic cookbooks/chefazure-ch06 -t ∼FC011                

The results list should now be an empty line, indicating that no issues were found with the cookbook:

Suppressing FoodCritic Messages for an Entire cookbook

To exclude rules for all users of a repo, we can create a .foodcritic file at the root of the specific cookbook, containing a list of the rules we want to exclude. For example, to exclude the rule FC011: Missing README in markdown format, the file should contain a single line as follows:

PS C:UsersStuartPrestonchefazure-ch06> echo "∼FC011" > cookbooks/chefazure-ch06/.foodcritic                  
PS C:UsersStuartPrestonchefazure-ch06> foodcritic cookbooks/chefazure-ch06

Again the results returned should be an empty line, indicating no issues were found:

Further FoodCritic Options

FoodCritic has a number of additional options that can be seen by typing foodcritic -h as shown in Table 6-2.

Table 6-2. FoodCritic command-line options

Command-line Argument

Description

-t, --tags TAGS

Check against (or exclude ∼) rules with the specified tags.

-l, --list

List all enabled rules and their descriptions.

-f, --epic-fail TAGS

Fail the build based on tags. Use 'any' to fail on all warnings.

-c, --chef-version VERSION

Only check against rules valid for this version of Chef.

-B, --cookbook-path PATH

Cookbook path(s) to check.

-C, --[no-]context

Show lines matched against rather than the default summary.

-E, --environment-path PATH

Environment path(s) to check.

-I, --include PATH

Additional rule file path(s) to load.

-G, --search-gems

Search rubygems for rule files with the path foodcritic/rules/**/*.rb

-P, --progress

Show progress of files being checked.

-R, --role-path PATH

Role path(s) to check.

-S, --search-grammar PATH

Specify grammar to use when validating search syntax.

-V, --version

Display the foodcritic version.

-X, --exclude PATH

Exclude path(s) from being linted.

Now that we’ve covered the basic of code linting and static analysis, we’ll move on to explain how to test your recipes.

Cookbook Testing

Cookbook Testing with Chef generally falls into two areas: Unit Testing and Acceptance Testing. From a Unit Testing perspective, we’re interested in testing individual units of code, independently of other circumstances in the system, such as the state of the environment. Because unit tests should not have any external dependencies such as connections to a remote system they should execute at speed.

From an Acceptance Testing perspective, we’re interested in testing that once we apply our code to an environment, the described target state is reached.

The Chef ecosystem has a great Unit Testing tool called ChefSpec, which is a set of extensions on top of the popular behavior-driven development (BDD) testing framework RSpec, and a great tool for Acceptance Testing called Test Kitchen that is a complete framework that allows you to test your cookbooks against multiple platforms. Figure 6-6 differentiates the types of testing and the tools used.

A346707_1_En_6_Fig6_HTML.jpg
Figure 6-6. Types of Cookbook Testing

Using ChefSpec

ChefSpec is a framework that simulates a Chef Client run and allows you to test resources and recipes without any other external dependencies. As a result, ChefSpec tests execute very quickly. Because of this, ChefSpec tests are typically placed early in a CI system’s pipeline after static analysis and are typically the first indicator of problems that may exist within a cookbook.

ChefSpec is based on a behavior-driven development (BDD) framework called RSpec that uses a natural language domain-specific language (DSL) to describe scenarios in which systems are being tested. RSpec allows a scenario to be set up, then executed with dummy parameters. The results are then compared to a predefined set of expectations. This syntax is shown in Figure 6-7 below.

A346707_1_En_6_Fig7_HTML.jpg
Figure 6-7. Syntax of an RSpec test (credit: https://docs.chef.io/chefspec.html )

Generating ChefSpec tests for Cookbooks

Luckily for us, cookbooks by default have a default set of ChefSpec tests created. They can be found under <cookbook>/spec/unit/recipes. Test ‘specifications’ by convention are named _spec.rb so that tools can find them by this pattern.

Figure 6-8 shows us the structure that is generated and a default test that has been generated.

A346707_1_En_6_Fig8_HTML.jpg
Figure 6-8. Directory structure showing the default test specification file created by chef generate app

Executing ChefSpec Tests

ChefSpec tests are executed from the cookbook directory and not the root of the repo. So let’s execute the default tests by changing directory into the cookbook directory and running rspec.

Tip

Use the -f documentation flag to get a list of the tests that are executed, also if your terminal supports it you can add the --color flag to get results in color.

PS C:UsersStuartPrestonchefazure-ch06> cd cookbooks/chefazure-06                
PS C:UsersStuartPrestonchefazure-ch06cookbookschefazure-ch06> rspec -f documentation


chefazure-ch06::default
  When all attributes are default, on an unspecified platform
    converges successfully


Finished in 0.71881 seconds (files took 15.13 seconds to load)
1 example, 0 failures

We can see from the output that we have one example, zero failures. This is the expected behavior. Let’s break down the default tests to understand what it is doing a bit more.

#
# Cookbook Name:: chefazure-ch06
# Spec:: default
#
# Copyright (c) 2016 The Authors, All Rights Reserved.


require 'spec_helper'

The statement require 'spec_helper' means that this file is including some statements from the common file spec/spec_helper.rb. All of the spec files will require this statement at the top of the file otherwise ChefSpec will not get loaded correctly. Let’s have a look at the default test:

describe 'chefazure-ch06::default' do
  context 'When all attributes are default, on an unspecified platform' do
    let(:chef_run) do
      runner = ChefSpec::ServerRunner.new
      runner.converge(described_recipe)
    end


    it 'converges successfully' do
      expect { chef_run }.to_not raise_error
    end
  end
end

Working outwards from the test:

  • The test itself (it 'converges successfully') is testing that the chef_run does not raise an error when it converges.

  • The let statement assigns variables that can be used elsewhere in the context block (in our case, chef_run).

  • The context block (context 'When all attributes are default, on an unspecified platform') provides a grouping for the test, and can be used to run different tests according to the platform being tested (not used here).

  • The describe statement (describe 'chefazure-ch06::default') is the scenario that is being tested. In this case a recipe: chefazure-ch06::default.

What we are trying to do with ChefSpec test is provide tests that cover each scenario you are writing recipes for, so that we can write the minimum recipe code that satisfies the test and eventually allows safe refactoring of the recipe code.

Imagine you have a scenario that means you want to write a recipe to ensure a specific file is deleted. Most of the tests follow the pattern of the following:

expect(chef_run).to <action>_<resource>('<name>')

We know in Chef to write this recipe we need to use the file resource and the :delete action, so our resulting test would look something like this:

expect(chef_run).to delete_file('c:/test.txt')

The resource in our recipe would look something like this:

file 'c:/test.txt' do
  action :delete
end

We can see the resource name, the name of the file and the action match those we specified in the test.

Let’s try it. First of all, open up the cookbooks/chefazure-ch06/spec/unit/recipes/default_spec.rb in your text editor and add the following text within the context block:

it 'deletes the test.txt file' do
  expect(chef_run).to delete_file('c:/test.txt')
end

The resulting file should look as shown in Figure 6-9:

A346707_1_En_6_Fig9_HTML.jpg
Figure 6-9. Adding a test to our default_spec.rb file

Now we can execute our test by running rspec with the -f documentation and --color options:

PS C:UsersStuartPrestonchefazure-ch06cookbookschefazure-ch06> rspec -f documentation --color              

After a few seconds you should see a similar test failure to below.

chefazure-ch06::default
  When all attributes are default, on an unspecified platform
    converges successfully
    deletes the test.txt file (FAILED - 1)


Failures:

  1) chefazure-ch06::default When all attributes are default, on an unspecified platform
deletes the test.txt file
     Failure/Error: expect(chef_run).to delete_file('c:/test.txt')
       expected "file[c:/test.txt]" with action :delete to be in Chef run. Other file
resources:


     # ./spec/unit/recipes/default_spec.rb:21:in `block (3 levels) in <top (required)>'

Finished in 1.06 seconds (files took 15.93 seconds to load)
2 examples, 1 failure


Failed examples:

rspec ./spec/unit/recipes/default_spec.rb:20 # chefazure-ch06::default When all attributes
are default, on an unspecified platform deletes the test.txt file

The output shows you exactly which test has failed so you can go back to it and resolve it. It is, of course, correct that this test fails - we haven’t written the recipe yet! This style of test-first development is a commonly accepted practice in the development world. We first of all write our test, see it fail, write the minimum amount of code to satisfy the test, and then refactor our solution, maintaining test success all the while. Figure 6-10 shows the workflow behind this practice.

A346707_1_En_6_Fig10_HTML.jpg
Figure 6-10. Test-first development process (image credit: Xavier Pigeon)

We can now add to our default.rb recipe to satisfy the test:

file 'c:/test.txt' do
  action :delete
end

Having done that, let’s retry our test using rspec:

PS C:UsersStuartPrestonchefazure-ch06cookbookschefazure-ch06> rspec -f documentation --color                

chefazure-ch06::default
  When all attributes are default, on an unspecified platform
    converges successfully
    deletes the test.txt file


Finished in 1.05 seconds (files took 14.8 seconds to load)
2 examples, 0 failures

We have satisfied our tests and everything is ‘green’.

Adding Code Coverage to Cookbook Tests

ChefSpec offers a crude code coverage mechanism to let you know which resources have been touched by your tests as a percentage.

To enable it, simply add the following line to your spec_helper.rb file (found in your cookbook within the spec folder):

at_exit { ChefSpec::Coverage.report! }

Now when you run the RSpec tests you should get a coverage report at the end:

PS C:UsersStuartPrestonchefazure-ch06cookbookschefazure-ch06> rspec -f documentation --color                
chefazure-ch06::default
  When all attributes are default, on an unspecified platform
    converges successfully
    deletes the test.txt file


Finished in 9.38 seconds (files took 5.04 seconds to load)
2 examples, 0 failures


ChefSpec Coverage report generated...
  Total Resources:   1
  Touched Resources: 1
  Touch Coverage:    100.0%


You are awesome and so is your test coverage! Have a fantastic day!

Now we have seen how to add unit tests to your cookbooks to simulate behavior, we can now take a look at the other side of testing, using Test Kitchen and InSpec to perform acceptance tests on your recipes against real servers.

Using Test Kitchen and InSpec with Azure Resource Manager

Test Kitchen (see http://kitchen.ci ) is a test framework that allows you to execute code on one or more platforms in isolation, ensuring that no prior state exists. Test Kitchen is written in Ruby (so is cross-platform) and has a plug-in architecture that allows you to use it against popular cloud, virtualization, or bare metal resources. It is not directly connected with the Chef toolset, but is distributed with the ChefDK, which means it should be ready for us to use on our workstation.

Test Kitchen in the context of Chef makes it easy to add Acceptance Tests to our infrastructure code because we can spin up a brand new machine, execute our recipes, and then run tests to ensure the system is in the desired state after execution. Then the machine can either be thrown away, or optionally you can continue to work on it until you are closer to a working solution.

There are four primary stages of the Test Kitchen workflow : create, converge, verify, and destroy and these are shown in Figure 6-11.

A346707_1_En_6_Fig11_HTML.jpg
Figure 6-11. “kitchen test” workflow

This workflow can be used for both development purposes and also integrated within a Continuous Integration (CI) pipeline - if we complete the entire workflow and get no errors then we can continue to the next stage of our CI/CD pipeline. We’ll be covering this more in chapter 8.

In this chapter we’ll be using Test Kitchen with the Azure Resource Manager driver, kitchen-azurerm and identifying some of the commonly used configuration parameters. Let’s start by getting the kitchen-azurerm packages installed and configured for our Azure subscription.

Installing the Azure Resource Manager Driver for Test Kitchen

I’m going to assume at this point that you have a recent ChefDK installed (if not, full details were provided in chapter 1), so from your shell we can install the kitchen driver for Azure Resource Manager (see https://github.com/pendrica/kitchen-azurerm ) by using the chef gem install kitchen-azurerm command:

PS C:UsersStuartPreston> chef gem install kitchen-azurerm            

You should see output similar to the below if all was successful. You may see other dependencies get installed at the same time too - this is normal:

Successfully installed kitchen-azurerm-0.2.6
Parsing documentation for kitchen-azurerm-0.2.6
Installing ri documentation for kitchen-azurerm-0.2.6
Done installing documentation for kitchen-azurerm after 0 seconds
1 gem installed


PS C:UsersStuartPreston>

We can now proceed with the rest of the configuration.

Configuring the Credentials File

In chapter 4 we set up a Service Principal and granted that Service Principal access to your Azure Subscription. We then configured your ∼/.azure/credentials file so that it could be used for provisioning resources in Azure. We will be reusing that same mechanism here.

To recap; our credentials file should look something like this:

[b6e7eee9-YOUR-GUID-HERE-03ab624df016]
client_id = "48b9bba3-YOUR-GUID-HERE-90f0b68ce8ba"
client_secret = "my-top-secret-password"
tenant_id = "9c117323-YOUR-GUID-HERE-9ee430723ba3"

where the first line is our Subscription ID, the client_id is the Application ID of the Service Principal, the client_secret is the Shared Secret assigned to the application, and the tenant_id is the Tenant ID for the Subscription.

Configuring Test Kitchen within a Chef Repo

Test Kitchen is driven from a single, declarative configuration file called .kitchen.yml that resides in the root of your repo. A chef-generated app generated this file automatically for us at the beginning of this chapter, and Listing 6-1 shows the default .kitchen.yml file that you’ll find in your repository.

Listing 6-1. default .kitchen.yml file
---
driver:
  name: vagrant


provisioner:
  name: chef_zero


# Uncomment the following verifier to leverage Inspec instead of Busser (the
# default verifier)
# verifier:
#   name: inspec


platforms:
  - name: ubuntu-14.04
  - name: centos-7.1


suites:
  - name: default
    run_list:
      - recipe[chefazure-ch06::default]
    attributes:
Tip

If you do not have a .kitchen.yml file in your repo, one can be created automatically by typing kitchen init in the location you want one.

We can see our default configuration file has four mains sections:

  • Driver- in this section we provide the name of the driver to be used. The default driver supports Vagrant, which is a tool that is typically used in concert with a local virtualization provider such as Oracle VirtualBox. In our case we will be changing this to use the Azure Resource Manager driver.

  • Provisioner- the provisioner is used to specify what action should be taken when we ’converge’ our machine. We want to use the chef_zero provisioner, which provides the capability to transfer and execute recipes from our cookbook(s) within the machine itself.

  • Platforms- a list of platforms can be provided here. In the example we can see both Ubuntu-14.04 and CentOS 7.1 have been added. Test Kitchen builds a test matrix based on a combination of platforms and suites.

  • Suites- a suite is where we provide a run list containing the list of Chef recipes we wish to execute in order. We can also override any attributes that are settable within those recipes.

If you execute kitchen list at this point, we are provided with a list of the test instances that would be created if we ran kitchen create:

Instance             Driver   Provisioner  Verifier  Transport  Last Action
default-ubuntu-1404  Vagrant  ChefZero     Busser    Ssh        <Not Created>
default-centos-71    Vagrant  ChefZero     Busser    Ssh        <Not Created>
Note

If you see an error message, then it is likely you do not have Vagrant installed. We do not require Vagrant for the purposes of the book; however if you are interested in running Test Kitchen locally then you can download it from https://www.vagrantup.com/downloads.html

We’re going to edit our .kitchen.yml so that it uses the Azure Resource Manager (ARM) driver; so open the file in your text editor and edit it as follows:

---
driver:
  name: azurerm


driver_config:
  subscription_id: 'b6e7eee9-YOUR-GUID-HERE-03ab624df016'
  location: 'West Europe'
  machine_size: 'Standard_DS2'


provisioner:
  name: chef_zero


platforms:
  - name: windows2012-r2
    driver_config:
      image_urn: MicrosoftWindowsServer:WindowsServer:2012-R2-Datacenter:latest
    transport:
      name: winrm


verifier:
  name: inspec


suites:
  - name: default
    run_list:
      - recipe[chefazure-ch06::default]
    attributes:

We can see the driver name was changed to azurerm, and there’s a driver_config section that takes a subscription_id, location, and machine_size parameters. These parameters should be self-explanatory. Something that requires more explaining is the image_urn parameter.

The image_urn parameter is a four-part string in the format Publisher:Offer:Sku:Version that uniquely identifies an image in Azure. Here are some examples:

  • MicrosoftWindowsServer:WindowsServer:2012-R2-Datacenter:latest

  • Canonical:UbuntuServer:14.04.3-LTS:latest

  • Canonical:UbuntuServer:15.04:latest

  • OpenLogic:CentOS:7.1:latest

In chapter 5 we explained how to derive these values; so if you skipped that bit, now would be a good time to go back and read it as it is a slightly awkward mechanism to discover these images.

Creating an Instance - Kitchen Create

If we have our credentials file configured correctly, a valid subscription ID in our .kitchen.yml and a valid image_urn entry, we’re ready to start up a machine in Azure. To do this we use the kitchen create command. It will take a few minutes to provision.

PS C:UsersStuartPrestonchefazure-ch06> kitchen create              
-----> Starting Kitchen (v1.4.2)
-----> Creating <default-windows2012-r2>...
       Creating Resource Group: kitchen-default-windows2012-r2-20151107T001229
       Creating Deployment: deploy-fc2ef6c5988cb47e
       Resource Microsoft.Network/publicIPAddresses 'publicip' provisioning status is Running
       Resource Microsoft.Network/virtualNetworks 'vnet' provisioning status is Running
       Resource Microsoft.Storage/storageAccounts 'storagefc2ef6c5988cb47e' provisioning status is Running
       Resource Microsoft.Compute/virtualMachines 'vm' provisioning status is Running
       Resource Microsoft.Compute/virtualMachines 'vm' provisioning status is Running
       Resource Microsoft.Compute/virtualMachines 'vm' provisioning status is Running
[...]
       Resource Microsoft.Compute/virtualMachines 'vm' provisioning status is Running
       Resource Microsoft.Compute/virtualMachines/extensions 'vm/enableWinRM' provisioning status is Running
       Resource Microsoft.Compute/virtualMachines/extensions 'vm/enableWinRM' provisioning status is Running
       Resource Microsoft.Compute/virtualMachines/extensions 'vm/enableWinRM' provisioning status is Running
[...]
       Resource Microsoft.Compute/virtualMachines/extensions 'vm/enableWinRM' provisioning status is Running
       Resource Template deployment reached end state of 'Succeeded'.
       IP Address is: 104.40.217.123 [kitchen-c2ef6c5988cb47e.westeurope.cloudapp.azure.com]
       Finished creating <default-windows2012-r2> (9m39.36s).
-----> Kitchen is finished. (9m42.75s)

At the end of the creation process we have a machine running and we are presented its IP address. Test Kitchen stores this in a state file (named .kitchen/default-windows2012-r2.yml in our case) so that later phases can use this information to connect to the machine.

Converging an Instance - Kitchen Converge

Converging an instance simply means bringing the machine toward the desired state so that it can be ready for testing. We have specified the chef_zero provisioner in our configuration file (it’s the default), which means that when we execute kitchen converge the following things will happen:

  • The repository and any cookbooks specified as a dependency are transferred to the target machine using the specified transport (WinRM in our case).

  • If not installed already, a Chef Client will be downloaded and installed on the machine.

  • Chef Client will execute the specified recipes on the machine.

We already have a very basic recipe set up in our repository, which attempts to delete a file. Let’s converge our machine using kitchen converge and watch the output:

PS C:UsersStuartPrestonchefazure-ch06> kitchen converge                
-----> Starting Kitchen (v1.4.2)
-----> Converging <default-windows2012-r2>...
       Preparing files for transfer
       Preparing dna.json
       Preparing cookbooks from project directory
       Removing non-cookbook files before transfer
       Preparing validation.pem
       Preparing client.rb
-----> Installing Chef Omnibus (install only if missing)
       Downloading package from https://opscode-omnibus-packages.s3.amazonaws.com/windows/2008r2/i386/chef-client-12.5.1-1-x86.msi
       Download complete.
       Successfully verified C:UsersazureAppDataLocalTempchef-true.msi


       Installing Chef Omnibus package C:UsersazureAppDataLocalTempchef-true.msi
       Installation complete
       Transferring files to <default-windows2012-r2>
       Starting Chef Client, version 12.5.1
       Creating a new client identity for default-windows2012-r2 using the validator key.
       resolving cookbooks for run list: ["chefazure-ch06::default"]
       Synchronizing Cookbooks:
         - chefazure-ch06 (0.1.0)
       Compiling Cookbooks...
       Converging 1 resources
       Recipe: chefazure-ch06::default
         * file[c:/test.txt] action delete (up to date)


       Running handlers:
       Running handlers complete
       Chef Client finished, 0/1 resources updated in 01 minutes 01 seconds
       Finished converging <default-windows2012-r2> (3m12.08s).
-----> Kitchen is finished. (3m14.35s)

We can see that our recipe executed, and our file was found to be in the correct state (deleted), as seen by the following section:

Converging 1 resources
Recipe: chefazure-ch06::default
  * file[c:/test.txt] action delete (up to date)

No action was taken on our instance because the file did not already exist; therefore the machine was in the desired state as specified by the recipe. We can modify our recipe on our workstation and rerun kitchen converge as many times as we like.

Note

Each time you run kitchen converge with an updated recipe or suite of recipes you run the risk of changing the state of the machine to an unexpected starting position for the next run. It is always wise to destroy your machines regularly to ensure your recipes can run end to end.

Using InSpec and Kitchen Verify

InSpec is a recently released testing framework, similar to ChefSpec in that it uses BDD-like language constructs in the test specifications. However, InSpec does no simulation of the Chef Client run; instead it tests the actual running state of the machine. This makes InSpec tests a powerful tool for post-convergence automated testing, as we can use it to ensure that each action specified in our recipe has brought the machine to the correct target state. To give a couple of examples, here’s an example test to verify the ‘OS family’ on our target machine is Windows:

describe os[:family] do
  it { should eq 'windows' }
end

Here’s another test that tests whether an Apache configuration has a Listen parameter set to the value ‘443’:

describe apache_conf do
  its('Listen') { should eq '443'}
end

A full list of Resources and Matchers are available at https://docs.chef.io/inspec_reference.html .

As an example test, let’s run a couple of checks on our Windows 2012 R2 machine:

  • We’ll check the machine has a DHCP Client service installed, enabled, and running

  • We’ll also check that the machine is NOT listening on TCP port 80, as we have not installed a Web Server on it.

Looking at the list of resources, we can see that there are both the Service resource and Host resources available to accomplish this task. So let’s open up the file test/integration/default/default_spec.rb in your text editor and replace the contents with the text below:

describe service('Dhcp') do
  it { should be_installed }
  it { should be_enabled }
  it { should be_running }
end


describe host('localhost', port: 80, proto: 'tcp') do
  it { should_not be_reachable }
end

Once complete, the file should look like Figure 6-12.

A346707_1_En_6_Fig12_HTML.jpg
Figure 6-12. Inspec example spec located at test/integration/default/default_spec.rb

We can now run kitchen verify to get our test results.

PS C:UsersStuartPrestonchefazure-ch06> kitchen verify                
-----> Starting Kitchen (v1.4.2)
-----> Verifying <default-windows2012-r2>...
..."Test-NetConnection -ComputerName localhost -RemotePort 80| Select-Object -Property ComputerName, RemoteAddress, RemotePort, SourceAddress, PingSucceeded | ConvertTo-Json"
.


Finished in 8 seconds (files took 6.57 seconds to load)
4 examples, 0 failures

As seen from the output, we have 0 failures. Of course this isn’t a real test as if it was a real test we would now add some tests that we expect to pass (perhaps you are looking to define a Web Server on port 80, so it "should be_listening"). The beauty of using Test Kitchen for this is that it is a rapid, repeatable process.

For now, we’ve spent some credit running this VM with a public IP address on Azure and don’t need it any longer so let’s destroy it.

Destroying our Instance - Kitchen Destroy

Destroying our server is as simple as typing kitchen destroy. What is actually happening here is that we are deleting an Azure Resource Group that contains all the resources created by kitchen create. It actually takes some time to acquire locks on all the resources in Azure, so once the delete request has been accepted by Azure, the operation continues in the background, allowing you to continue to the next stage, or to start up a new instance.

Note

The author cannot be held responsible for large Azure bills if you forget to destroy your Test Kitchen instances!

PS C:UsersStuartPrestonchefazure-ch06> kitchen destroy              
-----> Starting Kitchen (v1.4.2)
-----> Destroying <default-windows2012-r2>...
       Destroying Resource Group: kitchen-default-windows2012-r2-20151107T001229
       Destroy operation accepted and will continue in the background.
       Finished destroying <default-windows2012-r2> (0m5.58s).
-----> Kitchen is finished. (0m8.95s)

Other Test Kitchen Commands

We covered the basic four stages of Test Kitchen above, but there are a couple of other commands that are useful to know about:

  • You can run kitchen test in order to execute all phases of Test Kitchen in order without stopping.

  • You can run kitchen diagnose --all in order to diagnose problems with configuration (note: most issues are caused by formatting issues in the .yml file.)

  • Finally, instead of running through each test suite and platform sequentially, these can be executed in parallel by running kitchen <command> --concurrency <n> where n is the number of threads you wish to start.

Summary

We’ve now taken a very quick lap through some important quality tools that are provided with the Chef Development Kit. We are able to run linters such as Rubocop and FoodCritic against our code to address consistency and code quality issues. We can add ChefSpec unit tests to simulate the behavior of our recipes and catch unexpected behavior early in the development cycle. We can configure Test Kitchen to use Azure Resource Manager to define ephemeral (short-lived) machines that we can use for testing, and we can use a Chef Zero provisioner and InSpec tests to verify the real state of the target system after convergence.

In the next two chapters we’re going to take everything we have learned from the book so far about real-world scenarios and see what a starting point for a Continuous Integration/Continuous Delivery pipeline might look like.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.151.26