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

  if Chef::Config[:solo_legacy_mode]
    raise UnsupportedFeature, "Policyfile does not support chef-solo. Use chef-client local mode instead."
  end

  if override_runlist
    raise UnsupportedFeature, "Policyfile does not support override run lists. Use named run_lists instead."
  end

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

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

Public Instance Methods

api_service() click to toggle source

@api private

# File lib/chef/policy_builder/policyfile.rb, line 497
def api_service
  @api_service ||= Chef::ServerAPI.new(config[:chef_server_url],
                                       { version_class: Chef::CookbookManifestVersions })
end
apply_policyfile_attributes() click to toggle source

@api private

Sets attributes from the policyfile on the node, using the role priority.

# File lib/chef/policy_builder/policyfile.rb, line 268
def apply_policyfile_attributes
  node.attributes.role_default = policy["default_attributes"]
  node.attributes.role_override = policy["override_attributes"]
  hoist_policyfile_attributes(node.policy_group) if node.policy_group
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 150
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)
  events.run_list_expanded(run_list_expansion_ish)

  # we must do this after `node.consume_external_attrs`
  node.automatic_attrs[:policy_name] = node.policy_name
  node.automatic_attrs[:policy_group] = node.policy_group
  node.automatic_attrs[:chef_environment] = node.policy_group

  node
rescue Exception => e
  events.node_load_failed(node_name, e, Chef::Config)
  raise
end
config() click to toggle source

@api private

# File lib/chef/policy_builder/policyfile.rb, line 503
def config
  Chef::Config
end
cookbook_lock_for(cookbook_name) click to toggle source

@api private

# File lib/chef/policy_builder/policyfile.rb, line 294
def cookbook_lock_for(cookbook_name)
  cookbook_locks[cookbook_name]
end
cookbook_locks() click to toggle source

@api private

# File lib/chef/policy_builder/policyfile.rb, line 487
def cookbook_locks
  policy["cookbook_locks"]
end
cookbooks_to_sync() click to toggle source

@api private 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 454
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

@api private

# File lib/chef/policy_builder/policyfile.rb, line 364
def deployment_group
  Chef::Config[:deployment_group] || raise(ConfigurationError, "Setting `deployment_group` is not configured.")
end
expand_run_list() click to toggle source

Sets `run_list` on the node from the policy, sets `roles` and `recipes` attributes on the node accordingly.

@return [RunListExpansionIsh] A RunListExpansion duck-type.

# File lib/chef/policy_builder/policyfile.rb, line 203
def expand_run_list
  CookbookCacheCleaner.instance.skip_removal = true if named_run_list_requested?

  node.run_list(run_list)
  node.automatic_attrs[:policy_revision] = revision_id
  node.automatic_attrs[:roles] = []
  node.automatic_attrs[:recipes] = run_list_expansion_ish.recipes
  run_list_expansion_ish
end
finish_load_node(node) click to toggle source

PolicyBuilder API ##

# File lib/chef/policy_builder/policyfile.rb, line 138
def finish_load_node(node)
  @node = node
  select_policy_name_and_group
  validate_policyfile
  events.policyfile_loaded(policy)
end
hoist_policyfile_attributes(policy_group) click to toggle source

@api private

Hoists attributes from role_X up to the equivalent role_X level

# File lib/chef/policy_builder/policyfile.rb, line 277
def hoist_policyfile_attributes(policy_group)
  Chef::Log.trace("Running attribute Hoist for group #{policy_group}")
  Chef::Mixin::DeepMerge.hash_only_merge!(node.role_default, node.role_default[policy_group]) if node.role_default.include?(policy_group)
  Chef::Mixin::DeepMerge.hash_only_merge!(node.role_override, node.role_override[policy_group]) if node.role_override.include?(policy_group)
end
manifest_for(cookbook_name, lock_data) click to toggle source

@api private 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 478
def manifest_for(cookbook_name, lock_data)
  if Chef::Config[:policy_document_native_api]
    artifact_manifest_for(cookbook_name, lock_data)
  else
    compat_mode_manifest_for(cookbook_name, lock_data)
  end
end
original_runlist() click to toggle source

Override #run_list is not supported.

# File lib/chef/policy_builder/policyfile.rb, line 117
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 122
def override_runlist
  nil
end
parse_recipe_spec(recipe_spec) click to toggle source

@api private

# File lib/chef/policy_builder/policyfile.rb, line 284
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

@api private

# File lib/chef/policy_builder/policyfile.rb, line 310
def policy
  @policy ||= api_service.get(policyfile_location)
rescue Net::HTTPServerException => e
  raise ConfigurationError, "Error loading policyfile from `#{policyfile_location}': #{e.class} - #{e.message}"
end
policy_group() click to toggle source

@api private

# File lib/chef/policy_builder/policyfile.rb, line 376
def policy_group
  Chef::Config[:policy_group]
end
policy_group_from_config() click to toggle source

@api private

# File lib/chef/policy_builder/policyfile.rb, line 425
def policy_group_from_config
  Chef::Config[:policy_group]
end
policy_group_from_json_attribs() click to toggle source

@api private

# File lib/chef/policy_builder/policyfile.rb, line 415
def policy_group_from_json_attribs
  json_attribs["policy_group"]
end
policy_group_from_node() click to toggle source

@api private

# File lib/chef/policy_builder/policyfile.rb, line 435
def policy_group_from_node
  node.policy_group
end
policy_name() click to toggle source

@api private

# File lib/chef/policy_builder/policyfile.rb, line 381
def policy_name
  Chef::Config[:policy_name]
end
policy_name_from_config() click to toggle source

@api private

# File lib/chef/policy_builder/policyfile.rb, line 430
def policy_name_from_config
  Chef::Config[:policy_name]
end
policy_name_from_json_attribs() click to toggle source

@api private

# File lib/chef/policy_builder/policyfile.rb, line 420
def policy_name_from_json_attribs
  json_attribs["policy_name"]
end
policy_name_from_node() click to toggle source

@api private

# File lib/chef/policy_builder/policyfile.rb, line 440
def policy_name_from_node
  node.policy_name
end
policyfile_location() click to toggle source

@api private

# File lib/chef/policy_builder/policyfile.rb, line 317
def policyfile_location
  if Chef::Config[:policy_document_native_api]
    validate_policy_config!
    "policy_groups/#{policy_group}/policies/#{policy_name}"
  else
    "data/policyfiles/#{deployment_group}"
  end
end
revision_id() click to toggle source

@api private

# File lib/chef/policy_builder/policyfile.rb, line 492
def revision_id
  policy["revision_id"]
end
run_list() click to toggle source

@api private

# File lib/chef/policy_builder/policyfile.rb, line 299
def run_list
  if named_run_list_requested?
    named_run_list || raise(ConfigurationError,
      "Policy '#{retrieved_policy_name}' revision '#{revision_id}' does not have named_run_list '#{named_run_list_name}'" +
      "(available named_run_lists: [#{available_named_run_lists.join(', ')}])")
  else
    policy["run_list"]
  end
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 132
def run_list_expansion
  run_list_expansion_ish
end
run_list_expansion_ish() click to toggle source

@api private

Sets up a RunListExpansionIsh object so that it can be used in place of a RunListExpansion object, to satisfy the API contract of expand_run_list

# File lib/chef/policy_builder/policyfile.rb, line 257
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

@api private

Generates an array of strings with recipe names including version and identifier info.

# File lib/chef/policy_builder/policyfile.rb, line 243
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
select_policy_name_and_group() click to toggle source

@api private

Selects the `policy_name` and `policy_group` from the following sources in priority order:

  1. JSON attribs (i.e., `-j JSON_FILE`)

  2. `Chef::Config`

  3. The node object

The selected values are then copied to `Chef::Config` and the node.

# File lib/chef/policy_builder/policyfile.rb, line 395
def select_policy_name_and_group
  policy_name_to_set =
    policy_name_from_json_attribs ||
    policy_name_from_config ||
    policy_name_from_node

  policy_group_to_set =
    policy_group_from_json_attribs ||
    policy_group_from_config ||
    policy_group_from_node

  node.policy_name = policy_name_to_set
  node.policy_group = policy_group_to_set
  node.chef_environment = policy_group_to_set

  Chef::Config[:policy_name] = policy_name_to_set
  Chef::Config[:policy_group] = policy_group_to_set
end
setup_run_context(specific_recipes = nil) click to toggle source

Synchronizes cookbooks and initializes the run context object for the run.

@return [Chef::RunContext]

# File lib/chef/policy_builder/policyfile.rb, line 182
def setup_run_context(specific_recipes = nil)
  Chef::Cookbook::FileVendor.fetch_from_remote(api_service)
  sync_cookbooks
  cookbook_collection = Chef::CookbookCollection.new(cookbooks_to_sync)
  cookbook_collection.validate!
  cookbook_collection.install_gems(events)

  run_context = Chef::RunContext.new(node, cookbook_collection, events)

  setup_chef_class(run_context)

  run_context.load(run_list_expansion_ish)

  setup_chef_class(run_context)
  run_context
end
sync_cookbooks() click to toggle source

Synchronizes cookbooks. In a normal chef-client run, this is handled by setup_run_context, but may be called directly in some circumstances.

@return [Hash{String => Chef::CookbookManifest}] A map of

CookbookManifest objects by cookbook name.
# File lib/chef/policy_builder/policyfile.rb, line 218
def sync_cookbooks
  Chef::Log.trace("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.

@return [false]

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

@api private

# File lib/chef/policy_builder/policyfile.rb, line 369
def validate_policy_config!
  raise ConfigurationError, "Setting `policy_group` is not configured." unless policy_group

  raise ConfigurationError, "Setting `policy_name` is not configured." unless policy_name
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 331
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

@api private

# File lib/chef/policy_builder/policyfile.rb, line 354
def validate_recipe_spec(recipe_spec)
  parse_recipe_spec(recipe_spec)
  nil
rescue PolicyfileError => e
  e.message
end

Private Instance Methods

artifact_manifest_for(cookbook_name, lock_data) click to toggle source
# File lib/chef/policy_builder/policyfile.rb, line 550
def artifact_manifest_for(cookbook_name, lock_data)
  identifier = lock_data["identifier"]
  rel_url = "cookbook_artifacts/#{cookbook_name}/#{identifier}"
  inflate_cbv_object(api_service.get(rel_url))
rescue Exception => e
  message = "Error loading cookbook #{cookbook_name} with identifier #{identifier} from #{rel_url}: #{e.class} - #{e.message}"
  err = Chef::Exceptions::CookbookNotFound.new(message)
  err.set_backtrace(e.backtrace)
  raise err
end
available_named_run_lists() click to toggle source
# File lib/chef/policy_builder/policyfile.rb, line 527
def available_named_run_lists
  (policy["named_run_lists"] || {}).keys
end
compat_mode_manifest_for(cookbook_name, lock_data) click to toggle source
# File lib/chef/policy_builder/policyfile.rb, line 539
def compat_mode_manifest_for(cookbook_name, lock_data)
  xyz_version = lock_data["dotted_decimal_identifier"]
  rel_url = "cookbooks/#{cookbook_name}/#{xyz_version}"
  inflate_cbv_object(api_service.get(rel_url))
rescue Exception => e
  message = "Error loading cookbook #{cookbook_name} at version #{xyz_version} from #{rel_url}: #{e.class} - #{e.message}"
  err = Chef::Exceptions::CookbookNotFound.new(message)
  err.set_backtrace(e.backtrace)
  raise err
end
inflate_cbv_object(raw_manifest) click to toggle source
# File lib/chef/policy_builder/policyfile.rb, line 561
def inflate_cbv_object(raw_manifest)
  Chef::CookbookVersion.from_cb_artifact_data(raw_manifest)
end
named_run_list() click to toggle source
# File lib/chef/policy_builder/policyfile.rb, line 523
def named_run_list
  policy["named_run_lists"] && policy["named_run_lists"][named_run_list_name]
end
named_run_list_name() click to toggle source
# File lib/chef/policy_builder/policyfile.rb, line 535
def named_run_list_name
  Chef::Config[:named_run_list]
end
named_run_list_requested?() click to toggle source
# File lib/chef/policy_builder/policyfile.rb, line 531
def named_run_list_requested?
  !!Chef::Config[:named_run_list]
end
retrieved_policy_name() click to toggle source
# File lib/chef/policy_builder/policyfile.rb, line 519
def retrieved_policy_name
  policy["name"]
end
setup_chef_class(run_context) click to toggle source

This method injects the #run_context and into the Chef class.

NOTE: This is duplicated with the ExpandNodeObject implementation. If it gets any more complicated, it needs to be moved elsewhere.

@param #run_context [Chef::RunContext] the #run_context to inject

# File lib/chef/policy_builder/policyfile.rb, line 515
def setup_chef_class(run_context)
  Chef.set_run_context(run_context)
end