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.
- checksums.yaml +4 -4
- data/Puppetfile +3 -1
- data/bolt-modules/boltlib/lib/puppet/functions/download_file.rb +123 -0
- data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +6 -0
- data/bolt-modules/ctrl/lib/puppet/functions/ctrl/do_until.rb +12 -6
- data/bolt-modules/dir/lib/puppet/functions/dir/children.rb +35 -0
- data/bolt-modules/out/lib/puppet/functions/out/message.rb +1 -1
- data/lib/bolt/analytics.rb +1 -1
- data/lib/bolt/bolt_option_parser.rb +74 -24
- data/lib/bolt/catalog.rb +12 -3
- data/lib/bolt/cli.rb +305 -108
- data/lib/bolt/config.rb +18 -10
- data/lib/bolt/config/options.rb +14 -0
- data/lib/bolt/executor.rb +26 -5
- data/lib/bolt/inventory/group.rb +3 -2
- data/lib/bolt/inventory/inventory.rb +4 -3
- data/lib/bolt/logger.rb +9 -0
- data/lib/bolt/module.rb +2 -1
- data/lib/bolt/outputter.rb +56 -0
- data/lib/bolt/outputter/human.rb +0 -9
- data/lib/bolt/outputter/json.rb +0 -4
- data/lib/bolt/outputter/rainbow.rb +9 -2
- data/lib/bolt/pal.rb +11 -9
- data/lib/bolt/pal/yaml_plan/evaluator.rb +23 -2
- data/lib/bolt/pal/yaml_plan/step.rb +24 -2
- data/lib/bolt/pal/yaml_plan/step/download.rb +38 -0
- data/lib/bolt/pal/yaml_plan/step/message.rb +30 -0
- data/lib/bolt/pal/yaml_plan/step/upload.rb +3 -3
- data/lib/bolt/plugin/module.rb +2 -4
- data/lib/bolt/plugin/prompt.rb +3 -3
- data/lib/bolt/plugin/puppetdb.rb +3 -2
- data/lib/bolt/project.rb +14 -9
- data/lib/bolt/puppetdb/client.rb +2 -0
- data/lib/bolt/puppetdb/config.rb +16 -0
- data/lib/bolt/result.rb +7 -0
- data/lib/bolt/shell/bash.rb +24 -4
- data/lib/bolt/shell/powershell.rb +10 -4
- data/lib/bolt/transport/base.rb +24 -0
- data/lib/bolt/transport/docker.rb +8 -0
- data/lib/bolt/transport/docker/connection.rb +20 -2
- data/lib/bolt/transport/local/connection.rb +14 -1
- data/lib/bolt/transport/orch.rb +12 -0
- data/lib/bolt/transport/simple.rb +6 -0
- data/lib/bolt/transport/ssh/connection.rb +9 -1
- data/lib/bolt/transport/ssh/exec_connection.rb +22 -1
- data/lib/bolt/transport/winrm/connection.rb +118 -8
- data/lib/bolt/util.rb +26 -11
- data/lib/bolt/version.rb +1 -1
- data/lib/bolt_server/pe/pal.rb +1 -1
- data/lib/bolt_server/transport_app.rb +3 -2
- data/lib/bolt_spec/bolt_context.rb +7 -2
- data/lib/bolt_spec/plans.rb +15 -2
- data/lib/bolt_spec/plans/action_stubs.rb +3 -2
- data/lib/bolt_spec/plans/action_stubs/download_stub.rb +66 -0
- data/lib/bolt_spec/plans/mock_executor.rb +14 -1
- data/lib/bolt_spec/run.rb +22 -0
- data/libexec/apply_catalog.rb +2 -2
- data/libexec/bolt_catalog +4 -3
- data/libexec/custom_facts.rb +1 -1
- data/libexec/query_resources.rb +1 -1
- data/modules/secure_env_vars/plans/init.pp +20 -0
- metadata +8 -2
data/lib/bolt/catalog.rb
CHANGED
@@ -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
|
-
|
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 =
|
149
|
+
definitions = case ast
|
150
|
+
when Puppet::Pops::Model::BlockExpression
|
142
151
|
ast.statements.select { |st| st.is_a?(Puppet::Pops::Model::NodeDefinition) }
|
143
|
-
|
152
|
+
when Puppet::Pops::Model::NodeDefinition
|
144
153
|
[ast]
|
145
154
|
else
|
146
155
|
[]
|
data/lib/bolt/cli.rb
CHANGED
@@ -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
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
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
|
-
|
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
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
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
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
331
|
-
|
332
|
-
|
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
|
-
|
359
|
-
|
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
|
-
|
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
|
-
|
400
|
+
when 'inventory'
|
372
401
|
if options[:detail]
|
373
402
|
show_targets
|
374
403
|
else
|
375
404
|
list_targets
|
376
405
|
end
|
377
|
-
|
406
|
+
when 'group'
|
378
407
|
list_groups
|
379
408
|
end
|
380
409
|
return 0
|
381
|
-
|
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
|
-
|
426
|
+
case options[:action]
|
427
|
+
when 'init'
|
395
428
|
code = initialize_project
|
396
|
-
|
429
|
+
when 'migrate'
|
397
430
|
code = migrate_project
|
398
431
|
end
|
399
432
|
when 'plan'
|
400
|
-
|
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
|
-
|
440
|
+
case options[:action]
|
441
|
+
when 'generate-types'
|
403
442
|
code = generate_types
|
404
|
-
|
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
|
-
|
450
|
-
|
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.
|
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.
|
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
|
-
|
611
|
-
|
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
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
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
|
-
|
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
|