File: module_tool.rb

package info (click to toggle)
puppet-agent 8.10.0-6
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 27,404 kB
  • sloc: ruby: 286,820; sh: 492; xml: 116; makefile: 88; cs: 68
file content (195 lines) | stat: -rw-r--r-- 7,115 bytes parent folder | download | duplicates (2)
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'