bolt 2.29.0 → 2.33.2

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 (79) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +15 -14
  3. data/bolt-modules/boltlib/lib/puppet/functions/download_file.rb +1 -1
  4. data/bolt-modules/boltlib/lib/puppet/functions/facts.rb +6 -0
  5. data/bolt-modules/boltlib/lib/puppet/functions/puppetdb_query.rb +2 -2
  6. data/bolt-modules/boltlib/lib/puppet/functions/run_command.rb +1 -1
  7. data/bolt-modules/boltlib/lib/puppet/functions/run_script.rb +1 -1
  8. data/bolt-modules/boltlib/lib/puppet/functions/run_task.rb +1 -1
  9. data/bolt-modules/boltlib/lib/puppet/functions/run_task_with.rb +1 -1
  10. data/bolt-modules/boltlib/lib/puppet/functions/upload_file.rb +1 -1
  11. data/bolt-modules/boltlib/lib/puppet/functions/write_file.rb +2 -2
  12. data/bolt-modules/out/lib/puppet/functions/out/message.rb +44 -1
  13. data/bolt-modules/prompt/lib/puppet/functions/prompt.rb +3 -0
  14. data/guides/logging.txt +18 -0
  15. data/guides/module.txt +19 -0
  16. data/guides/modulepath.txt +25 -0
  17. data/lib/bolt/bolt_option_parser.rb +48 -9
  18. data/lib/bolt/catalog.rb +1 -1
  19. data/lib/bolt/cli.rb +154 -116
  20. data/lib/bolt/config.rb +13 -1
  21. data/lib/bolt/config/modulepath.rb +30 -0
  22. data/lib/bolt/config/options.rb +32 -13
  23. data/lib/bolt/config/transport/options.rb +2 -2
  24. data/lib/bolt/error.rb +4 -0
  25. data/lib/bolt/executor.rb +13 -13
  26. data/lib/bolt/inventory.rb +10 -9
  27. data/lib/bolt/module_installer.rb +198 -0
  28. data/lib/bolt/{puppetfile → module_installer}/installer.rb +3 -2
  29. data/lib/bolt/module_installer/puppetfile.rb +117 -0
  30. data/lib/bolt/module_installer/puppetfile/forge_module.rb +54 -0
  31. data/lib/bolt/module_installer/puppetfile/git_module.rb +37 -0
  32. data/lib/bolt/module_installer/puppetfile/module.rb +26 -0
  33. data/lib/bolt/module_installer/resolver.rb +76 -0
  34. data/lib/bolt/module_installer/specs.rb +93 -0
  35. data/lib/bolt/module_installer/specs/forge_spec.rb +85 -0
  36. data/lib/bolt/module_installer/specs/git_spec.rb +179 -0
  37. data/lib/bolt/outputter.rb +2 -45
  38. data/lib/bolt/outputter/human.rb +78 -18
  39. data/lib/bolt/outputter/json.rb +22 -7
  40. data/lib/bolt/outputter/logger.rb +2 -2
  41. data/lib/bolt/pal.rb +55 -45
  42. data/lib/bolt/pal/yaml_plan.rb +4 -2
  43. data/lib/bolt/pal/yaml_plan/evaluator.rb +23 -1
  44. data/lib/bolt/pal/yaml_plan/loader.rb +14 -9
  45. data/lib/bolt/plugin.rb +1 -1
  46. data/lib/bolt/plugin/module.rb +1 -1
  47. data/lib/bolt/project.rb +32 -21
  48. data/lib/bolt/project_migrator.rb +80 -0
  49. data/lib/bolt/project_migrator/base.rb +39 -0
  50. data/lib/bolt/project_migrator/config.rb +67 -0
  51. data/lib/bolt/project_migrator/inventory.rb +67 -0
  52. data/lib/bolt/project_migrator/modules.rb +200 -0
  53. data/lib/bolt/result.rb +23 -11
  54. data/lib/bolt/shell/bash.rb +15 -9
  55. data/lib/bolt/shell/powershell.rb +11 -6
  56. data/lib/bolt/transport/base.rb +18 -18
  57. data/lib/bolt/transport/docker.rb +23 -6
  58. data/lib/bolt/transport/orch.rb +23 -14
  59. data/lib/bolt/transport/remote.rb +2 -2
  60. data/lib/bolt/transport/simple.rb +6 -6
  61. data/lib/bolt/transport/ssh/connection.rb +1 -1
  62. data/lib/bolt/util.rb +22 -0
  63. data/lib/bolt/version.rb +1 -1
  64. data/lib/bolt_server/acl.rb +2 -2
  65. data/lib/bolt_server/base_config.rb +3 -3
  66. data/lib/bolt_server/schemas/partials/task.json +17 -2
  67. data/lib/bolt_server/transport_app.rb +92 -12
  68. data/lib/bolt_spec/bolt_context.rb +4 -2
  69. data/lib/bolt_spec/plans.rb +1 -1
  70. data/lib/bolt_spec/plans/action_stubs/command_stub.rb +1 -1
  71. data/lib/bolt_spec/plans/action_stubs/script_stub.rb +1 -1
  72. data/lib/bolt_spec/plans/mock_executor.rb +6 -6
  73. data/lib/bolt_spec/run.rb +1 -1
  74. metadata +29 -10
  75. data/lib/bolt/project_migrate.rb +0 -138
  76. data/lib/bolt/puppetfile.rb +0 -160
  77. data/lib/bolt/puppetfile/module.rb +0 -89
  78. data/lib/bolt_server/pe/pal.rb +0 -67
  79. 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,55 +27,12 @@ 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
37
33
 
38
- def stringify(message)
39
- formatted = format_message(message)
40
- if formatted.is_a?(Hash) || formatted.is_a?(Array)
41
- ::JSON.pretty_generate(formatted)
42
- else
43
- formatted
44
- end
45
- end
46
-
47
- def format_message(message)
48
- case message
49
- when Array
50
- message.map { |item| format_message(item) }
51
- when Bolt::ApplyResult
52
- format_apply_result(message)
53
- when Bolt::Result, Bolt::ResultSet
54
- # This is equivalent to to_s, but formattable
55
- message.to_data
56
- when Bolt::RunFailure
57
- formatted_resultset = message.result_set.to_data
58
- message.to_h.merge('result_set' => formatted_resultset)
59
- when Hash
60
- message.each_with_object({}) do |(k, v), h|
61
- h[format_message(k)] = format_message(v)
62
- end
63
- when Integer, Float, NilClass
64
- message
65
- else
66
- message.to_s
67
- end
68
- end
69
-
70
- def format_apply_result(result)
71
- logs = result.resource_logs&.map do |log|
72
- # Omit low-level info/debug messages
73
- next if %w[info debug].include?(log['level'])
74
- indent(2, format_log(log))
75
- end
76
- hash = result.to_data
77
- hash['logs'] = logs unless logs.empty?
78
- hash
34
+ def print_error
35
+ raise NotImplementedError, "print_error() must be implemented by the outputter class"
79
36
  end
80
37
  end
81
38
  end
@@ -5,9 +5,12 @@ require 'bolt/pal'
5
5
  module Bolt
6
6
  class Outputter
7
7
  class Human < Bolt::Outputter
8
- COLORS = { red: "31",
9
- green: "32",
10
- yellow: "33" }.freeze
8
+ COLORS = {
9
+ red: "31",
10
+ green: "32",
11
+ yellow: "33",
12
+ cyan: "36"
13
+ }.freeze
11
14
 
12
15
  def print_head; end
13
16
 
@@ -31,6 +34,10 @@ module Bolt
31
34
  string.sub(/\s\z/, '')
32
35
  end
33
36
 
37
+ def wrap(string, width = 80)
38
+ string.gsub(/(.{1,#{width}})(\s+|\Z)/, "\\1\n")
39
+ end
40
+
34
41
  def handle_event(event)
35
42
  case event[:type]
36
43
  when :enable_default_output
@@ -38,7 +45,7 @@ module Bolt
38
45
  when :disable_default_output
39
46
  @disable_depth += 1
40
47
  when :message
41
- print_message_event(event)
48
+ print_message(event[:message])
42
49
  end
43
50
 
44
51
  if enabled?
@@ -48,9 +55,9 @@ module Bolt
48
55
  when :node_result
49
56
  print_result(event[:result]) if @verbose
50
57
  when :step_start
51
- print_step_start(event) if plan_logging?
58
+ print_step_start(**event) if plan_logging?
52
59
  when :step_finish
53
- print_step_finish(event) if plan_logging?
60
+ print_step_finish(**event) if plan_logging?
54
61
  when :plan_start
55
62
  print_plan_start(event)
56
63
  when :plan_finish
@@ -188,7 +195,7 @@ module Bolt
188
195
  @stream.puts total_msg
189
196
  end
190
197
 
191
- def print_table(results)
198
+ def print_table(results, padding_left = 0, padding_right = 3)
192
199
  # lazy-load expensive gem code
193
200
  require 'terminal-table'
194
201
 
@@ -198,8 +205,8 @@ module Bolt
198
205
  border_x: '',
199
206
  border_y: '',
200
207
  border_i: '',
201
- padding_left: 0,
202
- padding_right: 3,
208
+ padding_left: padding_left,
209
+ padding_right: padding_right,
203
210
  border_top: false,
204
211
  border_bottom: false
205
212
  }
@@ -241,7 +248,7 @@ module Bolt
241
248
  task_info << "MODULE:\n"
242
249
 
243
250
  path = task.files.first['path'].chomp("/tasks/#{task.files.first['name']}")
244
- task_info << if path.start_with?(Bolt::PAL::MODULES_PATH)
251
+ task_info << if path.start_with?(Bolt::Config::Modulepath::MODULES_PATH)
245
252
  "built-in module"
246
253
  else
247
254
  path
@@ -271,7 +278,7 @@ module Bolt
271
278
  plan_info << "MODULE:\n"
272
279
 
273
280
  path = plan['module']
274
- plan_info << if path.start_with?(Bolt::PAL::MODULES_PATH)
281
+ plan_info << if path.start_with?(Bolt::Config::Modulepath::MODULES_PATH)
275
282
  "built-in module"
276
283
  else
277
284
  path
@@ -299,9 +306,9 @@ module Bolt
299
306
  def print_module_list(module_list)
300
307
  module_list.each do |path, modules|
301
308
  if (mod = modules.find { |m| m[:internal_module_group] })
302
- @stream.puts(mod[:internal_module_group])
309
+ @stream.puts(colorize(:cyan, mod[:internal_module_group]))
303
310
  else
304
- @stream.puts(path)
311
+ @stream.puts(colorize(:cyan, path))
305
312
  end
306
313
 
307
314
  if modules.empty?
@@ -317,17 +324,35 @@ module Bolt
317
324
  [m[:name], version]
318
325
  end
319
326
 
320
- print_table(module_info)
327
+ print_table(module_info, 2, 1)
321
328
  end
322
329
 
323
330
  @stream.write("\n")
324
331
  end
325
332
  end
326
333
 
327
- def print_targets(targets)
328
- count = "#{targets.count} target#{'s' unless targets.count == 1}"
329
- @stream.puts targets.map(&:name).join("\n")
330
- @stream.puts colorize(:green, count)
334
+ def print_targets(target_list, inventoryfile)
335
+ adhoc = colorize(:yellow, "(Not found in inventory file)")
336
+
337
+ targets = []
338
+ targets += target_list[:inventory].map { |target| [target.name, nil] }
339
+ targets += target_list[:adhoc].map { |target| [target.name, adhoc] }
340
+
341
+ if targets.any?
342
+ print_table(targets, 0, 2)
343
+ @stream.puts
344
+ end
345
+
346
+ @stream.puts "INVENTORY FILE:"
347
+ if File.exist?(inventoryfile)
348
+ @stream.puts inventoryfile
349
+ else
350
+ @stream.puts wrap("Tried to load inventory from #{inventoryfile}, but the file does not exist")
351
+ end
352
+
353
+ @stream.puts "\nTARGET COUNT:"
354
+ @stream.puts "#{targets.count} total, #{target_list[:inventory].count} from inventory, "\
355
+ "#{target_list[:adhoc].count} adhoc"
331
356
  end
332
357
 
333
358
  def print_target_info(targets)
@@ -394,6 +419,41 @@ module Bolt
394
419
  @stream.puts(message)
395
420
  end
396
421
 
422
+ def print_error(message)
423
+ @stream.puts(colorize(:red, message))
424
+ end
425
+
426
+ def print_prompt(prompt)
427
+ @stream.print(colorize(:cyan, indent(4, prompt)))
428
+ end
429
+
430
+ def print_prompt_error(message)
431
+ @stream.puts(colorize(:red, indent(4, message)))
432
+ end
433
+
434
+ def print_action_step(step)
435
+ first, *remaining = wrap(step, 76).lines
436
+
437
+ first = indent(2, "→ #{first}")
438
+ remaining = remaining.map { |line| indent(4, line) }
439
+ step = [first, *remaining, "\n"].join
440
+
441
+ @stream.puts(step)
442
+ end
443
+
444
+ def print_action_error(error)
445
+ # Running everything through 'wrap' messes with newlines. Separating
446
+ # into lines and wrapping each individually ensures separate errors are
447
+ # distinguishable.
448
+ first, *remaining = error.lines
449
+ first = colorize(:red, indent(2, "→ #{wrap(first, 76)}"))
450
+ wrapped = remaining.map { |l| wrap(l) }
451
+ to_print = wrapped.map { |line| colorize(:red, indent(4, line)) }
452
+ step = [first, *to_print, "\n"].join
453
+
454
+ @stream.puts(step)
455
+ end
456
+
397
457
  def duration_to_string(duration)
398
458
  hrs = (duration / 3600).floor
399
459
  mins = ((duration % 3600) / 60).floor
@@ -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
 
@@ -48,7 +48,7 @@ module Bolt
48
48
 
49
49
  def print_task_info(task)
50
50
  path = task.files.first['path'].chomp("/tasks/#{task.files.first['name']}")
51
- module_dir = if path.start_with?(Bolt::PAL::MODULES_PATH)
51
+ module_dir = if path.start_with?(Bolt::Config::Modulepath::MODULES_PATH)
52
52
  "built-in module"
53
53
  else
54
54
  path
@@ -62,7 +62,7 @@ module Bolt
62
62
 
63
63
  def print_plan_info(plan)
64
64
  path = plan.delete('module')
65
- plan['module_dir'] = if path.start_with?(Bolt::PAL::MODULES_PATH)
65
+ plan['module_dir'] = if path.start_with?(Bolt::Config::Modulepath::MODULES_PATH)
66
66
  "built-in module"
67
67
  else
68
68
  path
@@ -97,13 +97,22 @@ module Bolt
97
97
  def print_puppetfile_result(success, puppetfile, moduledir)
98
98
  @stream.puts({ "success": success,
99
99
  "puppetfile": puppetfile,
100
- "moduledir": moduledir }.to_json)
100
+ "moduledir": moduledir.to_s }.to_json)
101
101
  end
102
102
 
103
- def print_targets(targets)
103
+ def print_targets(target_list, inventoryfile)
104
104
  @stream.puts ::JSON.pretty_generate(
105
- "targets": targets.map(&:name),
106
- "count": targets.count
105
+ "inventory": {
106
+ "targets": target_list[:inventory].map(&:name),
107
+ "count": target_list[:inventory].count,
108
+ "file": inventoryfile.to_s
109
+ },
110
+ "adhoc": {
111
+ "targets": target_list[:adhoc].map(&:name),
112
+ "count": target_list[:adhoc].count
113
+ },
114
+ "targets": target_list.values.flatten.map(&:name),
115
+ "count": target_list.values.flatten.count
107
116
  )
108
117
  end
109
118
 
@@ -135,6 +144,12 @@ module Bolt
135
144
  def print_message(message)
136
145
  $stderr.puts(message)
137
146
  end
147
+ alias print_error print_message
148
+
149
+ def print_action_step(step)
150
+ $stderr.puts(step)
151
+ end
152
+ alias print_action_error print_action_step
138
153
  end
139
154
  end
140
155
  end