bolt 2.18.0 → 2.23.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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +3 -1
  3. data/bolt-modules/boltlib/lib/puppet/functions/download_file.rb +123 -0
  4. data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +6 -0
  5. data/bolt-modules/ctrl/lib/puppet/functions/ctrl/do_until.rb +12 -6
  6. data/bolt-modules/dir/lib/puppet/functions/dir/children.rb +35 -0
  7. data/bolt-modules/out/lib/puppet/functions/out/message.rb +1 -1
  8. data/lib/bolt/analytics.rb +1 -1
  9. data/lib/bolt/bolt_option_parser.rb +74 -24
  10. data/lib/bolt/catalog.rb +12 -3
  11. data/lib/bolt/cli.rb +305 -108
  12. data/lib/bolt/config.rb +18 -10
  13. data/lib/bolt/config/options.rb +14 -0
  14. data/lib/bolt/executor.rb +26 -5
  15. data/lib/bolt/inventory/group.rb +3 -2
  16. data/lib/bolt/inventory/inventory.rb +4 -3
  17. data/lib/bolt/logger.rb +9 -0
  18. data/lib/bolt/module.rb +2 -1
  19. data/lib/bolt/outputter.rb +56 -0
  20. data/lib/bolt/outputter/human.rb +0 -9
  21. data/lib/bolt/outputter/json.rb +0 -4
  22. data/lib/bolt/outputter/rainbow.rb +9 -2
  23. data/lib/bolt/pal.rb +11 -9
  24. data/lib/bolt/pal/yaml_plan/evaluator.rb +23 -2
  25. data/lib/bolt/pal/yaml_plan/step.rb +24 -2
  26. data/lib/bolt/pal/yaml_plan/step/download.rb +38 -0
  27. data/lib/bolt/pal/yaml_plan/step/message.rb +30 -0
  28. data/lib/bolt/pal/yaml_plan/step/upload.rb +3 -3
  29. data/lib/bolt/plugin/module.rb +2 -4
  30. data/lib/bolt/plugin/prompt.rb +3 -3
  31. data/lib/bolt/plugin/puppetdb.rb +3 -2
  32. data/lib/bolt/project.rb +14 -9
  33. data/lib/bolt/puppetdb/client.rb +2 -0
  34. data/lib/bolt/puppetdb/config.rb +16 -0
  35. data/lib/bolt/result.rb +7 -0
  36. data/lib/bolt/shell/bash.rb +24 -4
  37. data/lib/bolt/shell/powershell.rb +10 -4
  38. data/lib/bolt/transport/base.rb +24 -0
  39. data/lib/bolt/transport/docker.rb +8 -0
  40. data/lib/bolt/transport/docker/connection.rb +20 -2
  41. data/lib/bolt/transport/local/connection.rb +14 -1
  42. data/lib/bolt/transport/orch.rb +12 -0
  43. data/lib/bolt/transport/simple.rb +6 -0
  44. data/lib/bolt/transport/ssh/connection.rb +9 -1
  45. data/lib/bolt/transport/ssh/exec_connection.rb +22 -1
  46. data/lib/bolt/transport/winrm/connection.rb +118 -8
  47. data/lib/bolt/util.rb +26 -11
  48. data/lib/bolt/version.rb +1 -1
  49. data/lib/bolt_server/pe/pal.rb +1 -1
  50. data/lib/bolt_server/transport_app.rb +3 -2
  51. data/lib/bolt_spec/bolt_context.rb +7 -2
  52. data/lib/bolt_spec/plans.rb +15 -2
  53. data/lib/bolt_spec/plans/action_stubs.rb +3 -2
  54. data/lib/bolt_spec/plans/action_stubs/download_stub.rb +66 -0
  55. data/lib/bolt_spec/plans/mock_executor.rb +14 -1
  56. data/lib/bolt_spec/run.rb +22 -0
  57. data/libexec/apply_catalog.rb +2 -2
  58. data/libexec/bolt_catalog +4 -3
  59. data/libexec/custom_facts.rb +1 -1
  60. data/libexec/query_resources.rb +1 -1
  61. data/modules/secure_env_vars/plans/init.pp +20 -0
  62. metadata +8 -2
@@ -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
22
+ attr_reader :config_files, :warnings, :data, :transports, :project, :modified_concurrency, :deprecations
23
23
 
24
24
  BOLT_CONFIG_NAME = 'bolt.yaml'
25
25
  BOLT_DEFAULTS_NAME = 'bolt-defaults.yaml'
@@ -41,7 +41,8 @@ module Bolt
41
41
  data = load_defaults(project).push(
42
42
  filepath: project.config_file,
43
43
  data: conf,
44
- warnings: []
44
+ warnings: [],
45
+ deprecations: []
45
46
  )
46
47
 
47
48
  new(project, data, overrides)
@@ -59,7 +60,8 @@ module Bolt
59
60
  data = load_defaults(project).push(
60
61
  filepath: project.config_file,
61
62
  data: conf,
62
- warnings: []
63
+ warnings: [],
64
+ deprecations: []
63
65
  )
64
66
 
65
67
  new(project, data, overrides)
@@ -140,7 +142,7 @@ module Bolt
140
142
  data = data.merge(data.delete('inventory-config'))
141
143
  end
142
144
 
143
- { filepath: filepath, data: data, warnings: warnings }
145
+ { filepath: filepath, data: data, warnings: warnings, deprecations: [] }
144
146
  end
145
147
 
146
148
  # Loads a 'bolt.yaml' file, the legacy configuration file. There's no special munging needed
@@ -148,10 +150,11 @@ module Bolt
148
150
  def self.load_bolt_yaml(dir)
149
151
  filepath = dir + BOLT_CONFIG_NAME
150
152
  data = Bolt::Util.read_yaml_hash(filepath, 'config')
151
- warnings = [msg: "Configuration file #{filepath} is deprecated and will be removed in a future version "\
152
- "of Bolt. Use '#{dir + BOLT_DEFAULTS_NAME}' instead."]
153
+ deprecations = [{ type: 'Using bolt.yaml for system configuration',
154
+ msg: "Configuration file #{filepath} is deprecated and will be removed in a future version "\
155
+ "of Bolt. Use '#{dir + BOLT_DEFAULTS_NAME}' instead." }]
153
156
 
154
- { filepath: filepath, data: data, warnings: warnings }
157
+ { filepath: filepath, data: data, warnings: [], deprecations: deprecations }
155
158
  end
156
159
 
157
160
  def self.load_defaults(project)
@@ -182,12 +185,16 @@ module Bolt
182
185
 
183
186
  def initialize(project, config_data, overrides = {})
184
187
  unless config_data.is_a?(Array)
185
- config_data = [{ filepath: project.config_file, data: config_data, warnings: [] }]
188
+ config_data = [{ filepath: project.config_file,
189
+ data: config_data,
190
+ warnings: [],
191
+ deprecations: [] }]
186
192
  end
187
193
 
188
194
  @logger = Logging.logger[self]
189
195
  @project = project
190
196
  @warnings = @project.warnings.dup
197
+ @deprecations = @project.deprecations.dup
191
198
  @transports = {}
192
199
  @config_files = []
193
200
 
@@ -208,6 +215,7 @@ module Bolt
208
215
 
209
216
  loaded_data = config_data.each_with_object([]) do |data, acc|
210
217
  @warnings.concat(data[:warnings]) if data[:warnings].any?
218
+ @deprecations.concat(data[:deprecations]) if data[:deprecations].any?
211
219
 
212
220
  if data[:data].any?
213
221
  @config_files.push(data[:filepath])
@@ -378,7 +386,7 @@ module Bolt
378
386
  raise Bolt::ValidationError, "Compilation is CPU-intensive, set concurrency less than #{compile_limit}"
379
387
  end
380
388
 
381
- if (format == 'rainbow' && Bolt::Util.windows?) || !(%w[human json rainbow].include? format)
389
+ unless %w[human json rainbow].include? format
382
390
  raise Bolt::ValidationError, "Unsupported format: '#{format}'"
383
391
  end
384
392
 
@@ -491,7 +499,7 @@ module Bolt
491
499
  end
492
500
 
493
501
  def matching_paths(paths)
494
- [*paths].map { |p| Dir.glob([p, casefold(p)]) }.flatten.uniq.reject { |p| [*paths].include?(p) }
502
+ Array(paths).map { |p| Dir.glob([p, casefold(p)]) }.flatten.uniq.reject { |p| Array(paths).include?(p) }
495
503
  end
496
504
 
497
505
  private def casefold(path)
@@ -275,11 +275,25 @@ module Bolt
275
275
  type: String,
276
276
  _example: "/etc/puppetlabs/puppet/ssl/certs/my-host.example.com.pem"
277
277
  },
278
+ "connect_timeout" => {
279
+ description: "How long to wait in seconds when establishing connections with PuppetDB.",
280
+ type: Integer,
281
+ minimum: 1,
282
+ _default: 60,
283
+ _example: 120
284
+ },
278
285
  "key" => {
279
286
  description: "The private key for the certificate.",
280
287
  type: String,
281
288
  _example: "/etc/puppetlabs/puppet/ssl/private_keys/my-host.example.com.pem"
282
289
  },
290
+ "read_timeout" => {
291
+ description: "How long to wait in seconds for a response from PuppetDB.",
292
+ type: Integer,
293
+ minimum: 1,
294
+ _default: 60,
295
+ _example: 120
296
+ },
283
297
  "server_urls" => {
284
298
  description: "An array containing the PuppetDB host to connect to. Include the protocol `https` "\
285
299
  "and the port, which is usually `8081`. For example, "\
@@ -320,6 +320,27 @@ module Bolt
320
320
  end
321
321
  end
322
322
 
323
+ def download_file(targets, source, destination, options = {})
324
+ description = options.fetch(:description, "file download from #{source} to #{destination}")
325
+
326
+ begin
327
+ FileUtils.mkdir_p(destination)
328
+ rescue Errno::EEXIST => e
329
+ message = "#{e.message}; unable to create destination directory #{destination}"
330
+ raise Bolt::Error.new(message, 'bolt/file-exist-error')
331
+ end
332
+
333
+ log_action(description, targets) do
334
+ options[:run_as] = run_as if run_as && !options.key?(:run_as)
335
+
336
+ batch_execute(targets) do |transport, batch|
337
+ with_node_logging("Downloading file #{source} to #{destination}", batch) do
338
+ transport.batch_download(batch, source, destination, options, &method(:publish_event))
339
+ end
340
+ end
341
+ end
342
+ end
343
+
323
344
  def run_plan(scope, plan, params)
324
345
  plan.call_by_name_with_scope(scope, params, true)
325
346
  end
@@ -360,19 +381,19 @@ module Bolt
360
381
  end
361
382
 
362
383
  def prompt(prompt, options)
363
- unless STDIN.tty?
384
+ unless $stdin.tty?
364
385
  raise Bolt::Error.new('STDIN is not a tty, unable to prompt', 'bolt/no-tty-error')
365
386
  end
366
387
 
367
- STDERR.print("#{prompt}: ")
388
+ $stderr.print("#{prompt}: ")
368
389
 
369
390
  value = if options[:sensitive]
370
- STDIN.noecho(&:gets).to_s.chomp
391
+ $stdin.noecho(&:gets).to_s.chomp
371
392
  else
372
- STDIN.gets.to_s.chomp
393
+ $stdin.gets.to_s.chomp
373
394
  end
374
395
 
375
- STDERR.puts if options[:sensitive]
396
+ $stderr.puts if options[:sensitive]
376
397
 
377
398
  value
378
399
  end
@@ -50,10 +50,11 @@ module Bolt
50
50
  # or it could be a name/alias of a target defined in another group.
51
51
  # We can't tell the difference until all groups have been resolved,
52
52
  # so we store the string on its own here and process it later.
53
- if target.is_a?(String)
53
+ case target
54
+ when String
54
55
  @string_targets << target
55
56
  # Handle plugins at this level so that lookups cannot trigger recursive lookups
56
- elsif target.is_a?(Hash)
57
+ when Hash
57
58
  add_target_definition(target)
58
59
  else
59
60
  raise ValidationError.new("Target entry must be a String or Hash, not #{target.class}", @name)
@@ -109,11 +109,12 @@ module Bolt
109
109
  private :resolve_name
110
110
 
111
111
  def expand_targets(targets)
112
- if targets.is_a? Bolt::Target
112
+ case targets
113
+ when Bolt::Target
113
114
  targets
114
- elsif targets.is_a? Array
115
+ when Array
115
116
  targets.map { |tish| expand_targets(tish) }
116
- elsif targets.is_a? String
117
+ when String
117
118
  # Expand a comma-separated list
118
119
  targets.split(/[[:space:],]+/).reject(&:empty?).map do |name|
119
120
  ts = resolve_name(name)
@@ -67,6 +67,10 @@ module Bolt
67
67
  end
68
68
  end
69
69
 
70
+ def self.analytics=(analytics)
71
+ @analytics = analytics
72
+ end
73
+
70
74
  def self.console_layout(color)
71
75
  color_scheme = :bolt if color
72
76
  Logging.layouts.pattern(
@@ -114,5 +118,10 @@ module Bolt
114
118
  end
115
119
  }
116
120
  end
121
+
122
+ def self.deprecation_warning(type, msg)
123
+ @analytics&.event('Warn', 'deprecation', label: type)
124
+ warn_once(type, msg)
125
+ end
117
126
  end
118
127
  end
@@ -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
@@ -372,10 +367,6 @@ module Bolt
372
367
  end
373
368
  end
374
369
 
375
- def print_message_event(event)
376
- print_message(event[:message])
377
- end
378
-
379
370
  def fatal_error(err)
380
371
  @stream.puts(colorize(:red, err.message))
381
372
  if err.is_a? Bolt::RunFailure
@@ -121,10 +121,6 @@ module Bolt
121
121
  @stream.puts '}' if @object_open
122
122
  end
123
123
 
124
- def print_message_event(event)
125
- print_message(event[:message])
126
- end
127
-
128
124
  def print_message(message)
129
125
  $stderr.puts(message)
130
126
  end
@@ -8,6 +8,12 @@ module Bolt
8
8
  def initialize(color, verbose, trace, stream = $stdout)
9
9
  begin
10
10
  require 'paint'
11
+ if Bolt::Util.windows?
12
+ # the Paint gem thinks that windows does not support ansi colors
13
+ # but windows 10 or later does
14
+ # we can display colors if we force mode to TRUE_COLOR
15
+ Paint.mode = 0xFFFFFF
16
+ end
11
17
  rescue LoadError
12
18
  raise "The 'paint' gem is required to use the rainbow outputter."
13
19
  end
@@ -33,9 +39,10 @@ module Bolt
33
39
  a = string.chars.map do |c|
34
40
  case @state
35
41
  when :normal
36
- if c == "\e"
42
+ case c
43
+ when "\e"
37
44
  @state = :ansi
38
- elsif c == "\n"
45
+ when "\n"
39
46
  @line_color += 1
40
47
  @color = @line_color
41
48
  c
@@ -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
@@ -150,7 +150,12 @@ module Bolt
150
150
  r = Puppet::Pal.in_tmp_environment('bolt', modulepath: @modulepath, facts: {}) do |pal|
151
151
  # Only load the project if it a) exists, b) has a name it can be loaded with
152
152
  bolt_project = @project if @project&.name
153
+ # Puppet currently won't receive the project unless it is a named project. Since
154
+ # the download_file plan function needs access to the project path, add it to the
155
+ # context.
156
+ bolt_project_data = @project
153
157
  Puppet.override(bolt_project: bolt_project,
158
+ bolt_project_data: bolt_project_data,
154
159
  yaml_plan_instantiator: Bolt::PAL::YamlPlan::Loader) do
155
160
  pal.with_script_compiler do |compiler|
156
161
  alias_types(compiler)
@@ -203,7 +208,7 @@ module Bolt
203
208
  # Skip syncing built-in plugins, since we vendor some Puppet 6
204
209
  # versions of "core" types, which are already present on the agent,
205
210
  # but may cause issues on Puppet 5 agents.
206
- @original_modulepath,
211
+ @user_modulepath,
207
212
  @project,
208
213
  pdb_client,
209
214
  @hiera_config,
@@ -273,15 +278,12 @@ module Bolt
273
278
  end
274
279
  end
275
280
 
276
- def list_modulepath
277
- @modulepath - [BOLTLIB_PATH, MODULES_PATH]
278
- end
279
-
280
281
  def parse_params(type, object_name, params)
281
282
  in_bolt_compiler do |compiler|
282
- if type == 'task'
283
+ case type
284
+ when 'task'
283
285
  param_spec = compiler.task_signature(object_name)&.task_hash&.dig('parameters')
284
- elsif type == 'plan'
286
+ when 'plan'
285
287
  plan = compiler.plan_signature(object_name)
286
288
  param_spec = plan.params_type.elements&.each_with_object({}) { |t, h| h[t.name] = t.value_type } if plan
287
289
  end
@@ -73,7 +73,7 @@ module Bolt
73
73
  end
74
74
 
75
75
  def upload_step(scope, step)
76
- source = step['source']
76
+ source = step['upload'] || step['source']
77
77
  destination = step['destination']
78
78
  targets = step['targets'] || step['target']
79
79
  description = step['description']
@@ -83,6 +83,17 @@ module Bolt
83
83
  scope.call_function('upload_file', args)
84
84
  end
85
85
 
86
+ def download_step(scope, step)
87
+ source = step['download']
88
+ destination = step['destination']
89
+ targets = step['targets'] || step['target']
90
+ description = step['description']
91
+
92
+ args = [source, destination, targets]
93
+ args << description if description
94
+ scope.call_function('download_file', args)
95
+ end
96
+
86
97
  def eval_step(_scope, step)
87
98
  step['eval']
88
99
  end
@@ -97,6 +108,10 @@ module Bolt
97
108
  apply_manifest(scope, targets, manifest)
98
109
  end
99
110
 
111
+ def message_step(scope, step)
112
+ scope.call_function('out::message', [step['message']])
113
+ end
114
+
100
115
  def generate_manifest(resources)
101
116
  # inspect returns the Ruby representation of the resource hashes,
102
117
  # which happens to be the same as the Puppet representation
@@ -142,7 +157,13 @@ module Bolt
142
157
  if plan.steps.any? { |step| step.body.key?('target') }
143
158
  msg = "The 'target' parameter for YAML plan steps is deprecated and will be removed "\
144
159
  "in a future version of Bolt. Use the 'targets' parameter instead."
145
- @logger.warn(msg)
160
+ Bolt::Logger.deprecation_warning("Using 'target' parameter for YAML plan steps, not 'targets'", msg)
161
+ end
162
+
163
+ if plan.steps.any? { |step| step.body.key?('source') }
164
+ msg = "The 'source' parameter for YAML plan upload steps is deprecated and will be removed "\
165
+ "in a future version of Bolt. Use the 'upload' parameter instead."
166
+ Bolt::Logger.deprecation_warning("Using 'source' parameter for YAML upload steps, not 'upload'", msg)
146
167
  end
147
168
 
148
169
  plan_result = closure_scope.with_local_scope(args_hash) do |scope|