bolt 3.14.1 → 3.17.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 (42) 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/guides/debugging.yaml +27 -0
  5. data/guides/inventory.yaml +23 -0
  6. data/guides/links.yaml +12 -0
  7. data/guides/logging.yaml +17 -0
  8. data/guides/module.yaml +18 -0
  9. data/guides/modulepath.yaml +24 -0
  10. data/guides/project.yaml +21 -0
  11. data/guides/targets.yaml +28 -0
  12. data/guides/transports.yaml +22 -0
  13. data/lib/bolt/analytics.rb +2 -19
  14. data/lib/bolt/application.rb +634 -0
  15. data/lib/bolt/bolt_option_parser.rb +28 -4
  16. data/lib/bolt/cli.rb +592 -788
  17. data/lib/bolt/fiber_executor.rb +7 -3
  18. data/lib/bolt/inventory/inventory.rb +68 -39
  19. data/lib/bolt/inventory.rb +2 -9
  20. data/lib/bolt/module_installer/puppetfile.rb +24 -10
  21. data/lib/bolt/outputter/human.rb +83 -32
  22. data/lib/bolt/outputter/json.rb +63 -38
  23. data/lib/bolt/pal/yaml_plan/transpiler.rb +1 -1
  24. data/lib/bolt/pal.rb +31 -11
  25. data/lib/bolt/plan_creator.rb +84 -25
  26. data/lib/bolt/plan_future.rb +11 -6
  27. data/lib/bolt/plan_result.rb +1 -1
  28. data/lib/bolt/plugin/task.rb +1 -1
  29. data/lib/bolt/plugin.rb +11 -17
  30. data/lib/bolt/project.rb +0 -7
  31. data/lib/bolt/result_set.rb +2 -1
  32. data/lib/bolt/transport/local/connection.rb +17 -1
  33. data/lib/bolt/transport/orch/connection.rb +13 -1
  34. data/lib/bolt/version.rb +1 -1
  35. data/lib/bolt_server/file_cache.rb +12 -0
  36. data/lib/bolt_server/schemas/action-apply.json +32 -0
  37. data/lib/bolt_server/schemas/action-apply_prep.json +19 -0
  38. data/lib/bolt_server/transport_app.rb +113 -24
  39. data/lib/bolt_spec/bolt_context.rb +1 -1
  40. data/lib/bolt_spec/run.rb +1 -1
  41. metadata +14 -3
  42. data/lib/bolt/secret.rb +0 -37
@@ -52,7 +52,7 @@ module Bolt
52
52
  # tracking which Futures to wait on when `wait()` is called without
53
53
  # arguments.
54
54
  @id += 1
55
- future = Bolt::PlanFuture.new(future, @id, name: name, plan_id: plan_id)
55
+ future = Bolt::PlanFuture.new(future, @id, name: name, plan_id: plan_id, scope: newscope)
56
56
  @logger.trace("Created future #{future.name}")
57
57
 
58
58
  # Register the PlanFuture with the FiberExecutor to be executed
@@ -68,11 +68,15 @@ module Bolt
68
68
  #
69
69
  def round_robin
70
70
  active_futures.each do |future|
71
- # If the Fiber is still running and can be resumed, then resume it
71
+ # If the Fiber is still running and can be resumed, then resume it.
72
+ # Override Puppet's global_scope to prevent ephemerals in other scopes
73
+ # from being popped off in the wrong order due to race conditions.
74
+ # This primarily happens when running executor functions from custom
75
+ # Puppet language functions, but may happen elsewhere.
72
76
  @logger.trace("Checking future '#{future.name}'")
73
77
  if future.alive?
74
78
  @logger.trace("Resuming future '#{future.name}'")
75
- future.resume
79
+ Puppet.override(global_scope: future.scope) { future.resume }
76
80
  end
77
81
 
78
82
  # Once we've restarted the Fiber, check to see if it's finished again
@@ -6,7 +6,7 @@ require 'bolt/inventory/target'
6
6
  module Bolt
7
7
  class Inventory
8
8
  class Inventory
9
- attr_reader :config, :plugins, :source, :targets, :transport
9
+ attr_reader :plugins, :source, :targets, :transport
10
10
 
11
11
  class WildcardError < Bolt::Error
12
12
  def initialize(target)
@@ -14,25 +14,15 @@ module Bolt
14
14
  end
15
15
  end
16
16
 
17
- # TODO: Pass transport config instead of config object
18
17
  def initialize(data, transport, transports, plugins, source = nil)
19
- @logger = Bolt::Logger.logger(self)
20
- @data = data || {}
21
- @transport = transport
22
- @config = transports
23
- @plugins = plugins
24
- @groups = Group.new(@data, plugins, all_group: true)
25
- @group_lookup = {}
26
- @targets = {}
27
- @source = source
28
-
29
- @groups.resolve_string_targets(@groups.target_aliases, @groups.all_targets)
30
-
31
- collect_groups
32
- end
33
-
34
- def validate
35
- @groups.validate
18
+ @logger = Bolt::Logger.logger(self)
19
+ @data = data || {}
20
+ @transport = transport
21
+ @config = transports
22
+ @config_resolved = transports.values.all?(&:resolved?)
23
+ @plugins = plugins
24
+ @targets = {}
25
+ @source = source
36
26
  end
37
27
 
38
28
  def version
@@ -43,13 +33,52 @@ module Bolt
43
33
  Bolt::Target
44
34
  end
45
35
 
46
- def collect_groups
47
- # Provide a lookup map for finding a group by name
48
- @group_lookup = @groups.collect_groups
36
+ # Load and resolve the groups in the inventory. Loading groups resolves
37
+ # all plugin references except for those for target data and config.
38
+ #
39
+ # @return [Bolt::Inventory::Group]
40
+ #
41
+ def groups
42
+ @groups ||= Group.new(@data, @plugins, all_group: true).tap do |groups|
43
+ groups.resolve_string_targets(groups.target_aliases, groups.all_targets)
44
+ groups.validate
45
+ end
49
46
  end
50
47
 
48
+ # Return a list of all group names in the inventory.
49
+ #
50
+ # @return [Array[String]]
51
+ #
51
52
  def group_names
52
- @group_lookup.keys
53
+ group_lookup.keys
54
+ end
55
+
56
+ # Return a map of all groups in the inventory.
57
+ #
58
+ # @return [Hash[String, Bolt::Inventory::Group]]
59
+ #
60
+ def group_lookup
61
+ @group_lookup ||= groups.collect_groups
62
+ end
63
+
64
+ # Return a map of transport configuration for the inventory. Any
65
+ # unresolved plugin references are resolved.
66
+ #
67
+ # @return [Hash[String, Bolt::Config::Transport]]
68
+ #
69
+ def config
70
+ if @config_resolved
71
+ @config
72
+ else
73
+ @config_resolved = true
74
+ @config.transform_values! { |t| t.resolved? ? t : t.resolve(@plugins) }
75
+ end
76
+ end
77
+
78
+ # Validates the inventory.
79
+ #
80
+ def validate
81
+ groups.validate
53
82
  end
54
83
 
55
84
  def group_names_for(target_name)
@@ -57,7 +86,7 @@ module Bolt
57
86
  end
58
87
 
59
88
  def target_names
60
- @groups.all_targets
89
+ groups.all_targets
61
90
  end
62
91
  # alias for analytics
63
92
  alias node_names target_names
@@ -81,7 +110,7 @@ module Bolt
81
110
 
82
111
  #### PRIVATE ####
83
112
  def group_data_for(target_name)
84
- @groups.group_collect(target_name)
113
+ groups.group_collect(target_name)
85
114
  end
86
115
 
87
116
  # If target is a group name, expand it to the members of that group.
@@ -89,15 +118,15 @@ module Bolt
89
118
  # If a wildcard string, error if no matches are found.
90
119
  # Else fall back to [target] if no matches are found.
91
120
  def resolve_name(target)
92
- if (group = @group_lookup[target])
121
+ if (group = group_lookup[target])
93
122
  group.all_targets
94
123
  else
95
124
  # Try to wildcard match targets in inventory
96
125
  # Ignore case because hostnames are generally case-insensitive
97
126
  regexp = Regexp.new("^#{Regexp.escape(target).gsub('\*', '.*?')}$", Regexp::IGNORECASE)
98
127
 
99
- targets = @groups.all_targets.select { |targ| targ =~ regexp }
100
- targets += @groups.target_aliases.select { |target_alias, _target| target_alias =~ regexp }.values
128
+ targets = groups.all_targets.select { |targ| targ =~ regexp }
129
+ targets += groups.target_aliases.select { |target_alias, _target| target_alias =~ regexp }.values
101
130
 
102
131
  if targets.empty?
103
132
  raise(WildcardError, target) if target.include?('*')
@@ -165,7 +194,7 @@ module Bolt
165
194
  # associated references. This is used when a target is resolved by
166
195
  # get_targets.
167
196
  def create_target_from_inventory(target_name)
168
- target_data = @groups.target_collect(target_name) || { 'uri' => target_name }
197
+ target_data = groups.target_collect(target_name) || { 'uri' => target_name }
169
198
 
170
199
  target = Bolt::Inventory::Target.new(target_data, self)
171
200
  @targets[target.name] = target
@@ -187,24 +216,24 @@ module Bolt
187
216
  @targets[new_target.name] = new_target
188
217
 
189
218
  if existing_target
190
- clear_alia_from_group(@groups, new_target.name)
219
+ clear_alia_from_group(groups, new_target.name)
191
220
  else
192
221
  add_to_group([new_target], 'all')
193
222
  end
194
223
 
195
224
  if new_target.target_alias
196
- @groups.insert_alia(new_target.name, Array(new_target.target_alias))
225
+ groups.insert_alia(new_target.name, Array(new_target.target_alias))
197
226
  end
198
227
 
199
228
  new_target
200
229
  end
201
230
 
202
231
  def validate_target_from_hash(target)
203
- groups = Set.new(group_names)
204
- targets = target_names
232
+ used_groups = Set.new(group_names)
233
+ used_targets = target_names
205
234
 
206
235
  # Make sure there are no group name conflicts
207
- if groups.include?(target.name)
236
+ if used_groups.include?(target.name)
208
237
  raise ValidationError.new("Target name #{target.name} conflicts with group of the same name", nil)
209
238
  end
210
239
 
@@ -217,11 +246,11 @@ module Bolt
217
246
  end
218
247
 
219
248
  # Make sure there are no conflicts with the new target aliases
220
- used_aliases = @groups.target_aliases
249
+ used_aliases = groups.target_aliases
221
250
  Array(target.target_alias).each do |alia|
222
- if groups.include?(alia)
251
+ if used_groups.include?(alia)
223
252
  raise ValidationError.new("Alias #{alia} conflicts with group of the same name", nil)
224
- elsif targets.include?(alia)
253
+ elsif used_targets.include?(alia)
225
254
  raise ValidationError.new("Alias #{alia} conflicts with target of the same name", nil)
226
255
  elsif used_aliases[alia] && used_aliases[alia] != target.name
227
256
  raise ValidationError.new(
@@ -250,7 +279,7 @@ module Bolt
250
279
  end
251
280
 
252
281
  if group_names.include?(desired_group)
253
- remove_target(@groups, @targets[target.first.name], desired_group)
282
+ remove_target(groups, @targets[target.first.name], desired_group)
254
283
  else
255
284
  raise ValidationError.new("Group #{desired_group} does not exist in inventory", nil)
256
285
  end
@@ -260,7 +289,7 @@ module Bolt
260
289
  if group_names.include?(desired_group)
261
290
  targets.each do |target|
262
291
  # Add the inventory copy of the target
263
- add_target(@groups, @targets[target.name], desired_group)
292
+ add_target(groups, @targets[target.name], desired_group)
264
293
  end
265
294
  else
266
295
  raise ValidationError.new("Group #{desired_group} does not exist in inventory", nil)
@@ -91,19 +91,12 @@ module Bolt
91
91
  end
92
92
  end
93
93
 
94
- # Resolve plugin references from transport config
95
- config.transports.each_value do |t|
96
- t.resolve(plugins) unless t.resolved?
97
- end
98
-
99
94
  Bolt::Validator.new.tap do |validator|
100
95
  validator.validate(data, schema, source)
101
96
  validator.warnings.each { |warning| Bolt::Logger.warn(warning[:id], warning[:msg]) }
102
97
  end
103
98
 
104
- inventory = create_version(data, config.transport, config.transports, plugins, source)
105
- inventory.validate
106
- inventory
99
+ create_version(data, config.transport, config.transports, plugins, source)
107
100
  end
108
101
 
109
102
  def self.create_version(data, transport, transports, plugins, source = nil)
@@ -119,7 +112,7 @@ module Bolt
119
112
 
120
113
  def self.empty
121
114
  config = Bolt::Config.default
122
- plugins = Bolt::Plugin.setup(config, nil)
115
+ plugins = Bolt::Plugin.new(config, nil)
123
116
 
124
117
  create_version({}, config.transport, config.transports, plugins, nil)
125
118
  end
@@ -97,20 +97,34 @@ module Bolt
97
97
  end
98
98
  end
99
99
 
100
- return if unsatisfied_specs.empty?
101
-
100
+ versionless_mods = @modules.select { |mod| mod.is_a?(ForgeModule) && mod.version.nil? }
102
101
  command = Bolt::Util.windows? ? 'Install-BoltModule -Force' : 'bolt module install --force'
103
102
 
104
- message = <<~MESSAGE.chomp
105
- Puppetfile does not include modules that satisfy the following specifications:
103
+ if unsatisfied_specs.any?
104
+ message = <<~MESSAGE.chomp
105
+ Puppetfile does not include modules that satisfy the following specifications:
106
+
107
+ #{unsatisfied_specs.map(&:to_hash).to_yaml.lines.drop(1).join.chomp}
108
+
109
+ This Puppetfile might not be managed by Bolt. To forcibly overwrite the
110
+ Puppetfile, run '#{command}'.
111
+ MESSAGE
106
112
 
107
- #{unsatisfied_specs.map(&:to_hash).to_yaml.lines.drop(1).join.chomp}
108
-
109
- This Puppetfile might not be managed by Bolt. To forcibly overwrite the
110
- Puppetfile, run '#{command}'.
111
- MESSAGE
113
+ raise Bolt::Error.new(message, 'bolt/missing-module-specs')
114
+ end
112
115
 
113
- raise Bolt::Error.new(message, 'bolt/missing-module-specs')
116
+ if versionless_mods.any?
117
+ message = <<~MESSAGE.chomp
118
+ Puppetfile includes Forge modules without a version requirement:
119
+
120
+ #{versionless_mods.map(&:to_spec).join.chomp}
121
+
122
+ This Puppetfile might not be managed by Bolt. To forcibly overwrite the
123
+ Puppetfile, run '#{command}'.
124
+ MESSAGE
125
+
126
+ raise Bolt::Error.new(message, 'bolt/missing-module-version-specs')
127
+ end
114
128
  end
115
129
  end
116
130
  end
@@ -310,7 +310,12 @@ module Bolt
310
310
  )
311
311
  end
312
312
 
313
- def print_tasks(tasks, modulepath)
313
+ # List available tasks.
314
+ #
315
+ # @param tasks [Array] A list of task names and descriptions.
316
+ # @param modulepath [Array] The modulepath.
317
+ #
318
+ def print_tasks(tasks:, modulepath:)
314
319
  command = Bolt::Util.powershell? ? 'Get-BoltTask -Name <TASK NAME>' : 'bolt task show <TASK NAME>'
315
320
 
316
321
  tasks = tasks.map do |name, description|
@@ -330,8 +335,11 @@ module Bolt
330
335
  @stream.puts indent(2, "Use '#{command}' to view details and parameters for a specific task.")
331
336
  end
332
337
 
333
- # @param [Hash] task A hash representing the task
334
- def print_task_info(task)
338
+ # Print information about a task.
339
+ #
340
+ # @param task [Bolt::Task] The task information.
341
+ #
342
+ def print_task_info(task:)
335
343
  params = (task.parameters || []).sort
336
344
 
337
345
  info = +''
@@ -443,7 +451,7 @@ module Bolt
443
451
  @stream.puts info
444
452
  end
445
453
 
446
- def print_plans(plans, modulepath)
454
+ def print_plans(plans:, modulepath:)
447
455
  command = Bolt::Util.powershell? ? 'Get-BoltPlan -Name <PLAN NAME>' : 'bolt plan show <PLAN NAME>'
448
456
 
449
457
  plans = plans.map do |name, description|
@@ -463,7 +471,11 @@ module Bolt
463
471
  @stream.puts indent(2, "Use '#{command}' to view details and parameters for a specific plan.")
464
472
  end
465
473
 
466
- def print_topics(topics)
474
+ # Print available guide topics.
475
+ #
476
+ # @param topics [Array] The available topics.
477
+ #
478
+ def print_topics(topics:, **_kwargs)
467
479
  info = +"#{colorize(:cyan, 'Topics')}\n"
468
480
  info << indent(2, topics.join("\n"))
469
481
  info << "\n\n#{colorize(:cyan, 'Additional information')}\n"
@@ -471,7 +483,11 @@ module Bolt
471
483
  @stream.puts info
472
484
  end
473
485
 
474
- def print_guide(topic:, guide:, documentation: nil)
486
+ # Print the guide for the specified topic.
487
+ #
488
+ # @param guide [String] The guide.
489
+ #
490
+ def print_guide(topic:, guide:, documentation: nil, **_kwargs)
475
491
  info = +"#{colorize(:cyan, topic)}\n"
476
492
  info << indent(2, guide)
477
493
 
@@ -595,17 +611,17 @@ module Bolt
595
611
  @stream.puts info
596
612
  end
597
613
 
598
- def print_plugin_list(plugin_list, modulepath)
614
+ def print_plugin_list(plugins:, modulepath:)
599
615
  info = +''
600
- length = plugin_list.values.map(&:keys).flatten.map(&:length).max + 4
616
+ length = plugins.values.map(&:keys).flatten.map(&:length).max + 4
601
617
 
602
- plugin_list.each do |hook, plugins|
603
- next if plugins.empty?
618
+ plugins.each do |hook, plugin|
619
+ next if plugin.empty?
604
620
  next if hook == :validate_resolve_reference
605
621
 
606
622
  info << colorize(:cyan, "#{hook}\n")
607
623
 
608
- plugins.each do |name, description|
624
+ plugin.each do |name, description|
609
625
  info << indent(2, name.ljust(length))
610
626
  info << truncate(description, 80 - length) if description
611
627
  info << "\n"
@@ -623,12 +639,37 @@ module Bolt
623
639
  @stream.puts info.chomp
624
640
  end
625
641
 
626
- def print_targets(target_list, inventory_source, default_inventory, target_flag)
627
- adhoc = colorize(:yellow, "(Not found in inventory file)")
642
+ def print_new_plan(name:, path:)
643
+ if Bolt::Util.powershell?
644
+ show_command = 'Get-BoltPlan -Name '
645
+ run_command = 'Invoke-BoltPlan -Name '
646
+ else
647
+ show_command = 'bolt plan show'
648
+ run_command = 'bolt plan run'
649
+ end
650
+
651
+ print_message(<<~OUTPUT)
652
+ Created plan '#{name}' at '#{path}'
653
+
654
+ Show this plan with:
655
+ #{show_command} #{name}
656
+ Run this plan with:
657
+ #{run_command} #{name}
658
+ OUTPUT
659
+ end
660
+
661
+ # Print target names and where they came from.
662
+ #
663
+ # @param adhoc [Hash] Adhoc targets provided on the command line.
664
+ # @param inventory [Hash] Targets provided from the inventory.
665
+ # @param flag [Boolean] Whether a targeting command-line option was used.
666
+ #
667
+ def print_targets(adhoc:, inventory:, flag:, **_kwargs)
668
+ adhoc_text = colorize(:yellow, "(Not found in inventory file)")
628
669
 
629
670
  targets = []
630
- targets += target_list[:inventory].map { |target| [target.name, nil] }
631
- targets += target_list[:adhoc].map { |target| [target.name, adhoc] }
671
+ targets += inventory[:targets].map { |target| [target['name'], nil] }
672
+ targets += adhoc[:targets].map { |target| [target['name'], adhoc_text] }
632
673
 
633
674
  info = +''
634
675
 
@@ -641,27 +682,31 @@ module Bolt
641
682
  end
642
683
  info << "\n\n"
643
684
 
644
- info << format_inventory_source(inventory_source, default_inventory)
645
- info << format_target_summary(target_list[:inventory].count, target_list[:adhoc].count, target_flag, false)
685
+ info << format_inventory_source(inventory[:file], inventory[:default])
686
+ info << format_target_summary(inventory[:count], adhoc[:count], flag, false)
646
687
 
647
688
  @stream.puts info
648
689
  end
649
690
 
650
- def print_target_info(target_list, inventory_source, default_inventory, target_flag)
651
- adhoc_targets = target_list[:adhoc].map(&:name).to_set
652
- inventory_targets = target_list[:inventory].map(&:name).to_set
653
- targets = target_list.values.flatten.sort_by(&:name)
691
+ # Print detailed target information.
692
+ #
693
+ # @param adhoc [Hash] Adhoc targets provided on the command line.
694
+ # @param inventory [Hash] Targets provided from the inventory.
695
+ # @param flag [Boolean] Whether a targeting command-line option was used.
696
+ #
697
+ def print_target_info(adhoc:, inventory:, flag:, **_kwargs)
698
+ targets = (adhoc[:targets] + inventory[:targets]).sort_by { |t| t['name'] }
654
699
 
655
700
  info = +''
656
701
 
657
702
  if targets.any?
658
- adhoc = colorize(:yellow, " (Not found in inventory file)")
703
+ adhoc_text = colorize(:yellow, " (Not found in inventory file)")
659
704
 
660
705
  targets.each do |target|
661
- info << colorize(:cyan, target.name)
662
- info << adhoc if adhoc_targets.include?(target.name)
706
+ info << colorize(:cyan, target['name'])
707
+ info << adhoc_text if adhoc[:targets].include?(target)
663
708
  info << "\n"
664
- info << indent(2, target.detail.to_yaml.lines.drop(1).join)
709
+ info << indent(2, target.to_yaml.lines.drop(1).join)
665
710
  info << "\n"
666
711
  end
667
712
  else
@@ -669,8 +714,8 @@ module Bolt
669
714
  info << indent(2, "No targets\n\n")
670
715
  end
671
716
 
672
- info << format_inventory_source(inventory_source, default_inventory)
673
- info << format_target_summary(inventory_targets.count, adhoc_targets.count, target_flag, true)
717
+ info << format_inventory_source(inventory[:file], inventory[:default])
718
+ info << format_target_summary(inventory[:count], adhoc[:count], flag, true)
674
719
 
675
720
  @stream.puts info
676
721
  end
@@ -716,7 +761,13 @@ module Bolt
716
761
  info
717
762
  end
718
763
 
719
- def print_groups(groups, inventory_source, default_inventory)
764
+ # Print inventory group information.
765
+ #
766
+ # @param count [Integer] Number of groups in the inventory.
767
+ # @param groups [Array] Names of groups in the inventory.
768
+ # @param inventory [Hash] Where the inventory was loaded from.
769
+ #
770
+ def print_groups(count:, groups:, inventory:)
720
771
  info = +''
721
772
 
722
773
  # Add group list
@@ -725,18 +776,18 @@ module Bolt
725
776
  info << "\n\n"
726
777
 
727
778
  # Add inventory file source
728
- info << format_inventory_source(inventory_source, default_inventory)
779
+ info << format_inventory_source(inventory[:source], inventory[:default])
729
780
 
730
781
  # Add group count summary
731
782
  info << colorize(:cyan, "Group count\n")
732
- info << indent(2, "#{groups.count} total")
783
+ info << indent(2, "#{count} total")
733
784
 
734
785
  @stream.puts info
735
786
  end
736
787
 
737
788
  # @param [Bolt::ResultSet] apply_result A ResultSet object representing the result of a `bolt apply`
738
- def print_apply_result(apply_result, elapsed_time)
739
- print_summary(apply_result, elapsed_time)
789
+ def print_apply_result(apply_result)
790
+ print_summary(apply_result, apply_result.elapsed_time)
740
791
  end
741
792
 
742
793
  # @param [Bolt::PlanResult] plan_result A PlanResult object