Skip to content

Instantly share code, notes, and snippets.

@andy-dufour
Last active August 13, 2018 16:41
Show Gist options
  • Save andy-dufour/3eb74ccdcd29e4c4afd1 to your computer and use it in GitHub Desktop.
Save andy-dufour/3eb74ccdcd29e4c4afd1 to your computer and use it in GitHub Desktop.

Creating a Custom Cookbook Generator

There are a few resources on the internet today about creating custom cookbook generators. The most popular is chef-gen-flavors.

These resources are generally pretty meta and slightly hard to understand, so I'm going to walk you through customizing a cookbook generator from the ground up. To do this we're going to start with a great generator called pan. After we customize the generators we're going to build the gem and install it locally.

One thing to know about chef-gen-flavors is that its heavily name based, so where-ever you see Pan and I'm replacing it with awesomesauce, you can replace with whatever the name of your flavor is, but it must be replaced in all of the same places.

Clone the repo

Fork this repo: https://github.com/echohack/pan

Then clone it

git clone https://github.com/<your_user>/pan

If you want to leave it named pan, feel free and you can skip most of the instructions below as far as renaming folders and classes. If you want to rename it create a new git project or (insert your scm of choice) project and push your updated code there.

For my example we're going to create a generator called awesomesauce.

When I cloned the repo down I created a folder called 'pan'. I'm going to move this to awesomesauce.

mv pan awesomesauce

Next, I'm going to modify the gemspec. It's going to look like this:

# -*- encoding: utf-8 -*-
Gem::Specification.new do |s|
  s.name = 'chef-flavor-awesomesauce'
  s.version = '0.1.0'
  s.add_runtime_dependency('chef-gen-flavors', ['~> 0.9'])
  s.required_rubygems_version = Gem::Requirement.new('>= 0') if s.respond_to? :required_rubygems_version=
  s.require_paths = ['lib']
  s.authors = ['Your Name']
  s.description = 'Awesomesauces automatic cookbook generator for [chef-gen-flavors](https://rubygems.org/gems/chef-gen-flavors).\n\n Original generator based off the Pan project by David Echols.'
  s.extra_rdoc_files = ['CHANGELOG.md', 'README.md']
  s.files = Dir.glob('**/*', File::FNM_DOTMATCH).keep_if { |file| File.file?(file) } - %w(. .. .DS_Store) - Dir.glob('{.git}/**/*')
  s.homepage = 'https://github.com/andy-dufour/awesomesauce'
  s.licenses = ['apache2']
  s.rdoc_options = ['--main', 'README.md']
  s.rubygems_version = '2.4.4'
  s.summary = 'A Chef automatic cookbook generator using [chef-gen-flavors](https://rubygems.org/gems/chef-gen-flavors)'
  s.specification_version = 4 if s.respond_to? :specification_version
end

We're changing name and version, as well as the author and description, but remember to give props to chef-gen-flavor and echohack since they put the work in to make this all possible.

Next, let's take a quick look at the structure of the gem:

.
├── CHANGELOG.md
├── LICENSE
├── README.md
├── awesomesauce.gemspec
├── flavor_cookbooks
│   ├── pan_base
│   │   ├── files
│   │   │   ├── default
│   │   │   │   ├── Berksfile
│   │   │   │   ├── Gemfile
│   │   │   │   ├── LICENSE
│   │   │   │   ├── Thorfile
│   │   │   │   ├── chefignore
│   │   │   │   └── gitignore
│   │   │   └── recipes
│   │   │       ├── _chef_client.rb
│   │   │       ├── _defaults_linux.rb
│   │   │       └── _defaults_windows.rb
│   │   ├── metadata.rb
│   │   ├── recipes
│   │   │   └── cookbook.rb
│   │   └── templates
│   │       └── default
│   │           ├── CHANGELOG.md.erb
│   │           ├── LICENSE.all_rights.erb
│   │           ├── LICENSE.apache2.erb
│   │           ├── README.md.erb
│   │           ├── default_attributes.rb.erb
│   │           ├── default_recipe.rb.erb
│   │           └── metadata.rb.erb
│   ├── pan_dsc_script
│   │   ├── files
│   │   │   ├── default
│   │   │   │   ├── Berksfile
│   │   │   │   ├── Gemfile
│   │   │   │   ├── LICENSE
│   │   │   │   ├── Thorfile
│   │   │   │   ├── chefignore
│   │   │   │   └── gitignore
│   │   │   ├── recipes
│   │   │   │   └── default.rb
│   │   │   └── templates
│   │   │       └── iis_config.ps1.erb
│   │   ├── metadata.rb
│   │   ├── recipes
│   │   │   └── cookbook.rb
│   │   └── templates
│   │       └── default
│   │           ├── CHANGELOG.md.erb
│   │           ├── LICENSE.all_rights.erb
│   │           ├── LICENSE.apache2.erb
│   │           ├── README.md.erb
│   │           ├── default_attributes.rb.erb
│   │           └── metadata.rb.erb
│   ├── pan_new
│   │   ├── files
│   │   │   ├── default
│   │   │   │   ├── Berksfile
│   │   │   │   ├── Gemfile
│   │   │   │   ├── LICENSE
│   │   │   │   ├── Thorfile
│   │   │   │   ├── chefignore
│   │   │   │   └── gitignore
│   │   │   └── recipes
│   │   │       └── default.rb
│   │   ├── metadata.rb
│   │   ├── recipes
│   │   │   └── cookbook.rb
│   │   └── templates
│   │       └── default
│   │           ├── CHANGELOG.md.erb
│   │           ├── LICENSE.all_rights.erb
│   │           ├── LICENSE.apache2.erb
│   │           ├── README.md.erb
│   │           ├── default_attributes.rb.erb
│   │           └── metadata.rb.erb
│   └── pan_review
│       ├── files
│       │   └── default
│       │       ├── Berksfile
│       │       ├── Gemfile
│       │       ├── LICENSE
│       │       ├── Thorfile
│       │       ├── chefignore
│       │       └── gitignore
│       ├── metadata.rb
│       └── recipes
│           └── cookbook.rb
└── lib
    └── chef_gen
        ├── flavor
        │   ├── base.rb
        │   ├── dsc_script.rb
        │   ├── new.rb
        │   └── review.rb
        └── helpers
            └── copy_helpers.rb

As you can see, we have some flavor cookbooks - this is what drives chef-gen-flavors under the hood. But first, let's take a look at the lib/chef_gen/flavor/ directory.

First off - let's remove everything except the base flavor. We can add more later. Your flavor directory should look like this when you're done:

└── lib
    └── chef_gen
        ├── flavor
        │   ├── base.rb

As well, we need to make a few changes to base.rb

This is what your base.rb should look like at the beginning:

require 'chef_gen/helpers/copy_helpers'

module ChefGen
  module Flavor
    class PanBase
      include ChefGen::CopyHelpers

      NAME = 'pan_base'
      DESC = 'Generate a base cookbook for organization wide policy.'
      VERSION = '1.0.0'

      def initialize(temp_path:)
        @temp_path = temp_path
      end

      def add_content
        copy_content(NAME)
      end
    end
  end
end

We need to rename the class from PanBase to AwesomesauceBase, as well as change NAME = 'pan_base' to NAME = 'awesomesauce_base' and make a slightly better description. The finished file is below:

require 'chef_gen/helpers/copy_helpers'

module ChefGen
  module Flavor
    class AwesomesauceBase
      include ChefGen::CopyHelpers

      NAME = 'awesomesauce_base'
      DESC = 'Generate an awesomesauce base cookbook for organization wide policy.'
      VERSION = '1.0.0'

      def initialize(temp_path:)
        @temp_path = temp_path
      end

      def add_content
        copy_content(NAME)
      end
    end
  end
end

Next, let's get rid of all of the cookbooks except pan_base. Your flavor_cookbooks directlry should look like this:

├── flavor_cookbooks
│   ├── pan_base
│   │   ├── files
│   │   │   ├── default
│   │   │   │   ├── Berksfile
│   │   │   │   ├── Gemfile
│   │   │   │   ├── LICENSE
│   │   │   │   ├── Thorfile
│   │   │   │   ├── chefignore
│   │   │   │   └── gitignore
│   │   │   └── recipes
│   │   │       ├── _chef_client.rb
│   │   │       ├── _defaults_linux.rb
│   │   │       └── _defaults_windows.rb
│   │   ├── metadata.rb
│   │   ├── recipes
│   │   │   └── cookbook.rb
│   │   └── templates
│   │       └── default
│   │           ├── CHANGELOG.md.erb
│   │           ├── LICENSE.all_rights.erb
│   │           ├── LICENSE.apache2.erb
│   │           ├── README.md.erb
│   │           ├── default_attributes.rb.erb
│   │           ├── default_recipe.rb.erb
│   │           └── metadata.rb.erb

Rename the pan_base flavor cookbook to awesomesauce_base.

Next we need to change the metadata.rb for the awesomesauce_base flavor cookbook.

open up flavor_cookbooks/awesomesauce_base/metadata.rb and change the line that reads:

name 'pan_base' to name 'awesomesauce_base'

At this point we should actually have a working cookbook generator. But let's talk about how the generator works for a minute.

Basically the cookbook generator is.. a cookbook. Within the flavor cookbook, you'll see a recipes folder and a recipe named cookbook.rb. All of the magic happens in cookbook.rb.

Let's look at what's in here courtesy of echohacks pan_base cookbook:

context = ChefDK::Generator.context
cookbook_dir = File.join(context.cookbook_root, context.cookbook_name)
attribute_context = context.cookbook_name.gsub(/-/, '_')

The most important part here for you to understand is that cookbook_dir is getting the base path where the cookbook will be created. This is distilled from ChefDK::Generator.context which houses information you will provide on the command line (e.g. chef generate cookbook test).

# Create cookbook directories
cookbook_directories = [
  'attributes',
  'recipes',
  'templates/default',
  'files/default',
  'test/integration/default'
]
cookbook_directories.each do |dir|
  directory File.join(cookbook_dir, dir) do
    recursive true
  end
end

Here, we're creating an array of directories and then creating each one. It's important to understand that you can use chef resources to customize your generated cookbooks within the generator cookbook. The same pattern repeats for files, and templates.

Let's look at the templates:

# Create basic files from template
files_template = %w(
  metadata.rb
  README.md
  CHANGELOG.md
  .kitchen.yml
)

Why don't we change our .kitchen.yml to stand up only CentOS 6 machines.

Let's modify the file flavor_cookbooks/awesomesauce_base/templates/default/.kitchen.yml.erb

It currently looks like this:

---
driver:
  name: vagrant

provisioner:
  name: chef_zero

platforms:
  - name: ubuntu-14.04

  - name: win2012r2
    driver_config:
      box: mwrock/Windows2012R2

suites:
  - name: default
    run_list:
      - recipe[<%= cookbook_name %>::default]
    attributes:

Let's remove the platforms and add centos-6.5 as a platform. Your .kitchen.yml.erb should now look like this:

---
driver:
  name: vagrant

provisioner:
  name: chef_zero

platforms:
  - name: centos-6.5

suites:
  - name: default
    run_list:
      - recipe[<%= cookbook_name %>::default]
    attributes:

OK - we've customized the pan cookbook generator, let's build it as a gem and install it. Make sure you're at the root of the awesomesauce repo, then run the following command:

/opt/chefdk/embedded/bin/gem build awesomesauce.gemspec

You should see output like this:

WARNING:  no email specified
WARNING:  See http://guides.rubygems.org/specification-reference/ for help
  Successfully built RubyGem
  Name: chef-flavor-awesomesauce
  Version: 0.1.0
  File: chef-flavor-awesomesauce-0.1.0.gem

Now let's install it:

/opt/chefdk/embedded/bin/gem install chef-flavor-awesomesauce-0.1.0.gem

You should see output like this:

Successfully installed chef-gen-flavors-0.9.1
Successfully installed chef-flavor-awesomesauce-0.1.0
2 gems installed

We need to enable chef gen flavors in your knife.rb file before we start.. edit ~/.chef/knife.rb and add:

# only load ChefGen::Flavors if we're being called from the ChefDK CLI
if defined?(ChefDK::CLI)
  require 'chef_gen/flavors'
  chefdk.generator_cookbook = ChefGen::Flavors.path
end

Or if you're in a monolithic repo you may need to add it to .chef/knife.rb in your repo.

Go to a directory you want to generate a cookbook - for example, I use ~/Development/cookbooks/

and execute chef generate cookbook awesomesauce_test_cookbook

You should see the output using ChefGen flavor 'awesomesauce_base' directly after running the command.

To check to make sure we've generated the cookbook we expect lets check for the .kitchen.yml we generated:

cat awesomesauce_test_cookbook/.kitchen.yml

we should see:

platforms:
  - name: centos-6.5

That's it for part 1, part 2 we'll add a second cookbook and chef-gen-flavors will prompt us for which one to use.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment