Chris's Blog

Custom chef_gem Resources

Since Chef 0.10.10, there’s been a new core resource for installing Ruby gems to support Chef cookbooks: chef_gem. This installs gems into the current Ruby at compile time, and handles making the newly-installed gems available to the running Chef client – wrapping up an ugly piece of code found in many cookbooks:

Before chef_gem (before.rb)
1
2
3
4
5
gem_package "ruby-shadow" do
  action :nothing
end.run_action :install

Gem.clear_paths

into a single resource declaration:

With chef_gem (after.rb)
1
chef_gem "ruby-shadow"

We hadn’t considered switching to this resource though, despite a compatibility cookbook being available for older Chef versions, because our local Ruby environment is based on RPMs. We build a single Ruby RPM for all our management tools, with individual gems also built into RPMs, so our gem dependency resources specify yum_package rather than gem_package.

This hasn’t given us any problems beyond a need to patch cookbooks to install our RPMs, but now we’re open sourcing some of our cookbooks, it means that our dependencies are specific to our local environment.

Using chef_gem as an abstraction though, we can specify dependencies in a way that’s appropriate for an open source cookbook but have a local override which actually installs our RPMs. We also avoid having to patch cookbooks just to make sure our RPMs are installed for dependencies.

This is the resource definition, which is a shim between the chef_gem resource and the yum_package provider, adjusting the requested package name as we need for our RPMs.

Local chef_gem (libraries/chefgem.rb)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Chef
  class Resource
    begin
      send(:remove_const, 'ChefGem')
    rescue
      nil
    end

    class ChefGem < Chef::Resource::YumPackage

      def initialize(name, run_context=nil)
        # our gem name -> rpm name mapping
        name = "rubygem19-#{name}"
        super
        @resource_name = :chef_gem
        @provider = Chef::Provider::Package::Yum
        after_created
      end

      def after_created
        Array(@action).flatten.compact.each do |action|
          self.run_action(action)
        end
        Gem.clear_paths
      end
    end
  end
end

This code goes in a local-enviroment cookbook as a library. Once we upgrade to Chef 0.10.10 or later, it will override the core chef_gem resource, and continue to install our RPMs.