bolt 3.9.2 → 3.10.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.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b54ca78ebd584ad7e8bd223f086ac1dfc86f6f8bca3e7a7a2aab93a466eebc44
4
- data.tar.gz: f68a3dfe2dad3a15bd9d970860ad629de9fac40882291d6cd7b677bf0da5814e
3
+ metadata.gz: fa1075046a81e0596ff3f0483dc7b7a5913a073dd6e998f66c70687049531e74
4
+ data.tar.gz: 9cdb2d3503e1bd2d84fa7c0fc846e3f4d2293780f648d9d07d549fd50d2b1a51
5
5
  SHA512:
6
- metadata.gz: 879279d0db3506639ed99118623e90435e4f3ee70239aebd43e50d0fb8527eebf1ecfcf4298b6b110fcd34b560f4037cfc44439b2ad9858f67d9dfa1e5ad27e6
7
- data.tar.gz: 36f05f2209c085c607200a857c380114eac7c631a6dd2b98550d27ece0488f3d0c3883693f83c74c3e87387a81e63d446727931084e40d27e6443a9c05eb1c4a
6
+ metadata.gz: 4b2f636ec7f5f038b69cd5c4f82e9fd93e59521035e15fa678cef67501a4e00287e29f40ce1672d2f5db0fa4972cbc26d9763f62480612e30ebcfae404d12a1f
7
+ data.tar.gz: f87b3966ea10cf5a5fe46937e9a47b73d4553924bff00ee578f6c659a70fe82864e2cc78b76a685dfb9e3d4a4e5a3fb7bea136730bf6b5160454e665f4f02ea8
@@ -0,0 +1,28 @@
1
+ TOPIC
2
+ debugging
3
+
4
+ DESCRIPTION
5
+ When Bolt isn't behaving as expected, there are a few helpful commands and
6
+ logs that can help identify common issues. The first place to look is in
7
+ `<PROJECT>/bolt-debug.log`, which contains debug-level logs from the last Bolt
8
+ run. This log file includes where the Bolt project was loaded from, the
9
+ location of any configuration or inventory files that were loaded, and the
10
+ modulepath that modules were loaded from.
11
+
12
+ If you're having issues with loading targets or target configuration, you
13
+ can see the list of resolved Bolt target names by running `bolt inventory
14
+ show` on *nix systems or `Get-BoltInventory` in PowerShell. To see the
15
+ resolved configuration for each target, run the command with the `--detail` or
16
+ `-Detail` options.
17
+
18
+ Lastly, if you're having trouble loading Bolt content you can use `bolt
19
+ module show` on *nix systems or `Get-BoltModule` in PowerShell to see the list
20
+ of loaded modules, including where they were loaded from. You can also use
21
+ `bolt task show` or `Get-BoltTask` to list loaded tasks, and `bolt plan show`
22
+ or `Get-BoltPlan` to list loaded plans.
23
+
24
+ Visit the linked documentation for more in-depth troubleshooting help for
25
+ specific issues.
26
+
27
+ DOCUMENTATION
28
+ https://pup.pt/bolt-troubleshooting
@@ -67,7 +67,7 @@ module Bolt
67
67
  { flags: OPTIONS[:global] + %w[format],
68
68
  banner: GUIDE_HELP }
69
69
  when 'lookup'
70
- { flags: ACTION_OPTS + %w[hiera-config],
70
+ { flags: ACTION_OPTS + %w[hiera-config plan-hierarchy],
71
71
  banner: LOOKUP_HELP }
72
72
  when 'module'
73
73
  case action
@@ -105,6 +105,15 @@ module Bolt
105
105
  { flags: OPTIONS[:global],
106
106
  banner: PLAN_HELP }
107
107
  end
108
+ when 'plugin'
109
+ case action
110
+ when 'show'
111
+ { flags: OPTIONS[:global] + %w[color format modulepath project],
112
+ banner: PLUGIN_SHOW_HELP }
113
+ else
114
+ { flags: OPTIONS[:global],
115
+ banner: PLUGIN_HELP }
116
+ end
108
117
  when 'project'
109
118
  case action
110
119
  when 'init'
@@ -192,6 +201,7 @@ module Bolt
192
201
  module Manage Bolt project modules
193
202
  lookup Look up a value with Hiera
194
203
  plan Convert, create, show, and run Bolt plans
204
+ plugin Show available plugins
195
205
  project Create and migrate Bolt projects
196
206
  script Upload a local script and run it remotely
197
207
  secret Create encryption keys and encrypt and decrypt values
@@ -407,7 +417,7 @@ module Bolt
407
417
  lookup
408
418
 
409
419
  #{colorize(:cyan, 'Usage')}
410
- bolt lookup <key> {--targets TARGETS | --query QUERY | --rerun FILTER}
420
+ bolt lookup <key> {--targets TARGETS | --query QUERY | --rerun FILTER | --plan-hierarchy}
411
421
  [options]
412
422
 
413
423
  #{colorize(:cyan, 'Description')}
@@ -418,6 +428,7 @@ module Bolt
418
428
 
419
429
  #{colorize(:cyan, 'Examples')}
420
430
  bolt lookup password --targets servers
431
+ bolt lookup password --plan-hierarchy variable=value
421
432
  HELP
422
433
 
423
434
  MODULE_HELP = <<~HELP
@@ -608,6 +619,37 @@ module Bolt
608
619
  bolt plan show aggregate::count
609
620
  HELP
610
621
 
622
+ PLUGIN_HELP = <<~HELP
623
+ #{colorize(:cyan, 'Name')}
624
+ plugin
625
+
626
+ #{colorize(:cyan, 'Usage')}
627
+ bolt plugin <action> [options]
628
+
629
+ #{colorize(:cyan, 'Description')}
630
+ Show available plugins.
631
+
632
+ #{colorize(:cyan, 'Documentation')}
633
+ Learn more about Bolt plugins at https://pup.pt/bolt-plugins.
634
+
635
+ #{colorize(:cyan, 'Actions')}
636
+ show Show available plugins
637
+ HELP
638
+
639
+ PLUGIN_SHOW_HELP = <<~HELP
640
+ #{colorize(:cyan, 'Name')}
641
+ show
642
+
643
+ #{colorize(:cyan, 'Usage')}
644
+ bolt plugin show [options]
645
+
646
+ #{colorize(:cyan, 'Description')}
647
+ Show available plugins.
648
+
649
+ #{colorize(:cyan, 'Documentation')}
650
+ Learn more about Bolt plugins at https://pup.pt/bolt-plugins.
651
+ HELP
652
+
611
653
  PROJECT_HELP = <<~HELP
612
654
  #{colorize(:cyan, 'Name')}
613
655
  project
@@ -988,6 +1030,11 @@ module Bolt
988
1030
  @options[:resolve] = resolve
989
1031
  end
990
1032
 
1033
+ separator "\n#{self.class.colorize(:cyan, 'Lookup options')}"
1034
+ define('--plan-hierarchy', 'Look up a value with Hiera in the context of a specific plan.') do |_|
1035
+ @options[:plan_hierarchy] = true
1036
+ end
1037
+
991
1038
  separator "\n#{self.class.colorize(:cyan, 'Plan options')}"
992
1039
  define('--pp', 'Create a new Puppet language plan.') do |_|
993
1040
  @options[:puppet] = true
data/lib/bolt/cli.rb CHANGED
@@ -42,6 +42,7 @@ module Bolt
42
42
  'lookup' => %w[],
43
43
  'module' => %w[add generate-types install show],
44
44
  'plan' => %w[show run convert new],
45
+ 'plugin' => %w[show],
45
46
  'project' => %w[init migrate],
46
47
  'script' => %w[run],
47
48
  'secret' => %w[encrypt decrypt createkeys],
@@ -354,12 +355,18 @@ module Bolt
354
355
  "the project, run '#{command} #{options[:object]}'."
355
356
  end
356
357
 
357
- if options[:subcommand] != 'file' && options[:subcommand] != 'script' &&
358
+ if !%w[file script lookup].include?(options[:subcommand]) &&
358
359
  !options[:leftovers].empty?
359
360
  raise Bolt::CLIError,
360
361
  "Unknown argument(s) #{options[:leftovers].join(', ')}"
361
362
  end
362
363
 
364
+ target_opts = options.keys.select { |opt| TARGETING_OPTIONS.include?(opt) }
365
+ if options[:subcommand] == 'lookup' &&
366
+ target_opts.any? && options[:plan_hierarchy]
367
+ raise Bolt::CLIError, "The 'lookup' command accepts either targeting option OR --plan-hierarchy."
368
+ end
369
+
363
370
  if options[:noop] &&
364
371
  !(options[:subcommand] == 'task' && options[:action] == 'run') && options[:subcommand] != 'apply'
365
372
  raise Bolt::CLIError,
@@ -432,10 +439,11 @@ module Bolt
432
439
  end
433
440
 
434
441
  # Initialize inventory and targets. Errors here are better to catch early.
435
- # options[:target_args] will contain a string/array version of the targetting options this is passed to plans
442
+ # options[:target_args] will contain a string/array version of the targeting options this is passed to plans
436
443
  # options[:targets] will contain a resolved set of Target objects
437
444
  unless %w[guide module project secret].include?(options[:subcommand]) ||
438
- %w[convert new show].include?(options[:action])
445
+ %w[convert new show].include?(options[:action]) ||
446
+ options[:plan_hierarchy]
439
447
  update_targets(options)
440
448
  end
441
449
 
@@ -487,6 +495,8 @@ module Bolt
487
495
  list_groups
488
496
  when 'module'
489
497
  list_modules
498
+ when 'plugin'
499
+ list_plugins
490
500
  end
491
501
  return 0
492
502
  when 'convert'
@@ -516,7 +526,13 @@ module Bolt
516
526
  code = Bolt::ProjectManager.new(config, outputter, pal).migrate
517
527
  end
518
528
  when 'lookup'
519
- code = lookup(options[:object], options[:targets])
529
+ plan_vars = Hash[options[:leftovers].map { |a| a.split('=', 2) }]
530
+ # Validate functions verifies one of these was passed
531
+ if options[:targets]
532
+ code = lookup(options[:object], options[:targets], plan_vars: plan_vars)
533
+ elsif options[:plan_hierarchy]
534
+ code = plan_lookup(options[:object], plan_vars: plan_vars)
535
+ end
520
536
  when 'plan'
521
537
  case options[:action]
522
538
  when 'new'
@@ -630,7 +646,7 @@ module Bolt
630
646
  end
631
647
 
632
648
  def list_tasks
633
- tasks = filter_content(pal.list_tasks(filter_content: true), options[:filter])
649
+ tasks = filter_content(pal.list_tasks_with_cache(filter_content: true), options[:filter])
634
650
  outputter.print_tasks(tasks, pal.user_modulepath)
635
651
  end
636
652
 
@@ -694,10 +710,19 @@ module Bolt
694
710
  outputter.print_groups(inventory.group_names.sort, inventory.source, config.default_inventoryfile)
695
711
  end
696
712
 
713
+ # Looks up a value with Hiera as if in a plan outside an apply block, using
714
+ # provided variable values for interpolations
715
+ #
716
+ def plan_lookup(key, plan_vars: {})
717
+ result = pal.plan_hierarchy_lookup(key, plan_vars: plan_vars)
718
+ outputter.print_plan_lookup(result)
719
+ 0
720
+ end
721
+
697
722
  # Looks up a value with Hiera, using targets as the contexts to perform the
698
- # look ups in.
723
+ # look ups in. This should return the same value as a lookup in an apply block.
699
724
  #
700
- def lookup(key, targets)
725
+ def lookup(key, targets, plan_vars: {})
701
726
  executor = Bolt::Executor.new(
702
727
  config.concurrency,
703
728
  analytics,
@@ -706,7 +731,7 @@ module Bolt
706
731
  config.future
707
732
  )
708
733
 
709
- executor.subscribe(outputter) if options.fetch(:format, 'human') == 'human'
734
+ executor.subscribe(outputter) if config.format == 'human'
710
735
  executor.subscribe(log_outputter)
711
736
  executor.publish_event(type: :plan_start, plan: nil)
712
737
 
@@ -716,7 +741,7 @@ module Bolt
716
741
  targets,
717
742
  inventory,
718
743
  executor,
719
- config.concurrency
744
+ plan_vars: plan_vars
720
745
  )
721
746
  end
722
747
 
@@ -827,6 +852,10 @@ module Bolt
827
852
  outputter.print_module_list(pal.list_modules)
828
853
  end
829
854
 
855
+ def list_plugins
856
+ outputter.print_plugin_list(plugins.list_plugins, pal.user_modulepath)
857
+ end
858
+
830
859
  def generate_types
831
860
  # generate_types will surface a nice error with helpful message if it fails
832
861
  pal.generate_types(cache: true)
@@ -460,6 +460,10 @@ module Bolt
460
460
  @stream.puts(guide)
461
461
  end
462
462
 
463
+ def print_plan_lookup(value)
464
+ @stream.puts(value)
465
+ end
466
+
463
467
  def print_module_list(module_list)
464
468
  module_list.each do |path, modules|
465
469
  if (mod = modules.find { |m| m[:internal_module_group] })
@@ -488,6 +492,34 @@ module Bolt
488
492
  end
489
493
  end
490
494
 
495
+ def print_plugin_list(plugin_list, modulepath)
496
+ info = +''
497
+ length = plugin_list.values.map(&:keys).flatten.map(&:length).max + 4
498
+
499
+ plugin_list.each do |hook, plugins|
500
+ next if plugins.empty?
501
+ next if hook == :validate_resolve_reference
502
+
503
+ info << colorize(:cyan, "#{hook}\n")
504
+
505
+ plugins.each do |name, description|
506
+ info << indent(2, name.ljust(length))
507
+ info << truncate(description, 80 - length) if description
508
+ info << "\n"
509
+ end
510
+
511
+ info << "\n"
512
+ end
513
+
514
+ info << colorize(:cyan, "Modulepath\n")
515
+ info << indent(2, "#{modulepath.join(File::PATH_SEPARATOR)}\n\n")
516
+
517
+ info << colorize(:cyan, "Additional information\n")
518
+ info << indent(2, "For more information about using plugins see https://pup.pt/bolt-plugins")
519
+
520
+ @stream.puts info.chomp
521
+ end
522
+
491
523
  def print_targets(target_list, inventory_source, default_inventory, target_flag)
492
524
  adhoc = colorize(:yellow, "(Not found in inventory file)")
493
525
 
@@ -60,6 +60,11 @@ module Bolt
60
60
  print_table('tasks' => tasks, 'modulepath' => modulepath)
61
61
  end
62
62
 
63
+ def print_plugin_list(plugins, modulepath)
64
+ plugins.delete(:validate_resolve_reference)
65
+ print_table('plugins' => plugins, 'modulepath' => modulepath)
66
+ end
67
+
63
68
  def print_plan_info(plan)
64
69
  path = plan.delete('module')
65
70
  plan['module_dir'] = if path.start_with?(Bolt::Config::Modulepath::MODULES_PATH)
@@ -98,6 +103,10 @@ module Bolt
98
103
  }.to_json)
99
104
  end
100
105
 
106
+ def print_plan_lookup(value)
107
+ @stream.puts(value.to_json)
108
+ end
109
+
101
110
  def print_puppetfile_result(success, puppetfile, moduledir)
102
111
  @stream.puts({ success: success,
103
112
  puppetfile: puppetfile,
data/lib/bolt/pal.rb CHANGED
@@ -163,9 +163,10 @@ module Bolt
163
163
  # Runs a block in a PAL script compiler configured for Bolt. Catches
164
164
  # exceptions thrown by the block and re-raises them ensuring they are
165
165
  # Bolt::Errors since the script compiler block will squash all exceptions.
166
- def in_bolt_compiler
166
+ def in_bolt_compiler(compiler_params: {})
167
167
  # TODO: If we always call this inside a bolt_executor we can remove this here
168
168
  setup
169
+ compiler_params = compiler_params.merge(set_local_facts: false)
169
170
  r = Puppet::Pal.in_tmp_environment('bolt', modulepath: full_modulepath, facts: {}) do |pal|
170
171
  # Only load the project if it a) exists, b) has a name it can be loaded with
171
172
  Puppet.override(bolt_project: @project,
@@ -174,7 +175,7 @@ module Bolt
174
175
  # of modules, it must happen *after* we have overridden
175
176
  # bolt_project or the project will be ignored
176
177
  detect_project_conflict(@project, Puppet.lookup(:environments).get('bolt'))
177
- pal.with_script_compiler(set_local_facts: false) do |compiler|
178
+ pal.with_script_compiler(**compiler_params) do |compiler|
178
179
  alias_types(compiler)
179
180
  register_resource_types(Puppet.lookup(:loaders)) if @resource_types
180
181
  begin
@@ -299,6 +300,49 @@ module Bolt
299
300
  end
300
301
  end
301
302
 
303
+ def list_tasks_with_cache(filter_content: false)
304
+ # Don't filter content yet, so that if users update their task filters
305
+ # we don't need to refresh the cache
306
+ task_names = list_tasks(filter_content: false).map(&:first)
307
+ task_cache = if @project
308
+ Bolt::Util.read_optional_json_file(@project.task_cache_file, 'Task cache file')
309
+ else
310
+ {}
311
+ end
312
+ updated = false
313
+
314
+ task_list = task_names.each_with_object([]) do |task_name, list|
315
+ data = task_cache[task_name] || get_task_info(task_name, with_mtime: true)
316
+
317
+ # Make sure all the keys are strings - if we get data from
318
+ # get_task_info they will be symbols
319
+ data = Bolt::Util.walk_keys(data, &:to_s)
320
+
321
+ # If any files in the task were updated, refresh the cache
322
+ if data['files']&.any?
323
+ # For all the files that are part of the task
324
+ data['files'].each do |f|
325
+ # If any file has been updated since we last cached, update the
326
+ # cache
327
+ next if File.mtime(f['path']).to_s == f['mtime']
328
+ data = get_task_info(task_name, with_mtime: true)
329
+ data = Bolt::Util.walk_keys(data, &:to_s)
330
+ # Tell Bolt to write to the cache file once we're done
331
+ updated = true
332
+ # Update the cache data
333
+ task_cache[task_name] = data
334
+ end
335
+ end
336
+ metadata = data['metadata'] || {}
337
+ # Don't add tasks to the list to return if they are private
338
+ list << [task_name, metadata['description']] unless metadata['private']
339
+ end
340
+
341
+ # Write the cache if any entries were updated
342
+ File.write(@project.task_cache_file, task_cache.to_json) if updated
343
+ filter_content ? filter_content(task_list, @project&.tasks) : task_list
344
+ end
345
+
302
346
  def list_tasks(filter_content: false)
303
347
  in_bolt_compiler do |compiler|
304
348
  tasks = compiler.list_tasks.map(&:name).sort.each_with_object([]) do |task_name, data|
@@ -350,14 +394,20 @@ module Bolt
350
394
  end
351
395
  end
352
396
 
353
- def get_task(task_name)
397
+ def get_task(task_name, with_mtime: false)
354
398
  task = task_signature(task_name)
355
399
 
356
400
  if task.nil?
357
401
  raise Bolt::Error.unknown_task(task_name)
358
402
  end
359
403
 
360
- Bolt::Task.from_task_signature(task)
404
+ task = Bolt::Task.from_task_signature(task)
405
+ task.add_mtimes if with_mtime
406
+ task
407
+ end
408
+
409
+ def get_task_info(task_name, with_mtime: false)
410
+ get_task(task_name, with_mtime: with_mtime).to_h
361
411
  end
362
412
 
363
413
  def list_plans_with_cache(filter_content: false)
@@ -372,20 +422,20 @@ module Bolt
372
422
  updated = false
373
423
 
374
424
  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)
425
+ data = plan_cache[plan_name] || get_plan_info(plan_name, with_mtime: true)
376
426
 
377
427
  # If the plan is a 'local' plan (in the project itself, or the
378
428
  # modules/ directory) then verify it hasn't been updated since we
379
429
  # cached it. If it has been updated, refresh the cache and use the
380
430
  # 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)
431
+ if data['file'] &&
432
+ File.mtime(data.dig('file', 'path')).to_s != data.dig('file', 'mtime')
433
+ data = get_plan_info(plan_name, with_mtime: true)
384
434
  updated = true
385
- plan_cache[plan_name] = info
435
+ plan_cache[plan_name] = data
386
436
  end
387
437
 
388
- list << [plan_name, info['description']] unless info['private']
438
+ list << [plan_name, data['description']] unless data['private']
389
439
  end
390
440
 
391
441
  File.write(@project.plan_cache_file, plan_cache.to_json) if updated
@@ -585,6 +635,7 @@ module Bolt
585
635
  inputs = generator.find_inputs(:pcore)
586
636
  FileUtils.mkdir_p(@resource_types)
587
637
  cache_plan_info if @project && cache
638
+ cache_task_info if @project && cache
588
639
  generator.generate(inputs, @resource_types, true)
589
640
  end
590
641
  end
@@ -600,6 +651,17 @@ module Bolt
600
651
  File.write(@project.plan_cache_file, plans_info.to_json)
601
652
  end
602
653
 
654
+ def cache_task_info
655
+ # task_name is an array here
656
+ tasks_info = list_tasks(filter_content: false).map do |task_name,|
657
+ data = get_task_info(task_name, with_mtime: true)
658
+ { task_name => data }
659
+ end.reduce({}, :merge)
660
+
661
+ FileUtils.touch(@project.task_cache_file)
662
+ File.write(@project.task_cache_file, tasks_info.to_json)
663
+ end
664
+
603
665
  def run_task(task_name, targets, params, executor, inventory, description = nil)
604
666
  in_task_compiler(executor, inventory) do |compiler|
605
667
  params = params.merge('_bolt_api_call' => true, '_catch_errors' => true)
@@ -632,7 +694,21 @@ module Bolt
632
694
  Bolt::PlanResult.new(e, 'failure')
633
695
  end
634
696
 
635
- def lookup(key, targets, inventory, executor, _concurrency)
697
+ def plan_hierarchy_lookup(key, plan_vars: {})
698
+ # Do a lookup with a script compiler, which uses the 'plan_hierarchy' key in
699
+ # Hiera config.
700
+ with_puppet_settings do
701
+ # We want all of the setup and teardown that `in_bolt_compiler` does,
702
+ # but also want to pass keys to the script compiler.
703
+ in_bolt_compiler(compiler_params: { variables: plan_vars }) do |compiler|
704
+ compiler.call_function('lookup', key)
705
+ end
706
+ rescue Puppet::Error => e
707
+ raise PALError.from_error(e)
708
+ end
709
+ end
710
+
711
+ def lookup(key, targets, inventory, executor, plan_vars: {})
636
712
  # Install the puppet-agent package and collect facts. Facts are
637
713
  # automatically added to the targets.
638
714
  in_plan_compiler(executor, inventory, nil) do |compiler|
@@ -654,21 +730,29 @@ module Bolt
654
730
 
655
731
  trusted = Puppet::Context::TrustedInformation.local(node).to_h
656
732
 
733
+ # Separate environment configuration from interpolation data the same
734
+ # way we do when compiling Puppet catalogs.
657
735
  env_conf = {
658
736
  modulepath: @modulepath.full_modulepath,
659
- facts: target.facts,
660
- variables: target.vars
737
+ facts: target.facts
738
+ }
739
+
740
+ interpolations = {
741
+ variables: plan_vars,
742
+ target_variables: target.vars
661
743
  }
662
744
 
663
745
  with_puppet_settings do
664
746
  Puppet::Pal.in_tmp_environment(target.name, **env_conf) do |pal|
665
747
  Puppet.override(overrides) do
666
748
  Puppet.lookup(:pal_current_node).trusted_data = trusted
667
- pal.with_catalog_compiler do |compiler|
749
+ pal.with_catalog_compiler(**interpolations) do |compiler|
668
750
  Bolt::Result.for_lookup(target, key, compiler.call_function('lookup', key))
669
751
  rescue StandardError => e
670
752
  Bolt::Result.from_exception(target, e)
671
753
  end
754
+ rescue Puppet::Error => e
755
+ raise PALError.from_error(e)
672
756
  end
673
757
  end
674
758
  end
data/lib/bolt/plugin.rb CHANGED
@@ -250,6 +250,44 @@ module Bolt
250
250
  end
251
251
  end
252
252
 
253
+ # Loads all plugins and returns a map of plugin names to hooks.
254
+ #
255
+ def list_plugins
256
+ load_all_plugins
257
+
258
+ hooks = KNOWN_HOOKS.map { |hook| [hook, {}] }.to_h
259
+
260
+ @plugins.sort.each do |name, plugin|
261
+ # Don't show the Puppet Connect plugin for now.
262
+ next if name == 'puppet_connect_data'
263
+
264
+ case plugin
265
+ when Bolt::Plugin::Module
266
+ plugin.hook_map.each do |hook, spec|
267
+ next unless hooks.include?(hook)
268
+ hooks[hook][name] = spec['task'].description
269
+ end
270
+ else
271
+ plugin.hook_descriptions.each do |hook, description|
272
+ hooks[hook][name] = description
273
+ end
274
+ end
275
+ end
276
+
277
+ hooks
278
+ end
279
+
280
+ # Loads all plugins available to the project.
281
+ #
282
+ private def load_all_plugins
283
+ modules.each do |name, mod|
284
+ next unless mod.plugin?
285
+ by_name(name)
286
+ end
287
+
288
+ RUBY_PLUGINS.each { |name| by_name(name) }
289
+ end
290
+
253
291
  def puppetdb_client
254
292
  by_name('puppetdb').puppetdb_client
255
293
  end
@@ -10,7 +10,14 @@ module Bolt
10
10
  end
11
11
 
12
12
  def hooks
13
- %i[resolve_reference validate_resolve_reference]
13
+ hook_descriptions.keys
14
+ end
15
+
16
+ def hook_descriptions
17
+ {
18
+ resolve_reference: 'Read values stored in environment variables.',
19
+ validate_resolve_reference: nil
20
+ }
14
21
  end
15
22
 
16
23
  def validate_resolve_reference(opts)
@@ -24,7 +24,7 @@ module Bolt
24
24
  end
25
25
  end
26
26
 
27
- attr_reader :config
27
+ attr_reader :config, :hook_map
28
28
 
29
29
  def initialize(mod:, context:, config:, **_opts)
30
30
  @module = mod
@@ -10,7 +10,14 @@ module Bolt
10
10
  end
11
11
 
12
12
  def hooks
13
- %i[resolve_reference validate_resolve_reference]
13
+ hook_descriptions.keys
14
+ end
15
+
16
+ def hook_descriptions
17
+ {
18
+ resolve_reference: 'Prompt the user for a sensitive value.',
19
+ validate_resolve_reference: nil
20
+ }
14
21
  end
15
22
 
16
23
  def validate_resolve_reference(opts)
@@ -49,7 +49,14 @@ module Bolt
49
49
  end
50
50
 
51
51
  def hooks
52
- %i[resolve_reference validate_resolve_reference]
52
+ hook_descriptions.keys
53
+ end
54
+
55
+ def hook_descriptions
56
+ {
57
+ resolve_reference: nil,
58
+ validate_resolve_reference: nil
59
+ }
53
60
  end
54
61
 
55
62
  def resolve_reference(opts)
@@ -27,7 +27,13 @@ module Bolt
27
27
  end
28
28
 
29
29
  def hooks
30
- [:resolve_reference]
30
+ hook_descriptions.keys
31
+ end
32
+
33
+ def hook_descriptions
34
+ {
35
+ resolve_reference: 'Query PuppetDB for a group of targets.'
36
+ }
31
37
  end
32
38
 
33
39
  def warn_missing_fact(certname, fact)
@@ -4,7 +4,15 @@ module Bolt
4
4
  class Plugin
5
5
  class Task
6
6
  def hooks
7
- %i[validate_resolve_reference puppet_library resolve_reference]
7
+ hook_descriptions.keys
8
+ end
9
+
10
+ def hook_descriptions
11
+ {
12
+ puppet_library: 'Run a task to install the Puppet agent package.',
13
+ resolve_reference: 'Run a task as a plugin.',
14
+ validate_resolve_reference: nil
15
+ }
8
16
  end
9
17
 
10
18
  def name
data/lib/bolt/project.rb CHANGED
@@ -14,7 +14,7 @@ module Bolt
14
14
  attr_reader :path, :data, :inventory_file, :hiera_config,
15
15
  :puppetfile, :rerunfile, :type, :resource_types, :project_file,
16
16
  :downloads, :plans_path, :modulepath, :managed_moduledir,
17
- :backup_dir, :plugin_cache_file, :plan_cache_file
17
+ :backup_dir, :plugin_cache_file, :plan_cache_file, :task_cache_file
18
18
 
19
19
  def self.default_project
20
20
  create_project(File.expand_path(File.join('~', '.puppetlabs', 'bolt')), 'user')
@@ -114,6 +114,7 @@ module Bolt
114
114
  @backup_dir = @path + '.bolt-bak'
115
115
  @plugin_cache_file = @path + '.plugin_cache.json'
116
116
  @plan_cache_file = @path + '.plan_cache.json'
117
+ @task_cache_file = @path + '.task_cache.json'
117
118
  @modulepath = [(@path + 'modules').to_s]
118
119
 
119
120
  if (tc = Bolt::Config::INVENTORY_OPTIONS.keys & data.keys).any?
data/lib/bolt/task.rb CHANGED
@@ -17,6 +17,7 @@ module Bolt
17
17
  remote supports_noop].freeze
18
18
 
19
19
  attr_reader :name, :files, :metadata, :remote
20
+ attr_accessor :mtime
20
21
 
21
22
  # name [String] name of the task
22
23
  # files [Array<Hash>] where each entry includes `name` and `path`
@@ -77,6 +78,12 @@ module Bolt
77
78
  file_map[file_name]['path']
78
79
  end
79
80
 
81
+ def add_mtimes
82
+ @files.each do |f|
83
+ f['mtime'] = File.mtime(f['path']) if File.exist?(f['path'])
84
+ end
85
+ end
86
+
80
87
  def implementations
81
88
  metadata['implementations']
82
89
  end
data/lib/bolt/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bolt
4
- VERSION = '3.9.2'
4
+ VERSION = '3.10.0'
5
5
  end
@@ -286,7 +286,7 @@ module BoltSpec
286
286
  future
287
287
  end
288
288
 
289
- def wait(results, _timeout, **_kwargs)
289
+ def wait(results, **_kwargs)
290
290
  results
291
291
  end
292
292
 
@@ -0,0 +1,214 @@
1
+ #####################################################################
2
+ ###
3
+ ### Bash autocompletion for bolt
4
+ ### include this file in your .bash_profile or .bashrc to use completion
5
+ ### "source bolt_bash_completion.sh"
6
+ ###
7
+ ### Marc Schoechlin ms-github@256bit.org
8
+
9
+ get_json_keys() {
10
+ /opt/puppetlabs/bolt/bin/ruby <<-EOF
11
+ require 'json'
12
+ data = JSON.parse(File.read("${1}"))
13
+ puts data.keys.uniq.sort.join(' ')
14
+ EOF
15
+ }
16
+
17
+ _bolt_complete() {
18
+ local prev
19
+ local cur
20
+ # Get the current word and previous word without colons
21
+ _get_comp_words_by_ref -n : cur
22
+ _get_comp_words_by_ref -n : prev
23
+
24
+ local next=""
25
+ local subcommand=${COMP_WORDS[1]}
26
+ [[ ${#COMP_WORDS[@]} -gt 2 ]] && local action=${COMP_WORDS[2]}
27
+ local context_opts="-m --modulepath --project"
28
+ local global_opts="--clear-cache -h --help --log-level --version"
29
+ local inventory_opts="-q --query --rerun -t --targets"
30
+ local authentication_opts="-u --user -p --password --password-prompt --private-key --host-key-check --no-host-key-check --ssl --no-ssl --ssl-verify --no-ssl-verify"
31
+ local escalation_opts="--run-as --sudo-password --sudo-password-prompt --sudo-executable"
32
+ local run_context_opts="-c --concurrency -i --inventoryfile --save-rerun --no-save-rerun --cleanup --no-cleanup"
33
+ local transport_opts="--transport --connect-timeout --tty --no-tty --native-ssh --no-native-ssh --ssh-command --copy-command"
34
+ local display_opts="--format --color --no-color -v --verbose --no-verbose --trace --stream"
35
+ local action_opts="${inventory_opts} ${context_opts} ${authentication_opts} ${escalation_opts} ${run_context_opts} ${transport_opts} ${display_opts}"
36
+ local apply_opts="--compile-concurrency --hiera-config"
37
+ local file_opts=" -m --modulepath --project --private-key -i --inventoryfile --hiera-config "
38
+
39
+ # If there's only one word and it's "bolt", tab complete the subcommand
40
+ if [ $COMP_CWORD -eq 1 ]; then
41
+ next="apply command file group guide inventory lookup module plan plugin project script secret task"
42
+ fi
43
+
44
+ # Tab complete files for options that accept files. The spaces are important!
45
+ # They make it so `bolt inventory` isn't confused with `--inventoryfile`.
46
+ if [[ $file_opts =~ " ${prev} " ]]; then
47
+ next=$(compgen -f -d "" -- $cur)
48
+ # Handle tab completing enumerable CLI options
49
+ elif [ "$prev" == "--log-level" ]; then
50
+ next="trace debug info warn error fatal any"
51
+ elif [ "$prev" == "--transport" ]; then
52
+ next="docker local lxd pcp podman remote ssh winrm"
53
+ elif [ "$prev" == "--format" ]; then
54
+ next="human json rainbow"
55
+ else
56
+ # Once we have subcommands, tab complete actions
57
+ case $subcommand in
58
+ apply)
59
+ next="${action_opts} ${apply_opts} -e --execute"
60
+ ;;
61
+ command)
62
+ if [ $COMP_CWORD -eq 2 ]; then
63
+ next="run"
64
+ elif [ $action = "run" ]; then
65
+ next="${action_opts} --env-var"
66
+ fi
67
+ ;;
68
+ file)
69
+ if [ $COMP_CWORD -eq 2 ]; then
70
+ next="download upload"
71
+ else
72
+ case $action in
73
+ download) next="${action_opts} --tmpdir";;
74
+ upload) next=$action_opts;;
75
+ esac
76
+ fi
77
+ ;;
78
+ group)
79
+ if [ $COMP_CWORD -eq 2 ]; then
80
+ next="show"
81
+ elif [ $action == 'show' ]; then
82
+ next="--project --format --inventoryfile"
83
+ fi
84
+ ;;
85
+ guide)
86
+ next="--format"
87
+ ;;
88
+ inventory)
89
+ if [ $COMP_CWORD -eq 2 ]; then
90
+ next="show"
91
+ elif [ $action == 'show' ]; then
92
+ next="${inventory_opts} --project --format --inventoryfile --detail"
93
+ fi
94
+ ;;
95
+ lookup)
96
+ next="${action_opts} --hiera-config --plan-hierarchy"
97
+ ;;
98
+ module)
99
+ if [ $COMP_CWORD -eq 2 ]; then
100
+ next="add generate-types install show"
101
+ else
102
+ case $action in
103
+ add | install) next="--project";;
104
+ generate-types) next="${context_opts}";;
105
+ show) next="${context_opts} --filter --format";;
106
+ esac
107
+ fi
108
+ ;;
109
+ plan)
110
+ if [ $COMP_CWORD -eq 2 ]; then
111
+ next="convert new run show"
112
+ elif [[ ($COMP_CWORD -eq 3 || \
113
+ ( $COMP_CWORD -eq 4 && "$cur" == *"::" ) || \
114
+ ( $COMP_CWORD -eq 5 && "$cur" == *"::"* )) &&
115
+ $action != "new" ]]; then
116
+
117
+ if [ -f "${PWD}/.plan_cache.json" ]; then
118
+ # Use Puppet's ruby instead of jq since we know it will be there.
119
+ next=$(get_json_keys "${PWD}/.plan_cache.json")
120
+ elif [ -f "${HOME}/.puppetlabs/bolt/.plan_cache.json" ]; then
121
+ next=$(get_json_keys "${HOME}/.puppetlabs/bolt/.plan_cache.json")
122
+ else
123
+ case $action in
124
+ convert) next="${context_opts}";;
125
+ new) next="--project --pp";;
126
+ run) next="${action_opts} ${apply_opts} --params --tmpdir";;
127
+ show) next="${context_opts} --filter --format";;
128
+ esac
129
+ fi
130
+ else
131
+ case $action in
132
+ convert) next="${context_opts}";;
133
+ new) next="--project --pp";;
134
+ run) next="${action_opts} ${apply_opts} --params --tmpdir";;
135
+ show) next="${context_opts} --filter --format";;
136
+ esac
137
+ fi
138
+ ;;
139
+ plugin)
140
+ if [ $COMP_CWORD -eq 2 ]; then
141
+ next="show"
142
+ else
143
+ next="${context_opts} --format --color --no-color"
144
+ fi
145
+ ;;
146
+ project)
147
+ if [ $COMP_CWORD -eq 2 ]; then
148
+ next="init migrate"
149
+ else
150
+ case $action in
151
+ init) next="--modules";;
152
+ migrate) next="--project --inventoryfile";;
153
+ esac
154
+ fi
155
+ ;;
156
+ script)
157
+ if [ $COMP_CWORD -eq 2 ]; then
158
+ next="run"
159
+ elif [[ $COMP_CWORD -eq 3 && $action == 'run' ]]; then
160
+ # List files and directories
161
+ next=$(compgen -f -d "" -- $cur)
162
+ elif [ $action == 'run' ]; then
163
+ next="${action_opts} --env-var --tmpdir"
164
+ fi
165
+ ;;
166
+ secret)
167
+ if [ $COMP_CWORD -eq 2 ]; then
168
+ next="createkeys encrypt decrypt"
169
+ else
170
+ case $action in
171
+ createkeys) next="${context_opts} --plugin --force";;
172
+ encrypt | decrypt) next="${context_opts} --plugin";;
173
+ esac
174
+ fi
175
+ ;;
176
+ task)
177
+ if [ $COMP_CWORD -eq 2 ]; then
178
+ next="run show"
179
+ elif [[ $COMP_CWORD -eq 3 || \
180
+ ( $COMP_CWORD -eq 4 && "$cur" == *"::" ) || \
181
+ ( $COMP_CWORD -eq 5 && "$cur" == *"::"* ) ]]; then
182
+ if [ -f "${PWD}/.task_cache.json" ]; then
183
+ # Use Puppet's ruby instead of jq since we know it will be there.
184
+ next=$(get_json_keys "${PWD}/.task_cache.json")
185
+ elif [ -f "${HOME}/.puppetlabs/bolt/.task_cache.json" ]; then
186
+ next=$(get_json_keys "${HOME}/.puppetlabs/bolt/.task_cache.json")
187
+ else
188
+ case $action in
189
+ run) next="${action_opts} --env-var --tmpdir";;
190
+ show) next="${context_opts} --filter --format";;
191
+ esac
192
+ fi
193
+ else
194
+ case $action in
195
+ run) next="${action_opts} --env-var --tmpdir";;
196
+ show) next="${context_opts} --filter --format";;
197
+ esac
198
+ fi
199
+ ;;
200
+ esac
201
+ fi
202
+
203
+ # If any of the next options are flags, we've reached the end of the
204
+ # building-a-Bolt-command part and can re-enable file and directory name
205
+ # completion and include Bolt's global flags.
206
+ if [[ "$next" =~ "--" ]]; then
207
+ next+=" ${global_opts}"
208
+ fi
209
+
210
+ COMPREPLY=($(compgen -W "$next" -- $cur))
211
+ __ltrim_colon_completions $cur
212
+ }
213
+
214
+ complete -F _bolt_complete bolt
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bolt
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.9.2
4
+ version: 3.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Puppet
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-06-08 00:00:00.000000000 Z
11
+ date: 2021-06-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: addressable
@@ -453,6 +453,7 @@ files:
453
453
  - bolt-modules/prompt/lib/puppet/functions/prompt/menu.rb
454
454
  - bolt-modules/system/lib/puppet/functions/system/env.rb
455
455
  - exe/bolt
456
+ - guides/debugging.txt
456
457
  - guides/guide.txt
457
458
  - guides/inventory.txt
458
459
  - guides/links.txt
@@ -636,6 +637,7 @@ files:
636
637
  - modules/canary/plans/init.pp
637
638
  - modules/puppet_connect/plans/test_input_data.pp
638
639
  - modules/puppetdb_fact/plans/init.pp
640
+ - resources/bolt_bash_completion.sh
639
641
  homepage: https://github.com/puppetlabs/bolt
640
642
  licenses:
641
643
  - Apache-2.0