Creating Custom Resources for Your Cookbooks

System Administration ?? Comments 07/mar/2019 qui

Introduction

Like we saw in the previous post, Chef Cookbooks can also provide custom resources that we can use in a dependant cookbooks' recipes. These custom resources can also use other resources from arbitrary cookbooks, or resources builtin to Chef.

A custom resource file is a Ruby file, ending in *.rb, and its syntax is, for example: [1]

property :property_name, RubyType, default: 'value'

load_current_value do
  # some Ruby for loading the current state of the resource
end

action :action_name do
 # a mix of built-in Chef resources and Ruby
end

action :another_action_name do
 # a mix of built-in Chef resources and Ruby
end

We will generally only have to worry about properties and actions. The load_current_value block can be used in more complex resources, to gather the current state of our resource, if it already exists, thus overriding any properties that we may want.

Development

Before starting to develop our custom resource we should create a regular cookbook, with the difference that our default recipe may not exist, and the recipes directory may not exist at all. This means that our resources cookbook has to have a proper metadata.rb file with proper versioning.

We should create a directory named resources, where our resource files reside. And very importantly remember that the resources made available by our cookbook are named after the cookbook's name and the resource's name. So, if our cookbook is named myapps and we have a resource file named superapp, to use the superapp resource we would call the myapps_superapp resource in the external cookbook.

Let us take as an example the nano text editor, an external Linux application that can be compiled manually and installed to arbitrary directories.

For this example we will call our resource file from_tar.rb and our cookbook pkg, so our directory structure should look something like:

pkg
├── Berksfile
├── metadata.rb
└── resources
    └── from_tar.rb

Our Berksfile is as simple as it gets:

And so is our metadata.rb file:

name             'pkg'
maintainer       'Manuel Torrinha'
maintainer_email 'torrinha at_ implement _dot pt'
description      'Installs tar and one resource to manage remote tar packages'
version '0.1.0'

For our resource we plan it first:

  1. install tar
  2. create extract directory
  3. fetch package
  4. extract package
  5. compile and install

Then we implement it:

property :pkg_source,      String,        default: 'https://www.nano-editor.org/dist/v3/nano-3.2.tar.gz'
property :prefix,          String,        default: '/usr/local'
property :configure_flags, Array,         default: []
property :creates,         String
property :tmp_dir,         String,        default: '/tmp'

action :install do
  basename = ::File.basename(new_resource.pkg_source)
  dirname = basename.chomp('.tar.gz')
  src_dir = "#{new_resource.tmp_dir}/#{dirname}"

  # Install package dependencies
  package 'tar'
  package 'gcc'
  package node['platform_family'] == 'debian' ? 'libncursesw5-dev': 'ncurses-devel'


  # Ensure extract dir exists
  directory src_dir do
    owner 'root'
    group 'root'
    mode  '0755'
    recursive true
    action :create
  end

  # Fetch nano to tmp_dir
  remote_file basename do
    source new_resource.pkg_source
    path "#{src_dir}/#{basename}"
    mode   '0755'
    action :create
  end

  # Unpack nano into src_dir
  execute 'unpack nano' do
    cwd src_dir
    command "tar xfz #{basename}"
    creates "#{src_dir}/#{dirname}"
  end

  # Configure and compile nano
  execute 'compile and install' do
    cwd "#{src_dir}/#{dirname}"
    flags = [new_resource.prefix ? "--prefix=#{new_resource.prefix}" : nil, *new_resource.configure_flags].compact.join(' ')
    command "./configure --quiet #{flags} && make -s && make -s install"
    creates new_resource.creates
  end
end

Note that when we want to use a property value, instead of using the variable name directly we use the namespace new_resource to access them.

We can use this sequence not just for nano, but for any application that:

  1. comes packaged in a '.tar.gz' format
  2. requires execution of configure, and only that, to generate a Makefile
  3. its Makefile supports the install argument

This specific process is actually already implemented by the tar cookbook available at the supermarket.

Testing

Of course that now that we have written our custom resource we want to be able to test it.

Let us start by creating a test cookbook, inside our pkg resource cookbook, which will have a nano_install recipe that uses our resource.

First create the metadata.rb file:

name 'test'
maintainer 'Manuel Torrinha'
maintainer_email 'torrinha at_ implement _dot pt'
description 'Cookbook for testing the pkg cookbook'
version '0.1.0'

depends 'pkg'

Then, the recipes/nano_install.rb file:

pkg_from_tar 'nano' do
  pkg_source      'https://www.nano-editor.org/dist/v3/nano-3.2.tar.gz'
  prefix          '/usr/local'
  configure_flags []
  creates         '/usr/local/bin/nano'
  tmp_dir         '/tmp'
  action :install
end

Next we update our original Berksfile to contemplate our test cookbook:

source 'https://supermarket.chef.io'

metadata

cookbook 'test', path: './test/cookbooks/test'

And finally we create a .kitchen.yml file for us to use with kitchen and test our resource in a local VM, in this case using Vagrant.

(Note that you will need vagrant and virtualbox installed on your workstation)

driver:
  name: vagrant

provisioner:
  name: chef_zero

platforms:
  - name: ubuntu-14.04
  - name: ubuntu-16.04
  - name: ubuntu-18.04
  - name: debian-8
  - name: debian-9
  - name: centos-6
  - name: centos-7

suites:
  - name: nano
    run_list:
      - recipe[test::nano_install]

So now our directory structure will now look like this:

pkg
├── Berksfile
├── .kitchen.yml
├── metadata.rb
├── resources
|   └── from_tar.rb
└── test
    └── cookbooks
        └── test
            ├── metadata.rb
            └── recipes
                └── nano_install.rb

And we can now test it with kitchen:

~/pkg$ kitchen list
Instance          Driver   Provisioner  Verifier  Transport  Last Action    Last Error
nano-ubuntu-1404  Vagrant  ChefZero     Busser    Ssh        <Not Created>  <None>
nano-ubuntu-1604  Vagrant  ChefZero     Busser    Ssh        <Not Created>  <None>
nano-ubuntu-1804  Vagrant  ChefZero     Busser    Ssh        <Not Created>  <None>
nano-debian-8     Vagrant  ChefZero     Busser    Ssh        <Not Created>  <None>
nano-debian-9     Vagrant  ChefZero     Busser    Ssh        <Not Created>  <None>
nano-centos-6     Vagrant  ChefZero     Busser    Ssh        <Not Created>  <None>
nano-centos-7     Vagrant  ChefZero     Busser    Ssh        <Not Created>  <None>
~/pkg$ kitchen test debian-9
-----> Starting Kitchen (v1.24.0)
-----> Cleaning up any prior instances of <nano-debian-9>
-----> Destroying <nano-debian-9>...
       Finished destroying <nano-debian-9> (0m0.00s).
-----> Testing <nano-debian-9>
-----> Creating <nano-debian-9>...
(...)
       Thank you for installing Chef!
       Transferring files to <nano-debian-9>
       Starting Chef Client, version 14.11.21
       Creating a new client identity for nano-debian-9 using the validator key.
       resolving cookbooks for run list: ["test::nano_install"]
       Synchronizing Cookbooks:
         - test (0.1.0)
         - pkg (0.1.0)
       Installing Cookbook Gems:
       Compiling Cookbooks...
       Converging 1 resources
       Recipe: test::nano_install
         * pkg_from_tar[nano] action install
           * apt_package[tar] action install (up to date)
           * apt_package[gcc] action install (up to date)
           * apt_package[libncursesw5-dev] action install
             - install version 6.0+20161126-1+deb9u2 of package libncursesw5-dev
           * directory[/tmp/nano-3.2] action create
             - create new directory /tmp/nano-3.2
             - change mode from '' to '0755'
             - change owner from '' to 'root'
             - change group from '' to 'root'
           * remote_file[nano-3.2.tar.gz] action create
             - create new file /tmp/nano-3.2/nano-3.2.tar.gz
             - update content in file /tmp/nano-3.2/nano-3.2.tar.gz from none to ca6945
             (new content is binary, diff output suppressed)
             - change mode from '' to '0755'
           * execute[unpack nano] action run
             - execute tar xfz nano-3.2.tar.gz
           * execute[compile and install] action run
             - execute ./configure --quiet --prefix=/usr/local && make -s && make -s install


       Running handlers:
       Running handlers complete
       Chef Client finished, 6/8 resources updated in 39 seconds
       Downloading files from <nano-debian-9>
       Finished converging <nano-debian-9> (1m3.59s).
-----> Setting up <nano-debian-9>...
       Finished setting up <nano-debian-9> (0m0.00s).
-----> Verifying <nano-debian-9>...
       Preparing files for transfer
       Transferring files to <nano-debian-9>
       Finished verifying <nano-debian-9> (0m0.00s).
-----> Destroying <nano-debian-9>...
       ==> default: Forcing shutdown of VM...
       ==> default: Destroying VM and associated drives...
       Vagrant instance <nano-debian-9> destroyed.
       Finished destroying <nano-debian-9> (0m4.10s).
       Finished testing <nano-debian-9> (2m8.26s).
-----> Kitchen is finished. (2m9.01s)

You can test all of the flavors by not passing any arguments after kitchen test.

You can also use kitchen converge instead of test, so that the VM is not destroyed after successful convergence, and you can see the state of the VM after applying the run list.

Conclusion

In this post we saw how to design and implement custom resources for Chef, from scratch. We also saw how to test said resources using kitchen and Vagrant.

A working cookbook representing what was reviewed in this post is available here

Feel free to post any questions, or point out any mistakes, in the comment box below.

References

[1]--, Custom Resources - Chef.io Documentation, 2018. [link]

Manuel Torrinha is an information systems engineer, with more than 10 years of experience in managing GNU/Linux environments. Has an MSc in Information Systems and Computer Engineering. Work interests include High Performance Computing, Data Analysis, and IT management and Administration. Knows diverse programming, scripting and markup languages. Speaks Portuguese and English.

Related content