bolt 2.23.0 → 2.27.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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +1 -1
  3. data/bolt-modules/boltlib/lib/puppet/datatypes/result.rb +2 -1
  4. data/bolt-modules/boltlib/lib/puppet/functions/download_file.rb +1 -1
  5. data/bolt-modules/dir/lib/puppet/functions/dir/children.rb +1 -1
  6. data/exe/bolt +1 -0
  7. data/guides/inventory.txt +19 -0
  8. data/guides/project.txt +22 -0
  9. data/lib/bolt/analytics.rb +11 -7
  10. data/lib/bolt/applicator.rb +11 -10
  11. data/lib/bolt/bolt_option_parser.rb +75 -13
  12. data/lib/bolt/catalog.rb +4 -2
  13. data/lib/bolt/cli.rb +156 -176
  14. data/lib/bolt/config.rb +55 -25
  15. data/lib/bolt/config/options.rb +28 -6
  16. data/lib/bolt/executor.rb +5 -3
  17. data/lib/bolt/inventory.rb +8 -1
  18. data/lib/bolt/inventory/group.rb +4 -4
  19. data/lib/bolt/inventory/inventory.rb +1 -1
  20. data/lib/bolt/inventory/target.rb +1 -1
  21. data/lib/bolt/logger.rb +12 -6
  22. data/lib/bolt/outputter/human.rb +10 -0
  23. data/lib/bolt/outputter/json.rb +11 -0
  24. data/lib/bolt/outputter/logger.rb +3 -3
  25. data/lib/bolt/outputter/rainbow.rb +15 -0
  26. data/lib/bolt/pal.rb +23 -12
  27. data/lib/bolt/pal/yaml_plan/evaluator.rb +1 -1
  28. data/lib/bolt/pal/yaml_plan/transpiler.rb +11 -3
  29. data/lib/bolt/plugin/puppetdb.rb +1 -1
  30. data/lib/bolt/project.rb +63 -17
  31. data/lib/bolt/project_migrate.rb +138 -0
  32. data/lib/bolt/puppetdb/client.rb +1 -1
  33. data/lib/bolt/puppetdb/config.rb +1 -1
  34. data/lib/bolt/puppetfile.rb +160 -0
  35. data/lib/bolt/puppetfile/installer.rb +43 -0
  36. data/lib/bolt/puppetfile/module.rb +66 -0
  37. data/lib/bolt/r10k_log_proxy.rb +1 -1
  38. data/lib/bolt/rerun.rb +2 -2
  39. data/lib/bolt/result.rb +23 -0
  40. data/lib/bolt/shell.rb +1 -1
  41. data/lib/bolt/shell/bash.rb +7 -7
  42. data/lib/bolt/task.rb +1 -1
  43. data/lib/bolt/transport/base.rb +1 -1
  44. data/lib/bolt/transport/docker/connection.rb +10 -10
  45. data/lib/bolt/transport/local/connection.rb +3 -3
  46. data/lib/bolt/transport/orch.rb +3 -3
  47. data/lib/bolt/transport/ssh.rb +1 -1
  48. data/lib/bolt/transport/ssh/connection.rb +6 -6
  49. data/lib/bolt/transport/ssh/exec_connection.rb +5 -5
  50. data/lib/bolt/transport/winrm.rb +1 -1
  51. data/lib/bolt/transport/winrm/connection.rb +9 -9
  52. data/lib/bolt/util.rb +2 -2
  53. data/lib/bolt/util/puppet_log_level.rb +4 -3
  54. data/lib/bolt/version.rb +1 -1
  55. data/lib/bolt_server/base_config.rb +2 -2
  56. data/lib/bolt_server/config.rb +1 -1
  57. data/lib/bolt_server/file_cache.rb +1 -1
  58. data/lib/bolt_server/transport_app.rb +189 -14
  59. data/lib/bolt_spec/plans.rb +1 -1
  60. data/lib/bolt_spec/run.rb +3 -0
  61. metadata +12 -12
@@ -19,7 +19,7 @@ module Bolt
19
19
  class Config
20
20
  include Bolt::Config::Options
21
21
 
22
- attr_reader :config_files, :warnings, :data, :transports, :project, :modified_concurrency, :deprecations
22
+ attr_reader :config_files, :logs, :data, :transports, :project, :modified_concurrency, :deprecations
23
23
 
24
24
  BOLT_CONFIG_NAME = 'bolt.yaml'
25
25
  BOLT_DEFAULTS_NAME = 'bolt-defaults.yaml'
@@ -32,16 +32,19 @@ module Bolt
32
32
  end
33
33
 
34
34
  def self.from_project(project, overrides = {})
35
+ logs = []
35
36
  conf = if project.project_file == project.config_file
36
37
  project.data
37
38
  else
38
- Bolt::Util.read_optional_yaml_hash(project.config_file, 'config')
39
+ c = Bolt::Util.read_optional_yaml_hash(project.config_file, 'config')
40
+ logs << { debug: "Loaded configuration from #{project.config_file}" } if File.exist?(project.config_file)
41
+ c
39
42
  end
40
43
 
41
44
  data = load_defaults(project).push(
42
45
  filepath: project.config_file,
43
46
  data: conf,
44
- warnings: [],
47
+ logs: logs,
45
48
  deprecations: []
46
49
  )
47
50
 
@@ -50,17 +53,20 @@ module Bolt
50
53
 
51
54
  def self.from_file(configfile, overrides = {})
52
55
  project = Bolt::Project.create_project(Pathname.new(configfile).expand_path.dirname)
56
+ logs = []
53
57
 
54
58
  conf = if project.project_file == project.config_file
55
59
  project.data
56
60
  else
57
- Bolt::Util.read_yaml_hash(configfile, 'config')
61
+ c = Bolt::Util.read_yaml_hash(configfile, 'config')
62
+ logs << { debug: "Loaded configuration from #{configfile}" }
63
+ c
58
64
  end
59
65
 
60
66
  data = load_defaults(project).push(
61
- filepath: project.config_file,
67
+ filepath: configfile,
62
68
  data: conf,
63
- warnings: [],
69
+ logs: logs,
64
70
  deprecations: []
65
71
  )
66
72
 
@@ -90,13 +96,13 @@ module Bolt
90
96
  def self.load_bolt_defaults_yaml(dir)
91
97
  filepath = dir + BOLT_DEFAULTS_NAME
92
98
  data = Bolt::Util.read_yaml_hash(filepath, 'config')
93
- warnings = []
99
+ logs = [{ debug: "Loaded configuration from #{filepath}" }]
94
100
 
95
101
  # Warn if 'bolt.yaml' detected in same directory.
96
102
  if File.exist?(bolt_yaml = dir + BOLT_CONFIG_NAME)
97
- warnings.push(
98
- msg: "Detected multiple configuration files: ['#{bolt_yaml}', '#{filepath}']. '#{bolt_yaml}' "\
99
- "will be ignored."
103
+ logs.push(
104
+ warn: "Detected multiple configuration files: ['#{bolt_yaml}', '#{filepath}']. '#{bolt_yaml}' "\
105
+ "will be ignored."
100
106
  )
101
107
  end
102
108
 
@@ -105,9 +111,9 @@ module Bolt
105
111
 
106
112
  if project_config.any?
107
113
  data.reject! { |key, _| project_config.include?(key) }
108
- warnings.push(
109
- msg: "Unsupported project configuration detected in '#{filepath}': #{project_config.keys}. "\
110
- "Project configuration should be set in 'bolt-project.yaml'."
114
+ logs.push(
115
+ warn: "Unsupported project configuration detected in '#{filepath}': #{project_config.keys}. "\
116
+ "Project configuration should be set in 'bolt-project.yaml'."
111
117
  )
112
118
  end
113
119
 
@@ -116,10 +122,10 @@ module Bolt
116
122
 
117
123
  if transport_config.any?
118
124
  data.reject! { |key, _| transport_config.include?(key) }
119
- warnings.push(
120
- msg: "Unsupported inventory configuration detected in '#{filepath}': #{transport_config.keys}. "\
121
- "Transport configuration should be set under the 'inventory-config' option or "\
122
- "in 'inventory.yaml'."
125
+ logs.push(
126
+ warn: "Unsupported inventory configuration detected in '#{filepath}': #{transport_config.keys}. "\
127
+ "Transport configuration should be set under the 'inventory-config' option or "\
128
+ "in 'inventory.yaml'."
123
129
  )
124
130
  end
125
131
 
@@ -142,7 +148,7 @@ module Bolt
142
148
  data = data.merge(data.delete('inventory-config'))
143
149
  end
144
150
 
145
- { filepath: filepath, data: data, warnings: warnings, deprecations: [] }
151
+ { filepath: filepath, data: data, logs: logs, deprecations: [] }
146
152
  end
147
153
 
148
154
  # Loads a 'bolt.yaml' file, the legacy configuration file. There's no special munging needed
@@ -150,11 +156,12 @@ module Bolt
150
156
  def self.load_bolt_yaml(dir)
151
157
  filepath = dir + BOLT_CONFIG_NAME
152
158
  data = Bolt::Util.read_yaml_hash(filepath, 'config')
159
+ logs = [{ debug: "Loaded configuration from #{filepath}" }]
153
160
  deprecations = [{ type: 'Using bolt.yaml for system configuration',
154
161
  msg: "Configuration file #{filepath} is deprecated and will be removed in a future version "\
155
162
  "of Bolt. Use '#{dir + BOLT_DEFAULTS_NAME}' instead." }]
156
163
 
157
- { filepath: filepath, data: data, warnings: [], deprecations: deprecations }
164
+ { filepath: filepath, data: data, logs: logs, deprecations: deprecations }
158
165
  end
159
166
 
160
167
  def self.load_defaults(project)
@@ -187,13 +194,13 @@ module Bolt
187
194
  unless config_data.is_a?(Array)
188
195
  config_data = [{ filepath: project.config_file,
189
196
  data: config_data,
190
- warnings: [],
197
+ logs: [],
191
198
  deprecations: [] }]
192
199
  end
193
200
 
194
- @logger = Logging.logger[self]
201
+ @logger = Bolt::Logger.logger(self)
195
202
  @project = project
196
- @warnings = @project.warnings.dup
203
+ @logs = @project.logs.dup
197
204
  @deprecations = @project.deprecations.dup
198
205
  @transports = {}
199
206
  @config_files = []
@@ -213,8 +220,15 @@ module Bolt
213
220
  'transport' => 'ssh'
214
221
  }
215
222
 
223
+ if project.path.directory?
224
+ default_data['log']['bolt-debug.log'] = {
225
+ 'level' => 'debug',
226
+ 'append' => false
227
+ }
228
+ end
229
+
216
230
  loaded_data = config_data.each_with_object([]) do |data, acc|
217
- @warnings.concat(data[:warnings]) if data[:warnings].any?
231
+ @logs.concat(data[:logs]) if data[:logs].any?
218
232
  @deprecations.concat(data[:deprecations]) if data[:deprecations].any?
219
233
 
220
234
  if data[:data].any?
@@ -330,10 +344,26 @@ module Bolt
330
344
  end
331
345
 
332
346
  private def update_logs(logs)
347
+ begin
348
+ if logs['bolt-debug.log'] && logs['bolt-debug.log'] != 'disable'
349
+ FileUtils.touch(File.expand_path('bolt-debug.log', @project.path))
350
+ end
351
+ rescue StandardError
352
+ logs.delete('bolt-debug.log')
353
+ end
354
+
333
355
  logs.each_with_object({}) do |(key, val), acc|
334
- next unless val.is_a?(Hash)
356
+ # Remove any disabled logs
357
+ next if val == 'disable'
335
358
 
336
359
  name = normalize_log(key)
360
+
361
+ # But otherwise it has to be a Hash
362
+ unless val.is_a?(Hash)
363
+ raise Bolt::ValidationError,
364
+ "config of log #{name} must be a Hash, received #{val.class} #{val.inspect}"
365
+ end
366
+
337
367
  acc[name] = val.slice('append', 'level')
338
368
  .transform_keys(&:to_sym)
339
369
 
@@ -358,7 +388,7 @@ module Bolt
358
388
  def validate
359
389
  if @data['future']
360
390
  msg = "Configuration option 'future' no longer exposes future behavior."
361
- @warnings << { option: 'future', msg: msg }
391
+ @logs << { warn: msg }
362
392
  end
363
393
 
364
394
  keys = OPTIONS.keys - %w[plugins plugin_hooks puppetdb]
@@ -176,7 +176,9 @@ module Bolt
176
176
  description: "A map of configuration for the logfile output. Under `log`, you can configure log options "\
177
177
  "for `console` and add configuration for individual log files, such as "\
178
178
  "`~/.puppetlabs/bolt/debug.log`. Individual log files must be valid filepaths. If the log "\
179
- "file does not exist, then Bolt will create it before logging information.",
179
+ "file does not exist, then Bolt will create it before logging information. Set the value to "\
180
+ "`disable` to remove a log file defined at an earlier level of the config hierarchy. By "\
181
+ "default, Bolt logs to a bolt-debug.log file in the Bolt project directory.",
180
182
  type: Hash,
181
183
  properties: {
182
184
  "console" => {
@@ -186,15 +188,16 @@ module Bolt
186
188
  "level" => {
187
189
  description: "The type of information to log.",
188
190
  type: String,
189
- enum: %w[debug error info notice warn fatal any],
190
- _default: "warn for console, notice for file"
191
+ enum: %w[trace debug error info warn fatal any],
192
+ _default: "warn"
191
193
  }
192
194
  }
193
195
  }
194
196
  },
195
197
  additionalProperties: {
196
198
  description: "Configuration for the logfile output.",
197
- type: Hash,
199
+ type: [String, Hash],
200
+ enum: ['disable'],
198
201
  properties: {
199
202
  "append" => {
200
203
  description: "Whether to append output to an existing log file.",
@@ -204,8 +207,8 @@ module Bolt
204
207
  "level" => {
205
208
  description: "The type of information to log.",
206
209
  type: String,
207
- enum: %w[debug error info notice warn fatal any],
208
- _default: "warn for console, notice for file"
210
+ enum: %w[trace debug error info warn fatal any],
211
+ _default: "warn"
209
212
  }
210
213
  }
211
214
  },
@@ -224,6 +227,24 @@ module Bolt
224
227
  _example: ["~/.puppetlabs/bolt/modules", "~/.puppetlabs/bolt/site-modules"],
225
228
  _default: ["project/modules", "project/site-modules", "project/site"]
226
229
  },
230
+ "modules" => {
231
+ description: "A list of module dependencies for the project. Each dependency is a map of data specifying "\
232
+ "the module to install. To install the project's module dependencies, run the `bolt module "\
233
+ "install` command.",
234
+ type: Array,
235
+ items: {
236
+ type: Hash,
237
+ required: ["name"],
238
+ properties: {
239
+ "name" => {
240
+ description: "The name of the module.",
241
+ type: String
242
+ }
243
+ }
244
+ },
245
+ _plugin: false,
246
+ _example: [{ "name" => "puppetlabs-mysql" }, { "name" => "puppetlabs-apache" }]
247
+ },
227
248
  "name" => {
228
249
  description: "The name of the Bolt project. When this option is configured, the project is considered a "\
229
250
  "[Bolt project](experimental_features.md#bolt-projects), allowing Bolt to load content from "\
@@ -473,6 +494,7 @@ module Bolt
473
494
  inventoryfile
474
495
  log
475
496
  modulepath
497
+ modules
476
498
  name
477
499
  plans
478
500
  plugin_hooks
@@ -40,7 +40,7 @@ module Bolt
40
40
  require 'concurrent'
41
41
 
42
42
  @analytics = analytics
43
- @logger = Logging.logger[self]
43
+ @logger = Bolt::Logger.logger(self)
44
44
 
45
45
  @transports = Bolt::TRANSPORTS.each_with_object({}) do |(key, val), coll|
46
46
  coll[key.to_s] = if key == :remote
@@ -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)
@@ -46,10 +46,13 @@ module Bolt
46
46
  end
47
47
 
48
48
  def self.from_config(config, plugins)
49
+ logger = Logging.logger[self]
50
+
49
51
  if ENV.include?(ENVIRONMENT_VAR)
50
52
  begin
51
53
  data = YAML.safe_load(ENV[ENVIRONMENT_VAR])
52
54
  raise Bolt::ParseError, "Could not parse inventory from $#{ENVIRONMENT_VAR}" unless data.is_a?(Hash)
55
+ logger.debug("Loaded inventory from environment variable #{ENVIRONMENT_VAR}")
53
56
  rescue Psych::Exception
54
57
  raise Bolt::ParseError, "Could not parse inventory from $#{ENVIRONMENT_VAR}"
55
58
  end
@@ -57,8 +60,12 @@ module Bolt
57
60
  data = if config.inventoryfile
58
61
  Bolt::Util.read_yaml_hash(config.inventoryfile, 'inventory')
59
62
  else
60
- Bolt::Util.read_optional_yaml_hash(config.default_inventoryfile, 'inventory')
63
+ i = Bolt::Util.read_optional_yaml_hash(config.default_inventoryfile, 'inventory')
64
+ logger.debug("Loaded inventory from #{config.default_inventoryfile}") if i
65
+ i
61
66
  end
67
+ # This avoids rubocop complaining about identical conditionals
68
+ logger.debug("Loaded inventory from #{config.inventoryfile}") if config.inventoryfile
62
69
  end
63
70
 
64
71
  # Resolve plugin references from transport config
@@ -19,7 +19,7 @@ module Bolt
19
19
  CONFIG_KEYS = Bolt::Config::INVENTORY_OPTIONS.keys
20
20
 
21
21
  def initialize(input, plugins)
22
- @logger = Logging.logger[self]
22
+ @logger = Bolt::Logger.logger(self)
23
23
  @plugins = plugins
24
24
 
25
25
  input = @plugins.resolve_top_level_references(input) if @plugins.reference?(input)
@@ -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
@@ -16,7 +16,7 @@ module Bolt
16
16
 
17
17
  # TODO: Pass transport config instead of config object
18
18
  def initialize(data, transport, transports, plugins)
19
- @logger = Logging.logger[self]
19
+ @logger = Bolt::Logger.logger(self)
20
20
  @data = data || {}
21
21
  @transport = transport
22
22
  @config = transports
@@ -13,7 +13,7 @@ module Bolt
13
13
  raise Bolt::Inventory::ValidationError.new("Target must have either a name or uri", nil)
14
14
  end
15
15
 
16
- @logger = Logging.logger[inventory]
16
+ @logger = Bolt::Logger.logger(inventory)
17
17
 
18
18
  # If the target isn't mentioned by any groups, it won't have a uri or
19
19
  # name and we will use the target_name as both
@@ -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]
@@ -29,7 +28,7 @@ module Bolt
29
28
  end
30
29
 
31
30
  def self.configure(destinations, color)
32
- root_logger = Logging.logger[:root]
31
+ root_logger = Bolt::Logger.logger(:root)
33
32
 
34
33
  root_logger.add_appenders Logging.appenders.stderr(
35
34
  'console',
@@ -67,6 +66,13 @@ module Bolt
67
66
  end
68
67
  end
69
68
 
69
+ # A helper to ensure the Logging library is always initialized with our
70
+ # custom log levels before retrieving a Logger instance.
71
+ def self.logger(name)
72
+ initialize_logging
73
+ Logging.logger[name]
74
+ end
75
+
70
76
  def self.analytics=(analytics)
71
77
  @analytics = analytics
72
78
  end
@@ -81,7 +87,7 @@ module Bolt
81
87
 
82
88
  def self.default_layout
83
89
  Logging.layouts.pattern(
84
- pattern: '%d %-6l %c: %m\n',
90
+ pattern: '%d %-6l [%T] [%c] %m\n',
85
91
  date_pattern: '%Y-%m-%dT%H:%M:%S.%6N'
86
92
  )
87
93
  end
@@ -91,7 +97,7 @@ module Bolt
91
97
  end
92
98
 
93
99
  def self.default_file_level
94
- :notice
100
+ :warn
95
101
  end
96
102
 
97
103
  # Explicitly check the log level names instead of the log level number, as levels
@@ -111,7 +117,7 @@ module Bolt
111
117
  def self.warn_once(type, msg)
112
118
  @mutex.synchronize {
113
119
  @warnings ||= []
114
- @logger ||= Logging.logger[self]
120
+ @logger ||= Bolt::Logger.logger(self)
115
121
  unless @warnings.include?(type)
116
122
  @logger.warn(msg)
117
123
  @warnings << type
@@ -286,6 +286,16 @@ module Bolt
286
286
  "details and parameters for a specific plan.")
287
287
  end
288
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
+
289
299
  def print_module_list(module_list)
290
300
  module_list.each do |path, modules|
291
301
  if (mod = modules.find { |m| m[:internal_module_group] })