bolt 3.24.0 → 3.25.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of bolt might be problematic. Click here for more details.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7ad3bbc9413aac278feeac8a7e31173bd065f0029fb211d7e1906c4803a4c390
4
- data.tar.gz: 387d3a07844139eb29a1db06d1792acfdad6a09eab2b5576a9c4d34fdffdaca8
3
+ metadata.gz: fe3e07dae1f3e80d3b4ade7c9d78749f54c592e246b2fd82e6264ab908b8baa1
4
+ data.tar.gz: 44af49cb408f92496c9795d892bc2e43e04788dd6ead42070a04e258958f00ac
5
5
  SHA512:
6
- metadata.gz: 83c1685fc238f049dad5cf841ea55f37c57a1aa59d66ef420d754b78d7b0ba8cca4533accb2cfbe53915587f1832beb7cbf956501486de63e135933b57f5fa98
7
- data.tar.gz: c6900fde1fd9234a603171c79c2f2b3a22b2d2661797b94bf0afd2050f4ef5cc32844562734d3e41bd94464806e1efaabaac346d6887773f547c801c33cc3d6f
6
+ metadata.gz: 0d158d8a0d8c72c99a34464ad4de0df9e7b04a83b1509b048c5b650baccbea9a06f99dfd512bfc8bdc91f5065e22148b76814a895766717bfe9f2c34bd81e55f
7
+ data.tar.gz: eca8af5592551ff9a273ad508831f9a1e07edf0327aa29dcc7078e5fb44743033c92388d79ab22ae2d3c5ea9574984b0c50849544159ab8dd32c2090a0dad939
data/Puppetfile CHANGED
@@ -6,7 +6,7 @@ moduledir File.join(File.dirname(__FILE__), 'modules')
6
6
 
7
7
  # Core modules used by 'apply'
8
8
  mod 'puppetlabs-service', '2.2.0'
9
- mod 'puppetlabs-puppet_agent', '4.11.0'
9
+ mod 'puppetlabs-puppet_agent', '4.12.1'
10
10
  mod 'puppetlabs-facts', '1.4.0'
11
11
 
12
12
  # Core types and providers for Puppet 6
@@ -23,13 +23,16 @@ mod 'puppetlabs-zone_core', '1.0.3'
23
23
 
24
24
  # Useful additional modules
25
25
  mod 'puppetlabs-package', '2.2.0'
26
- mod 'puppetlabs-powershell_task_helper', '0.1.0'
27
26
  mod 'puppetlabs-puppet_conf', '1.3.0'
28
- mod 'puppetlabs-python_task_helper', '0.5.0'
29
27
  mod 'puppetlabs-reboot', '4.2.0'
28
+ mod 'puppetlabs-stdlib', '8.4.0'
29
+
30
+ # Task helpers
31
+ mod 'puppetlabs-powershell_task_helper', '0.1.0'
30
32
  mod 'puppetlabs-ruby_task_helper', '0.6.1'
31
33
  mod 'puppetlabs-ruby_plugin_helper', '0.2.0'
32
- mod 'puppetlabs-stdlib', '8.2.0'
34
+ mod 'puppetlabs-python_task_helper', '0.5.0'
35
+ mod 'puppetlabs-bash_task_helper', '2.0.0'
33
36
 
34
37
  # Plugin modules
35
38
  mod 'puppetlabs-aws_inventory', '0.7.0'
@@ -12,11 +12,20 @@
12
12
  # keys](applying_manifest_blocks.md#result-keys).
13
13
  # @param target
14
14
  # The target the result is from.
15
+ # @param error
16
+ # An Error object constructed from the `_error` field of the result's value.
17
+ # @param catalog
18
+ # The Puppet catalog used to configure the target. The catalog describes the
19
+ # desired state of the target. The catalog is masked with the `Sensitive` data
20
+ # type to protect any sensitive information in the catalog from being printed
21
+ # to the console or logs. Using this function automatically unwraps the
22
+ # catalog. For more information about catalogs and the catalog compilation
23
+ # process, see [Catalog
24
+ # compilation](https://puppet.com/docs/puppet/latest/subsystem_catalog_compilation.html).
25
+
15
26
  #
16
27
  # @!method action
17
28
  # The action performed. `ApplyResult.action` always returns the string `apply`.
18
- # @!method error
19
- # Returns an Error object constructed from the `_error` field of the result's value.
20
29
  # @!method message
21
30
  # The `_output` field of the result's value.
22
31
  # @!method ok
@@ -30,10 +39,11 @@ Puppet::DataTypes.create_type('ApplyResult') do
30
39
  interface <<-PUPPET
31
40
  attributes => {
32
41
  'report' => Hash[String[1], Data],
33
- 'target' => Target
42
+ 'target' => Target,
43
+ 'error' => Optional[Error],
44
+ 'catalog' => Optional[Hash]
34
45
  },
35
46
  functions => {
36
- error => Callable[[], Optional[Error]],
37
47
  ok => Callable[[], Boolean],
38
48
  message => Callable[[], Optional[String]],
39
49
  action => Callable[[], String],
@@ -280,7 +280,6 @@ module Bolt
280
280
  result
281
281
  end
282
282
  else
283
-
284
283
  arguments = {
285
284
  'catalog' => Puppet::Pops::Types::PSensitiveType::Sensitive.new(catalog),
286
285
  'plugins' => Puppet::Pops::Types::PSensitiveType::Sensitive.new(plugins),
@@ -291,7 +290,7 @@ module Bolt
291
290
 
292
291
  callback = proc do |event|
293
292
  if event[:type] == :node_result
294
- event = event.merge(result: ApplyResult.from_task_result(event[:result]))
293
+ event = event.merge(result: ApplyResult.from_task_result(event[:result], catalog))
295
294
  end
296
295
  @executor.publish_event(event)
297
296
  end
@@ -299,7 +298,7 @@ module Bolt
299
298
  options[:run_as] = @executor.run_as if @executor.run_as && !options.key?(:run_as)
300
299
 
301
300
  results = transport.batch_task(batch, catalog_apply_task, arguments, options, &callback)
302
- Array(results).map { |result| ApplyResult.from_task_result(result) }
301
+ Array(results).map { |result| ApplyResult.from_task_result(result, catalog) }
303
302
  end
304
303
  end
305
304
  end
@@ -65,23 +65,30 @@ module Bolt
65
65
  end
66
66
  end
67
67
 
68
- def self.from_task_result(result)
68
+ def self.from_task_result(result, catalog = nil)
69
69
  if (puppet_missing = puppet_missing_error(result))
70
70
  new(result.target,
71
71
  error: puppet_missing,
72
- report: result.value.reject { |k| k == '_error' })
72
+ report: result.value.reject { |k| k == '_error' },
73
+ catalog: catalog)
73
74
  elsif !result.ok?
74
- new(result.target, error: result.error_hash)
75
+ new(result.target,
76
+ error: result.error_hash,
77
+ catalog: catalog)
75
78
  elsif (invalid_report = invalid_report_error(result))
76
79
  new(result.target,
77
80
  error: invalid_report,
78
- report: result.value.reject { |k| %w[_error _output].include?(k) })
81
+ report: result.value.reject { |k| %w[_error _output].include?(k) },
82
+ catalog: catalog)
79
83
  elsif (resource_error = resource_error(result))
80
84
  new(result.target,
81
85
  error: resource_error,
82
- report: result.value.reject { |k| k == '_error' })
86
+ report: result.value.reject { |k| k == '_error' },
87
+ catalog: catalog)
83
88
  else
84
- new(result.target, report: result.value)
89
+ new(result.target,
90
+ report: result.value,
91
+ catalog: catalog)
85
92
  end
86
93
  end
87
94
 
@@ -89,16 +96,21 @@ module Bolt
89
96
  def _pcore_init_hash
90
97
  { 'target' => @target,
91
98
  'error' => value['_error'],
92
- 'report' => value['report'] }
99
+ 'report' => value['report'],
100
+ 'catalog' => catalog }
93
101
  end
94
102
 
95
- def initialize(target, error: nil, report: nil)
103
+ def initialize(target, error: nil, report: nil, catalog: nil)
96
104
  @target = target
97
105
  @value = {}
98
106
  @action = 'apply'
99
107
  @value['report'] = report if report
100
108
  @value['_error'] = error if error
101
109
  @value['_output'] = metrics_message if metrics_message
110
+
111
+ if catalog
112
+ @value['_sensitive'] = Puppet::Pops::Types::PSensitiveType::Sensitive.new({ 'catalog' => catalog })
113
+ end
102
114
  end
103
115
 
104
116
  def event_metrics
@@ -131,6 +143,10 @@ module Bolt
131
143
  @value['report']
132
144
  end
133
145
 
146
+ def catalog
147
+ sensitive.unwrap['catalog'] if sensitive
148
+ end
149
+
134
150
  def generic_value
135
151
  {}
136
152
  end
@@ -10,8 +10,10 @@ module Bolt
10
10
  class Group
11
11
  attr_accessor :name, :groups
12
12
 
13
- # Regex used to validate group names and target aliases.
14
- NAME_REGEX = /\A[a-z0-9_][a-z0-9_-]*\Z/.freeze
13
+ # Illegal characters that are not permitted in group names or aliases.
14
+ # These characters are delimiters for target and group names and allowing
15
+ # them would cause unexpected behavior.
16
+ ILLEGAL_CHARS = /[\s,]/.freeze
15
17
 
16
18
  # NOTE: All keys should have a corresponding schema property in schemas/bolt-inventory.schema.json
17
19
  DATA_KEYS = %w[config facts vars features plugin_hooks].freeze
@@ -41,7 +43,10 @@ module Bolt
41
43
  @name = @plugins.resolve_references(input['name'])
42
44
 
43
45
  raise ValidationError.new("Group name must be a String, not #{@name.inspect}", nil) unless @name.is_a?(String)
44
- raise ValidationError.new("Invalid group name #{@name}", @name) unless @name =~ NAME_REGEX
46
+
47
+ if (illegal_char = @name.match(ILLEGAL_CHARS))
48
+ raise ValidationError.new("Illegal character '#{illegal_char}' in group name '#{@name}'", @name)
49
+ end
45
50
 
46
51
  validate_group_input(input)
47
52
 
@@ -167,7 +172,9 @@ module Bolt
167
172
 
168
173
  def insert_alia(target_name, aliases)
169
174
  aliases.each do |alia|
170
- raise ValidationError.new("Invalid alias #{alia}", @name) unless alia =~ NAME_REGEX
175
+ if (illegal_char = alia.match(ILLEGAL_CHARS))
176
+ raise ValidationError.new("Illegal character '#{illegal_char}' in alias '#{alia}'", @name)
177
+ end
171
178
 
172
179
  if (found = @aliases[alia])
173
180
  raise ValidationError.new(alias_conflict(alia, found, target_name), @name)
@@ -8,6 +8,11 @@ module Bolt
8
8
  class Target
9
9
  attr_reader :name, :uri, :safe_name, :target_alias, :resources
10
10
 
11
+ # Illegal characters that are not permitted in target names.
12
+ # These characters are delimiters for target and group names and allowing
13
+ # them would cause unexpected behavior.
14
+ ILLEGAL_CHARS = /[\s,]/.freeze
15
+
11
16
  def initialize(target_data, inventory)
12
17
  unless target_data['name'] || target_data['uri']
13
18
  raise Bolt::Inventory::ValidationError.new("Target must have either a name or uri", nil)
@@ -150,6 +155,10 @@ module Bolt
150
155
  raise Bolt::Inventory::ValidationError.new("Target name must be ASCII characters: #{@name}", nil)
151
156
  end
152
157
 
158
+ if (illegal_char = @name.match(ILLEGAL_CHARS))
159
+ raise ValidationError.new("Illegal character '#{illegal_char}' in target name '#{@name}'", nil)
160
+ end
161
+
153
162
  unless transport.nil? || Bolt::TRANSPORTS.include?(transport.to_sym)
154
163
  raise Bolt::UnknownTransportError.new(transport, uri)
155
164
  end
@@ -49,7 +49,7 @@ module Bolt
49
49
  spec_searcher_configuration: spec_searcher_config(config)
50
50
  )
51
51
  rescue StandardError => e
52
- raise Bolt::Error.new(e.message, 'bolt/module-resolver-error')
52
+ raise Bolt::Error.new("Unable to resolve modules: #{e.message}", 'bolt/module-resolver-error')
53
53
  end
54
54
 
55
55
  # Create the Puppetfile object.
@@ -1,9 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'json'
4
+ require 'net/http'
5
+ require 'open3'
4
6
  require 'set'
5
7
 
6
8
  require_relative '../../../bolt/error'
9
+ require_relative '../../../bolt/logger'
10
+ require_relative '../../../bolt/module_installer/specs/id/gitclone'
11
+ require_relative '../../../bolt/module_installer/specs/id/github'
12
+ require_relative '../../../bolt/module_installer/specs/id/gitlab'
7
13
 
8
14
  # This class represents a Git module specification.
9
15
  #
@@ -17,12 +23,19 @@ module Bolt
17
23
 
18
24
  attr_reader :git, :ref, :resolve, :type
19
25
 
20
- def initialize(init_hash)
21
- @resolve = init_hash.key?('resolve') ? init_hash['resolve'] : true
22
- @name = parse_name(init_hash['name'])
23
- @git, @repo = parse_git(init_hash['git'])
24
- @ref = init_hash['ref']
25
- @type = :git
26
+ def initialize(init_hash, config = {})
27
+ @logger = Bolt::Logger.logger(self)
28
+ @resolve = init_hash.key?('resolve') ? init_hash['resolve'] : true
29
+ @git = init_hash['git']
30
+ @ref = init_hash['ref']
31
+ @name = parse_name(init_hash['name'])
32
+ @proxy = config.dig('proxy')
33
+ @type = :git
34
+
35
+ unless @resolve == true || @resolve == false
36
+ raise Bolt::ValidationError,
37
+ "Option 'resolve' for module spec #{@git} must be a Boolean"
38
+ end
26
39
 
27
40
  if @name.nil? && @resolve == false
28
41
  raise Bolt::ValidationError,
@@ -30,9 +43,9 @@ module Bolt
30
43
  "must include a 'name' key when 'resolve' is false."
31
44
  end
32
45
 
33
- unless @resolve == true || @resolve == false
46
+ unless valid_url?(@git)
34
47
  raise Bolt::ValidationError,
35
- "Option 'resolve' for module spec #{@git} must be a Boolean"
48
+ "Invalid URI #{@git}. Valid URIs must begin with 'git@', 'http://', or 'https://'."
36
49
  end
37
50
  end
38
51
 
@@ -57,22 +70,6 @@ module Bolt
57
70
  match[:name]
58
71
  end
59
72
 
60
- # Gets the repo from the git URL.
61
- #
62
- private def parse_git(git)
63
- return [git, nil] unless @resolve
64
-
65
- repo = if git.start_with?('git@github.com:')
66
- git.split('git@github.com:').last.split('.git').first
67
- elsif git.start_with?('https://github.com')
68
- git.split('https://github.com/').last.split('.git').first
69
- else
70
- raise Bolt::ValidationError, invalid_git_msg(git)
71
- end
72
-
73
- [git, repo]
74
- end
75
-
76
73
  # Returns true if the specification is satisfied by the module.
77
74
  #
78
75
  def satisfied_by?(mod)
@@ -88,14 +85,6 @@ module Bolt
88
85
  }
89
86
  end
90
87
 
91
- # Returns an error message that the provided repo is not a git repo or
92
- # is private.
93
- #
94
- private def invalid_git_msg(repo_name)
95
- "#{repo_name} is not a public GitHub repository. See https://pup.pt/no-resolve "\
96
- "for information on how to install this module."
97
- end
98
-
99
88
  # Returns a PuppetfileResolver::Model::GitModule object for resolving.
100
89
  #
101
90
  def to_resolver_module
@@ -107,90 +96,53 @@ module Bolt
107
96
  end
108
97
  end
109
98
 
110
- # Resolves the module's title from the module metadata. This is lazily
111
- # resolved since Bolt does not always need to know a Git module's name.
99
+ # Returns the module's name.
112
100
  #
113
101
  def name
114
- @name ||= begin
115
- url = "https://raw.githubusercontent.com/#{@repo}/#{sha}/metadata.json"
116
- response = make_request(:Get, url)
117
-
118
- case response
119
- when Net::HTTPOK
120
- body = JSON.parse(response.body)
121
-
122
- unless body.key?('name')
123
- raise Bolt::Error.new(
124
- "Missing name in metadata.json at #{git}. This is not a valid module.",
125
- "bolt/missing-module-name-error"
126
- )
127
- end
128
-
129
- parse_name(body['name'])
130
- else
131
- raise Bolt::Error.new(
132
- "Missing metadata.json at #{git}. This is not a valid module.",
133
- "bolt/missing-module-metadata-error"
134
- )
135
- end
136
- end
102
+ @name ||= parse_name(id.name)
137
103
  end
138
104
 
139
- # Resolves the SHA for the specified ref. This is lazily resolved since
140
- # Bolt does not always need to know a Git module's SHA.
105
+ # Returns the SHA for the module's ref.
141
106
  #
142
107
  def sha
143
- @sha ||= begin
144
- url = "https://api.github.com/repos/#{@repo}/commits/#{ref}"
145
- headers = ENV['GITHUB_TOKEN'] ? { "Authorization" => "token #{ENV['GITHUB_TOKEN']}" } : {}
146
- response = make_request(:Get, url, headers)
147
-
148
- case response
149
- when Net::HTTPOK
150
- body = JSON.parse(response.body)
151
- body['sha']
152
- when Net::HTTPUnauthorized
153
- raise Bolt::Error.new(
154
- "Invalid token at GITHUB_TOKEN, unable to resolve git modules.",
155
- "bolt/invalid-git-token-error"
156
- )
157
- when Net::HTTPForbidden
158
- message = "GitHub API rate limit exceeded, unable to resolve git modules. "
159
-
160
- unless ENV['GITHUB_TOKEN']
161
- message += "To increase your rate limit, set the GITHUB_TOKEN environment "\
162
- "variable with a GitHub personal access token."
163
- end
164
-
165
- raise Bolt::Error.new(message, 'bolt/github-api-rate-limit-error')
166
- when Net::HTTPNotFound
167
- raise Bolt::Error.new(invalid_git_msg(git), "bolt/missing-git-repository-error")
168
- else
108
+ id.sha
109
+ end
110
+
111
+ # Gets the ID for the module based on the specified ref and git URL.
112
+ # This is lazily resolved since Bolt does not always need this information,
113
+ # and requesting it is expensive.
114
+ #
115
+ private def id
116
+ @id ||= begin
117
+ # The request methods here return an ID object if the module name and SHA
118
+ # were found and nil otherwise. This lets Bolt try multiple methods for
119
+ # finding the module name and SHA, and short circuiting as soon as it does.
120
+ module_id = Bolt::ModuleInstaller::Specs::ID::GitHub.request(@git, @ref, @proxy) ||
121
+ Bolt::ModuleInstaller::Specs::ID::GitLab.request(@git, @ref, @proxy) ||
122
+ Bolt::ModuleInstaller::Specs::ID::GitClone.request(@git, @ref, @proxy)
123
+
124
+ unless module_id
169
125
  raise Bolt::Error.new(
170
- "Ref #{ref} at #{git} is not a commit, tag, or branch.",
171
- "bolt/invalid-git-ref-error"
126
+ "Unable to locate metadata and calculate SHA for ref #{@ref} at #{@git}. This may "\
127
+ "not be a valid module. For more information about how Bolt attempted to locate "\
128
+ "this information, check the debugging logs.",
129
+ 'bolt/missing-module-metadata-error'
172
130
  )
173
131
  end
132
+
133
+ module_id
174
134
  end
175
135
  end
176
136
 
177
- # Makes a generic HTTP request.
137
+ # Returns true if the URL is valid.
178
138
  #
179
- private def make_request(verb, url, headers = {})
180
- require 'net/http'
181
-
182
- uri = URI.parse(url)
183
- opts = { use_ssl: uri.scheme == 'https' }
139
+ private def valid_url?(url)
140
+ return true if url.start_with?('git@')
184
141
 
185
- Net::HTTP.start(uri.host, uri.port, opts) do |client|
186
- request = Net::HTTP.const_get(verb).new(uri, headers)
187
- client.request(request)
188
- end
189
- rescue StandardError => e
190
- raise Bolt::Error.new(
191
- "Failed to connect to #{uri}: #{e.message}",
192
- "bolt/http-connect-error"
193
- )
142
+ uri = URI.parse(url)
143
+ uri.is_a?(URI::HTTP) && uri.host
144
+ rescue URI::InvalidURIError
145
+ false
194
146
  end
195
147
  end
196
148
  end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'net/http'
5
+
6
+ require_relative '../../../../bolt/error'
7
+ require_relative '../../../../bolt/logger'
8
+
9
+ module Bolt
10
+ class ModuleInstaller
11
+ class Specs
12
+ class ID
13
+ class Base
14
+ attr_reader :name, :sha
15
+
16
+ # @param name [String] The module's name.
17
+ # @param sha [String] The ref's SHA1.
18
+ #
19
+ def initialize(name, sha)
20
+ @name = name
21
+ @sha = sha
22
+ end
23
+
24
+ # Request the name and SHA for a module and ref.
25
+ # This method must return either an ID object or nil. The GitSpec
26
+ # class relies on this class to return an ID object to indicate
27
+ # the module was found, or nil to indicate that it should try to
28
+ # find it another way (such as cloning the repo).
29
+ #
30
+ # @param git [String] The URL to the git repo.
31
+ # @param ref [String] The ref to checkout.
32
+ # @param proxy [String] A proxy to use when making requests.
33
+ #
34
+ def self.request(git, ref, proxy)
35
+ name, sha = name_and_sha(git, ref, proxy)
36
+ name && sha ? new(name, sha) : nil
37
+ end
38
+
39
+ # Stub method for retrieving the module's name and SHA. Must
40
+ # be implemented by all sub classes.
41
+ #
42
+ private_class_method def self.name_and_sha(_git, _ref, _proxy)
43
+ raise NotImplementedError, 'Class does not implemented #name_and_sha'
44
+ end
45
+
46
+ # Makes a HTTP request.
47
+ #
48
+ # @param url [String] The URL to make the request to.
49
+ # @param proxy [String] A proxy to use when making the request.
50
+ # @param headers [Hash] Headers to send with the request.
51
+ #
52
+ private_class_method def self.make_request(url, proxy, headers = {})
53
+ uri = URI.parse(url)
54
+ opts = { use_ssl: uri.scheme == 'https' }
55
+ args = [uri.host, uri.port]
56
+
57
+ if proxy
58
+ proxy = URI.parse(proxy)
59
+ args += [proxy.host, proxy.port, proxy.user, proxy.password]
60
+ end
61
+
62
+ Bolt::Logger.debug("Making request to #{loc(url, proxy)}")
63
+
64
+ Net::HTTP.start(*args, opts) do |client|
65
+ client.request(Net::HTTP::Get.new(uri, headers))
66
+ end
67
+ rescue StandardError => e
68
+ raise Bolt::Error.new(
69
+ "Failed to connect to #{loc(uri, proxy)}: #{e.message}",
70
+ "bolt/http-connect-error"
71
+ )
72
+ end
73
+
74
+ # Returns a formatted string describing the URL and proxy used when making
75
+ # a request.
76
+ #
77
+ # @param url [String, URI::HTTP] The URL used.
78
+ # @param proxy [String, URI::HTTP] The proxy used.
79
+ #
80
+ private_class_method def self.loc(url, proxy)
81
+ proxy ? "#{url} with proxy #{proxy}" : url.to_s
82
+ end
83
+
84
+ # Parses the metadata and validates that it is a Hash.
85
+ #
86
+ # @param metadata [String] The JSON data to parse.
87
+ #
88
+ private_class_method def self.parse_name_from_metadata(metadata)
89
+ metadata = JSON.parse(metadata)
90
+
91
+ unless metadata.is_a?(Hash)
92
+ raise Bolt::Error.new(
93
+ "Invalid metadata. Expected a Hash, got a #{metadata.class}: #{metadata}",
94
+ "bolt/invalid-module-metadata-error"
95
+ )
96
+ end
97
+
98
+ unless metadata.key?('name')
99
+ raise Bolt::Error.new(
100
+ "Invalid metadata. Metadata must include a 'name' key.",
101
+ "bolt/missing-module-name-error"
102
+ )
103
+ end
104
+
105
+ metadata['name']
106
+ rescue JSON::ParserError => e
107
+ raise Bolt::Error.new(
108
+ "Unable to parse metadata as JSON: #{e.message}",
109
+ "bolt/metadata-parse-error"
110
+ )
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../../../bolt/module_installer/specs/id/base'
4
+
5
+ module Bolt
6
+ class ModuleInstaller
7
+ class Specs
8
+ class ID
9
+ class GitClone < Base
10
+ # Returns the name and SHA for the module at the given ref.
11
+ #
12
+ # @param git [String] The URL to the git repo.
13
+ # @param ref [String] The ref to checkout.
14
+ # @param proxy [String] The proxy to use when cloning.
15
+ #
16
+ private_class_method def self.name_and_sha(git, ref, proxy)
17
+ require 'open3'
18
+
19
+ unless git?
20
+ Bolt::Logger.debug("'git' executable not found, unable to use git clone resolution.")
21
+ return nil
22
+ end
23
+
24
+ # Clone the repo into a temp directory that will be automatically cleaned up.
25
+ Dir.mktmpdir do |dir|
26
+ return nil unless clone_repo(git, ref, dir, proxy)
27
+
28
+ # Extract the name from the metadata file and calculate the SHA.
29
+ Dir.chdir(dir) do
30
+ [request_name(git, ref), request_sha(git, ref)]
31
+ end
32
+ end
33
+ end
34
+
35
+ # Requests a module's metadata and returns the name from it.
36
+ #
37
+ # @param git [String] The URL to the git repo.
38
+ # @param ref [String] The ref to checkout.
39
+ #
40
+ private_class_method def self.request_name(git, ref)
41
+ command = %W[git show #{ref}:metadata.json]
42
+ Bolt::Logger.debug("Executing command '#{command.join(' ')}'")
43
+
44
+ out, err, status = Open3.capture3(*command)
45
+
46
+ unless status.success?
47
+ raise Bolt::Error.new(
48
+ "Unable to find metadata file at #{git}: #{err}",
49
+ "bolt/missing-metadata-file-error"
50
+ )
51
+ end
52
+
53
+ Bolt::Logger.debug("Found metadata file at #{git}")
54
+ parse_name_from_metadata(out)
55
+ end
56
+
57
+ # Requests the SHA for the specified ref.
58
+ #
59
+ # @param git [String] The URL to the git repo.
60
+ # @param ref [String] The ref to checkout.
61
+ #
62
+ private_class_method def self.request_sha(git, ref)
63
+ command = %W[git rev-parse #{ref}^{commit}]
64
+ Bolt::Logger.debug("Executing command '#{command.join(' ')}'")
65
+
66
+ out, err, status = Open3.capture3(*command)
67
+
68
+ if status.success?
69
+ out.strip
70
+ else
71
+ raise Bolt::Error.new(
72
+ "Unable to calculate SHA for ref #{ref} at #{git}: #{err}",
73
+ "bolt/invalid-ref-error"
74
+ )
75
+ end
76
+ end
77
+
78
+ # Clones the repository. First attempts to clone a bare repository
79
+ # and falls back to cloning the full repository if that fails. Cloning
80
+ # a bare repository is significantly faster for large modules, but
81
+ # cloning a bare repository using a commit is not supported.
82
+ #
83
+ # @param git [String] The URL to the git repo.
84
+ # @param ref [String] The ref to checkout.
85
+ # @param dir [String] The directory to clone the repo to.
86
+ # @param proxy [String] The proxy to use when cloning.
87
+ #
88
+ private_class_method def self.clone_repo(git, ref, dir, proxy)
89
+ clone = %W[git clone #{git} #{dir}]
90
+ clone += %W[--config "http.proxy=#{proxy}" --config "https.proxy=#{proxy}"] if proxy
91
+
92
+ bare_clone = clone + %w[--bare --depth=1]
93
+ bare_clone.push("--branch=#{ref}") unless ref == 'HEAD'
94
+
95
+ # Attempt to clone a bare repository
96
+ Bolt::Logger.debug("Executing command '#{bare_clone.join(' ')}'")
97
+ _out, err, status = Open3.capture3(*bare_clone)
98
+ return true if status.success?
99
+ Bolt::Logger.debug("Unable to clone bare repository at #{loc(git, proxy)}: #{err}")
100
+
101
+ # Fall back to cloning the full repository
102
+ Bolt::Logger.debug("Executing command '#{clone.join(' ')}'")
103
+ _out, err, status = Open3.capture3(*clone)
104
+ Bolt::Logger.debug("Unable to clone repository at #{loc(git, proxy)}: #{err}") unless status.success?
105
+ status.success?
106
+ end
107
+
108
+ # Returns true if the 'git' executable is available.
109
+ #
110
+ private_class_method def self.git?
111
+ Open3.capture3('git', '--version')
112
+ true
113
+ rescue Errno::ENOENT
114
+ false
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../../../bolt/module_installer/specs/id/base'
4
+
5
+ module Bolt
6
+ class ModuleInstaller
7
+ class Specs
8
+ class ID
9
+ class GitHub < Base
10
+ # Returns the name and SHA for the module at the given ref.
11
+ #
12
+ # @param git [String] The URL to the git repo.
13
+ # @param ref [String] The ref to use.
14
+ # @param proxy [String] The proxy to use when making requests.
15
+ #
16
+ private_class_method def self.name_and_sha(git, ref, proxy)
17
+ repo = parse_repo(git)
18
+ return nil unless repo
19
+ [request_name(repo, ref, proxy), request_sha(repo, ref, proxy)]
20
+ end
21
+
22
+ # Parses the repo path out of the URL.
23
+ #
24
+ # @param git [String] The URL to the git repo.
25
+ #
26
+ private_class_method def self.parse_repo(git)
27
+ if git.start_with?('git@github.com:')
28
+ git.split('git@github.com:').last.split('.git').first
29
+ elsif git.start_with?('https://github.com')
30
+ git.split('https://github.com/').last.split('.git').first
31
+ end
32
+ end
33
+
34
+ # Requests a module's metadata and returns the name from it.
35
+ #
36
+ # @param repo [String] The repo ID, i.e. 'owner/repo'
37
+ # @param ref [String] The ref to use.
38
+ # @param proxy [String] The proxy to use when making requests.
39
+ #
40
+ private_class_method def self.request_name(repo, ref, proxy)
41
+ metadata_url = "https://raw.githubusercontent.com/#{repo}/#{ref}/metadata.json"
42
+ response = make_request(metadata_url, proxy)
43
+
44
+ case response
45
+ when Net::HTTPOK
46
+ Bolt::Logger.debug("Found metadata file at #{loc(metadata_url, proxy)}")
47
+ parse_name_from_metadata(response.body)
48
+ else
49
+ Bolt::Logger.debug("Unable to find metadata file at #{loc(metadata_url, proxy)}")
50
+ nil
51
+ end
52
+ end
53
+
54
+ # Requests the SHA for the specified ref.
55
+ #
56
+ # @param repo [String] The repo ID, i.e. 'owner/repo'
57
+ # @param ref [String] The ref to resolve.
58
+ # @param proxy [String] The proxy to use when making requests.
59
+ #
60
+ private_class_method def self.request_sha(repo, ref, proxy)
61
+ url = "https://api.github.com/repos/#{repo}/commits/#{ref}"
62
+ headers = ENV['GITHUB_TOKEN'] ? { "Authorization" => "token #{ENV['GITHUB_TOKEN']}" } : {}
63
+ response = make_request(url, proxy, headers)
64
+
65
+ case response
66
+ when Net::HTTPOK
67
+ JSON.parse(response.body).fetch('sha', nil)
68
+ when Net::HTTPUnauthorized
69
+ Bolt::Logger.debug("Invalid token at GITHUB_TOKEN, unable to calculate SHA.")
70
+ nil
71
+ when Net::HTTPForbidden
72
+ message = "GitHub API rate limit exceeded, unable to calculate SHA."
73
+
74
+ unless ENV['GITHUB_TOKEN']
75
+ message += " To increase your rate limit, set the GITHUB_TOKEN environment "\
76
+ "variable with a GitHub personal access token."
77
+ end
78
+
79
+ Bolt::Logger.debug(message)
80
+ nil
81
+ else
82
+ Bolt::Logger.debug("Unable to calculate SHA for ref #{ref}")
83
+ nil
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../../../bolt/module_installer/specs/id/base'
4
+
5
+ module Bolt
6
+ class ModuleInstaller
7
+ class Specs
8
+ class ID
9
+ class GitLab < Base
10
+ # Returns the name and SHA for the module at the given ref.
11
+ #
12
+ # @param git [String] The URL to the git repo.
13
+ # @param ref [String] The ref to use.
14
+ # @param proxy [String] The proxy to use when making requests.
15
+ #
16
+ private_class_method def self.name_and_sha(git, ref, proxy)
17
+ repo = parse_repo(git)
18
+ return nil unless repo
19
+ [request_name(repo, ref, proxy), request_sha(repo, ref, proxy)]
20
+ end
21
+
22
+ # Parses the repo path out of the URL.
23
+ #
24
+ # @param git [String] The URL to the git repo.
25
+ #
26
+ private_class_method def self.parse_repo(git)
27
+ if git.start_with?('git@gitlab.com:')
28
+ git.split('git@gitlab.com:').last.split('.git').first
29
+ elsif git.start_with?('https://gitlab.com')
30
+ git.split('https://gitlab.com/').last.split('.git').first
31
+ end
32
+ end
33
+
34
+ # Requests a module's metadata and returns the name from it.
35
+ #
36
+ # @param repo [String] The repo ID, i.e. 'owner/repo'
37
+ # @param ref [String] The ref to use.
38
+ # @param proxy [String] The proxy to use when making requests.
39
+ #
40
+ private_class_method def self.request_name(repo, ref, proxy)
41
+ metadata_url = "https://gitlab.com/#{repo}/-/raw/#{ref}/metadata.json"
42
+ response = make_request(metadata_url, proxy)
43
+
44
+ case response
45
+ when Net::HTTPOK
46
+ Bolt::Logger.debug("Found metadata file at #{loc(metadata_url, proxy)}")
47
+ parse_name_from_metadata(response.body)
48
+ else
49
+ Bolt::Logger.debug("Unable to find metadata file at #{loc(metadata_url, proxy)}")
50
+ nil
51
+ end
52
+ end
53
+
54
+ # Requests the SHA for the specified ref.
55
+ #
56
+ # @param repo [String] The repo ID, i.e. 'owner/repo'
57
+ # @param ref [String] The ref to resolve.
58
+ # @param proxy [String] The proxy to use when making requests.
59
+ #
60
+ private_class_method def self.request_sha(repo, ref, proxy)
61
+ require 'cgi'
62
+
63
+ url = "https://gitlab.com/api/v4/projects/#{CGI.escape(repo)}/repository/commits/#{ref}"
64
+ headers = ENV['GITLAB_TOKEN'] ? { "PRIVATE-TOKEN" => ENV['GITLAB_TOKEN'] } : {}
65
+ response = make_request(url, proxy, headers)
66
+
67
+ case response
68
+ when Net::HTTPOK
69
+ JSON.parse(response.body).fetch('id', nil)
70
+ when Net::HTTPUnauthorized
71
+ Bolt::Logger.debug("Invalid token at GITLAB_TOKEN, unable to calculate SHA.")
72
+ nil
73
+ when Net::HTTPForbidden
74
+ message = "GitLab API rate limit exceeded, unable to calculate SHA."
75
+
76
+ unless ENV['GITLAB_TOKEN']
77
+ message += " To increase your rate limit, set the GITLAB_TOKEN environment "\
78
+ "variable with a GitLab personal access token."
79
+ end
80
+
81
+ Bolt::Logger.debug(message)
82
+ nil
83
+ else
84
+ Bolt::Logger.debug("Unable to calculate SHA for ref #{ref}")
85
+ nil
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -7,8 +7,10 @@ require_relative 'specs/git_spec'
7
7
  module Bolt
8
8
  class ModuleInstaller
9
9
  class Specs
10
- def initialize(specs = [])
11
- @specs = []
10
+ def initialize(specs = [], config = {})
11
+ @specs = []
12
+ @config = config
13
+
12
14
  add_specs(specs)
13
15
  assert_unique_names
14
16
  end
@@ -49,7 +51,7 @@ module Bolt
49
51
  #
50
52
  private def spec_from_hash(hash)
51
53
  return ForgeSpec.new(hash) if ForgeSpec.implements?(hash)
52
- return GitSpec.new(hash) if GitSpec.implements?(hash)
54
+ return GitSpec.new(hash, @config) if GitSpec.implements?(hash)
53
55
 
54
56
  raise Bolt::ValidationError, <<~MESSAGE.chomp
55
57
  Invalid module specification:
@@ -18,7 +18,7 @@ module Bolt
18
18
  # Adds a single module to the project.
19
19
  #
20
20
  def add(name, specs, puppetfile_path, moduledir, project_file, config)
21
- project_specs = Specs.new(specs)
21
+ project_specs = Specs.new(specs, config)
22
22
 
23
23
  # Exit early if project config already includes a spec with this name.
24
24
  if project_specs.include?(name)
@@ -151,7 +151,7 @@ module Bolt
151
151
  @outputter.print_message("Installing project modules\n\n")
152
152
 
153
153
  if resolve != false
154
- specs = Specs.new(specs)
154
+ specs = Specs.new(specs, config)
155
155
 
156
156
  # If forcibly installing or if there is no Puppetfile, resolve
157
157
  # and write a Puppetfile.
data/lib/bolt/result.rb CHANGED
@@ -200,16 +200,18 @@ module Bolt
200
200
 
201
201
  def to_data
202
202
  serialized_value = safe_value
203
+
203
204
  if serialized_value.key?('_sensitive') &&
204
205
  serialized_value['_sensitive'].is_a?(Puppet::Pops::Types::PSensitiveType::Sensitive)
205
206
  serialized_value['_sensitive'] = serialized_value['_sensitive'].to_s
206
207
  end
208
+
207
209
  {
208
210
  "target" => @target.name,
209
211
  "action" => action,
210
212
  "object" => object,
211
213
  "status" => status,
212
- "value" => serialized_value
214
+ "value" => serialized_value
213
215
  }
214
216
  end
215
217
 
data/lib/bolt/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bolt
4
- VERSION = '3.24.0'
4
+ VERSION = '3.25.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bolt
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.24.0
4
+ version: 3.25.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Puppet
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-06-29 00:00:00.000000000 Z
11
+ date: 2022-07-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: addressable
@@ -222,16 +222,22 @@ dependencies:
222
222
  name: puppetfile-resolver
223
223
  requirement: !ruby/object:Gem::Requirement
224
224
  requirements:
225
- - - "~>"
225
+ - - ">="
226
226
  - !ruby/object:Gem::Version
227
- version: '0.5'
227
+ version: 0.6.2
228
+ - - "<"
229
+ - !ruby/object:Gem::Version
230
+ version: '1.0'
228
231
  type: :runtime
229
232
  prerelease: false
230
233
  version_requirements: !ruby/object:Gem::Requirement
231
234
  requirements:
232
- - - "~>"
235
+ - - ">="
233
236
  - !ruby/object:Gem::Version
234
- version: '0.5'
237
+ version: 0.6.2
238
+ - - "<"
239
+ - !ruby/object:Gem::Version
240
+ version: '1.0'
235
241
  - !ruby/object:Gem::Dependency
236
242
  name: puppet-resource_api
237
243
  requirement: !ruby/object:Gem::Requirement
@@ -526,6 +532,10 @@ files:
526
532
  - lib/bolt/module_installer/specs.rb
527
533
  - lib/bolt/module_installer/specs/forge_spec.rb
528
534
  - lib/bolt/module_installer/specs/git_spec.rb
535
+ - lib/bolt/module_installer/specs/id/base.rb
536
+ - lib/bolt/module_installer/specs/id/gitclone.rb
537
+ - lib/bolt/module_installer/specs/id/github.rb
538
+ - lib/bolt/module_installer/specs/id/gitlab.rb
529
539
  - lib/bolt/node/errors.rb
530
540
  - lib/bolt/node/output.rb
531
541
  - lib/bolt/outputter.rb