bolt 2.40.2 → 3.1.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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +19 -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 +54 -155
  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 +94 -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/result.rb +6 -1
  45. data/lib/bolt/shell/bash.rb +9 -4
  46. data/lib/bolt/shell/bash/tmpdir.rb +4 -1
  47. data/lib/bolt/shell/powershell.rb +9 -5
  48. data/lib/bolt/shell/powershell/snippets.rb +37 -150
  49. data/lib/bolt/task.rb +1 -1
  50. data/lib/bolt/transport/base.rb +0 -9
  51. data/lib/bolt/transport/docker.rb +1 -125
  52. data/lib/bolt/transport/docker/connection.rb +86 -161
  53. data/lib/bolt/transport/local.rb +1 -9
  54. data/lib/bolt/transport/orch/connection.rb +1 -1
  55. data/lib/bolt/transport/ssh.rb +1 -2
  56. data/lib/bolt/transport/ssh/connection.rb +1 -1
  57. data/lib/bolt/validator.rb +2 -2
  58. data/lib/bolt/version.rb +1 -1
  59. data/lib/bolt_server/config.rb +1 -1
  60. data/lib/bolt_server/transport_app.rb +48 -31
  61. data/lib/bolt_spec/bolt_context.rb +9 -4
  62. data/lib/bolt_spec/plans.rb +1 -109
  63. data/libexec/bolt_catalog +1 -1
  64. data/modules/aggregate/plans/count.pp +21 -0
  65. data/modules/aggregate/plans/targets.pp +21 -0
  66. data/modules/puppet_connect/plans/test_input_data.pp +67 -0
  67. data/modules/puppetdb_fact/plans/init.pp +10 -0
  68. metadata +28 -19
  69. 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 && @stream.isatty
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
 
@@ -213,6 +215,7 @@ module Bolt
213
215
  def with_bolt_executor(executor, inventory, pdb_client = nil, applicator = nil, &block)
214
216
  setup
215
217
  opts = {
218
+ bolt_project: @project,
216
219
  bolt_executor: executor,
217
220
  bolt_inventory: inventory,
218
221
  bolt_pdb_client: pdb_client,
@@ -357,19 +360,52 @@ module Bolt
357
360
  Bolt::Task.from_task_signature(task)
358
361
  end
359
362
 
363
+ def list_plans_with_cache(filter_content: false)
364
+ # Don't filter content yet, so that if users update their plan filters
365
+ # we don't need to refresh the cache
366
+ plan_names = list_plans(filter_content: false).map(&:first)
367
+ plan_cache = if @project
368
+ Bolt::Util.read_optional_json_file(@project.plan_cache_file, 'Plan cache file')
369
+ else
370
+ {}
371
+ end
372
+ updated = false
373
+
374
+ 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)
376
+
377
+ # If the plan is a 'local' plan (in the project itself, or the
378
+ # modules/ directory) then verify it hasn't been updated since we
379
+ # cached it. If it has been updated, refresh the cache and use the
380
+ # 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)
384
+ updated = true
385
+ plan_cache[plan_name] = info
386
+ end
387
+
388
+ list << [plan_name] unless info['private']
389
+ end
390
+
391
+ File.write(@project.plan_cache_file, plan_cache.to_json) if updated
392
+
393
+ filter_content ? filter_content(plan_list, @project&.plans) : plan_list
394
+ end
395
+
360
396
  def list_plans(filter_content: false)
361
397
  in_bolt_compiler do |compiler|
362
398
  errors = []
363
399
  plans = compiler.list_plans(nil, errors).map { |plan| [plan.name] }.sort
364
400
  errors.each do |error|
365
- @logger.warn(error.details['original_error'])
401
+ Bolt::Logger.warn("plan_load_error", error.details['original_error'])
366
402
  end
367
403
 
368
404
  filter_content ? filter_content(plans, @project&.plans) : plans
369
405
  end
370
406
  end
371
407
 
372
- def get_plan_info(plan_name)
408
+ def get_plan_info(plan_name, with_mtime: false)
373
409
  plan_sig = in_bolt_compiler do |compiler|
374
410
  compiler.plan_signature(plan_name)
375
411
  end
@@ -412,16 +448,28 @@ module Bolt
412
448
  params[name]['default_value'] = defaults[name] if defaults.key?(name)
413
449
  params[name]['description'] = param.text unless param.text.empty?
414
450
  else
415
- @logger.warn("The documented parameter '#{name}' does not exist in plan signature")
451
+ Bolt::Logger.warn(
452
+ "missing_plan_parameter",
453
+ "The documented parameter '#{name}' does not exist in signature for plan '#{plan.name}'"
454
+ )
416
455
  end
417
456
  end
418
457
 
419
- {
420
- 'name' => plan_name,
458
+ privie = plan.tag(:private)&.text
459
+ unless privie.nil? || %w[true false].include?(privie.downcase)
460
+ msg = "Plan #{plan_name} key 'private' must be a boolean, received: #{privie}"
461
+ raise Bolt::Error.new(msg, 'bolt/invalid-plan')
462
+ end
463
+
464
+ pp_info = {
465
+ 'name' => plan_name,
421
466
  'description' => description,
422
- 'parameters' => parameters,
423
- 'module' => mod
467
+ 'parameters' => parameters,
468
+ 'module' => mod
424
469
  }
470
+ pp_info.merge!({ 'private' => privie&.downcase == 'true' }) unless privie.nil?
471
+ pp_info.merge!(get_plan_mtime(plan.file)) if with_mtime
472
+ pp_info
425
473
 
426
474
  # If it's a YAML plan, fall back to limited data
427
475
  else
@@ -444,12 +492,32 @@ module Bolt
444
492
  params[name]['default_value'] = param.value unless param.value.nil?
445
493
  params[name]['description'] = param.description if param.description
446
494
  end
447
- {
448
- 'name' => plan_name,
495
+
496
+ yaml_info = {
497
+ 'name' => plan_name,
449
498
  'description' => plan.description,
450
- 'parameters' => parameters,
451
- 'module' => mod
499
+ 'parameters' => parameters,
500
+ 'module' => mod
452
501
  }
502
+ yaml_info.merge!({ 'private' => plan.private }) unless plan.private.nil?
503
+ yaml_info.merge!(get_plan_mtime(yaml_path)) if with_mtime
504
+ yaml_info
505
+ end
506
+ end
507
+
508
+ def get_plan_mtime(path)
509
+ # If the plan is from the project modules/ directory, or is in the
510
+ # project itself, include the last mtime of the file so we can compare
511
+ # if the plan has been updated since it was cached.
512
+ if @project &&
513
+ File.exist?(path) &&
514
+ (path.include?(File.join(@project.path, 'modules')) ||
515
+ path.include?(@project.plans_path.to_s))
516
+
517
+ { 'file' => { 'mtime' => File.mtime(path),
518
+ 'path' => path } }
519
+ else
520
+ {}
453
521
  end
454
522
  end
455
523
 
@@ -490,16 +558,28 @@ module Bolt
490
558
  end
491
559
  end
492
560
 
493
- def generate_types
561
+ def generate_types(cache: false)
494
562
  require 'puppet/face/generate'
495
563
  in_bolt_compiler do
496
564
  generator = Puppet::Generate::Type
497
565
  inputs = generator.find_inputs(:pcore)
498
566
  FileUtils.mkdir_p(@resource_types)
567
+ cache_plan_info if @project && cache
499
568
  generator.generate(inputs, @resource_types, true)
500
569
  end
501
570
  end
502
571
 
572
+ def cache_plan_info
573
+ # plan_name is an array here
574
+ plans_info = list_plans(filter_content: false).map do |plan_name,|
575
+ data = get_plan_info(plan_name, with_mtime: true)
576
+ { plan_name => data }
577
+ end.reduce({}, :merge)
578
+
579
+ FileUtils.touch(@project.plan_cache_file)
580
+ File.write(@project.plan_cache_file, plans_info.to_json)
581
+ end
582
+
503
583
  def run_task(task_name, targets, params, executor, inventory, description = nil)
504
584
  in_task_compiler(executor, inventory) do |compiler|
505
585
  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