class ChefCLI::Policyfile::SolutionDependencies
def self.from_lock(lock_data)
def self.from_lock(lock_data) new.tap { |e| e.consume_lock_data(lock_data) } end
def add_cookbook_dep(cookbook_name, version, dependency_list)
def add_cookbook_dep(cookbook_name, version, dependency_list) cookbook = Cookbook.new(cookbook_name, version) add_cookbook_obj_dep(cookbook, dependency_list) end
def add_cookbook_dep_from_lock_data(name_and_version, deps_list)
def add_cookbook_dep_from_lock_data(name_and_version, deps_list) unless name_and_version.is_a?(String) show = "#{name_and_version.inspect} => #{deps_list.inspect}" msg = %Q{lockfile cookbook_dependencies entries must be of the form "$COOKBOOK_NAME ($VERSION)" => [ $dependency, ...] (got: #{show}) } raise InvalidLockfile, msg end unless Cookbook.valid_str?(name_and_version) msg = %Q{lockfile cookbook_dependencies entry keys must be of the form "$COOKBOOK_NAME ($VERSION)" (got: #{name_and_version.inspect}) } raise InvalidLockfile, msg end unless deps_list.is_a?(Array) msg = %Q{lockfile cookbook_dependencies entry values must be an Array like [ [ "$COOKBOOK_NAME", "$CONSTRAINT" ], ... ] (got: #{deps_list.inspect}) } raise InvalidLockfile, msg end deps_list.each do |entry| unless entry.is_a?(Array) && entry.size == 2 msg = %Q{lockfile solution_dependencies dependencies entry must be like [ "$COOKBOOK_NAME", "$CONSTRAINT" ] (got: #{entry.inspect})} raise InvalidLockfile, msg end dep_name, constraint = entry unless dep_name.is_a?(String) && !dep_name.empty? msg = "malformed lockfile solution_dependencies dependencies entry. Cookbook name portion must be a string (got: #{entry.inspect})" raise InvalidLockfile, msg end unless constraint.is_a?(String) && !constraint.empty? msg = "malformed lockfile solution_dependencies dependencies entry. Version constraint portion must be a string (got: #{entry.inspect})" raise InvalidLockfile, msg end end cookbook = Cookbook.parse(name_and_version) add_cookbook_obj_dep(cookbook, deps_list) end
def add_cookbook_obj_dep(cookbook, dependency_map)
def add_cookbook_obj_dep(cookbook, dependency_map) @cookbook_dependencies[cookbook] = dependency_map.map do |dep_name, constraint| [ dep_name, Semverse::Constraint.new(constraint) ] end end
def add_policyfile_dep(cookbook, constraint)
def add_policyfile_dep(cookbook, constraint) @policyfile_dependencies << [ cookbook, Semverse::Constraint.new(constraint) ] end
def add_policyfile_dep_from_lock_data(entry)
def add_policyfile_dep_from_lock_data(entry) unless entry.is_a?(Array) && entry.size == 2 msg = %Q{lockfile solution_dependencies Policyfile dependencies entry must be like [ "$COOKBOOK_NAME", "$CONSTRAINT" ] (got: #{entry.inspect})} raise InvalidLockfile, msg end cookbook_name, constraint = entry unless cookbook_name.is_a?(String) && !cookbook_name.empty? msg = "lockfile solution_dependencies Policyfile dependencies entry. Cookbook name portion must be a string (got: #{entry.inspect})" raise InvalidLockfile, msg end unless constraint.is_a?(String) && !constraint.empty? msg = "malformed lockfile solution_dependencies Policyfile dependencies entry. Version constraint portion must be a string (got: #{entry.inspect})" raise InvalidLockfile, msg end add_policyfile_dep(cookbook_name, constraint) rescue Semverse::InvalidConstraintFormat msg = "malformed lockfile solution_dependencies Policyfile dependencies entry. Version constraint portion must be a valid version constraint (got: #{entry.inspect})" raise InvalidLockfile, msg end
def assert_cookbook_deps_valid!(cookbook_name, version)
def assert_cookbook_deps_valid!(cookbook_name, version) dependency_conflicts = cookbook_deps_conflicts_for(cookbook_name, version) return false if dependency_conflicts.empty? message = "Cookbook #{cookbook_name} (#{version}) has dependency constraints that cannot be met by the existing cookbook set:\n" full_message = message + dependency_conflicts.join("\n") raise DependencyConflict, full_message end
def assert_cookbook_version_valid!(cookbook_name, version)
def assert_cookbook_version_valid!(cookbook_name, version) policyfile_conflicts = policyfile_conflicts_with(cookbook_name, version) cookbook_conflicts = cookbook_conflicts_with(cookbook_name, version) all_conflicts = policyfile_conflicts + cookbook_conflicts return false if all_conflicts.empty? details = all_conflicts.map { |source, name, constraint| "#{source} depends on #{name} #{constraint}" } message = "Cookbook #{cookbook_name} (#{version}) conflicts with other dependencies:\n" full_message = message + details.join("\n") raise DependencyConflict, full_message end
def consume_lock_data(lock_data)
def consume_lock_data(lock_data) unless lock_data.key?("Policyfile") && lock_data.key?("dependencies") msg = %Q|lockfile solution_dependencies must be a Hash of the form `{"Policyfile": [], "dependencies": {} }' (got: #{lock_data.inspect})| raise InvalidLockfile, msg end set_policyfile_deps_from_lock_data(lock_data) set_cookbook_deps_from_lock_data(lock_data) end
def cookbook_conflicts_with(cookbook_name, version)
def cookbook_conflicts_with(cookbook_name, version) cookbook_conflicts = [] @cookbook_dependencies.each do |top_level_dep_name, dependencies| dependencies.each do |dep_name, constraint| if dep_name == cookbook_name && !constraint.satisfies?(version) cookbook_conflicts << [top_level_dep_name, dep_name, constraint] end end end cookbook_conflicts end
def cookbook_deps_conflicts_for(cookbook_name, version)
def cookbook_deps_conflicts_for(cookbook_name, version) conflicts = [] transitive_deps = find_cookbook_dep_by_name_and_version(cookbook_name, version) transitive_deps.each do |name, constraint| existing_cookbook = find_cookbook_dep_by_name(name) if existing_cookbook.nil? conflicts << "Cookbook #{name} isn't included in the existing cookbook set." elsif !constraint.satisfies?(existing_cookbook[0].version) conflicts << "Dependency on #{name} #{constraint} conflicts with existing version #{existing_cookbook[0]}" end end conflicts end
def cookbook_deps_for_lock
def cookbook_deps_for_lock cookbook_dependencies.inject({}) do |map, (cookbook, deps)| map[cookbook.to_s] = deps.map do |name, constraint| [ name, constraint.to_s ] end map end.sort.to_h end
def find_cookbook_dep_by_name(name)
def find_cookbook_dep_by_name(name) @cookbook_dependencies.find { |k, v| k.name == name } end
def find_cookbook_dep_by_name_and_version(name, version)
def find_cookbook_dep_by_name_and_version(name, version) @cookbook_dependencies[Cookbook.new(name, version)] end
def have_cookbook_dep?(name, version)
def have_cookbook_dep?(name, version) @cookbook_dependencies.key?(Cookbook.new(name, version)) end
def initialize
def initialize @policyfile_dependencies = [] @cookbook_dependencies = {} end
def policyfile_conflicts_with(cookbook_name, version)
def policyfile_conflicts_with(cookbook_name, version) policyfile_conflicts = [] @policyfile_dependencies.each do |dep_name, constraint| if dep_name == cookbook_name && !constraint.satisfies?(version) policyfile_conflicts << ["Policyfile", dep_name, constraint] end end policyfile_conflicts end
def policyfile_dependencies_for_lock
def policyfile_dependencies_for_lock policyfile_dependencies.map do |name, constraint| [ name, constraint.to_s ] end.sort end
def set_cookbook_deps_from_lock_data(lock_data)
def set_cookbook_deps_from_lock_data(lock_data) cookbook_dependencies_data = lock_data["dependencies"] unless cookbook_dependencies_data.is_a?(Hash) msg = "lockfile solution_dependencies dependencies entry must be a Hash (JSON object) of dependencies (got: #{cookbook_dependencies_data.inspect})" raise InvalidLockfile, msg end cookbook_dependencies_data.each do |name_and_version, deps_list| add_cookbook_dep_from_lock_data(name_and_version, deps_list) end end
def set_policyfile_deps_from_lock_data(lock_data)
def set_policyfile_deps_from_lock_data(lock_data) policyfile_deps_data = lock_data["Policyfile"] unless policyfile_deps_data.is_a?(Array) msg = "lockfile solution_dependencies Policyfile dependencies must be an array of cookbooks and constraints (got: #{policyfile_deps_data.inspect})" raise InvalidLockfile, msg end policyfile_deps_data.each do |entry| add_policyfile_dep_from_lock_data(entry) end end
def test_conflict!(cookbook_name, version)
def test_conflict!(cookbook_name, version) unless have_cookbook_dep?(cookbook_name, version) raise CookbookNotInWorkingSet, "Cookbook #{cookbook_name} (#{version}) not in the working set, cannot test for conflicts" end assert_cookbook_version_valid!(cookbook_name, version) assert_cookbook_deps_valid!(cookbook_name, version) end
def to_lock
def to_lock { "Policyfile" => policyfile_dependencies_for_lock, "dependencies" => cookbook_deps_for_lock } end
def transitive_deps(names)
def transitive_deps(names) deps = Set.new to_explore = names.dup until to_explore.empty? ck_name = to_explore.shift next unless deps.add?(ck_name) # explore each ck only once my_deps = find_cookbook_dep_by_name(ck_name) dep_names = my_deps[1].map(&:first) to_explore += dep_names end deps.to_a.sort end
def update_cookbook_dep(cookbook_name, new_version, new_dependency_list)
def update_cookbook_dep(cookbook_name, new_version, new_dependency_list) @cookbook_dependencies.delete_if { |cb, _deps| cb.name == cookbook_name } add_cookbook_dep(cookbook_name, new_version, new_dependency_list) end