bolt 2.19.0 → 2.24.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/exe/bolt +1 -0
- data/guides/inventory.txt +19 -0
- data/guides/project.txt +22 -0
- data/lib/bolt/analytics.rb +5 -5
- data/lib/bolt/applicator.rb +4 -3
- data/lib/bolt/bolt_option_parser.rb +100 -27
- data/lib/bolt/catalog.rb +12 -3
- data/lib/bolt/cli.rb +356 -156
- data/lib/bolt/config.rb +2 -2
- data/lib/bolt/config/options.rb +18 -4
- data/lib/bolt/executor.rb +30 -7
- data/lib/bolt/inventory/group.rb +6 -5
- data/lib/bolt/inventory/inventory.rb +4 -3
- data/lib/bolt/logger.rb +3 -4
- data/lib/bolt/module.rb +2 -1
- data/lib/bolt/outputter.rb +56 -0
- data/lib/bolt/outputter/human.rb +10 -9
- data/lib/bolt/outputter/json.rb +11 -4
- data/lib/bolt/outputter/logger.rb +2 -2
- data/lib/bolt/outputter/rainbow.rb +18 -2
- data/lib/bolt/pal.rb +13 -11
- data/lib/bolt/pal/yaml_plan/evaluator.rb +22 -1
- 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/pal/yaml_plan/transpiler.rb +11 -3
- data/lib/bolt/plugin/prompt.rb +3 -3
- data/lib/bolt/plugin/puppetdb.rb +3 -2
- data/lib/bolt/project.rb +7 -4
- data/lib/bolt/project_migrate.rb +138 -0
- 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 +31 -11
- 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 +28 -10
- data/lib/bolt/transport/local/connection.rb +15 -2
- data/lib/bolt/transport/orch.rb +15 -3
- data/lib/bolt/transport/simple.rb +6 -0
- data/lib/bolt/transport/ssh/connection.rb +13 -5
- data/lib/bolt/transport/ssh/exec_connection.rb +24 -3
- data/lib/bolt/transport/winrm/connection.rb +125 -15
- data/lib/bolt/util.rb +27 -12
- data/lib/bolt/util/puppet_log_level.rb +4 -3
- data/lib/bolt/version.rb +1 -1
- data/lib/bolt_server/base_config.rb +1 -1
- data/lib/bolt_server/pe/pal.rb +1 -1
- data/lib/bolt_server/transport_app.rb +79 -2
- data/lib/bolt_spec/bolt_context.rb +7 -2
- data/lib/bolt_spec/plans.rb +16 -3
- 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 +11 -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
@@ -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
|
-
'file' => %w[upload],
|
35
|
+
'plan' => %w[show run convert new],
|
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[]
|
42
|
+
'apply' => %w[],
|
43
|
+
'guide' => %w[] }.freeze
|
42
44
|
|
43
45
|
attr_reader :config, :options
|
44
46
|
|
@@ -75,72 +77,100 @@ module Bolt
|
|
75
77
|
end
|
76
78
|
private :help?
|
77
79
|
|
80
|
+
# Wrapper method that is called by the Bolt executable. Parses the command and
|
81
|
+
# then loads the project and config. Once config is loaded, it completes the
|
82
|
+
# setup process by configuring Bolt and issuing warnings.
|
83
|
+
#
|
84
|
+
# This separation is needed since the Bolt::Outputter class that normally handles
|
85
|
+
# printing errors relies on config being loaded. All setup that happens before
|
86
|
+
# config is loaded will have errors printed directly to stdout, while all errors
|
87
|
+
# raised after config is loaded are handled by the outputter.
|
78
88
|
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
|
89
|
+
parse_command
|
90
|
+
load_config
|
91
|
+
finalize_setup
|
92
|
+
end
|
89
93
|
|
90
|
-
|
94
|
+
# Parses the command and validates options. All errors that are raised here
|
95
|
+
# are not handled by the outputter, as it relies on config being loaded.
|
96
|
+
def parse_command
|
97
|
+
parser = BoltOptionParser.new(options)
|
98
|
+
# This part aims to handle both `bolt <mode> --help` and `bolt help <mode>`.
|
99
|
+
remaining = handle_parser_errors { parser.permute(@argv) } unless @argv.empty?
|
100
|
+
if @argv.empty? || help?(remaining)
|
101
|
+
# Update the parser for the subcommand (or lack thereof)
|
102
|
+
parser.update
|
103
|
+
puts parser.help
|
104
|
+
raise Bolt::CLIExit
|
105
|
+
end
|
91
106
|
|
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] = {}
|
107
|
+
options[:object] = remaining.shift
|
108
|
+
|
109
|
+
# Only parse task_options for task or plan
|
110
|
+
if %w[task plan].include?(options[:subcommand])
|
111
|
+
task_options, remaining = remaining.partition { |s| s =~ /.+=/ }
|
112
|
+
if options[:task_options]
|
113
|
+
unless task_options.empty?
|
114
|
+
raise Bolt::CLIError,
|
115
|
+
"Parameters must be specified through either the --params " \
|
116
|
+
"option or param=value pairs, not both"
|
108
117
|
end
|
118
|
+
options[:params_parsed] = true
|
119
|
+
elsif task_options.any?
|
120
|
+
options[:params_parsed] = false
|
121
|
+
options[:task_options] = Hash[task_options.map { |a| a.split('=', 2) }]
|
122
|
+
else
|
123
|
+
options[:params_parsed] = true
|
124
|
+
options[:task_options] = {}
|
109
125
|
end
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
126
|
+
end
|
127
|
+
options[:leftovers] = remaining
|
128
|
+
|
129
|
+
# Default to verbose for everything except plans
|
130
|
+
unless options.key?(:verbose)
|
131
|
+
options[:verbose] = options[:subcommand] != 'plan'
|
132
|
+
end
|
133
|
+
|
134
|
+
validate(options)
|
135
|
+
|
136
|
+
# Deprecation warnings can't be issued until after config is loaded, so
|
137
|
+
# store them for later.
|
138
|
+
@parser_deprecations = parser.deprecations
|
139
|
+
rescue Bolt::Error => e
|
140
|
+
fatal_error(e)
|
141
|
+
raise e
|
142
|
+
end
|
143
|
+
|
144
|
+
# Loads the project and configuration. All errors that are raised here are not
|
145
|
+
# handled by the outputter, as it relies on config being loaded.
|
146
|
+
def load_config
|
147
|
+
@config = if ENV['BOLT_PROJECT']
|
148
|
+
project = Bolt::Project.create_project(ENV['BOLT_PROJECT'], 'environment')
|
149
|
+
Bolt::Config.from_project(project, options)
|
150
|
+
elsif options[:configfile]
|
151
|
+
Bolt::Config.from_file(options[:configfile], options)
|
152
|
+
else
|
153
|
+
project = if options[:boltdir]
|
154
|
+
dir = Pathname.new(options[:boltdir])
|
155
|
+
if (dir + Bolt::Project::BOLTDIR_NAME).directory?
|
156
|
+
Bolt::Project.create_project(dir + Bolt::Project::BOLTDIR_NAME)
|
127
157
|
else
|
128
|
-
Bolt::Project.
|
158
|
+
Bolt::Project.create_project(dir)
|
129
159
|
end
|
130
|
-
|
131
|
-
|
160
|
+
else
|
161
|
+
Bolt::Project.find_boltdir(Dir.pwd)
|
162
|
+
end
|
163
|
+
Bolt::Config.from_project(project, options)
|
164
|
+
end
|
165
|
+
rescue Bolt::Error => e
|
166
|
+
fatal_error(e)
|
167
|
+
raise e
|
168
|
+
end
|
132
169
|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
# Print the error message in red, mimicking outputter.fatal_error
|
138
|
-
$stdout.puts("\033[31m#{e.message}\033[0m")
|
139
|
-
else
|
140
|
-
$stdout.puts(e.message)
|
141
|
-
end
|
142
|
-
raise e
|
143
|
-
end
|
170
|
+
# Completes the setup process by configuring Bolt and issuing warnings
|
171
|
+
def finalize_setup
|
172
|
+
Bolt::Logger.configure(config.log, config.color)
|
173
|
+
Bolt::Logger.analytics = analytics
|
144
174
|
|
145
175
|
# Logger must be configured before checking path case and project file, otherwise warnings will not display
|
146
176
|
config.check_path_case('modulepath', config.modulepath)
|
@@ -151,28 +181,11 @@ module Bolt
|
|
151
181
|
|
152
182
|
# Display warnings created during parser and config initialization
|
153
183
|
config.warnings.each { |warning| @logger.warn(warning[:msg]) }
|
154
|
-
|
184
|
+
@parser_deprecations.each { |dep| Bolt::Logger.deprecation_warning(dep[:type], dep[:msg]) }
|
155
185
|
config.deprecations.each { |dep| Bolt::Logger.deprecation_warning(dep[:type], dep[:msg]) }
|
156
186
|
|
157
|
-
# After validation, initialize inventory and targets. Errors here are better to catch early.
|
158
|
-
# After this step
|
159
|
-
# options[:target_args] will contain a string/array version of the targetting options this is passed to plans
|
160
|
-
# options[:targets] will contain a resolved set of Target objects
|
161
|
-
unless options[:subcommand] == 'puppetfile' ||
|
162
|
-
options[:subcommand] == 'secret' ||
|
163
|
-
options[:subcommand] == 'project' ||
|
164
|
-
options[:action] == 'show' ||
|
165
|
-
options[:action] == 'convert'
|
166
|
-
|
167
|
-
update_targets(options)
|
168
|
-
end
|
169
|
-
|
170
|
-
unless options.key?(:verbose)
|
171
|
-
# Default to verbose for everything except plans
|
172
|
-
options[:verbose] = options[:subcommand] != 'plan'
|
173
|
-
end
|
174
|
-
|
175
187
|
warn_inventory_overrides_cli(options)
|
188
|
+
|
176
189
|
options
|
177
190
|
rescue Bolt::Error => e
|
178
191
|
outputter.fatal_error(e)
|
@@ -247,6 +260,13 @@ module Bolt
|
|
247
260
|
"Option '--noop' may only be specified when running a task or applying manifest code"
|
248
261
|
end
|
249
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
|
+
|
250
270
|
if options[:subcommand] == 'apply' && (options[:object] && options[:code])
|
251
271
|
raise Bolt::CLIError, "--execute is unsupported when specifying a manifest file"
|
252
272
|
end
|
@@ -265,6 +285,10 @@ module Bolt
|
|
265
285
|
raise Bolt::CLIError, "Must specify a value to #{options[:action]}"
|
266
286
|
end
|
267
287
|
|
288
|
+
if options[:subcommand] == 'plan' && options[:action] == 'new' && !options[:object]
|
289
|
+
raise Bolt::CLIError, "Must specify a plan name."
|
290
|
+
end
|
291
|
+
|
268
292
|
if options.key?(:debug) && options.key?(:log)
|
269
293
|
raise Bolt::CLIError, "Only one of '--debug' or '--log-level' may be specified"
|
270
294
|
end
|
@@ -329,9 +353,12 @@ module Bolt
|
|
329
353
|
exit!
|
330
354
|
end
|
331
355
|
|
332
|
-
|
333
|
-
|
334
|
-
|
356
|
+
# Initialize inventory and targets. Errors here are better to catch early.
|
357
|
+
# options[:target_args] will contain a string/array version of the targetting options this is passed to plans
|
358
|
+
# options[:targets] will contain a resolved set of Target objects
|
359
|
+
unless %w[project puppetfile secret guide].include?(options[:subcommand]) ||
|
360
|
+
%w[convert new show].include?(options[:action])
|
361
|
+
update_targets(options)
|
335
362
|
end
|
336
363
|
|
337
364
|
screen = "#{options[:subcommand]}_#{options[:action]}"
|
@@ -357,32 +384,37 @@ module Bolt
|
|
357
384
|
|
358
385
|
analytics.screen_view(screen, screen_view_fields)
|
359
386
|
|
360
|
-
|
361
|
-
|
387
|
+
case options[:action]
|
388
|
+
when 'show'
|
389
|
+
case options[:subcommand]
|
390
|
+
when 'task'
|
362
391
|
if options[:object]
|
363
392
|
show_task(options[:object])
|
364
393
|
else
|
365
394
|
list_tasks
|
366
395
|
end
|
367
|
-
|
396
|
+
when 'plan'
|
368
397
|
if options[:object]
|
369
398
|
show_plan(options[:object])
|
370
399
|
else
|
371
400
|
list_plans
|
372
401
|
end
|
373
|
-
|
402
|
+
when 'inventory'
|
374
403
|
if options[:detail]
|
375
404
|
show_targets
|
376
405
|
else
|
377
406
|
list_targets
|
378
407
|
end
|
379
|
-
|
408
|
+
when 'group'
|
380
409
|
list_groups
|
381
410
|
end
|
382
411
|
return 0
|
383
|
-
|
412
|
+
when 'show-modules'
|
384
413
|
list_modules
|
385
414
|
return 0
|
415
|
+
when 'convert'
|
416
|
+
pal.convert_plan(options[:object])
|
417
|
+
return 0
|
386
418
|
end
|
387
419
|
|
388
420
|
message = 'There may be processes left executing on some nodes.'
|
@@ -392,18 +424,33 @@ module Bolt
|
|
392
424
|
end
|
393
425
|
|
394
426
|
case options[:subcommand]
|
427
|
+
when 'guide'
|
428
|
+
code = if options[:object]
|
429
|
+
show_guide(options[:object])
|
430
|
+
else
|
431
|
+
list_topics
|
432
|
+
end
|
395
433
|
when 'project'
|
396
|
-
|
434
|
+
case options[:action]
|
435
|
+
when 'init'
|
397
436
|
code = initialize_project
|
398
|
-
|
399
|
-
|
437
|
+
when 'migrate'
|
438
|
+
inv = config.inventoryfile
|
439
|
+
path = config.project.path
|
440
|
+
code = Bolt::ProjectMigrate.new(path, outputter, inv).migrate_project
|
400
441
|
end
|
401
442
|
when 'plan'
|
402
|
-
|
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
|
403
449
|
when 'puppetfile'
|
404
|
-
|
450
|
+
case options[:action]
|
451
|
+
when 'generate-types'
|
405
452
|
code = generate_types
|
406
|
-
|
453
|
+
when 'install'
|
407
454
|
code = install_puppetfile(config.puppetfile_config, config.puppetfile, config.modulepath)
|
408
455
|
end
|
409
456
|
when 'secret'
|
@@ -424,6 +471,7 @@ module Bolt
|
|
424
471
|
elapsed_time = Benchmark.realtime do
|
425
472
|
executor_opts = {}
|
426
473
|
executor_opts[:description] = options[:description] if options.key?(:description)
|
474
|
+
executor_opts[:env_vars] = options[:env_vars] if options.key?(:env_vars)
|
427
475
|
executor.subscribe(outputter)
|
428
476
|
executor.subscribe(log_outputter)
|
429
477
|
results =
|
@@ -445,11 +493,22 @@ module Bolt
|
|
445
493
|
src = options[:object]
|
446
494
|
dest = options[:leftovers].first
|
447
495
|
|
496
|
+
if src.nil?
|
497
|
+
raise Bolt::CLIError, "A source path must be specified"
|
498
|
+
end
|
499
|
+
|
448
500
|
if dest.nil?
|
449
501
|
raise Bolt::CLIError, "A destination path must be specified"
|
450
502
|
end
|
451
|
-
|
452
|
-
|
503
|
+
|
504
|
+
case options[:action]
|
505
|
+
when 'download'
|
506
|
+
dest = File.expand_path(dest, Dir.pwd)
|
507
|
+
executor.download_file(targets, src, dest, executor_opts)
|
508
|
+
when 'upload'
|
509
|
+
validate_file('source file', src, true)
|
510
|
+
executor.upload_file(targets, src, dest, executor_opts)
|
511
|
+
end
|
453
512
|
end
|
454
513
|
end
|
455
514
|
|
@@ -477,7 +536,7 @@ module Bolt
|
|
477
536
|
tasks = pal.list_tasks
|
478
537
|
tasks.select! { |task| task.first.include?(options[:filter]) } if options[:filter]
|
479
538
|
tasks.select! { |task| config.project.tasks.include?(task.first) } unless config.project.tasks.nil?
|
480
|
-
outputter.print_tasks(tasks, pal.
|
539
|
+
outputter.print_tasks(tasks, pal.user_modulepath)
|
481
540
|
end
|
482
541
|
|
483
542
|
def show_plan(plan_name)
|
@@ -488,7 +547,7 @@ module Bolt
|
|
488
547
|
plans = pal.list_plans
|
489
548
|
plans.select! { |plan| plan.first.include?(options[:filter]) } if options[:filter]
|
490
549
|
plans.select! { |plan| config.project.plans.include?(plan.first) } unless config.project.plans.nil?
|
491
|
-
outputter.print_plans(plans, pal.
|
550
|
+
outputter.print_plans(plans, pal.user_modulepath)
|
492
551
|
end
|
493
552
|
|
494
553
|
def list_targets
|
@@ -506,6 +565,118 @@ module Bolt
|
|
506
565
|
outputter.print_groups(groups)
|
507
566
|
end
|
508
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
|
+
|
509
680
|
def run_plan(plan_name, plan_arguments, nodes, options)
|
510
681
|
unless nodes.empty?
|
511
682
|
if plan_arguments['nodes'] || plan_arguments['targets']
|
@@ -609,8 +780,26 @@ module Bolt
|
|
609
780
|
# Initializes a specified directory as a Bolt project and installs any modules
|
610
781
|
# specified by the user, along with their dependencies
|
611
782
|
def initialize_project
|
612
|
-
|
613
|
-
|
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'
|
614
803
|
puppetfile = project + 'Puppetfile'
|
615
804
|
modulepath = [project + 'modules']
|
616
805
|
|
@@ -631,18 +820,24 @@ module Bolt
|
|
631
820
|
|
632
821
|
# Warn the user if the project directory already exists. We don't error here since users
|
633
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
|
634
826
|
if config.exist?
|
635
|
-
@logger.warn "Found existing project directory at #{project}"
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
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}."
|
641
832
|
# Bless the project directory as a...wait for it...project
|
642
|
-
if FileUtils.touch(config)
|
643
|
-
outputter.print_message "Successfully created Bolt project at #{project}"
|
644
833
|
else
|
645
|
-
|
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
|
646
841
|
end
|
647
842
|
|
648
843
|
# Write the generated Puppetfile to the fancy new project
|
@@ -718,49 +913,6 @@ module Bolt
|
|
718
913
|
end
|
719
914
|
end
|
720
915
|
|
721
|
-
def migrate_project
|
722
|
-
inventory_file = config.inventoryfile || config.default_inventoryfile
|
723
|
-
data = Bolt::Util.read_yaml_hash(inventory_file, 'inventory')
|
724
|
-
|
725
|
-
data.delete('version') if data['version'] != 2
|
726
|
-
|
727
|
-
migrated = migrate_group(data)
|
728
|
-
|
729
|
-
ok = File.write(inventory_file, data.to_yaml) if migrated
|
730
|
-
|
731
|
-
result = if migrated && ok
|
732
|
-
"Successfully migrated Bolt project to latest version."
|
733
|
-
elsif !migrated
|
734
|
-
"Bolt project already on latest version. Nothing to do."
|
735
|
-
else
|
736
|
-
"Could not migrate Bolt project to latest version."
|
737
|
-
end
|
738
|
-
outputter.print_message result
|
739
|
-
|
740
|
-
ok ? 0 : 1
|
741
|
-
end
|
742
|
-
|
743
|
-
# Walks an inventory hash and replaces all 'nodes' keys with 'targets' keys
|
744
|
-
# and all 'name' keys nested in a 'targets' hash with 'uri' keys. Data is
|
745
|
-
# modified in place.
|
746
|
-
def migrate_group(group)
|
747
|
-
migrated = false
|
748
|
-
if group.key?('nodes')
|
749
|
-
migrated = true
|
750
|
-
targets = group['nodes'].map do |target|
|
751
|
-
target['uri'] = target.delete('name') if target.is_a?(Hash)
|
752
|
-
target
|
753
|
-
end
|
754
|
-
group.delete('nodes')
|
755
|
-
group['targets'] = targets
|
756
|
-
end
|
757
|
-
(group['groups'] || []).each do |subgroup|
|
758
|
-
migrated_group = migrate_group(subgroup)
|
759
|
-
migrated ||= migrated_group
|
760
|
-
end
|
761
|
-
migrated
|
762
|
-
end
|
763
|
-
|
764
916
|
def install_puppetfile(config, puppetfile, modulepath)
|
765
917
|
require 'r10k/cli'
|
766
918
|
require 'bolt/r10k_log_proxy'
|
@@ -803,8 +955,46 @@ module Bolt
|
|
803
955
|
config.project)
|
804
956
|
end
|
805
957
|
|
806
|
-
|
807
|
-
|
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
|
808
998
|
end
|
809
999
|
|
810
1000
|
def validate_file(type, path, allow_dir = false)
|
@@ -871,7 +1061,7 @@ module Bolt
|
|
871
1061
|
msg = <<~MSG.chomp
|
872
1062
|
Loaded configuration from: '#{config.config_files.join("', '")}'
|
873
1063
|
MSG
|
874
|
-
@logger.
|
1064
|
+
@logger.info(msg)
|
875
1065
|
end
|
876
1066
|
|
877
1067
|
# Gem installs include the aggregate, canary, and puppetdb_fact modules, while
|
@@ -879,5 +1069,15 @@ module Bolt
|
|
879
1069
|
def incomplete_install?
|
880
1070
|
(Dir.children(Bolt::PAL::MODULES_PATH) - %w[aggregate canary puppetdb_fact]).empty?
|
881
1071
|
end
|
1072
|
+
|
1073
|
+
# Mimicks the output from Outputter::Human#fatal_error. This should be used to print
|
1074
|
+
# errors prior to config being loaded, as the outputter relies on config being loaded.
|
1075
|
+
def fatal_error(error)
|
1076
|
+
if $stdout.isatty
|
1077
|
+
$stdout.puts("\033[31m#{error.message}\033[0m")
|
1078
|
+
else
|
1079
|
+
$stdout.puts(error.message)
|
1080
|
+
end
|
1081
|
+
end
|
882
1082
|
end
|
883
1083
|
end
|