bolt 3.12.0 → 3.16.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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +1 -1
  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 +3 -20
  26. data/lib/bolt/application.rb +620 -0
  27. data/lib/bolt/bolt_option_parser.rb +18 -6
  28. data/lib/bolt/cli.rb +592 -772
  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/executor.rb +12 -4
  33. data/lib/bolt/fiber_executor.rb +63 -14
  34. data/lib/bolt/outputter/human.rb +201 -43
  35. data/lib/bolt/outputter/json.rb +68 -43
  36. data/lib/bolt/outputter/logger.rb +6 -0
  37. data/lib/bolt/pal.rb +67 -14
  38. data/lib/bolt/pal/yaml_plan/step.rb +2 -0
  39. data/lib/bolt/pal/yaml_plan/step/message.rb +0 -8
  40. data/lib/bolt/pal/yaml_plan/step/verbose.rb +31 -0
  41. data/lib/bolt/pal/yaml_plan/transpiler.rb +1 -1
  42. data/lib/bolt/plan_creator.rb +2 -20
  43. data/lib/bolt/plan_future.rb +23 -3
  44. data/lib/bolt/plan_result.rb +1 -1
  45. data/lib/bolt/plugin/task.rb +1 -1
  46. data/lib/bolt/project.rb +0 -7
  47. data/lib/bolt/result_set.rb +2 -1
  48. data/lib/bolt/transport/local/connection.rb +17 -1
  49. data/lib/bolt/transport/orch/connection.rb +13 -1
  50. data/lib/bolt/transport/ssh/exec_connection.rb +3 -1
  51. data/lib/bolt/util/format.rb +68 -0
  52. data/lib/bolt/version.rb +1 -1
  53. data/lib/bolt_server/schemas/partials/target-ssh.json +4 -0
  54. data/lib/bolt_server/schemas/partials/target-winrm.json +4 -0
  55. data/lib/bolt_server/transport_app.rb +92 -50
  56. data/lib/bolt_spec/bolt_context.rb +9 -0
  57. data/lib/bolt_spec/plans.rb +1 -1
  58. data/lib/bolt_spec/plans/mock_executor.rb +31 -7
  59. data/lib/bolt_spec/plans/publish_stub.rb +4 -4
  60. data/resources/bolt_bash_completion.sh +1 -1
  61. metadata +29 -15
  62. data/guides/guide.txt +0 -17
  63. data/lib/bolt/secret.rb +0 -37
@@ -6,7 +6,7 @@ require 'bolt/util'
6
6
 
7
7
  module Bolt
8
8
  class PlanResult
9
- attr_accessor :value, :status
9
+ attr_accessor :status, :value
10
10
 
11
11
  # This must be called from inside a compiler
12
12
  def self.from_pcore(result, status)
@@ -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
data/lib/bolt/project.rb CHANGED
@@ -209,12 +209,5 @@ module Bolt
209
209
  Bolt::Logger.warn("missing_project_name", message)
210
210
  end
211
211
  end
212
-
213
- def check_deprecated_file
214
- if (@path + 'project.yaml').file?
215
- msg = "Project configuration file 'project.yaml' is deprecated; use 'bolt-project.yaml' instead."
216
- Bolt::Logger.warn("project_yaml", msg)
217
- end
218
- end
219
212
  end
220
213
  end
@@ -2,7 +2,8 @@
2
2
 
3
3
  module Bolt
4
4
  class ResultSet
5
- attr_reader :results
5
+ attr_accessor :elapsed_time
6
+ attr_reader :results
6
7
 
7
8
  include Enumerable
8
9
 
@@ -10,6 +10,8 @@ module Bolt
10
10
  module Transport
11
11
  class Local < Simple
12
12
  class Connection
13
+ RUBY_ENV_VARS = %w[GEM_PATH GEM_HOME RUBYLIB RUBYLIB_PREFIX RUBYOPT RUBYPATH RUBYSHELL].freeze
14
+
13
15
  attr_accessor :user, :logger, :target
14
16
 
15
17
  def initialize(target)
@@ -68,7 +70,21 @@ module Bolt
68
70
  end
69
71
  end
70
72
 
71
- Open3.popen3(*command)
73
+ # Only do this if bundled-ruby is set to false, not nil
74
+ ruby_env_vars = if target.transport_config['bundled-ruby'] == false
75
+ RUBY_ENV_VARS.each_with_object({}) do |e, acc|
76
+ acc[e] = ENV["BOLT_ORIG_#{e}"] if ENV["BOLT_ORIG_#{e}"]
77
+ end
78
+ end
79
+
80
+ if target.transport_config['bundled-ruby'] == false &&
81
+ Gem.loaded_specs.keys.include?('bundler')
82
+ Bundler.with_unbundled_env do
83
+ Open3.popen3(ruby_env_vars || {}, *command)
84
+ end
85
+ else
86
+ Open3.popen3(ruby_env_vars || {}, *command)
87
+ end
72
88
  end
73
89
 
74
90
  # This is used by the Bash shell to decide whether to `cd` before
@@ -36,7 +36,8 @@ module Bolt
36
36
  end
37
37
  logger.debug("Creating orchestrator client for #{client_opts}")
38
38
  @client = OrchestratorClient.new(client_opts, true)
39
- @plan_job = start_plan(plan_context)
39
+ @plan_context = plan_context
40
+ @plan_job = start_plan(@plan_context)
40
41
  logger.debug("Started plan #{@plan_job}")
41
42
  @environment = opts["task-environment"]
42
43
  end
@@ -87,6 +88,17 @@ module Bolt
87
88
  def run_task(targets, task, arguments, options)
88
89
  body = build_request(targets, task, arguments, options[:description])
89
90
  @client.run_task(body)
91
+ rescue OrchestratorClient::ApiError => e
92
+ if e.data['kind'] == 'puppetlabs.orchestrator/plan-already-finished'
93
+ @logger.debug("Retrying the task")
94
+ # Instead of recursing, just retry once
95
+ @plan_job = start_plan(@plan_context)
96
+ # Rebuild the request with the new plan job ID
97
+ body = build_request(targets, task, arguments, options[:description])
98
+ @client.run_task(body)
99
+ else
100
+ raise e
101
+ end
90
102
  end
91
103
 
92
104
  def query_inventory(targets)
@@ -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.12.0'
4
+ VERSION = '3.16.0'
5
5
  end
@@ -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
 
@@ -688,60 +756,34 @@ module BoltServer
688
756
  raise BoltServer::RequestError, "'versioned_project' is a required argument" if params['versioned_project'].nil?
689
757
  content_type :json
690
758
 
691
- # Inspired by Bolt::Applicator.build_plugin_tarball
692
- start_time = Time.now
693
-
694
- # Fetch the plugin files
695
- plugin_files = in_bolt_project(params['versioned_project']) do |context|
696
- files = {}
697
-
698
- # Bolt also sets plugin_modulepath to user modulepath so do it here too for
699
- # consistency
700
- plugin_modulepath = context[:pal].user_modulepath
701
- Puppet.lookup(:current_environment).override_with(modulepath: plugin_modulepath).modules.each do |mod|
702
- search_dirs = []
703
- search_dirs << mod.plugins if mod.plugins?
704
- search_dirs << mod.pluginfacts if mod.pluginfacts?
705
-
706
- files[mod] ||= []
707
- Find.find(*search_dirs).each do |file|
708
- files[mod] << file if File.file?(file)
709
- end
710
- end
711
-
712
- 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
713
764
  end
714
765
 
715
- # Pack the plugin files
716
- sio = StringIO.new
717
- begin
718
- output = Minitar::Output.new(Zlib::GzipWriter.new(sio))
719
-
720
- plugin_files.each do |mod, files|
721
- tar_dir = Pathname.new(mod.name)
722
- mod_dir = Pathname.new(mod.path)
766
+ [200, plugins_tarball.to_json]
767
+ end
723
768
 
724
- files.each do |file|
725
- tar_path = tar_dir + Pathname.new(file).relative_path_from(mod_dir)
726
- stat = File.stat(file)
727
- content = File.binread(file)
728
- output.tar.add_file_simple(
729
- tar_path.to_s,
730
- data: content,
731
- size: content.size,
732
- mode: stat.mode & 0o777,
733
- mtime: stat.mtime
734
- )
735
- end
736
- 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
737
775
 
738
- duration = Time.now - start_time
739
- @logger.trace("Packed plugins in #{duration * 1000} ms")
740
- ensure
741
- 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
742
784
  end
743
785
 
744
- [200, Base64.encode64(sio.string).to_json]
786
+ [200, plugins_tarball.to_json]
745
787
  end
746
788
 
747
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