bolt 2.42.0 → 2.44.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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +12 -12
  3. data/bolt-modules/boltlib/lib/puppet/functions/parallelize.rb +6 -8
  4. data/lib/bolt/analytics.rb +3 -2
  5. data/lib/bolt/applicator.rb +11 -1
  6. data/lib/bolt/bolt_option_parser.rb +20 -13
  7. data/lib/bolt/catalog.rb +10 -29
  8. data/lib/bolt/cli.rb +22 -32
  9. data/lib/bolt/config.rb +84 -82
  10. data/lib/bolt/config/options.rb +68 -0
  11. data/lib/bolt/config/transport/options.rb +7 -0
  12. data/lib/bolt/config/transport/orch.rb +1 -0
  13. data/lib/bolt/executor.rb +15 -5
  14. data/lib/bolt/inventory.rb +1 -1
  15. data/lib/bolt/inventory/group.rb +7 -4
  16. data/lib/bolt/logger.rb +114 -10
  17. data/lib/bolt/module_installer.rb +4 -2
  18. data/lib/bolt/module_installer/resolver.rb +59 -14
  19. data/lib/bolt/module_installer/specs/forge_spec.rb +8 -2
  20. data/lib/bolt/module_installer/specs/git_spec.rb +17 -2
  21. data/lib/bolt/outputter/human.rb +8 -4
  22. data/lib/bolt/outputter/rainbow.rb +3 -3
  23. data/lib/bolt/pal.rb +93 -14
  24. data/lib/bolt/pal/yaml_plan.rb +8 -2
  25. data/lib/bolt/pal/yaml_plan/evaluator.rb +2 -2
  26. data/lib/bolt/pal/yaml_plan/transpiler.rb +1 -0
  27. data/lib/bolt/plugin.rb +2 -2
  28. data/lib/bolt/plugin/cache.rb +7 -7
  29. data/lib/bolt/plugin/module.rb +1 -1
  30. data/lib/bolt/plugin/puppet_connect_data.rb +35 -0
  31. data/lib/bolt/plugin/puppetdb.rb +1 -1
  32. data/lib/bolt/project.rb +56 -43
  33. data/lib/bolt/rerun.rb +1 -1
  34. data/lib/bolt/shell/bash.rb +1 -1
  35. data/lib/bolt/shell/bash/tmpdir.rb +4 -1
  36. data/lib/bolt/shell/powershell.rb +2 -2
  37. data/lib/bolt/task.rb +1 -1
  38. data/lib/bolt/transport/docker/connection.rb +2 -2
  39. data/lib/bolt/transport/local.rb +1 -1
  40. data/lib/bolt/transport/orch/connection.rb +1 -1
  41. data/lib/bolt/transport/ssh.rb +1 -2
  42. data/lib/bolt/transport/ssh/connection.rb +1 -1
  43. data/lib/bolt/validator.rb +2 -2
  44. data/lib/bolt/version.rb +1 -1
  45. data/lib/bolt_server/config.rb +1 -1
  46. data/lib/bolt_server/transport_app.rb +2 -1
  47. data/libexec/bolt_catalog +1 -1
  48. metadata +9 -8
@@ -13,14 +13,20 @@ module Bolt
13
13
  class ForgeSpec
14
14
  NAME_REGEX = %r{\A[a-zA-Z0-9]+[-/](?<name>[a-z][a-z0-9_]*)\z}.freeze
15
15
  REQUIRED_KEYS = Set.new(%w[name]).freeze
16
- KNOWN_KEYS = Set.new(%w[name version_requirement]).freeze
16
+ KNOWN_KEYS = Set.new(%w[name resolve version_requirement]).freeze
17
17
 
18
- attr_reader :full_name, :name, :semantic_version, :type
18
+ attr_reader :full_name, :name, :resolve, :semantic_version, :type, :version_requirement
19
19
 
20
20
  def initialize(init_hash)
21
+ @resolve = init_hash.key?('resolve') ? init_hash['resolve'] : true
21
22
  @full_name, @name = parse_name(init_hash['name'])
22
23
  @version_requirement, @semantic_version = parse_version_requirement(init_hash['version_requirement'])
23
24
  @type = :forge
25
+
26
+ unless @resolve == true || @resolve == false
27
+ raise Bolt::ValidationError,
28
+ "Option 'resolve' for module spec #{@full_name} must be a Boolean"
29
+ end
24
30
  end
25
31
 
26
32
  def self.implements?(hash)
@@ -13,18 +13,31 @@ module Bolt
13
13
  class GitSpec
14
14
  NAME_REGEX = %r{\A(?:[a-zA-Z0-9]+[-/])?(?<name>[a-z][a-z0-9_]*)\z}.freeze
15
15
  REQUIRED_KEYS = Set.new(%w[git ref]).freeze
16
+ KNOWN_KEYS = Set.new(%w[git name ref resolve]).freeze
16
17
 
17
- attr_reader :git, :ref, :type
18
+ attr_reader :git, :ref, :resolve, :type
18
19
 
19
20
  def initialize(init_hash)
21
+ @resolve = init_hash.key?('resolve') ? init_hash['resolve'] : true
20
22
  @name = parse_name(init_hash['name'])
21
23
  @git, @repo = parse_git(init_hash['git'])
22
24
  @ref = init_hash['ref']
23
25
  @type = :git
26
+
27
+ if @name.nil? && @resolve == false
28
+ raise Bolt::ValidationError,
29
+ "Missing name for Git module specification: #{@git}. Git module specifications "\
30
+ "must include a 'name' key when 'resolve' is false."
31
+ end
32
+
33
+ unless @resolve == true || @resolve == false
34
+ raise Bolt::ValidationError,
35
+ "Option 'resolve' for module spec #{@git} must be a Boolean"
36
+ end
24
37
  end
25
38
 
26
39
  def self.implements?(hash)
27
- REQUIRED_KEYS == hash.keys.to_set
40
+ KNOWN_KEYS.superset?(hash.keys.to_set) && REQUIRED_KEYS.subset?(hash.keys.to_set)
28
41
  end
29
42
 
30
43
  # Parses the name into owner and name segments, and formats the full
@@ -47,6 +60,8 @@ module Bolt
47
60
  # Gets the repo from the git URL.
48
61
  #
49
62
  private def parse_git(git)
63
+ return [git, nil] unless @resolve
64
+
50
65
  repo = if git.start_with?('git@github.com:')
51
66
  git.split('git@github.com:').last.split('.git').first
52
67
  elsif git.start_with?('https://github.com')
@@ -32,8 +32,8 @@ module Bolt
32
32
  end
33
33
 
34
34
  def start_spin
35
- return unless @spin && @stream.isatty
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 && @stream.isatty
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
@@ -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
@@ -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 plan signature"
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)
@@ -157,13 +157,13 @@ module Bolt
157
157
  if plan.steps.any? { |step| step.body.key?('target') }
158
158
  msg = "The 'target' parameter for YAML plan steps is deprecated and will be removed "\
159
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)
160
+ Bolt::Logger.deprecate("yaml_plan_target", msg)
161
161
  end
162
162
 
163
163
  if plan.steps.any? { |step| step.body.key?('source') }
164
164
  msg = "The 'source' parameter for YAML plan upload steps is deprecated and will be removed "\
165
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)
166
+ Bolt::Logger.deprecate("yaml_plan_source", msg)
167
167
  end
168
168
 
169
169
  plan_result = closure_scope.with_local_scope(args_hash) do |scope|
@@ -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}("
@@ -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
@@ -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
@@ -187,7 +187,7 @@ module Bolt
187
187
  if params.key?('private-key') || params.key?('public-key')
188
188
  message = "pkcs7 keys 'private-key' and 'public-key' have been deprecated and will be "\
189
189
  "removed in a future version of Bolt; use 'private_key' and 'public_key' instead."
190
- Bolt::Logger.deprecation_warning('PKCS7 keys using hyphens, not underscores', message)
190
+ Bolt::Logger.deprecate("pkcs7_params", message)
191
191
  end
192
192
 
193
193
  params['private_key'] = params.delete('private-key') if params.key?('private-key')
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bolt
4
+ class Plugin
5
+ class PuppetConnectData
6
+ def initialize(context:, **_opts)
7
+ puppet_connect_data_yaml_path = File.join(context.boltdir, 'puppet_connect_data.yaml')
8
+ @data = Bolt::Util.read_optional_yaml_hash(
9
+ puppet_connect_data_yaml_path,
10
+ 'puppet_connect_data.yaml'
11
+ )
12
+ end
13
+
14
+ def name
15
+ 'puppet_connect_data'
16
+ end
17
+
18
+ def hooks
19
+ %i[resolve_reference validate_resolve_reference]
20
+ end
21
+
22
+ def resolve_reference(opts)
23
+ key = opts['key']
24
+ @data[key]
25
+ end
26
+
27
+ def validate_resolve_reference(opts)
28
+ unless opts['key']
29
+ raise Bolt::ValidationError,
30
+ "puppet_connect_data plugin requires that 'key' be specified"
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -31,7 +31,7 @@ module Bolt
31
31
  end
32
32
 
33
33
  def warn_missing_fact(certname, fact)
34
- @logger.warn("Could not find fact #{fact} for node #{certname}")
34
+ Bolt::Logger.warn("puppetdb_missing_fact", "Could not find fact #{fact} for node #{certname}")
35
35
  end
36
36
 
37
37
  def fact_path(raw_fact)