bolt 2.34.0 → 2.40.1

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 (74) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +1 -1
  3. data/bolt-modules/boltlib/lib/puppet/datatypes/applyresult.rb +1 -0
  4. data/bolt-modules/boltlib/lib/puppet/functions/catch_errors.rb +1 -3
  5. data/bolt-modules/boltlib/lib/puppet/functions/download_file.rb +17 -6
  6. data/bolt-modules/boltlib/lib/puppet/functions/parallelize.rb +56 -0
  7. data/bolt-modules/boltlib/lib/puppet/functions/run_command.rb +24 -6
  8. data/bolt-modules/boltlib/lib/puppet/functions/run_script.rb +27 -8
  9. data/bolt-modules/boltlib/lib/puppet/functions/run_task.rb +21 -1
  10. data/bolt-modules/boltlib/lib/puppet/functions/run_task_with.rb +18 -1
  11. data/bolt-modules/boltlib/lib/puppet/functions/upload_file.rb +24 -6
  12. data/lib/bolt/analytics.rb +27 -8
  13. data/lib/bolt/apply_result.rb +3 -3
  14. data/lib/bolt/bolt_option_parser.rb +45 -18
  15. data/lib/bolt/cli.rb +98 -116
  16. data/lib/bolt/config.rb +184 -80
  17. data/lib/bolt/config/options.rb +148 -87
  18. data/lib/bolt/config/transport/base.rb +10 -19
  19. data/lib/bolt/config/transport/local.rb +1 -7
  20. data/lib/bolt/config/transport/options.rb +12 -69
  21. data/lib/bolt/config/transport/ssh.rb +8 -19
  22. data/lib/bolt/error.rb +24 -0
  23. data/lib/bolt/executor.rb +92 -18
  24. data/lib/bolt/inventory.rb +25 -0
  25. data/lib/bolt/inventory/group.rb +0 -8
  26. data/lib/bolt/inventory/options.rb +130 -0
  27. data/lib/bolt/inventory/target.rb +10 -11
  28. data/lib/bolt/module_installer.rb +21 -13
  29. data/lib/bolt/module_installer/resolver.rb +1 -1
  30. data/lib/bolt/outputter.rb +19 -5
  31. data/lib/bolt/outputter/human.rb +22 -3
  32. data/lib/bolt/outputter/json.rb +1 -1
  33. data/lib/bolt/outputter/logger.rb +1 -1
  34. data/lib/bolt/outputter/rainbow.rb +13 -2
  35. data/lib/bolt/pal.rb +18 -6
  36. data/lib/bolt/pal/yaml_plan.rb +7 -0
  37. data/lib/bolt/plugin.rb +41 -12
  38. data/lib/bolt/plugin/cache.rb +76 -0
  39. data/lib/bolt/plugin/module.rb +4 -4
  40. data/lib/bolt/plugin/puppetdb.rb +1 -1
  41. data/lib/bolt/project.rb +59 -40
  42. data/lib/bolt/project_manager.rb +201 -0
  43. data/lib/bolt/{project_migrator/config.rb → project_manager/config_migrator.rb} +49 -4
  44. data/lib/bolt/{project_migrator/inventory.rb → project_manager/inventory_migrator.rb} +3 -3
  45. data/lib/bolt/{project_migrator/base.rb → project_manager/migrator.rb} +2 -2
  46. data/lib/bolt/{project_migrator/modules.rb → project_manager/module_migrator.rb} +5 -3
  47. data/lib/bolt/puppetdb/client.rb +11 -2
  48. data/lib/bolt/puppetdb/config.rb +4 -3
  49. data/lib/bolt/rerun.rb +1 -5
  50. data/lib/bolt/shell/bash.rb +8 -2
  51. data/lib/bolt/shell/powershell.rb +21 -3
  52. data/lib/bolt/target.rb +4 -0
  53. data/lib/bolt/task/run.rb +1 -1
  54. data/lib/bolt/transport/local.rb +13 -0
  55. data/lib/bolt/transport/orch.rb +0 -5
  56. data/lib/bolt/transport/orch/connection.rb +10 -3
  57. data/lib/bolt/transport/ssh/exec_connection.rb +6 -2
  58. data/lib/bolt/util.rb +36 -7
  59. data/lib/bolt/validator.rb +227 -0
  60. data/lib/bolt/version.rb +1 -1
  61. data/lib/bolt/yarn.rb +23 -0
  62. data/lib/bolt_server/base_config.rb +3 -1
  63. data/lib/bolt_server/config.rb +3 -1
  64. data/lib/bolt_server/plugin.rb +13 -0
  65. data/lib/bolt_server/plugin/puppet_connect_data.rb +37 -0
  66. data/lib/bolt_server/schemas/connect-data.json +22 -0
  67. data/lib/bolt_server/schemas/partials/task.json +2 -2
  68. data/lib/bolt_server/transport_app.rb +82 -23
  69. data/lib/bolt_spec/plans/mock_executor.rb +4 -1
  70. data/libexec/apply_catalog.rb +1 -1
  71. data/libexec/custom_facts.rb +1 -1
  72. data/libexec/query_resources.rb +1 -1
  73. metadata +22 -14
  74. data/lib/bolt/project_migrator.rb +0 -80
@@ -8,6 +8,7 @@ module Bolt
8
8
  module Transport
9
9
  class Local < Base
10
10
  WINDOWS_OPTIONS = %w[
11
+ bundled-ruby
11
12
  cleanup
12
13
  interpreters
13
14
  tmpdir
@@ -29,13 +30,6 @@ module Bolt
29
30
  if @config['interpreters']
30
31
  @config['interpreters'] = normalize_interpreters(@config['interpreters'])
31
32
  end
32
-
33
- if (run_as_cmd = @config['run-as-command'])
34
- unless run_as_cmd.all? { |n| n.is_a?(String) }
35
- raise Bolt::ValidationError,
36
- "run-as-command must be an Array of Strings, received #{run_as_cmd.class} #{run_as_cmd.inspect}"
37
- end
38
- end
39
33
  end
40
34
  end
41
35
  end
@@ -6,73 +6,8 @@ module Bolt
6
6
  module Options
7
7
  LOGIN_SHELLS = %w[sh bash zsh dash ksh powershell].freeze
8
8
 
9
- # The following constants define the various configuration options available to Bolt's
10
- # transports. Each constant is a hash where keys are the configuration option and values
11
- # are the option's definition. These options are used in multiple locations:
12
- #
13
- # - Automatic type validation when loading and setting configuration
14
- # - Generating reference documentation for configuration files
15
- # - Generating JSON schemas for configuration files
16
- #
17
- # Data includes keys defined by JSON Schema Draft 07 as well as some metadata used
18
- # by Bolt to generate documentation. The following keys are used:
19
- #
20
- # :description String A detailed description of the option and what it does. This
21
- # field is used in both documentation and the JSON schemas,
22
- # and should provide as much detail as possible, including
23
- # links to relevant documentation.
24
- #
25
- # :type Class The expected type of a value. These should be Ruby classes,
26
- # as this field is used to perform automatic type validation.
27
- # If an option can accept more than one type, this should be
28
- # an array of types. Boolean values should set :type to
29
- # [TrueClass, FalseClass], as Ruby does not have a single
30
- # Boolean class.
31
- #
32
- # :items Hash A definition hash for items in an array. Similar to values
33
- # for top-level options, items can have a :description, :type,
34
- # or any other key in this list.
35
- #
36
- # :uniqueItems Boolean Whether or not an array should contain only unique items.
37
- #
38
- # :properties Hash A hash where keys are sub-options and values are definitions
39
- # for the sub-option. Similar to values for top-level options,
40
- # properties can have a :description, :type, or any other key
41
- # in this list.
42
- #
43
- # :additionalProperties A variation of the :properties key, where the hash is a
44
- # Hash definition for any properties not specified in :properties.
45
- # This can be used to permit arbitrary sub-options, such as
46
- # logs for the 'log' option.
47
- #
48
- # :propertyNames Hash A hash that defines the properties that an option's property
49
- # names must adhere to.
50
- #
51
- # :required Array An array of properties that are required for options that
52
- # accept Hash values.
53
- #
54
- # :minimum Integer The minimum integer value for an option.
55
- #
56
- # :enum Array An array of values that the option recognizes.
57
- #
58
- # :pattern String A JSON regex pattern that the option's vaue should match.
59
- #
60
- # :format String Requires that a string value matches a format defined by the
61
- # JSON Schema draft.
62
- #
63
- # :_plugin Boolean Whether the option accepts a plugin reference. This is used
64
- # when generating the JSON schemas to determine whether or not
65
- # to include a reference to the _plugin definition. If :_plugin
66
- # is set to true, the script that generates JSON schemas will
67
- # automatically recurse through the :items and :properties keys
68
- # and add plugin references if applicable.
69
- #
70
- # :_example Any An example value for the option. This is used to generate
71
- # reference documentation for configuration files.
72
- #
73
- # :_default Any The documented default value for the option. This is only
74
- # used to generate reference documentation for configuration
75
- # files and is not used by Bolt to actually set default values.
9
+ # Definitions used to validate config options.
10
+ # https://github.com/puppetlabs/bolt/blob/main/schemas/README.md
76
11
  TRANSPORT_OPTIONS = {
77
12
  "basic-auth-only" => {
78
13
  type: [TrueClass, FalseClass],
@@ -81,6 +16,13 @@ module Bolt
81
16
  _default: false,
82
17
  _example: true
83
18
  },
19
+ "bundled-ruby" => {
20
+ description: "Whether to use the Ruby bundled with Bolt packages for local targets.",
21
+ type: [TrueClass, FalseClass],
22
+ _plugin: false,
23
+ _example: true,
24
+ _default: false
25
+ },
84
26
  "cacert" => {
85
27
  type: String,
86
28
  description: "The path to the CA certificate.",
@@ -201,7 +143,8 @@ module Bolt
201
143
  "`task.py`) and the extension is case sensitive. When a target's name is `localhost`, "\
202
144
  "Ruby tasks run with the Bolt Ruby interpreter by default.",
203
145
  additionalProperties: {
204
- type: String
146
+ type: String,
147
+ _plugin: false
205
148
  },
206
149
  propertyNames: {
207
150
  pattern: "^.?[a-zA-Z0-9]+$"
@@ -357,7 +300,7 @@ module Bolt
357
300
  description: "The URL of the host used for API requests.",
358
301
  format: "uri",
359
302
  _plugin: true,
360
- _example: "https://api.example.com:<port>"
303
+ _example: "https://api.example.com:8143"
361
304
  },
362
305
  "shell-command" => {
363
306
  type: String,
@@ -73,6 +73,14 @@ module Bolt
73
73
  %w[ssh-command native-ssh].concat(OPTIONS)
74
74
  end
75
75
 
76
+ def self.schema
77
+ {
78
+ type: Hash,
79
+ properties: self::TRANSPORT_OPTIONS.slice(*(self::OPTIONS + self::NATIVE_OPTIONS)),
80
+ _plugin: true
81
+ }
82
+ end
83
+
76
84
  private def filter(unfiltered)
77
85
  # Because we filter before merging config together it's impossible to
78
86
  # know whether both ssh-command *and* native-ssh will be specified
@@ -87,12 +95,6 @@ module Bolt
87
95
  super
88
96
 
89
97
  if (key_opt = @config['private-key'])
90
- unless key_opt.instance_of?(String) || (key_opt.instance_of?(Hash) && key_opt.include?('key-data'))
91
- raise Bolt::ValidationError,
92
- "private-key option must be a path to a private key file or a Hash containing the 'key-data', "\
93
- "received #{key_opt.class} #{key_opt}"
94
- end
95
-
96
98
  if key_opt.instance_of?(String)
97
99
  @config['private-key'] = File.expand_path(key_opt, @project)
98
100
 
@@ -109,19 +111,6 @@ module Bolt
109
111
  @config['interpreters'] = normalize_interpreters(@config['interpreters'])
110
112
  end
111
113
 
112
- if @config['login-shell'] && !LOGIN_SHELLS.include?(@config['login-shell'])
113
- raise Bolt::ValidationError,
114
- "Unsupported login-shell #{@config['login-shell']}. Supported shells are #{LOGIN_SHELLS.join(', ')}"
115
- end
116
-
117
- %w[encryption-algorithms host-key-algorithms kex-algorithms mac-algorithms run-as-command].each do |opt|
118
- next unless @config.key?(opt)
119
- unless @config[opt].all? { |n| n.is_a?(String) }
120
- raise Bolt::ValidationError,
121
- "#{opt} must be an Array of Strings, received #{@config[opt].inspect}"
122
- end
123
- end
124
-
125
114
  if @config['login-shell'] == 'powershell'
126
115
  %w[tty run-as].each do |key|
127
116
  if @config[key]
@@ -90,6 +90,20 @@ module Bolt
90
90
  end
91
91
  end
92
92
 
93
+ class ParallelFailure < Bolt::Error
94
+ def initialize(results, failed_indices)
95
+ details = {
96
+ 'action' => 'parallelize',
97
+ 'failed_indices' => failed_indices,
98
+ 'results' => results
99
+ }
100
+ message = "Plan aborted: parallel block failed on #{failed_indices.length} target"
101
+ message += "s" unless failed_indices.length == 1
102
+ super(message, 'bolt/parallel-failure', details)
103
+ @error_code = 2
104
+ end
105
+ end
106
+
93
107
  class PlanFailure < Error
94
108
  def initialize(*args)
95
109
  super(*args)
@@ -131,6 +145,16 @@ module Bolt
131
145
  end
132
146
  end
133
147
 
148
+ class InvalidParallelResult < Error
149
+ def initialize(result_str, file, line)
150
+ super("Parallel block returned an invalid result: #{result_str}",
151
+ 'bolt/invalid-plan-result',
152
+ { 'file' => file,
153
+ 'line' => line,
154
+ 'result_string' => result_str })
155
+ end
156
+ end
157
+
134
158
  class ValidationError < Bolt::Error
135
159
  def initialize(msg)
136
160
  super(msg, 'bolt/validation-error')
@@ -17,6 +17,7 @@ require 'bolt/transport/orch'
17
17
  require 'bolt/transport/local'
18
18
  require 'bolt/transport/docker'
19
19
  require 'bolt/transport/remote'
20
+ require 'bolt/yarn'
20
21
 
21
22
  module Bolt
22
23
  TRANSPORTS = {
@@ -29,7 +30,7 @@ module Bolt
29
30
  }.freeze
30
31
 
31
32
  class Executor
32
- attr_reader :noop, :transports
33
+ attr_reader :noop, :transports, :in_parallel
33
34
  attr_accessor :run_as
34
35
 
35
36
  def initialize(concurrency = 1,
@@ -60,6 +61,7 @@ module Bolt
60
61
 
61
62
  @noop = noop
62
63
  @run_as = nil
64
+ @in_parallel = false
63
65
  @pool = if concurrency > 0
64
66
  Concurrent::ThreadPoolExecutor.new(name: 'exec', max_threads: concurrency)
65
67
  else
@@ -84,6 +86,14 @@ module Bolt
84
86
  self
85
87
  end
86
88
 
89
+ def unsubscribe(subscriber, types = nil)
90
+ if types.nil? || types.sort == @subscribers[subscriber]&.sort
91
+ @subscribers.delete(subscriber)
92
+ elsif @subscribers[subscriber].is_a?(Array)
93
+ @subscribers[subscriber] = @subscribers[subscriber] - types
94
+ end
95
+ end
96
+
87
97
  def publish_event(event)
88
98
  @subscribers.each do |subscriber, types|
89
99
  # If types isn't set or if the subscriber is subscribed to
@@ -246,10 +256,10 @@ module Bolt
246
256
  @logger.trace { "Failed to submit analytics event: #{e.message}" }
247
257
  end
248
258
 
249
- def with_node_logging(description, batch)
250
- @logger.info("#{description} on #{batch.map(&:safe_name)}")
259
+ def with_node_logging(description, batch, log_level = :info)
260
+ @logger.send(log_level, "#{description} on #{batch.map(&:safe_name)}")
251
261
  result = yield
252
- @logger.info(result.to_json)
262
+ @logger.send(log_level, result.to_json)
253
263
  result
254
264
  end
255
265
 
@@ -279,32 +289,20 @@ module Bolt
279
289
  end
280
290
  end
281
291
 
282
- def run_task(targets, task, arguments, options = {}, position = [])
292
+ def run_task(targets, task, arguments, options = {}, position = [], log_level = :info)
283
293
  description = options.fetch(:description, "task #{task.name}")
284
294
  log_action(description, targets) do
285
295
  options[:run_as] = run_as if run_as && !options.key?(:run_as)
286
296
  arguments['_task'] = task.name
287
297
 
288
298
  batch_execute(targets) do |transport, batch|
289
- with_node_logging("Running task #{task.name} with '#{arguments.to_json}'", batch) do
299
+ with_node_logging("Running task #{task.name} with '#{arguments.to_json}'", batch, log_level) do
290
300
  transport.batch_task(batch, task, arguments, options, position, &method(:publish_event))
291
301
  end
292
302
  end
293
303
  end
294
304
  end
295
305
 
296
- def run_task_with_minimal_logging(targets, task, arguments, options = {})
297
- description = options.fetch(:description, "task #{task.name}")
298
- log_action(description, targets) do
299
- options[:run_as] = run_as if run_as && !options.key?(:run_as)
300
- arguments['_task'] = task.name
301
-
302
- batch_execute(targets) do |transport, batch|
303
- transport.batch_task(batch, task, arguments, options, [], &method(:publish_event))
304
- end
305
- end
306
- end
307
-
308
306
  def run_task_with(target_mapping, task, options = {}, position = [])
309
307
  targets = target_mapping.keys
310
308
  description = options.fetch(:description, "task #{task.name}")
@@ -359,6 +357,82 @@ module Bolt
359
357
  plan.call_by_name_with_scope(scope, params, true)
360
358
  end
361
359
 
360
+ def create_yarn(scope, block, object, index)
361
+ fiber = Fiber.new do
362
+ # Create the new scope
363
+ newscope = Puppet::Parser::Scope.new(scope.compiler)
364
+ local = Puppet::Parser::Scope::LocalScope.new
365
+
366
+ # Compress the current scopes into a single vars hash to add to the new scope
367
+ current_scope = scope.effective_symtable(true)
368
+ until current_scope.nil?
369
+ current_scope.instance_variable_get(:@symbols)&.each_pair { |k, v| local[k] = v }
370
+ current_scope = current_scope.parent
371
+ end
372
+ newscope.push_ephemerals([local])
373
+
374
+ begin
375
+ result = catch(:return) do
376
+ args = { block.parameters[0][1].to_s => object }
377
+ block.closure.call_by_name_with_scope(newscope, args, true)
378
+ end
379
+
380
+ # If we got a return from the block, get it's value
381
+ # Otherwise the result is the last line from the block
382
+ result = result.value if result.is_a?(Puppet::Pops::Evaluator::Return)
383
+
384
+ # Validate the result is a PlanResult
385
+ unless Puppet::Pops::Types::TypeParser.singleton.parse('Boltlib::PlanResult').instance?(result)
386
+ raise Bolt::InvalidParallelResult.new(result.to_s, *Puppet::Pops::PuppetStack.top_of_stack)
387
+ end
388
+
389
+ result
390
+ rescue Puppet::PreformattedError => e
391
+ if e.cause.is_a?(Bolt::Error)
392
+ e.cause
393
+ else
394
+ raise e
395
+ end
396
+ end
397
+ end
398
+
399
+ Bolt::Yarn.new(fiber, index)
400
+ end
401
+
402
+ def handle_event(event)
403
+ case event[:type]
404
+ when :node_result
405
+ @thread_completed = true
406
+ end
407
+ end
408
+
409
+ def round_robin(skein)
410
+ subscribe(self, [:node_result])
411
+ results = Array.new(skein.length)
412
+ @in_parallel = true
413
+
414
+ until skein.empty?
415
+ @thread_completed = false
416
+ r = nil
417
+
418
+ skein.each do |yarn|
419
+ if yarn.alive?
420
+ r = yarn.resume
421
+ else
422
+ results[yarn.index] = yarn.value
423
+ skein.delete(yarn)
424
+ end
425
+ end
426
+
427
+ next unless r == 'unfinished'
428
+ sleep(0.1) until @thread_completed || skein.empty?
429
+ end
430
+
431
+ @in_parallel = false
432
+ unsubscribe(self, [:node_result])
433
+ results
434
+ end
435
+
362
436
  class TimeoutError < RuntimeError; end
363
437
 
364
438
  def wait_until_available(targets,
@@ -4,13 +4,17 @@ require 'set'
4
4
  require 'bolt/config'
5
5
  require 'bolt/inventory/group'
6
6
  require 'bolt/inventory/inventory'
7
+ require 'bolt/inventory/options'
7
8
  require 'bolt/target'
8
9
  require 'bolt/util'
9
10
  require 'bolt/plugin'
11
+ require 'bolt/validator'
10
12
  require 'yaml'
11
13
 
12
14
  module Bolt
13
15
  class Inventory
16
+ include Bolt::Inventory::Options
17
+
14
18
  ENVIRONMENT_VAR = 'BOLT_INVENTORY'
15
19
 
16
20
  class ValidationError < Bolt::Error
@@ -45,11 +49,25 @@ module Bolt
45
49
  end
46
50
  end
47
51
 
52
+ # Builds the schema used by the validator.
53
+ #
54
+ def self.schema
55
+ schema = {
56
+ type: Hash,
57
+ properties: OPTIONS.map { |opt| [opt, _ref: opt] }.to_h,
58
+ definitions: DEFINITIONS
59
+ }
60
+
61
+ schema[:definitions]['config'][:properties] = Bolt::Config.transport_definitions
62
+ schema
63
+ end
64
+
48
65
  def self.from_config(config, plugins)
49
66
  logger = Bolt::Logger.logger(self)
50
67
 
51
68
  if ENV.include?(ENVIRONMENT_VAR)
52
69
  begin
70
+ source = ENVIRONMENT_VAR
53
71
  data = YAML.safe_load(ENV[ENVIRONMENT_VAR])
54
72
  raise Bolt::ParseError, "Could not parse inventory from $#{ENVIRONMENT_VAR}" unless data.is_a?(Hash)
55
73
  logger.debug("Loaded inventory from environment variable #{ENVIRONMENT_VAR}")
@@ -57,9 +75,11 @@ module Bolt
57
75
  raise Bolt::ParseError, "Could not parse inventory from $#{ENVIRONMENT_VAR}"
58
76
  end
59
77
  elsif config.inventoryfile
78
+ source = config.inventoryfile
60
79
  data = Bolt::Util.read_yaml_hash(config.inventoryfile, 'inventory')
61
80
  logger.debug("Loaded inventory from #{config.inventoryfile}")
62
81
  else
82
+ source = config.default_inventoryfile
63
83
  data = Bolt::Util.read_optional_yaml_hash(config.default_inventoryfile, 'inventory')
64
84
 
65
85
  if config.default_inventoryfile.exist?
@@ -74,6 +94,11 @@ module Bolt
74
94
  t.resolve(plugins) unless t.resolved?
75
95
  end
76
96
 
97
+ Bolt::Validator.new.tap do |validator|
98
+ validator.validate(data, schema, source)
99
+ validator.warnings.each { |warning| logger.warn(warning) }
100
+ end
101
+
77
102
  inventory = create_version(data, config.transport, config.transports, plugins)
78
103
  inventory.validate
79
104
  inventory