bolt 2.31.0 → 2.35.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.

Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +7 -7
  3. data/bolt-modules/boltlib/lib/puppet/functions/catch_errors.rb +1 -3
  4. data/bolt-modules/boltlib/lib/puppet/functions/download_file.rb +17 -6
  5. data/bolt-modules/boltlib/lib/puppet/functions/facts.rb +6 -0
  6. data/bolt-modules/boltlib/lib/puppet/functions/parallelize.rb +56 -0
  7. data/bolt-modules/boltlib/lib/puppet/functions/puppetdb_query.rb +2 -2
  8. data/bolt-modules/boltlib/lib/puppet/functions/run_command.rb +24 -6
  9. data/bolt-modules/boltlib/lib/puppet/functions/run_script.rb +27 -8
  10. data/bolt-modules/boltlib/lib/puppet/functions/run_task.rb +21 -1
  11. data/bolt-modules/boltlib/lib/puppet/functions/run_task_with.rb +18 -1
  12. data/bolt-modules/boltlib/lib/puppet/functions/upload_file.rb +24 -6
  13. data/bolt-modules/out/lib/puppet/functions/out/message.rb +44 -1
  14. data/bolt-modules/prompt/lib/puppet/functions/prompt.rb +3 -0
  15. data/guides/logging.txt +18 -0
  16. data/guides/module.txt +19 -0
  17. data/guides/modulepath.txt +25 -0
  18. data/lib/bolt/bolt_option_parser.rb +6 -1
  19. data/lib/bolt/cli.rb +70 -144
  20. data/lib/bolt/config/options.rb +35 -17
  21. data/lib/bolt/config/transport/options.rb +1 -1
  22. data/lib/bolt/error.rb +37 -3
  23. data/lib/bolt/executor.rb +111 -13
  24. data/lib/bolt/inventory/group.rb +2 -1
  25. data/lib/bolt/module_installer.rb +71 -115
  26. data/lib/bolt/{puppetfile → module_installer}/installer.rb +3 -2
  27. data/lib/bolt/module_installer/puppetfile.rb +117 -0
  28. data/lib/bolt/module_installer/puppetfile/forge_module.rb +54 -0
  29. data/lib/bolt/module_installer/puppetfile/git_module.rb +37 -0
  30. data/lib/bolt/module_installer/puppetfile/module.rb +26 -0
  31. data/lib/bolt/module_installer/resolver.rb +76 -0
  32. data/lib/bolt/module_installer/specs.rb +93 -0
  33. data/lib/bolt/module_installer/specs/forge_spec.rb +85 -0
  34. data/lib/bolt/module_installer/specs/git_spec.rb +179 -0
  35. data/lib/bolt/outputter.rb +0 -47
  36. data/lib/bolt/outputter/human.rb +23 -11
  37. data/lib/bolt/outputter/json.rb +1 -1
  38. data/lib/bolt/pal.rb +48 -30
  39. data/lib/bolt/pal/yaml_plan.rb +11 -2
  40. data/lib/bolt/pal/yaml_plan/evaluator.rb +23 -1
  41. data/lib/bolt/pal/yaml_plan/loader.rb +14 -9
  42. data/lib/bolt/plan_creator.rb +160 -0
  43. data/lib/bolt/plugin.rb +1 -1
  44. data/lib/bolt/project.rb +5 -10
  45. data/lib/bolt/project_migrator/config.rb +2 -1
  46. data/lib/bolt/project_migrator/inventory.rb +2 -2
  47. data/lib/bolt/project_migrator/modules.rb +10 -8
  48. data/lib/bolt/puppetdb/client.rb +3 -2
  49. data/lib/bolt/puppetdb/config.rb +8 -6
  50. data/lib/bolt/result.rb +23 -11
  51. data/lib/bolt/shell/bash.rb +11 -6
  52. data/lib/bolt/shell/powershell.rb +12 -7
  53. data/lib/bolt/task/run.rb +1 -1
  54. data/lib/bolt/transport/base.rb +18 -18
  55. data/lib/bolt/transport/docker.rb +23 -6
  56. data/lib/bolt/transport/orch.rb +23 -19
  57. data/lib/bolt/transport/orch/connection.rb +10 -3
  58. data/lib/bolt/transport/remote.rb +3 -3
  59. data/lib/bolt/transport/simple.rb +6 -6
  60. data/lib/bolt/util.rb +5 -0
  61. data/lib/bolt/version.rb +1 -1
  62. data/lib/bolt/yarn.rb +23 -0
  63. data/lib/bolt_server/file_cache.rb +2 -0
  64. data/lib/bolt_server/schemas/partials/task.json +17 -2
  65. data/lib/bolt_server/transport_app.rb +38 -7
  66. data/lib/bolt_spec/plans/action_stubs/command_stub.rb +1 -1
  67. data/lib/bolt_spec/plans/action_stubs/script_stub.rb +1 -1
  68. data/lib/bolt_spec/plans/mock_executor.rb +9 -6
  69. metadata +25 -8
  70. data/lib/bolt/puppetfile.rb +0 -149
  71. data/lib/bolt/puppetfile/module.rb +0 -93
  72. data/modules/secure_env_vars/plans/init.pp +0 -20
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'set'
5
+
6
+ require 'bolt/error'
7
+
8
+ # This class represents a Git module specification.
9
+ #
10
+ module Bolt
11
+ class ModuleInstaller
12
+ class Specs
13
+ class GitSpec
14
+ NAME_REGEX = %r{\A(?:[a-zA-Z0-9]+[-/])?(?<name>[a-z][a-z0-9_]*)\z}.freeze
15
+ REQUIRED_KEYS = Set.new(%w[git ref]).freeze
16
+
17
+ attr_reader :git, :ref, :type
18
+
19
+ def initialize(init_hash)
20
+ @name = parse_name(init_hash['name'])
21
+ @git, @repo = parse_git(init_hash['git'])
22
+ @ref = init_hash['ref']
23
+ @type = :git
24
+ end
25
+
26
+ def self.implements?(hash)
27
+ REQUIRED_KEYS == hash.keys.to_set
28
+ end
29
+
30
+ # Parses the name into owner and name segments, and formats the full
31
+ # name.
32
+ #
33
+ private def parse_name(name)
34
+ return unless name
35
+
36
+ unless (match = name.match(NAME_REGEX))
37
+ raise Bolt::ValidationError,
38
+ "Invalid name for Git module specification: #{name}. Name must match "\
39
+ "'name' or 'owner/name'. Owner segment may only include letters or digits. "\
40
+ "Name segment must start with a lowercase letter and may only include "\
41
+ "lowercase letters, digits, and underscores."
42
+ end
43
+
44
+ match[:name]
45
+ end
46
+
47
+ # Gets the repo from the git URL.
48
+ #
49
+ private def parse_git(git)
50
+ repo = if git.start_with?('git@github.com:')
51
+ git.split('git@github.com:').last.split('.git').first
52
+ elsif git.start_with?('https://github.com')
53
+ git.split('https://github.com/').last.split('.git').first
54
+ else
55
+ raise Bolt::ValidationError,
56
+ "Invalid git source: #{git}. Only GitHub modules are supported."
57
+ end
58
+
59
+ [git, repo]
60
+ end
61
+
62
+ # Returns true if the specification is satisfied by the module.
63
+ #
64
+ def satisfied_by?(mod)
65
+ @type == mod.type && @git == mod.git
66
+ end
67
+
68
+ # Returns a hash matching the module spec in bolt-project.yaml
69
+ #
70
+ def to_hash
71
+ {
72
+ 'git' => @git,
73
+ 'ref' => @ref
74
+ }
75
+ end
76
+
77
+ # Returns a PuppetfileResolver::Model::GitModule object for resolving.
78
+ #
79
+ def to_resolver_module
80
+ require 'puppetfile-resolver'
81
+
82
+ PuppetfileResolver::Puppetfile::GitModule.new(name).tap do |mod|
83
+ mod.remote = @git
84
+ mod.ref = sha
85
+ end
86
+ end
87
+
88
+ # Resolves the module's title from the module metadata. This is lazily
89
+ # resolved since Bolt does not always need to know a Git module's name.
90
+ #
91
+ def name
92
+ @name ||= begin
93
+ url = "https://raw.githubusercontent.com/#{@repo}/#{sha}/metadata.json"
94
+ response = make_request(:Get, url)
95
+
96
+ case response
97
+ when Net::HTTPOK
98
+ body = JSON.parse(response.body)
99
+
100
+ unless body.key?('name')
101
+ raise Bolt::Error.new(
102
+ "Missing name in metadata.json at #{git}. This is not a valid module.",
103
+ "bolt/missing-module-name-error"
104
+ )
105
+ end
106
+
107
+ parse_name(body['name'])
108
+ else
109
+ raise Bolt::Error.new(
110
+ "Missing metadata.json at #{git}. This is not a valid module.",
111
+ "bolt/missing-module-metadata-error"
112
+ )
113
+ end
114
+ end
115
+ end
116
+
117
+ # Resolves the SHA for the specified ref. This is lazily resolved since
118
+ # Bolt does not always need to know a Git module's SHA.
119
+ #
120
+ def sha
121
+ @sha ||= begin
122
+ url = "https://api.github.com/repos/#{@repo}/commits/#{ref}"
123
+ headers = ENV['GITHUB_TOKEN'] ? { "Authorization" => "token #{ENV['GITHUB_TOKEN']}" } : {}
124
+ response = make_request(:Get, url, headers)
125
+
126
+ case response
127
+ when Net::HTTPOK
128
+ body = JSON.parse(response.body)
129
+ body['sha']
130
+ when Net::HTTPUnauthorized
131
+ raise Bolt::Error.new(
132
+ "Invalid token at GITHUB_TOKEN, unable to resolve git modules.",
133
+ "bolt/invalid-git-token-error"
134
+ )
135
+ when Net::HTTPForbidden
136
+ message = "GitHub API rate limit exceeded, unable to resolve git modules. "
137
+
138
+ unless ENV['GITHUB_TOKEN']
139
+ message += "To increase your rate limit, set the GITHUB_TOKEN environment "\
140
+ "variable with a GitHub personal access token."
141
+ end
142
+
143
+ raise Bolt::Error.new(message, 'bolt/github-api-rate-limit-error')
144
+ when Net::HTTPNotFound
145
+ raise Bolt::Error.new(
146
+ "#{git} is not a git repository.",
147
+ "bolt/missing-git-repository-error"
148
+ )
149
+ else
150
+ raise Bolt::Error.new(
151
+ "Ref #{ref} at #{git} is not a commit, tag, or branch.",
152
+ "bolt/invalid-git-ref-error"
153
+ )
154
+ end
155
+ end
156
+ end
157
+
158
+ # Makes a generic HTTP request.
159
+ #
160
+ private def make_request(verb, url, headers = {})
161
+ require 'net/http'
162
+
163
+ uri = URI.parse(url)
164
+ opts = { use_ssl: uri.scheme == 'https' }
165
+
166
+ Net::HTTP.start(uri.host, uri.port, opts) do |client|
167
+ request = Net::HTTP.const_get(verb).new(uri, headers)
168
+ client.request(request)
169
+ end
170
+ rescue StandardError => e
171
+ raise Bolt::Error.new(
172
+ "Failed to connect to #{uri}: #{e.message}",
173
+ "bolt/http-connect-error"
174
+ )
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end
@@ -27,10 +27,6 @@ module Bolt
27
27
  string.gsub(/^/, indent.to_s)
28
28
  end
29
29
 
30
- def print_message_event(event)
31
- print_message(stringify(event[:message]))
32
- end
33
-
34
30
  def print_message
35
31
  raise NotImplementedError, "print_message() must be implemented by the outputter class"
36
32
  end
@@ -38,49 +34,6 @@ module Bolt
38
34
  def print_error
39
35
  raise NotImplementedError, "print_error() must be implemented by the outputter class"
40
36
  end
41
-
42
- def stringify(message)
43
- formatted = format_message(message)
44
- if formatted.is_a?(Hash) || formatted.is_a?(Array)
45
- ::JSON.pretty_generate(formatted)
46
- else
47
- formatted
48
- end
49
- end
50
-
51
- def format_message(message)
52
- case message
53
- when Array
54
- message.map { |item| format_message(item) }
55
- when Bolt::ApplyResult
56
- format_apply_result(message)
57
- when Bolt::Result, Bolt::ResultSet
58
- # This is equivalent to to_s, but formattable
59
- message.to_data
60
- when Bolt::RunFailure
61
- formatted_resultset = message.result_set.to_data
62
- message.to_h.merge('result_set' => formatted_resultset)
63
- when Hash
64
- message.each_with_object({}) do |(k, v), h|
65
- h[format_message(k)] = format_message(v)
66
- end
67
- when Integer, Float, NilClass
68
- message
69
- else
70
- message.to_s
71
- end
72
- end
73
-
74
- def format_apply_result(result)
75
- logs = result.resource_logs&.map do |log|
76
- # Omit low-level info/debug messages
77
- next if %w[info debug].include?(log['level'])
78
- indent(2, format_log(log))
79
- end
80
- hash = result.to_data
81
- hash['logs'] = logs unless logs.empty?
82
- hash
83
- end
84
37
  end
85
38
  end
86
39
 
@@ -45,7 +45,7 @@ module Bolt
45
45
  when :disable_default_output
46
46
  @disable_depth += 1
47
47
  when :message
48
- print_message_event(event)
48
+ print_message(event[:message])
49
49
  end
50
50
 
51
51
  if enabled?
@@ -214,9 +214,10 @@ module Bolt
214
214
  end
215
215
 
216
216
  def print_tasks(tasks, modulepath)
217
- print_table(tasks)
217
+ command = Bolt::Util.powershell? ? 'Get-BoltTask -Task <TASK NAME>' : 'bolt task show <TASK NAME>'
218
+ tasks.any? ? print_table(tasks) : print_message('No available tasks')
218
219
  print_message("\nMODULEPATH:\n#{modulepath.join(File::PATH_SEPARATOR)}\n"\
219
- "\nUse `bolt task show <task-name>` to view "\
220
+ "\nUse '#{command}' to view "\
220
221
  "details and parameters for a specific task.")
221
222
  end
222
223
 
@@ -225,20 +226,26 @@ module Bolt
225
226
  # Building lots of strings...
226
227
  pretty_params = +""
227
228
  task_info = +""
228
- usage = +"bolt task run --targets <node-name> #{task.name}"
229
+ usage = if Bolt::Util.powershell?
230
+ +"Invoke-BoltTask -Name #{task.name} -Targets <targets>"
231
+ else
232
+ +"bolt task run #{task.name} --targets <targets>"
233
+ end
229
234
 
230
235
  task.parameters&.each do |k, v|
231
236
  pretty_params << "- #{k}: #{v['type'] || 'Any'}\n"
232
237
  pretty_params << " Default: #{v['default'].inspect}\n" if v.key?('default')
233
238
  pretty_params << " #{v['description']}\n" if v['description']
234
- usage << if v['type'].is_a?(Puppet::Pops::Types::POptionalType)
239
+ usage << if v['type'].start_with?("Optional")
235
240
  " [#{k}=<value>]"
236
241
  else
237
242
  " #{k}=<value>"
238
243
  end
239
244
  end
240
245
 
241
- usage << " [--noop]" if task.supports_noop
246
+ if task.supports_noop
247
+ usage << Bolt::Util.powershell? ? '[-Noop]' : '[--noop]'
248
+ end
242
249
 
243
250
  task_info << "\n#{task.name}"
244
251
  task_info << " - #{task.description}" if task.description
@@ -261,7 +268,11 @@ module Bolt
261
268
  # Building lots of strings...
262
269
  pretty_params = +""
263
270
  plan_info = +""
264
- usage = +"bolt plan run #{plan['name']}"
271
+ usage = if Bolt::Util.powershell?
272
+ +"Invoke-BoltPlan -Name #{plan['name']}"
273
+ else
274
+ +"bolt plan run #{plan['name']}"
275
+ end
265
276
 
266
277
  plan['parameters'].each do |name, p|
267
278
  pretty_params << "- #{name}: #{p['type']}\n"
@@ -287,16 +298,17 @@ module Bolt
287
298
  end
288
299
 
289
300
  def print_plans(plans, modulepath)
290
- print_table(plans)
301
+ command = Bolt::Util.powershell? ? 'Get-BoltPlan -Name <PLAN NAME>' : 'bolt plan show <PLAN NAME>'
302
+ plans.any? ? print_table(plans) : print_message('No available plans')
291
303
  print_message("\nMODULEPATH:\n#{modulepath.join(File::PATH_SEPARATOR)}\n"\
292
- "\nUse `bolt plan show <plan-name>` to view "\
304
+ "\nUse '#{command}' to view "\
293
305
  "details and parameters for a specific plan.")
294
306
  end
295
307
 
296
308
  def print_topics(topics)
297
309
  print_message("Available topics are:")
298
310
  print_message(topics.join("\n"))
299
- print_message("\nUse `bolt guide <topic>` to view a specific guide.")
311
+ print_message("\nUse 'bolt guide <TOPIC>' to view a specific guide.")
300
312
  end
301
313
 
302
314
  def print_guide(guide, _topic)
@@ -344,7 +356,7 @@ module Bolt
344
356
  end
345
357
 
346
358
  @stream.puts "INVENTORY FILE:"
347
- if inventoryfile.exist?
359
+ if File.exist?(inventoryfile)
348
360
  @stream.puts inventoryfile
349
361
  else
350
362
  @stream.puts wrap("Tried to load inventory from #{inventoryfile}, but the file does not exist")
@@ -22,7 +22,7 @@ module Bolt
22
22
  when :node_result
23
23
  print_result(event[:result])
24
24
  when :message
25
- print_message_event(event)
25
+ print_message(event[:message])
26
26
  end
27
27
  end
28
28
 
@@ -14,17 +14,11 @@ module Bolt
14
14
  # Bolt::Errors
15
15
  class PALError < Bolt::Error
16
16
  def self.from_preformatted_error(err)
17
- if err.cause.is_a? Bolt::Error
18
- err.cause
19
- else
20
- from_error(err)
21
- end
22
- end
23
-
24
- # Generate a Bolt::Pal::PALError for non-bolt errors
25
- def self.from_error(err)
26
- # Use the original error message if available
27
- message = err.cause ? err.cause.message : err.message
17
+ error = if err.cause.is_a? Bolt::Error
18
+ err.cause
19
+ else
20
+ from_error(err)
21
+ end
28
22
 
29
23
  # Provide the location of an error if it came from a plan
30
24
  details = {}
@@ -32,8 +26,15 @@ module Bolt
32
26
  details[:line] = err.line if defined?(err.line)
33
27
  details[:column] = err.pos if defined?(err.pos)
34
28
 
35
- e = new(message, details.compact)
29
+ error.add_filelineno(details.compact)
30
+ error
31
+ end
36
32
 
33
+ # Generate a Bolt::Pal::PALError for non-bolt errors
34
+ def self.from_error(err)
35
+ # Use the original error message if available
36
+ message = err.cause ? err.cause.message : err.message
37
+ e = new(message)
37
38
  e.set_backtrace(err.backtrace)
38
39
  e
39
40
  end
@@ -256,19 +257,24 @@ module Bolt
256
257
 
257
258
  # TODO: PUP-8553 should replace this
258
259
  def with_puppet_settings
259
- Dir.mktmpdir('bolt') do |dir|
260
- cli = []
261
- Puppet::Settings::REQUIRED_APP_SETTINGS.each do |setting|
262
- cli << "--#{setting}" << dir
263
- end
264
- Puppet.settings.send(:clear_everything_for_tests)
265
- Puppet.initialize_settings(cli)
266
- Puppet::GettextConfig.create_default_text_domain
267
- Puppet[:trusted_external_command] = @trusted_external
268
- Puppet.settings[:hiera_config] = @hiera_config
269
- self.class.configure_logging
270
- yield
260
+ dir = Dir.mktmpdir('bolt')
261
+
262
+ cli = []
263
+ Puppet::Settings::REQUIRED_APP_SETTINGS.each do |setting|
264
+ cli << "--#{setting}" << dir
271
265
  end
266
+ Puppet.settings.send(:clear_everything_for_tests)
267
+ Puppet.initialize_settings(cli)
268
+ Puppet::GettextConfig.create_default_text_domain
269
+ Puppet[:trusted_external_command] = @trusted_external
270
+ Puppet.settings[:hiera_config] = @hiera_config
271
+ self.class.configure_logging
272
+ yield
273
+ ensure
274
+ # Delete the tmpdir if it still exists. This check is needed to
275
+ # prevent Bolt from erroring if the tmpdir is somehow deleted
276
+ # before reaching this point.
277
+ FileUtils.remove_entry_secure(dir) if File.exist?(dir)
272
278
  end
273
279
 
274
280
  # Parses a snippet of Puppet manifest code and returns the AST represented
@@ -280,15 +286,26 @@ module Bolt
280
286
  raise Bolt::PAL::PALError, "Failed to parse manifest: #{e}"
281
287
  end
282
288
 
283
- def list_tasks
289
+ # Filters content by a list of names and glob patterns specified in project
290
+ # configuration.
291
+ def filter_content(content, patterns)
292
+ return content unless content && patterns
293
+
294
+ content.select do |name,|
295
+ patterns.any? { |pattern| File.fnmatch?(pattern, name, File::FNM_EXTGLOB) }
296
+ end
297
+ end
298
+
299
+ def list_tasks(filter_content: false)
284
300
  in_bolt_compiler do |compiler|
285
- tasks = compiler.list_tasks
286
- tasks.map(&:name).sort.each_with_object([]) do |task_name, data|
301
+ tasks = compiler.list_tasks.map(&:name).sort.each_with_object([]) do |task_name, data|
287
302
  task_sig = compiler.task_signature(task_name)
288
303
  unless task_sig.task_hash['metadata']['private']
289
304
  data << [task_name, task_sig.task_hash['metadata']['description']]
290
305
  end
291
306
  end
307
+
308
+ filter_content ? filter_content(tasks, @project&.tasks) : tasks
292
309
  end
293
310
  end
294
311
 
@@ -340,14 +357,15 @@ module Bolt
340
357
  Bolt::Task.from_task_signature(task)
341
358
  end
342
359
 
343
- def list_plans
360
+ def list_plans(filter_content: false)
344
361
  in_bolt_compiler do |compiler|
345
362
  errors = []
346
363
  plans = compiler.list_plans(nil, errors).map { |plan| [plan.name] }.sort
347
364
  errors.each do |error|
348
365
  @logger.warn(error.details['original_error'])
349
366
  end
350
- plans
367
+
368
+ filter_content ? filter_content(plans, @project&.plans) : plans
351
369
  end
352
370
  end
353
371
 
@@ -384,7 +402,7 @@ module Bolt
384
402
  plan.docstring
385
403
  end
386
404
 
387
- defaults = plan.parameters.reject { |_, value| value.nil? }.to_h
405
+ defaults = plan.parameters.to_h.compact
388
406
  signature_params = Set.new(plan.parameters.map(&:first))
389
407
  parameters = plan.tags(:param).each_with_object({}) do |param, params|
390
408
  name = param.name