Skip to main content

5. Create a Custom Resource - The Git Example

Aaron LippoldAbout 7 min

Let's practice creating our own custom resource. Let's say we want to write tests that examine the current state of a local Git repository. We want to create a git resource to handle all of InSpec's interactions with the Git repo under the hood, so that we can focus on writing clean and easy-to-read profile code.

Let's take a look at this InSpec video that walks through this example and then try it out ourselves.

Create new InSpec profile

Let's start by creating a new profile:

Command
inspec init profile git

Develop controls to test / run profile

To write tests, we first need to know and have what we are testing! In your Codespaces environment, there is a git repository that we will test under the resources folder. The git repository will be the test target, similarly to how the docker containers acted as test targets in previous sections. Unzip the target git repository using the following command:

unzip ./resources/git_test.zip 

This will generate a git_test repository which we will use for these examples.

Now let's write some controls and test that they run. You can put these controls in the example.rb file generated in the controls folder of your git InSpec profile. These controls are written using the command resource which is provided by InSpec. We will write a git resource in this section to improve this test. Note that you will need to put the full directory path of the .git file from your git_test repository as the git_dir value on line 4 of example.rb. To get the full path of your current location in the terminal, use pwd.

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

git_dir = "/workspaces/saf-training-lab-environment/git_test/.git"

# The following banches should exist
describe command("git --git-dir #{git_dir} branch") do
  its('stdout') { should match /master/ }
end

describe command("git --git-dir #{git_dir} branch") do
  its('stdout') { should match /testBranch/ }
end

# What is the current branch
describe command("git --git-dir #{git_dir} branch") do
  its('stdout') { should match /^\* master/ }
end

# What is the latest commit
describe command("git --git-dir #{git_dir} log -1 --pretty=format:'%h'") do
  its('stdout') { should match /edc207f/ }
end

# What is the second to last commit
describe command("git --git-dir #{git_dir} log --skip=1 -1 --pretty=format:'%h'") do
  its('stdout') { should match /8c30bff/ }
end
Command
inspec exec git

Our tests pass, but they all use the command resource. It's not best practice to do this -- for one thing, it makes our tests more complicated, and the output too long.

But What If I Don't Care About The Tests Being Complicated And The Output Being Too Long?

Some test writers like to wrap their favorite bash commands in a command block and call it a day.
However, best practice is to write clean and maintainable InSpec code even if you yourself have no trouble using the command resource to do everything.

Recall that other developers and assessors need to be able to understand how your tests function. Nobody likes trying to debug someone else's profile that assumes that the operator knows exactly how the profile writer's favorite terminal commands work.

Let's rewrite these tests in a way that abstracts away the complexity of working with the git command into a resource.

Rewrite test

Let's rewrite the first test in our example file to make it more readable with a git resource as follows:

# The following banches should exist
describe git(git_dir) do
  its('branches') { should include 'master' }
end

Now let's run the profile.

Command
inspec exec git

We should get an error because the git method and resource are not defined yet. We should fix that.

Develop the git resource

Let's start by creating a new file called git.rb in the libraries directory. If you do not already have a libraries directory, you can make one in the git InSpec profile directory. The content of the file should look like this:

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

class Git < Inspec.resource(1)
    name 'git'

end

Setting Up a Resource Using InSpec Init

Instead of just creating the git.rb file in the libraries directory, you can use InSpec to assist you in creating a resource. Run inspec init resource <your-resource-name> and follow the prompts to create the foundation and see examples for a resource.

Now run the profile again.

Command
inspec exec git

This time we get another error letting us know that we have a resource that has been given the incorrect number of arguments. This means we have given an additional parameter to this resource that we have not yet accepted.

Each resource will require an initialization method.

For our git.rb file let's add that initialization method:

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

class Git < Inspec.resource(1)
    name 'git'

    def initialize(path)
        @path = path
    end

end

This is saving the path we are passing in from the control into an instance method called path.

Now when we run the profile.

Command
inspec exec git

The test will run but we will get an error saying we do not have a branches method. Remember that the other 4 tests are still passing because they are not yet using the git resource, but are still relying on InSpec's command resource.

Let's go back to our git.rb file to fix that by adding a branches method:

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

class Git < Inspec.resource(1)
    name 'git'

    def initialize(path)
        @path = path
    end

    def branches

    end

end

We have now defined the branches method. Let's see what the test output shows us.

Command
inspec exec git

Now the error message says that the branches method is returning a null value when it's expecting an array or something that is able to accept the include method invoked on it.

We can use the InSpec helper method which enables you to invoke any other inspec resource as seen below:

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

class Git < Inspec.resource(1)
    name 'git'

    def initialize(path)
        @path = path
    end

    def branches
        inspec.command("git --git-dir #{@path} branch").stdout
    end

end

We have borrowed the built-in command resource to handle running Git's CLI commands.

Now we see that we get a passing test!

Now let's adjust our test to also check for our second branch that we created earlier as well as check our current branch:

# The following banches should exist
describe git(git_dir) do
  its('branches') { should include 'master' }
  its('branches') { should include 'testBranch' }
  its('current_branch') { should cmp 'master' }
end
Command
inspec exec git

Let's head over to the git.rb file to create the current_branch method we are invoking in the above test:

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

class Git < Inspec.resource(1)
    name 'git'

    def initialize(path)
        @path = path
    end

    def branches
        inspec.command("git --git-dir #{@path} branch").stdout
    end

    def current_branch
        branch_name = inspec.command("git --git-dir #{@path} branch").stdout.strip.split("\n").find do |name|
            name.start_with?('*')
        end
        branch_name.gsub(/^\*/,'').strip
    end

end

Now we can run the profile again.

Command
inspec exec git

All the tests should pass!

Exercise!

As a solo exercise, try to create a method in the git.rb file to check what the last commit is.