bolt 2.16.0 → 2.21.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 +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_command.rb +2 -0
  6. data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +6 -0
  7. data/bolt-modules/boltlib/lib/puppet/functions/run_script.rb +6 -4
  8. data/bolt-modules/dir/lib/puppet/functions/dir/children.rb +35 -0
  9. data/lib/bolt/applicator.rb +19 -14
  10. data/lib/bolt/apply_result.rb +1 -1
  11. data/lib/bolt/bolt_option_parser.rb +60 -16
  12. data/lib/bolt/catalog.rb +3 -2
  13. data/lib/bolt/cli.rb +121 -43
  14. data/lib/bolt/config.rb +37 -34
  15. data/lib/bolt/config/options.rb +340 -173
  16. data/lib/bolt/config/transport/options.rb +315 -160
  17. data/lib/bolt/config/transport/ssh.rb +24 -10
  18. data/lib/bolt/executor.rb +21 -0
  19. data/lib/bolt/inventory/group.rb +3 -2
  20. data/lib/bolt/inventory/inventory.rb +4 -3
  21. data/lib/bolt/logger.rb +24 -1
  22. data/lib/bolt/outputter.rb +1 -1
  23. data/lib/bolt/outputter/rainbow.rb +14 -3
  24. data/lib/bolt/pal.rb +28 -10
  25. data/lib/bolt/pal/yaml_plan/evaluator.rb +23 -2
  26. data/lib/bolt/pal/yaml_plan/step.rb +24 -2
  27. data/lib/bolt/pal/yaml_plan/step/download.rb +38 -0
  28. data/lib/bolt/pal/yaml_plan/step/message.rb +30 -0
  29. data/lib/bolt/pal/yaml_plan/step/upload.rb +3 -3
  30. data/lib/bolt/plugin/module.rb +2 -4
  31. data/lib/bolt/plugin/puppetdb.rb +3 -2
  32. data/lib/bolt/project.rb +20 -6
  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 +45 -37
  37. data/lib/bolt/shell/powershell.rb +21 -11
  38. data/lib/bolt/shell/powershell/snippets.rb +15 -6
  39. data/lib/bolt/transport/base.rb +24 -0
  40. data/lib/bolt/transport/docker.rb +16 -4
  41. data/lib/bolt/transport/docker/connection.rb +20 -2
  42. data/lib/bolt/transport/local/connection.rb +14 -1
  43. data/lib/bolt/transport/orch.rb +20 -0
  44. data/lib/bolt/transport/simple.rb +6 -0
  45. data/lib/bolt/transport/ssh.rb +7 -1
  46. data/lib/bolt/transport/ssh/connection.rb +9 -1
  47. data/lib/bolt/transport/ssh/exec_connection.rb +23 -2
  48. data/lib/bolt/transport/winrm/connection.rb +118 -8
  49. data/lib/bolt/util.rb +26 -11
  50. data/lib/bolt/version.rb +1 -1
  51. data/lib/bolt_server/transport_app.rb +3 -2
  52. data/lib/bolt_spec/bolt_context.rb +7 -2
  53. data/lib/bolt_spec/plans.rb +15 -2
  54. data/lib/bolt_spec/plans/action_stubs.rb +2 -1
  55. data/lib/bolt_spec/plans/action_stubs/download_stub.rb +66 -0
  56. data/lib/bolt_spec/plans/mock_executor.rb +14 -1
  57. data/lib/bolt_spec/run.rb +22 -0
  58. data/libexec/bolt_catalog +3 -2
  59. data/modules/secure_env_vars/plans/init.pp +20 -0
  60. metadata +21 -29
@@ -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(
@@ -89,8 +94,10 @@ module Bolt
89
94
  :notice
90
95
  end
91
96
 
97
+ # Explicitly check the log level names instead of the log level number, as levels
98
+ # that are stringified integers (e.g. "level" => "42") will return a truthy value
92
99
  def self.valid_level?(level)
93
- !Logging.level_num(level).nil?
100
+ Logging::LEVELS.include?(Logging.levelify(level))
94
101
  end
95
102
 
96
103
  def self.levels
@@ -100,5 +107,21 @@ module Bolt
100
107
  def self.reset_logging
101
108
  Logging.reset
102
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
103
126
  end
104
127
  end
@@ -26,5 +26,5 @@ end
26
26
 
27
27
  require 'bolt/outputter/human'
28
28
  require 'bolt/outputter/json'
29
- require 'bolt/outputter/rainbow'
30
29
  require 'bolt/outputter/logger'
30
+ require 'bolt/outputter/rainbow'
@@ -1,12 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'bolt/pal'
4
- require 'paint'
5
4
 
6
5
  module Bolt
7
6
  class Outputter
8
7
  class Rainbow < Bolt::Outputter::Human
9
8
  def initialize(color, verbose, trace, stream = $stdout)
9
+ begin
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
17
+ rescue LoadError
18
+ raise "The 'paint' gem is required to use the rainbow outputter."
19
+ end
10
20
  super
11
21
  @line_color = 0
12
22
  @color = 0
@@ -29,9 +39,10 @@ module Bolt
29
39
  a = string.chars.map do |c|
30
40
  case @state
31
41
  when :normal
32
- if c == "\e"
42
+ case c
43
+ when "\e"
33
44
  @state = :ansi
34
- elsif c == "\n"
45
+ when "\n"
35
46
  @line_color += 1
36
47
  @color = @line_color
37
48
  c
@@ -15,25 +15,36 @@ module Bolt
15
15
  # PALError is used to convert errors from executing puppet code into
16
16
  # Bolt::Errors
17
17
  class PALError < Bolt::Error
18
- # Puppet sometimes rescues exceptions notes the location and reraises.
19
- # Return the original error.
20
18
  def self.from_preformatted_error(err)
21
19
  if err.cause&.is_a? Bolt::Error
22
20
  err.cause
23
21
  else
24
- from_error(err.cause || err)
22
+ from_error(err)
25
23
  end
26
24
  end
27
25
 
28
26
  # Generate a Bolt::Pal::PALError for non-bolt errors
29
27
  def self.from_error(err)
30
- e = new(err.message)
28
+ # Use the original error message if available
29
+ message = err.cause ? err.cause.message : err.message
30
+
31
+ # Provide the location of an error if it came from a plan
32
+ details = if defined?(err.file) && err.file
33
+ { file: err.file,
34
+ line: err.line,
35
+ column: err.pos }.compact
36
+ else
37
+ {}
38
+ end
39
+
40
+ e = new(message, details)
41
+
31
42
  e.set_backtrace(err.backtrace)
32
43
  e
33
44
  end
34
45
 
35
- def initialize(msg)
36
- super(msg, 'bolt/pal-error')
46
+ def initialize(msg, details = {})
47
+ super(msg, 'bolt/pal-error', details)
37
48
  end
38
49
  end
39
50
 
@@ -139,7 +150,12 @@ module Bolt
139
150
  r = Puppet::Pal.in_tmp_environment('bolt', modulepath: @modulepath, facts: {}) do |pal|
140
151
  # Only load the project if it a) exists, b) has a name it can be loaded with
141
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
142
157
  Puppet.override(bolt_project: bolt_project,
158
+ bolt_project_data: bolt_project_data,
143
159
  yaml_plan_instantiator: Bolt::PAL::YamlPlan::Loader) do
144
160
  pal.with_script_compiler do |compiler|
145
161
  alias_types(compiler)
@@ -159,8 +175,9 @@ module Bolt
159
175
  if e.issue_code == :UNKNOWN_VARIABLE &&
160
176
  %w[facts trusted server_facts settings].include?(e.arguments[:name])
161
177
  message = "Evaluation Error: Variable '#{e.arguments[:name]}' is not available in the current scope "\
162
- "unless explicitly defined. (file: #{e.file}, line: #{e.line}, column: #{e.pos})"
163
- PALError.new(message)
178
+ "unless explicitly defined."
179
+ details = { file: e.file, line: e.line, column: e.pos }
180
+ PALError.new(message, details)
164
181
  else
165
182
  PALError.from_preformatted_error(e)
166
183
  end
@@ -267,9 +284,10 @@ module Bolt
267
284
 
268
285
  def parse_params(type, object_name, params)
269
286
  in_bolt_compiler do |compiler|
270
- if type == 'task'
287
+ case type
288
+ when 'task'
271
289
  param_spec = compiler.task_signature(object_name)&.task_hash&.dig('parameters')
272
- elsif type == 'plan'
290
+ when 'plan'
273
291
  plan = compiler.plan_signature(object_name)
274
292
  param_spec = plan.params_type.elements&.each_with_object({}) { |t, h| h[t.name] = t.value_type } if plan
275
293
  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|
@@ -12,7 +12,19 @@ module Bolt
12
12
  Set['name', 'description', 'target', 'targets']
13
13
  end
14
14
 
15
- STEP_KEYS = %w[command script task plan source destination eval resources].freeze
15
+ STEP_KEYS = %w[
16
+ command
17
+ destination
18
+ download
19
+ eval
20
+ message
21
+ plan
22
+ resources
23
+ script
24
+ source
25
+ task
26
+ upload
27
+ ].freeze
16
28
 
17
29
  def self.create(step_body, step_number)
18
30
  type_keys = (STEP_KEYS & step_body.keys)
@@ -22,8 +34,10 @@ module Bolt
22
34
  when 1
23
35
  type = type_keys.first
24
36
  else
25
- if type_keys.to_set == Set['source', 'destination']
37
+ if [Set['source', 'destination'], Set['upload', 'destination']].include?(type_keys.to_set)
26
38
  type = 'upload'
39
+ elsif type_keys.to_set == Set['download', 'destination']
40
+ type = 'download'
27
41
  else
28
42
  raise step_error("Multiple action keys detected: #{type_keys.inspect}", step_body['name'], step_number)
29
43
  end
@@ -89,6 +103,12 @@ module Bolt
89
103
  missing_keys -= ['targets']
90
104
  end
91
105
 
106
+ # Handle cases where upload step uses deprecated 'source' key instead of 'upload'
107
+ # TODO: Remove when 'source' is removed
108
+ if body.include?('source')
109
+ missing_keys -= ['upload']
110
+ end
111
+
92
112
  if missing_keys.any?
93
113
  error_message = "The #{step_type.inspect} step requires: #{missing_keys.to_a.inspect} key(s)"
94
114
  err = step_error(error_message, body['name'], step_number)
@@ -156,3 +176,5 @@ require 'bolt/pal/yaml_plan/step/resources'
156
176
  require 'bolt/pal/yaml_plan/step/script'
157
177
  require 'bolt/pal/yaml_plan/step/task'
158
178
  require 'bolt/pal/yaml_plan/step/upload'
179
+ require 'bolt/pal/yaml_plan/step/download'
180
+ require 'bolt/pal/yaml_plan/step/message'
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bolt
4
+ class PAL
5
+ class YamlPlan
6
+ class Step
7
+ class Download < Step
8
+ def self.allowed_keys
9
+ super + Set['download', 'destination']
10
+ end
11
+
12
+ def self.required_keys
13
+ Set['download', 'destination', 'targets']
14
+ end
15
+
16
+ def initialize(step_body)
17
+ super
18
+ @source = step_body['download']
19
+ @destination = step_body['destination']
20
+ end
21
+
22
+ def transpile
23
+ code = String.new(" ")
24
+ code << "$#{@name} = " if @name
25
+
26
+ fn = 'download_file'
27
+ args = [@source, @destination, @targets]
28
+ args << @description if @description
29
+
30
+ code << function_call(fn, args)
31
+
32
+ code << "\n"
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end