bolt 2.17.0 → 2.22.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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +3 -1
  3. data/bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb +20 -9
  4. data/bolt-modules/boltlib/lib/puppet/functions/download_file.rb +123 -0
  5. data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +6 -0
  6. data/bolt-modules/dir/lib/puppet/functions/dir/children.rb +35 -0
  7. data/lib/bolt/applicator.rb +19 -14
  8. data/lib/bolt/apply_result.rb +1 -1
  9. data/lib/bolt/bolt_option_parser.rb +68 -13
  10. data/lib/bolt/catalog.rb +12 -3
  11. data/lib/bolt/cli.rb +232 -47
  12. data/lib/bolt/config.rb +34 -13
  13. data/lib/bolt/config/options.rb +16 -1
  14. data/lib/bolt/config/transport/options.rb +16 -10
  15. data/lib/bolt/config/transport/ssh.rb +24 -10
  16. data/lib/bolt/executor.rb +21 -0
  17. data/lib/bolt/inventory/group.rb +3 -2
  18. data/lib/bolt/inventory/inventory.rb +4 -3
  19. data/lib/bolt/logger.rb +21 -0
  20. data/lib/bolt/module.rb +2 -1
  21. data/lib/bolt/outputter/rainbow.rb +9 -2
  22. data/lib/bolt/pal.rb +8 -2
  23. data/lib/bolt/pal/yaml_plan/evaluator.rb +23 -2
  24. data/lib/bolt/pal/yaml_plan/step.rb +24 -2
  25. data/lib/bolt/pal/yaml_plan/step/download.rb +38 -0
  26. data/lib/bolt/pal/yaml_plan/step/message.rb +30 -0
  27. data/lib/bolt/pal/yaml_plan/step/upload.rb +3 -3
  28. data/lib/bolt/plugin/module.rb +2 -4
  29. data/lib/bolt/plugin/puppetdb.rb +3 -2
  30. data/lib/bolt/project.rb +25 -11
  31. data/lib/bolt/puppetdb/client.rb +2 -0
  32. data/lib/bolt/puppetdb/config.rb +16 -0
  33. data/lib/bolt/result.rb +7 -0
  34. data/lib/bolt/shell/bash.rb +24 -4
  35. data/lib/bolt/shell/powershell.rb +10 -4
  36. data/lib/bolt/shell/powershell/snippets.rb +15 -6
  37. data/lib/bolt/transport/base.rb +24 -0
  38. data/lib/bolt/transport/docker.rb +8 -0
  39. data/lib/bolt/transport/docker/connection.rb +20 -2
  40. data/lib/bolt/transport/local/connection.rb +14 -1
  41. data/lib/bolt/transport/orch.rb +12 -0
  42. data/lib/bolt/transport/simple.rb +6 -0
  43. data/lib/bolt/transport/ssh.rb +7 -1
  44. data/lib/bolt/transport/ssh/connection.rb +9 -1
  45. data/lib/bolt/transport/ssh/exec_connection.rb +23 -2
  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/transport_app.rb +3 -2
  50. data/lib/bolt_spec/bolt_context.rb +7 -2
  51. data/lib/bolt_spec/plans.rb +15 -2
  52. data/lib/bolt_spec/plans/action_stubs.rb +2 -1
  53. data/lib/bolt_spec/plans/action_stubs/download_stub.rb +66 -0
  54. data/lib/bolt_spec/plans/mock_executor.rb +14 -1
  55. data/lib/bolt_spec/run.rb +22 -0
  56. data/libexec/bolt_catalog +3 -2
  57. data/modules/secure_env_vars/plans/init.pp +20 -0
  58. 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'
@@ -28,7 +28,7 @@ module Bolt
28
28
  DEFAULT_DEFAULT_CONCURRENCY = 100
29
29
 
30
30
  def self.default
31
- new(Bolt::Project.create_project('.'), {})
31
+ new(Bolt::Project.default_project, {})
32
32
  end
33
33
 
34
34
  def self.from_project(project, overrides = {})
@@ -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)
@@ -121,13 +123,26 @@ module Bolt
121
123
  )
122
124
  end
123
125
 
124
- # Move data under transport-config to top-level so it can be easily merged with
125
- # config from other sources.
126
+ # Move data under inventory-config to top-level so it can be easily merged with
127
+ # config from other sources. Error early if inventory-config is not a hash or
128
+ # has a plugin reference.
126
129
  if data.key?('inventory-config')
130
+ unless data['inventory-config'].is_a?(Hash)
131
+ raise Bolt::ValidationError,
132
+ "Option 'inventory-config' must be of type Hash, received #{data['inventory-config']} "\
133
+ "#{data['inventory-config']} (file: #{filepath})"
134
+ end
135
+
136
+ if data['inventory-config'].key?('_plugin')
137
+ raise Bolt::ValidationError,
138
+ "Found unsupported key '_plugin' for option 'inventory-config'; supported keys are "\
139
+ "'#{INVENTORY_OPTIONS.keys.join("', '")}' (file: #{filepath})"
140
+ end
141
+
127
142
  data = data.merge(data.delete('inventory-config'))
128
143
  end
129
144
 
130
- { filepath: filepath, data: data, warnings: warnings }
145
+ { filepath: filepath, data: data, warnings: warnings, deprecations: [] }
131
146
  end
132
147
 
133
148
  # Loads a 'bolt.yaml' file, the legacy configuration file. There's no special munging needed
@@ -135,10 +150,11 @@ module Bolt
135
150
  def self.load_bolt_yaml(dir)
136
151
  filepath = dir + BOLT_CONFIG_NAME
137
152
  data = Bolt::Util.read_yaml_hash(filepath, 'config')
138
- warnings = [msg: "Configuration file #{filepath} is deprecated and will be removed in a future version "\
139
- "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." }]
140
156
 
141
- { filepath: filepath, data: data, warnings: warnings }
157
+ { filepath: filepath, data: data, warnings: [], deprecations: deprecations }
142
158
  end
143
159
 
144
160
  def self.load_defaults(project)
@@ -169,12 +185,16 @@ module Bolt
169
185
 
170
186
  def initialize(project, config_data, overrides = {})
171
187
  unless config_data.is_a?(Array)
172
- 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: [] }]
173
192
  end
174
193
 
175
194
  @logger = Logging.logger[self]
176
195
  @project = project
177
196
  @warnings = @project.warnings.dup
197
+ @deprecations = @project.deprecations.dup
178
198
  @transports = {}
179
199
  @config_files = []
180
200
 
@@ -195,6 +215,7 @@ module Bolt
195
215
 
196
216
  loaded_data = config_data.each_with_object([]) do |data, acc|
197
217
  @warnings.concat(data[:warnings]) if data[:warnings].any?
218
+ @deprecations.concat(data[:deprecations]) if data[:deprecations].any?
198
219
 
199
220
  if data[:data].any?
200
221
  @config_files.push(data[:filepath])
@@ -365,7 +386,7 @@ module Bolt
365
386
  raise Bolt::ValidationError, "Compilation is CPU-intensive, set concurrency less than #{compile_limit}"
366
387
  end
367
388
 
368
- if (format == 'rainbow' && Bolt::Util.windows?) || !(%w[human json rainbow].include? format)
389
+ unless %w[human json rainbow].include? format
369
390
  raise Bolt::ValidationError, "Unsupported format: '#{format}'"
370
391
  end
371
392
 
@@ -478,7 +499,7 @@ module Bolt
478
499
  end
479
500
 
480
501
  def matching_paths(paths)
481
- [*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) }
482
503
  end
483
504
 
484
505
  private def casefold(path)
@@ -175,7 +175,7 @@ module Bolt
175
175
  "log" => {
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
- "~/.puppetlabs/bolt/debug.log`. Individual log files must be valid filepaths. If the log "\
178
+ "`~/.puppetlabs/bolt/debug.log`. Individual log files must be valid filepaths. If the log "\
179
179
  "file does not exist, then Bolt will create it before logging information.",
180
180
  type: Hash,
181
181
  properties: {
@@ -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, "\
@@ -440,6 +454,7 @@ module Bolt
440
454
  concurrency
441
455
  format
442
456
  inventory-config
457
+ log
443
458
  plugin_hooks
444
459
  plugins
445
460
  puppetdb
@@ -108,17 +108,16 @@ module Bolt
108
108
  },
109
109
  "copy-command" => {
110
110
  type: [Array, String],
111
- description: "The command to use when copying files using ssh-command. Bolt runs `<copy-command> <src> "\
111
+ description: "The command to use when copying files using native SSH. Bolt runs `<copy-command> <src> "\
112
112
  "<dest>`. This option is used when you need support for features or algorithms that are not "\
113
113
  "supported by the net-ssh Ruby library. **This option is experimental.** You can read more "\
114
- "about this option in [External SSH "\
115
- "transport](experimental_features.md#external-ssh-transport).",
114
+ "about this option in [Native SSH transport](experimental_features.md#native-ssh-transport).",
116
115
  items: {
117
116
  type: String
118
117
  },
119
118
  _plugin: true,
120
- _default: "scp -r",
121
- _example: "scp -r -F ~/ssh-config/myconf"
119
+ _default: %w[scp -r],
120
+ _example: %w[scp -r -F ~/ssh-config/myconf]
122
121
  },
123
122
  "disconnect-timeout" => {
124
123
  type: Integer,
@@ -270,6 +269,13 @@ module Bolt
270
269
  _plugin: true,
271
270
  _example: %w[defaults hmac-md5]
272
271
  },
272
+ "native-ssh" => {
273
+ type: [TrueClass, FalseClass],
274
+ description: "This enables the native SSH transport, which shells out to SSH instead of using the "\
275
+ "net-ssh Ruby library",
276
+ _default: false,
277
+ _example: true
278
+ },
273
279
  "password" => {
274
280
  type: String,
275
281
  description: "The password to use to login.",
@@ -368,16 +374,16 @@ module Bolt
368
374
  },
369
375
  "ssh-command" => {
370
376
  type: [Array, String],
371
- description: "The command and flags to use when SSHing. This enables the external SSH transport, which "\
372
- "shells out to the specified command. This option is used when you need support for "\
377
+ description: "The command and flags to use when SSHing. This option is used when you need support for "\
373
378
  "features or algorithms that are not supported by the net-ssh Ruby library. **This option "\
374
- "is experimental.** You can read more about this option in [External SSH "\
375
- "transport](experimental_features.md#external-ssh-transport).",
379
+ "is experimental.** You can read more about this option in [Native SSH "\
380
+ "transport](experimental_features.md#native-ssh-transport).",
376
381
  items: {
377
382
  type: String
378
383
  },
379
384
  _plugin: true,
380
- _example: "ssh"
385
+ _default: 'ssh',
386
+ _example: %w[ssh -o Ciphers=chacha20-poly1305@openssh.com]
381
387
  },
382
388
  "ssl" => {
383
389
  type: [TrueClass, FalseClass],
@@ -32,13 +32,14 @@ module Bolt
32
32
  user
33
33
  ].concat(RUN_AS_OPTIONS).sort.freeze
34
34
 
35
- # Options available when using the external ssh transport
36
- EXTERNAL_OPTIONS = %w[
35
+ # Options available when using the native ssh transport
36
+ NATIVE_OPTIONS = %w[
37
37
  cleanup
38
38
  copy-command
39
39
  host
40
40
  host-key-check
41
41
  interpreters
42
+ native-ssh
42
43
  port
43
44
  private-key
44
45
  script-dir
@@ -56,17 +57,30 @@ module Bolt
56
57
  "tty" => false
57
58
  }.freeze
58
59
 
59
- # The set of options available for the ssh and external ssh transports overlap, so we
60
+ # The set of options available for the ssh and native ssh transports overlap, so we
60
61
  # need to check which transport is used before fully initializing, otherwise options
61
62
  # may not be filtered correctly.
62
63
  def initialize(data = {}, project = nil)
63
64
  assert_hash_or_config(data)
64
- @external = true if data['ssh-command']
65
+ @native = true if data['native-ssh']
65
66
  super(data, project)
66
67
  end
67
68
 
69
+ # This method is used to filter CLI options in the Config class. This
70
+ # should include `ssh-command` so that we can later warn if the option
71
+ # is present without `native-ssh`
72
+ def self.options
73
+ %w[ssh-command native-ssh].concat(OPTIONS)
74
+ end
75
+
68
76
  private def filter(unfiltered)
69
- @external ? unfiltered.slice(*EXTERNAL_OPTIONS) : unfiltered.slice(*OPTIONS)
77
+ # Because we filter before merging config together it's impossible to
78
+ # know whether both ssh-command *and* native-ssh will be specified
79
+ # unless they are both in the filter. However, we can't add
80
+ # ssh-command to OPTIONS since that's used for documenting available
81
+ # options. This makes it so that ssh-command is preserved so we can
82
+ # warn once all config is resolved if native-ssh isn't set.
83
+ @native ? unfiltered.slice(*NATIVE_OPTIONS) : unfiltered.slice(*self.class.options)
70
84
  end
71
85
 
72
86
  private def validate
@@ -83,11 +97,11 @@ module Bolt
83
97
  @config['private-key'] = File.expand_path(key_opt, @project)
84
98
 
85
99
  # We have an explicit test for this to only warn if using net-ssh transport
86
- Bolt::Util.validate_file('ssh key', @config['private-key']) if @config['ssh-command']
100
+ Bolt::Util.validate_file('ssh key', @config['private-key']) if @config['native-ssh']
87
101
  end
88
102
 
89
- if key_opt.instance_of?(Hash) && @config['ssh-command']
90
- raise Bolt::ValidationError, 'private-key must be a filepath when using ssh-command'
103
+ if key_opt.instance_of?(Hash) && @config['native-ssh']
104
+ raise Bolt::ValidationError, 'private-key must be a filepath when using native-ssh'
91
105
  end
92
106
  end
93
107
 
@@ -117,8 +131,8 @@ module Bolt
117
131
  end
118
132
  end
119
133
 
120
- if @config['ssh-command'] && !@config['load-config']
121
- msg = 'Cannot use external SSH transport with load-config set to false'
134
+ if @config['native-ssh'] && !@config['load-config']
135
+ msg = 'Cannot use native SSH transport with load-config set to false'
122
136
  raise Bolt::ValidationError, msg
123
137
  end
124
138
  end
@@ -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
@@ -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)
@@ -15,6 +15,7 @@ module Bolt
15
15
  return if Logging.initialized?
16
16
 
17
17
  Logging.init :debug, :info, :notice, :warn, :error, :fatal, :any
18
+ @mutex = Mutex.new
18
19
 
19
20
  Logging.color_scheme(
20
21
  'bolt',
@@ -66,6 +67,10 @@ module Bolt
66
67
  end
67
68
  end
68
69
 
70
+ def self.analytics=(analytics)
71
+ @analytics = analytics
72
+ end
73
+
69
74
  def self.console_layout(color)
70
75
  color_scheme = :bolt if color
71
76
  Logging.layouts.pattern(
@@ -102,5 +107,21 @@ module Bolt
102
107
  def self.reset_logging
103
108
  Logging.reset
104
109
  end
110
+
111
+ def self.warn_once(type, msg)
112
+ @mutex.synchronize {
113
+ @warnings ||= []
114
+ @logger ||= Logging.logger[self]
115
+ unless @warnings.include?(type)
116
+ @logger.warn(msg)
117
+ @warnings << type
118
+ end
119
+ }
120
+ end
121
+
122
+ def self.deprecation_warning(type, msg)
123
+ @analytics&.event('Warn', 'deprecation', label: type)
124
+ warn_once(type, msg)
125
+ end
105
126
  end
106
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|
@@ -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
@@ -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)
@@ -279,9 +284,10 @@ module Bolt
279
284
 
280
285
  def parse_params(type, object_name, params)
281
286
  in_bolt_compiler do |compiler|
282
- if type == 'task'
287
+ case type
288
+ when 'task'
283
289
  param_spec = compiler.task_signature(object_name)&.task_hash&.dig('parameters')
284
- elsif type == 'plan'
290
+ when 'plan'
285
291
  plan = compiler.plan_signature(object_name)
286
292
  param_spec = plan.params_type.elements&.each_with_object({}) { |t, h| h[t.name] = t.value_type } if plan
287
293
  end