Delicious new Chef Sugars

Chef Sugar Posted on

A few months ago, I blogged about a thing called Chef Sugar. To quickly refresh your memory, Chef Sugar is an extension of the Chef core, recipe DSL, and select resources designed to make life as a Chef engineer as awesome as possible.

I first wrote Chef Sugar for incredibly selfish reasons - I was really sick of seeing the same patterns repeated across all of our cookbooks. It all started with the shell extensions, because simple methods like which and installed_at_version? are incredibly useful. I added a few other "sugars" and published the first version of the gem and cookbook.

Chef Sugar quickly became popular - way more popular than I originally expected. Since that initial release, Chef Sugar has been downloaded almost 8,000 times. Interally, our team has been using at least one Chef Sugar feature in every cookbook we develop. I was even asked to do an interview with InfoQ on Chef Sugar, and I will be speaking about Chef Sugar at ChefConf 2014.

So why all this back story?

Since the initial release of Chef Sugar, I thought it was "done". Turns out - I was wrong. In working with Chef on a daily basis, I found a continued need to add more sugars. There have even been some awesome conversations with community members around syntax and verbage for various sugars. I am happy to announce Chef Sugar v1.2.0 has hit the market and includes some of the freshest new produce for consumption.

Attribute "namespace"

I am a Ruby developer by trade, so hashes are no stranger to me. However, in working with customers and the community, I found that the "attribute" syntax was especially intimidating. Chef includes an awesome recipe DSL, but attributes just felt bland. They needed some sugar!

With Chef Sugar, long hashes are reduced to a simple DSL:

default['apache2']['config']['root'] = '/var/www'

Becomes:

namespace 'apache2' do
  namespace 'config' do
    root '/var/www'
  end
end

Or even:

namespace 'apache2', 'config' do
  root '/var/www'
end

NOTE: There is a bug/feature in Chef that makes using the namespace functionality a bit of a challenge. You need to install and require Chef Sugar before the initial Chef Client run begins due to the way Chef loads attributes during the compliation phase. I am working to find a better solution, but you can gem install chef-sugar as part of your bootstrap to avoid this limitation.

Constraints

Dealing with constraints is really hard. Whether you need to see if a certain version of a particular software is installed or check the version of Chef, it has always been a painful experience... until now!

Where you would formerly write something like:

Gem::Requirement.new('>= 1.0.0').satisfied_by?(Gem::Version.new(node['software']['version']))

Becomes:

version(node['software']['version']).satisfies?('>= 1.0.0')

There's even a helpful method around Chef::VERSION specifically for handling different versions of Chef:

package 'apache2' do
  not_if { chef_version.satisfies?('~> 11.0') } # Ignore Chef 11
end

require_chef_gem

When installing a gem into Chef (i.e. chef_gem, not gem_package), Chef often raises a less than desirable and confusing exception message. require_chef_gem is a very tiny wrapper around Ruby's native require method that outputs a nicer error message:

require_chef_gem 'bacon'

Instead of a LoadError, the end user gets a much more verbose explanation of what happened:

Chef could not load the gem `bacon'! You may need to install the gem
manually with `gem install bacon', or include a recipe before you can
use this resource. Please consult the documentation for this cookbook
for proper usage.

deep_fetch

For the same reasons I outlined in the attribute namespacing section, I also wanted a way to quickly fetch a deeply nested key inside the node attribute. Undoubtably you have seen this error before:

NoMethodError: undefined method `[]' for nil:NilClass

This happens when you try to access a value in the node object that does not exist. For example, suppose the node object looks like this:

{
  "apache2": {
    "config": {
      "root": "/var/www"
    }
  }
}

If you try to access node['apache2']['confgi']['root'] (note the typographical error in "confgi"), you will get an error. That is because node['apache2']['confgi'] is nil, since that key does not exist. Thus you are calling nil['root'], which raises an exception.

Chef::Node#deep_fetch makes this problem obsolete. deep_fetch takes a list of keys and recurses deeply into the hash. If it hits a nil value, it just returns nil - no exception!

node.deep_fetch('apache2', 'confgi', 'root') #=> nil

But I know what you are thinking - how is the helpful? Sometimes you are loading external data and you do not really care if a deeply nested key exists. But sometimes you want an exception when the key does not exist. That is why I wrote Chef::Node#deep_fetch!. It behaves exactly the same as deep_fetch, but raises an AttributeDoesNotExistError if any of the resulting keys does not exist. This error is much more semantic and provides a nice help output:

node.deep_fetch!('apache2', 'confgi', 'root') #=> AttributeDoesNotExistError

With a nice message:

No attribute `node['apache2']['confgi']['root']' exists on
the current node. Please make sure you have spelled everything correctly.

It is nothing short of awesome, and it makes debugging cookbook attributes much easier.


What started as a tiny side project has turned into an incredibly valuable tool that I could not develop cookbooks without. I encourage everyone to give Chef Sugar a try in your next project!

You can find more information about Chef Sugar on:

A special thank you and shout-out to all the contributors, issue reporters, and users of Chef Sugar. Keep being awesome and happy cook(book)ing!

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.