Spice up your recipes with Chef Sugar

Chef, Chef Sugar, Ruby Posted on

A few months ago, I was having a discussion with some colleagues internally and CHEF-494 came up. In short, the ticket was created by Seth Chisamore and proposed creating a core cookbook that included some useful primitives for common patterns:

We need a cookbook that contains helpful libraries that would useful across all cookbooks...

The comment thread went on with suggestions of methods and solutions, including ubuntu_before_lucid?, best_ip_for, vagrant helpers, and more. It was very clear this was something that was desired by the community.

Well, it just so happened I had a cross-country plane ride that week. In case you don't know, absolutely all of my open source projects have been born on airplanes. I started brainstorming the design pattern for Chef Sugar in my head, talked it over with some colleagues, and was ready to cook as the plane took off.

What is it?

Before I talk about design and structure, let me introduce Chef Sugar. Ultimately, Chef Sugar is an extension of the Chef core providing helpful DSL-methods and logic that makes recipe-writing a pleasure.

Chef Sugar? More like crème brûlée. Beautiful Ruby code and exceedingly useful! - Doug Ireton via Twitter

  • platform? and platform_family? are extended to include specific matchers like windows? and ubuntu_before_lucid?
  • Cloud providers each have helpful predicate methods like ec2? and linode?
  • Encrypted Data Bags have an encrypted_data_bag_item Recipe DSL method
  • Shell functions, like which, dev_null, and installed?, are available in guards

For example, you can turn this:

include_recipe 'cookbook::_windows_setup' if platform_family?('windows')
include_recipe 'cookbook::_ec2_setup' if node['ec2'] || node['eucalyptus']

package 'foo' do
  action :nothing
end.run_action(:install)

execute 'untar package' do
  if node['kernel']['machine'] == 'x86_64'
    command 'ARCH_FLAGS=x64 make'
  else
    command 'ARCH_FLAGS=i386 make'
  end
  not_if do
    ::File.exists?('/bin/tool') &&
    ::File.executable?('/bin/tool') &&
    shell_out!('/bin/tool --version').stdout.strip == node['tool']['version']
  end
end

credentials = Chef::EncryptedDataBagItem.load('accounts', 'passwords')

into this:

include_recipe 'cookbook::_windows_setup' if windows?
include_recipe 'cookbook::_ec2_setup' if ec2? || eucalyptus?

compile_time do
  package 'apache2'
end

execute 'untar package' do
  if _64_bit?
    command 'ARCH_FLAGS=x64 make'
  else
    command 'ARCH_FLAGS=i386 make'
  end
  not_if { installed_at_version?('/bin/tool', node['tool']['version']) }
end

credentials = encrypted_data_bag_item('accounts', 'passwords')

For a full list of features and the most recent API, see the Chef Sugar README on GitHub.

The nerd parts

I really enjoyed writing Chef Sugar, and I think the design principles are really solid and extensible. Each component is a self-extending module, meaning they are accessible outside of a Recipe or Resource. This was an important design decision, as it allows developers to use Sugar as a library. For example, consider the following resource definition

class Chef
  class Resource::MyResource < Resource
    def initialize(name)
      # ... usual business ...

      #
      # In here, you don't have access to the Recipe DSL
      # like `package` and `template`. Instead, you need
      # to fully instantiate resources with their full
      # classes, like `Chef::Resource::Template.new`. If
      # Chef Sugar just extended the Recipe DSL, you
      # would not be able to leverage any of the custom
      # libraries in heavy-weight resources.
      #
      # That being said, there is one key difference
      # between using the library versus using a recipe
      # DSL - the `node` object. The Recipe DSL methods
      # assume there's a local variable named "node"
      # that is a Chef object. When used as a library,
      # we are not afforded the same luxury of such an
      # assumption. As such, the `node` object is the
      # required first parameter to most of the library
      # implementations.
      #

      if Chef::Sugar::Platform.linux_mint?(@node)
        raise RuntimeError, "This cookbook does not work on Linux Mint!"
      end
    end
  end
end

To summarize, in a recipe:

# cookbook/recipes/default.rb
do_something if windows?

In a library as a singleton:

# cookbook/libraries/default.rb
def only_on_windows(&block)
  yield if Chef::Sugar::PlatformFamily.windows?(@node)
end

In a library as a mixin:

# cookbook/libraries/default.rb
include Chef::Sugar::PlatformFamily

def only_on_windows(&block)
  yield if windows?(@node)
end

Conclusion

In closing, I really hope you enjoy Chef Sugar. The code is on GitHub, and it is also distributed as a cookbook on the Chef Community Site.

About Seth

Seth Vargo is an engineer at Google. Previously he worked at HashiCorp, Chef Software, CustomInk, and some Pittsburgh-based startups. He is the author of Learning Chef and is passionate about reducing inequality in technology. When he is not writing, working on open source, teaching, or speaking at conferences, Seth advises non-profits.