lib/chef-dk/policyfile/solution_dependencies.rb in chef-dk-3.9.0 vs lib/chef-dk/policyfile/solution_dependencies.rb in chef-dk-3.10.1

- old
+ new

@@ -1,311 +1,311 @@ -# -# Copyright:: Copyright (c) 2014-2018, Chef Software Inc. -# License:: Apache License, Version 2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -require "semverse" -require "set" -require "chef-dk/exceptions" - -module ChefDK - module Policyfile - - class SolutionDependencies - - Cookbook = Struct.new(:name, :version) - - class Cookbook - - VALID_STRING_FORMAT = /\A[^\s]+ \([^\s]+\)\Z/ - - def self.valid_str?(str) - !!(str =~ VALID_STRING_FORMAT) - end - - def self.parse(str) - name, version_w_parens = str.split(" ") - version = version_w_parens[/\(([^)]+)\)/, 1] - new(name, version) - end - - def to_s - "#{name} (#{version})" - end - - def eql?(other) - other.kind_of?(self.class) && - other.name == name && - other.version == version - end - - def hash - [name, version].hash - end - - end - - def self.from_lock(lock_data) - new.tap { |e| e.consume_lock_data(lock_data) } - end - - attr_reader :policyfile_dependencies - - attr_reader :cookbook_dependencies - - def initialize - @policyfile_dependencies = [] - @cookbook_dependencies = {} - end - - def add_policyfile_dep(cookbook, constraint) - @policyfile_dependencies << [ cookbook, Semverse::Constraint.new(constraint) ] - end - - def add_cookbook_dep(cookbook_name, version, dependency_list) - cookbook = Cookbook.new(cookbook_name, version) - add_cookbook_obj_dep(cookbook, dependency_list) - end - - 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 - - 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 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 - { "Policyfile" => policyfile_dependencies_for_lock, "dependencies" => cookbook_deps_for_lock } - end - - def policyfile_dependencies_for_lock - policyfile_dependencies.map do |name, constraint| - [ name, constraint.to_s ] - end.sort - end - - 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 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 - - private - - 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 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 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 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 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) - 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 have_cookbook_dep?(name, version) - @cookbook_dependencies.key?(Cookbook.new(name, version)) - end - - 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) - @cookbook_dependencies[Cookbook.new(name, version)] - end - - def set_policyfile_deps_from_lock_data(lock_data) - policyfile_deps_data = lock_data["Policyfile"] - - unless policyfile_deps_data.kind_of?(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 add_policyfile_dep_from_lock_data(entry) - unless entry.kind_of?(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.kind_of?(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.kind_of?(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 set_cookbook_deps_from_lock_data(lock_data) - cookbook_dependencies_data = lock_data["dependencies"] - - unless cookbook_dependencies_data.kind_of?(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 add_cookbook_dep_from_lock_data(name_and_version, deps_list) - unless name_and_version.kind_of?(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.kind_of?(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.kind_of?(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.kind_of?(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.kind_of?(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 - - end - - end -end +# +# Copyright:: Copyright (c) 2014-2018, Chef Software Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require "semverse" +require "set" +require "chef-dk/exceptions" + +module ChefDK + module Policyfile + + class SolutionDependencies + + Cookbook = Struct.new(:name, :version) + + class Cookbook + + VALID_STRING_FORMAT = /\A[^\s]+ \([^\s]+\)\Z/ + + def self.valid_str?(str) + !!(str =~ VALID_STRING_FORMAT) + end + + def self.parse(str) + name, version_w_parens = str.split(" ") + version = version_w_parens[/\(([^)]+)\)/, 1] + new(name, version) + end + + def to_s + "#{name} (#{version})" + end + + def eql?(other) + other.kind_of?(self.class) && + other.name == name && + other.version == version + end + + def hash + [name, version].hash + end + + end + + def self.from_lock(lock_data) + new.tap { |e| e.consume_lock_data(lock_data) } + end + + attr_reader :policyfile_dependencies + + attr_reader :cookbook_dependencies + + def initialize + @policyfile_dependencies = [] + @cookbook_dependencies = {} + end + + def add_policyfile_dep(cookbook, constraint) + @policyfile_dependencies << [ cookbook, Semverse::Constraint.new(constraint) ] + end + + def add_cookbook_dep(cookbook_name, version, dependency_list) + cookbook = Cookbook.new(cookbook_name, version) + add_cookbook_obj_dep(cookbook, dependency_list) + end + + 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 + + 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 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 + { "Policyfile" => policyfile_dependencies_for_lock, "dependencies" => cookbook_deps_for_lock } + end + + def policyfile_dependencies_for_lock + policyfile_dependencies.map do |name, constraint| + [ name, constraint.to_s ] + end.sort + end + + 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 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 + + private + + 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 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 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 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 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) + 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 have_cookbook_dep?(name, version) + @cookbook_dependencies.key?(Cookbook.new(name, version)) + end + + 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) + @cookbook_dependencies[Cookbook.new(name, version)] + end + + def set_policyfile_deps_from_lock_data(lock_data) + policyfile_deps_data = lock_data["Policyfile"] + + unless policyfile_deps_data.kind_of?(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 add_policyfile_dep_from_lock_data(entry) + unless entry.kind_of?(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.kind_of?(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.kind_of?(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 set_cookbook_deps_from_lock_data(lock_data) + cookbook_dependencies_data = lock_data["dependencies"] + + unless cookbook_dependencies_data.kind_of?(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 add_cookbook_dep_from_lock_data(name_and_version, deps_list) + unless name_and_version.kind_of?(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.kind_of?(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.kind_of?(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.kind_of?(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.kind_of?(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 + + end + + end +end