bolt 2.38.0 → 3.0.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 (66) 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 +54 -155
  11. data/lib/bolt/config.rb +63 -269
  12. data/lib/bolt/config/options.rb +59 -97
  13. data/lib/bolt/config/transport/local.rb +1 -0
  14. data/lib/bolt/config/transport/options.rb +10 -2
  15. data/lib/bolt/config/transport/orch.rb +1 -0
  16. data/lib/bolt/config/transport/ssh.rb +0 -5
  17. data/lib/bolt/executor.rb +15 -5
  18. data/lib/bolt/inventory.rb +3 -2
  19. data/lib/bolt/inventory/group.rb +35 -12
  20. data/lib/bolt/inventory/inventory.rb +1 -1
  21. data/lib/bolt/logger.rb +115 -11
  22. data/lib/bolt/module.rb +10 -2
  23. data/lib/bolt/module_installer.rb +4 -2
  24. data/lib/bolt/module_installer/resolver.rb +65 -12
  25. data/lib/bolt/module_installer/specs/forge_spec.rb +8 -2
  26. data/lib/bolt/module_installer/specs/git_spec.rb +17 -2
  27. data/lib/bolt/outputter/human.rb +9 -5
  28. data/lib/bolt/outputter/json.rb +16 -16
  29. data/lib/bolt/outputter/rainbow.rb +3 -3
  30. data/lib/bolt/pal.rb +93 -14
  31. data/lib/bolt/pal/yaml_plan.rb +8 -2
  32. data/lib/bolt/pal/yaml_plan/evaluator.rb +7 -19
  33. data/lib/bolt/pal/yaml_plan/step.rb +3 -24
  34. data/lib/bolt/pal/yaml_plan/step/upload.rb +2 -2
  35. data/lib/bolt/pal/yaml_plan/transpiler.rb +6 -1
  36. data/lib/bolt/plugin.rb +3 -3
  37. data/lib/bolt/plugin/cache.rb +8 -8
  38. data/lib/bolt/plugin/module.rb +0 -23
  39. data/lib/bolt/plugin/puppet_connect_data.rb +77 -0
  40. data/lib/bolt/plugin/puppetdb.rb +1 -1
  41. data/lib/bolt/project.rb +54 -81
  42. data/lib/bolt/project_manager.rb +4 -3
  43. data/lib/bolt/project_manager/module_migrator.rb +6 -5
  44. data/lib/bolt/rerun.rb +1 -1
  45. data/lib/bolt/shell/bash.rb +1 -1
  46. data/lib/bolt/shell/bash/tmpdir.rb +4 -1
  47. data/lib/bolt/shell/powershell.rb +3 -4
  48. data/lib/bolt/shell/powershell/snippets.rb +9 -149
  49. data/lib/bolt/task.rb +1 -1
  50. data/lib/bolt/transport/docker/connection.rb +2 -2
  51. data/lib/bolt/transport/local.rb +1 -9
  52. data/lib/bolt/transport/orch/connection.rb +1 -1
  53. data/lib/bolt/transport/ssh.rb +1 -2
  54. data/lib/bolt/transport/ssh/connection.rb +1 -1
  55. data/lib/bolt/validator.rb +16 -15
  56. data/lib/bolt/version.rb +1 -1
  57. data/lib/bolt_server/config.rb +1 -1
  58. data/lib/bolt_server/schemas/partials/task.json +1 -1
  59. data/lib/bolt_server/transport_app.rb +3 -2
  60. data/libexec/bolt_catalog +1 -1
  61. data/modules/aggregate/plans/count.pp +21 -0
  62. data/modules/aggregate/plans/targets.pp +21 -0
  63. data/modules/puppet_connect/plans/test_input_data.pp +31 -0
  64. data/modules/puppetdb_fact/plans/init.pp +10 -0
  65. metadata +26 -17
  66. data/modules/aggregate/plans/nodes.pp +0 -36
@@ -32,8 +32,8 @@ module Bolt
32
32
  end
33
33
 
34
34
  def start_spin
35
- return unless @spin
36
- @spin = true
35
+ return unless @spin && @stream.isatty && !@spinning
36
+ @spinning = true
37
37
  @spin_thread = Thread.new do
38
38
  loop do
39
39
  sleep(0.1)
@@ -43,9 +43,9 @@ module Bolt
43
43
  end
44
44
 
45
45
  def stop_spin
46
- return unless @spin
46
+ return unless @spin && @stream.isatty && @spinning
47
+ @spinning = false
47
48
  @spin_thread.terminate
48
- @spin = false
49
49
  @stream.print("\b")
50
50
  end
51
51
 
@@ -81,6 +81,10 @@ module Bolt
81
81
  print_plan_start(event)
82
82
  when :plan_finish
83
83
  print_plan_finish(event)
84
+ when :start_spin
85
+ start_spin
86
+ when :stop_spin
87
+ stop_spin
84
88
  end
85
89
  end
86
90
  end
@@ -388,7 +392,7 @@ module Bolt
388
392
 
389
393
  def print_target_info(targets)
390
394
  @stream.puts ::JSON.pretty_generate(
391
- "targets": targets.map(&:detail)
395
+ targets: targets.map(&:detail)
392
396
  )
393
397
  count = "#{targets.count} target#{'s' unless targets.count == 1}"
394
398
  @stream.puts colorize(:green, count)
@@ -95,38 +95,38 @@ module Bolt
95
95
  end
96
96
 
97
97
  def print_puppetfile_result(success, puppetfile, moduledir)
98
- @stream.puts({ "success": success,
99
- "puppetfile": puppetfile,
100
- "moduledir": moduledir.to_s }.to_json)
98
+ @stream.puts({ success: success,
99
+ puppetfile: puppetfile,
100
+ moduledir: moduledir.to_s }.to_json)
101
101
  end
102
102
 
103
103
  def print_targets(target_list, inventoryfile)
104
104
  @stream.puts ::JSON.pretty_generate(
105
- "inventory": {
106
- "targets": target_list[:inventory].map(&:name),
107
- "count": target_list[:inventory].count,
108
- "file": inventoryfile.to_s
105
+ inventory: {
106
+ targets: target_list[:inventory].map(&:name),
107
+ count: target_list[:inventory].count,
108
+ file: inventoryfile.to_s
109
109
  },
110
- "adhoc": {
111
- "targets": target_list[:adhoc].map(&:name),
112
- "count": target_list[:adhoc].count
110
+ adhoc: {
111
+ targets: target_list[:adhoc].map(&:name),
112
+ count: target_list[:adhoc].count
113
113
  },
114
- "targets": target_list.values.flatten.map(&:name),
115
- "count": target_list.values.flatten.count
114
+ targets: target_list.values.flatten.map(&:name),
115
+ count: target_list.values.flatten.count
116
116
  )
117
117
  end
118
118
 
119
119
  def print_target_info(targets)
120
120
  @stream.puts ::JSON.pretty_generate(
121
- "targets": targets.map(&:detail),
122
- "count": targets.count
121
+ targets: targets.map(&:detail),
122
+ count: targets.count
123
123
  )
124
124
  end
125
125
 
126
126
  def print_groups(groups)
127
127
  count = groups.count
128
- @stream.puts({ "groups": groups,
129
- "count": count }.to_json)
128
+ @stream.puts({ groups: groups,
129
+ count: count }.to_json)
130
130
  end
131
131
 
132
132
  def fatal_error(err)
@@ -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