bolt 3.6.1 → 3.9.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 (67) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +3 -3
  3. data/bolt-modules/boltlib/lib/puppet/datatypes/applyresult.rb +26 -0
  4. data/bolt-modules/boltlib/lib/puppet/datatypes/containerresult.rb +27 -0
  5. data/bolt-modules/boltlib/lib/puppet/datatypes/future.rb +25 -0
  6. data/bolt-modules/boltlib/lib/puppet/datatypes/resourceinstance.rb +43 -0
  7. data/bolt-modules/boltlib/lib/puppet/datatypes/result.rb +29 -0
  8. data/bolt-modules/boltlib/lib/puppet/datatypes/resultset.rb +34 -0
  9. data/bolt-modules/boltlib/lib/puppet/datatypes/target.rb +55 -0
  10. data/bolt-modules/boltlib/lib/puppet/functions/add_to_group.rb +1 -0
  11. data/bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb +10 -6
  12. data/bolt-modules/boltlib/lib/puppet/functions/background.rb +61 -0
  13. data/bolt-modules/boltlib/lib/puppet/functions/download_file.rb +5 -9
  14. data/bolt-modules/boltlib/lib/puppet/functions/parallelize.rb +29 -13
  15. data/bolt-modules/boltlib/lib/puppet/functions/puppetdb_command.rb +66 -0
  16. data/bolt-modules/boltlib/lib/puppet/functions/remove_from_group.rb +1 -0
  17. data/bolt-modules/boltlib/lib/puppet/functions/run_command.rb +5 -15
  18. data/bolt-modules/boltlib/lib/puppet/functions/run_script.rb +10 -18
  19. data/bolt-modules/boltlib/lib/puppet/functions/run_task.rb +5 -17
  20. data/bolt-modules/boltlib/lib/puppet/functions/run_task_with.rb +5 -15
  21. data/bolt-modules/boltlib/lib/puppet/functions/upload_file.rb +10 -18
  22. data/bolt-modules/boltlib/lib/puppet/functions/wait.rb +91 -0
  23. data/bolt-modules/boltlib/lib/puppet/functions/write_file.rb +1 -0
  24. data/bolt-modules/boltlib/types/planresult.pp +1 -0
  25. data/bolt-modules/ctrl/lib/puppet/functions/ctrl/do_until.rb +2 -0
  26. data/bolt-modules/file/lib/puppet/functions/file/exists.rb +9 -3
  27. data/bolt-modules/file/lib/puppet/functions/file/read.rb +6 -2
  28. data/bolt-modules/file/lib/puppet/functions/file/readable.rb +8 -3
  29. data/guides/guide.txt +17 -0
  30. data/guides/inventory.txt +5 -0
  31. data/guides/links.txt +13 -0
  32. data/guides/targets.txt +29 -0
  33. data/guides/transports.txt +23 -0
  34. data/lib/bolt/applicator.rb +4 -3
  35. data/lib/bolt/bolt_option_parser.rb +353 -227
  36. data/lib/bolt/catalog.rb +2 -1
  37. data/lib/bolt/cli.rb +94 -36
  38. data/lib/bolt/config/options.rb +2 -1
  39. data/lib/bolt/config/transport/docker.rb +5 -1
  40. data/lib/bolt/config/transport/lxd.rb +1 -1
  41. data/lib/bolt/config/transport/options.rb +2 -1
  42. data/lib/bolt/config/transport/podman.rb +5 -1
  43. data/lib/bolt/error.rb +11 -1
  44. data/lib/bolt/executor.rb +51 -72
  45. data/lib/bolt/fiber_executor.rb +141 -0
  46. data/lib/bolt/inventory.rb +5 -4
  47. data/lib/bolt/inventory/inventory.rb +3 -2
  48. data/lib/bolt/logger.rb +1 -1
  49. data/lib/bolt/module_installer/specs.rb +1 -1
  50. data/lib/bolt/module_installer/specs/git_spec.rb +10 -6
  51. data/lib/bolt/outputter/human.rb +59 -29
  52. data/lib/bolt/outputter/json.rb +8 -4
  53. data/lib/bolt/pal.rb +64 -3
  54. data/lib/bolt/pal/yaml_plan/step.rb +4 -2
  55. data/lib/bolt/plan_creator.rb +2 -2
  56. data/lib/bolt/plan_future.rb +66 -0
  57. data/lib/bolt/puppetdb/client.rb +54 -0
  58. data/lib/bolt/result.rb +5 -0
  59. data/lib/bolt/transport/docker/connection.rb +7 -4
  60. data/lib/bolt/transport/lxd/connection.rb +4 -0
  61. data/lib/bolt/transport/podman/connection.rb +4 -0
  62. data/lib/bolt/transport/ssh/connection.rb +3 -6
  63. data/lib/bolt/util.rb +73 -1
  64. data/lib/bolt/version.rb +1 -1
  65. data/lib/bolt_spec/plans/mock_executor.rb +42 -45
  66. metadata +12 -3
  67. data/lib/bolt/yarn.rb +0 -23
data/lib/bolt/catalog.rb CHANGED
@@ -65,7 +65,8 @@ module Bolt
65
65
  puppet_overrides = {
66
66
  bolt_pdb_client: pdb_client,
67
67
  bolt_inventory: inv,
68
- bolt_project: bolt_project
68
+ bolt_project: bolt_project,
69
+ future: request['future']
69
70
  }
70
71
 
71
72
  # Facts will be set by the catalog compiler, so we need to ensure
data/lib/bolt/cli.rb CHANGED
@@ -33,20 +33,23 @@ module Bolt
33
33
 
34
34
  class CLI
35
35
  COMMANDS = {
36
- 'command' => %w[run],
37
- 'script' => %w[run],
38
- 'task' => %w[show run],
39
- 'plan' => %w[show run convert new],
40
- 'file' => %w[download upload],
41
- 'secret' => %w[encrypt decrypt createkeys],
36
+ 'apply' => %w[],
37
+ 'command' => %w[run],
38
+ 'file' => %w[download upload],
39
+ 'group' => %w[show],
40
+ 'guide' => %w[],
42
41
  'inventory' => %w[show],
43
- 'group' => %w[show],
44
- 'project' => %w[init migrate],
45
- 'module' => %w[add generate-types install show],
46
- 'apply' => %w[],
47
- 'guide' => %w[]
42
+ 'lookup' => %w[],
43
+ 'module' => %w[add generate-types install show],
44
+ 'plan' => %w[show run convert new],
45
+ 'project' => %w[init migrate],
46
+ 'script' => %w[run],
47
+ 'secret' => %w[encrypt decrypt createkeys],
48
+ 'task' => %w[show run]
48
49
  }.freeze
49
50
 
51
+ TARGETING_OPTIONS = %i[query rerun targets].freeze
52
+
50
53
  attr_reader :config, :options
51
54
 
52
55
  def initialize(argv)
@@ -150,7 +153,6 @@ module Bolt
150
153
  options[:subcommand] = nil unless COMMANDS.include?(options[:subcommand])
151
154
 
152
155
  if Bolt::Util.first_run?
153
- FileUtils.mkdir_p(Bolt::Util.first_runs_free.dirname)
154
156
  FileUtils.touch(Bolt::Util.first_runs_free)
155
157
 
156
158
  if options[:subcommand].nil? && $stdout.isatty
@@ -262,7 +264,7 @@ module Bolt
262
264
  end
263
265
 
264
266
  def update_targets(options)
265
- target_opts = options.keys.select { |opt| %i[query rerun targets].include?(opt) }
267
+ target_opts = options.keys.select { |opt| TARGETING_OPTIONS.include?(opt) }
266
268
  target_string = "'--targets', '--rerun', or '--query'"
267
269
  if target_opts.length > 1
268
270
  raise Bolt::CLIError, "Only one targeting option #{target_string} can be specified"
@@ -324,6 +326,10 @@ module Bolt
324
326
  raise Bolt::CLIError, "a manifest file or --execute is required"
325
327
  end
326
328
 
329
+ if options[:subcommand] == 'lookup' && !options[:object]
330
+ raise Bolt::CLIError, "Must specify a key to look up"
331
+ end
332
+
327
333
  if options[:subcommand] == 'command' && (!options[:object] || options[:object].empty?)
328
334
  raise Bolt::CLIError, "Must specify a command to run"
329
335
  end
@@ -509,6 +515,8 @@ module Bolt
509
515
  when 'migrate'
510
516
  code = Bolt::ProjectManager.new(config, outputter, pal).migrate
511
517
  end
518
+ when 'lookup'
519
+ code = lookup(options[:object], options[:targets])
512
520
  when 'plan'
513
521
  case options[:action]
514
522
  when 'new'
@@ -561,7 +569,8 @@ module Bolt
561
569
  when 'command'
562
570
  executor.run_command(targets, options[:object], executor_opts)
563
571
  when 'script'
564
- script_path = find_file(options[:object])
572
+ script_path = find_file(options[:object], executor.future&.fetch('file_paths', false))
573
+ validate_file('script', script_path)
565
574
  executor.run_script(targets, script_path, options[:leftovers], executor_opts)
566
575
  when 'task'
567
576
  pal.run_task(options[:object],
@@ -586,8 +595,9 @@ module Bolt
586
595
  dest = File.expand_path(dest, Dir.pwd)
587
596
  executor.download_file(targets, src, dest, executor_opts)
588
597
  when 'upload'
589
- validate_file('source file', src, true)
590
- executor.upload_file(targets, src, dest, executor_opts)
598
+ src_path = find_file(src, executor.future&.fetch('file_paths', false))
599
+ validate_file('source file', src_path, true)
600
+ executor.upload_file(targets, src_path, dest, executor_opts)
591
601
  end
592
602
  end
593
603
  end
@@ -634,13 +644,33 @@ module Bolt
634
644
  end
635
645
 
636
646
  def list_targets
637
- inventoryfile = config.inventoryfile || config.default_inventoryfile
638
- outputter.print_targets(group_targets_by_source, inventoryfile)
647
+ if options.keys.any? { |key| TARGETING_OPTIONS.include?(key) }
648
+ target_flag = true
649
+ else
650
+ options[:targets] = 'all'
651
+ end
652
+
653
+ outputter.print_targets(
654
+ group_targets_by_source,
655
+ inventory.source,
656
+ config.default_inventoryfile,
657
+ target_flag
658
+ )
639
659
  end
640
660
 
641
661
  def show_targets
642
- inventoryfile = config.inventoryfile || config.default_inventoryfile
643
- outputter.print_target_info(group_targets_by_source, inventoryfile)
662
+ if options.keys.any? { |key| TARGETING_OPTIONS.include?(key) }
663
+ target_flag = true
664
+ else
665
+ options[:targets] = 'all'
666
+ end
667
+
668
+ outputter.print_target_info(
669
+ group_targets_by_source,
670
+ inventory.source,
671
+ config.default_inventoryfile,
672
+ target_flag
673
+ )
644
674
  end
645
675
 
646
676
  # Returns a hash of targets sorted by those that are found in the
@@ -661,8 +691,39 @@ module Bolt
661
691
  end
662
692
 
663
693
  def list_groups
664
- groups = inventory.group_names
665
- outputter.print_groups(groups)
694
+ outputter.print_groups(inventory.group_names.sort, inventory.source, config.default_inventoryfile)
695
+ end
696
+
697
+ # Looks up a value with Hiera, using targets as the contexts to perform the
698
+ # look ups in.
699
+ #
700
+ def lookup(key, targets)
701
+ executor = Bolt::Executor.new(
702
+ config.concurrency,
703
+ analytics,
704
+ options[:noop],
705
+ config.modified_concurrency,
706
+ config.future
707
+ )
708
+
709
+ executor.subscribe(outputter) if options.fetch(:format, 'human') == 'human'
710
+ executor.subscribe(log_outputter)
711
+ executor.publish_event(type: :plan_start, plan: nil)
712
+
713
+ results = outputter.spin do
714
+ pal.lookup(
715
+ key,
716
+ targets,
717
+ inventory,
718
+ executor,
719
+ config.concurrency
720
+ )
721
+ end
722
+
723
+ executor.shutdown
724
+ outputter.print_result_set(results)
725
+
726
+ results.ok ? 0 : 1
666
727
  end
667
728
 
668
729
  def run_plan(plan_name, plan_arguments, nodes, options)
@@ -871,7 +932,7 @@ module Bolt
871
932
 
872
933
  # Display the list of available Bolt guides.
873
934
  def list_topics
874
- outputter.print_topics(guides.keys)
935
+ outputter.print_topics(guides.keys - ['guide'])
875
936
  0
876
937
  end
877
938
 
@@ -908,20 +969,17 @@ module Bolt
908
969
  # the path is a Puppet file path and looks for the file in a module's files
909
970
  # directory.
910
971
  #
911
- def find_file(path)
912
- unless File.exist?(path) || Pathname.new(path).absolute?
913
- modulepath = Bolt::Config::Modulepath.new(config.modulepath)
914
- modules = Bolt::Module.discover(modulepath.full_modulepath, config.project)
915
- mod, file = path.split(File::SEPARATOR, 2)
916
-
917
- if modules[mod]
918
- @logger.debug("Did not find file at #{File.expand_path(path)}, checking in module '#{mod}'")
919
- path = File.join(modules[mod].path, 'files', file)
920
- end
972
+ def find_file(path, future_file_paths)
973
+ return path if File.exist?(path) || Pathname.new(path).absolute?
974
+ modulepath = Bolt::Config::Modulepath.new(config.modulepath)
975
+ modules = Bolt::Module.discover(modulepath.full_modulepath, config.project)
976
+ mod, file = path.split(File::SEPARATOR, 2)
977
+
978
+ if modules[mod]
979
+ @logger.debug("Did not find file at #{File.expand_path(path)}, checking in module '#{mod}'")
980
+ found = Bolt::Util.find_file_in_module(modules[mod].path, file || "", future_file_paths)
981
+ path = found.nil? ? File.join(modules[mod].path, 'files', file) : found
921
982
  end
922
-
923
- Bolt::Util.validate_file('script', path)
924
-
925
983
  path
926
984
  end
927
985
 
@@ -279,7 +279,8 @@ module Bolt
279
279
  "modules" => {
280
280
  description: "A list of module dependencies for the project. Each dependency is a map of data specifying "\
281
281
  "the module to install. To install the project's module dependencies, run the `bolt module "\
282
- "install` command.",
282
+ "install` command. For more information about specifying modules, see [the "\
283
+ "documentation](https://pup.pt/bolt-module-specs).",
283
284
  type: Array,
284
285
  items: {
285
286
  type: [Hash, String],
@@ -15,7 +15,7 @@ module Bolt
15
15
  shell-command
16
16
  tmpdir
17
17
  tty
18
- ].freeze
18
+ ].concat(RUN_AS_OPTIONS).sort.freeze
19
19
 
20
20
  DEFAULTS = {
21
21
  'cleanup' => true
@@ -27,6 +27,10 @@ module Bolt
27
27
  if @config['interpreters']
28
28
  @config['interpreters'] = normalize_interpreters(@config['interpreters'])
29
29
  end
30
+
31
+ if Bolt::Util.windows? && @config['run-as']
32
+ raise Bolt::ValidationError, "run-as is not supported when using PowerShell"
33
+ end
30
34
  end
31
35
  end
32
36
  end
@@ -11,7 +11,7 @@ module Bolt
11
11
  cleanup
12
12
  remote
13
13
  tmpdir
14
- ].freeze
14
+ ].concat(RUN_AS_OPTIONS).sort.freeze
15
15
 
16
16
  DEFAULTS = {
17
17
  'cleanup' => true,
@@ -235,7 +235,8 @@ module Bolt
235
235
  "private-key" => {
236
236
  type: [Hash, String],
237
237
  description: "Either the path to the private key file to use for authentication, or "\
238
- "a hash with the key `key-data` and the contents of the private key.",
238
+ "a hash with the key `key-data` and the contents of the private key. Note that "\
239
+ "the key cannot be encrypted if using the `key-data` hash.",
239
240
  required: ["key-data"],
240
241
  properties: {
241
242
  "key-data" => {
@@ -14,7 +14,7 @@ module Bolt
14
14
  shell-command
15
15
  tmpdir
16
16
  tty
17
- ].freeze
17
+ ].concat(RUN_AS_OPTIONS).sort.freeze
18
18
 
19
19
  DEFAULTS = {
20
20
  'cleanup' => true
@@ -26,6 +26,10 @@ module Bolt
26
26
  if @config['interpreters']
27
27
  @config['interpreters'] = normalize_interpreters(@config['interpreters'])
28
28
  end
29
+
30
+ if Bolt::Util.windows? && @config['run-as']
31
+ raise Bolt::ValidationError, "run-as is not supported when using PowerShell"
32
+ end
29
33
  end
30
34
  end
31
35
  end
data/lib/bolt/error.rb CHANGED
@@ -105,6 +105,16 @@ module Bolt
105
105
  end
106
106
  end
107
107
 
108
+ class FutureTimeoutError < Bolt::Error
109
+ def initialize(name, timeout)
110
+ details = {
111
+ 'future' => name
112
+ }
113
+ message = "Future '#{name}' timed out after #{timeout} seconds."
114
+ super(message, 'bolt/future-timeout-error', details)
115
+ end
116
+ end
117
+
108
118
  class ParallelFailure < Bolt::Error
109
119
  def initialize(results, failed_indices)
110
120
  details = {
@@ -162,7 +172,7 @@ module Bolt
162
172
 
163
173
  class InvalidParallelResult < Error
164
174
  def initialize(result_str, file, line)
165
- super("Parallel block returned an invalid result: #{result_str}",
175
+ super("Background block returned an invalid result: #{result_str}",
166
176
  'bolt/invalid-plan-result',
167
177
  { 'file' => file,
168
178
  'line' => line,
data/lib/bolt/executor.rb CHANGED
@@ -7,10 +7,11 @@ require 'logging'
7
7
  require 'pathname'
8
8
  require 'set'
9
9
  require 'bolt/analytics'
10
- require 'bolt/result'
11
10
  require 'bolt/config'
12
- require 'bolt/result_set'
11
+ require 'bolt/fiber_executor'
13
12
  require 'bolt/puppetdb'
13
+ require 'bolt/result'
14
+ require 'bolt/result_set'
14
15
  # Load transports
15
16
  require 'bolt/transport/docker'
16
17
  require 'bolt/transport/local'
@@ -20,7 +21,6 @@ require 'bolt/transport/podman'
20
21
  require 'bolt/transport/remote'
21
22
  require 'bolt/transport/ssh'
22
23
  require 'bolt/transport/winrm'
23
- require 'bolt/yarn'
24
24
 
25
25
  module Bolt
26
26
  TRANSPORTS = {
@@ -35,7 +35,7 @@ module Bolt
35
35
  }.freeze
36
36
 
37
37
  class Executor
38
- attr_reader :noop, :transports, :in_parallel, :future
38
+ attr_reader :noop, :transports, :future
39
39
  attr_accessor :run_as
40
40
 
41
41
  def initialize(concurrency = 1,
@@ -66,7 +66,6 @@ module Bolt
66
66
 
67
67
  @noop = noop
68
68
  @run_as = nil
69
- @in_parallel = false
70
69
  @future = future
71
70
  @pool = if concurrency > 0
72
71
  Concurrent::ThreadPoolExecutor.new(name: 'exec', max_threads: concurrency)
@@ -77,6 +76,7 @@ module Bolt
77
76
 
78
77
  @concurrency = concurrency
79
78
  @warn_concurrency = modified_concurrency
79
+ @fiber_executor = Bolt::FiberExecutor.new
80
80
  end
81
81
 
82
82
  def transport(transport)
@@ -373,83 +373,62 @@ module Bolt
373
373
  plan.call_by_name_with_scope(scope, params, true)
374
374
  end
375
375
 
376
- def create_yarn(scope, block, object, index)
377
- fiber = Fiber.new do
378
- # Create the new scope
379
- newscope = Puppet::Parser::Scope.new(scope.compiler)
380
- local = Puppet::Parser::Scope::LocalScope.new
381
-
382
- # Compress the current scopes into a single vars hash to add to the new scope
383
- current_scope = scope.effective_symtable(true)
384
- until current_scope.nil?
385
- current_scope.instance_variable_get(:@symbols)&.each_pair { |k, v| local[k] = v }
386
- current_scope = current_scope.parent
387
- end
388
- newscope.push_ephemerals([local])
389
-
390
- begin
391
- result = catch(:return) do
392
- args = { block.parameters[0][1].to_s => object }
393
- block.closure.call_by_name_with_scope(newscope, args, true)
394
- end
395
-
396
- # If we got a return from the block, get it's value
397
- # Otherwise the result is the last line from the block
398
- result = result.value if result.is_a?(Puppet::Pops::Evaluator::Return)
376
+ # Call into FiberExecutor to avoid this class getting
377
+ # overloaded while also minimizing the Puppet lookups needed from plan
378
+ # functions
379
+ #
380
+ def create_future(scope: nil, name: nil, &block)
381
+ @fiber_executor.create_future(scope: scope, name: name, &block)
382
+ end
399
383
 
400
- # Validate the result is a PlanResult
401
- unless Puppet::Pops::Types::TypeParser.singleton.parse('Boltlib::PlanResult').instance?(result)
402
- raise Bolt::InvalidParallelResult.new(result.to_s, *Puppet::Pops::PuppetStack.top_of_stack)
403
- end
384
+ def plan_complete?
385
+ @fiber_executor.plan_complete?
386
+ end
404
387
 
405
- result
406
- rescue Puppet::PreformattedError => e
407
- if e.cause.is_a?(Bolt::Error)
408
- e.cause
409
- else
410
- raise e
411
- end
412
- end
413
- end
388
+ def round_robin
389
+ @fiber_executor.round_robin
390
+ end
414
391
 
415
- Bolt::Yarn.new(fiber, index)
392
+ def in_parallel?
393
+ @fiber_executor.in_parallel?
416
394
  end
417
395
 
418
- def handle_event(event)
419
- case event[:type]
420
- when :node_result
421
- @thread_completed = true
422
- end
396
+ def wait(futures, **opts)
397
+ @fiber_executor.wait(futures, **opts)
423
398
  end
424
399
 
425
- def round_robin(skein)
426
- subscribe(self, [:node_result])
427
- results = Array.new(skein.length)
428
- @in_parallel = true
429
- publish_event(type: :stop_spin)
400
+ def plan_futures
401
+ @fiber_executor.plan_futures
402
+ end
430
403
 
431
- until skein.empty?
432
- @thread_completed = false
433
- r = nil
434
-
435
- skein.each do |yarn|
436
- if yarn.alive?
437
- publish_event(type: :stop_spin)
438
- r = yarn.resume
439
- else
440
- results[yarn.index] = yarn.value
441
- skein.delete(yarn)
442
- end
443
- end
404
+ # Execute a plan function concurrently. This function accepts the executor
405
+ # function to be run and the parameters to pass to it, and returns the
406
+ # result of running the executor function.
407
+ #
408
+ def run_in_thread
409
+ require 'concurrent'
410
+ require 'fiber'
411
+ future = Concurrent::Future.execute do
412
+ yield
413
+ end
444
414
 
445
- next unless r == 'unfinished'
446
- sleep(0.1) until @thread_completed || skein.empty?
415
+ # Used to track how often we resume the same executor function
416
+ still_running = 0
417
+ # While the thread is still running
418
+ while future.incomplete?
419
+ # If the Fiber gets resumed, increment the resume tracker. This means
420
+ # the tracker starts at 1 since it needs to increment before yielding,
421
+ # since it can't yield then increment.
422
+ still_running += 1
423
+ # If the Fiber has been resumed before, still_running will be 2 or
424
+ # more. Yield different values for when the same Fiber is resumed
425
+ # multiple times and when it's resumed the first time in order to know
426
+ # if progress was made in the plan.
427
+ Fiber.yield(still_running < 2 ? :something_happened : :returned_immediately)
447
428
  end
448
429
 
449
- publish_event(type: :stop_spin)
450
- @in_parallel = false
451
- unsubscribe(self, [:node_result])
452
- results
430
+ # Once the thread completes, return the result.
431
+ future.value || future.reason
453
432
  end
454
433
 
455
434
  class TimeoutError < RuntimeError; end
@@ -519,7 +498,7 @@ module Bolt
519
498
  # coupled with the orchestrator transport since the transport behaves
520
499
  # differently when a plan is running. In order to limit how much this
521
500
  # pollutes the transport API we only handle the orchestrator transport here.
522
- # Since we callt this function without resolving targets this will result
501
+ # Since we call this function without resolving targets this will result
523
502
  # in the orchestrator transport always being initialized during plan runs.
524
503
  # For now that's ok.
525
504
  #