bolt 2.20.0 → 2.24.1

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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +3 -1
  3. data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +6 -0
  4. data/bolt-modules/ctrl/lib/puppet/functions/ctrl/do_until.rb +12 -6
  5. data/bolt-modules/dir/lib/puppet/functions/dir/children.rb +35 -0
  6. data/bolt-modules/out/lib/puppet/functions/out/message.rb +1 -1
  7. data/exe/bolt +1 -0
  8. data/guides/inventory.txt +19 -0
  9. data/guides/project.txt +22 -0
  10. data/lib/bolt/analytics.rb +5 -5
  11. data/lib/bolt/applicator.rb +4 -3
  12. data/lib/bolt/bolt_option_parser.rb +75 -25
  13. data/lib/bolt/catalog.rb +9 -1
  14. data/lib/bolt/cli.rb +226 -73
  15. data/lib/bolt/config.rb +7 -0
  16. data/lib/bolt/config/options.rb +4 -4
  17. data/lib/bolt/executor.rb +16 -8
  18. data/lib/bolt/inventory/group.rb +3 -3
  19. data/lib/bolt/logger.rb +3 -4
  20. data/lib/bolt/module.rb +2 -1
  21. data/lib/bolt/outputter.rb +56 -0
  22. data/lib/bolt/outputter/human.rb +10 -9
  23. data/lib/bolt/outputter/json.rb +11 -4
  24. data/lib/bolt/outputter/logger.rb +2 -2
  25. data/lib/bolt/outputter/rainbow.rb +15 -0
  26. data/lib/bolt/pal.rb +5 -9
  27. data/lib/bolt/pal/yaml_plan/evaluator.rb +4 -0
  28. data/lib/bolt/pal/yaml_plan/step.rb +14 -1
  29. data/lib/bolt/pal/yaml_plan/step/message.rb +30 -0
  30. data/lib/bolt/pal/yaml_plan/transpiler.rb +11 -3
  31. data/lib/bolt/plugin/prompt.rb +3 -3
  32. data/lib/bolt/project.rb +6 -4
  33. data/lib/bolt/project_migrate.rb +138 -0
  34. data/lib/bolt/shell/bash.rb +7 -7
  35. data/lib/bolt/transport/docker/connection.rb +9 -9
  36. data/lib/bolt/transport/local/connection.rb +2 -2
  37. data/lib/bolt/transport/orch.rb +3 -3
  38. data/lib/bolt/transport/ssh/connection.rb +5 -5
  39. data/lib/bolt/transport/ssh/exec_connection.rb +4 -4
  40. data/lib/bolt/transport/winrm/connection.rb +17 -8
  41. data/lib/bolt/util.rb +1 -1
  42. data/lib/bolt/util/puppet_log_level.rb +4 -3
  43. data/lib/bolt/version.rb +1 -1
  44. data/lib/bolt_server/base_config.rb +1 -1
  45. data/lib/bolt_server/pe/pal.rb +1 -1
  46. data/lib/bolt_server/transport_app.rb +76 -0
  47. data/lib/bolt_spec/plans.rb +1 -1
  48. data/lib/bolt_spec/plans/action_stubs.rb +1 -1
  49. data/libexec/apply_catalog.rb +2 -2
  50. data/libexec/bolt_catalog +1 -1
  51. data/libexec/custom_facts.rb +1 -1
  52. data/libexec/query_resources.rb +1 -1
  53. data/modules/secure_env_vars/plans/init.pp +20 -0
  54. metadata +8 -2
@@ -76,7 +76,15 @@ module Bolt
76
76
  target = request['target']
77
77
  plan_vars = shadow_vars('plan', request['plan_vars'], target['facts'])
78
78
  target_vars = shadow_vars('target', target['variables'], target['facts'])
79
- topscope_vars = target_vars.merge(plan_vars)
79
+
80
+ # Merge plan vars with target vars, while maintaining the order of the plan
81
+ # vars. It's critical that the order of plan vars is not changed, as Puppet
82
+ # will deserialize the variables in the order they appear. Variables may
83
+ # contain local references to variables that appear earlier in a plan. If
84
+ # these variables are moved before the variable they reference, Puppet will
85
+ # be unable to deserialize the data and raise an error.
86
+ topscope_vars = target_vars.reject { |k, _v| plan_vars.key?(k) }.merge(plan_vars)
87
+
80
88
  env_conf = { modulepath: request['modulepath'],
81
89
  facts: target['facts'],
82
90
  variables: topscope_vars }
@@ -20,6 +20,7 @@ require 'bolt/logger'
20
20
  require 'bolt/outputter'
21
21
  require 'bolt/puppetdb'
22
22
  require 'bolt/plugin'
23
+ require 'bolt/project_migrate'
23
24
  require 'bolt/pal'
24
25
  require 'bolt/target'
25
26
  require 'bolt/version'
@@ -31,14 +32,15 @@ module Bolt
31
32
  COMMANDS = { 'command' => %w[run],
32
33
  'script' => %w[run],
33
34
  'task' => %w[show run],
34
- 'plan' => %w[show run convert],
35
+ 'plan' => %w[show run convert new],
35
36
  'file' => %w[download upload],
36
37
  'puppetfile' => %w[install show-modules generate-types],
37
38
  'secret' => %w[encrypt decrypt createkeys],
38
39
  'inventory' => %w[show],
39
40
  'group' => %w[show],
40
41
  'project' => %w[init migrate],
41
- 'apply' => %w[] }.freeze
42
+ 'apply' => %w[],
43
+ 'guide' => %w[] }.freeze
42
44
 
43
45
  attr_reader :config, :options
44
46
 
@@ -258,6 +260,13 @@ module Bolt
258
260
  "Option '--noop' may only be specified when running a task or applying manifest code"
259
261
  end
260
262
 
263
+ if options[:env_vars]
264
+ unless %w[command script].include?(options[:subcommand]) && options[:action] == 'run'
265
+ raise Bolt::CLIError,
266
+ "Option '--env-var' may only be specified when running a command or script"
267
+ end
268
+ end
269
+
261
270
  if options[:subcommand] == 'apply' && (options[:object] && options[:code])
262
271
  raise Bolt::CLIError, "--execute is unsupported when specifying a manifest file"
263
272
  end
@@ -276,6 +285,10 @@ module Bolt
276
285
  raise Bolt::CLIError, "Must specify a value to #{options[:action]}"
277
286
  end
278
287
 
288
+ if options[:subcommand] == 'plan' && options[:action] == 'new' && !options[:object]
289
+ raise Bolt::CLIError, "Must specify a plan name."
290
+ end
291
+
279
292
  if options.key?(:debug) && options.key?(:log)
280
293
  raise Bolt::CLIError, "Only one of '--debug' or '--log-level' may be specified"
281
294
  end
@@ -343,19 +356,11 @@ module Bolt
343
356
  # Initialize inventory and targets. Errors here are better to catch early.
344
357
  # options[:target_args] will contain a string/array version of the targetting options this is passed to plans
345
358
  # options[:targets] will contain a resolved set of Target objects
346
- unless options[:subcommand] == 'puppetfile' ||
347
- options[:subcommand] == 'secret' ||
348
- options[:subcommand] == 'project' ||
349
- options[:action] == 'show' ||
350
- options[:action] == 'convert'
359
+ unless %w[project puppetfile secret guide].include?(options[:subcommand]) ||
360
+ %w[convert new show].include?(options[:action])
351
361
  update_targets(options)
352
362
  end
353
363
 
354
- if options[:action] == 'convert'
355
- convert_plan(options[:object])
356
- return 0
357
- end
358
-
359
364
  screen = "#{options[:subcommand]}_#{options[:action]}"
360
365
  # submit a different screen for `bolt task show` and `bolt task show foo`
361
366
  if options[:action] == 'show' && options[:object]
@@ -407,6 +412,9 @@ module Bolt
407
412
  when 'show-modules'
408
413
  list_modules
409
414
  return 0
415
+ when 'convert'
416
+ pal.convert_plan(options[:object])
417
+ return 0
410
418
  end
411
419
 
412
420
  message = 'There may be processes left executing on some nodes.'
@@ -416,15 +424,28 @@ module Bolt
416
424
  end
417
425
 
418
426
  case options[:subcommand]
427
+ when 'guide'
428
+ code = if options[:object]
429
+ show_guide(options[:object])
430
+ else
431
+ list_topics
432
+ end
419
433
  when 'project'
420
434
  case options[:action]
421
435
  when 'init'
422
436
  code = initialize_project
423
437
  when 'migrate'
424
- code = migrate_project
438
+ inv = config.inventoryfile
439
+ path = config.project.path
440
+ code = Bolt::ProjectMigrate.new(path, outputter, inv).migrate_project
425
441
  end
426
442
  when 'plan'
427
- code = run_plan(options[:object], options[:task_options], options[:target_args], options)
443
+ case options[:action]
444
+ when 'new'
445
+ code = new_plan(options[:object])
446
+ when 'run'
447
+ code = run_plan(options[:object], options[:task_options], options[:target_args], options)
448
+ end
428
449
  when 'puppetfile'
429
450
  case options[:action]
430
451
  when 'generate-types'
@@ -450,6 +471,7 @@ module Bolt
450
471
  elapsed_time = Benchmark.realtime do
451
472
  executor_opts = {}
452
473
  executor_opts[:description] = options[:description] if options.key?(:description)
474
+ executor_opts[:env_vars] = options[:env_vars] if options.key?(:env_vars)
453
475
  executor.subscribe(outputter)
454
476
  executor.subscribe(log_outputter)
455
477
  results =
@@ -514,7 +536,7 @@ module Bolt
514
536
  tasks = pal.list_tasks
515
537
  tasks.select! { |task| task.first.include?(options[:filter]) } if options[:filter]
516
538
  tasks.select! { |task| config.project.tasks.include?(task.first) } unless config.project.tasks.nil?
517
- outputter.print_tasks(tasks, pal.list_modulepath)
539
+ outputter.print_tasks(tasks, pal.user_modulepath)
518
540
  end
519
541
 
520
542
  def show_plan(plan_name)
@@ -525,7 +547,7 @@ module Bolt
525
547
  plans = pal.list_plans
526
548
  plans.select! { |plan| plan.first.include?(options[:filter]) } if options[:filter]
527
549
  plans.select! { |plan| config.project.plans.include?(plan.first) } unless config.project.plans.nil?
528
- outputter.print_plans(plans, pal.list_modulepath)
550
+ outputter.print_plans(plans, pal.user_modulepath)
529
551
  end
530
552
 
531
553
  def list_targets
@@ -543,6 +565,118 @@ module Bolt
543
565
  outputter.print_groups(groups)
544
566
  end
545
567
 
568
+ def new_plan(plan_name)
569
+ @logger.warn("Command 'bolt plan new' is experimental and subject to changes.")
570
+
571
+ if config.project.name.nil?
572
+ raise Bolt::Error.new(
573
+ "Project directory '#{config.project.path}' is not a named project. Unable to create "\
574
+ "a project-level plan. To name a project, set the 'name' key in the 'bolt-project.yaml' "\
575
+ "configuration file.",
576
+ "bolt/unnamed-project-error"
577
+ )
578
+ end
579
+
580
+ if plan_name !~ Bolt::Module::CONTENT_NAME_REGEX
581
+ message = <<~MESSAGE.chomp
582
+ Invalid plan name '#{plan_name}'. Plan names are composed of one or more name segments
583
+ separated by double colons '::'.
584
+
585
+ Each name segment must begin with a lowercase letter, and may only include lowercase
586
+ letters, digits, and underscores.
587
+
588
+ Examples of valid plan names:
589
+ - #{config.project.name}
590
+ - #{config.project.name}::my_plan
591
+ MESSAGE
592
+
593
+ raise Bolt::ValidationError, message
594
+ end
595
+
596
+ prefix, *name_segments, basename = plan_name.split('::')
597
+
598
+ # If the plan name is just the project name, then create an 'init' plan.
599
+ # Otherwise, use the last name segment for the plan's filename.
600
+ basename ||= 'init'
601
+
602
+ unless prefix == config.project.name
603
+ message = "First segment of plan name '#{plan_name}' must match project name '#{config.project.name}'. "\
604
+ "Did you mean '#{config.project.name}::#{plan_name}'?"
605
+
606
+ raise Bolt::ValidationError, message
607
+ end
608
+
609
+ dir_path = config.project.plans_path.join(*name_segments)
610
+
611
+ %w[pp yaml].each do |ext|
612
+ next unless (path = config.project.plans_path + "#{basename}.#{ext}").exist?
613
+ raise Bolt::Error.new(
614
+ "A plan with the name '#{plan_name}' already exists at '#{path}', nothing to do.",
615
+ 'bolt/existing-plan-error'
616
+ )
617
+ end
618
+
619
+ begin
620
+ FileUtils.mkdir_p(dir_path)
621
+ rescue Errno::EEXIST => e
622
+ raise Bolt::Error.new(
623
+ "#{e.message}; unable to create plan directory '#{dir_path}'",
624
+ 'bolt/existing-file-error'
625
+ )
626
+ end
627
+
628
+ plan_path = dir_path + "#{basename}.yaml"
629
+
630
+ plan_template = <<~PLAN
631
+ # This is the structure of a simple plan. To learn more about writing
632
+ # YAML plans, see the documentation: http://pup.pt/bolt-yaml-plans
633
+
634
+ # The description sets the description of the plan that will appear
635
+ # in 'bolt plan show' output.
636
+ description: A plan created with bolt plan new
637
+
638
+ # The parameters key defines the parameters that can be passed to
639
+ # the plan.
640
+ parameters:
641
+ targets:
642
+ type: TargetSpec
643
+ description: A list of targets to run actions on
644
+ default: localhost
645
+
646
+ # The steps key defines the actions the plan will take in order.
647
+ steps:
648
+ - message: Hello from #{plan_name}
649
+ - name: command_step
650
+ command: whoami
651
+ targets: $targets
652
+
653
+ # The return key sets the return value of the plan.
654
+ return: $command_step
655
+ PLAN
656
+
657
+ begin
658
+ File.write(plan_path, plan_template)
659
+ rescue Errno::EACCES => e
660
+ raise Bolt::FileError.new(
661
+ "#{e.message}; unable to create plan",
662
+ plan_path
663
+ )
664
+ end
665
+
666
+ output = <<~OUTPUT
667
+ Created plan '#{plan_name}' at '#{plan_path}'
668
+
669
+ Show this plan with:
670
+ bolt plan show #{plan_name}
671
+ Run this plan with:
672
+ bolt plan run #{plan_name}
673
+ OUTPUT
674
+
675
+ outputter.print_message(output)
676
+
677
+ 0
678
+ end
679
+
546
680
  def run_plan(plan_name, plan_arguments, nodes, options)
547
681
  unless nodes.empty?
548
682
  if plan_arguments['nodes'] || plan_arguments['targets']
@@ -646,8 +780,26 @@ module Bolt
646
780
  # Initializes a specified directory as a Bolt project and installs any modules
647
781
  # specified by the user, along with their dependencies
648
782
  def initialize_project
649
- project = Pathname.new(File.expand_path(options[:object] || Dir.pwd))
650
- config = project + 'bolt.yaml'
783
+ # Dir.pwd will return backslashes on Windows, but Pathname always uses
784
+ # forward slashes to concatenate paths. This results in paths like
785
+ # C:\User\Administrator/modules, which fail module install. This ensure
786
+ # forward slashes in the cwd path.
787
+ dir = File.expand_path(Dir.pwd)
788
+ name = options[:object] || File.basename(dir)
789
+ if name !~ Bolt::Module::MODULE_NAME_REGEX
790
+ if options[:object]
791
+ raise Bolt::ValidationError, "The provided project name '#{name}' is invalid; "\
792
+ "project name must begin with a lowercase letter and can include lowercase "\
793
+ "letters, numbers, and underscores."
794
+ else
795
+ raise Bolt::ValidationError, "The current directory name '#{name}' is an invalid "\
796
+ "project name. Please specify a name using 'bolt project init <name>'."
797
+ end
798
+ end
799
+
800
+ project = Pathname.new(dir)
801
+ old_config = project + 'bolt.yaml'
802
+ config = project + 'bolt-project.yaml'
651
803
  puppetfile = project + 'Puppetfile'
652
804
  modulepath = [project + 'modules']
653
805
 
@@ -668,18 +820,24 @@ module Bolt
668
820
 
669
821
  # Warn the user if the project directory already exists. We don't error here since users
670
822
  # might not have installed any modules yet.
823
+ # If both bolt.yaml and bolt-project.yaml exist, this will just warn
824
+ # about bolt-project.yaml and subsequent Bolt actions will warn about
825
+ # both files existing
671
826
  if config.exist?
672
- @logger.warn "Found existing project directory at #{project}"
673
- end
674
-
675
- # Create the project directory
676
- FileUtils.mkdir_p(project)
677
-
827
+ @logger.warn "Found existing project directory at #{project}. Skipping file creation."
828
+ # This won't get called if bolt-project.yaml exists
829
+ elsif old_config.exist?
830
+ @logger.warn "Found existing #{old_config.basename} at #{project}. "\
831
+ "#{old_config.basename} is deprecated, please rename to #{config.basename}."
678
832
  # Bless the project directory as a...wait for it...project
679
- if FileUtils.touch(config)
680
- outputter.print_message "Successfully created Bolt project at #{project}"
681
833
  else
682
- raise Bolt::FileError.new("Could not create Bolt project directory at #{project}", nil)
834
+ begin
835
+ content = { 'name' => name }
836
+ File.write(config.to_path, content.to_yaml)
837
+ outputter.print_message "Successfully created Bolt project at #{project}"
838
+ rescue StandardError => e
839
+ raise Bolt::FileError.new("Could not create bolt-project.yaml at #{project}: #{e.message}", nil)
840
+ end
683
841
  end
684
842
 
685
843
  # Write the generated Puppetfile to the fancy new project
@@ -755,49 +913,6 @@ module Bolt
755
913
  end
756
914
  end
757
915
 
758
- def migrate_project
759
- inventory_file = config.inventoryfile || config.default_inventoryfile
760
- data = Bolt::Util.read_yaml_hash(inventory_file, 'inventory')
761
-
762
- data.delete('version') if data['version'] != 2
763
-
764
- migrated = migrate_group(data)
765
-
766
- ok = File.write(inventory_file, data.to_yaml) if migrated
767
-
768
- result = if migrated && ok
769
- "Successfully migrated Bolt project to latest version."
770
- elsif !migrated
771
- "Bolt project already on latest version. Nothing to do."
772
- else
773
- "Could not migrate Bolt project to latest version."
774
- end
775
- outputter.print_message result
776
-
777
- ok ? 0 : 1
778
- end
779
-
780
- # Walks an inventory hash and replaces all 'nodes' keys with 'targets' keys
781
- # and all 'name' keys nested in a 'targets' hash with 'uri' keys. Data is
782
- # modified in place.
783
- def migrate_group(group)
784
- migrated = false
785
- if group.key?('nodes')
786
- migrated = true
787
- targets = group['nodes'].map do |target|
788
- target['uri'] = target.delete('name') if target.is_a?(Hash)
789
- target
790
- end
791
- group.delete('nodes')
792
- group['targets'] = targets
793
- end
794
- (group['groups'] || []).each do |subgroup|
795
- migrated_group = migrate_group(subgroup)
796
- migrated ||= migrated_group
797
- end
798
- migrated
799
- end
800
-
801
916
  def install_puppetfile(config, puppetfile, modulepath)
802
917
  require 'r10k/cli'
803
918
  require 'bolt/r10k_log_proxy'
@@ -840,8 +955,46 @@ module Bolt
840
955
  config.project)
841
956
  end
842
957
 
843
- def convert_plan(plan)
844
- pal.convert_plan(plan)
958
+ # Collects the list of Bolt guides and maps them to their topics.
959
+ def guides
960
+ @guides ||= begin
961
+ root_path = File.expand_path(File.join(__dir__, '..', '..', 'guides'))
962
+ files = Dir.children(root_path).sort
963
+
964
+ files.each_with_object({}) do |file, guides|
965
+ next if file !~ /\.txt\z/
966
+ topic = File.basename(file, '.txt')
967
+ guides[topic] = File.join(root_path, file)
968
+ end
969
+ rescue SystemCallError => e
970
+ raise Bolt::FileError.new("#{e.message}: unable to load guides directory", root_path)
971
+ end
972
+ end
973
+
974
+ # Display the list of available Bolt guides.
975
+ def list_topics
976
+ outputter.print_topics(guides.keys)
977
+ 0
978
+ end
979
+
980
+ # Display a specific Bolt guide.
981
+ def show_guide(topic)
982
+ if guides[topic]
983
+ analytics.event('Guide', 'known_topic', label: topic)
984
+
985
+ begin
986
+ guide = File.read(guides[topic])
987
+ rescue SystemCallError => e
988
+ raise Bolt::FileError("#{e.message}: unable to load guide page", filepath)
989
+ end
990
+
991
+ outputter.print_guide(guide, topic)
992
+ else
993
+ analytics.event('Guide', 'unknown_topic', label: topic)
994
+ outputter.print_message("Did not find guide for topic '#{topic}'.\n\n")
995
+ list_topics
996
+ end
997
+ 0
845
998
  end
846
999
 
847
1000
  def validate_file(type, path, allow_dir = false)
@@ -908,7 +1061,7 @@ module Bolt
908
1061
  msg = <<~MSG.chomp
909
1062
  Loaded configuration from: '#{config.config_files.join("', '")}'
910
1063
  MSG
911
- @logger.debug(msg)
1064
+ @logger.info(msg)
912
1065
  end
913
1066
 
914
1067
  # Gem installs include the aggregate, canary, and puppetdb_fact modules, while
@@ -213,6 +213,13 @@ module Bolt
213
213
  'transport' => 'ssh'
214
214
  }
215
215
 
216
+ if project.path.directory?
217
+ default_data['log']['bolt-debug.log'] = {
218
+ 'level' => 'debug',
219
+ 'append' => false
220
+ }
221
+ end
222
+
216
223
  loaded_data = config_data.each_with_object([]) do |data, acc|
217
224
  @warnings.concat(data[:warnings]) if data[:warnings].any?
218
225
  @deprecations.concat(data[:deprecations]) if data[:deprecations].any?