1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195
|
# encoding: UTF-8
# frozen_string_literal: true
# Load standard libraries
require 'pathname'
require 'fileutils'
require_relative '../puppet/util/colors'
module Puppet
module ModuleTool
require_relative 'module_tool/tar'
extend Puppet::Util::Colors
# Directory and names that should not be checksummed.
ARTIFACTS = ['pkg', /^\./, /^~/, /^#/, 'coverage', 'checksums.json', 'REVISION']
FULL_MODULE_NAME_PATTERN = %r{\A([^-/|.]+)[-|/](.+)\z}
REPOSITORY_URL = Puppet.settings[:module_repository]
# Is this a directory that shouldn't be checksummed?
#
# TODO: Should this be part of Checksums?
# TODO: Rename this method to reflect its purpose?
# TODO: Shouldn't this be used when building packages too?
def self.artifact?(path)
case File.basename(path)
when *ARTIFACTS
true
else
false
end
end
# Return the +username+ and +modname+ for a given +full_module_name+, or raise an
# ArgumentError if the argument isn't parseable.
def self.username_and_modname_from(full_module_name)
matcher = full_module_name.match(FULL_MODULE_NAME_PATTERN)
if matcher
matcher.captures
else
raise ArgumentError, _("Not a valid full name: %{full_module_name}") % { full_module_name: full_module_name }
end
end
# Find the module root when given a path by checking each directory up from
# its current location until it finds one that satisfies is_module_root?
#
# @param path [Pathname, String] path to start from
# @return [Pathname, nil] the root path of the module directory or nil if
# we cannot find one
def self.find_module_root(path)
path = Pathname.new(path) if path.instance_of?(String)
path.expand_path.ascend do |p|
return p if is_module_root?(p)
end
nil
end
# Analyse path to see if it is a module root directory by detecting a
# file named 'metadata.json'
#
# @param path [Pathname, String] path to analyse
# @return [Boolean] true if the path is a module root, false otherwise
def self.is_module_root?(path)
path = Pathname.new(path) if path.instance_of?(String)
FileTest.file?(path + 'metadata.json')
end
# Builds a formatted tree from a list of node hashes containing +:text+
# and +:dependencies+ keys.
def self.format_tree(nodes, level = 0)
str = ''.dup
nodes.each_with_index do |node, i|
last_node = nodes.length - 1 == i
deps = node[:dependencies] || []
str << (indent = " " * level)
str << (last_node ? "└" : "├")
str << "─"
str << (deps.empty? ? "─" : "┬")
str << " #{node[:text]}\n"
branch = format_tree(deps, level + 1)
branch.gsub!(/^#{indent} /, indent + '│') unless last_node
str << branch
end
str
end
def self.build_tree(mods, dir)
mods.each do |mod|
version_string = mod[:version].to_s.sub(/^(?!v)/, 'v')
if mod[:action] == :upgrade
previous_version = mod[:previous_version].to_s.sub(/^(?!v)/, 'v')
version_string = "#{previous_version} -> #{version_string}"
end
mod[:text] = "#{mod[:name]} (#{colorize(:cyan, version_string)})"
mod[:text] += " [#{mod[:path]}]" unless mod[:path].to_s == dir.to_s
deps = mod[:dependencies] || []
deps.sort_by! { |a| a[:name] }
build_tree(deps, dir)
end
end
# @param options [Hash<Symbol,String>] This hash will contain any
# command-line arguments that are not Settings, as those will have already
# been extracted by the underlying application code.
#
# @note Unfortunately the whole point of this method is the side effect of
# modifying the options parameter. This same hash is referenced both
# when_invoked and when_rendering. For this reason, we are not returning
# a duplicate.
# @todo Validate the above note...
#
# An :environment_instance and a :target_dir are added/updated in the
# options parameter.
#
# @api private
def self.set_option_defaults(options)
current_environment = environment_from_options(options)
modulepath = [options[:target_dir]] + current_environment.full_modulepath
face_environment = current_environment.override_with(:modulepath => modulepath.compact)
options[:environment_instance] = face_environment
# Note: environment will have expanded the path
options[:target_dir] = face_environment.full_modulepath.first
end
# Given a hash of options, we should discover or create a
# {Puppet::Node::Environment} instance that reflects the provided options.
#
# Generally speaking, the `:modulepath` parameter should supersede all
# others, the `:environment` parameter should follow after that, and we
# should default to Puppet's current environment.
#
# @param options [{Symbol => Object}] the options to derive environment from
# @return [Puppet::Node::Environment] the environment described by the options
def self.environment_from_options(options)
if options[:modulepath]
path = options[:modulepath].split(File::PATH_SEPARATOR)
Puppet::Node::Environment.create(:anonymous, path, '')
elsif options[:environment].is_a?(Puppet::Node::Environment)
options[:environment]
elsif options[:environment]
# This use of looking up an environment is correct since it honours
# a request to get a particular environment via environment name.
Puppet.lookup(:environments).get!(options[:environment])
else
Puppet.lookup(:current_environment)
end
end
# Handles parsing of module dependency expressions into proper
# {SemanticPuppet::VersionRange}s, including reasonable error handling.
#
# @param where [String] a description of the thing we're parsing the
# dependency expression for
# @param dep [Hash] the dependency description to parse
# @return [Array(String, SemanticPuppet::VersionRange, String)] a tuple of the
# dependent module's name, the version range dependency, and the
# unparsed range expression.
def self.parse_module_dependency(where, dep)
dep_name = dep['name'].tr('/', '-')
range = dep['version_requirement'] || '>= 0.0.0'
begin
parsed_range = Module.parse_range(range)
rescue ArgumentError => e
Puppet.debug "Error in #{where} parsing dependency #{dep_name} (#{e.message}); using empty range."
parsed_range = SemanticPuppet::VersionRange::EMPTY_RANGE
end
[dep_name, parsed_range, range]
end
end
end
# Load remaining libraries
require_relative 'module_tool/errors'
require_relative 'module_tool/applications'
require_relative 'module_tool/checksums'
require_relative 'module_tool/contents_description'
require_relative 'module_tool/dependency'
require_relative 'module_tool/metadata'
require_relative '../puppet/forge/cache'
require_relative '../puppet/forge'
|