Skip to main content

6. Create a Custom Resource - The Docker Example

Aaron LippoldAbout 7 min

Custom Resource - Docker

Now let's try a more complicated example. Let's say we want to create a resource that can parse a docker-compose file.

Create new profile and setup Docker files

First, we need a test target! Check out the resources/docker-compose.yml file in Codespaces for what we can test. It looks like this:

version: '3'
services:
  workstation:
    container_name: workstation
    image: learnchef/inspec_workstation
    stdin_open: true
    tty: true
    links:
      - target
    volumes:
      - .:/root
  target:
    image: learnchef/inspec_target
    stdin_open: true
    tty: true

We will continue writing our controls to check against this docker file:

Command
inspec init profile docker-workstations

Develop controls to test/run profile

Conceptually, we want to write tests with this profile that will check different settings in a docker-compose file. If you are not sure what the InSpec code looks like for a particular test, start by writing what conceptually you want to achieve, then modify it to be correct syntax. We can do that with the idea of checking a setting in a docker-compose file, which we know is a YAML file, as such:

In the docker-workstations/controls/example.rb file, write the control:

describe yaml('file_name') do
  its('setting') { should_not eq 'value' }
end

We test early and often. We know that the test we wrote is not complete, but we can see if we are on the right track. Remember that the command line output can help guide your development!

Command
inspec exec docker-workstations

We need to replace the file_name above with the location of the docker-compose.yml file. We also need to change the setting to grab the tag we want to retrieve. Finally we need to change value with the actual value as shown in the docker compose file. You can write multiple expectation statements in the describe block.

describe yaml('/path/to/docker-compose.yml') do
  its(['services', 'workstation', 'image']) { should eq 'learnchef/inspec_workstation' }
  its(['services', 'workstation', 'volumes']) { should cmp '.:/root' }
end

Now if we test this control using the following command we should see all the tests pass.

Command
inspec exec docker-workstations

If you received an error above! - Concept Check

If you saw this as your output:

Profile:   InSpec Profile (docker-workstations)
Version:   0.1.0
Target:    local://
Target ID: 6dcb9e6f-5ede-5474-9521-595fadf5c7ce

  YAML /path/to/docker-compose.yml
     β†Ί  Can't find file: /path/to/docker-compose.yml

Test Summary: 0 successful, 0 failures, 1 skipped

It is because you did not give YOUR path to the docker-compose file. You need to replace the path in your example.rb file to be something like this:

describe yaml('/workspaces/saf-training-lab-environment/resources/docker-compose.yml') do
  its(['services', 'workstation', 'image']) { should eq 'learnchef/inspec_workstation' }
  its(['services', 'workstation', 'volumes']) { should cmp '.:/root' }
end

Rewrite test to utilize a new resource

Going back to the control, we will write it using a resource that doesn't exist called docker-compose-config that is going to take a path as a parameter.

Test Driven Development

Remember the idea of Test Driven Development (TDD), the red-green-clean cycle. This way of development is driven by the tests. In this way, you know when you have succeeded while developing something new! In other words, before writing a solution, first write the test (which will fail - red), so that you know exactly what the expectation should be and when you have achieved it. Then you can write the solution to make the test pass (green). Finally, clean up the solution to make it easy to read and efficient!
Test Driven Development

Tests
describe yaml('/workspaces/saf-training-lab-environment/resources/docker-compose.yml') do
  its(['services', 'workstation', 'image']) { should eq 'learnchef/inspec_workstation' }
  its(['services', 'workstation', 'volumes']) { should cmp '.:/root' }
end

describe docker_compose_config('/workspaces/saf-training-lab-environment/resources/docker-compose.yml') do
  its('services.workstation.image') { should eq 'learnchef/inspec_workstation' }
  its('services.workstation.volumes') { should cmp '.:/root' }
end

Now we should see an error if we go back to terminal and run the same command to execute a scan. We should get an error saying the docker_compose_config method does not yet exist. That's because we have not yet defined this resource.

Command
inspec exec docker-workstations

Develop the Docker resource

In the libraries directory of the profile we will make a docker_compose_config.rb file, , the content of the file should look like this:

# encoding: utf-8
# copyright: 2019, The Authors

class DockerComposeConfig < Inspec.resource(1)

  name 'docker_compose_config'

end

Using InSpec Init to Create the Resource

Alternatively, you can use inspec init resource <your-resource-name> to create the template for your custom resource. You may have a "lib" folder or a "libraries" folder. Make sure that InSpec recognizes the location of your custom resource.

Command
inspec init resource docker_compose_config --overwrite

Now when we save and run the profile again using:

Command
inspec exec docker-workstations

We will get an error saying we gave it the wrong number of arguments: was given 1 but expected 0. This is because every class in Ruby that has a parameter must have an initialize function to accept that parameter.

# encoding: utf-8
# copyright: 2019, The Authors

class DockerComposeConfig < Inspec.resource(1)

  name 'docker_compose_config'

  def initialize(path)
    @path = path
  end

end

Now let's run the profile once more.

Command
inspec exec docker-workstations

This time the profile runs, but we get a message that the docker_compose_config resource does not have the services method. So let's define that method now:

# encoding: utf-8
# copyright: 2019, The Authors

class DockerComposeConfig < Inspec.resource(1)

  name 'docker_compose_config'

  def initialize(path)
    @path = path
  end

  def services

  end

end

Start by just defining the services method. Then, let's run the profile once more.

Command
inspec exec docker-workstations

Now we got a different failure that tells us a nil value was returned. So now we will go ahead and define the services method. We will use an already existing InSpec resource to parse the path file.

# encoding: utf-8
# copyright: 2019, The Authors

class DockerComposeConfig < Inspec.resource(1)

  name 'docker_compose_config'

  def initialize(path)
    @path = path
    @yaml = inspec.yaml(path)
  end

  def services
    @yaml['services']
  end

end

Now let's run the profile once more.

Command
inspec exec docker-workstations

You will notice that it parses correctly, but instead of our result we end up getting a hash. We need to convert the hash into an object that appears like other objects so that we may use our dot notation. So we will wrap our hash in a Ruby class called a Hashie::Mash. This gives us a quick way to convert a hash into a Ruby object with a number of methods attached to it. You will have to import the Hashie library by running gem install hashie and import it in the resource file to be used. It and is written as follows:

# encoding: utf-8
# copyright: 2019, The Authors

require "hashie/mash"

class DockerComposeConfig < Inspec.resource(1)

  name 'docker_compose_config'

  def initialize(path)
    @path = path
    @yaml = inspec.yaml(path)
  end

  def services
    Hashie::Mash.new(@yaml['services'])
  end

end

Lets run the profile again.

Command
inspec exec docker-workstations

Everything passed!

Check your work

Check your work with the InSpec video below that walks through this docker resource example!