bolt 3.8.1 → 3.11.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +4 -4
  3. data/bolt-modules/boltlib/lib/puppet/datatypes/future.rb +25 -0
  4. data/bolt-modules/boltlib/lib/puppet/functions/background.rb +61 -0
  5. data/bolt-modules/boltlib/lib/puppet/functions/download_file.rb +5 -9
  6. data/bolt-modules/boltlib/lib/puppet/functions/parallelize.rb +28 -13
  7. data/bolt-modules/boltlib/lib/puppet/functions/run_command.rb +5 -15
  8. data/bolt-modules/boltlib/lib/puppet/functions/run_script.rb +5 -17
  9. data/bolt-modules/boltlib/lib/puppet/functions/run_task.rb +8 -17
  10. data/bolt-modules/boltlib/lib/puppet/functions/run_task_with.rb +8 -15
  11. data/bolt-modules/boltlib/lib/puppet/functions/upload_file.rb +5 -17
  12. data/bolt-modules/boltlib/lib/puppet/functions/wait.rb +91 -0
  13. data/bolt-modules/boltlib/types/planresult.pp +1 -0
  14. data/guides/debugging.txt +28 -0
  15. data/guides/inventory.txt +5 -0
  16. data/lib/bolt/applicator.rb +3 -2
  17. data/lib/bolt/bolt_option_parser.rb +51 -4
  18. data/lib/bolt/cli.rb +38 -9
  19. data/lib/bolt/config/transport/docker.rb +1 -1
  20. data/lib/bolt/config/transport/lxd.rb +1 -1
  21. data/lib/bolt/config/transport/podman.rb +1 -1
  22. data/lib/bolt/error.rb +11 -1
  23. data/lib/bolt/executor.rb +55 -72
  24. data/lib/bolt/fiber_executor.rb +141 -0
  25. data/lib/bolt/module_installer/installer.rb +1 -1
  26. data/lib/bolt/outputter/human.rb +46 -2
  27. data/lib/bolt/outputter/json.rb +9 -0
  28. data/lib/bolt/pal.rb +117 -17
  29. data/lib/bolt/plan_future.rb +66 -0
  30. data/lib/bolt/plugin.rb +38 -0
  31. data/lib/bolt/plugin/env_var.rb +8 -1
  32. data/lib/bolt/plugin/module.rb +1 -1
  33. data/lib/bolt/plugin/prompt.rb +8 -1
  34. data/lib/bolt/plugin/puppet_connect_data.rb +8 -1
  35. data/lib/bolt/plugin/puppetdb.rb +7 -1
  36. data/lib/bolt/plugin/task.rb +9 -1
  37. data/lib/bolt/project.rb +2 -1
  38. data/lib/bolt/task.rb +7 -0
  39. data/lib/bolt/transport/docker/connection.rb +5 -2
  40. data/lib/bolt/transport/lxd/connection.rb +4 -0
  41. data/lib/bolt/transport/podman/connection.rb +4 -0
  42. data/lib/bolt/version.rb +1 -1
  43. data/lib/bolt_server/config.rb +1 -1
  44. data/lib/bolt_server/request_error.rb +11 -0
  45. data/lib/bolt_server/transport_app.rb +133 -95
  46. data/lib/bolt_spec/plans/mock_executor.rb +40 -45
  47. data/lib/bolt_spec/run.rb +4 -1
  48. data/modules/puppet_connect/plans/test_input_data.pp +8 -3
  49. data/resources/bolt_bash_completion.sh +214 -0
  50. metadata +10 -3
  51. data/lib/bolt/yarn.rb +0 -23
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/logger'
4
+ require 'bolt/plan_future'
5
+
6
+ module Bolt
7
+ class FiberExecutor
8
+ attr_reader :plan_futures
9
+
10
+ def initialize
11
+ @logger = Bolt::Logger.logger(self)
12
+ @id = 0
13
+ @plan_futures = []
14
+ end
15
+
16
+ # Whether there is more than one fiber running in parallel.
17
+ #
18
+ def in_parallel?
19
+ plan_futures.length > 1
20
+ end
21
+
22
+ # Creates a new Puppet scope from the current Plan scope so that variables
23
+ # can be used inside the block and won't interact with the outer scope.
24
+ # Then creates a new Fiber to execute the block, wraps the Fiber in a
25
+ # Bolt::PlanFuture, and returns the Bolt::PlanFuture.
26
+ #
27
+ def create_future(scope: nil, name: nil)
28
+ newscope = nil
29
+ if scope
30
+ # Save existing variables to the new scope before starting the future
31
+ # itself so that if the plan returns before the backgrounded block
32
+ # starts, we still have the variables.
33
+ newscope = Puppet::Parser::Scope.new(scope.compiler)
34
+ local = Puppet::Parser::Scope::LocalScope.new
35
+
36
+ # Compress the current scopes into a single vars hash to add to the new scope
37
+ scope.to_hash(true, true).each_pair { |k, v| local[k] = v }
38
+ newscope.push_ephemerals([local])
39
+ end
40
+
41
+ # Create a new Fiber that will execute the provided block.
42
+ future = Fiber.new do
43
+ # Yield the new scope - this should be ignored by the block if
44
+ # `newscope` is nil.
45
+ yield newscope
46
+ end
47
+
48
+ # PlanFutures are assigned an ID, which is just a global incrementing
49
+ # integer. The main plan should always have ID 0.
50
+ @id += 1
51
+ future = Bolt::PlanFuture.new(future, @id, name)
52
+ @logger.trace("Created future #{future.name}")
53
+
54
+ # Register the PlanFuture with the FiberExecutor to be executed
55
+ plan_futures << future
56
+ future
57
+ end
58
+
59
+ # Visit each PlanFuture registered with the FiberExecutor and resume it.
60
+ # Fibers will yield themselves back, either if they kicked off a
61
+ # long-running process or if the current long-running process hasn't
62
+ # completed. If the Fiber finishes after being resumed, store the result in
63
+ # the PlanFuture and remove the PlanFuture from the FiberExecutor.
64
+ #
65
+ def round_robin
66
+ plan_futures.each do |future|
67
+ # If the Fiber is still running and can be resumed, then resume it
68
+ @logger.trace("Checking future '#{future.name}'")
69
+ if future.alive?
70
+ @logger.trace("Resuming future '#{future.name}'")
71
+ future.resume
72
+ end
73
+
74
+ # Once we've restarted the Fiber, check to see if it's finished again
75
+ # and cleanup if it has.
76
+ next if future.alive?
77
+ @logger.trace("Cleaning up future '#{future.name}'")
78
+
79
+ # If the future errored and the main plan has already exited, log the
80
+ # error at warn level.
81
+ unless plan_futures.map(&:id).include?(0) || future.state == "done"
82
+ Bolt::Logger.warn('errored_futures', "Error in future '#{future.name}': #{future.value}")
83
+ end
84
+
85
+ # Remove the PlanFuture from the FiberExecutor.
86
+ plan_futures.delete(future)
87
+ end
88
+
89
+ # If the Fiber immediately returned or if the Fiber is blocking on a
90
+ # `wait` call, Bolt should pause for long enough that something can
91
+ # execute before checking again. This mitigates CPU
92
+ # thrashing.
93
+ return unless plan_futures.all? { |f| %i[returned_immediately unfinished].include?(f.value) }
94
+ @logger.trace("Nothing can be resumed. Rechecking in 0.5 seconds.")
95
+
96
+ sleep(0.5)
97
+ end
98
+
99
+ # Whether all PlanFutures have finished executing, indicating that the
100
+ # entire plan (main plan and any PlanFutures it spawned) has finished and
101
+ # Bolt can exit.
102
+ #
103
+ def plan_complete?
104
+ plan_futures.empty?
105
+ end
106
+
107
+ # Block until the provided PlanFuture objects have finished, or the timeout is reached.
108
+ #
109
+ def wait(futures, timeout: nil, catch_errors: false, **_kwargs)
110
+ if timeout.nil?
111
+ Fiber.yield(:unfinished) until futures.map(&:alive?).none?
112
+ else
113
+ start = Time.now
114
+ Fiber.yield(:unfinished) until (Time.now - start > timeout) || futures.map(&:alive?).none?
115
+ # Raise an error for any futures that are still alive
116
+ futures.each do |f|
117
+ if f.alive?
118
+ f.raise(Bolt::FutureTimeoutError.new(f.name, timeout))
119
+ end
120
+ end
121
+ end
122
+
123
+ results = futures.map(&:value)
124
+
125
+ failed_indices = results.each_index.select do |i|
126
+ results[i].is_a?(Bolt::Error)
127
+ end
128
+
129
+ if failed_indices.any?
130
+ if catch_errors
131
+ failed_indices.each { |i| results[i] = results[i].to_puppet_error }
132
+ else
133
+ # Do this after handling errors for simplicity and pretty printing
134
+ raise Bolt::ParallelFailure.new(results, failed_indices)
135
+ end
136
+ end
137
+
138
+ results
139
+ end
140
+ end
141
+ end
@@ -30,7 +30,7 @@ module Bolt
30
30
 
31
31
  settings = R10K::Settings.global_settings.evaluate(@config)
32
32
  R10K::Initializers::GlobalInitializer.new(settings).call
33
- install_action = R10K::Action::Puppetfile::Install.new(r10k_opts, nil)
33
+ install_action = R10K::Action::Puppetfile::Install.new(r10k_opts, nil, {})
34
34
 
35
35
  # Override the r10k logger with a proxy to our own logger
36
36
  R10K::Logging.instance_variable_set(:@outputter, Bolt::R10KLogProxy.new)
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'bolt/container_result'
3
4
  require 'bolt/pal'
4
5
 
5
6
  module Bolt
@@ -173,11 +174,22 @@ module Bolt
173
174
  if result.message?
174
175
  @stream.puts(remove_trail(indent(2, result.message)))
175
176
  end
176
-
177
177
  case result.action
178
178
  when 'command', 'script'
179
179
  safe_value = result.safe_value
180
- @stream.puts(indent(2, safe_value['merged_output'])) unless safe_value['merged_output'].strip.empty?
180
+ if safe_value["merged_output"]
181
+ @stream.puts(indent(2, safe_value['merged_output'])) unless safe_value['merged_output'].strip.empty?
182
+
183
+ else # output stdout or stderr
184
+ unless safe_value['stdout'].nil? || safe_value['stdout'].strip.empty?
185
+ @stream.puts(indent(2, "STDOUT:"))
186
+ @stream.puts(indent(4, safe_value['stdout']))
187
+ end
188
+ unless safe_value['stderr'].nil? || safe_value['stderr'].strip.empty?
189
+ @stream.puts(indent(2, "STDERR:"))
190
+ @stream.puts(indent(4, safe_value['stderr']))
191
+ end
192
+ end
181
193
  when 'lookup'
182
194
  @stream.puts(indent(2, result['value']))
183
195
  else
@@ -459,6 +471,10 @@ module Bolt
459
471
  @stream.puts(guide)
460
472
  end
461
473
 
474
+ def print_plan_lookup(value)
475
+ @stream.puts(value)
476
+ end
477
+
462
478
  def print_module_list(module_list)
463
479
  module_list.each do |path, modules|
464
480
  if (mod = modules.find { |m| m[:internal_module_group] })
@@ -487,6 +503,34 @@ module Bolt
487
503
  end
488
504
  end
489
505
 
506
+ def print_plugin_list(plugin_list, modulepath)
507
+ info = +''
508
+ length = plugin_list.values.map(&:keys).flatten.map(&:length).max + 4
509
+
510
+ plugin_list.each do |hook, plugins|
511
+ next if plugins.empty?
512
+ next if hook == :validate_resolve_reference
513
+
514
+ info << colorize(:cyan, "#{hook}\n")
515
+
516
+ plugins.each do |name, description|
517
+ info << indent(2, name.ljust(length))
518
+ info << truncate(description, 80 - length) if description
519
+ info << "\n"
520
+ end
521
+
522
+ info << "\n"
523
+ end
524
+
525
+ info << colorize(:cyan, "Modulepath\n")
526
+ info << indent(2, "#{modulepath.join(File::PATH_SEPARATOR)}\n\n")
527
+
528
+ info << colorize(:cyan, "Additional information\n")
529
+ info << indent(2, "For more information about using plugins see https://pup.pt/bolt-plugins")
530
+
531
+ @stream.puts info.chomp
532
+ end
533
+
490
534
  def print_targets(target_list, inventory_source, default_inventory, target_flag)
491
535
  adhoc = colorize(:yellow, "(Not found in inventory file)")
492
536
 
@@ -60,6 +60,11 @@ module Bolt
60
60
  print_table('tasks' => tasks, 'modulepath' => modulepath)
61
61
  end
62
62
 
63
+ def print_plugin_list(plugins, modulepath)
64
+ plugins.delete(:validate_resolve_reference)
65
+ print_table('plugins' => plugins, 'modulepath' => modulepath)
66
+ end
67
+
63
68
  def print_plan_info(plan)
64
69
  path = plan.delete('module')
65
70
  plan['module_dir'] = if path.start_with?(Bolt::Config::Modulepath::MODULES_PATH)
@@ -98,6 +103,10 @@ module Bolt
98
103
  }.to_json)
99
104
  end
100
105
 
106
+ def print_plan_lookup(value)
107
+ @stream.puts(value.to_json)
108
+ end
109
+
101
110
  def print_puppetfile_result(success, puppetfile, moduledir)
102
111
  @stream.puts({ success: success,
103
112
  puppetfile: puppetfile,
data/lib/bolt/pal.rb CHANGED
@@ -163,9 +163,10 @@ module Bolt
163
163
  # Runs a block in a PAL script compiler configured for Bolt. Catches
164
164
  # exceptions thrown by the block and re-raises them ensuring they are
165
165
  # Bolt::Errors since the script compiler block will squash all exceptions.
166
- def in_bolt_compiler
166
+ def in_bolt_compiler(compiler_params: {})
167
167
  # TODO: If we always call this inside a bolt_executor we can remove this here
168
168
  setup
169
+ compiler_params = compiler_params.merge(set_local_facts: false)
169
170
  r = Puppet::Pal.in_tmp_environment('bolt', modulepath: full_modulepath, facts: {}) do |pal|
170
171
  # Only load the project if it a) exists, b) has a name it can be loaded with
171
172
  Puppet.override(bolt_project: @project,
@@ -174,7 +175,7 @@ module Bolt
174
175
  # of modules, it must happen *after* we have overridden
175
176
  # bolt_project or the project will be ignored
176
177
  detect_project_conflict(@project, Puppet.lookup(:environments).get('bolt'))
177
- pal.with_script_compiler(set_local_facts: false) do |compiler|
178
+ pal.with_script_compiler(**compiler_params) do |compiler|
178
179
  alias_types(compiler)
179
180
  register_resource_types(Puppet.lookup(:loaders)) if @resource_types
180
181
  begin
@@ -299,6 +300,49 @@ module Bolt
299
300
  end
300
301
  end
301
302
 
303
+ def list_tasks_with_cache(filter_content: false)
304
+ # Don't filter content yet, so that if users update their task filters
305
+ # we don't need to refresh the cache
306
+ task_names = list_tasks(filter_content: false).map(&:first)
307
+ task_cache = if @project
308
+ Bolt::Util.read_optional_json_file(@project.task_cache_file, 'Task cache file')
309
+ else
310
+ {}
311
+ end
312
+ updated = false
313
+
314
+ task_list = task_names.each_with_object([]) do |task_name, list|
315
+ data = task_cache[task_name] || get_task_info(task_name, with_mtime: true)
316
+
317
+ # Make sure all the keys are strings - if we get data from
318
+ # get_task_info they will be symbols
319
+ data = Bolt::Util.walk_keys(data, &:to_s)
320
+
321
+ # If any files in the task were updated, refresh the cache
322
+ if data['files']&.any?
323
+ # For all the files that are part of the task
324
+ data['files'].each do |f|
325
+ # If any file has been updated since we last cached, update the
326
+ # cache
327
+ next if File.mtime(f['path']).to_s == f['mtime']
328
+ data = get_task_info(task_name, with_mtime: true)
329
+ data = Bolt::Util.walk_keys(data, &:to_s)
330
+ # Tell Bolt to write to the cache file once we're done
331
+ updated = true
332
+ # Update the cache data
333
+ task_cache[task_name] = data
334
+ end
335
+ end
336
+ metadata = data['metadata'] || {}
337
+ # Don't add tasks to the list to return if they are private
338
+ list << [task_name, metadata['description']] unless metadata['private']
339
+ end
340
+
341
+ # Write the cache if any entries were updated
342
+ File.write(@project.task_cache_file, task_cache.to_json) if updated
343
+ filter_content ? filter_content(task_list, @project&.tasks) : task_list
344
+ end
345
+
302
346
  def list_tasks(filter_content: false)
303
347
  in_bolt_compiler do |compiler|
304
348
  tasks = compiler.list_tasks.map(&:name).sort.each_with_object([]) do |task_name, data|
@@ -350,14 +394,20 @@ module Bolt
350
394
  end
351
395
  end
352
396
 
353
- def get_task(task_name)
397
+ def get_task(task_name, with_mtime: false)
354
398
  task = task_signature(task_name)
355
399
 
356
400
  if task.nil?
357
401
  raise Bolt::Error.unknown_task(task_name)
358
402
  end
359
403
 
360
- Bolt::Task.from_task_signature(task)
404
+ task = Bolt::Task.from_task_signature(task)
405
+ task.add_mtimes if with_mtime
406
+ task
407
+ end
408
+
409
+ def get_task_info(task_name, with_mtime: false)
410
+ get_task(task_name, with_mtime: with_mtime).to_h
361
411
  end
362
412
 
363
413
  def list_plans_with_cache(filter_content: false)
@@ -372,20 +422,20 @@ module Bolt
372
422
  updated = false
373
423
 
374
424
  plan_list = plan_names.each_with_object([]) do |plan_name, list|
375
- info = plan_cache[plan_name] || get_plan_info(plan_name, with_mtime: true)
425
+ data = plan_cache[plan_name] || get_plan_info(plan_name, with_mtime: true)
376
426
 
377
427
  # If the plan is a 'local' plan (in the project itself, or the
378
428
  # modules/ directory) then verify it hasn't been updated since we
379
429
  # cached it. If it has been updated, refresh the cache and use the
380
430
  # new data.
381
- if info['file'] &&
382
- (File.mtime(info.dig('file', 'path')) <=> info.dig('file', 'mtime')) != 0
383
- info = get_plan_info(plan_name, with_mtime: true)
431
+ if data['file'] &&
432
+ File.mtime(data.dig('file', 'path')).to_s != data.dig('file', 'mtime')
433
+ data = get_plan_info(plan_name, with_mtime: true)
384
434
  updated = true
385
- plan_cache[plan_name] = info
435
+ plan_cache[plan_name] = data
386
436
  end
387
437
 
388
- list << [plan_name, info['description']] unless info['private']
438
+ list << [plan_name, data['description']] unless data['private']
389
439
  end
390
440
 
391
441
  File.write(@project.plan_cache_file, plan_cache.to_json) if updated
@@ -585,6 +635,7 @@ module Bolt
585
635
  inputs = generator.find_inputs(:pcore)
586
636
  FileUtils.mkdir_p(@resource_types)
587
637
  cache_plan_info if @project && cache
638
+ cache_task_info if @project && cache
588
639
  generator.generate(inputs, @resource_types, true)
589
640
  end
590
641
  end
@@ -600,6 +651,17 @@ module Bolt
600
651
  File.write(@project.plan_cache_file, plans_info.to_json)
601
652
  end
602
653
 
654
+ def cache_task_info
655
+ # task_name is an array here
656
+ tasks_info = list_tasks(filter_content: false).map do |task_name,|
657
+ data = get_task_info(task_name, with_mtime: true)
658
+ { task_name => data }
659
+ end.reduce({}, :merge)
660
+
661
+ FileUtils.touch(@project.task_cache_file)
662
+ File.write(@project.task_cache_file, tasks_info.to_json)
663
+ end
664
+
603
665
  def run_task(task_name, targets, params, executor, inventory, description = nil)
604
666
  in_task_compiler(executor, inventory) do |compiler|
605
667
  params = params.merge('_bolt_api_call' => true, '_catch_errors' => true)
@@ -607,16 +669,46 @@ module Bolt
607
669
  end
608
670
  end
609
671
 
610
- def run_plan(plan_name, params, executor = nil, inventory = nil, pdb_client = nil, applicator = nil)
672
+ def run_plan(plan_name, params, executor, inventory = nil, pdb_client = nil, applicator = nil)
673
+ # Start the round robin inside the plan compiler, so that
674
+ # backgrounded tasks can finish once the main plan exits
611
675
  in_plan_compiler(executor, inventory, pdb_client, applicator) do |compiler|
612
- r = compiler.call_function('run_plan', plan_name, params.merge('_bolt_api_call' => true))
613
- Bolt::PlanResult.from_pcore(r, 'success')
676
+ # Create a Fiber for the main plan. This will be run along with any
677
+ # other Fibers created during the plan run in the round_robin, with the
678
+ # main plan always taking precedence in being resumed.
679
+ future = executor.create_future(name: plan_name) do |_scope|
680
+ r = compiler.call_function('run_plan', plan_name, params.merge('_bolt_api_call' => true))
681
+ Bolt::PlanResult.from_pcore(r, 'success')
682
+ rescue Bolt::Error => e
683
+ Bolt::PlanResult.new(e, 'failure')
684
+ end
685
+
686
+ # Round robin until all Fibers, including the main plan, have finished.
687
+ # This will stay alive until backgrounded tasks have finished.
688
+ executor.round_robin until executor.plan_complete?
689
+
690
+ # Return the result from the main plan
691
+ future.value
614
692
  end
615
693
  rescue Bolt::Error => e
616
694
  Bolt::PlanResult.new(e, 'failure')
617
695
  end
618
696
 
619
- def lookup(key, targets, inventory, executor, _concurrency)
697
+ def plan_hierarchy_lookup(key, plan_vars: {})
698
+ # Do a lookup with a script compiler, which uses the 'plan_hierarchy' key in
699
+ # Hiera config.
700
+ with_puppet_settings do
701
+ # We want all of the setup and teardown that `in_bolt_compiler` does,
702
+ # but also want to pass keys to the script compiler.
703
+ in_bolt_compiler(compiler_params: { variables: plan_vars }) do |compiler|
704
+ compiler.call_function('lookup', key)
705
+ end
706
+ rescue Puppet::Error => e
707
+ raise PALError.from_error(e)
708
+ end
709
+ end
710
+
711
+ def lookup(key, targets, inventory, executor, plan_vars: {})
620
712
  # Install the puppet-agent package and collect facts. Facts are
621
713
  # automatically added to the targets.
622
714
  in_plan_compiler(executor, inventory, nil) do |compiler|
@@ -638,21 +730,29 @@ module Bolt
638
730
 
639
731
  trusted = Puppet::Context::TrustedInformation.local(node).to_h
640
732
 
733
+ # Separate environment configuration from interpolation data the same
734
+ # way we do when compiling Puppet catalogs.
641
735
  env_conf = {
642
736
  modulepath: @modulepath.full_modulepath,
643
- facts: target.facts,
644
- variables: target.vars
737
+ facts: target.facts
738
+ }
739
+
740
+ interpolations = {
741
+ variables: plan_vars,
742
+ target_variables: target.vars
645
743
  }
646
744
 
647
745
  with_puppet_settings do
648
746
  Puppet::Pal.in_tmp_environment(target.name, **env_conf) do |pal|
649
747
  Puppet.override(overrides) do
650
748
  Puppet.lookup(:pal_current_node).trusted_data = trusted
651
- pal.with_catalog_compiler do |compiler|
749
+ pal.with_catalog_compiler(**interpolations) do |compiler|
652
750
  Bolt::Result.for_lookup(target, key, compiler.call_function('lookup', key))
653
751
  rescue StandardError => e
654
752
  Bolt::Result.from_exception(target, e)
655
753
  end
754
+ rescue Puppet::Error => e
755
+ raise PALError.from_error(e)
656
756
  end
657
757
  end
658
758
  end