bolt 3.11.0 → 3.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

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
@@ -1,7 +1,6 @@
1
- TOPIC
2
- modulepath
3
-
4
- DESCRIPTION
1
+ ---
2
+ topic: modulepath
3
+ guide: |
5
4
  The modulepath is an ordered list of directories that Bolt loads modules
6
5
  from. When Bolt runs a command, it automatically loads modules from the
7
6
  modulepath.
@@ -21,5 +20,5 @@ DESCRIPTION
21
20
 
22
21
  To learn more about modules, see the 'module' guide.
23
22
 
24
- DOCUMENTATION
25
- https://pup.pt/bolt-project-reference#modulepath
23
+ documentation:
24
+ - https://pup.pt/bolt-project-reference#modulepath
@@ -1,7 +1,6 @@
1
- TOPIC
2
- project
3
-
4
- DESCRIPTION
1
+ ---
2
+ topic: project
3
+ guide: |
5
4
  A Bolt project is a directory that serves as the launching point for Bolt
6
5
  and allows you to create a shareable orchestration application. Projects
7
6
  typically include a project configuration file, an inventory file, and any
@@ -17,6 +16,6 @@ DESCRIPTION
17
16
  and content, including inventory files, unless the data and content are part
18
17
  of a project.
19
18
 
20
- DOCUMENTATION
21
- https://pup.pt/bolt-projects
22
- https://pup.pt/bolt-project-reference
19
+ documentation:
20
+ - https://pup.pt/bolt-projects
21
+ - https://pup.pt/bolt-project-reference
@@ -1,7 +1,6 @@
1
- TOPIC
2
- targets
3
-
4
- DESCRIPTION
1
+ ---
2
+ topic: targets
3
+ guide: |
5
4
  A target is a device that Bolt connects to and runs actions on. Targets can
6
5
  be physical, such as servers, or virtual, such as containers or virtual
7
6
  machines.
@@ -25,5 +24,5 @@ DESCRIPTION
25
24
  project's inventory file. For more information about inventory files,
26
25
  see 'bolt guide inventory'.
27
26
 
28
- DOCUMENTATION
29
- https://pup.pt/bolt-commands
27
+ documentation:
28
+ - https://pup.pt/bolt-commands
@@ -1,7 +1,6 @@
1
- TOPIC
2
- transports
3
-
4
- DESCRIPTION
1
+ ---
2
+ topic: transports
3
+ guide: |
5
4
  Bolt uses transports (also known as protocols) to establish a connection
6
5
  with a target in order to run actions on the target. The default transport is
7
6
  SSH, and you can see available transports along with their configuration
@@ -18,6 +17,6 @@ DESCRIPTION
18
17
  Finally, you can set the transport for a target in the inventory. For more
19
18
  information about the Bolt inventory, run 'bolt guide inventory'.
20
19
 
21
- DOCUMENTATION
22
- https://pup.pt/bolt-commands#specify-a-transport
23
- http://pup.pt/bolt-inventory#transport-configuration
20
+ documentation:
21
+ - https://pup.pt/bolt-commands#specify-a-transport
22
+ - http://pup.pt/bolt-inventory#transport-configuration
@@ -33,7 +33,7 @@ module Bolt
33
33
  logger = Bolt::Logger.logger(self)
34
34
  begin
35
35
  config_file = config_path
36
- config = load_config(config_file)
36
+ config = enabled ? load_config(config_file) : {}
37
37
  rescue ArgumentError
38
38
  config = { 'disabled' => true }
39
39
  end
@@ -122,7 +122,13 @@ module Bolt
122
122
  logs.each do |log|
123
123
  bolt_level = Bolt::Util::PuppetLogLevel::MAPPING[log['level'].to_sym]
124
124
  message = log['message'].chomp
125
- @logger.send(bolt_level, "#{target.name}: #{message}")
125
+
126
+ case bolt_level
127
+ when :warn
128
+ handle_warning(target, message)
129
+ else
130
+ @logger.send(bolt_level, "#{target.name}: #{message}")
131
+ end
126
132
  end
127
133
  end
128
134
 
@@ -138,6 +144,22 @@ module Bolt
138
144
  result
139
145
  end
140
146
 
147
+ # Handles logging Puppet warnings, some of which are suppressable.
148
+ #
149
+ # @param target [Bolt::Target] The target the apply ran on.
150
+ # @param message [String] The log message.
151
+ #
152
+ private def handle_warning(target, message)
153
+ # Messages about exported resource declaration and collection, which are
154
+ # not supported in manifest blocks.
155
+ if message.include?(Puppet::Pops::Issues::RT_NO_STORECONFIGS_EXPORT.format) ||
156
+ message.include?(Puppet::Pops::Issues::RT_NO_STORECONFIGS.format)
157
+ Bolt::Logger.warn('exported_resources', "#{target.name}: #{message}")
158
+ else
159
+ @logger.send(:warn, "#{target.name}: #{message}")
160
+ end
161
+ end
162
+
141
163
  def validate_hiera_config(hiera_config)
142
164
  if File.exist?(File.path(hiera_config))
143
165
  data = File.open(File.path(hiera_config), "r:UTF-8") { |f| YAML.safe_load(f.read, [Symbol]) }
@@ -507,11 +507,14 @@ module Bolt
507
507
  show
508
508
 
509
509
  #{colorize(:cyan, 'Usage')}
510
- bolt module show [options]
510
+ bolt module show [module name] [options]
511
511
 
512
512
  #{colorize(:cyan, 'Description')}
513
513
  List modules available to the Bolt project.
514
514
 
515
+ Providing the name of a module will display detailed documentation for
516
+ the module.
517
+
515
518
  #{colorize(:cyan, 'Documentation')}
516
519
  To learn more about Bolt modules, run 'bolt guide module'.
517
520
  HELP
@@ -1088,11 +1091,11 @@ module Bolt
1088
1091
  end
1089
1092
  define('--log-level LEVEL',
1090
1093
  "Set the log level for the console. Available options are",
1091
- "trace, debug, info, warn, error, fatal, any.") do |level|
1094
+ "trace, debug, info, warn, error, fatal.") do |level|
1092
1095
  @options[:log] = { 'console' => { 'level' => level } }
1093
1096
  end
1094
1097
  define('--clear-cache',
1095
- "Clear plugin cache before executing.") do |_|
1098
+ "Clear plugin, plan, and task caches before executing.") do |_|
1096
1099
  @options[:clear_cache] = true
1097
1100
  end
1098
1101
  define('--plugin PLUGIN', 'Select the plugin to use.') do |plug|
data/lib/bolt/cli.rb CHANGED
@@ -238,8 +238,10 @@ module Bolt
238
238
  config.check_path_case('modulepath', config.modulepath)
239
239
  config.project.check_deprecated_file
240
240
 
241
- if options[:clear_cache] && File.exist?(config.project.plugin_cache_file)
242
- FileUtils.rm(config.project.plugin_cache_file)
241
+ if options[:clear_cache]
242
+ FileUtils.rm(config.project.plugin_cache_file) if File.exist?(config.project.plugin_cache_file)
243
+ FileUtils.rm(config.project.task_cache_file) if File.exist?(config.project.task_cache_file)
244
+ FileUtils.rm(config.project.plan_cache_file) if File.exist?(config.project.plan_cache_file)
243
245
  end
244
246
 
245
247
  warn_inventory_overrides_cli(options)
@@ -494,7 +496,11 @@ module Bolt
494
496
  when 'group'
495
497
  list_groups
496
498
  when 'module'
497
- list_modules
499
+ if options[:object]
500
+ show_module(options[:object])
501
+ else
502
+ list_modules
503
+ end
498
504
  when 'plugin'
499
505
  list_plugins
500
506
  end
@@ -787,8 +793,8 @@ module Bolt
787
793
  if %w[human rainbow].include?(options.fetch(:format, 'human'))
788
794
  executor.subscribe(outputter)
789
795
  else
790
- # Only subscribe to out::message events for JSON outputter
791
- executor.subscribe(outputter, [:message])
796
+ # Only subscribe to out module events for JSON outputter
797
+ executor.subscribe(outputter, %i[message verbose])
792
798
  end
793
799
 
794
800
  executor.subscribe(log_outputter)
@@ -832,13 +838,16 @@ module Bolt
832
838
 
833
839
  results = nil
834
840
  elapsed_time = Benchmark.realtime do
835
- pal.in_plan_compiler(executor, inventory, puppetdb_client) do |compiler|
836
- compiler.call_function('apply_prep', targets)
841
+ apply_prep_results = pal.in_plan_compiler(executor, inventory, puppetdb_client) do |compiler|
842
+ compiler.call_function('apply_prep', targets, '_catch_errors' => true)
837
843
  end
838
844
 
839
- results = pal.with_bolt_executor(executor, inventory, puppetdb_client) do
840
- Puppet.lookup(:apply_executor).apply_ast(ast, targets, catch_errors: true, noop: noop)
845
+ apply_results = pal.with_bolt_executor(executor, inventory, puppetdb_client) do
846
+ Puppet.lookup(:apply_executor)
847
+ .apply_ast(ast, apply_prep_results.ok_set.targets, catch_errors: true, noop: noop)
841
848
  end
849
+
850
+ results = Bolt::ResultSet.new(apply_prep_results.error_set.results + apply_results.results)
842
851
  end
843
852
 
844
853
  executor.shutdown
@@ -852,6 +861,10 @@ module Bolt
852
861
  outputter.print_module_list(pal.list_modules)
853
862
  end
854
863
 
864
+ def show_module(name)
865
+ outputter.print_module_info(**pal.show_module(name))
866
+ end
867
+
855
868
  def list_plugins
856
869
  outputter.print_plugin_list(plugins.list_plugins, pal.user_modulepath)
857
870
  end
@@ -950,8 +963,9 @@ module Bolt
950
963
  files = Dir.children(root_path).sort
951
964
 
952
965
  files.each_with_object({}) do |file, guides|
953
- next if file !~ /\.txt\z/
954
- topic = File.basename(file, '.txt')
966
+ next if file !~ /\.(yaml|yml)\z/
967
+ # The ".*" here removes any suffix
968
+ topic = File.basename(file, ".*")
955
969
  guides[topic] = File.join(root_path, file)
956
970
  end
957
971
  rescue SystemCallError => e
@@ -961,7 +975,7 @@ module Bolt
961
975
 
962
976
  # Display the list of available Bolt guides.
963
977
  def list_topics
964
- outputter.print_topics(guides.keys - ['guide'])
978
+ outputter.print_topics(guides.keys)
965
979
  0
966
980
  end
967
981
 
@@ -971,12 +985,18 @@ module Bolt
971
985
  analytics.event('Guide', 'known_topic', label: topic)
972
986
 
973
987
  begin
974
- guide = File.read(guides[topic])
988
+ guide = Bolt::Util.read_yaml_hash(guides[topic], 'guide')
975
989
  rescue SystemCallError => e
976
990
  raise Bolt::FileError("#{e.message}: unable to load guide page", filepath)
977
991
  end
978
992
 
979
- outputter.print_guide(guide, topic)
993
+ # Make sure both topic and guide keys are defined
994
+ unless (%w[topic guide] - guide.keys).empty?
995
+ msg = "Guide file #{guides[topic]} must have a 'topic' key and 'guide' key, but has #{guide.keys} keys."
996
+ raise Bolt::Error.new(msg, 'bolt/invalid-guide')
997
+ end
998
+
999
+ outputter.print_guide(**Bolt::Util.symbolize_top_level_keys(guide))
980
1000
  else
981
1001
  analytics.event('Guide', 'unknown_topic', label: topic)
982
1002
  outputter.print_message("Did not find guide for topic '#{topic}'.\n\n")
@@ -202,7 +202,7 @@ module Bolt
202
202
  "level" => {
203
203
  description: "The type of information to log.",
204
204
  type: String,
205
- enum: %w[trace debug error info warn fatal any],
205
+ enum: %w[trace debug error info warn fatal],
206
206
  _default: "warn"
207
207
  }
208
208
  }
@@ -221,7 +221,7 @@ module Bolt
221
221
  "level" => {
222
222
  description: "The type of information to log.",
223
223
  type: String,
224
- enum: %w[trace debug error info warn fatal any],
224
+ enum: %w[trace debug error info warn fatal],
225
225
  _default: "warn"
226
226
  }
227
227
  }
@@ -16,6 +16,18 @@ module Bolt
16
16
  _default: false,
17
17
  _example: true
18
18
  },
19
+ "batch-mode" => {
20
+ type: [TrueClass, FalseClass],
21
+ description: "Whether to disable password querying. When set to `false`, SSH will fall back to "\
22
+ "prompting for a password if key authentication fails. This might cause Bolt to hang. "\
23
+ "To prevent Bolt from hanging, you can configure `ssh-command` to use an SSH utility "\
24
+ "such as sshpass that supports providing a password non-interactively. For more "\
25
+ "information, see [Providing a password non-interactively using "\
26
+ "`native-ssh`](troubleshooting.md#providing-a-password-non-interactively-using-native-ssh).",
27
+ _plugin: true,
28
+ _default: true,
29
+ _example: false
30
+ },
19
31
  "bundled-ruby" => {
20
32
  description: "Whether to use the Ruby bundled with Bolt packages for local targets.",
21
33
  type: [TrueClass, FalseClass],
@@ -34,6 +34,7 @@ module Bolt
34
34
 
35
35
  # Options available when using the native ssh transport
36
36
  NATIVE_OPTIONS = %w[
37
+ batch-mode
37
38
  cleanup
38
39
  copy-command
39
40
  host
@@ -49,6 +50,7 @@ module Bolt
49
50
  ].concat(RUN_AS_OPTIONS).sort.freeze
50
51
 
51
52
  DEFAULTS = {
53
+ "batch-mode" => true,
52
54
  "cleanup" => true,
53
55
  "connect-timeout" => 10,
54
56
  "disconnect-timeout" => 5,
@@ -124,6 +126,11 @@ module Bolt
124
126
  msg = 'Cannot use native SSH transport with load-config set to false'
125
127
  raise Bolt::ValidationError, msg
126
128
  end
129
+
130
+ if !@config['batch-mode'] && !@config['ssh-command']
131
+ raise Bolt::ValidationError,
132
+ 'Must set ssh-command when batch-mode is set to false'
133
+ end
127
134
  end
128
135
  end
129
136
  end
data/lib/bolt/error.rb CHANGED
@@ -69,7 +69,7 @@ module Bolt
69
69
  'value' => result.value,
70
70
  'object' => result.object
71
71
  }
72
- message = "Plan aborted: Running container '#{result.object}' failed."
72
+ message = "Running container '#{result.object}' failed."
73
73
  super(message, 'bolt/container-failure', details)
74
74
  @result = result
75
75
  @error_code = 2
@@ -86,7 +86,7 @@ module Bolt
86
86
  'result_set' => result_set
87
87
  }
88
88
  object_msg = " '#{object}'" if object
89
- message = "Plan aborted: #{action}#{object_msg} failed on #{result_set.error_set.length} target"
89
+ message = "#{action}#{object_msg} failed on #{result_set.error_set.length} target"
90
90
  message += "s" unless result_set.error_set.length == 1
91
91
  super(message, 'bolt/run-failure', details)
92
92
  @result_set = result_set
@@ -122,7 +122,7 @@ module Bolt
122
122
  'failed_indices' => failed_indices,
123
123
  'results' => results
124
124
  }
125
- message = "Plan aborted: parallel block failed on #{failed_indices.length} target"
125
+ message = "parallel block failed on #{failed_indices.length} target"
126
126
  message += "s" unless failed_indices.length == 1
127
127
  super(message, 'bolt/parallel-failure', details)
128
128
  @error_code = 2
data/lib/bolt/executor.rb CHANGED
@@ -381,8 +381,16 @@ module Bolt
381
381
  # overloaded while also minimizing the Puppet lookups needed from plan
382
382
  # functions
383
383
  #
384
- def create_future(scope: nil, name: nil, &block)
385
- @fiber_executor.create_future(scope: scope, name: name, &block)
384
+ def create_future(plan_id:, scope: nil, name: nil, &block)
385
+ @fiber_executor.create_future(scope: scope, name: name, plan_id: plan_id, &block)
386
+ end
387
+
388
+ def get_current_future(fiber:)
389
+ @fiber_executor.get_current_future(fiber: fiber)
390
+ end
391
+
392
+ def get_current_plan_id(fiber:)
393
+ @fiber_executor.get_current_plan_id(fiber: fiber)
386
394
  end
387
395
 
388
396
  def plan_complete?
@@ -401,8 +409,8 @@ module Bolt
401
409
  @fiber_executor.wait(futures, **opts)
402
410
  end
403
411
 
404
- def plan_futures
405
- @fiber_executor.plan_futures
412
+ def get_futures_for_plan(plan_id:)
413
+ @fiber_executor.get_futures_for_plan(plan_id: plan_id)
406
414
  end
407
415
 
408
416
  # Execute a plan function concurrently. This function accepts the executor
@@ -5,18 +5,19 @@ require 'bolt/plan_future'
5
5
 
6
6
  module Bolt
7
7
  class FiberExecutor
8
- attr_reader :plan_futures
8
+ attr_reader :active_futures, :finished_futures
9
9
 
10
10
  def initialize
11
11
  @logger = Bolt::Logger.logger(self)
12
12
  @id = 0
13
- @plan_futures = []
13
+ @active_futures = []
14
+ @finished_futures = []
14
15
  end
15
16
 
16
17
  # Whether there is more than one fiber running in parallel.
17
18
  #
18
19
  def in_parallel?
19
- plan_futures.length > 1
20
+ active_futures.length > 1
20
21
  end
21
22
 
22
23
  # Creates a new Puppet scope from the current Plan scope so that variables
@@ -24,7 +25,7 @@ module Bolt
24
25
  # Then creates a new Fiber to execute the block, wraps the Fiber in a
25
26
  # Bolt::PlanFuture, and returns the Bolt::PlanFuture.
26
27
  #
27
- def create_future(scope: nil, name: nil)
28
+ def create_future(plan_id:, scope: nil, name: nil)
28
29
  newscope = nil
29
30
  if scope
30
31
  # Save existing variables to the new scope before starting the future
@@ -46,13 +47,16 @@ module Bolt
46
47
  end
47
48
 
48
49
  # PlanFutures are assigned an ID, which is just a global incrementing
49
- # integer. The main plan should always have ID 0.
50
+ # integer. The main plan should always have ID 0. They also have a
51
+ # plan_id, which identifies which plan spawned them. This is used for
52
+ # tracking which Futures to wait on when `wait()` is called without
53
+ # arguments.
50
54
  @id += 1
51
- future = Bolt::PlanFuture.new(future, @id, name)
55
+ future = Bolt::PlanFuture.new(future, @id, name: name, plan_id: plan_id)
52
56
  @logger.trace("Created future #{future.name}")
53
57
 
54
58
  # Register the PlanFuture with the FiberExecutor to be executed
55
- plan_futures << future
59
+ active_futures << future
56
60
  future
57
61
  end
58
62
 
@@ -63,7 +67,7 @@ module Bolt
63
67
  # the PlanFuture and remove the PlanFuture from the FiberExecutor.
64
68
  #
65
69
  def round_robin
66
- plan_futures.each do |future|
70
+ active_futures.each do |future|
67
71
  # If the Fiber is still running and can be resumed, then resume it
68
72
  @logger.trace("Checking future '#{future.name}'")
69
73
  if future.alive?
@@ -78,19 +82,19 @@ module Bolt
78
82
 
79
83
  # If the future errored and the main plan has already exited, log the
80
84
  # error at warn level.
81
- unless plan_futures.map(&:id).include?(0) || future.state == "done"
85
+ unless active_futures.map(&:id).include?(0) || future.state == "done"
82
86
  Bolt::Logger.warn('errored_futures', "Error in future '#{future.name}': #{future.value}")
83
87
  end
84
88
 
85
89
  # Remove the PlanFuture from the FiberExecutor.
86
- plan_futures.delete(future)
90
+ finished_futures.push(active_futures.delete(future))
87
91
  end
88
92
 
89
93
  # If the Fiber immediately returned or if the Fiber is blocking on a
90
94
  # `wait` call, Bolt should pause for long enough that something can
91
95
  # execute before checking again. This mitigates CPU
92
96
  # thrashing.
93
- return unless plan_futures.all? { |f| %i[returned_immediately unfinished].include?(f.value) }
97
+ return unless active_futures.all? { |f| %i[returned_immediately unfinished].include?(f.value) }
94
98
  @logger.trace("Nothing can be resumed. Rechecking in 0.5 seconds.")
95
99
 
96
100
  sleep(0.5)
@@ -101,12 +105,53 @@ module Bolt
101
105
  # Bolt can exit.
102
106
  #
103
107
  def plan_complete?
104
- plan_futures.empty?
108
+ active_futures.empty?
109
+ end
110
+
111
+ def all_futures
112
+ active_futures + finished_futures
113
+ end
114
+
115
+ # Get the PlanFuture object that is currently executing
116
+ #
117
+ def get_current_future(fiber:)
118
+ all_futures.select { |f| f.fiber == fiber }.first
119
+ end
120
+
121
+ # Get the plan invocation ID for the PlanFuture that is currently executing
122
+ #
123
+ def get_current_plan_id(fiber:)
124
+ get_current_future(fiber: fiber).current_plan
125
+ end
126
+
127
+ # Get the Future objects associated with a particular plan invocation.
128
+ #
129
+ def get_futures_for_plan(plan_id:)
130
+ all_futures.select { |f| f.original_plan == plan_id }
105
131
  end
106
132
 
107
133
  # Block until the provided PlanFuture objects have finished, or the timeout is reached.
108
134
  #
109
135
  def wait(futures, timeout: nil, catch_errors: false, **_kwargs)
136
+ if futures.nil?
137
+ results = []
138
+ plan_id = get_current_plan_id(fiber: Fiber.current)
139
+ # Recollect the futures for this plan until all of the futures have
140
+ # finished. This ensures that we include futures created inside of
141
+ # futures being waited on.
142
+ until (futures = get_futures_for_plan(plan_id: plan_id)).map(&:alive?).none?
143
+ if futures.map(&:fiber).include?(Fiber.current)
144
+ msg = "The wait() function cannot be called with no arguments inside a "\
145
+ "background block in the same plan."
146
+ raise Bolt::Error.new(msg, 'bolt/infinite-wait')
147
+ end
148
+ # Wait for all the futures we know about so far before recollecting
149
+ # Futures for the plan and waiting again
150
+ results = wait(futures, timeout: timeout, catch_errors: catch_errors)
151
+ end
152
+ return results
153
+ end
154
+
110
155
  if timeout.nil?
111
156
  Fiber.yield(:unfinished) until futures.map(&:alive?).none?
112
157
  else