chef-gen-flavors
- home :: https://github.com/Nordstrom/chef-gen-flavors
- license :: Apache2
- gem version ::
- build status ::
- code climate ::
- docs ::
DESCRIPTION
chef-gen-flavors is a framework for creating custom templates for the 'chef generate' command provided by ChefDK.
This gem simply provides a framework; templates are provided by separate gems, which you can host privately for use within your organization or publicly for the Chef community to use.
At present this is focused primarily on providing templates for generation of cookbooks, as this is where most organization-specific customization takes place. Support for the other artifacts that ChefDK can generate may work, but is not the focus of early releases.
INSTALLATION
chef gem install chef-gen-flavors
You will also need to install at least one plugin, which may be distributed
via Rubygems (in which case you install using chef gem
) or as source, in
which case you should refer to the installation documentation that comes with
the plugin.
PREREQUISITES
This gem requires that you have ChefDK (at least version 0.3.6) installed. chef-dk is not a dependency of this gem because chef-dk should always be installed using the omnibus packages provided by Chef, not as a gem.
CONFIGURATION
In your knife.rb
file, add this snippet:
# 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
When you run chef generate
, all available plugins will be loaded. If more
than one plugin is found, you will be prompted as to which you want to use:
$ chef generate cookbook my_app
If you set the environment variable CHEFGEN_FLAVOR
to the name of a
plugin, it will be chosen instead of presenting a prompt:
$ CHEFGEN_FLAVOR=mytemplate chef generate cookbook my_app
USING THE BUILT-IN CHEFDK TEMPLATE
By default, this gem does not offer the built-in ChefDK template as an
option. By setting the environment variable CHEFDK_FLAVOR, the
option builtin
will be offered.
TERMINOLOGY
(because everything in the Chef ecosystem has to have foodie names)
- Flavor - a type of template. Provided by a plugin in the namespace
ChefGen::Flavor::
. Flavors can be distributed as ruby gems inside or outside of your organization. - Snippet - a small piece of a code_generator cookbook that flavors can compose together to avoid repeating themselves. Nominally provided by a module in the namespace
ChefGen::Snippet::
, but can be defined in any module. chef-gen-flavors comes with several common snippets, but you can create your own and package them as standalone gems or as part of a flavor gem
FLAVORS
This gem uses little-plugger to make adding template flavors easy. Each flavor is defined by a plugin named using little-pluggers's rules.
The plugin must define a class inside the naming hierarchy
ChefGen::Flavor::
. The class name should be the filename converted to
CamelCase (e.g. foo_bar.rb
= FooBar
)
The name of the module must not be in all caps, as little-plugger ignores these (assuming that they are constants).
Plugins must also define a class method named description
, which is used
both to find the path to the file that defines the plugin and in the prompt
displayed when more than one plugin is available.
You do not have to require
your plugin; little-plugger searches all
installed gems for files matching the globspec.
EXAMPLE FLAVOR STRUCTURE
This example defines a flavor named Example
. It can only generate
cookbooks, as its code_generator cookbook contains no other recipes.
A functional copy of this plugin is available on rubygems as
chef-gen-flavor-example
.
The directory structure of a plugin looks like this:
chef-gen-flavor-example
├── code_generator
│ ├── files
│ │ └── default
│ ├── .rb
│ ├── recipes
│ │ └── cookbook.rb
│ └── templates
│ └── default
└── lib
└── chef_gen
└── flavor
└── example.rb
It is important that the name of the directory in which your generator cookbook lives matches the name in metadata.rb. ChefDK uses the last element of the path as the cookbook name, so if you put your cookbook in a folder called 'cookbook' but your metadata.rb declares the name as 'code_generator', ChefDK won't be able to find your recipe.
ALTERNATE code_generator PATHS
By default, the code_generator cookbook is assumed to live in a directory
named code_generator
four levels higher than the path of the file
definining the plugin.
To specify that the code_generator cookbook lives elsewhere, define a class
method named code_generator_path
which takes one argument (the path to the
plugin class) and returns the path to the code_generator cookbook. If the
Example
plugin wanted to place the code_generator cookbook in a directory
named template
instead of code_generator
, it would define an instance
method like this:
module ChefGen
module Flavor
class Example
class << self
def description
'example cookbook template'
end
def code_generator_path(classfile)
File.(
File.join(
classfile,
'..', '..', '..', '..',
'template'
)
)
end
end
end
end
end
For compatibility with all platforms supported by ChefDK, plugins should use
the methods in the File
class to construct relative paths rather than
assuming what the path separator should be.
GENERATOR PATH COPY
When #path is called, chef-gen-flavors makes a copy of the selected generator cookbook to a temporary path, which is what gets returned and used by ChefDK. This path is cleaned up at exit unless the environment variable CHEFGEN_NOCLEANTMP is set.
ADDING CONTENT TO THE GENERATOR COPY
After the generator is copied to a temporary path, the #add_content instance method is called (if it exists) on the flavor class. It is passed one arg: the path to the temporary copy.
This allows flavors to create content dynamically by writing files to the proper directly. It is exploited by the flavor base class described below.
FLAVOR BASE CLASS
Inside of your plugin's code_generator cookbook, you can do anything that chef-solo can do. If you aren't familiar with the mechanics of the default generators that comes with chef-dk, you should study those recipes first before attempting to create your own.
To make the job of authoring a custom template easier, this gem comes with a base class that plugins can inherit from. This provides some useful features:
- helpers to create directories and render files and templates using simple name translation
- the ability to create and use snippets that set up commonly used functionality (i.e. ChefSpec or Test Kitchen)
- the ability to prevent files from being overwritten when a template is being applied overtop an existing cookbook
To use the base class, make it a dependency of your gem and inherit from it:
require 'chef_gen/flavor_base'
module ChefGen
module Flavor
class Amazing < FlavorBase
end
end
end
Then in one of your generator recipes like recipes/cookbook.rb
, create
an instance of your plugin, passing the recipe into which resources will
be injected:
template = ChefGen::Flavor::Amazing.new(self)
The plugin has several helper methods you can use:
target_path
returns the full path to the target directorydirectories
is aArray
of directories to createfiles
is anArray
of files to createfiles_if_missing
is anArray
of files to create which should not be overwritten if they existtemplates
is anArray
of templates to rendertemplates_if_missing
is anArray
of templates to render which should not be overwritten if they existfail_on_clobber
is a boolean accessor which causes generation to fail if any files in thefiles
ortemplates
arrays already exist. Defaults to true, but can be set to false by adding-a clobber
to thechef generate
command linereport_actions
is a boolean accessor which causes the generator to report all of the actions it tooknext_steps
is a message to be displayed to the user as the last thing the generator does
The distinction between files that are overwritten and those that are created only if they do not exist allows for updating a cookbook to an organization-wide policy while still allowing for per-cookbook customization.
For example, if your cookbook template has a standard Rakefile and you wish
to add a target to it, you can do so if Rakefile is in the files
array.
When the generator is used on top of an existing cookbook, the Rakefile will
be rewritten. Custom rake tasks can be placed in .rake
files in the
directory lib/tasks
, which will not be overwritten.
Once the template object has been set up to your satisfaction, call the
#generate
method, which creates the Chef resources to generate your
target.
The list of files and templates take the path to the rendered file (e.g.
spec/spec_helper.rb
). The source for the file or template will be
transformed by replacing foreward slashes and dots with underscores.
Additionally, templates have .erb
appended to the source.
This code:
template = ChefGen::Flavor::Amazing.new
template.files << 'spec/spec_helper.rb'
template.templates << '.rubocop.yml'
template.generate
is equivalent to manually creating these resources:
file "#{cookbook_dir}/spec/spec_helper.rb" do
source 'spec_spec_helper_rb'
end
template "#{cookbook_dir}/.rubocop.yml" do
source '\_rubocop\_yml.erb'
end
TEMPLATE SNIPPETS
Many templates will use common patterns, such as providing a README.md and CHANGELOG.md, providing the files necessary to create a ChefSpec unit testing suite, or the files necessary to create a Test Kitchen integration testing suite.
Rather than have every template author create these, this gem ships with a number of snippets, which can be included in your plugin class like so:
require 'chef_gen/flavor'
require 'chef_gen/snippets'
module ChefGen
module Flavor
class Amazing < FlavorBase
include ChefGen::Snippet::ChefSpec
end
end
end
Snippets can add both declarations (files and templates to be rendered) and content. This reduces the amount of content that a flavor author has to include in their distribution.
The snippets that ship with this gem are:
CookbookBase
- sets up the basic files any cookbook needs (README, CHANGELOG, etc.)StyleFoodcritic
- sets up the files for style checking with FoodcriticStyleRubocop
- sets up the files for style checking with RubocopStyleTailor
- sets up the files for style checking with TailorChefSpec
- sets up the files for basic ChefSpec unit testingTestKitchen
- sets up the files for basic Test Kitchen integration testingRecipes
- creates recipes/default.rbAttributes
- creates attributes/default.rbExampleFile
- creates files/default/example.confExampleTemplate
- creates templates/default/example.conf.erbResourceProvider
- sets up a sample LWRP resource and provider
You can also create your own snippet. A snippet is simply a module that
provides some number of public methods prefixed with snippet\_
. Any such
methods will be called, passing the recipe object as the only parameter when
#generate
is called. For example, a simplified ChefSpec snippet might look
like this:
module ChefGen
module Snippet
module ChefSpec
def snippet_chefspec(recipe)
@directories << 'spec'
@directories << File.join('spec', '/recipes')
@files << '.rspec'
@files << File.join('spec', 'spec_helper.rb')
@files << File.join('spec', 'recipes', 'default_spec.rb')
end
end
end
end
SNIPPET INITIALIZERS
Because you cannot add on to #initialize in a class when including a
module, the FlavorBase initializer will call any public method provided
by a snippet prefixed by init\_
. For example, the StandardIgnore
snippet initializes its list of patterns:
# initializes the pattern arrays
def init_standardignore_instancevars
@chefignore_patterns = %w(
.DS_Store Icon? nohup.out ehthumbs.db Thumbs.db
.sasscache \#* .#* *~ *.sw[az] *.bak REVISION TAGS*
tmtags *_flymake.* *_flymake *.tmproj .project .settings
mkmf.log a.out *.o *.pyc *.so *.com *.class *.dll
*.exe */rdoc/ .watchr test/* features/* Procfile
.git */.git .gitignore .gitmodules .gitconfig .gitattributes
.svn */.bzr/* */.hg/*
)
@gitignore_patterns = %w(
Berksfile.lock *~ *# .#* \#*# .*.sw[az] *.un~
bin/* .bundle/*
)
end
AFTER SNIPPETS HOOK
After all snippets have run, the method #after_run_snippets will be run if defined. This is useful to unwind or remove things in a derived flavor.
SNIPPET CONTENT
FlavorBase provides an #add_content method to allow snippets to create
content. For example, the Attributes
snippet is defined like this:
module ChefGen
module Snippet
module Attributes
def snippet_attributes_dirs(recipe)
@directories << 'attributes'
end
def snippet_attributes_files(recipe)
@templates_if_missing << File.join('attributes', 'default.rb')
end
def content_attribute_files(path)
copy_snippet_file(
File.join(
File.dirname(__FILE__), '..', '..', '..',
'shared', 'snippet', 'attributes', 'attributes_default_rb.erb'
),
File.join(path, 'templates', 'default', 'attributes_default_rb.erb')
)
end
end
end
end
When a flavor that includes this snippet is selected, the file
shared/snippet/attributes/attributes_default_rb.erb
is copied to
the path templates/default/attributes_default_rb.erb
in the temporary
generator path.
Look at the snippets in the lib/chef_gen/snippet
directory and the
example flavor
for an full demonstration of how these hooks work.
SNIPPET DOCUMENTATION
Some of the snippets provide extra functionality worth calling out:
CookbookBase
Provides several reader methods:
cookbook_gems
, a hash of gem names to gem constraints. Flavors and other snippets can add to this list; all the gems are rendered to a Gemfile by this snippetgem_sources
, an array of gem source URL. This defaults to 'https://rubygems.org' and can be replaced or added ontoberks_sources
, an array of Berksfile source URLs. This defaults to 'https://supermarket.chef.io' and can be replaced or added ontorake_tasks
, a hash of task names to task content. This allows snippets to add extra tasks to the Rakefile, which is rendered by this snippetguard_sets
, a hash of guard set names to set content. This allows snippets to add extra tasks to the Guardfile, which is rendered by this snippet
Look at the content of these snippets for a better understanding of how to use these methods. For example, the ChefSpec snippet adds gems, rake tasks and guard sets.
StandardIgnore
Provides several reader methods:
chefignore_patterns
, an array of patterns to write to the chefignore filegitignore_patterns
, an array of patterns to write to the .gitignore file
FEATURE TESTING FLAVORS
chef-gen-flavors provides a number of useful step definitions for Aruba
(a CLI driver for Cucumber) to make it easier to test flavors. To
access these definitions, add the following line to your
features/support/env.rb
file:
require 'chef_gen/flavors/cucumber'
For an example of how to use these steps in your features, refer to the reference implementation of a flavor: chef-gen-flavor-example.
Documentation for the steps themselves is in the file ARUBA_STEPS.md
AUTHOR
James FitzGibbon - [email protected] - Nordstrom, Inc.
LICENSE
Copyright 2015 Nordstrom, Inc.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.