bolt 2.27.0 → 2.32.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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +13 -12
  3. data/bolt-modules/boltlib/lib/puppet/functions/write_file.rb +2 -2
  4. data/bolt-modules/out/lib/puppet/functions/out/message.rb +44 -1
  5. data/bolt-modules/prompt/lib/puppet/functions/prompt.rb +3 -0
  6. data/guides/module.txt +19 -0
  7. data/guides/modulepath.txt +25 -0
  8. data/lib/bolt/applicator.rb +14 -14
  9. data/lib/bolt/bolt_option_parser.rb +74 -22
  10. data/lib/bolt/catalog.rb +1 -1
  11. data/lib/bolt/cli.rb +178 -127
  12. data/lib/bolt/config.rb +13 -1
  13. data/lib/bolt/config/modulepath.rb +30 -0
  14. data/lib/bolt/config/options.rb +38 -9
  15. data/lib/bolt/config/transport/options.rb +1 -1
  16. data/lib/bolt/executor.rb +1 -1
  17. data/lib/bolt/inventory.rb +11 -10
  18. data/lib/bolt/logger.rb +26 -19
  19. data/lib/bolt/module_installer.rb +197 -0
  20. data/lib/bolt/{puppetfile → module_installer}/installer.rb +3 -2
  21. data/lib/bolt/module_installer/puppetfile.rb +117 -0
  22. data/lib/bolt/module_installer/puppetfile/forge_module.rb +54 -0
  23. data/lib/bolt/module_installer/puppetfile/git_module.rb +37 -0
  24. data/lib/bolt/module_installer/puppetfile/module.rb +26 -0
  25. data/lib/bolt/module_installer/resolver.rb +76 -0
  26. data/lib/bolt/module_installer/specs.rb +93 -0
  27. data/lib/bolt/module_installer/specs/forge_spec.rb +84 -0
  28. data/lib/bolt/module_installer/specs/git_spec.rb +178 -0
  29. data/lib/bolt/outputter.rb +2 -45
  30. data/lib/bolt/outputter/human.rb +78 -18
  31. data/lib/bolt/outputter/json.rb +22 -7
  32. data/lib/bolt/outputter/logger.rb +2 -2
  33. data/lib/bolt/pal.rb +29 -25
  34. data/lib/bolt/plugin.rb +1 -1
  35. data/lib/bolt/plugin/module.rb +1 -1
  36. data/lib/bolt/project.rb +32 -22
  37. data/lib/bolt/project_migrator.rb +80 -0
  38. data/lib/bolt/project_migrator/base.rb +39 -0
  39. data/lib/bolt/project_migrator/config.rb +67 -0
  40. data/lib/bolt/project_migrator/inventory.rb +67 -0
  41. data/lib/bolt/project_migrator/modules.rb +200 -0
  42. data/lib/bolt/shell/bash.rb +4 -3
  43. data/lib/bolt/transport/base.rb +4 -4
  44. data/lib/bolt/transport/ssh/connection.rb +1 -1
  45. data/lib/bolt/util.rb +51 -10
  46. data/lib/bolt/version.rb +1 -1
  47. data/lib/bolt_server/acl.rb +2 -2
  48. data/lib/bolt_server/base_config.rb +3 -3
  49. data/lib/bolt_server/file_cache.rb +11 -11
  50. data/lib/bolt_server/schemas/partials/task.json +17 -2
  51. data/lib/bolt_server/transport_app.rb +93 -13
  52. data/lib/bolt_spec/bolt_context.rb +8 -6
  53. data/lib/bolt_spec/plans.rb +1 -1
  54. data/lib/bolt_spec/plans/mock_executor.rb +1 -1
  55. data/lib/bolt_spec/run.rb +1 -1
  56. metadata +30 -11
  57. data/lib/bolt/project_migrate.rb +0 -138
  58. data/lib/bolt/puppetfile.rb +0 -160
  59. data/lib/bolt/puppetfile/module.rb +0 -66
  60. data/lib/bolt_server/pe/pal.rb +0 -67
@@ -0,0 +1,178 @@
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-z][a-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', must start with a lowercase letter, and may "\
40
+ "only include lowercase letters, digits, and underscores."
41
+ end
42
+
43
+ match[:name]
44
+ end
45
+
46
+ # Gets the repo from the git URL.
47
+ #
48
+ private def parse_git(git)
49
+ repo = if git.start_with?('git@github.com:')
50
+ git.split('git@github.com:').last.split('.git').first
51
+ elsif git.start_with?('https://github.com')
52
+ git.split('https://github.com/').last.split('.git').first
53
+ else
54
+ raise Bolt::ValidationError,
55
+ "Invalid git source: #{git}. Only GitHub modules are supported."
56
+ end
57
+
58
+ [git, repo]
59
+ end
60
+
61
+ # Returns true if the specification is satisfied by the module.
62
+ #
63
+ def satisfied_by?(mod)
64
+ @type == mod.type && @git == mod.git
65
+ end
66
+
67
+ # Returns a hash matching the module spec in bolt-project.yaml
68
+ #
69
+ def to_hash
70
+ {
71
+ 'git' => @git,
72
+ 'ref' => @ref
73
+ }
74
+ end
75
+
76
+ # Returns a PuppetfileResolver::Model::GitModule object for resolving.
77
+ #
78
+ def to_resolver_module
79
+ require 'puppetfile-resolver'
80
+
81
+ PuppetfileResolver::Puppetfile::GitModule.new(name).tap do |mod|
82
+ mod.remote = @git
83
+ mod.ref = sha
84
+ end
85
+ end
86
+
87
+ # Resolves the module's title from the module metadata. This is lazily
88
+ # resolved since Bolt does not always need to know a Git module's name.
89
+ #
90
+ def name
91
+ @name ||= begin
92
+ url = "https://raw.githubusercontent.com/#{@repo}/#{sha}/metadata.json"
93
+ response = make_request(:Get, url)
94
+
95
+ case response
96
+ when Net::HTTPOK
97
+ body = JSON.parse(response.body)
98
+
99
+ unless body.key?('name')
100
+ raise Bolt::Error.new(
101
+ "Missing name in metadata.json at #{git}. This is not a valid module.",
102
+ "bolt/missing-module-name-error"
103
+ )
104
+ end
105
+
106
+ parse_name(body['name'])
107
+ else
108
+ raise Bolt::Error.new(
109
+ "Missing metadata.json at #{git}. This is not a valid module.",
110
+ "bolt/missing-module-metadata-error"
111
+ )
112
+ end
113
+ end
114
+ end
115
+
116
+ # Resolves the SHA for the specified ref. This is lazily resolved since
117
+ # Bolt does not always need to know a Git module's SHA.
118
+ #
119
+ def sha
120
+ @sha ||= begin
121
+ url = "https://api.github.com/repos/#{@repo}/commits/#{ref}"
122
+ headers = ENV['GITHUB_TOKEN'] ? { "Authorization" => "token #{ENV['GITHUB_TOKEN']}" } : {}
123
+ response = make_request(:Get, url, headers)
124
+
125
+ case response
126
+ when Net::HTTPOK
127
+ body = JSON.parse(response.body)
128
+ body['sha']
129
+ when Net::HTTPUnauthorized
130
+ raise Bolt::Error.new(
131
+ "Invalid token at GITHUB_TOKEN, unable to resolve git modules.",
132
+ "bolt/invalid-git-token-error"
133
+ )
134
+ when Net::HTTPForbidden
135
+ message = "GitHub API rate limit exceeded, unable to resolve git modules. "
136
+
137
+ unless ENV['GITHUB_TOKEN']
138
+ message += "To increase your rate limit, set the GITHUB_TOKEN environment "\
139
+ "variable with a GitHub personal access token."
140
+ end
141
+
142
+ raise Bolt::Error.new(message, 'bolt/github-api-rate-limit-error')
143
+ when Net::HTTPNotFound
144
+ raise Bolt::Error.new(
145
+ "#{git} is not a git repository.",
146
+ "bolt/missing-git-repository-error"
147
+ )
148
+ else
149
+ raise Bolt::Error.new(
150
+ "Ref #{ref} at #{git} is not a commit, tag, or branch.",
151
+ "bolt/invalid-git-ref-error"
152
+ )
153
+ end
154
+ end
155
+ end
156
+
157
+ # Makes a generic HTTP request.
158
+ #
159
+ private def make_request(verb, url, headers = {})
160
+ require 'net/http'
161
+
162
+ uri = URI.parse(url)
163
+ opts = { use_ssl: uri.scheme == 'https' }
164
+
165
+ Net::HTTP.start(uri.host, uri.port, opts) do |client|
166
+ request = Net::HTTP.const_get(verb).new(uri, headers)
167
+ client.request(request)
168
+ end
169
+ rescue StandardError => e
170
+ raise Bolt::Error.new(
171
+ "Failed to connect to #{uri}: #{e.message}",
172
+ "bolt/http-connect-error"
173
+ )
174
+ end
175
+ end
176
+ end
177
+ end
178
+ 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