bolt 3.11.0 → 3.15.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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +2 -2
  3. data/bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb +137 -104
  4. data/bolt-modules/boltlib/lib/puppet/functions/background.rb +2 -1
  5. data/bolt-modules/boltlib/lib/puppet/functions/parallelize.rb +5 -1
  6. data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +13 -0
  7. data/bolt-modules/boltlib/lib/puppet/functions/wait.rb +47 -7
  8. data/bolt-modules/log/lib/puppet/functions/log/debug.rb +39 -0
  9. data/bolt-modules/log/lib/puppet/functions/log/error.rb +40 -0
  10. data/bolt-modules/log/lib/puppet/functions/log/fatal.rb +40 -0
  11. data/bolt-modules/log/lib/puppet/functions/log/info.rb +39 -0
  12. data/bolt-modules/log/lib/puppet/functions/log/trace.rb +39 -0
  13. data/bolt-modules/log/lib/puppet/functions/log/warn.rb +41 -0
  14. data/bolt-modules/out/lib/puppet/functions/out/message.rb +9 -49
  15. data/bolt-modules/out/lib/puppet/functions/out/verbose.rb +35 -0
  16. data/guides/{debugging.txt → debugging.yaml} +5 -6
  17. data/guides/{inventory.txt → inventory.yaml} +6 -7
  18. data/guides/{links.txt → links.yaml} +3 -4
  19. data/guides/{logging.txt → logging.yaml} +5 -6
  20. data/guides/{module.txt → module.yaml} +5 -6
  21. data/guides/{modulepath.txt → modulepath.yaml} +5 -6
  22. data/guides/{project.txt → project.yaml} +6 -7
  23. data/guides/{targets.txt → targets.yaml} +5 -6
  24. data/guides/{transports.txt → transports.yaml} +6 -7
  25. data/lib/bolt/analytics.rb +1 -1
  26. data/lib/bolt/applicator.rb +23 -1
  27. data/lib/bolt/bolt_option_parser.rb +6 -3
  28. data/lib/bolt/cli.rb +34 -14
  29. data/lib/bolt/config/options.rb +2 -2
  30. data/lib/bolt/config/transport/options.rb +12 -0
  31. data/lib/bolt/config/transport/ssh.rb +7 -0
  32. data/lib/bolt/error.rb +3 -3
  33. data/lib/bolt/executor.rb +12 -4
  34. data/lib/bolt/fiber_executor.rb +57 -12
  35. data/lib/bolt/outputter/human.rb +124 -15
  36. data/lib/bolt/outputter/json.rb +5 -5
  37. data/lib/bolt/outputter/logger.rb +6 -0
  38. data/lib/bolt/pal.rb +81 -21
  39. data/lib/bolt/pal/yaml_plan/step.rb +2 -0
  40. data/lib/bolt/pal/yaml_plan/step/message.rb +0 -8
  41. data/lib/bolt/pal/yaml_plan/step/verbose.rb +31 -0
  42. data/lib/bolt/pal/yaml_plan/transpiler.rb +1 -1
  43. data/lib/bolt/plan_future.rb +21 -6
  44. data/lib/bolt/plugin/task.rb +1 -1
  45. data/lib/bolt/transport/ssh/exec_connection.rb +3 -1
  46. data/lib/bolt/util/format.rb +68 -0
  47. data/lib/bolt/version.rb +1 -1
  48. data/lib/bolt_server/schemas/connect-data.json +4 -1
  49. data/lib/bolt_server/schemas/partials/target-ssh.json +4 -0
  50. data/lib/bolt_server/schemas/partials/target-winrm.json +4 -0
  51. data/lib/bolt_server/transport_app.rb +93 -52
  52. data/lib/bolt_spec/bolt_context.rb +9 -0
  53. data/lib/bolt_spec/plans.rb +1 -1
  54. data/lib/bolt_spec/plans/mock_executor.rb +31 -7
  55. data/lib/bolt_spec/plans/publish_stub.rb +4 -4
  56. data/modules/canary/plans/init.pp +1 -1
  57. data/resources/bolt_bash_completion.sh +1 -1
  58. metadata +28 -14
  59. data/guides/guide.txt +0 -17
@@ -78,6 +78,8 @@ module Bolt
78
78
  @disable_depth += 1
79
79
  when :message
80
80
  print_message(event[:message])
81
+ when :verbose
82
+ print_message(event[:message]) if @verbose
81
83
  end
82
84
 
83
85
  if enabled?
@@ -339,7 +341,7 @@ module Bolt
339
341
  info << if task.description
340
342
  indent(2, task.description.chomp)
341
343
  else
342
- indent(2, 'No description')
344
+ indent(2, 'No description available.')
343
345
  end
344
346
  info << "\n\n"
345
347
 
@@ -398,7 +400,7 @@ module Bolt
398
400
  info << if plan['description']
399
401
  indent(2, plan['description'].chomp)
400
402
  else
401
- indent(2, 'No description')
403
+ indent(2, 'No description available.')
402
404
  end
403
405
  info << "\n\n"
404
406
 
@@ -462,13 +464,23 @@ module Bolt
462
464
  end
463
465
 
464
466
  def print_topics(topics)
465
- print_message("Available topics are:")
466
- print_message(topics.join("\n"))
467
- print_message("\nUse 'bolt guide <TOPIC>' to view a specific guide.")
467
+ info = +"#{colorize(:cyan, 'Topics')}\n"
468
+ info << indent(2, topics.join("\n"))
469
+ info << "\n\n#{colorize(:cyan, 'Additional information')}\n"
470
+ info << indent(2, "Use 'bolt guide <TOPIC>' to view a specific guide.")
471
+ @stream.puts info
468
472
  end
469
473
 
470
- def print_guide(guide, _topic)
471
- @stream.puts(guide)
474
+ def print_guide(topic:, guide:, documentation: nil)
475
+ info = +"#{colorize(:cyan, topic)}\n"
476
+ info << indent(2, guide)
477
+
478
+ if documentation
479
+ info << "\n#{colorize(:cyan, 'Documentation')}\n"
480
+ info << indent(2, documentation.join("\n"))
481
+ end
482
+
483
+ @stream.puts info
472
484
  end
473
485
 
474
486
  def print_plan_lookup(value)
@@ -476,15 +488,19 @@ module Bolt
476
488
  end
477
489
 
478
490
  def print_module_list(module_list)
491
+ info = +''
492
+
479
493
  module_list.each do |path, modules|
480
- if (mod = modules.find { |m| m[:internal_module_group] })
481
- @stream.puts(colorize(:cyan, mod[:internal_module_group]))
482
- else
483
- @stream.puts(colorize(:cyan, path))
484
- end
494
+ info << if (mod = modules.find { |m| m[:internal_module_group] })
495
+ colorize(:cyan, mod[:internal_module_group])
496
+ else
497
+ colorize(:cyan, path)
498
+ end
499
+
500
+ info << "\n"
485
501
 
486
502
  if modules.empty?
487
- @stream.puts('(no modules installed)')
503
+ info << '(no modules installed)'
488
504
  else
489
505
  module_info = modules.map do |m|
490
506
  version = if m[:version].nil?
@@ -496,11 +512,87 @@ module Bolt
496
512
  [m[:name], version]
497
513
  end
498
514
 
499
- @stream.puts format_table(module_info, 2, 1)
515
+ info << format_table(module_info, 2, 1).to_s
500
516
  end
501
517
 
502
- @stream.write("\n")
518
+ info << "\n\n"
503
519
  end
520
+
521
+ command = Bolt::Util.powershell? ? 'Get-BoltModule -Name <MODULE>' : 'bolt module show <MODULE>'
522
+ info << colorize(:cyan, "Additional information\n")
523
+ info << indent(2, "Use '#{command}' to view details for a specific module.")
524
+
525
+ @stream.puts info
526
+ end
527
+
528
+ # Prints detailed module information.
529
+ #
530
+ # @param name [String] The module's short name.
531
+ # @param metadata [Hash] The module's metadata.
532
+ # @param path [String] The path to the module.
533
+ # @param plans [Array] The module's plans.
534
+ # @param tasks [Array] The module's tasks.
535
+ #
536
+ def print_module_info(name:, metadata:, path:, plans:, tasks:, **_kwargs)
537
+ info = +''
538
+
539
+ info << colorize(:cyan, name)
540
+
541
+ info << colorize(:dim, " [#{metadata['version']}]") if metadata['version']
542
+ info << "\n"
543
+
544
+ info << if metadata['summary']
545
+ indent(2, wrap(metadata['summary'].strip, 76))
546
+ else
547
+ indent(2, "No description available.\n")
548
+ end
549
+ info << "\n"
550
+
551
+ if tasks.any?
552
+ length = tasks.map(&:first).map(&:length).max
553
+ data = tasks.map { |task, desc| [task, truncate(desc, 76 - length)] }
554
+ info << colorize(:cyan, "Tasks\n")
555
+ info << format_table(data, 2).to_s
556
+ info << "\n\n"
557
+ end
558
+
559
+ if plans.any?
560
+ length = plans.map(&:first).map(&:length).max
561
+ data = plans.map { |plan, desc| [plan, truncate(desc, 76 - length)] }
562
+ info << colorize(:cyan, "Plans\n")
563
+ info << format_table(data, 2).to_s
564
+ info << "\n\n"
565
+ end
566
+
567
+ if metadata['operatingsystem_support']&.any?
568
+ supported = metadata['operatingsystem_support'].map do |os|
569
+ [os['operatingsystem'], os['operatingsystemrelease']&.join(', ')]
570
+ end
571
+
572
+ info << colorize(:cyan, "Operating system support\n")
573
+ info << format_table(supported, 2).to_s
574
+ info << "\n\n"
575
+ end
576
+
577
+ if metadata['dependencies']&.any?
578
+ dependencies = metadata['dependencies'].map do |dep|
579
+ [dep['name'], dep['version_requirement']]
580
+ end
581
+
582
+ info << colorize(:cyan, "Dependencies\n")
583
+ info << format_table(dependencies, 2).to_s
584
+ info << "\n\n"
585
+ end
586
+
587
+ info << colorize(:cyan, "Path\n")
588
+ info << if path.start_with?(Bolt::Config::Modulepath::MODULES_PATH) ||
589
+ path.start_with?(Bolt::Config::Modulepath::BOLTLIB_PATH)
590
+ indent(2, 'built-in module')
591
+ else
592
+ indent(2, path)
593
+ end
594
+
595
+ @stream.puts info
504
596
  end
505
597
 
506
598
  def print_plugin_list(plugin_list, modulepath)
@@ -661,6 +753,12 @@ module Bolt
661
753
  print_container_result(value.result)
662
754
  when Bolt::ResultSet
663
755
  print_result_set(value)
756
+ when Bolt::Result
757
+ print_result(value)
758
+ when Bolt::ApplyResult
759
+ print_apply_result(value)
760
+ when Bolt::Error
761
+ print_bolt_error(**value.to_h.transform_keys(&:to_sym))
664
762
  else
665
763
  @stream.puts(::JSON.pretty_generate(plan_result, quirks_mode: true))
666
764
  end
@@ -700,6 +798,17 @@ module Bolt
700
798
  @stream.puts(colorize(:red, message))
701
799
  end
702
800
 
801
+ def print_bolt_error(msg:, details:, **_kwargs)
802
+ err = msg
803
+ if (f = details[:file])
804
+ err += "\n (file: #{f}"
805
+ err += ", line: #{details[:line]}" if details[:line]
806
+ err += ", column: #{details[:column]}" if details[:column]
807
+ err += ")"
808
+ end
809
+ @stream.puts(colorize(:red, err))
810
+ end
811
+
703
812
  def print_prompt(prompt)
704
813
  @stream.print(colorize(:cyan, indent(4, prompt)))
705
814
  end
@@ -23,6 +23,8 @@ module Bolt
23
23
  print_result(event[:result])
24
24
  when :message
25
25
  print_message(event[:message])
26
+ when :verbose
27
+ print_message(event[:message]) if @verbose
26
28
  end
27
29
  end
28
30
 
@@ -45,6 +47,7 @@ module Bolt
45
47
  @stream.puts results.to_json
46
48
  end
47
49
  alias print_module_list print_table
50
+ alias print_module_info print_table
48
51
 
49
52
  def print_task_info(task)
50
53
  path = task.files.first['path'].chomp("/tasks/#{task.files.first['name']}")
@@ -96,11 +99,8 @@ module Bolt
96
99
  print_table('topics' => topics)
97
100
  end
98
101
 
99
- def print_guide(guide, topic)
100
- @stream.puts({
101
- 'topic' => topic,
102
- 'guide' => guide
103
- }.to_json)
102
+ def print_guide(**kwargs)
103
+ @stream.puts(kwargs.to_json)
104
104
  end
105
105
 
106
106
  def print_plan_lookup(value)
@@ -24,6 +24,8 @@ module Bolt
24
24
  log_container_start(event)
25
25
  when :container_finish
26
26
  log_container_finish(event)
27
+ when :log, :message, :verbose
28
+ log_message(**event)
27
29
  end
28
30
  end
29
31
 
@@ -65,6 +67,10 @@ module Bolt
65
67
  @logger.info("Finished: run container '#{result.object}' failed.")
66
68
  end
67
69
  end
70
+
71
+ def log_message(level:, message:, **_kwargs)
72
+ @logger.send(level, message)
73
+ end
68
74
  end
69
75
  end
70
76
  end
data/lib/bolt/pal.rb CHANGED
@@ -324,7 +324,7 @@ module Bolt
324
324
  data['files'].each do |f|
325
325
  # If any file has been updated since we last cached, update the
326
326
  # cache
327
- next if File.mtime(f['path']).to_s == f['mtime']
327
+ next unless file_modified?(f['path'], f['mtime'])
328
328
  data = get_task_info(task_name, with_mtime: true)
329
329
  data = Bolt::Util.walk_keys(data, &:to_s)
330
330
  # Tell Bolt to write to the cache file once we're done
@@ -339,7 +339,7 @@ module Bolt
339
339
  end
340
340
 
341
341
  # Write the cache if any entries were updated
342
- File.write(@project.task_cache_file, task_cache.to_json) if updated
342
+ File.write(@project.task_cache_file, task_cache.to_json) if updated && @project
343
343
  filter_content ? filter_content(task_list, @project&.tasks) : task_list
344
344
  end
345
345
 
@@ -423,13 +423,10 @@ module Bolt
423
423
 
424
424
  plan_list = plan_names.each_with_object([]) do |plan_name, list|
425
425
  data = plan_cache[plan_name] || get_plan_info(plan_name, with_mtime: true)
426
-
427
- # If the plan is a 'local' plan (in the project itself, or the
428
- # modules/ directory) then verify it hasn't been updated since we
429
- # cached it. If it has been updated, refresh the cache and use the
430
- # new data.
431
- if data['file'] &&
432
- File.mtime(data.dig('file', 'path')).to_s != data.dig('file', 'mtime')
426
+ # If the plan is a 'local' plan (in the project itself, or the modules/
427
+ # directory) then verify it hasn't been updated since we cached it. If
428
+ # it has been updated, refresh the cache and use the new data.
429
+ if file_modified?(data.dig('file', 'path'), data.dig('file', 'mtime'))
433
430
  data = get_plan_info(plan_name, with_mtime: true)
434
431
  updated = true
435
432
  plan_cache[plan_name] = data
@@ -438,11 +435,21 @@ module Bolt
438
435
  list << [plan_name, data['description']] unless data['private']
439
436
  end
440
437
 
441
- File.write(@project.plan_cache_file, plan_cache.to_json) if updated
438
+ File.write(@project.plan_cache_file, plan_cache.to_json) if updated && @project
442
439
 
443
440
  filter_content ? filter_content(plan_list, @project&.plans) : plan_list
444
441
  end
445
442
 
443
+ # Returns true if a file has been modified or no longer exists, false
444
+ # otherwise.
445
+ #
446
+ # @param path [String] The path to the file.
447
+ # @param mtime [String] The last time the file was modified.
448
+ #
449
+ private def file_modified?(path, mtime)
450
+ path && !(File.exist?(path) && File.mtime(path).to_s == mtime.to_s)
451
+ end
452
+
446
453
  def list_plans(filter_content: false)
447
454
  in_bolt_compiler do |compiler|
448
455
  errors = []
@@ -505,19 +512,14 @@ module Bolt
505
512
  end
506
513
  end
507
514
 
508
- privie = plan.tag(:private)&.text
509
- unless privie.nil? || %w[true false].include?(privie.downcase)
510
- msg = "Plan #{plan_name} key 'private' must be a boolean, received: #{privie}"
511
- raise Bolt::Error.new(msg, 'bolt/invalid-plan')
512
- end
513
-
514
515
  pp_info = {
515
516
  'name' => plan_name,
516
517
  'description' => description,
517
518
  'parameters' => parameters,
518
- 'module' => mod
519
+ 'module' => mod,
520
+ 'private' => private_plan?(plan)
519
521
  }
520
- pp_info.merge!({ 'private' => privie&.downcase == 'true' }) unless privie.nil?
522
+
521
523
  pp_info.merge!(get_plan_mtime(plan.file)) if with_mtime
522
524
  pp_info
523
525
 
@@ -547,14 +549,39 @@ module Bolt
547
549
  'name' => plan_name,
548
550
  'description' => plan.description,
549
551
  'parameters' => parameters,
550
- 'module' => mod
552
+ 'module' => mod,
553
+ 'private' => !!plan.private
551
554
  }
552
- yaml_info.merge!({ 'private' => plan.private }) unless plan.private.nil?
555
+
553
556
  yaml_info.merge!(get_plan_mtime(yaml_path)) if with_mtime
554
557
  yaml_info
555
558
  end
556
559
  end
557
560
 
561
+ # Returns true if the plan is private, false otherwise.
562
+ #
563
+ # @param plan [PuppetStrings::Yard::CodeObjects::Plan] The puppet-strings plan documentation.
564
+ # @return [Boolean]
565
+ #
566
+ private def private_plan?(plan)
567
+ if plan.tag(:private)
568
+ value = plan.tag(:private).text
569
+ api_value = value.downcase == 'true' ? 'private' : 'public'
570
+
571
+ Bolt::Logger.deprecate(
572
+ 'plan_private_tag',
573
+ "Tag '@private #{value}' in plan '#{plan.name}' is deprecated, use '@api #{api_value}' instead"
574
+ )
575
+
576
+ unless %w[true false].include?(plan.tag(:private).text.downcase)
577
+ msg = "Value for '@private' tag in plan '#{plan.name}' must be a boolean, received: #{value}"
578
+ raise Bolt::Error.new(msg, 'bolt/invalid-plan')
579
+ end
580
+ end
581
+
582
+ plan.tag(:api).text == 'private' || plan.tag(:private)&.text&.downcase == 'true'
583
+ end
584
+
558
585
  def get_plan_mtime(path)
559
586
  # If the plan is from the project modules/ directory, or is in the
560
587
  # project itself, include the last mtime of the file so we can compare
@@ -628,6 +655,36 @@ module Bolt
628
655
  end
629
656
  end
630
657
 
658
+ # Return information about a module.
659
+ #
660
+ # @param name [String] The name of the module.
661
+ # @return [Hash]
662
+ #
663
+ def show_module(name)
664
+ name = name.tr('-', '/')
665
+
666
+ data = in_bolt_compiler do |_compiler|
667
+ mod = Puppet.lookup(:current_environment).module(name.split(%r{[/-]}, 2).last)
668
+
669
+ unless mod && (mod.forge_name == name || mod.name == name)
670
+ raise Bolt::Error.new("Could not find module '#{name}' on the modulepath.", 'bolt/unknown-module')
671
+ end
672
+
673
+ {
674
+ name: mod.forge_name || mod.name,
675
+ metadata: mod.metadata,
676
+ path: mod.path,
677
+ plans: mod.plans.map(&:name).sort,
678
+ tasks: mod.tasks.map(&:name).sort
679
+ }
680
+ end
681
+
682
+ data[:plans] = list_plans_with_cache.to_h.slice(*data[:plans]).to_a
683
+ data[:tasks] = list_tasks_with_cache.to_h.slice(*data[:tasks]).to_a
684
+
685
+ data
686
+ end
687
+
631
688
  def generate_types(cache: false)
632
689
  require 'puppet/face/generate'
633
690
  in_bolt_compiler do
@@ -676,7 +733,10 @@ module Bolt
676
733
  # Create a Fiber for the main plan. This will be run along with any
677
734
  # other Fibers created during the plan run in the round_robin, with the
678
735
  # main plan always taking precedence in being resumed.
679
- future = executor.create_future(name: plan_name) do |_scope|
736
+ #
737
+ # Every future except for the main plan needs to have a plan id in
738
+ # order to be tracked for the `wait()` function with no arguments.
739
+ future = executor.create_future(name: plan_name, plan_id: 1) do |_scope|
680
740
  r = compiler.call_function('run_plan', plan_name, params.merge('_bolt_api_call' => true))
681
741
  Bolt::PlanResult.from_pcore(r, 'success')
682
742
  rescue Bolt::Error => e
@@ -13,6 +13,7 @@ module Bolt
13
13
  download
14
14
  eval
15
15
  message
16
+ verbose
16
17
  plan
17
18
  resources
18
19
  script
@@ -219,3 +220,4 @@ require 'bolt/pal/yaml_plan/step/task'
219
220
  require 'bolt/pal/yaml_plan/step/upload'
220
221
  require 'bolt/pal/yaml_plan/step/download'
221
222
  require 'bolt/pal/yaml_plan/step/message'
223
+ require 'bolt/pal/yaml_plan/step/verbose'
@@ -24,14 +24,6 @@ module Bolt
24
24
  private def function
25
25
  'out::message'
26
26
  end
27
-
28
- # Transpiles the step into the plan language
29
- #
30
- def transpile
31
- code = String.new(" ")
32
- code << function_call(function, format_args(body))
33
- code << "\n"
34
- end
35
27
  end
36
28
  end
37
29
  end