# 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:¶ ↑

- #override_runlist

    This could potentially be integrated into the

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

- specific_recipes

    put more design thought into this use case.

- #run_list in #json_attribs

    would be ignored anyway, so it raises an error.

- chef-solo

    not currently supported. Need more design thought around

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