Spice up your recipes with Chef Sugar
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?
andplatform_family?
are extended to include specific matchers likewindows?
andubuntu_before_lucid?
- Cloud providers each have helpful predicate methods like
ec2?
andlinode?
- Encrypted Data Bags have an
encrypted_data_bag_item
Recipe DSL method - Shell functions, like
which
,dev_null
, andinstalled?
, 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 a Distinguished Software 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.