Chef Resource Condtionals

Lately it seems like all of my posts are about things that are super, painfully, embarrassingly obvious in hindsight. The trend continues!

Over the last week I've been learning to use Chef to set up some servers at work (with the help of the iron_chef gem, which was written by a co-worker of mine). At this point I feel like a real dummy for never having bothered to use Chef before, especially since it's been around for some time now. If you're not using Chef for server management you really ought to look into it. It makes automating your setup easy and having everything that your servers need documented in your scripts is awesome.

Despite quickly becoming a "why wasn't I using this before?" sort of tool there's been a few conceptual hurdles, as there always is with any framework or DSL. The one that really got me is the not_if/only_if conditional guards on resource blocks. The Chef documentation lays it out in what seems like a straightforward manner:

The not_if and only_if conditional executions can be used to put additional guards around certain resources so that they are only run when the condition is met.

Seems simple right? Well, if you look around enough you'll see examples of not_if and only_if used with either a block passed as the argument or with a String passed as the argument.

Here's two quick real and I swear not-contrived examples. One with a block:

bash 'unarchive-lame-source' do
  cwd ::File.dirname(src_filepath)

  code <<-EOH
    tar zxf #{::File.basename(src_filepath)} -C #{::File.dirname(src_filepath)}

  not_if {[:file_cache_path] || 'tmp', "lame-#{node['lame']['version']}")) }

And one with a string:

bash 'compile-lame-source' do
  cwd ::File.dirname(src_filepath)

  code <<-EOH
    cd lame-#{node['lame']['version']} &&
    ./configure #{lame_options.join(' ')} &&
    make &&
    make install

  not_if 'sudo ldconfig && ldconfig -p | grep libmp3lame'

Here comes the embarrassing part. To me, at least, it wasn't clear what each form of the method call did, or really that there is a difference between the two. When passing a block as the argument, the result of the block, truthy or falsy, determines whether or not the resource is run. When passing a String, it is executed as a shell command and the return result of the command is used to determine whether or not the resource is run. Remember, for shell commands a return result of 0 indicates success (or true) and anything else, typically 1, but it can be any non-zero value, indicates failure (or false).

At first I was naively trying to use not_if like this not_if { 'sudo ldconfig && ldconfig -p | grep libmp3lame' } expecting the block to run the command. Instead, the block just returns the string. Since Strings are truthy the block always returns true and always skips the resource for not_if or runs the resource for only_if.

If we take a look at the source for Chef::Resource::Conditional#initialize it becomes pretty clear what's going on.

def initialize(positivity, command=nil, command_opts={}, &block)
  @positivity = positivity
  case command
  when String
    @command, @command_opts = command, command_opts
    @block = nil
  when nil
    raise ArgumentError, "only_if/not_if requires either a command or a block" unless block_given?
    @command, @command_opts = nil, nil
    @block = block
    raise ArgumentError, "Invalid only_if/not_if command: #{command.inspect} (#{command.class})"

Here we can clearly see that if the optional command is passed as a String the Chef::Resource::Conditional object is initialized with the command and command options and the block instance variable set to nil (and importantly, ignored if it was passed at all). If no command was passed but a block was given then the command and command options instance variables are set to nil and the block instance variable is set to the block that was passed. And finally an exception is raised if no command or block is given or if something weird is passed as the command.

And if you look a little bit further down in the source you'll find where the conditional is actually evaluated:

def evaluate
  @command ? evaluate_command : evaluate_block

def evaluate_command
  shell_out(@command, @command_opts).status.success?
rescue Chef::Exceptions::CommandTimeout
  Chef::Log.warn "Command '#{@command}' timed out"

def evaluate_block

Pretty much exactly as I described above. If the command instance variable is present, it'll evaluate the command, otherwise it'll call the block. If you're interested in seeing how the cross-platform shell_out method works you can check out the source, it's definitely worth a read.

In fact, I think the takeaway from all of this is, when in doubt, go straight to the source code. It'll save you lots of time and you'd be hard pressed to not learn something new, especially if you're diving into a well-known and properly designed library.