6. Create a Custom Resource - The Docker Example
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:
inspec init profile docker-workstations
βββββββββββββββββββββββββββ InSpec Code Generator βββββββββββββββββββββββββββ
Creating new profile at /workspaces/saf-training-lab-environment/docker-workstations
β’ Creating file inspec.yml
β’ Creating directory /workspaces/saf-training-lab-environment/docker-workstations/controls
β’ Creating file controls/example.rb
β’ Creating file README.md
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!
inspec exec docker-workstations
Profile: InSpec Profile (docker-workstations)
Version: 0.1.0
Target: local://
Target ID: 6dcb9e6f-5ede-5474-9521-595fadf5c7ce
YAML file_name
βΊ Can't find file: file_name
Test Summary: 0 successful, 0 failures, 1 skipped
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.
inspec exec docker-workstations
Profile: InSpec Profile (docker-workstations)
Version: 0.1.0
Target: local://
Target ID: 6dcb9e6f-5ede-5474-9521-595fadf5c7ce
YAML /workspaces/saf-training-lab-environment/resources/docker-compose.yml
β ["services", "workstation", "image"] is expected to eq "learnchef/inspec_workstation"
β ["services", "workstation", "volumes"] is expected to cmp == ".:/root"
Test Summary: 2 successful, 0 failures, 0 skipped
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!
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
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
describe docker_compose_config('/path/to/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.
inspec exec docker-workstations
[2023-02-22T18:37:03+00:00] ERROR: Failed to load profile docker-workstations: Failed to load source for controls/example.rb: undefined method `docker_compose_config' for #<Inspec::ControlEvalContext:0x000000000593bb10>
Profile: InSpec Profile (docker-workstations)
Version: 0.1.0
Failure Message: Failed to load source for controls/example.rb: undefined method `docker_compose_config' for #<Inspec::ControlEvalContext:0x000000000593bb10>
Target: local://
Target ID: 6dcb9e6f-5ede-5474-9521-595fadf5c7ce
No tests executed.
Test Summary: 0 successful, 0 failures, 0 skipped
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.
inspec init resource docker_compose_config --overwrite
Enter Subdirectory under which to create files: ./docker-workstations
Choose File layout, either 'resource-pack' or 'core': Resource Pack
Choose Which type of resource template to use: Basic
Enter the description of this resource: A resource for testing docker-compose files
Enter Class Name for your resource.: DockerComposeConfig
βββββββββββββββββββββββββββ InSpec Code Generator βββββββββββββββββββββββββββ
Creating new resource at /workspaces/saf-training-lab-environment/docker-workstations
β’ Creating directory /workspaces/saf-training-lab-environment/docker-workstations/docs
β’ Creating file docs/docker_compose_config.md
β’ Creating directory /workspaces/saf-training-lab-environment/docker-workstations/libraries
β’ Creating file libraries/docker_compose_config.rb
Now when we save and run the profile again using:
inspec exec docker-workstations
[2023-02-22T18:38:40+00:00] ERROR: Failed to load profile docker-workstations: Failed to load source for controls/example.rb: wrong number of arguments (given 1, expected 0)
Profile: InSpec Profile (docker-workstations)
Version: 0.1.0
Failure Message: Failed to load source for controls/example.rb: wrong number of arguments (given 1, expected 0)
Target: local://
Target ID: 6dcb9e6f-5ede-5474-9521-595fadf5c7ce
No tests executed.
Test Summary: 0 successful, 0 failures, 0 skipped
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.
inspec exec docker-workstations
Profile: InSpec Profile (docker-workstations)
Version: 0.1.0
Target: local://
Target ID: 6dcb9e6f-5ede-5474-9521-595fadf5c7ce
YAML /workspaces/saf-training-lab-environment/resources/docker-compose.yml
β ["services", "workstation", "image"] is expected to eq "learnchef/inspec_workstation"
β ["services", "workstation", "volumes"] is expected to cmp == ".:/root"
docker_compose_config
Γ services.workstation.image
undefined method `services' for #<#<Class:0x00000000050205a8>:0x00000000032cbbd8>
Γ services.workstation.volumes
undefined method `services' for #<#<Class:0x00000000050205a8>:0x00000000032cbbd8>
Test Summary: 2 successful, 2 failures, 0 skipped
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.
inspec exec docker-workstations
Profile: InSpec Profile (docker-workstations)
Version: 0.1.0
Target: local://
Target ID: 6dcb9e6f-5ede-5474-9521-595fadf5c7ce
YAML /workspaces/saf-training-lab-environment/resources/docker-compose.yml
β ["services", "workstation", "image"] is expected to eq "learnchef/inspec_workstation"
β ["services", "workstation", "volumes"] is expected to cmp == ".:/root"
docker_compose_config
Γ services.workstation.image
undefined method `workstation' for nil:NilClass
Γ services.workstation.volumes
undefined method `workstation' for nil:NilClass
Test Summary: 2 successful, 2 failures, 0 skipped
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.
inspec exec docker-workstations
Profile: InSpec Profile (docker-workstations)
Version: 0.1.0
Target: local://
Target ID: 6dcb9e6f-5ede-5474-9521-595fadf5c7ce
YAML /workspaces/saf-training-lab-environment/resources/docker-compose.yml
β ["services", "workstation", "image"] is expected to eq "learnchef/inspec_workstation"
β ["services", "workstation", "volumes"] is expected to cmp == ".:/root"
docker_compose_config
Γ services.workstation.image
undefined method `workstation' for <Hash:0x0000000003abada8>
Γ services.workstation.volumes
undefined method `workstation' for <Hash:0x0000000003abada8>
Test Summary: 2 successful, 2 failures, 0 skipped
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.
inspec exec docker-workstations
Profile: InSpec Profile (docker-workstations)
Version: 0.1.0
Target: local://
Target ID: 6dcb9e6f-5ede-5474-9521-595fadf5c7ce
YAML /workspaces/saf-training-lab-environment/resources/docker-compose.yml
β ["services", "workstation", "image"] is expected to eq "learnchef/inspec_workstation"
β ["services", "workstation", "volumes"] is expected to cmp == ".:/root"
docker_compose_config
β services.workstation.image is expected to eq "learnchef/inspec_workstation"
β services.workstation.volumes is expected to cmp == ".:/root"
Test Summary: 4 successful, 0 failures, 0 skipped
Everything passed!
Check your work
Check your work with the InSpec video below that walks through this docker resource example!