bolt 2.40.1 → 3.0.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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +17 -17
  3. data/bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb +25 -0
  4. data/bolt-modules/boltlib/lib/puppet/functions/parallelize.rb +6 -8
  5. data/bolt-modules/boltlib/lib/puppet/functions/wait_until_available.rb +7 -3
  6. data/lib/bolt/analytics.rb +3 -2
  7. data/lib/bolt/applicator.rb +11 -1
  8. data/lib/bolt/bolt_option_parser.rb +3 -113
  9. data/lib/bolt/catalog.rb +10 -29
  10. data/lib/bolt/cli.rb +58 -157
  11. data/lib/bolt/config.rb +62 -239
  12. data/lib/bolt/config/options.rb +58 -97
  13. data/lib/bolt/config/transport/local.rb +1 -0
  14. data/lib/bolt/config/transport/options.rb +8 -1
  15. data/lib/bolt/config/transport/orch.rb +1 -0
  16. data/lib/bolt/executor.rb +15 -5
  17. data/lib/bolt/inventory.rb +3 -2
  18. data/lib/bolt/inventory/group.rb +35 -4
  19. data/lib/bolt/inventory/inventory.rb +1 -1
  20. data/lib/bolt/logger.rb +115 -11
  21. data/lib/bolt/module.rb +10 -2
  22. data/lib/bolt/module_installer.rb +4 -2
  23. data/lib/bolt/module_installer/resolver.rb +65 -12
  24. data/lib/bolt/module_installer/specs/forge_spec.rb +8 -2
  25. data/lib/bolt/module_installer/specs/git_spec.rb +17 -2
  26. data/lib/bolt/outputter/human.rb +9 -5
  27. data/lib/bolt/outputter/json.rb +16 -16
  28. data/lib/bolt/outputter/rainbow.rb +3 -3
  29. data/lib/bolt/pal.rb +93 -14
  30. data/lib/bolt/pal/yaml_plan.rb +8 -2
  31. data/lib/bolt/pal/yaml_plan/evaluator.rb +7 -19
  32. data/lib/bolt/pal/yaml_plan/step.rb +3 -24
  33. data/lib/bolt/pal/yaml_plan/step/upload.rb +2 -2
  34. data/lib/bolt/pal/yaml_plan/transpiler.rb +6 -1
  35. data/lib/bolt/plugin.rb +3 -3
  36. data/lib/bolt/plugin/cache.rb +7 -7
  37. data/lib/bolt/plugin/module.rb +0 -23
  38. data/lib/bolt/plugin/puppet_connect_data.rb +77 -0
  39. data/lib/bolt/plugin/puppetdb.rb +1 -1
  40. data/lib/bolt/project.rb +54 -81
  41. data/lib/bolt/project_manager.rb +4 -3
  42. data/lib/bolt/project_manager/module_migrator.rb +6 -5
  43. data/lib/bolt/rerun.rb +1 -1
  44. data/lib/bolt/shell/bash.rb +1 -1
  45. data/lib/bolt/shell/bash/tmpdir.rb +4 -1
  46. data/lib/bolt/shell/powershell.rb +3 -4
  47. data/lib/bolt/shell/powershell/snippets.rb +9 -149
  48. data/lib/bolt/task.rb +1 -1
  49. data/lib/bolt/transport/docker/connection.rb +2 -2
  50. data/lib/bolt/transport/local.rb +1 -9
  51. data/lib/bolt/transport/orch/connection.rb +1 -1
  52. data/lib/bolt/transport/ssh.rb +1 -2
  53. data/lib/bolt/transport/ssh/connection.rb +1 -1
  54. data/lib/bolt/validator.rb +2 -2
  55. data/lib/bolt/version.rb +1 -1
  56. data/lib/bolt_server/config.rb +1 -1
  57. data/lib/bolt_server/schemas/partials/task.json +1 -1
  58. data/lib/bolt_server/transport_app.rb +3 -2
  59. data/libexec/bolt_catalog +1 -1
  60. data/modules/aggregate/plans/count.pp +21 -0
  61. data/modules/aggregate/plans/targets.pp +21 -0
  62. data/modules/puppet_connect/plans/test_input_data.pp +31 -0
  63. data/modules/puppetdb_fact/plans/init.pp +10 -0
  64. metadata +27 -18
  65. data/modules/aggregate/plans/nodes.pp +0 -36
@@ -63,12 +63,12 @@ module Bolt
63
63
  end
64
64
 
65
65
  def start_spin
66
- return unless @spin
67
- @spin = true
66
+ return unless @spin && @stream.isatty && !@spinning
67
+ @spinning = true
68
68
  @spin_thread = Thread.new do
69
69
  loop do
70
- @stream.print(colorize(:rainbow, @pinwheel.rotate!.first + "\b"))
71
70
  sleep(0.1)
71
+ @stream.print(colorize(:rainbow, @pinwheel.rotate!.first + "\b"))
72
72
  end
73
73
  end
74
74
  end
data/lib/bolt/pal.rb CHANGED
@@ -153,8 +153,10 @@ module Bolt
153
153
  Dir.children(path).select { |name| Puppet::Module.is_module_directory?(name, path) }
154
154
  end
155
155
  if modules.include?(project.name)
156
- Bolt::Logger.warn_once("project shadows module",
157
- "The project '#{project.name}' shadows an existing module of the same name")
156
+ Bolt::Logger.warn_once(
157
+ "project_shadows_module",
158
+ "The project '#{project.name}' shadows an existing module of the same name"
159
+ )
158
160
  end
159
161
  end
160
162
 
@@ -357,19 +359,52 @@ module Bolt
357
359
  Bolt::Task.from_task_signature(task)
358
360
  end
359
361
 
362
+ def list_plans_with_cache(filter_content: false)
363
+ # Don't filter content yet, so that if users update their plan filters
364
+ # we don't need to refresh the cache
365
+ plan_names = list_plans(filter_content: false).map(&:first)
366
+ plan_cache = if @project
367
+ Bolt::Util.read_optional_json_file(@project.plan_cache_file, 'Plan cache file')
368
+ else
369
+ {}
370
+ end
371
+ updated = false
372
+
373
+ plan_list = plan_names.each_with_object([]) do |plan_name, list|
374
+ info = plan_cache[plan_name] || get_plan_info(plan_name, with_mtime: true)
375
+
376
+ # If the plan is a 'local' plan (in the project itself, or the
377
+ # modules/ directory) then verify it hasn't been updated since we
378
+ # cached it. If it has been updated, refresh the cache and use the
379
+ # new data.
380
+ if info['file'] &&
381
+ (File.mtime(info.dig('file', 'path')) <=> info.dig('file', 'mtime')) != 0
382
+ info = get_plan_info(plan_name, with_mtime: true)
383
+ updated = true
384
+ plan_cache[plan_name] = info
385
+ end
386
+
387
+ list << [plan_name] unless info['private']
388
+ end
389
+
390
+ File.write(@project.plan_cache_file, plan_cache.to_json) if updated
391
+
392
+ filter_content ? filter_content(plan_list, @project&.plans) : plan_list
393
+ end
394
+
360
395
  def list_plans(filter_content: false)
361
396
  in_bolt_compiler do |compiler|
362
397
  errors = []
363
398
  plans = compiler.list_plans(nil, errors).map { |plan| [plan.name] }.sort
364
399
  errors.each do |error|
365
- @logger.warn(error.details['original_error'])
400
+ Bolt::Logger.warn("plan_load_error", error.details['original_error'])
366
401
  end
367
402
 
368
403
  filter_content ? filter_content(plans, @project&.plans) : plans
369
404
  end
370
405
  end
371
406
 
372
- def get_plan_info(plan_name)
407
+ def get_plan_info(plan_name, with_mtime: false)
373
408
  plan_sig = in_bolt_compiler do |compiler|
374
409
  compiler.plan_signature(plan_name)
375
410
  end
@@ -412,16 +447,28 @@ module Bolt
412
447
  params[name]['default_value'] = defaults[name] if defaults.key?(name)
413
448
  params[name]['description'] = param.text unless param.text.empty?
414
449
  else
415
- @logger.warn("The documented parameter '#{name}' does not exist in plan signature")
450
+ Bolt::Logger.warn(
451
+ "missing_plan_parameter",
452
+ "The documented parameter '#{name}' does not exist in signature for plan '#{plan.name}'"
453
+ )
416
454
  end
417
455
  end
418
456
 
419
- {
420
- 'name' => plan_name,
457
+ privie = plan.tag(:private)&.text
458
+ unless privie.nil? || %w[true false].include?(privie.downcase)
459
+ msg = "Plan #{plan_name} key 'private' must be a boolean, received: #{privie}"
460
+ raise Bolt::Error.new(msg, 'bolt/invalid-plan')
461
+ end
462
+
463
+ pp_info = {
464
+ 'name' => plan_name,
421
465
  'description' => description,
422
- 'parameters' => parameters,
423
- 'module' => mod
466
+ 'parameters' => parameters,
467
+ 'module' => mod
424
468
  }
469
+ pp_info.merge!({ 'private' => privie&.downcase == 'true' }) unless privie.nil?
470
+ pp_info.merge!(get_plan_mtime(plan.file)) if with_mtime
471
+ pp_info
425
472
 
426
473
  # If it's a YAML plan, fall back to limited data
427
474
  else
@@ -444,12 +491,32 @@ module Bolt
444
491
  params[name]['default_value'] = param.value unless param.value.nil?
445
492
  params[name]['description'] = param.description if param.description
446
493
  end
447
- {
448
- 'name' => plan_name,
494
+
495
+ yaml_info = {
496
+ 'name' => plan_name,
449
497
  'description' => plan.description,
450
- 'parameters' => parameters,
451
- 'module' => mod
498
+ 'parameters' => parameters,
499
+ 'module' => mod
452
500
  }
501
+ yaml_info.merge!({ 'private' => plan.private }) unless plan.private.nil?
502
+ yaml_info.merge!(get_plan_mtime(yaml_path)) if with_mtime
503
+ yaml_info
504
+ end
505
+ end
506
+
507
+ def get_plan_mtime(path)
508
+ # If the plan is from the project modules/ directory, or is in the
509
+ # project itself, include the last mtime of the file so we can compare
510
+ # if the plan has been updated since it was cached.
511
+ if @project &&
512
+ File.exist?(path) &&
513
+ (path.include?(File.join(@project.path, 'modules')) ||
514
+ path.include?(@project.plans_path.to_s))
515
+
516
+ { 'file' => { 'mtime' => File.mtime(path),
517
+ 'path' => path } }
518
+ else
519
+ {}
453
520
  end
454
521
  end
455
522
 
@@ -490,16 +557,28 @@ module Bolt
490
557
  end
491
558
  end
492
559
 
493
- def generate_types
560
+ def generate_types(cache: false)
494
561
  require 'puppet/face/generate'
495
562
  in_bolt_compiler do
496
563
  generator = Puppet::Generate::Type
497
564
  inputs = generator.find_inputs(:pcore)
498
565
  FileUtils.mkdir_p(@resource_types)
566
+ cache_plan_info if @project && cache
499
567
  generator.generate(inputs, @resource_types, true)
500
568
  end
501
569
  end
502
570
 
571
+ def cache_plan_info
572
+ # plan_name is an array here
573
+ plans_info = list_plans(filter_content: false).map do |plan_name,|
574
+ data = get_plan_info(plan_name, with_mtime: true)
575
+ { plan_name => data }
576
+ end.reduce({}, :merge)
577
+
578
+ FileUtils.touch(@project.plan_cache_file)
579
+ File.write(@project.plan_cache_file, plans_info.to_json)
580
+ end
581
+
503
582
  def run_task(task_name, targets, params, executor, inventory, description = nil)
504
583
  in_task_compiler(executor, inventory) do |compiler|
505
584
  params = params.merge('_bolt_api_call' => true, '_catch_errors' => true)
@@ -6,10 +6,10 @@ require 'bolt/pal/yaml_plan/step'
6
6
  module Bolt
7
7
  class PAL
8
8
  class YamlPlan
9
- PLAN_KEYS = Set['parameters', 'steps', 'return', 'version', 'description']
9
+ PLAN_KEYS = Set['parameters', 'private', 'steps', 'return', 'version', 'description']
10
10
  VAR_NAME_PATTERN = /\A[a-z_][a-z0-9_]*\z/.freeze
11
11
 
12
- attr_reader :name, :parameters, :steps, :return, :description
12
+ attr_reader :name, :parameters, :private, :steps, :return, :description
13
13
 
14
14
  def initialize(name, plan)
15
15
  # Top-level plan keys aren't allowed to be Puppet code, so force them
@@ -30,6 +30,12 @@ module Bolt
30
30
  Parameter.new(param, definition)
31
31
  end.freeze
32
32
 
33
+ @private = plan['private']
34
+ unless @private.nil? || @private.is_a?(TrueClass) || @private.is_a?(FalseClass)
35
+ msg = "Plan #{@name} key 'private' must be a boolean, received: #{@private.inspect}"
36
+ raise Bolt::Error.new(msg, "bolt/invalid-plan")
37
+ end
38
+
33
39
  # Validate top level plan keys
34
40
  top_level_keys = plan.keys.to_set
35
41
  unless PLAN_KEYS.superset?(top_level_keys)
@@ -24,7 +24,7 @@ module Bolt
24
24
 
25
25
  def task_step(scope, step)
26
26
  task = step['task']
27
- targets = step['targets'] || step['target']
27
+ targets = step['targets']
28
28
  description = step['description']
29
29
  params = step['parameters'] || {}
30
30
 
@@ -48,7 +48,7 @@ module Bolt
48
48
 
49
49
  def script_step(scope, step)
50
50
  script = step['script']
51
- targets = step['targets'] || step['target']
51
+ targets = step['targets']
52
52
  description = step['description']
53
53
  arguments = step['arguments'] || []
54
54
 
@@ -64,7 +64,7 @@ module Bolt
64
64
 
65
65
  def command_step(scope, step)
66
66
  command = step['command']
67
- targets = step['targets'] || step['target']
67
+ targets = step['targets']
68
68
  description = step['description']
69
69
 
70
70
  args = [command, targets]
@@ -73,9 +73,9 @@ module Bolt
73
73
  end
74
74
 
75
75
  def upload_step(scope, step)
76
- source = step['upload'] || step['source']
76
+ source = step['upload']
77
77
  destination = step['destination']
78
- targets = step['targets'] || step['target']
78
+ targets = step['targets']
79
79
  description = step['description']
80
80
 
81
81
  args = [source, destination, targets]
@@ -86,7 +86,7 @@ module Bolt
86
86
  def download_step(scope, step)
87
87
  source = step['download']
88
88
  destination = step['destination']
89
- targets = step['targets'] || step['target']
89
+ targets = step['targets']
90
90
  description = step['description']
91
91
 
92
92
  args = [source, destination, targets]
@@ -99,7 +99,7 @@ module Bolt
99
99
  end
100
100
 
101
101
  def resources_step(scope, step)
102
- targets = step['targets'] || step['target']
102
+ targets = step['targets']
103
103
 
104
104
  # TODO: Only call apply_prep when needed
105
105
  scope.call_function('apply_prep', targets)
@@ -154,18 +154,6 @@ module Bolt
154
154
  # This is the method that Puppet calls to evaluate the plan. The name
155
155
  # makes more sense for .pp plans.
156
156
  def evaluate_block_with_bindings(closure_scope, args_hash, plan)
157
- if plan.steps.any? { |step| step.body.key?('target') }
158
- msg = "The 'target' parameter for YAML plan steps is deprecated and will be removed "\
159
- "in a future version of Bolt. Use the 'targets' parameter instead."
160
- Bolt::Logger.deprecation_warning("Using 'target' parameter for YAML plan steps, not 'targets'", msg)
161
- end
162
-
163
- if plan.steps.any? { |step| step.body.key?('source') }
164
- msg = "The 'source' parameter for YAML plan upload steps is deprecated and will be removed "\
165
- "in a future version of Bolt. Use the 'upload' parameter instead."
166
- Bolt::Logger.deprecation_warning("Using 'source' parameter for YAML upload steps, not 'upload'", msg)
167
- end
168
-
169
157
  plan_result = closure_scope.with_local_scope(args_hash) do |scope|
170
158
  plan.steps.each do |step|
171
159
  step_result = dispatch_step(scope, step)
@@ -9,19 +9,17 @@ module Bolt
9
9
  attr_reader :name, :type, :body, :targets
10
10
 
11
11
  def self.allowed_keys
12
- Set['name', 'description', 'target', 'targets']
12
+ Set['name', 'description', 'targets']
13
13
  end
14
14
 
15
15
  STEP_KEYS = %w[
16
16
  command
17
- destination
18
17
  download
19
18
  eval
20
19
  message
21
20
  plan
22
21
  resources
23
22
  script
24
- source
25
23
  task
26
24
  upload
27
25
  ].freeze
@@ -34,13 +32,7 @@ module Bolt
34
32
  when 1
35
33
  type = type_keys.first
36
34
  else
37
- if [Set['source', 'destination'], Set['upload', 'destination']].include?(type_keys.to_set)
38
- type = 'upload'
39
- elsif type_keys.to_set == Set['download', 'destination']
40
- type = 'download'
41
- else
42
- raise step_error("Multiple action keys detected: #{type_keys.inspect}", step_body['name'], step_number)
43
- end
35
+ raise step_error("Multiple action keys detected: #{type_keys.inspect}", step_body['name'], step_number)
44
36
  end
45
37
 
46
38
  step_class = const_get("Bolt::PAL::YamlPlan::Step::#{type.capitalize}")
@@ -51,7 +43,7 @@ module Bolt
51
43
  def initialize(step_body)
52
44
  @name = step_body['name']
53
45
  @description = step_body['description']
54
- @targets = step_body['targets'] || step_body['target']
46
+ @targets = step_body['targets']
55
47
  @body = step_body
56
48
  end
57
49
 
@@ -96,19 +88,6 @@ module Bolt
96
88
  # Ensure all required keys are present
97
89
  missing_keys = required_keys - body.keys
98
90
 
99
- # Handle cases where steps with a required 'targets' key are using the deprecated
100
- # 'target' key instead.
101
- # TODO: Remove this when 'target' is removed
102
- if body.include?('target')
103
- missing_keys -= ['targets']
104
- end
105
-
106
- # Handle cases where upload step uses deprecated 'source' key instead of 'upload'
107
- # TODO: Remove when 'source' is removed
108
- if body.include?('source')
109
- missing_keys -= ['upload']
110
- end
111
-
112
91
  if missing_keys.any?
113
92
  error_message = "The #{step_type.inspect} step requires: #{missing_keys.to_a.inspect} key(s)"
114
93
  err = step_error(error_message, body['name'], step_number)
@@ -6,7 +6,7 @@ module Bolt
6
6
  class Step
7
7
  class Upload < Step
8
8
  def self.allowed_keys
9
- super + Set['source', 'destination', 'upload']
9
+ super + Set['destination', 'upload']
10
10
  end
11
11
 
12
12
  def self.required_keys
@@ -15,7 +15,7 @@ module Bolt
15
15
 
16
16
  def initialize(step_body)
17
17
  super
18
- @source = step_body['upload'] || step_body['source']
18
+ @source = step_body['upload']
19
19
  @destination = step_body['destination']
20
20
  end
21
21
 
@@ -30,6 +30,7 @@ module Bolt
30
30
  plan_string = String.new('')
31
31
  plan_string << "# #{plan_object.description}\n" if plan_object.description
32
32
  plan_string << "# WARNING: This is an autogenerated plan. It may not behave as expected.\n"
33
+ plan_string << "# @private #{plan_object.private}\n" unless plan_object.private.nil?
33
34
  plan_string << "#{param_descriptions}\n" unless param_descriptions.empty?
34
35
 
35
36
  plan_string << "plan #{plan_object.name}("
@@ -64,7 +65,11 @@ module Bolt
64
65
  raise Bolt::FileError.new(msg, @plan_path)
65
66
  end
66
67
 
67
- Bolt::PAL::YamlPlan::Loader.from_string(@modulename, file_contents, @plan_path)
68
+ begin
69
+ Bolt::PAL::YamlPlan::Loader.from_string(@modulename, file_contents, @plan_path)
70
+ rescue Puppet::PreformattedError, StandardError => e
71
+ raise PALError.from_preformatted_error(e)
72
+ end
68
73
  end
69
74
 
70
75
  def validate_path
data/lib/bolt/plugin.rb CHANGED
@@ -139,7 +139,7 @@ module Bolt
139
139
  plugins
140
140
  end
141
141
 
142
- RUBY_PLUGINS = %w[task prompt env_var puppetdb].freeze
142
+ RUBY_PLUGINS = %w[task prompt env_var puppetdb puppet_connect_data].freeze
143
143
  BUILTIN_PLUGINS = %w[task terraform pkcs7 prompt vault aws_inventory puppetdb azure_inventory
144
144
  yaml env_var gcloud_inventory].freeze
145
145
  DEFAULT_PLUGIN_HOOKS = { 'puppet_library' => { 'plugin' => 'puppet_agent', 'stop_service' => true } }.freeze
@@ -170,7 +170,7 @@ module Bolt
170
170
  end
171
171
 
172
172
  def modules
173
- @modules ||= Bolt::Module.discover(@pal.full_modulepath)
173
+ @modules ||= Bolt::Module.discover(@pal.full_modulepath, @config.project)
174
174
  end
175
175
 
176
176
  def add_plugin(plugin)
@@ -297,7 +297,7 @@ module Bolt
297
297
  def resolve_single_reference(reference)
298
298
  plugin_cache = if cache?(reference)
299
299
  cache = Bolt::Plugin::Cache.new(reference,
300
- @config.project.cache_file,
300
+ @config.project.plugin_cache_file,
301
301
  @config.plugin_cache)
302
302
  entry = cache.read_and_clean_cache
303
303
  return entry unless entry.nil?
@@ -7,11 +7,11 @@ require 'bolt/util'
7
7
  module Bolt
8
8
  class Plugin
9
9
  class Cache
10
- attr_reader :reference, :cache_file, :default_config, :id
10
+ attr_reader :reference, :plugin_cache_file, :default_config, :id
11
11
 
12
- def initialize(reference, cache_file, default_config)
12
+ def initialize(reference, plugin_cache_file, default_config)
13
13
  @reference = reference
14
- @cache_file = cache_file
14
+ @plugin_cache_file = plugin_cache_file
15
15
  @default_config = default_config
16
16
  end
17
17
 
@@ -32,21 +32,21 @@ module Bolt
32
32
  unmodified = false if expired
33
33
  expired
34
34
  end
35
- File.write(cache_file, cache.to_json) unless cache.empty? || unmodified
35
+ File.write(plugin_cache_file, cache.to_json) unless cache.empty? || unmodified
36
36
 
37
37
  cache.dig(id, 'result')
38
38
  end
39
39
 
40
40
  private def cache
41
- @cache ||= Bolt::Util.read_optional_json_file(@cache_file, 'cache')
41
+ @cache ||= Bolt::Util.read_optional_json_file(@plugin_cache_file, 'cache')
42
42
  end
43
43
 
44
44
  def write_cache(result)
45
45
  cache.merge!({ id => { 'result' => result,
46
46
  'mtime' => Time.now,
47
47
  'ttl' => ttl } })
48
- FileUtils.touch(cache_file)
49
- File.write(cache_file, cache.to_json)
48
+ FileUtils.touch(plugin_cache_file)
49
+ File.write(plugin_cache_file, cache.to_json)
50
50
  end
51
51
 
52
52
  def validate