bolt 2.21.0 → 2.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.

Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +1 -1
  3. data/bolt-modules/ctrl/lib/puppet/functions/ctrl/do_until.rb +12 -6
  4. data/bolt-modules/out/lib/puppet/functions/out/message.rb +1 -1
  5. data/exe/bolt +1 -0
  6. data/guides/inventory.txt +19 -0
  7. data/guides/project.txt +22 -0
  8. data/lib/bolt/analytics.rb +5 -5
  9. data/lib/bolt/applicator.rb +4 -3
  10. data/lib/bolt/bolt_option_parser.rb +64 -23
  11. data/lib/bolt/catalog.rb +9 -1
  12. data/lib/bolt/cli.rb +218 -73
  13. data/lib/bolt/config.rb +7 -0
  14. data/lib/bolt/config/options.rb +4 -4
  15. data/lib/bolt/executor.rb +9 -7
  16. data/lib/bolt/inventory/group.rb +3 -3
  17. data/lib/bolt/logger.rb +3 -4
  18. data/lib/bolt/module.rb +2 -1
  19. data/lib/bolt/outputter.rb +56 -0
  20. data/lib/bolt/outputter/human.rb +10 -9
  21. data/lib/bolt/outputter/json.rb +11 -4
  22. data/lib/bolt/outputter/logger.rb +2 -2
  23. data/lib/bolt/outputter/rainbow.rb +15 -0
  24. data/lib/bolt/pal.rb +5 -9
  25. data/lib/bolt/pal/yaml_plan/evaluator.rb +1 -1
  26. data/lib/bolt/pal/yaml_plan/transpiler.rb +11 -3
  27. data/lib/bolt/plugin/prompt.rb +3 -3
  28. data/lib/bolt/project.rb +6 -4
  29. data/lib/bolt/project_migrate.rb +138 -0
  30. data/lib/bolt/shell/bash.rb +7 -7
  31. data/lib/bolt/transport/docker/connection.rb +9 -9
  32. data/lib/bolt/transport/local/connection.rb +2 -2
  33. data/lib/bolt/transport/orch.rb +3 -3
  34. data/lib/bolt/transport/ssh/connection.rb +5 -5
  35. data/lib/bolt/transport/ssh/exec_connection.rb +4 -4
  36. data/lib/bolt/transport/winrm/connection.rb +8 -8
  37. data/lib/bolt/util.rb +1 -1
  38. data/lib/bolt/util/puppet_log_level.rb +4 -3
  39. data/lib/bolt/version.rb +1 -1
  40. data/lib/bolt_server/base_config.rb +1 -1
  41. data/lib/bolt_server/pe/pal.rb +1 -1
  42. data/lib/bolt_server/transport_app.rb +76 -0
  43. data/lib/bolt_spec/plans.rb +1 -1
  44. data/lib/bolt_spec/plans/action_stubs.rb +1 -1
  45. data/lib/bolt_spec/run.rb +3 -0
  46. data/libexec/apply_catalog.rb +2 -2
  47. data/libexec/bolt_catalog +1 -1
  48. data/libexec/custom_facts.rb +1 -1
  49. data/libexec/query_resources.rb +1 -1
  50. metadata +7 -4
@@ -213,6 +213,13 @@ module Bolt
213
213
  'transport' => 'ssh'
214
214
  }
215
215
 
216
+ if project.path.directory?
217
+ default_data['log']['bolt-debug.log'] = {
218
+ 'level' => 'debug',
219
+ 'append' => false
220
+ }
221
+ end
222
+
216
223
  loaded_data = config_data.each_with_object([]) do |data, acc|
217
224
  @warnings.concat(data[:warnings]) if data[:warnings].any?
218
225
  @deprecations.concat(data[:deprecations]) if data[:deprecations].any?
@@ -186,8 +186,8 @@ module Bolt
186
186
  "level" => {
187
187
  description: "The type of information to log.",
188
188
  type: String,
189
- enum: %w[debug error info notice warn fatal any],
190
- _default: "warn for console, notice for file"
189
+ enum: %w[trace debug error info warn fatal any],
190
+ _default: "warn"
191
191
  }
192
192
  }
193
193
  }
@@ -204,8 +204,8 @@ module Bolt
204
204
  "level" => {
205
205
  description: "The type of information to log.",
206
206
  type: String,
207
- enum: %w[debug error info notice warn fatal any],
208
- _default: "warn for console, notice for file"
207
+ enum: %w[trace debug error info warn fatal any],
208
+ _default: "warn"
209
209
  }
210
210
  }
211
211
  },
@@ -56,11 +56,12 @@ module Bolt
56
56
  @reported_transports = Set.new
57
57
  @subscribers = {}
58
58
  @publisher = Concurrent::SingleThreadExecutor.new
59
+ @publisher.post { Thread.current[:name] = 'event-publisher' }
59
60
 
60
61
  @noop = noop
61
62
  @run_as = nil
62
63
  @pool = if concurrency > 0
63
- Concurrent::ThreadPoolExecutor.new(max_threads: concurrency)
64
+ Concurrent::ThreadPoolExecutor.new(name: 'exec', max_threads: concurrency)
64
65
  else
65
66
  Concurrent.global_immediate_executor
66
67
  end
@@ -125,6 +126,7 @@ module Bolt
125
126
  # Pass this argument through to avoid retaining a reference to a
126
127
  # local variable that will change on the next iteration of the loop.
127
128
  @pool.post(batch_promises) do |result_promises|
129
+ Thread.current[:name] ||= Thread.current.name
128
130
  results = yield transport, batch
129
131
  Array(results).each do |result|
130
132
  result_promises[result.target].set(result)
@@ -241,7 +243,7 @@ module Bolt
241
243
 
242
244
  @analytics&.event('Plan', 'yaml', plan_steps: steps, return_type: return_type)
243
245
  rescue StandardError => e
244
- @logger.debug { "Failed to submit analytics event: #{e.message}" }
246
+ @logger.trace { "Failed to submit analytics event: #{e.message}" }
245
247
  end
246
248
 
247
249
  def with_node_logging(description, batch)
@@ -381,19 +383,19 @@ module Bolt
381
383
  end
382
384
 
383
385
  def prompt(prompt, options)
384
- unless STDIN.tty?
386
+ unless $stdin.tty?
385
387
  raise Bolt::Error.new('STDIN is not a tty, unable to prompt', 'bolt/no-tty-error')
386
388
  end
387
389
 
388
- STDERR.print("#{prompt}: ")
390
+ $stderr.print("#{prompt}: ")
389
391
 
390
392
  value = if options[:sensitive]
391
- STDIN.noecho(&:gets).to_s.chomp
393
+ $stdin.noecho(&:gets).to_s.chomp
392
394
  else
393
- STDIN.gets.to_s.chomp
395
+ $stdin.gets.to_s.chomp
394
396
  end
395
397
 
396
- STDERR.puts if options[:sensitive]
398
+ $stderr.puts if options[:sensitive]
397
399
 
398
400
  value
399
401
  end
@@ -119,7 +119,7 @@ module Bolt
119
119
  end
120
120
 
121
121
  if contains_target?(t_name)
122
- @logger.warn("Ignoring duplicate target in #{@name}: #{target}")
122
+ @logger.debug("Ignoring duplicate target in #{@name}: #{target}")
123
123
  return
124
124
  end
125
125
 
@@ -200,14 +200,14 @@ module Bolt
200
200
  # If this is an alias for an existing target, then add it to this group
201
201
  elsif (canonical_name = aliases[string_target])
202
202
  if contains_target?(canonical_name)
203
- @logger.warn("Ignoring duplicate target in #{@name}: #{canonical_name}")
203
+ @logger.debug("Ignoring duplicate target in #{@name}: #{canonical_name}")
204
204
  else
205
205
  @unresolved_targets[canonical_name] = { 'name' => canonical_name }
206
206
  end
207
207
  # If it's not the name or alias of an existing target, then make a
208
208
  # new target using the string as the URI
209
209
  elsif contains_target?(string_target)
210
- @logger.warn("Ignoring duplicate target in #{@name}: #{string_target}")
210
+ @logger.debug("Ignoring duplicate target in #{@name}: #{string_target}")
211
211
  else
212
212
  @unresolved_targets[string_target] = { 'uri' => string_target }
213
213
  end
@@ -14,13 +14,12 @@ module Bolt
14
14
  # redefs, so skip it if it's already been initialized
15
15
  return if Logging.initialized?
16
16
 
17
- Logging.init :debug, :info, :notice, :warn, :error, :fatal, :any
17
+ Logging.init :trace, :debug, :info, :notice, :warn, :error, :fatal, :any
18
18
  @mutex = Mutex.new
19
19
 
20
20
  Logging.color_scheme(
21
21
  'bolt',
22
22
  lines: {
23
- notice: :green,
24
23
  warn: :yellow,
25
24
  error: :red,
26
25
  fatal: %i[white on_red]
@@ -81,7 +80,7 @@ module Bolt
81
80
 
82
81
  def self.default_layout
83
82
  Logging.layouts.pattern(
84
- pattern: '%d %-6l %c: %m\n',
83
+ pattern: '%d %-6l [%T] [%c] %m\n',
85
84
  date_pattern: '%Y-%m-%dT%H:%M:%S.%6N'
86
85
  )
87
86
  end
@@ -91,7 +90,7 @@ module Bolt
91
90
  end
92
91
 
93
92
  def self.default_file_level
94
- :notice
93
+ :warn
95
94
  end
96
95
 
97
96
  # Explicitly check the log level names instead of the log level number, as levels
@@ -3,7 +3,8 @@
3
3
  # Is this Bolt::Pobs::Module?
4
4
  module Bolt
5
5
  class Module
6
- MODULE_NAME_REGEX = /\A[a-z][a-z0-9_]*\z/.freeze
6
+ CONTENT_NAME_REGEX = /\A[a-z][a-z0-9_]*(::[a-z][a-z0-9_]*)*\z/.freeze
7
+ MODULE_NAME_REGEX = /\A[a-z][a-z0-9_]*\z/.freeze
7
8
 
8
9
  def self.discover(modulepath)
9
10
  modulepath.each_with_object({}) do |path, mods|
@@ -21,6 +21,62 @@ module Bolt
21
21
  @trace = trace
22
22
  @stream = stream
23
23
  end
24
+
25
+ def indent(indent, string)
26
+ indent = ' ' * indent
27
+ string.gsub(/^/, indent.to_s)
28
+ end
29
+
30
+ def print_message_event(event)
31
+ print_message(stringify(event[:message]))
32
+ end
33
+
34
+ def print_message
35
+ raise NotImplementedError, "print_message() must be implemented by the outputter class"
36
+ end
37
+
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
79
+ end
24
80
  end
25
81
  end
26
82
 
@@ -27,11 +27,6 @@ module Bolt
27
27
  end
28
28
  end
29
29
 
30
- def indent(indent, string)
31
- indent = ' ' * indent
32
- string.gsub(/^/, indent.to_s)
33
- end
34
-
35
30
  def remove_trail(string)
36
31
  string.sub(/\s\z/, '')
37
32
  end
@@ -291,6 +286,16 @@ module Bolt
291
286
  "details and parameters for a specific plan.")
292
287
  end
293
288
 
289
+ def print_topics(topics)
290
+ print_message("Available topics are:")
291
+ print_message(topics.join("\n"))
292
+ print_message("\nUse `bolt guide <topic>` to view a specific guide.")
293
+ end
294
+
295
+ def print_guide(guide, _topic)
296
+ @stream.puts(guide)
297
+ end
298
+
294
299
  def print_module_list(module_list)
295
300
  module_list.each do |path, modules|
296
301
  if (mod = modules.find { |m| m[:internal_module_group] })
@@ -372,10 +377,6 @@ module Bolt
372
377
  end
373
378
  end
374
379
 
375
- def print_message_event(event)
376
- print_message(event[:message])
377
- end
378
-
379
380
  def fatal_error(err)
380
381
  @stream.puts(colorize(:red, err.message))
381
382
  if err.is_a? Bolt::RunFailure
@@ -83,6 +83,17 @@ module Bolt
83
83
  @stream.puts result.to_json
84
84
  end
85
85
 
86
+ def print_topics(topics)
87
+ print_table('topics' => topics)
88
+ end
89
+
90
+ def print_guide(guide, topic)
91
+ @stream.puts({
92
+ 'topic' => topic,
93
+ 'guide' => guide
94
+ }.to_json)
95
+ end
96
+
86
97
  def print_puppetfile_result(success, puppetfile, moduledir)
87
98
  @stream.puts({ "success": success,
88
99
  "puppetfile": puppetfile,
@@ -121,10 +132,6 @@ module Bolt
121
132
  @stream.puts '}' if @object_open
122
133
  end
123
134
 
124
- def print_message_event(event)
125
- print_message(event[:message])
126
- end
127
-
128
135
  def print_message(message)
129
136
  $stderr.puts(message)
130
137
  end
@@ -40,13 +40,13 @@ module Bolt
40
40
 
41
41
  def log_plan_start(event)
42
42
  plan = event[:plan]
43
- @logger.notice("Starting: plan #{plan}")
43
+ @logger.info("Starting: plan #{plan}")
44
44
  end
45
45
 
46
46
  def log_plan_finish(event)
47
47
  plan = event[:plan]
48
48
  duration = event[:duration]
49
- @logger.notice("Finished: plan #{plan} in #{duration.round(2)} sec")
49
+ @logger.info("Finished: plan #{plan} in #{duration.round(2)} sec")
50
50
  end
51
51
  end
52
52
  end
@@ -86,6 +86,21 @@ module Bolt
86
86
  total_msg << " in #{duration_to_string(elapsed_time)}" unless elapsed_time.nil?
87
87
  @stream.puts colorize(:rainbow, total_msg)
88
88
  end
89
+
90
+ def print_guide(guide, _topic)
91
+ @stream.puts colorize(:rainbow, guide)
92
+ end
93
+
94
+ def print_topics(topics)
95
+ content = String.new("Available topics are:\n")
96
+ content += topics.join("\n")
97
+ content += "\n\nUse `bolt guide <topic>` to view a specific guide."
98
+ @stream.puts colorize(:rainbow, content)
99
+ end
100
+
101
+ def print_message(message)
102
+ @stream.puts colorize(:rainbow, message)
103
+ end
89
104
  end
90
105
  end
91
106
  end
@@ -48,7 +48,7 @@ module Bolt
48
48
  end
49
49
  end
50
50
 
51
- attr_reader :modulepath
51
+ attr_reader :modulepath, :user_modulepath
52
52
 
53
53
  def initialize(modulepath, hiera_config, resource_types, max_compiles = Etc.nprocessors,
54
54
  trusted_external = nil, apply_settings = {}, project = nil)
@@ -56,7 +56,7 @@ module Bolt
56
56
  # is safe and in practice only happens in tests
57
57
  self.class.load_puppet
58
58
 
59
- @original_modulepath = modulepath
59
+ @user_modulepath = modulepath
60
60
  @modulepath = [BOLTLIB_PATH, *modulepath, MODULES_PATH]
61
61
  @hiera_config = hiera_config
62
62
  @trusted_external = trusted_external
@@ -67,7 +67,7 @@ module Bolt
67
67
 
68
68
  @logger = Logging.logger[self]
69
69
  if modulepath && !modulepath.empty?
70
- @logger.info("Loading modules from #{@modulepath.join(File::PATH_SEPARATOR)}")
70
+ @logger.debug("Loading modules from #{@modulepath.join(File::PATH_SEPARATOR)}")
71
71
  end
72
72
 
73
73
  @loaded = false
@@ -208,7 +208,7 @@ module Bolt
208
208
  # Skip syncing built-in plugins, since we vendor some Puppet 6
209
209
  # versions of "core" types, which are already present on the agent,
210
210
  # but may cause issues on Puppet 5 agents.
211
- @original_modulepath,
211
+ @user_modulepath,
212
212
  @project,
213
213
  pdb_client,
214
214
  @hiera_config,
@@ -278,10 +278,6 @@ module Bolt
278
278
  end
279
279
  end
280
280
 
281
- def list_modulepath
282
- @modulepath - [BOLTLIB_PATH, MODULES_PATH]
283
- end
284
-
285
281
  def parse_params(type, object_name, params)
286
282
  in_bolt_compiler do |compiler|
287
283
  case type
@@ -413,7 +409,7 @@ module Bolt
413
409
  end
414
410
  params[name] = { 'type' => type_str }
415
411
  params[name]['sensitive'] = param.type_expr.instance_of?(Puppet::Pops::Types::PSensitiveType)
416
- params[name]['default_value'] = param.value
412
+ params[name]['default_value'] = param.value unless param.value.nil?
417
413
  params[name]['description'] = param.description if param.description
418
414
  end
419
415
  {
@@ -109,7 +109,7 @@ module Bolt
109
109
  end
110
110
 
111
111
  def message_step(scope, step)
112
- scope.call_function('out::message', step['message'])
112
+ scope.call_function('out::message', [step['message']])
113
113
  end
114
114
 
115
115
  def generate_manifest(resources)
@@ -21,10 +21,18 @@ module Bolt
21
21
  validate_path
22
22
 
23
23
  plan_object = parse_plan
24
+ param_descriptions = plan_object.parameters.map do |param|
25
+ str = String.new("# @param #{param.name}")
26
+ str << " #{param.description}" if param.description
27
+ str
28
+ end.join("\n")
24
29
 
25
- plan_string = String.new("# WARNING: This is an autogenerated plan. " \
26
- "It may not behave as expected.\n" \
27
- "plan #{plan_object.name}(")
30
+ plan_string = String.new('')
31
+ plan_string << "# #{plan_object.description}\n" if plan_object.description
32
+ plan_string << "# WARNING: This is an autogenerated plan. It may not behave as expected.\n"
33
+ plan_string << "#{param_descriptions}\n" unless param_descriptions.empty?
34
+
35
+ plan_string << "plan #{plan_object.name}("
28
36
  # Parameters are Bolt::PAL::YamlPlan::Parameter
29
37
  plan_object.parameters&.each_with_index do |param, i|
30
38
  plan_string << param.transpile
@@ -18,9 +18,9 @@ module Bolt
18
18
  end
19
19
 
20
20
  def resolve_reference(opts)
21
- STDERR.print("#{opts['message']}: ")
22
- value = STDIN.noecho(&:gets).to_s.chomp
23
- STDERR.puts
21
+ $stderr.print("#{opts['message']}: ")
22
+ value = $stdin.noecho(&:gets).to_s.chomp
23
+ $stderr.puts
24
24
 
25
25
  value
26
26
  end
@@ -3,6 +3,7 @@
3
3
  require 'pathname'
4
4
  require 'bolt/config'
5
5
  require 'bolt/pal'
6
+ require 'bolt/module'
6
7
 
7
8
  module Bolt
8
9
  class Project
@@ -17,7 +18,7 @@ module Bolt
17
18
 
18
19
  attr_reader :path, :data, :config_file, :inventory_file, :modulepath, :hiera_config,
19
20
  :puppetfile, :rerunfile, :type, :resource_types, :warnings, :project_file,
20
- :deprecations, :downloads
21
+ :deprecations, :downloads, :plans_path
21
22
 
22
23
  def self.default_project
23
24
  create_project(File.expand_path(File.join('~', '.puppetlabs', 'bolt')), 'user')
@@ -82,6 +83,7 @@ module Bolt
82
83
  @resource_types = @path + '.resource_types'
83
84
  @type = type
84
85
  @downloads = @path + 'downloads'
86
+ @plans_path = @path + 'plans'
85
87
 
86
88
  tc = Bolt::Config::INVENTORY_OPTIONS.keys & raw_data.keys
87
89
  if tc.any?
@@ -138,10 +140,10 @@ module Bolt
138
140
 
139
141
  def validate
140
142
  if name
141
- name_regex = /^[a-z][a-z0-9_]*$/
142
- if name !~ name_regex
143
+ if name !~ Bolt::Module::MODULE_NAME_REGEX
143
144
  raise Bolt::ValidationError, <<~ERROR_STRING
144
- Invalid project name '#{name}' in bolt-project.yaml; project name must match #{name_regex.inspect}
145
+ Invalid project name '#{name}' in bolt-project.yaml; project name must begin with a lowercase letter
146
+ and can include lowercase letters, numbers, and underscores.
145
147
  ERROR_STRING
146
148
  elsif Dir.children(Bolt::PAL::BOLTLIB_PATH).include?(name)
147
149
  raise Bolt::ValidationError, "The project '#{name}' will not be loaded. The project name conflicts "\