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.
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.