class Chef::PolicyBuilder::Policyfile

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

Does not support legacy chef-solo or roles/environments.

Constants

RunListExpansionIsh

Attributes

events[R]
json_attribs[R]
node[R]
node_name[R]
ohai_data[R]
override_runlist[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 80
def initialize(node_name, ohai_data, json_attribs, override_runlist, events)
  @node_name = node_name
  @ohai_data = ohai_data
  @override_runlist = override_runlist
  @json_attribs = json_attribs
  @events = events

  @node = nil

  if Chef::Config[:solo_legacy_mode]
    raise UnsupportedFeature, "Policyfile does not support chef-solo. Use #{ChefUtils::Dist::Infra::CLIENT} local mode instead."
  end

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

Public Instance Methods

api_service() click to toggle source

@api private

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

  unless Chef::Config[:policy_document_native_api]
    Chef.deprecated(:policyfile_compat_mode, "The chef-server 11 policyfile compat mode is deprecated, please set policy_document_native_api to true in your config")
  end

  node.reset_defaults_and_overrides

  node.consume_external_attrs(ohai_data, json_attribs)

  setup_run_list_override

  expand_run_list
  apply_policyfile_attributes

  if persistent_run_list_set?
    Chef::Log.warn("The node.run_list setting is overriding the Policyfile run_list")
  end
  Chef::Log.info("Run List is [#{run_list}]")
  Chef::Log.info("Run List expands to [#{run_list_with_versions_for_display(run_list).join(", ")}]")

  events.node_load_completed(node, run_list_with_versions_for_display(run_list), 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 514
def config
  Chef::Config
end
cookbook_lock_for(cookbook_name) click to toggle source

@api private

# File lib/chef/policy_builder/policyfile.rb, line 298
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 498
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 465
def cookbooks_to_sync
  @cookbook_to_sync ||= begin
    events.cookbook_resolution_start(run_list_with_versions_for_display(run_list))

    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(run_list), e)
  raise
end
deployment_group() click to toggle source

@api private

# File lib/chef/policy_builder/policyfile.rb, line 375
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 198
def expand_run_list
  validate_run_list!(run_list)

  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 110
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 276
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 identifier 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 489
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
parse_recipe_spec(recipe_spec) click to toggle source

@api private

# File lib/chef/policy_builder/policyfile.rb, line 283
def parse_recipe_spec(recipe_spec)
  rmatch = recipe_spec.to_s.match(/recipe\[([^:]+)::([^:]+)\]/)
  if rmatch.nil?
    rmatch = recipe_spec.to_s.match(/recipe\[([^:]+)\]/)
    if rmatch.nil?
      raise PolicyfileError, "invalid recipe specification #{recipe_spec} in Policyfile from #{policyfile_location}"
    else
      [rmatch[1], "default"]
    end
  else
    [rmatch[1], rmatch[2]]
  end
end
policy() click to toggle source

@api private

# File lib/chef/policy_builder/policyfile.rb, line 321
def policy
  @policy ||= api_service.get(policyfile_location)
rescue Net::HTTPClientException => 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 387
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 436
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 426
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 446
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 392
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 441
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 431
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 451
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 328
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 503
def revision_id
  policy["revision_id"]
end
run_list() click to toggle source

@api private @return [Array<String>]

# File lib/chef/policy_builder/policyfile.rb, line 304
def run_list
  return override_runlist.map(&:to_s) if override_runlist

  if json_attribs["run_list"]
    json_attribs["run_list"]
  elsif persistent_run_list_set?
    node.run_list
  elsif 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 104
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 256
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(run_list) 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 242
def run_list_with_versions_for_display(run_list)
  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 406
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, run_context = 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 164
def setup_run_context(specific_recipes = nil, run_context = nil)
  run_context ||= Chef::RunContext.new
  run_context.node = node
  run_context.events = events

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

  setup_chef_class(run_context)

  events.cookbook_compilation_start(run_context)

  run_context.load(run_list_expansion_ish)
  if specific_recipes
    specific_recipes.each do |recipe_file|
      run_context.load_recipe_file(recipe_file)
    end
  end

  events.cookbook_compilation_complete(run_context)

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

Indicates whether the policy is temporary, which means an override_runlist was provided. Chef::Client uses this to decide whether to do the final node save at the end of the run or not.

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

@api private

# File lib/chef/policy_builder/policyfile.rb, line 380
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 minimal 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 342
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.is_a?(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 365
def validate_recipe_spec(recipe_spec)
  parse_recipe_spec(recipe_spec)
  nil
rescue PolicyfileError => e
  e.message
end
validate_run_list!(run_list) click to toggle source

@api private

Validate run_list against policyfile cookbooks

# File lib/chef/policy_builder/policyfile.rb, line 230
def validate_run_list!(run_list)
  run_list.map do |recipe_spec|
    cookbook, recipe = parse_recipe_spec(recipe_spec)
    lock_data = cookbook_lock_for(cookbook)
    raise PolicyfileError, "invalid run_list item '#{recipe_spec}' not in cookbook set of PolicyFile #{policyfile_location}" unless lock_data
  end
end

Private Instance Methods

artifact_manifest_for(cookbook_name, lock_data) click to toggle source
# File lib/chef/policy_builder/policyfile.rb, line 572
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 545
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 561
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 583
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 541
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 557
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 553
def named_run_list_requested?
  !!Chef::Config[:named_run_list]
end
persistent_run_list_set?() click to toggle source
# File lib/chef/policy_builder/policyfile.rb, line 549
def persistent_run_list_set?
  Chef::Config[:policy_persist_run_list] && node.run_list && !node.run_list.empty?
end
retrieved_policy_name() click to toggle source
# File lib/chef/policy_builder/policyfile.rb, line 537
def retrieved_policy_name
  policy["name"]
end
runlist_override_sanity_check!() click to toggle source

Ensures runlist override contains RunListItem instances

# File lib/chef/policy_builder/policyfile.rb, line 598
def runlist_override_sanity_check!
  # Convert to array and remove whitespace
  if override_runlist.is_a?(String)
    @override_runlist = override_runlist.split(",").map(&:strip)
  end
  @override_runlist = [override_runlist].flatten.compact
  override_runlist.map! do |item|
    if item.is_a?(Chef::RunList::RunListItem)
      item
    else
      Chef::RunList::RunListItem.new(item)
    end
  end
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 533
def setup_chef_class(run_context)
  Chef.set_run_context(run_context)
end
setup_run_list_override() click to toggle source
# File lib/chef/policy_builder/policyfile.rb, line 587
def setup_run_list_override
  unless override_runlist.nil?
    runlist_override_sanity_check!
    node.override_runlist = override_runlist
    Chef::Log.warn "Run List override has been provided."
    Chef::Log.warn "Original Run List: [#{node.primary_runlist}]"
    Chef::Log.warn "Overridden Run List: [#{node.run_list}]"
  end
end