# frozen_string_literal: true module Puppet::ModuleTool::Shared include Puppet::ModuleTool::Errors def get_local_constraints @local = Hash.new { |h, k| h[k] = {} } @conditions = Hash.new { |h, k| h[k] = [] } @installed = Hash.new { |h, k| h[k] = [] } @environment.modules_by_path.values.flatten.each do |mod| mod_name = (mod.forge_name || mod.name).tr('/', '-') @installed[mod_name] << mod d = @local["#{mod_name}@#{mod.version}"] (mod.dependencies || []).each do |hash| name, conditions = hash['name'], hash['version_requirement'] name = name.tr('/', '-') d[name] = conditions @conditions[name] << { :module => mod_name, :version => mod.version, :dependency => conditions } end end end def get_remote_constraints(forge) @remote = Hash.new { |h, k| h[k] = {} } @urls = {} @versions = Hash.new { |h, k| h[k] = [] } Puppet.notice _("Downloading from %{uri} ...") % { uri: forge.uri } author, modname = Puppet::ModuleTool.username_and_modname_from(@module_name) info = forge.remote_dependency_info(author, modname, @options[:version]) info.each do |pair| mod_name, releases = pair mod_name = mod_name.tr('/', '-') releases.each do |rel| semver = SemanticPuppet::Version.parse(rel['version']) rescue SemanticPuppet::Version::MIN @versions[mod_name] << { :vstring => rel['version'], :semver => semver } @versions[mod_name].sort_by! { |a| a[:semver] } @urls["#{mod_name}@#{rel['version']}"] = rel['file'] d = @remote["#{mod_name}@#{rel['version']}"] (rel['dependencies'] || []).each do |name, conditions| d[name.tr('/', '-')] = conditions end end end end def implicit_version(mod) return :latest if @conditions[mod].empty? if @conditions[mod].all? { |c| c[:queued] || c[:module] == :you } return :latest end return :best end def annotated_version(mod, versions) if versions.empty? return implicit_version(mod) else return "#{implicit_version(mod)}: #{versions.last}" end end def resolve_constraints(dependencies, source = [{ :name => :you }], seen = {}, action = @action) dependencies = dependencies.filter_map do |mod, range| source.last[:dependency] = range @conditions[mod] << { :module => source.last[:name], :version => source.last[:version], :dependency => range, :queued => true } if forced? range = Puppet::Module.parse_range(@version) rescue Puppet::Module.parse_range('>= 0.0.0') else range = (@conditions[mod]).map do |r| Puppet::Module.parse_range(r[:dependency]) rescue Puppet::Module.parse_range('>= 0.0.0') end.inject(&:&) end if @action == :install && seen.include?(mod) next if range === seen[mod][:semver] req_module = @module_name req_versions = @versions["#{@module_name}"].map { |v| v[:semver] } raise InvalidDependencyCycleError, :module_name => mod, :source => (source + [{ :name => mod, :version => source.last[:dependency] }]), :requested_module => req_module, :requested_version => @version || annotated_version(req_module, req_versions), :conditions => @conditions end if !(forced? || @installed[mod].empty? || source.last[:name] == :you) next if range === SemanticPuppet::Version.parse(@installed[mod].first.version) action = :upgrade elsif @installed[mod].empty? action = :install end if action == :upgrade @conditions.each { |_, conds| conds.delete_if { |c| c[:module] == mod } } end versions = @versions["#{mod}"].select { |h| range === h[:semver] } valid_versions = versions.select { |x| x[:semver].special == '' } valid_versions = versions if valid_versions.empty? version = valid_versions.last unless version req_module = @module_name req_versions = @versions["#{@module_name}"].map { |v| v[:semver] } raise NoVersionsSatisfyError, :requested_name => req_module, :requested_version => @version || annotated_version(req_module, req_versions), :installed_version => @installed[@module_name].empty? ? nil : @installed[@module_name].first.version, :dependency_name => mod, :conditions => @conditions[mod], :action => @action end seen[mod] = version { :module => mod, :version => version, :action => action, :previous_version => @installed[mod].empty? ? nil : @installed[mod].first.version, :file => @urls["#{mod}@#{version[:vstring]}"], :path => action == :install ? @options[:target_dir] : (@installed[mod].empty? ? @options[:target_dir] : @installed[mod].first.modulepath), :dependencies => [] } end dependencies.each do |mod| deps = @remote["#{mod[:module]}@#{mod[:version][:vstring]}"].sort_by(&:first) mod[:dependencies] = resolve_constraints(deps, source + [{ :name => mod[:module], :version => mod[:version][:vstring] }], seen, :install) end unless @ignore_dependencies return dependencies end def download_tarballs(graph, default_path, forge) graph.map do |release| begin if release[:tarball] cache_path = Pathname(release[:tarball]) else cache_path = forge.retrieve(release[:file]) end rescue OpenURI::HTTPError => e raise RuntimeError, _("Could not download module: %{message}") % { message: e.message }, e.backtrace end [ { (release[:path] ||= default_path) => cache_path }, *download_tarballs(release[:dependencies], default_path, forge) ] end.flatten end def forced? options[:force] end def add_module_name_constraints_to_graph(graph) # Puppet modules are installed by "module name", but resolved by # "full name" (including namespace). So that we don't run into # problems at install time, we should reject any solution that # depends on multiple nodes with the same "module name". graph.add_graph_constraint('PMT') do |nodes| names = nodes.map { |x| x.dependency_names + [x.name] }.flatten names = names.map { |x| x.tr('/', '-') }.uniq names = names.map { |x| x[/-(.*)/, 1] } names.length == names.uniq.length end end end