class Chef::PolicyBuilder::Policyfile

Policyfile is an experimental policy builder implementation that gets run list and cookbook version information from a single document.

WARNING

This implementation is experimental. It may be changed in incompatible ways in minor or even patch releases, or even abandoned altogether. If using this with other tools, you may be forced to upgrade those tools in lockstep with chef-client because of incompatible behavior changes.

Unsupported Options:

policyfile, or replaced with a similar feature that has different semantics.

how this should work.

Constants

RunListExpansionIsh

Attributes

events[R]
json_attribs[R]
node[R]
node_name[R]
ohai_data[R]
run_context[R]

Public Class Methods

new(node_name, ohai_data, json_attribs, override_runlist, events) click to toggle source
# File lib/chef/policy_builder/policyfile.rb, line 63
def initialize(node_name, ohai_data, json_attribs, override_runlist, events)
  @node_name = node_name
  @ohai_data = ohai_data
  @json_attribs = json_attribs
  @events = events

  @node = nil

  Chef::Log.warn("Using experimental Policyfile feature")

  if Chef::Config[:solo]
    raise UnsupportedFeature, "Policyfile does not support chef-solo at this time."
  end

  if override_runlist
    raise UnsupportedFeature, "Policyfile does not support override run lists at this time"
  end

  if json_attribs && json_attribs.key?("run_list")
    raise UnsupportedFeature, "Policyfile does not support setting the run_list in json data at this time"
  end

  if Chef::Config[:environment] && !Chef::Config[:environment].chomp.empty?
    raise UnsupportedFeature, "Policyfile does not work with Chef Environments"
  end
end

Public Instance Methods

apply_policyfile_attributes() click to toggle source
# File lib/chef/policy_builder/policyfile.rb, line 214
def apply_policyfile_attributes
  node.attributes.role_default = policy["default_attributes"]
  node.attributes.role_override = policy["override_attributes"]
end
build_node() click to toggle source

Applies environment, external JSON attributes, and override run list to the node, Then expands the run_list.

Returns

node<Chef::Node>

The modified node object. node is modified in place.

# File lib/chef/policy_builder/policyfile.rb, line 133
def build_node
  # consume_external_attrs may add items to the run_list. Save the
  # expanded run_list, which we will pass to the server later to
  # determine which versions of cookbooks to use.
  node.reset_defaults_and_overrides

  node.consume_external_attrs(ohai_data, json_attribs)

  expand_run_list
  apply_policyfile_attributes

  Chef::Log.info("Run List is [#{run_list}]")
  Chef::Log.info("Run List expands to [#{run_list_with_versions_for_display.join(', ')}]")


  events.node_load_completed(node, run_list_with_versions_for_display, Chef::Config)

  node
rescue Exception => e
  events.node_load_failed(node_name, e, Chef::Config)
  raise
end
config() click to toggle source
# File lib/chef/policy_builder/policyfile.rb, line 337
def config
  Chef::Config
end
cookbook_lock_for(cookbook_name) click to toggle source
# File lib/chef/policy_builder/policyfile.rb, line 228
def cookbook_lock_for(cookbook_name)
  cookbook_locks[cookbook_name]
end
cookbook_locks() click to toggle source
# File lib/chef/policy_builder/policyfile.rb, line 329
def cookbook_locks
  policy["cookbook_locks"]
end
cookbooks_to_sync() click to toggle source

Builds a 'cookbook_hash' map of the form

"COOKBOOK_NAME" => "IDENTIFIER"

This can be passed to a Chef::CookbookSynchronizer object to synchronize the cookbooks.

TODO: Currently this makes N API calls to the server to get the cookbook objects. With server support (bulk API or the like), this should be reduced to a single call.

# File lib/chef/policy_builder/policyfile.rb, line 296
def cookbooks_to_sync
  @cookbook_to_sync ||= begin
    events.cookbook_resolution_start(run_list_with_versions_for_display)

    cookbook_versions_by_name = cookbook_locks.inject({}) do |cb_map, (name, lock_data)|
      cb_map[name] = manifest_for(name, lock_data)
      cb_map
    end
    events.cookbook_resolution_complete(cookbook_versions_by_name)

    cookbook_versions_by_name
  end
rescue Exception => e
  # TODO: wrap/munge exception to provide helpful error output
  events.cookbook_resolution_failed(run_list_with_versions_for_display, e)
  raise
end
deployment_group() click to toggle source
# File lib/chef/policy_builder/policyfile.rb, line 282
def deployment_group
  Chef::Config[:deployment_group] or
    raise ConfigurationError, "Setting `deployment_group` is not configured."
end
expand_run_list() click to toggle source
# File lib/chef/policy_builder/policyfile.rb, line 170
def expand_run_list
  node.run_list(run_list)
  node.automatic_attrs[:roles] = []
  node.automatic_attrs[:recipes] = run_list_expansion_ish.recipes
  run_list_expansion_ish
end
http_api() click to toggle source
# File lib/chef/policy_builder/policyfile.rb, line 333
def http_api
  @api_service ||= Chef::REST.new(config[:chef_server_url])
end
load_node() click to toggle source

Loads the node state from the server.

# File lib/chef/policy_builder/policyfile.rb, line 116
def load_node
  events.node_load_start(node_name, Chef::Config)
  Chef::Log.debug("Building node object for #{node_name}")

  @node = Chef::Node.find_or_create(node_name)
  validate_policyfile
  node
rescue Exception => e
  events.node_load_failed(node_name, e, Chef::Config)
  raise
end
manifest_for(cookbook_name, lock_data) click to toggle source

Fetches the CookbookVersion object for the given name and identifer specified in the lock_data. TODO: This only implements Chef 11 compatibility mode, which means that cookbooks are fetched by the “dotted_decimal_identifier”: a representation of a SHA1 in the traditional x.y.z version format.

# File lib/chef/policy_builder/policyfile.rb, line 319
def manifest_for(cookbook_name, lock_data)
  xyz_version = lock_data["dotted_decimal_identifier"]
  http_api.get("cookbooks/#{cookbook_name}/#{xyz_version}")
rescue Exception => e
  message = "Error loading cookbook #{cookbook_name} at version #{xyz_version}: #{e.class} - #{e.message}"
  err = Chef::Exceptions::CookbookNotFound.new(message)
  err.set_backtrace(e.backtrace)
  raise err
end
original_runlist() click to toggle source

Override #run_list is not supported.

# File lib/chef/policy_builder/policyfile.rb, line 94
def original_runlist
  nil
end
override_runlist() click to toggle source

Override #run_list is not supported.

# File lib/chef/policy_builder/policyfile.rb, line 99
def override_runlist
  nil
end
parse_recipe_spec(recipe_spec) click to toggle source
# File lib/chef/policy_builder/policyfile.rb, line 219
def parse_recipe_spec(recipe_spec)
  rmatch = recipe_spec.match(/recipe\[([^:]+)::([^:]+)\]/)
  if rmatch.nil?
    raise PolicyfileError, "invalid recipe specification #{recipe_spec} in Policyfile from #{policyfile_location}"
  else
    [rmatch[1], rmatch[2]]
  end
end
policy() click to toggle source
# File lib/chef/policy_builder/policyfile.rb, line 236
def policy
  @policy ||= http_api.get(policyfile_location)
rescue Net::HTTPServerException => e
  raise ConfigurationError, "Error loading policyfile from `#{policyfile_location}': #{e.class} - #{e.message}"
end
policyfile_location() click to toggle source
# File lib/chef/policy_builder/policyfile.rb, line 242
def policyfile_location
  "data/policyfiles/#{deployment_group}"
end
run_list() click to toggle source
# File lib/chef/policy_builder/policyfile.rb, line 232
def run_list
  policy["run_list"]
end
run_list_expansion() click to toggle source

Policyfile gives you the #run_list already expanded, but users of this class may expect to get a #run_list expansion compatible object by calling this method.

Returns

RunListExpansionIsh

A RunListExpansion duck type

# File lib/chef/policy_builder/policyfile.rb, line 109
def run_list_expansion
  run_list_expansion_ish
end
run_list_expansion_ish() click to toggle source
# File lib/chef/policy_builder/policyfile.rb, line 206
def run_list_expansion_ish
  recipes = run_list.map do |recipe_spec|
    cookbook, recipe = parse_recipe_spec(recipe_spec)
    "#{cookbook}::#{recipe}"
  end
  RunListExpansionIsh.new(recipes, [])
end
run_list_with_versions_for_display() click to toggle source

Internal Public API ##

# File lib/chef/policy_builder/policyfile.rb, line 197
def run_list_with_versions_for_display
  run_list.map do |recipe_spec|
    cookbook, recipe = parse_recipe_spec(recipe_spec)
    lock_data = cookbook_lock_for(cookbook)
    display = "#{cookbook}::#{recipe}@#{lock_data["version"]} (#{lock_data["identifier"][0...7]})"
    display
  end
end
setup_run_context(specific_recipes=nil) click to toggle source
# File lib/chef/policy_builder/policyfile.rb, line 156
def setup_run_context(specific_recipes=nil)
  # TODO: This file vendor stuff is duplicated and initializing it with a
  # block traps a reference to this object in a global context which will
  # prevent it from getting GC'd. Simplify it.
  Chef::Cookbook::FileVendor.on_create { |manifest| Chef::Cookbook::RemoteFileVendor.new(manifest, api_service) }
  sync_cookbooks
  cookbook_collection = Chef::CookbookCollection.new(cookbooks_to_sync)
  run_context = Chef::RunContext.new(node, cookbook_collection, events)

  run_context.load(run_list_expansion_ish)

  run_context
end
sync_cookbooks() click to toggle source
# File lib/chef/policy_builder/policyfile.rb, line 178
def sync_cookbooks
  Chef::Log.debug("Synchronizing cookbooks")
  synchronizer = Chef::CookbookSynchronizer.new(cookbooks_to_sync, events)
  synchronizer.sync_cookbooks

  # register the file cache path in the cookbook path so that CookbookLoader actually picks up the synced cookbooks
  Chef::Config[:cookbook_path] = File.join(Chef::Config[:file_cache_path], "cookbooks")

  cookbooks_to_sync
end
temporary_policy?() click to toggle source

Whether or not this is a temporary policy. Since PolicyBuilder doesn't support #override_runlist, this is always false.

# File lib/chef/policy_builder/policyfile.rb, line 191
def temporary_policy?
  false
end
validate_policyfile() click to toggle source

Do some mimimal validation of the policyfile we fetched from the server. Compatibility mode relies on using data bags to store policy files; therefore no real validation will be performed server-side and we need to make additional checks to ensure the data will be formatted correctly.

# File lib/chef/policy_builder/policyfile.rb, line 251
def validate_policyfile
  errors = []
  unless run_list
    errors << "Policyfile is missing run_list element"
  end
  unless policy.key?("cookbook_locks")
    errors << "Policyfile is missing cookbook_locks element"
  end
  if run_list.kind_of?(Array)
    run_list_errors = run_list.select do |maybe_recipe_spec|
      validate_recipe_spec(maybe_recipe_spec)
    end
    errors += run_list_errors
  else
    errors << "Policyfile run_list is malformed, must be an array of `recipe[cb_name::recipe_name]` items: #{policy["run_list"]}"
  end

  unless errors.empty?
    raise PolicyfileError, "Policyfile fetched from #{policyfile_location} was invalid:\n#{errors.join("\n")}"
  end
end
validate_recipe_spec(recipe_spec) click to toggle source
# File lib/chef/policy_builder/policyfile.rb, line 273
def validate_recipe_spec(recipe_spec)
  parse_recipe_spec(recipe_spec)
  nil
rescue PolicyfileError => e
  e.message
end