bolt 2.17.0 → 2.22.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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +3 -1
  3. data/bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb +20 -9
  4. data/bolt-modules/boltlib/lib/puppet/functions/download_file.rb +123 -0
  5. data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +6 -0
  6. data/bolt-modules/dir/lib/puppet/functions/dir/children.rb +35 -0
  7. data/lib/bolt/applicator.rb +19 -14
  8. data/lib/bolt/apply_result.rb +1 -1
  9. data/lib/bolt/bolt_option_parser.rb +68 -13
  10. data/lib/bolt/catalog.rb +12 -3
  11. data/lib/bolt/cli.rb +232 -47
  12. data/lib/bolt/config.rb +34 -13
  13. data/lib/bolt/config/options.rb +16 -1
  14. data/lib/bolt/config/transport/options.rb +16 -10
  15. data/lib/bolt/config/transport/ssh.rb +24 -10
  16. data/lib/bolt/executor.rb +21 -0
  17. data/lib/bolt/inventory/group.rb +3 -2
  18. data/lib/bolt/inventory/inventory.rb +4 -3
  19. data/lib/bolt/logger.rb +21 -0
  20. data/lib/bolt/module.rb +2 -1
  21. data/lib/bolt/outputter/rainbow.rb +9 -2
  22. data/lib/bolt/pal.rb +8 -2
  23. data/lib/bolt/pal/yaml_plan/evaluator.rb +23 -2
  24. data/lib/bolt/pal/yaml_plan/step.rb +24 -2
  25. data/lib/bolt/pal/yaml_plan/step/download.rb +38 -0
  26. data/lib/bolt/pal/yaml_plan/step/message.rb +30 -0
  27. data/lib/bolt/pal/yaml_plan/step/upload.rb +3 -3
  28. data/lib/bolt/plugin/module.rb +2 -4
  29. data/lib/bolt/plugin/puppetdb.rb +3 -2
  30. data/lib/bolt/project.rb +25 -11
  31. data/lib/bolt/puppetdb/client.rb +2 -0
  32. data/lib/bolt/puppetdb/config.rb +16 -0
  33. data/lib/bolt/result.rb +7 -0
  34. data/lib/bolt/shell/bash.rb +24 -4
  35. data/lib/bolt/shell/powershell.rb +10 -4
  36. data/lib/bolt/shell/powershell/snippets.rb +15 -6
  37. data/lib/bolt/transport/base.rb +24 -0
  38. data/lib/bolt/transport/docker.rb +8 -0
  39. data/lib/bolt/transport/docker/connection.rb +20 -2
  40. data/lib/bolt/transport/local/connection.rb +14 -1
  41. data/lib/bolt/transport/orch.rb +12 -0
  42. data/lib/bolt/transport/simple.rb +6 -0
  43. data/lib/bolt/transport/ssh.rb +7 -1
  44. data/lib/bolt/transport/ssh/connection.rb +9 -1
  45. data/lib/bolt/transport/ssh/exec_connection.rb +23 -2
  46. data/lib/bolt/transport/winrm/connection.rb +118 -8
  47. data/lib/bolt/util.rb +26 -11
  48. data/lib/bolt/version.rb +1 -1
  49. data/lib/bolt_server/transport_app.rb +3 -2
  50. data/lib/bolt_spec/bolt_context.rb +7 -2
  51. data/lib/bolt_spec/plans.rb +15 -2
  52. data/lib/bolt_spec/plans/action_stubs.rb +2 -1
  53. data/lib/bolt_spec/plans/action_stubs/download_stub.rb +66 -0
  54. data/lib/bolt_spec/plans/mock_executor.rb +14 -1
  55. data/lib/bolt_spec/run.rb +22 -0
  56. data/libexec/bolt_catalog +3 -2
  57. data/modules/secure_env_vars/plans/init.pp +20 -0
  58. 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 }
@@ -138,9 +146,10 @@ module Bolt
138
146
  # That means the apply body either a) consists of just a
139
147
  # NodeDefinition, b) consists of a BlockExpression which may
140
148
  # contain NodeDefinitions, or c) doesn't contain NodeDefinitions.
141
- definitions = if ast.is_a?(Puppet::Pops::Model::BlockExpression)
149
+ definitions = case ast
150
+ when Puppet::Pops::Model::BlockExpression
142
151
  ast.statements.select { |st| st.is_a?(Puppet::Pops::Model::NodeDefinition) }
143
- elsif ast.is_a?(Puppet::Pops::Model::NodeDefinition)
152
+ when Puppet::Pops::Model::NodeDefinition
144
153
  [ast]
145
154
  else
146
155
  []
@@ -31,8 +31,8 @@ module Bolt
31
31
  COMMANDS = { 'command' => %w[run],
32
32
  'script' => %w[run],
33
33
  'task' => %w[show run],
34
- 'plan' => %w[show run convert],
35
- 'file' => %w[upload],
34
+ 'plan' => %w[show run convert new],
35
+ 'file' => %w[download upload],
36
36
  'puppetfile' => %w[install show-modules generate-types],
37
37
  'secret' => %w[encrypt decrypt createkeys],
38
38
  'inventory' => %w[show],
@@ -46,7 +46,6 @@ module Bolt
46
46
  Bolt::Logger.initialize_logging
47
47
  @logger = Logging.logger[self]
48
48
  @argv = argv
49
- @config = Bolt::Config.default
50
49
  @options = {}
51
50
  end
52
51
 
@@ -76,7 +75,23 @@ module Bolt
76
75
  end
77
76
  private :help?
78
77
 
78
+ # Wrapper method that is called by the Bolt executable. Parses the command and
79
+ # then loads the project and config. Once config is loaded, it completes the
80
+ # setup process by configuring Bolt and issuing warnings.
81
+ #
82
+ # This separation is needed since the Bolt::Outputter class that normally handles
83
+ # printing errors relies on config being loaded. All setup that happens before
84
+ # config is loaded will have errors printed directly to stdout, while all errors
85
+ # raised after config is loaded are handled by the outputter.
79
86
  def parse
87
+ parse_command
88
+ load_config
89
+ finalize_setup
90
+ end
91
+
92
+ # Parses the command and validates options. All errors that are raised here
93
+ # are not handled by the outputter, as it relies on config being loaded.
94
+ def parse_command
80
95
  parser = BoltOptionParser.new(options)
81
96
  # This part aims to handle both `bolt <mode> --help` and `bolt help <mode>`.
82
97
  remaining = handle_parser_errors { parser.permute(@argv) } unless @argv.empty?
@@ -109,9 +124,28 @@ module Bolt
109
124
  end
110
125
  options[:leftovers] = remaining
111
126
 
127
+ # Default to verbose for everything except plans
128
+ unless options.key?(:verbose)
129
+ options[:verbose] = options[:subcommand] != 'plan'
130
+ end
131
+
112
132
  validate(options)
113
133
 
114
- @config = if options[:configfile]
134
+ # Deprecation warnings can't be issued until after config is loaded, so
135
+ # store them for later.
136
+ @parser_deprecations = parser.deprecations
137
+ rescue Bolt::Error => e
138
+ fatal_error(e)
139
+ raise e
140
+ end
141
+
142
+ # Loads the project and configuration. All errors that are raised here are not
143
+ # handled by the outputter, as it relies on config being loaded.
144
+ def load_config
145
+ @config = if ENV['BOLT_PROJECT']
146
+ project = Bolt::Project.create_project(ENV['BOLT_PROJECT'], 'environment')
147
+ Bolt::Config.from_project(project, options)
148
+ elsif options[:configfile]
115
149
  Bolt::Config.from_file(options[:configfile], options)
116
150
  else
117
151
  project = if options[:boltdir]
@@ -126,39 +160,30 @@ module Bolt
126
160
  end
127
161
  Bolt::Config.from_project(project, options)
128
162
  end
163
+ rescue Bolt::Error => e
164
+ fatal_error(e)
165
+ raise e
166
+ end
129
167
 
168
+ # Completes the setup process by configuring Bolt and issuing warnings
169
+ def finalize_setup
130
170
  Bolt::Logger.configure(config.log, config.color)
171
+ Bolt::Logger.analytics = analytics
131
172
 
132
173
  # Logger must be configured before checking path case and project file, otherwise warnings will not display
133
- @config.check_path_case('modulepath', @config.modulepath)
134
- @config.project.check_deprecated_file
174
+ config.check_path_case('modulepath', config.modulepath)
175
+ config.project.check_deprecated_file
135
176
 
136
177
  # Log the file paths for loaded config files
137
178
  config_loaded
138
179
 
139
180
  # Display warnings created during parser and config initialization
140
- parser.warnings.each { |warning| @logger.warn(warning[:msg]) }
141
181
  config.warnings.each { |warning| @logger.warn(warning[:msg]) }
142
-
143
- # After validation, initialize inventory and targets. Errors here are better to catch early.
144
- # After this step
145
- # options[:target_args] will contain a string/array version of the targetting options this is passed to plans
146
- # options[:targets] will contain a resolved set of Target objects
147
- unless options[:subcommand] == 'puppetfile' ||
148
- options[:subcommand] == 'secret' ||
149
- options[:subcommand] == 'project' ||
150
- options[:action] == 'show' ||
151
- options[:action] == 'convert'
152
-
153
- update_targets(options)
154
- end
155
-
156
- unless options.key?(:verbose)
157
- # Default to verbose for everything except plans
158
- options[:verbose] = options[:subcommand] != 'plan'
159
- end
182
+ @parser_deprecations.each { |dep| Bolt::Logger.deprecation_warning(dep[:type], dep[:msg]) }
183
+ config.deprecations.each { |dep| Bolt::Logger.deprecation_warning(dep[:type], dep[:msg]) }
160
184
 
161
185
  warn_inventory_overrides_cli(options)
186
+
162
187
  options
163
188
  rescue Bolt::Error => e
164
189
  outputter.fatal_error(e)
@@ -233,6 +258,13 @@ module Bolt
233
258
  "Option '--noop' may only be specified when running a task or applying manifest code"
234
259
  end
235
260
 
261
+ if options[:env_vars]
262
+ unless %w[command script].include?(options[:subcommand]) && options[:action] == 'run'
263
+ raise Bolt::CLIError,
264
+ "Option '--env-var' may only be specified when running a command or script"
265
+ end
266
+ end
267
+
236
268
  if options[:subcommand] == 'apply' && (options[:object] && options[:code])
237
269
  raise Bolt::CLIError, "--execute is unsupported when specifying a manifest file"
238
270
  end
@@ -251,6 +283,10 @@ module Bolt
251
283
  raise Bolt::CLIError, "Must specify a value to #{options[:action]}"
252
284
  end
253
285
 
286
+ if options[:subcommand] == 'plan' && options[:action] == 'new' && !options[:object]
287
+ raise Bolt::CLIError, "Must specify a plan name."
288
+ end
289
+
254
290
  if options.key?(:debug) && options.key?(:log)
255
291
  raise Bolt::CLIError, "Only one of '--debug' or '--log-level' may be specified"
256
292
  end
@@ -281,12 +317,12 @@ module Bolt
281
317
  def warn_inventory_overrides_cli(opts)
282
318
  inventory_source = if ENV[Bolt::Inventory::ENVIRONMENT_VAR]
283
319
  Bolt::Inventory::ENVIRONMENT_VAR
284
- elsif @config.inventoryfile && Bolt::Util.file_stat(@config.inventoryfile)
285
- @config.inventoryfile
320
+ elsif config.inventoryfile && Bolt::Util.file_stat(config.inventoryfile)
321
+ config.inventoryfile
286
322
  else
287
323
  begin
288
- Bolt::Util.file_stat(@config.default_inventoryfile)
289
- @config.default_inventoryfile
324
+ Bolt::Util.file_stat(config.default_inventoryfile)
325
+ config.default_inventoryfile
290
326
  rescue Errno::ENOENT
291
327
  nil
292
328
  end
@@ -315,9 +351,12 @@ module Bolt
315
351
  exit!
316
352
  end
317
353
 
318
- if options[:action] == 'convert'
319
- convert_plan(options[:object])
320
- return 0
354
+ # Initialize inventory and targets. Errors here are better to catch early.
355
+ # options[:target_args] will contain a string/array version of the targetting options this is passed to plans
356
+ # options[:targets] will contain a resolved set of Target objects
357
+ unless %w[project puppetfile secret].include?(options[:subcommand]) ||
358
+ %w[convert new show].include?(options[:action])
359
+ update_targets(options)
321
360
  end
322
361
 
323
362
  screen = "#{options[:subcommand]}_#{options[:action]}"
@@ -343,32 +382,37 @@ module Bolt
343
382
 
344
383
  analytics.screen_view(screen, screen_view_fields)
345
384
 
346
- if options[:action] == 'show'
347
- if options[:subcommand] == 'task'
385
+ case options[:action]
386
+ when 'show'
387
+ case options[:subcommand]
388
+ when 'task'
348
389
  if options[:object]
349
390
  show_task(options[:object])
350
391
  else
351
392
  list_tasks
352
393
  end
353
- elsif options[:subcommand] == 'plan'
394
+ when 'plan'
354
395
  if options[:object]
355
396
  show_plan(options[:object])
356
397
  else
357
398
  list_plans
358
399
  end
359
- elsif options[:subcommand] == 'inventory'
400
+ when 'inventory'
360
401
  if options[:detail]
361
402
  show_targets
362
403
  else
363
404
  list_targets
364
405
  end
365
- elsif options[:subcommand] == 'group'
406
+ when 'group'
366
407
  list_groups
367
408
  end
368
409
  return 0
369
- elsif options[:action] == 'show-modules'
410
+ when 'show-modules'
370
411
  list_modules
371
412
  return 0
413
+ when 'convert'
414
+ convert_plan(options[:object])
415
+ return 0
372
416
  end
373
417
 
374
418
  message = 'There may be processes left executing on some nodes.'
@@ -379,18 +423,25 @@ module Bolt
379
423
 
380
424
  case options[:subcommand]
381
425
  when 'project'
382
- if options[:action] == 'init'
426
+ case options[:action]
427
+ when 'init'
383
428
  code = initialize_project
384
- elsif options[:action] == 'migrate'
429
+ when 'migrate'
385
430
  code = migrate_project
386
431
  end
387
432
  when 'plan'
388
- code = run_plan(options[:object], options[:task_options], options[:target_args], options)
433
+ case options[:action]
434
+ when 'new'
435
+ code = new_plan(options[:object])
436
+ when 'run'
437
+ code = run_plan(options[:object], options[:task_options], options[:target_args], options)
438
+ end
389
439
  when 'puppetfile'
390
- if options[:action] == 'generate-types'
440
+ case options[:action]
441
+ when 'generate-types'
391
442
  code = generate_types
392
- elsif options[:action] == 'install'
393
- code = install_puppetfile(@config.puppetfile_config, @config.puppetfile, @config.modulepath)
443
+ when 'install'
444
+ code = install_puppetfile(config.puppetfile_config, config.puppetfile, config.modulepath)
394
445
  end
395
446
  when 'secret'
396
447
  code = Bolt::Secret.execute(plugins, outputter, options)
@@ -410,6 +461,7 @@ module Bolt
410
461
  elapsed_time = Benchmark.realtime do
411
462
  executor_opts = {}
412
463
  executor_opts[:description] = options[:description] if options.key?(:description)
464
+ executor_opts[:env_vars] = options[:env_vars] if options.key?(:env_vars)
413
465
  executor.subscribe(outputter)
414
466
  executor.subscribe(log_outputter)
415
467
  results =
@@ -431,11 +483,22 @@ module Bolt
431
483
  src = options[:object]
432
484
  dest = options[:leftovers].first
433
485
 
486
+ if src.nil?
487
+ raise Bolt::CLIError, "A source path must be specified"
488
+ end
489
+
434
490
  if dest.nil?
435
491
  raise Bolt::CLIError, "A destination path must be specified"
436
492
  end
437
- validate_file('source file', src, true)
438
- executor.upload_file(targets, src, dest, executor_opts)
493
+
494
+ case options[:action]
495
+ when 'download'
496
+ dest = File.expand_path(dest, Dir.pwd)
497
+ executor.download_file(targets, src, dest, executor_opts)
498
+ when 'upload'
499
+ validate_file('source file', src, true)
500
+ executor.upload_file(targets, src, dest, executor_opts)
501
+ end
439
502
  end
440
503
  end
441
504
 
@@ -492,6 +555,118 @@ module Bolt
492
555
  outputter.print_groups(groups)
493
556
  end
494
557
 
558
+ def new_plan(plan_name)
559
+ @logger.warn("Command 'bolt plan new' is experimental and subject to changes.")
560
+
561
+ if config.project.name.nil?
562
+ raise Bolt::Error.new(
563
+ "Project directory '#{config.project.path}' is not a named project. Unable to create "\
564
+ "a project-level plan. To name a project, set the 'name' key in the 'bolt-project.yaml' "\
565
+ "configuration file.",
566
+ "bolt/unnamed-project-error"
567
+ )
568
+ end
569
+
570
+ if plan_name !~ Bolt::Module::CONTENT_NAME_REGEX
571
+ message = <<~MESSAGE.chomp
572
+ Invalid plan name '#{plan_name}'. Plan names are composed of one or more name segments
573
+ separated by double colons '::'.
574
+
575
+ Each name segment must begin with a lowercase letter, and may only include lowercase
576
+ letters, digits, and underscores.
577
+
578
+ Examples of valid plan names:
579
+ - #{config.project.name}
580
+ - #{config.project.name}::my_plan
581
+ MESSAGE
582
+
583
+ raise Bolt::ValidationError, message
584
+ end
585
+
586
+ prefix, *name_segments, basename = plan_name.split('::')
587
+
588
+ # If the plan name is just the project name, then create an 'init' plan.
589
+ # Otherwise, use the last name segment for the plan's filename.
590
+ basename ||= 'init'
591
+
592
+ unless prefix == config.project.name
593
+ message = "First segment of plan name '#{plan_name}' must match project name '#{config.project.name}'. "\
594
+ "Did you mean '#{config.project.name}::#{plan_name}'?"
595
+
596
+ raise Bolt::ValidationError, message
597
+ end
598
+
599
+ dir_path = config.project.plans_path.join(*name_segments)
600
+
601
+ %w[pp yaml].each do |ext|
602
+ next unless (path = config.project.plans_path + "#{basename}.#{ext}").exist?
603
+ raise Bolt::Error.new(
604
+ "A plan with the name '#{plan_name}' already exists at '#{path}', nothing to do.",
605
+ 'bolt/existing-plan-error'
606
+ )
607
+ end
608
+
609
+ begin
610
+ FileUtils.mkdir_p(dir_path)
611
+ rescue Errno::EEXIST => e
612
+ raise Bolt::Error.new(
613
+ "#{e.message}; unable to create plan directory '#{dir_path}'",
614
+ 'bolt/existing-file-error'
615
+ )
616
+ end
617
+
618
+ plan_path = dir_path + "#{basename}.yaml"
619
+
620
+ plan_template = <<~PLAN
621
+ # This is the structure of a simple plan. To learn more about writing
622
+ # YAML plans, see the documentation: http://pup.pt/bolt-yaml-plans
623
+
624
+ # The description sets the description of the plan that will appear
625
+ # in 'bolt plan show' output.
626
+ description: A plan created with bolt plan new
627
+
628
+ # The parameters key defines the parameters that can be passed to
629
+ # the plan.
630
+ parameters:
631
+ targets:
632
+ type: TargetSpec
633
+ description: A list of targets to run actions on
634
+ default: localhost
635
+
636
+ # The steps key defines the actions the plan will take in order.
637
+ steps:
638
+ - message: Hello from #{plan_name}
639
+ - name: command_step
640
+ command: whoami
641
+ targets: $targets
642
+
643
+ # The return key sets the return value of the plan.
644
+ return: $command_step
645
+ PLAN
646
+
647
+ begin
648
+ File.write(plan_path, plan_template)
649
+ rescue Errno::EACCES => e
650
+ raise Bolt::FileError.new(
651
+ "#{e.message}; unable to create plan",
652
+ plan_path
653
+ )
654
+ end
655
+
656
+ output = <<~OUTPUT
657
+ Created plan '#{plan_name}' at '#{plan_path}'
658
+
659
+ Show this plan with:
660
+ bolt plan show #{plan_name}
661
+ Run this plan with:
662
+ bolt plan run #{plan_name}
663
+ OUTPUT
664
+
665
+ outputter.print_message(output)
666
+
667
+ 0
668
+ end
669
+
495
670
  def run_plan(plan_name, plan_arguments, nodes, options)
496
671
  unless nodes.empty?
497
672
  if plan_arguments['nodes'] || plan_arguments['targets']
@@ -802,7 +977,7 @@ module Bolt
802
977
  end
803
978
 
804
979
  def rerun
805
- @rerun ||= Bolt::Rerun.new(@config.rerunfile, @config.save_rerun)
980
+ @rerun ||= Bolt::Rerun.new(config.rerunfile, config.save_rerun)
806
981
  end
807
982
 
808
983
  def outputter
@@ -865,5 +1040,15 @@ module Bolt
865
1040
  def incomplete_install?
866
1041
  (Dir.children(Bolt::PAL::MODULES_PATH) - %w[aggregate canary puppetdb_fact]).empty?
867
1042
  end
1043
+
1044
+ # Mimicks the output from Outputter::Human#fatal_error. This should be used to print
1045
+ # errors prior to config being loaded, as the outputter relies on config being loaded.
1046
+ def fatal_error(error)
1047
+ if $stdout.isatty
1048
+ $stdout.puts("\033[31m#{error.message}\033[0m")
1049
+ else
1050
+ $stdout.puts(error.message)
1051
+ end
1052
+ end
868
1053
  end
869
1054
  end