class ChefDK::PolicyfileLock

Constants

RUN_LIST_ITEM_FORMAT

Attributes

cookbook_locks[R]
default_attributes[RW]
included_policy_locks[R]
install_report[R]
name[RW]
named_run_lists[RW]
override_attributes[RW]
run_list[RW]
solution_dependencies[R]
storage_config[R]

Public Class Methods

build(storage_config) { |lock| ... } click to toggle source
# File lib/chef-dk/policyfile_lock.rb, line 69
def self.build(storage_config)
  lock = new(storage_config)
  yield lock
  lock
end
build_from_compiler(compiler, storage_config) click to toggle source
# File lib/chef-dk/policyfile_lock.rb, line 75
def self.build_from_compiler(compiler, storage_config)
  lock = new(storage_config)
  lock.build_from_compiler(compiler)
  lock
end
new(storage_config, ui: nil) click to toggle source
# File lib/chef-dk/policyfile_lock.rb, line 99
def initialize(storage_config, ui: nil)
  @name = nil
  @run_list = []
  @named_run_lists = {}
  @cookbook_locks = {}
  @relative_paths_root = Dir.pwd
  @storage_config = storage_config
  @ui = ui || UI.null

  @default_attributes = {}
  @override_attributes = {}

  @solution_dependencies = Policyfile::SolutionDependencies.new

  @included_policy_locks = []

  @install_report = InstallReport.new(ui: @ui, policyfile_lock: self)
end

Public Instance Methods

build_from_archive(lock_data) click to toggle source
# File lib/chef-dk/policyfile_lock.rb, line 279
def build_from_archive(lock_data)
  set_name_from_lock_data(lock_data)
  set_run_list_from_lock_data(lock_data)
  set_named_run_lists_from_lock_data(lock_data)
  set_cookbook_locks_as_archives_from_lock_data(lock_data)
  set_attributes_from_lock_data(lock_data)
  set_solution_dependencies_from_lock_data(lock_data)
  set_included_policy_locks_from_lock_data(lock_data)
  self
end
build_from_compiler(compiler) click to toggle source
# File lib/chef-dk/policyfile_lock.rb, line 230
def build_from_compiler(compiler)
  @name = compiler.name

  @run_list = compiler.normalized_run_list

  @named_run_lists = compiler.normalized_named_run_lists

  compiler.all_cookbook_location_specs.each do |cookbook_name, spec|
    if spec.mirrors_canonical_upstream?
      cached_cookbook(cookbook_name) do |cached_cb|
        cached_cb.cache_key = spec.cache_key
        cached_cb.origin = spec.uri
        cached_cb.source_options = spec.source_options_for_lock
      end
    else
      local_cookbook(cookbook_name) do |local_cb|
        local_cb.source = spec.relative_path
        local_cb.source_options = spec.source_options_for_lock
      end
    end
  end

  @default_attributes = compiler.default_attributes
  @override_attributes = compiler.override_attributes

  @solution_dependencies = compiler.solution_dependencies

  @included_policy_locks = compiler.included_policies.map do |policy|
    {
      "name" => policy.name,
      "revision_id" => policy.revision_id,
      "source_options" => policy.source_options_for_lock,
    }
  end

  self
end
build_from_lock_data(lock_data) click to toggle source
# File lib/chef-dk/policyfile_lock.rb, line 268
def build_from_lock_data(lock_data)
  set_name_from_lock_data(lock_data)
  set_run_list_from_lock_data(lock_data)
  set_named_run_lists_from_lock_data(lock_data)
  set_cookbook_locks_from_lock_data(lock_data)
  set_attributes_from_lock_data(lock_data)
  set_solution_dependencies_from_lock_data(lock_data)
  set_included_policy_locks_from_lock_data(lock_data)
  self
end
cached_cookbook(name) { |cached_cookbook| ... } click to toggle source
# File lib/chef-dk/policyfile_lock.rb, line 122
def cached_cookbook(name)
  cached_cookbook = Policyfile::CachedCookbook.new(name, storage_config)
  yield cached_cookbook if block_given?
  @cookbook_locks[name] = cached_cookbook
end
canonical_revision_string() click to toggle source

Generates a string representation of the lock data in a specialized format suitable for generating a checksum of the lock itself. Only data that modifies the behavior of a chef-client using the lockfile is included in this format; for example, a modification to the source options in a `Policyfile.rb` that yields identical code (such as switching to a github fork at the same revision) will not cause a change in the PolicyfileLock's canonical_revision_string.

This format is intended to be used only for generating an identifier for a particular revision of a PolicyfileLock. It should not be used as a serialization format, and is not guaranteed to be a stable interface.

# File lib/chef-dk/policyfile_lock.rb, line 169
def canonical_revision_string
  canonical_rev_text = ""

  canonical_rev_text << "name:#{name}\n"

  run_list.each do |item|
    canonical_rev_text << "run-list-item:#{item}\n"
  end

  named_run_lists.each do |name, run_list|
    run_list.each do |item|
      canonical_rev_text << "named-run-list:#{name};run-list-item:#{item}\n"
    end
  end

  cookbook_locks_for_lockfile.each do |name, lock|
    canonical_rev_text << "cookbook:#{name};id:#{lock["identifier"]}\n"
  end

  canonical_rev_text << "default_attributes:#{canonicalize(default_attributes)}\n"

  canonical_rev_text << "override_attributes:#{canonicalize(override_attributes)}\n"

  canonical_rev_text
end
cookbook_locks_for_lockfile() click to toggle source
# File lib/chef-dk/policyfile_lock.rb, line 195
def cookbook_locks_for_lockfile
  cookbook_locks.inject({}) do |locks_map, (name, location_spec)|
    location_spec.validate!
    location_spec.gather_profile_data
    locks_map[name] = location_spec.to_lock
    locks_map
  end.sort.to_h
end
dependencies() { |solution_dependencies| ... } click to toggle source
# File lib/chef-dk/policyfile_lock.rb, line 134
def dependencies
  yield solution_dependencies
end
ensure_cache_dir_exists() click to toggle source
# File lib/chef-dk/policyfile_lock.rb, line 300
def ensure_cache_dir_exists
  # note: duplicates PolicyfileCompiler#ensure_cache_dir_exists
  unless File.exist?(cache_path)
    FileUtils.mkdir_p(cache_path)
  end
end
install_cookbooks() click to toggle source
# File lib/chef-dk/policyfile_lock.rb, line 290
def install_cookbooks
  # note: duplicates PolicyfileCompiler#ensure_cache_dir_exists
  ensure_cache_dir_exists

  cookbook_locks.each do |cookbook_name, cookbook_lock|
    install_report.installing_cookbook(cookbook_lock)
    cookbook_lock.install_locked
  end
end
local_cookbook(name) { |local_cookbook| ... } click to toggle source
# File lib/chef-dk/policyfile_lock.rb, line 128
def local_cookbook(name)
  local_cookbook = Policyfile::LocalCookbook.new(name, storage_config)
  yield local_cookbook if block_given?
  @cookbook_locks[name] = local_cookbook
end
lock_data_for(cookbook_name) click to toggle source
# File lib/chef-dk/policyfile_lock.rb, line 118
def lock_data_for(cookbook_name)
  @cookbook_locks[cookbook_name]
end
revision_id() click to toggle source

Returns a fingerprint of the PolicyfileLock by computing the SHA1 hash of canonical_revision_string

# File lib/chef-dk/policyfile_lock.rb, line 154
def revision_id
  Digest::SHA256.new.hexdigest(canonical_revision_string)
end
to_lock() click to toggle source
# File lib/chef-dk/policyfile_lock.rb, line 138
def to_lock
  {}.tap do |lock|
    lock["revision_id"] = revision_id
    lock["name"] = name
    lock["run_list"] = run_list
    lock["named_run_lists"] = named_run_lists unless named_run_lists.empty?
    lock["included_policy_locks"] = included_policy_locks
    lock["cookbook_locks"] = cookbook_locks_for_lockfile
    lock["default_attributes"] = default_attributes
    lock["override_attributes"] = override_attributes
    lock["solution_dependencies"] = solution_dependencies.to_lock
  end
end
validate_cookbooks!() click to toggle source
# File lib/chef-dk/policyfile_lock.rb, line 204
def validate_cookbooks!
  cookbook_locks.each do |name, cookbook_lock|
    cookbook_lock.validate!
    cookbook_lock.refresh!
  end

  # Check that versions and dependencies are still valid. First we need to
  # refresh the dependency info for everything that has changed, then we
  # check that the new versions and dependencies are valid for the working
  # set of cookbooks. We can't do this in a single loop because the user
  # may have modified two cookbooks such that the versions and constraints
  # are only valid when both changes are considered together.
  cookbook_locks.each do |name, cookbook_lock|
    if cookbook_lock.updated?
      solution_dependencies.update_cookbook_dep(name, cookbook_lock.version, cookbook_lock.dependencies)
    end
  end
  cookbook_locks.each do |name, cookbook_lock|
    if cookbook_lock.updated?
      solution_dependencies.test_conflict!(cookbook_lock.name, cookbook_lock.version)
    end
  end

  true
end

Private Instance Methods

build_cookbook_lock_as_archive_from_lock_data(name, lock_info) click to toggle source
# File lib/chef-dk/policyfile_lock.rb, line 563
def build_cookbook_lock_as_archive_from_lock_data(name, lock_info)
  unless lock_info.is_a?(Hash)
    raise InvalidLockfile, "lockfile cookbook_locks entries must be a Hash (JSON object). (got: #{lock_info.inspect})"
  end

  if lock_info["cache_key"].nil?
    local_cookbook = Policyfile::LocalCookbook.new(name, storage_config)
    local_cookbook.build_from_lock_data(lock_info)
    archived = Policyfile::ArchivedCookbook.new(local_cookbook, storage_config)
    @cookbook_locks[name] = archived
  else
    cached_cookbook = Policyfile::CachedCookbook.new(name, storage_config)
    cached_cookbook.build_from_lock_data(lock_info)
    archived = Policyfile::ArchivedCookbook.new(cached_cookbook, storage_config)
    @cookbook_locks[name] = archived
  end
end
build_cookbook_lock_from_lock_data(name, lock_info) click to toggle source
# File lib/chef-dk/policyfile_lock.rb, line 551
def build_cookbook_lock_from_lock_data(name, lock_info)
  unless lock_info.is_a?(Hash)
    raise InvalidLockfile, "lockfile cookbook_locks entries must be a Hash (JSON object). (got: #{lock_info.inspect})"
  end

  if lock_info["cache_key"].nil?
    local_cookbook(name).build_from_lock_data(lock_info)
  else
    cached_cookbook(name).build_from_lock_data(lock_info)
  end
end
canonicalize(attributes) click to toggle source

Generates a canonical JSON representation of the attributes. Based on wiki.laptop.org/go/Canonical_JSON but not quite as strict, yet.

In particular:

  • String encoding stuff isn't normalized

  • We allow floats that fit within the range/precision requirements of IEEE 754-2008 binary64 (double precision) numbers.

  • +/- Infinity and NaN are banned, but float/numeric size aren't checked. numerics should be in range [-(2**53)+1, (2**53)-1] to comply with IEEE 754-2008

Recursive, so absurd nesting levels could cause a SystemError. Invalid input will cause an InvalidPolicyfileAttribute exception.

# File lib/chef-dk/policyfile_lock.rb, line 322
def canonicalize(attributes)
  unless attributes.is_a?(Hash)
    raise "Top level attributes must be a Hash (you gave: #{attributes})"
  end

  canonicalize_elements(attributes)
end
canonicalize_elements(item) click to toggle source
# File lib/chef-dk/policyfile_lock.rb, line 330
def canonicalize_elements(item)
  case item
  when Hash
    # Hash keys will sort differently based on the encoding, but after a
    # JSON round trip everything will be UTF-8, so we have to normalize the
    # keys to UTF-8 first so that the sort order uses the UTF-8 strings.
    item_with_normalized_keys = item.inject({}) do |normalized_item, (key, value)|
      validate_attr_key(key)
      normalized_item[key.encode("utf-8")] = value
      normalized_item
    end
    elements = item_with_normalized_keys.keys.sort.map do |key|
      k = '"' << key << '":'
      v = canonicalize_elements(item_with_normalized_keys[key])
      k << v
    end
    "{" << elements.join(",") << "}"
  when String
    '"' << item.encode("utf-8") << '"'
  when Array
    elements = item.map { |i| canonicalize_elements(i) }
    "[" << elements.join(",") << "]"
  when Integer
    item.to_s
  when Float
    unless item.finite?
      raise InvalidPolicyfileAttribute, "Floating point numbers cannot be infinite or NaN. You gave #{item.inspect}"
    end

    # Support for floats assumes that any implementation of our JSON
    # canonicalization routine will use IEEE-754 doubles. In decimal terms,
    # doubles give 15-17 digits of precision, so we err on the safe side
    # and only use 15 digits in the string conversion. We use the `g`
    # format, which is a documented-enough "do what I mean" where floats
    # >= 0.1 and < precsion are represented as floating point literals, and
    # other numbers use the exponent notation with a lowercase 'e'. Note
    # that both Ruby and Erlang document what their `g` does but have some
    # differences both subtle and non-subtle:
    #
    # ```ruby
    # format("%.15g", 0.1) #=> "0.1"
    # format("%.15g", 1_000_000_000.0) #=> "1000000000"
    # ```
    #
    # Whereas:
    #
    # ```erlang
    # lists:flatten(io_lib:format("~.15g", [0.1])). %=> "0.100000000000000"
    # lists:flatten(io_lib:format("~.15e", [1000000000.0])). %=> "1.00000000000000e+9"
    # ```
    #
    # Other implementations should normalize to ruby's %.15g behavior.
    Kernel.format("%.15g", item)
  when NilClass
    "null"
  when TrueClass
    "true"
  when FalseClass
    "false"
  else
    raise InvalidPolicyfileAttribute,
      "Invalid type in attributes. Only Hash, Array, String, Integer, Float, true, false, and nil are accepted. You gave #{item.inspect} (#{item.class})"
  end
end
set_attributes_from_lock_data(lock_data) click to toggle source
# File lib/chef-dk/policyfile_lock.rb, line 497
def set_attributes_from_lock_data(lock_data)
  default_attr_data = lock_data["default_attributes"]

  if default_attr_data.nil?
    raise InvalidLockfile, "lockfile does not have a `default_attributes` attribute"
  end

  unless default_attr_data.is_a?(Hash)
    raise InvalidLockfile, "lockfile's `default_attributes` attribute must be a Hash (JSON object). (got: #{default_attr_data.inspect})"
  end

  override_attr_data = lock_data["override_attributes"]

  if override_attr_data.nil?
    raise InvalidLockfile, "lockfile does not have a `override_attributes` attribute"
  end

  unless override_attr_data.is_a?(Hash)
    raise InvalidLockfile, "lockfile's `override_attributes` attribute must be a Hash (JSON object). (got: #{override_attr_data.inspect})"
  end

  @default_attributes   = default_attr_data
  @override_attributes  = override_attr_data
end
set_cookbook_locks_as_archives_from_lock_data(lock_data) click to toggle source
# File lib/chef-dk/policyfile_lock.rb, line 481
def set_cookbook_locks_as_archives_from_lock_data(lock_data)
  cookbook_lock_data = lock_data["cookbook_locks"]

  if cookbook_lock_data.nil?
    raise InvalidLockfile, "lockfile does not have a cookbook_locks attribute"
  end

  unless cookbook_lock_data.is_a?(Hash)
    raise InvalidLockfile, "lockfile's cookbook_locks attribute must be a Hash (JSON object). (got: #{cookbook_lock_data.inspect})"
  end

  lock_data["cookbook_locks"].each do |name, lock_info|
    build_cookbook_lock_as_archive_from_lock_data(name, lock_info)
  end
end
set_cookbook_locks_from_lock_data(lock_data) click to toggle source
# File lib/chef-dk/policyfile_lock.rb, line 465
def set_cookbook_locks_from_lock_data(lock_data)
  cookbook_lock_data = lock_data["cookbook_locks"]

  if cookbook_lock_data.nil?
    raise InvalidLockfile, "lockfile does not have a cookbook_locks attribute"
  end

  unless cookbook_lock_data.is_a?(Hash)
    raise InvalidLockfile, "lockfile's cookbook_locks attribute must be a Hash (JSON object). (got: #{cookbook_lock_data.inspect})"
  end

  lock_data["cookbook_locks"].each do |name, lock_info|
    build_cookbook_lock_from_lock_data(name, lock_info)
  end
end
set_included_policy_locks_from_lock_data(lock_data) click to toggle source
# File lib/chef-dk/policyfile_lock.rb, line 537
def set_included_policy_locks_from_lock_data(lock_data)
  locks = lock_data["included_policy_locks"]
  if locks.nil?
    @included_policy_locks = []
  else
    locks.each do |lock_info|
      unless %w{revision_id name source_options}.all? { |key| !lock_info[key].nil? }
        raise InvalidLockfile, "lockfile included policy missing one of the required keys"
      end
    end
    @included_policy_locks = locks
  end
end
set_name_from_lock_data(lock_data) click to toggle source
# File lib/chef-dk/policyfile_lock.rb, line 402
def set_name_from_lock_data(lock_data)
  name_attribute = lock_data["name"]

  raise InvalidLockfile, "lockfile does not have a `name' attribute" if name_attribute.nil?

  unless name_attribute.is_a?(String)
    raise InvalidLockfile, "lockfile's name attribute must be a String (got: #{name_attribute.inspect})"
  end

  if name_attribute.empty?
    raise InvalidLockfile, "lockfile's name attribute cannot be an empty string"
  end

  @name = name_attribute
end
set_named_run_lists_from_lock_data(lock_data) click to toggle source
# File lib/chef-dk/policyfile_lock.rb, line 437
def set_named_run_lists_from_lock_data(lock_data)
  return unless lock_data.key?("named_run_lists")

  lock_data_named_run_lists = lock_data["named_run_lists"]

  unless lock_data_named_run_lists.is_a?(Hash)
    msg = "lockfile's named_run_lists must be a Hash (JSON object). (got: #{lock_data_named_run_lists.inspect})"
    raise InvalidLockfile, msg
  end

  lock_data_named_run_lists.each do |name, run_list|
    unless name.is_a?(String)
      msg = "Keys in lockfile's named_run_lists must be Strings. (got: #{name.inspect})"
      raise InvalidLockfile, msg
    end
    unless run_list.is_a?(Array)
      msg = "Values in lockfile's named_run_lists must be Arrays. (got: #{run_list.inspect})"
      raise InvalidLockfile, msg
    end
    bad_run_list_items = run_list.select { |e| e !~ RUN_LIST_ITEM_FORMAT }
    unless bad_run_list_items.empty?
      msg = "lockfile's run_list items must be formatted like `recipe[$COOKBOOK_NAME::$RECIPE_NAME]'. Invalid items: `#{bad_run_list_items.join("' `")}'"
      raise InvalidLockfile, msg
    end
  end
  @named_run_lists = lock_data_named_run_lists
end
set_run_list_from_lock_data(lock_data) click to toggle source
# File lib/chef-dk/policyfile_lock.rb, line 418
def set_run_list_from_lock_data(lock_data)
  run_list_attribute = lock_data["run_list"]

  raise InvalidLockfile, "lockfile does not have a run_list attribute" if run_list_attribute.nil?

  unless run_list_attribute.is_a?(Array)
    raise InvalidLockfile, "lockfile's run_list must be an array of run list items (got: #{run_list_attribute.inspect})"
  end

  bad_run_list_items = run_list_attribute.select { |e| e !~ RUN_LIST_ITEM_FORMAT }

  unless bad_run_list_items.empty?
    msg = "lockfile's run_list items must be formatted like `recipe[$COOKBOOK_NAME::$RECIPE_NAME]'. Invalid items: `#{bad_run_list_items.join("' `")}'"
    raise InvalidLockfile, msg
  end

  @run_list = run_list_attribute
end
set_solution_dependencies_from_lock_data(lock_data) click to toggle source
# File lib/chef-dk/policyfile_lock.rb, line 522
def set_solution_dependencies_from_lock_data(lock_data)
  soln_deps = lock_data["solution_dependencies"]

  if soln_deps.nil?
    raise InvalidLockfile, "lockfile does not have a solution_dependencies attribute"
  end

  unless soln_deps.is_a?(Hash)
    raise InvalidLockfile, "lockfile's solution_dependencies attribute must be a Hash (JSON object). (got: #{soln_deps.inspect})"
  end

  s = Policyfile::SolutionDependencies.from_lock(lock_data["solution_dependencies"])
  @solution_dependencies = s
end
validate_attr_key(key) click to toggle source
# File lib/chef-dk/policyfile_lock.rb, line 395
def validate_attr_key(key)
  unless key.is_a?(String)
    raise InvalidPolicyfileAttribute,
      "Attribute keys must be Strings (other types are not allowed in JSON). You gave: #{key.inspect} (#{key.class})"
  end
end