bolt 2.18.0 → 2.23.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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +3 -1
  3. data/bolt-modules/boltlib/lib/puppet/functions/download_file.rb +123 -0
  4. data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +6 -0
  5. data/bolt-modules/ctrl/lib/puppet/functions/ctrl/do_until.rb +12 -6
  6. data/bolt-modules/dir/lib/puppet/functions/dir/children.rb +35 -0
  7. data/bolt-modules/out/lib/puppet/functions/out/message.rb +1 -1
  8. data/lib/bolt/analytics.rb +1 -1
  9. data/lib/bolt/bolt_option_parser.rb +74 -24
  10. data/lib/bolt/catalog.rb +12 -3
  11. data/lib/bolt/cli.rb +305 -108
  12. data/lib/bolt/config.rb +18 -10
  13. data/lib/bolt/config/options.rb +14 -0
  14. data/lib/bolt/executor.rb +26 -5
  15. data/lib/bolt/inventory/group.rb +3 -2
  16. data/lib/bolt/inventory/inventory.rb +4 -3
  17. data/lib/bolt/logger.rb +9 -0
  18. data/lib/bolt/module.rb +2 -1
  19. data/lib/bolt/outputter.rb +56 -0
  20. data/lib/bolt/outputter/human.rb +0 -9
  21. data/lib/bolt/outputter/json.rb +0 -4
  22. data/lib/bolt/outputter/rainbow.rb +9 -2
  23. data/lib/bolt/pal.rb +11 -9
  24. data/lib/bolt/pal/yaml_plan/evaluator.rb +23 -2
  25. data/lib/bolt/pal/yaml_plan/step.rb +24 -2
  26. data/lib/bolt/pal/yaml_plan/step/download.rb +38 -0
  27. data/lib/bolt/pal/yaml_plan/step/message.rb +30 -0
  28. data/lib/bolt/pal/yaml_plan/step/upload.rb +3 -3
  29. data/lib/bolt/plugin/module.rb +2 -4
  30. data/lib/bolt/plugin/prompt.rb +3 -3
  31. data/lib/bolt/plugin/puppetdb.rb +3 -2
  32. data/lib/bolt/project.rb +14 -9
  33. data/lib/bolt/puppetdb/client.rb +2 -0
  34. data/lib/bolt/puppetdb/config.rb +16 -0
  35. data/lib/bolt/result.rb +7 -0
  36. data/lib/bolt/shell/bash.rb +24 -4
  37. data/lib/bolt/shell/powershell.rb +10 -4
  38. data/lib/bolt/transport/base.rb +24 -0
  39. data/lib/bolt/transport/docker.rb +8 -0
  40. data/lib/bolt/transport/docker/connection.rb +20 -2
  41. data/lib/bolt/transport/local/connection.rb +14 -1
  42. data/lib/bolt/transport/orch.rb +12 -0
  43. data/lib/bolt/transport/simple.rb +6 -0
  44. data/lib/bolt/transport/ssh/connection.rb +9 -1
  45. data/lib/bolt/transport/ssh/exec_connection.rb +22 -1
  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/pe/pal.rb +1 -1
  50. data/lib/bolt_server/transport_app.rb +3 -2
  51. data/lib/bolt_spec/bolt_context.rb +7 -2
  52. data/lib/bolt_spec/plans.rb +15 -2
  53. data/lib/bolt_spec/plans/action_stubs.rb +3 -2
  54. data/lib/bolt_spec/plans/action_stubs/download_stub.rb +66 -0
  55. data/lib/bolt_spec/plans/mock_executor.rb +14 -1
  56. data/lib/bolt_spec/run.rb +22 -0
  57. data/libexec/apply_catalog.rb +2 -2
  58. data/libexec/bolt_catalog +4 -3
  59. data/libexec/custom_facts.rb +1 -1
  60. data/libexec/query_resources.rb +1 -1
  61. data/modules/secure_env_vars/plans/init.pp +20 -0
  62. 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],
@@ -75,71 +75,100 @@ module Bolt
75
75
  end
76
76
  private :help?
77
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.
78
86
  def parse
79
- begin
80
- parser = BoltOptionParser.new(options)
81
- # This part aims to handle both `bolt <mode> --help` and `bolt help <mode>`.
82
- remaining = handle_parser_errors { parser.permute(@argv) } unless @argv.empty?
83
- if @argv.empty? || help?(remaining)
84
- # Update the parser for the subcommand (or lack thereof)
85
- parser.update
86
- puts parser.help
87
- raise Bolt::CLIExit
88
- end
87
+ parse_command
88
+ load_config
89
+ finalize_setup
90
+ end
89
91
 
90
- options[:object] = remaining.shift
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
95
+ parser = BoltOptionParser.new(options)
96
+ # This part aims to handle both `bolt <mode> --help` and `bolt help <mode>`.
97
+ remaining = handle_parser_errors { parser.permute(@argv) } unless @argv.empty?
98
+ if @argv.empty? || help?(remaining)
99
+ # Update the parser for the subcommand (or lack thereof)
100
+ parser.update
101
+ puts parser.help
102
+ raise Bolt::CLIExit
103
+ end
91
104
 
92
- # Only parse task_options for task or plan
93
- if %w[task plan].include?(options[:subcommand])
94
- task_options, remaining = remaining.partition { |s| s =~ /.+=/ }
95
- if options[:task_options]
96
- unless task_options.empty?
97
- raise Bolt::CLIError,
98
- "Parameters must be specified through either the --params " \
99
- "option or param=value pairs, not both"
100
- end
101
- options[:params_parsed] = true
102
- elsif task_options.any?
103
- options[:params_parsed] = false
104
- options[:task_options] = Hash[task_options.map { |a| a.split('=', 2) }]
105
- else
106
- options[:params_parsed] = true
107
- options[:task_options] = {}
105
+ options[:object] = remaining.shift
106
+
107
+ # Only parse task_options for task or plan
108
+ if %w[task plan].include?(options[:subcommand])
109
+ task_options, remaining = remaining.partition { |s| s =~ /.+=/ }
110
+ if options[:task_options]
111
+ unless task_options.empty?
112
+ raise Bolt::CLIError,
113
+ "Parameters must be specified through either the --params " \
114
+ "option or param=value pairs, not both"
108
115
  end
109
- end
110
- options[:leftovers] = remaining
111
-
112
- validate(options)
113
-
114
- @config = if ENV['BOLT_PROJECT']
115
- project = Bolt::Project.create_project(ENV['BOLT_PROJECT'], 'environment')
116
- Bolt::Config.from_project(project, options)
117
- elsif options[:configfile]
118
- Bolt::Config.from_file(options[:configfile], options)
119
- else
120
- project = if options[:boltdir]
121
- dir = Pathname.new(options[:boltdir])
122
- if (dir + Bolt::Project::BOLTDIR_NAME).directory?
123
- Bolt::Project.create_project(dir + Bolt::Project::BOLTDIR_NAME)
124
- else
125
- Bolt::Project.create_project(dir)
126
- end
127
- else
128
- Bolt::Project.find_boltdir(Dir.pwd)
129
- end
130
- Bolt::Config.from_project(project, options)
131
- end
132
-
133
- Bolt::Logger.configure(config.log, config.color)
134
- rescue Bolt::Error => e
135
- if $stdout.isatty
136
- # Print the error message in red, mimicking outputter.fatal_error
137
- $stdout.puts("\033[31m#{e.message}\033[0m")
116
+ options[:params_parsed] = true
117
+ elsif task_options.any?
118
+ options[:params_parsed] = false
119
+ options[:task_options] = Hash[task_options.map { |a| a.split('=', 2) }]
138
120
  else
139
- $stdout.puts(e.message)
121
+ options[:params_parsed] = true
122
+ options[:task_options] = {}
140
123
  end
141
- raise e
142
124
  end
125
+ options[:leftovers] = remaining
126
+
127
+ # Default to verbose for everything except plans
128
+ unless options.key?(:verbose)
129
+ options[:verbose] = options[:subcommand] != 'plan'
130
+ end
131
+
132
+ validate(options)
133
+
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]
149
+ Bolt::Config.from_file(options[:configfile], options)
150
+ else
151
+ project = if options[:boltdir]
152
+ dir = Pathname.new(options[:boltdir])
153
+ if (dir + Bolt::Project::BOLTDIR_NAME).directory?
154
+ Bolt::Project.create_project(dir + Bolt::Project::BOLTDIR_NAME)
155
+ else
156
+ Bolt::Project.create_project(dir)
157
+ end
158
+ else
159
+ Bolt::Project.find_boltdir(Dir.pwd)
160
+ end
161
+ Bolt::Config.from_project(project, options)
162
+ end
163
+ rescue Bolt::Error => e
164
+ fatal_error(e)
165
+ raise e
166
+ end
167
+
168
+ # Completes the setup process by configuring Bolt and issuing warnings
169
+ def finalize_setup
170
+ Bolt::Logger.configure(config.log, config.color)
171
+ Bolt::Logger.analytics = analytics
143
172
 
144
173
  # Logger must be configured before checking path case and project file, otherwise warnings will not display
145
174
  config.check_path_case('modulepath', config.modulepath)
@@ -149,28 +178,12 @@ module Bolt
149
178
  config_loaded
150
179
 
151
180
  # Display warnings created during parser and config initialization
152
- parser.warnings.each { |warning| @logger.warn(warning[:msg]) }
153
181
  config.warnings.each { |warning| @logger.warn(warning[:msg]) }
154
-
155
- # After validation, initialize inventory and targets. Errors here are better to catch early.
156
- # After this step
157
- # options[:target_args] will contain a string/array version of the targetting options this is passed to plans
158
- # options[:targets] will contain a resolved set of Target objects
159
- unless options[:subcommand] == 'puppetfile' ||
160
- options[:subcommand] == 'secret' ||
161
- options[:subcommand] == 'project' ||
162
- options[:action] == 'show' ||
163
- options[:action] == 'convert'
164
-
165
- update_targets(options)
166
- end
167
-
168
- unless options.key?(:verbose)
169
- # Default to verbose for everything except plans
170
- options[:verbose] = options[:subcommand] != 'plan'
171
- 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]) }
172
184
 
173
185
  warn_inventory_overrides_cli(options)
186
+
174
187
  options
175
188
  rescue Bolt::Error => e
176
189
  outputter.fatal_error(e)
@@ -245,6 +258,13 @@ module Bolt
245
258
  "Option '--noop' may only be specified when running a task or applying manifest code"
246
259
  end
247
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
+
248
268
  if options[:subcommand] == 'apply' && (options[:object] && options[:code])
249
269
  raise Bolt::CLIError, "--execute is unsupported when specifying a manifest file"
250
270
  end
@@ -263,6 +283,10 @@ module Bolt
263
283
  raise Bolt::CLIError, "Must specify a value to #{options[:action]}"
264
284
  end
265
285
 
286
+ if options[:subcommand] == 'plan' && options[:action] == 'new' && !options[:object]
287
+ raise Bolt::CLIError, "Must specify a plan name."
288
+ end
289
+
266
290
  if options.key?(:debug) && options.key?(:log)
267
291
  raise Bolt::CLIError, "Only one of '--debug' or '--log-level' may be specified"
268
292
  end
@@ -327,9 +351,12 @@ module Bolt
327
351
  exit!
328
352
  end
329
353
 
330
- if options[:action] == 'convert'
331
- convert_plan(options[:object])
332
- 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)
333
360
  end
334
361
 
335
362
  screen = "#{options[:subcommand]}_#{options[:action]}"
@@ -355,32 +382,37 @@ module Bolt
355
382
 
356
383
  analytics.screen_view(screen, screen_view_fields)
357
384
 
358
- if options[:action] == 'show'
359
- if options[:subcommand] == 'task'
385
+ case options[:action]
386
+ when 'show'
387
+ case options[:subcommand]
388
+ when 'task'
360
389
  if options[:object]
361
390
  show_task(options[:object])
362
391
  else
363
392
  list_tasks
364
393
  end
365
- elsif options[:subcommand] == 'plan'
394
+ when 'plan'
366
395
  if options[:object]
367
396
  show_plan(options[:object])
368
397
  else
369
398
  list_plans
370
399
  end
371
- elsif options[:subcommand] == 'inventory'
400
+ when 'inventory'
372
401
  if options[:detail]
373
402
  show_targets
374
403
  else
375
404
  list_targets
376
405
  end
377
- elsif options[:subcommand] == 'group'
406
+ when 'group'
378
407
  list_groups
379
408
  end
380
409
  return 0
381
- elsif options[:action] == 'show-modules'
410
+ when 'show-modules'
382
411
  list_modules
383
412
  return 0
413
+ when 'convert'
414
+ convert_plan(options[:object])
415
+ return 0
384
416
  end
385
417
 
386
418
  message = 'There may be processes left executing on some nodes.'
@@ -391,17 +423,24 @@ module Bolt
391
423
 
392
424
  case options[:subcommand]
393
425
  when 'project'
394
- if options[:action] == 'init'
426
+ case options[:action]
427
+ when 'init'
395
428
  code = initialize_project
396
- elsif options[:action] == 'migrate'
429
+ when 'migrate'
397
430
  code = migrate_project
398
431
  end
399
432
  when 'plan'
400
- 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
401
439
  when 'puppetfile'
402
- if options[:action] == 'generate-types'
440
+ case options[:action]
441
+ when 'generate-types'
403
442
  code = generate_types
404
- elsif options[:action] == 'install'
443
+ when 'install'
405
444
  code = install_puppetfile(config.puppetfile_config, config.puppetfile, config.modulepath)
406
445
  end
407
446
  when 'secret'
@@ -422,6 +461,7 @@ module Bolt
422
461
  elapsed_time = Benchmark.realtime do
423
462
  executor_opts = {}
424
463
  executor_opts[:description] = options[:description] if options.key?(:description)
464
+ executor_opts[:env_vars] = options[:env_vars] if options.key?(:env_vars)
425
465
  executor.subscribe(outputter)
426
466
  executor.subscribe(log_outputter)
427
467
  results =
@@ -443,11 +483,22 @@ module Bolt
443
483
  src = options[:object]
444
484
  dest = options[:leftovers].first
445
485
 
486
+ if src.nil?
487
+ raise Bolt::CLIError, "A source path must be specified"
488
+ end
489
+
446
490
  if dest.nil?
447
491
  raise Bolt::CLIError, "A destination path must be specified"
448
492
  end
449
- validate_file('source file', src, true)
450
- 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
451
502
  end
452
503
  end
453
504
 
@@ -475,7 +526,7 @@ module Bolt
475
526
  tasks = pal.list_tasks
476
527
  tasks.select! { |task| task.first.include?(options[:filter]) } if options[:filter]
477
528
  tasks.select! { |task| config.project.tasks.include?(task.first) } unless config.project.tasks.nil?
478
- outputter.print_tasks(tasks, pal.list_modulepath)
529
+ outputter.print_tasks(tasks, pal.user_modulepath)
479
530
  end
480
531
 
481
532
  def show_plan(plan_name)
@@ -486,7 +537,7 @@ module Bolt
486
537
  plans = pal.list_plans
487
538
  plans.select! { |plan| plan.first.include?(options[:filter]) } if options[:filter]
488
539
  plans.select! { |plan| config.project.plans.include?(plan.first) } unless config.project.plans.nil?
489
- outputter.print_plans(plans, pal.list_modulepath)
540
+ outputter.print_plans(plans, pal.user_modulepath)
490
541
  end
491
542
 
492
543
  def list_targets
@@ -504,6 +555,118 @@ module Bolt
504
555
  outputter.print_groups(groups)
505
556
  end
506
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
+
507
670
  def run_plan(plan_name, plan_arguments, nodes, options)
508
671
  unless nodes.empty?
509
672
  if plan_arguments['nodes'] || plan_arguments['targets']
@@ -607,8 +770,26 @@ module Bolt
607
770
  # Initializes a specified directory as a Bolt project and installs any modules
608
771
  # specified by the user, along with their dependencies
609
772
  def initialize_project
610
- project = Pathname.new(File.expand_path(options[:object] || Dir.pwd))
611
- config = project + 'bolt.yaml'
773
+ # Dir.pwd will return backslashes on Windows, but Pathname always uses
774
+ # forward slashes to concatenate paths. This results in paths like
775
+ # C:\User\Administrator/modules, which fail module install. This ensure
776
+ # forward slashes in the cwd path.
777
+ dir = File.expand_path(Dir.pwd)
778
+ name = options[:object] || File.basename(dir)
779
+ if name !~ Bolt::Module::MODULE_NAME_REGEX
780
+ if options[:object]
781
+ raise Bolt::ValidationError, "The provided project name '#{name}' is invalid; "\
782
+ "project name must begin with a lowercase letter and can include lowercase "\
783
+ "letters, numbers, and underscores."
784
+ else
785
+ raise Bolt::ValidationError, "The current directory name '#{name}' is an invalid "\
786
+ "project name. Please specify a name using 'bolt project init <name>'."
787
+ end
788
+ end
789
+
790
+ project = Pathname.new(dir)
791
+ old_config = project + 'bolt.yaml'
792
+ config = project + 'bolt-project.yaml'
612
793
  puppetfile = project + 'Puppetfile'
613
794
  modulepath = [project + 'modules']
614
795
 
@@ -629,18 +810,24 @@ module Bolt
629
810
 
630
811
  # Warn the user if the project directory already exists. We don't error here since users
631
812
  # might not have installed any modules yet.
813
+ # If both bolt.yaml and bolt-project.yaml exist, this will just warn
814
+ # about bolt-project.yaml and subsequent Bolt actions will warn about
815
+ # both files existing
632
816
  if config.exist?
633
- @logger.warn "Found existing project directory at #{project}"
634
- end
635
-
636
- # Create the project directory
637
- FileUtils.mkdir_p(project)
638
-
817
+ @logger.warn "Found existing project directory at #{project}. Skipping file creation."
818
+ # This won't get called if bolt-project.yaml exists
819
+ elsif old_config.exist?
820
+ @logger.warn "Found existing #{old_config.basename} at #{project}. "\
821
+ "#{old_config.basename} is deprecated, please rename to #{config.basename}."
639
822
  # Bless the project directory as a...wait for it...project
640
- if FileUtils.touch(config)
641
- outputter.print_message "Successfully created Bolt project at #{project}"
642
823
  else
643
- raise Bolt::FileError.new("Could not create Bolt project directory at #{project}", nil)
824
+ begin
825
+ content = { 'name' => name }
826
+ File.write(config.to_path, content.to_yaml)
827
+ outputter.print_message "Successfully created Bolt project at #{project}"
828
+ rescue StandardError => e
829
+ raise Bolt::FileError.new("Could not create bolt-project.yaml at #{project}: #{e.message}", nil)
830
+ end
644
831
  end
645
832
 
646
833
  # Write the generated Puppetfile to the fancy new project
@@ -877,5 +1064,15 @@ module Bolt
877
1064
  def incomplete_install?
878
1065
  (Dir.children(Bolt::PAL::MODULES_PATH) - %w[aggregate canary puppetdb_fact]).empty?
879
1066
  end
1067
+
1068
+ # Mimicks the output from Outputter::Human#fatal_error. This should be used to print
1069
+ # errors prior to config being loaded, as the outputter relies on config being loaded.
1070
+ def fatal_error(error)
1071
+ if $stdout.isatty
1072
+ $stdout.puts("\033[31m#{error.message}\033[0m")
1073
+ else
1074
+ $stdout.puts(error.message)
1075
+ end
1076
+ end
880
1077
  end
881
1078
  end