bolt 3.11.0 → 3.15.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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +2 -2
  3. data/bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb +137 -104
  4. data/bolt-modules/boltlib/lib/puppet/functions/background.rb +2 -1
  5. data/bolt-modules/boltlib/lib/puppet/functions/parallelize.rb +5 -1
  6. data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +13 -0
  7. data/bolt-modules/boltlib/lib/puppet/functions/wait.rb +47 -7
  8. data/bolt-modules/log/lib/puppet/functions/log/debug.rb +39 -0
  9. data/bolt-modules/log/lib/puppet/functions/log/error.rb +40 -0
  10. data/bolt-modules/log/lib/puppet/functions/log/fatal.rb +40 -0
  11. data/bolt-modules/log/lib/puppet/functions/log/info.rb +39 -0
  12. data/bolt-modules/log/lib/puppet/functions/log/trace.rb +39 -0
  13. data/bolt-modules/log/lib/puppet/functions/log/warn.rb +41 -0
  14. data/bolt-modules/out/lib/puppet/functions/out/message.rb +9 -49
  15. data/bolt-modules/out/lib/puppet/functions/out/verbose.rb +35 -0
  16. data/guides/{debugging.txt → debugging.yaml} +5 -6
  17. data/guides/{inventory.txt → inventory.yaml} +6 -7
  18. data/guides/{links.txt → links.yaml} +3 -4
  19. data/guides/{logging.txt → logging.yaml} +5 -6
  20. data/guides/{module.txt → module.yaml} +5 -6
  21. data/guides/{modulepath.txt → modulepath.yaml} +5 -6
  22. data/guides/{project.txt → project.yaml} +6 -7
  23. data/guides/{targets.txt → targets.yaml} +5 -6
  24. data/guides/{transports.txt → transports.yaml} +6 -7
  25. data/lib/bolt/analytics.rb +1 -1
  26. data/lib/bolt/applicator.rb +23 -1
  27. data/lib/bolt/bolt_option_parser.rb +6 -3
  28. data/lib/bolt/cli.rb +34 -14
  29. data/lib/bolt/config/options.rb +2 -2
  30. data/lib/bolt/config/transport/options.rb +12 -0
  31. data/lib/bolt/config/transport/ssh.rb +7 -0
  32. data/lib/bolt/error.rb +3 -3
  33. data/lib/bolt/executor.rb +12 -4
  34. data/lib/bolt/fiber_executor.rb +57 -12
  35. data/lib/bolt/outputter/human.rb +124 -15
  36. data/lib/bolt/outputter/json.rb +5 -5
  37. data/lib/bolt/outputter/logger.rb +6 -0
  38. data/lib/bolt/pal.rb +81 -21
  39. data/lib/bolt/pal/yaml_plan/step.rb +2 -0
  40. data/lib/bolt/pal/yaml_plan/step/message.rb +0 -8
  41. data/lib/bolt/pal/yaml_plan/step/verbose.rb +31 -0
  42. data/lib/bolt/pal/yaml_plan/transpiler.rb +1 -1
  43. data/lib/bolt/plan_future.rb +21 -6
  44. data/lib/bolt/plugin/task.rb +1 -1
  45. data/lib/bolt/transport/ssh/exec_connection.rb +3 -1
  46. data/lib/bolt/util/format.rb +68 -0
  47. data/lib/bolt/version.rb +1 -1
  48. data/lib/bolt_server/schemas/connect-data.json +4 -1
  49. data/lib/bolt_server/schemas/partials/target-ssh.json +4 -0
  50. data/lib/bolt_server/schemas/partials/target-winrm.json +4 -0
  51. data/lib/bolt_server/transport_app.rb +93 -52
  52. data/lib/bolt_spec/bolt_context.rb +9 -0
  53. data/lib/bolt_spec/plans.rb +1 -1
  54. data/lib/bolt_spec/plans/mock_executor.rb +31 -7
  55. data/lib/bolt_spec/plans/publish_stub.rb +4 -4
  56. data/modules/canary/plans/init.pp +1 -1
  57. data/resources/bolt_bash_completion.sh +1 -1
  58. metadata +28 -14
  59. data/guides/guide.txt +0 -17
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bolt
4
+ class PAL
5
+ class YamlPlan
6
+ class Step
7
+ class Verbose < Step
8
+ def self.allowed_keys
9
+ super + Set['verbose']
10
+ end
11
+
12
+ def self.required_keys
13
+ Set['verbose']
14
+ end
15
+
16
+ # Returns an array of arguments to pass to the step's function call
17
+ #
18
+ private def format_args(body)
19
+ [body['verbose']]
20
+ end
21
+
22
+ # Returns the function corresponding to the step
23
+ #
24
+ private def function
25
+ 'out::verbose'
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -30,7 +30,7 @@ module Bolt
30
30
  plan_string = String.new('')
31
31
  plan_string << "# #{plan_object.description}\n" if plan_object.description
32
32
  plan_string << "# WARNING: This is an autogenerated plan. It might not behave as expected.\n"
33
- plan_string << "# @private #{plan_object.private}\n" unless plan_object.private.nil?
33
+ plan_string << "# @api #{plan_object.private ? 'private' : 'public'}\n" unless plan_object.private.nil?
34
34
  plan_string << "#{param_descriptions}\n" unless param_descriptions.empty?
35
35
 
36
36
  plan_string << "plan #{plan_object.name}("
@@ -5,13 +5,28 @@ require 'fiber'
5
5
  module Bolt
6
6
  class PlanFuture
7
7
  attr_reader :fiber, :id
8
- attr_accessor :value
8
+ attr_accessor :value, :plan_stack
9
9
 
10
- def initialize(fiber, id, name = nil)
11
- @fiber = fiber
12
- @id = id
13
- @name = name
14
- @value = nil
10
+ def initialize(fiber, id, plan_id:, name: nil)
11
+ @fiber = fiber
12
+ @id = id
13
+ @name = name
14
+ @value = nil
15
+ # The plan invocation ID when the Future is created may be
16
+ # different from the plan ID of the Future when we switch to it if a new
17
+ # plan was run inside the Future, so keep track of the plans that a
18
+ # Future is executing in as a stack. When one plan finishes, pop it off
19
+ # since now we're in the calling plan. These IDs are unique to each plan
20
+ # invocation, not just plan names.
21
+ @plan_stack = [plan_id]
22
+ end
23
+
24
+ def original_plan
25
+ @plan_stack.last
26
+ end
27
+
28
+ def current_plan
29
+ @plan_stack.first
15
30
  end
16
31
 
17
32
  def name
@@ -59,7 +59,7 @@ module Bolt
59
59
  run_opts = {}
60
60
  run_opts[:run_as] = opts['_run_as'] if opts['_run_as']
61
61
  begin
62
- task = apply_prep.get_task(opts['task'], params)
62
+ task = @context.get_validated_task(opts['task'], params)
63
63
  rescue Bolt::Error => e
64
64
  raise Bolt::Plugin::PluginError::ExecutionError.new(e.message, name, 'puppet_library')
65
65
  end
@@ -47,7 +47,9 @@ module Bolt
47
47
  cmd = []
48
48
  # BatchMode is SSH's noninteractive option: if key authentication
49
49
  # fails it will error out instead of falling back to password prompt
50
- cmd += %w[-o BatchMode=yes]
50
+ batch_mode = @target.transport_config['batch-mode'] ? 'yes' : 'no'
51
+ cmd += %W[-o BatchMode=#{batch_mode}]
52
+
51
53
  cmd += %W[-o Port=#{@target.port}] if @target.port
52
54
 
53
55
  if @target.transport_config.key?('host-key-check')
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bolt
4
+ module Util
5
+ module Format
6
+ class << self
7
+ # Stringifies an object, formatted as valid JSON.
8
+ #
9
+ # @param message [Object] The object to stringify.
10
+ # @return [String] The JSON string.
11
+ #
12
+ def stringify(message)
13
+ formatted = format_message(message)
14
+ if formatted.is_a?(Hash) || formatted.is_a?(Array)
15
+ ::JSON.pretty_generate(formatted)
16
+ else
17
+ formatted
18
+ end
19
+ end
20
+
21
+ # Recursively formats an object into a format that can be represented by
22
+ # JSON.
23
+ #
24
+ # @param message [Object] The object to stringify.
25
+ # @return [Array, Hash, String]
26
+ #
27
+ private def format_message(message)
28
+ case message
29
+ when Array
30
+ message.map { |item| format_message(item) }
31
+ when Bolt::ApplyResult
32
+ format_apply_result(message)
33
+ when Bolt::Result, Bolt::ResultSet
34
+ # This is equivalent to to_s, but formattable
35
+ message.to_data
36
+ when Bolt::RunFailure
37
+ formatted_resultset = message.result_set.to_data
38
+ message.to_h.merge('result_set' => formatted_resultset)
39
+ when Hash
40
+ message.each_with_object({}) do |(k, v), h|
41
+ h[format_message(k)] = format_message(v)
42
+ end
43
+ when Integer, Float, NilClass
44
+ message
45
+ else
46
+ message.to_s
47
+ end
48
+ end
49
+
50
+ # Formats a Bolt::ApplyResult object.
51
+ #
52
+ # @param result [Bolt::ApplyResult] The apply result.
53
+ # @return [Hash]
54
+ #
55
+ private def format_apply_result(result)
56
+ logs = result.resource_logs&.map do |log|
57
+ # Omit low-level info/debug messages
58
+ next if %w[info debug].include?(log['level'])
59
+ indent(2, format_log(log))
60
+ end
61
+ hash = result.to_data
62
+ hash['logs'] = logs unless logs.empty?
63
+ hash
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
data/lib/bolt/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bolt
4
- VERSION = '3.11.0'
4
+ VERSION = '3.15.0'
5
5
  end
@@ -16,7 +16,10 @@
16
16
  }
17
17
  },
18
18
  "additionalProperties": false
19
+ },
20
+ "versioned_project": {
21
+ "type": "string"
19
22
  }
20
23
  },
21
- "required": ["puppet_connect_data"]
24
+ "required": ["puppet_connect_data", "versioned_project"]
22
25
  }
@@ -62,6 +62,10 @@
62
62
  "interpreters": {
63
63
  "type": "object",
64
64
  "description": "Map of file extensions to remote executable"
65
+ },
66
+ "plugin_hooks": {
67
+ "type": "object",
68
+ "description": "Configuration for plugins to use"
65
69
  }
66
70
  },
67
71
  "oneOf": [
@@ -56,6 +56,10 @@
56
56
  "smb-port": {
57
57
  "type": "integer",
58
58
  "description": "Port for SMB protocol"
59
+ },
60
+ "plugin_hooks": {
61
+ "type": "object",
62
+ "description": "Configuration for plugins to use"
59
63
  }
60
64
  },
61
65
  "required": ["hostname", "user", "password"],
@@ -120,9 +120,20 @@ module BoltServer
120
120
 
121
121
  def run_task(target, body)
122
122
  validate_schema(@schemas["action-run_task"], body)
123
+
123
124
  task_data = body['task']
124
125
  task = Bolt::Task::PuppetServer.new(task_data['name'], task_data['metadata'], task_data['files'], @file_cache)
125
126
  parameters = body['parameters'] || {}
127
+ # Wrap parameters marked with '"sensitive": true' in the task metadata with a
128
+ # Sensitive wrapper type. This way it's not shown in logs.
129
+ if (param_spec = task.parameters)
130
+ parameters.each do |k, v|
131
+ if param_spec[k] && param_spec[k]['sensitive']
132
+ parameters[k] = Puppet::Pops::Types::PSensitiveType::Sensitive.new(v)
133
+ end
134
+ end
135
+ end
136
+
126
137
  @executor.run_task(target, task, parameters).each do |result|
127
138
  value = result.value
128
139
  next unless value.is_a?(Hash)
@@ -378,6 +389,63 @@ module BoltServer
378
389
  end
379
390
  end
380
391
 
392
+ # The provided block takes a module object and returns the list
393
+ # of directories to search through. This is similar to
394
+ # Bolt::Applicator.build_plugin_tarball.
395
+ def build_project_plugins_tarball(versioned_project, &block)
396
+ start_time = Time.now
397
+
398
+ # Fetch the plugin files
399
+ plugin_files = in_bolt_project(versioned_project) do |context|
400
+ files = {}
401
+
402
+ # Bolt also sets plugin_modulepath to user modulepath so do it here too for
403
+ # consistency
404
+ plugin_modulepath = context[:pal].user_modulepath
405
+ Puppet.lookup(:current_environment).override_with(modulepath: plugin_modulepath).modules.each do |mod|
406
+ search_dirs = block.call(mod)
407
+
408
+ files[mod] ||= []
409
+ Find.find(*search_dirs).each do |file|
410
+ files[mod] << file if File.file?(file)
411
+ end
412
+ end
413
+
414
+ files
415
+ end
416
+
417
+ # Pack the plugin files
418
+ sio = StringIO.new
419
+ begin
420
+ output = Minitar::Output.new(Zlib::GzipWriter.new(sio))
421
+
422
+ plugin_files.each do |mod, files|
423
+ tar_dir = Pathname.new(mod.name)
424
+ mod_dir = Pathname.new(mod.path)
425
+
426
+ files.each do |file|
427
+ tar_path = tar_dir + Pathname.new(file).relative_path_from(mod_dir)
428
+ stat = File.stat(file)
429
+ content = File.binread(file)
430
+ output.tar.add_file_simple(
431
+ tar_path.to_s,
432
+ data: content,
433
+ size: content.size,
434
+ mode: stat.mode & 0o777,
435
+ mtime: stat.mtime
436
+ )
437
+ end
438
+ end
439
+
440
+ duration = Time.now - start_time
441
+ @logger.trace("Packed plugins in #{duration * 1000} ms")
442
+ ensure
443
+ output.close
444
+ end
445
+
446
+ Base64.encode64(sio.string)
447
+ end
448
+
381
449
  get '/' do
382
450
  200
383
451
  end
@@ -430,7 +498,7 @@ module BoltServer
430
498
  'uri' => target_hash['hostname'],
431
499
  'config' => {
432
500
  'transport' => 'ssh',
433
- 'ssh' => opts
501
+ 'ssh' => opts.slice(*Bolt::Config::Transport::SSH.options)
434
502
  }
435
503
  }
436
504
 
@@ -468,7 +536,7 @@ module BoltServer
468
536
  'uri' => target_hash['hostname'],
469
537
  'config' => {
470
538
  'transport' => 'winrm',
471
- 'winrm' => opts
539
+ 'winrm' => opts.slice(*Bolt::Config::Transport::WinRM.options)
472
540
  }
473
541
  }
474
542
 
@@ -637,11 +705,10 @@ module BoltServer
637
705
  #
638
706
  # @param versioned_project [String] the versioned_project to compute the inventory from
639
707
  post '/project_inventory_targets' do
640
- raise BoltServer::RequestError, "'versioned_project' is a required argument" if params['versioned_project'].nil?
641
708
  content_type :json
642
709
  body = JSON.parse(request.body.read)
643
710
  validate_schema(@schemas["connect-data"], body)
644
- in_bolt_project(params['versioned_project']) do |context|
711
+ in_bolt_project(body['versioned_project']) do |context|
645
712
  if context[:config].inventoryfile &&
646
713
  context[:config].project.inventory_file.to_s !=
647
714
  context[:config].inventoryfile
@@ -689,60 +756,34 @@ module BoltServer
689
756
  raise BoltServer::RequestError, "'versioned_project' is a required argument" if params['versioned_project'].nil?
690
757
  content_type :json
691
758
 
692
- # Inspired by Bolt::Applicator.build_plugin_tarball
693
- start_time = Time.now
694
-
695
- # Fetch the plugin files
696
- plugin_files = in_bolt_project(params['versioned_project']) do |context|
697
- files = {}
698
-
699
- # Bolt also sets plugin_modulepath to user modulepath so do it here too for
700
- # consistency
701
- plugin_modulepath = context[:pal].user_modulepath
702
- Puppet.lookup(:current_environment).override_with(modulepath: plugin_modulepath).modules.each do |mod|
703
- search_dirs = []
704
- search_dirs << mod.plugins if mod.plugins?
705
- search_dirs << mod.pluginfacts if mod.pluginfacts?
706
-
707
- files[mod] ||= []
708
- Find.find(*search_dirs).each do |file|
709
- files[mod] << file if File.file?(file)
710
- end
711
- end
712
-
713
- files
759
+ plugins_tarball = build_project_plugins_tarball(params['versioned_project']) do |mod|
760
+ search_dirs = []
761
+ search_dirs << mod.plugins if mod.plugins?
762
+ search_dirs << mod.pluginfacts if mod.pluginfacts?
763
+ search_dirs
714
764
  end
715
765
 
716
- # Pack the plugin files
717
- sio = StringIO.new
718
- begin
719
- output = Minitar::Output.new(Zlib::GzipWriter.new(sio))
720
-
721
- plugin_files.each do |mod, files|
722
- tar_dir = Pathname.new(mod.name)
723
- mod_dir = Pathname.new(mod.path)
766
+ [200, plugins_tarball.to_json]
767
+ end
724
768
 
725
- files.each do |file|
726
- tar_path = tar_dir + Pathname.new(file).relative_path_from(mod_dir)
727
- stat = File.stat(file)
728
- content = File.binread(file)
729
- output.tar.add_file_simple(
730
- tar_path.to_s,
731
- data: content,
732
- size: content.size,
733
- mode: stat.mode & 0o777,
734
- mtime: stat.mtime
735
- )
736
- end
737
- end
769
+ # Returns the base64 encoded tar archive of _all_ plugin code for a project
770
+ #
771
+ # @param versioned_project [String] the versioned_project to build the plugin tarball from
772
+ get '/project_plugin_tarball' do
773
+ raise BoltServer::RequestError, "'versioned_project' is a required argument" if params['versioned_project'].nil?
774
+ content_type :json
738
775
 
739
- duration = Time.now - start_time
740
- @logger.trace("Packed plugins in #{duration * 1000} ms")
741
- ensure
742
- output.close
776
+ plugins_tarball = build_project_plugins_tarball(params['versioned_project']) do |mod|
777
+ search_dirs = []
778
+ search_dirs << mod.plugins if mod.plugins?
779
+ search_dirs << mod.pluginfacts if mod.pluginfacts?
780
+ search_dirs << mod.files if mod.files?
781
+ type_files = "#{mod.path}/types"
782
+ search_dirs << type_files if File.exist?(type_files)
783
+ search_dirs
743
784
  end
744
785
 
745
- [200, Base64.encode64(sio.string).to_json]
786
+ [200, plugins_tarball.to_json]
746
787
  end
747
788
 
748
789
  error 404 do
@@ -191,6 +191,15 @@ module BoltSpec
191
191
  allow_out_message.expect_call
192
192
  end
193
193
 
194
+ def allow_out_verbose
195
+ executor.stub_out_verbose.add_stub
196
+ end
197
+ alias allow_any_out_verbose allow_out_verbose
198
+
199
+ def expect_out_verbose
200
+ allow_out_verbose.expect_call
201
+ end
202
+
194
203
  # Example helpers to mock other run functions
195
204
  # The with_targets method makes sense for all stubs
196
205
  # with_params could be reused for options
@@ -98,7 +98,7 @@ module BoltSpec
98
98
  Puppet[:tasks] = true
99
99
 
100
100
  # Ensure logger is initialized with Puppet levels so 'notice' works when running plan specs.
101
- Logging.init :trace, :debug, :info, :notice, :warn, :error, :fatal, :any
101
+ Logging.init :trace, :debug, :info, :notice, :warn, :error, :fatal
102
102
  end
103
103
 
104
104
  # Provided as a class so expectations can be placed on it.
@@ -29,6 +29,7 @@ module BoltSpec
29
29
  @modulepath = [modulepath].flatten.map { |path| File.absolute_path(path) }
30
30
  MOCKED_ACTIONS.each { |action| instance_variable_set(:"@#{action}_doubles", {}) }
31
31
  @stub_out_message = nil
32
+ @stub_out_verbose = nil
32
33
  @transport_features = ['puppet-agent']
33
34
  @executor_real = Bolt::Executor.new
34
35
  # by default, we want to execute any plan that we come across without error
@@ -38,6 +39,7 @@ module BoltSpec
38
39
  # plans that are allowed to be executed by the @executor_real
39
40
  @allowed_exec_plans = {}
40
41
  @id = 0
42
+ @plan_futures = []
41
43
  end
42
44
 
43
45
  def module_file_id(file)
@@ -187,6 +189,7 @@ module BoltSpec
187
189
  end
188
190
  end
189
191
  @stub_out_message.assert_called('out::message') if @stub_out_message
192
+ @stub_out_verbose.assert_called('out::verbose') if @stub_out_verbose
190
193
  end
191
194
 
192
195
  MOCKED_ACTIONS.each do |action|
@@ -199,6 +202,10 @@ module BoltSpec
199
202
  @stub_out_message ||= ActionDouble.new(:PublishStub)
200
203
  end
201
204
 
205
+ def stub_out_verbose
206
+ @stub_out_verbose ||= ActionDouble.new(:PublishStub)
207
+ end
208
+
202
209
  def stub_apply
203
210
  @allow_apply = true
204
211
  end
@@ -220,12 +227,20 @@ module BoltSpec
220
227
  end
221
228
 
222
229
  def publish_event(event)
223
- if event[:type] == :message
230
+ case event[:type]
231
+ when :message
224
232
  unless @stub_out_message
225
233
  @error_message = "Unexpected call to 'out::message(#{event[:message]})'"
226
234
  raise UnexpectedInvocation, @error_message
227
235
  end
228
236
  @stub_out_message.process(event[:message])
237
+
238
+ when :verbose
239
+ unless @stub_out_verbose
240
+ @error_message = "Unexpected call to 'out::verbose(#{event[:message]})'"
241
+ raise UnexpectedInvocation, @error_message
242
+ end
243
+ @stub_out_verbose.process(event[:message])
229
244
  end
230
245
  end
231
246
 
@@ -266,7 +281,7 @@ module BoltSpec
266
281
  false
267
282
  end
268
283
 
269
- def create_future(scope: nil, name: nil)
284
+ def create_future(plan_id:, scope: nil, name: nil)
270
285
  newscope = nil
271
286
  if scope
272
287
  # Create the new scope
@@ -281,13 +296,18 @@ module BoltSpec
281
296
  # Execute "futures" serially when running in BoltSpec
282
297
  result = yield newscope
283
298
  @id += 1
284
- future = Bolt::PlanFuture.new(nil, @id, name: name)
299
+ future = Bolt::PlanFuture.new(nil, @id, name: name, plan_id: plan_id)
285
300
  future.value = result
301
+ @plan_futures << future
286
302
  future
287
303
  end
288
304
 
289
- def wait(results, **_kwargs)
290
- results
305
+ def get_futures_for_plan(plan_id:)
306
+ @plan_futures.select { |future| future.plan_id == plan_id }
307
+ end
308
+
309
+ def wait(futures, **_kwargs)
310
+ futures.map(&:value)
291
311
  end
292
312
 
293
313
  # Since Futures are executed immediately once created, this will always
@@ -296,8 +316,12 @@ module BoltSpec
296
316
  true
297
317
  end
298
318
 
299
- def plan_futures
300
- []
319
+ def get_current_future(fiber)
320
+ @plan_futures.select { |f| f.fiber == fiber }&.first
321
+ end
322
+
323
+ def get_current_plan_id(fiber)
324
+ get_current_future(fiber)&.current_plan
301
325
  end
302
326
 
303
327
  # Public methods on Bolt::Executor that need to be mocked so there aren't