Accessing chef node attributes from kitchen tests / by Matt Wrock

This should not happen often and maybe never, but there have been a few occasions where I needed the attribute values stored in the "node under test" to verify my kitchen test verifications. For example, maybe your recipe uses the node's IP or machine names as part of writing a config file and your kitchen driver uses dhcp to obtain an IP and auto generates unique machine names. In this case, you cant know the IP or machine name at the time you are authoring your test to check that the correct text was used in the config file.

This post demonstrates a very small and simple technique to retrieve this data inside of the test in order to dynamically verify the correct values were used in your recipe that you are testing.

At CenturyLink Cloud we use this in a few serverspec tests. One place we use this is in testing our haproxy configuration. Some values we need to inject into the configuration include the subnet of the current node and its chef environment which we include in the server names. Depending on where the kitchen test is being run, a different environment may be used so we cant be sure what the names of the subnet or chef environment will be while writing the test.

We solve this by adding a recipe to the end of our kitchen run list called "export-node":

suites:
  - name: my-suite
    run_list:
      - recipe[cookbook-under-test]
      - recipe[export-node]

This is a very simple recipe that simply converts the current node's attributes to json and writes it to a known location in /tmp/kitchen on the node.

ruby_block "Save node attributes" do
  block do
    if Dir::exist?('/tmp/kitchen')
      IO.write("/tmp/kitchen/chef_node.json", node.to_json)
    end
  end
end

Then our test can read that file and parse its json to a hash to be used throughout the test.

require 'json'
require 'serverspec'

set :backend, :exec

describe file('/etc/haproxy/haproxy.cfg') do
  let(:node) { JSON.parse(IO.read('/tmp/kitchen/chef_node.json')) }
  let(:subnet) {
    ip = node["automatic"]["ipaddress"]
    ip[0,ip.rindex(".")]
  }
  let(:env) { node["chef_environment"].upcase}

  it { should be_a_file }
  its(:content) {
    should match <<-EOS
      backend elasticsearch_backend
        mode http
        balance roundrobin
        server #{env}SRCH01 #{subnet}.121:92 weight 1 check port 92
        server #{env}SRCH02 #{subnet}.122:92 weight 1 check port 92
        server #{env}SRCH03 #{subnet}.123:92 weight 1 check port 92

      backend web_backend
        mode http
        balance roundrobin
        timeout server 5m
        server #{env}WEB01 #{subnet}.131:80 weight 1 check port 80
        server #{env}WEB02 #{subnet}.132:80 weight 1 check port 80

      backend rabbit_backend
        mode http
        balance roundrobin
        server #{env}RABBIT01 #{subnet}.141:1567 weight 1 check port 1567
        server #{env}RABBIT02 #{subnet}.142:1567 weight 1 check port 1567
        server #{env}RABBIT03 #{subnet}.143:1567 weight 1 check port 1567
    EOS
  }
end

Above is a serverspec test that parses the node json to a hash. That hash is then accessible to our it blocks.

I have extracted the handful of lines in the export-node recipe to its own cookbook so you can grab it here.