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
@@ -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.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
@@ -86,6 +86,7 @@ module Bolt
86
86
  if config.default_inventoryfile.exist?
87
87
  logger.debug("Loaded inventory from #{config.default_inventoryfile}")
88
88
  else
89
+ source = nil
89
90
  logger.debug("Tried to load inventory from #{config.default_inventoryfile}, but the file does not exist")
90
91
  end
91
92
  end
@@ -100,17 +101,17 @@ module Bolt
100
101
  validator.warnings.each { |warning| Bolt::Logger.warn(warning[:id], warning[:msg]) }
101
102
  end
102
103
 
103
- inventory = create_version(data, config.transport, config.transports, plugins)
104
+ inventory = create_version(data, config.transport, config.transports, plugins, source)
104
105
  inventory.validate
105
106
  inventory
106
107
  end
107
108
 
108
- def self.create_version(data, transport, transports, plugins)
109
+ def self.create_version(data, transport, transports, plugins, source = nil)
109
110
  version = (data || {}).delete('version') { 2 }
110
111
 
111
112
  case version
112
113
  when 2
113
- Bolt::Inventory::Inventory.new(data, transport, transports, plugins)
114
+ Bolt::Inventory::Inventory.new(data, transport, transports, plugins, source)
114
115
  else
115
116
  raise ValidationError.new("Unsupported version #{version} specified in inventory", nil)
116
117
  end
@@ -120,7 +121,7 @@ module Bolt
120
121
  config = Bolt::Config.default
121
122
  plugins = Bolt::Plugin.setup(config, nil)
122
123
 
123
- create_version({}, config.transport, config.transports, plugins)
124
+ create_version({}, config.transport, config.transports, plugins, nil)
124
125
  end
125
126
  end
126
127
  end
@@ -6,7 +6,7 @@ require 'bolt/inventory/target'
6
6
  module Bolt
7
7
  class Inventory
8
8
  class Inventory
9
- attr_reader :targets, :plugins, :config, :transport
9
+ attr_reader :config, :plugins, :source, :targets, :transport
10
10
 
11
11
  class WildcardError < Bolt::Error
12
12
  def initialize(target)
@@ -15,7 +15,7 @@ module Bolt
15
15
  end
16
16
 
17
17
  # TODO: Pass transport config instead of config object
18
- def initialize(data, transport, transports, plugins)
18
+ def initialize(data, transport, transports, plugins, source = nil)
19
19
  @logger = Bolt::Logger.logger(self)
20
20
  @data = data || {}
21
21
  @transport = transport
@@ -24,6 +24,7 @@ module Bolt
24
24
  @groups = Group.new(@data, plugins, all_group: true)
25
25
  @group_lookup = {}
26
26
  @targets = {}
27
+ @source = source
27
28
 
28
29
  @groups.resolve_string_targets(@groups.target_aliases, @groups.all_targets)
29
30
 
data/lib/bolt/logger.rb CHANGED
@@ -203,7 +203,7 @@ module Bolt
203
203
  def self.flush_queue
204
204
  @mutex.synchronize do
205
205
  @message_queue.each do |message|
206
- log_message(message)
206
+ log_message(**message)
207
207
  end
208
208
 
209
209
  @message_queue.clear
@@ -55,7 +55,7 @@ module Bolt
55
55
  Invalid module specification:
56
56
  #{hash.to_yaml.lines.drop(1).join.chomp}
57
57
 
58
- To read more about specifying modules, see https://pup.pt/bolt-modules
58
+ To read more about specifying modules, see https://pup.pt/bolt-module-specs
59
59
  MESSAGE
60
60
  end
61
61
 
@@ -67,8 +67,7 @@ module Bolt
67
67
  elsif git.start_with?('https://github.com')
68
68
  git.split('https://github.com/').last.split('.git').first
69
69
  else
70
- raise Bolt::ValidationError,
71
- "Invalid git source: #{git}. Only GitHub modules are supported."
70
+ raise Bolt::ValidationError, invalid_git_msg(git)
72
71
  end
73
72
 
74
73
  [git, repo]
@@ -89,6 +88,14 @@ module Bolt
89
88
  }
90
89
  end
91
90
 
91
+ # Returns an error message that the provided repo is not a git repo or
92
+ # is private.
93
+ #
94
+ private def invalid_git_msg(repo_name)
95
+ "#{repo_name} is not a public GitHub repository. See https://pup.pt/no-resolve "\
96
+ "for information on how to install this module."
97
+ end
98
+
92
99
  # Returns a PuppetfileResolver::Model::GitModule object for resolving.
93
100
  #
94
101
  def to_resolver_module
@@ -157,10 +164,7 @@ module Bolt
157
164
 
158
165
  raise Bolt::Error.new(message, 'bolt/github-api-rate-limit-error')
159
166
  when Net::HTTPNotFound
160
- raise Bolt::Error.new(
161
- "#{git} is not a git repository.",
162
- "bolt/missing-git-repository-error"
163
- )
167
+ raise Bolt::Error.new(invalid_git_msg(git), "bolt/missing-git-repository-error")
164
168
  else
165
169
  raise Bolt::Error.new(
166
170
  "Ref #{ref} at #{git} is not a commit, tag, or branch.",
@@ -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
@@ -174,12 +175,16 @@ module Bolt
174
175
  @stream.puts(remove_trail(indent(2, result.message)))
175
176
  end
176
177
 
177
- # Use special handling if the result looks like a command or script result
178
- if result.generic_value.keys == %w[stdout stderr merged_output exit_code]
178
+ case result.action
179
+ when 'command', 'script'
179
180
  safe_value = result.safe_value
180
181
  @stream.puts(indent(2, safe_value['merged_output'])) unless safe_value['merged_output'].strip.empty?
181
- elsif result.generic_value.any?
182
- @stream.puts(indent(2, ::JSON.pretty_generate(result.generic_value)))
182
+ when 'lookup'
183
+ @stream.puts(indent(2, result['value']))
184
+ else
185
+ if result.generic_value.any?
186
+ @stream.puts(indent(2, ::JSON.pretty_generate(result.generic_value)))
187
+ end
183
188
  end
184
189
  end
185
190
  end
@@ -293,7 +298,7 @@ module Bolt
293
298
  end
294
299
 
295
300
  def print_tasks(tasks, modulepath)
296
- command = Bolt::Util.powershell? ? 'Get-BoltTask -Task <TASK NAME>' : 'bolt task show <TASK NAME>'
301
+ command = Bolt::Util.powershell? ? 'Get-BoltTask -Name <TASK NAME>' : 'bolt task show <TASK NAME>'
297
302
 
298
303
  tasks = tasks.map do |name, description|
299
304
  description = truncate(description, 72)
@@ -483,7 +488,7 @@ module Bolt
483
488
  end
484
489
  end
485
490
 
486
- def print_targets(target_list, inventoryfile)
491
+ def print_targets(target_list, inventory_source, default_inventory, target_flag)
487
492
  adhoc = colorize(:yellow, "(Not found in inventory file)")
488
493
 
489
494
  targets = []
@@ -501,16 +506,13 @@ module Bolt
501
506
  end
502
507
  info << "\n\n"
503
508
 
504
- @stream.puts info
509
+ info << format_inventory_source(inventory_source, default_inventory)
510
+ info << format_target_summary(target_list[:inventory].count, target_list[:adhoc].count, target_flag, false)
505
511
 
506
- print_inventory_summary(
507
- target_list[:inventory].count,
508
- target_list[:adhoc].count,
509
- inventoryfile
510
- )
512
+ @stream.puts info
511
513
  end
512
514
 
513
- def print_target_info(target_list, inventoryfile)
515
+ def print_target_info(target_list, inventory_source, default_inventory, target_flag)
514
516
  adhoc_targets = target_list[:adhoc].map(&:name).to_set
515
517
  inventory_targets = target_list[:inventory].map(&:name).to_set
516
518
  targets = target_list.values.flatten.sort_by(&:name)
@@ -532,26 +534,27 @@ module Bolt
532
534
  info << indent(2, "No targets\n\n")
533
535
  end
534
536
 
535
- @stream.puts info
537
+ info << format_inventory_source(inventory_source, default_inventory)
538
+ info << format_target_summary(inventory_targets.count, adhoc_targets.count, target_flag, true)
536
539
 
537
- print_inventory_summary(
538
- inventory_targets.count,
539
- adhoc_targets.count,
540
- inventoryfile
541
- )
540
+ @stream.puts info
542
541
  end
543
542
 
544
- private def print_inventory_summary(inventory_count, adhoc_count, inventoryfile)
543
+ private def format_inventory_source(inventory_source, default_inventory)
545
544
  info = +''
546
545
 
547
546
  # Add inventory file source
548
- info << colorize(:cyan, "Inventory file\n")
549
- info << if File.exist?(inventoryfile)
550
- indent(2, "#{inventoryfile}\n")
547
+ info << colorize(:cyan, "Inventory source\n")
548
+ info << if inventory_source
549
+ indent(2, "#{inventory_source}\n")
551
550
  else
552
- indent(2, wrap("Tried to load inventory from #{inventoryfile}, but the file does not exist\n"))
551
+ indent(2, wrap("Tried to load inventory from #{default_inventory}, but the file does not exist\n"))
553
552
  end
554
553
  info << "\n"
554
+ end
555
+
556
+ private def format_target_summary(inventory_count, adhoc_count, target_flag, detail_flag)
557
+ info = +''
555
558
 
556
559
  # Add target count summary
557
560
  count = "#{inventory_count + adhoc_count} total, "\
@@ -560,13 +563,40 @@ module Bolt
560
563
  info << colorize(:cyan, "Target count\n")
561
564
  info << indent(2, count)
562
565
 
563
- @stream.puts info
566
+ # Add filtering information
567
+ unless target_flag && detail_flag
568
+ info << colorize(:cyan, "\n\nAdditional information\n")
569
+
570
+ unless target_flag
571
+ opt = Bolt::Util.windows? ? "'-Targets', '-Query', or '-Rerun'" : "'--targets', '--query', or '--rerun'"
572
+ info << indent(2, "Use the #{opt} option to view specific targets\n")
573
+ end
574
+
575
+ unless detail_flag
576
+ opt = Bolt::Util.windows? ? '-Detail' : '--detail'
577
+ info << indent(2, "Use the '#{opt}' option to view target configuration and data")
578
+ end
579
+ end
580
+
581
+ info
564
582
  end
565
583
 
566
- def print_groups(groups)
567
- count = "#{groups.count} group#{'s' unless groups.count == 1}"
568
- @stream.puts groups.join("\n")
569
- @stream.puts colorize(:green, count)
584
+ def print_groups(groups, inventory_source, default_inventory)
585
+ info = +''
586
+
587
+ # Add group list
588
+ info << colorize(:cyan, "Groups\n")
589
+ info << indent(2, groups.join("\n"))
590
+ info << "\n\n"
591
+
592
+ # Add inventory file source
593
+ info << format_inventory_source(inventory_source, default_inventory)
594
+
595
+ # Add group count summary
596
+ info << colorize(:cyan, "Group count\n")
597
+ info << indent(2, "#{groups.count} total")
598
+
599
+ @stream.puts info
570
600
  end
571
601
 
572
602
  # @param [Bolt::ResultSet] apply_result A ResultSet object representing the result of a `bolt apply`
@@ -83,6 +83,10 @@ module Bolt
83
83
  @stream.puts result.to_json
84
84
  end
85
85
 
86
+ def print_result_set(result_set)
87
+ @stream.puts result_set.to_json
88
+ end
89
+
86
90
  def print_topics(topics)
87
91
  print_table('topics' => topics)
88
92
  end
@@ -100,12 +104,12 @@ module Bolt
100
104
  moduledir: moduledir.to_s }.to_json)
101
105
  end
102
106
 
103
- def print_targets(target_list, inventoryfile)
107
+ def print_targets(target_list, inventory_source, default_inventory, _target_flag)
104
108
  @stream.puts ::JSON.pretty_generate(
105
109
  inventory: {
106
110
  targets: target_list[:inventory].map(&:name),
107
111
  count: target_list[:inventory].count,
108
- file: inventoryfile.to_s
112
+ file: (inventory_source || default_inventory).to_s
109
113
  },
110
114
  adhoc: {
111
115
  targets: target_list[:adhoc].map(&:name),
@@ -116,7 +120,7 @@ module Bolt
116
120
  )
117
121
  end
118
122
 
119
- def print_target_info(target_list, _inventoryfile)
123
+ def print_target_info(target_list, _inventory_source, _default_inventory, _target_flag)
120
124
  targets = target_list.values.flatten
121
125
 
122
126
  @stream.puts ::JSON.pretty_generate(
@@ -125,7 +129,7 @@ module Bolt
125
129
  )
126
130
  end
127
131
 
128
- def print_groups(groups)
132
+ def print_groups(groups, _inventory_source, _default_inventory)
129
133
  count = groups.count
130
134
  @stream.puts({ groups: groups,
131
135
  count: count }.to_json)