bolt 2.21.0 → 2.25.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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +1 -1
  3. data/bolt-modules/ctrl/lib/puppet/functions/ctrl/do_until.rb +12 -6
  4. data/bolt-modules/out/lib/puppet/functions/out/message.rb +1 -1
  5. data/exe/bolt +1 -0
  6. data/guides/inventory.txt +19 -0
  7. data/guides/project.txt +22 -0
  8. data/lib/bolt/analytics.rb +5 -5
  9. data/lib/bolt/applicator.rb +4 -3
  10. data/lib/bolt/bolt_option_parser.rb +64 -23
  11. data/lib/bolt/catalog.rb +9 -1
  12. data/lib/bolt/cli.rb +218 -73
  13. data/lib/bolt/config.rb +7 -0
  14. data/lib/bolt/config/options.rb +4 -4
  15. data/lib/bolt/executor.rb +9 -7
  16. data/lib/bolt/inventory/group.rb +3 -3
  17. data/lib/bolt/logger.rb +3 -4
  18. data/lib/bolt/module.rb +2 -1
  19. data/lib/bolt/outputter.rb +56 -0
  20. data/lib/bolt/outputter/human.rb +10 -9
  21. data/lib/bolt/outputter/json.rb +11 -4
  22. data/lib/bolt/outputter/logger.rb +2 -2
  23. data/lib/bolt/outputter/rainbow.rb +15 -0
  24. data/lib/bolt/pal.rb +5 -9
  25. data/lib/bolt/pal/yaml_plan/evaluator.rb +1 -1
  26. data/lib/bolt/pal/yaml_plan/transpiler.rb +11 -3
  27. data/lib/bolt/plugin/prompt.rb +3 -3
  28. data/lib/bolt/project.rb +6 -4
  29. data/lib/bolt/project_migrate.rb +138 -0
  30. data/lib/bolt/shell/bash.rb +7 -7
  31. data/lib/bolt/transport/docker/connection.rb +9 -9
  32. data/lib/bolt/transport/local/connection.rb +2 -2
  33. data/lib/bolt/transport/orch.rb +3 -3
  34. data/lib/bolt/transport/ssh/connection.rb +5 -5
  35. data/lib/bolt/transport/ssh/exec_connection.rb +4 -4
  36. data/lib/bolt/transport/winrm/connection.rb +8 -8
  37. data/lib/bolt/util.rb +1 -1
  38. data/lib/bolt/util/puppet_log_level.rb +4 -3
  39. data/lib/bolt/version.rb +1 -1
  40. data/lib/bolt_server/base_config.rb +1 -1
  41. data/lib/bolt_server/pe/pal.rb +1 -1
  42. data/lib/bolt_server/transport_app.rb +76 -0
  43. data/lib/bolt_spec/plans.rb +1 -1
  44. data/lib/bolt_spec/plans/action_stubs.rb +1 -1
  45. data/lib/bolt_spec/run.rb +3 -0
  46. data/libexec/apply_catalog.rb +2 -2
  47. data/libexec/bolt_catalog +1 -1
  48. data/libexec/custom_facts.rb +1 -1
  49. data/libexec/query_resources.rb +1 -1
  50. metadata +7 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ef2991c1d3979e03b9070dc168be0e8c44c309914fd7eb02badc60e5b5d907bf
4
- data.tar.gz: 9508d434488486b8b16d062f910f6d3bcf8d7b3e89121ccb8033dd393a7a21db
3
+ metadata.gz: 8bfb8de382e2a2ab6931f43ffce1eb97187fda6e2e56036b20637afb8ae8da2d
4
+ data.tar.gz: 56cf9c3fda7f29434413feb8b7f2eb3b358c1ed4316f2d5f4e1e47875a6919c5
5
5
  SHA512:
6
- metadata.gz: 351084ca010d6f3d051a588e0d216f07d32473026703378dd7003c9f71f411fa2499926d4e1c6272df55cffc8eeb360fbc6fff4c5a0930c14cba006dfbf3874c
7
- data.tar.gz: eef7d1b6b0dfe5584bd216bb5ebe421b61b9e25a4131509cf310f33fc75f7a3afa072e919215eefadb43ac7415cdd51546e6b634daf53d8a7a568366bb31ee68
6
+ metadata.gz: ec5bf195ad19bac051942f2689e58a325982a8c47b12d8970e1ed6a6758e714d8f22ce5275dc3fb7160ea6d06e7ee63bf528b2f18d67ee754b9ae41c3e92a7c5
7
+ data.tar.gz: cc6a87ed720484d04c7bb2275cd6812ac211733e2efce265b69cfb5ee5614fa743122ce50369079697ad08374eccea6e9797e8398176d7669846887b0f0832aa
data/Puppetfile CHANGED
@@ -6,7 +6,7 @@ moduledir File.join(File.dirname(__FILE__), 'modules')
6
6
 
7
7
  # Core modules used by 'apply'
8
8
  mod 'puppetlabs-service', '1.3.0'
9
- mod 'puppetlabs-puppet_agent', '3.2.0'
9
+ mod 'puppetlabs-puppet_agent', '4.1.1'
10
10
  mod 'puppetlabs-facts', '1.0.0'
11
11
 
12
12
  # Core types and providers for Puppet 6
@@ -4,30 +4,36 @@
4
4
  Puppet::Functions.create_function(:'ctrl::do_until') do
5
5
  # @param options A hash of additional options.
6
6
  # @option options [Numeric] limit The number of times to repeat the block.
7
+ # @option options [Numeric] interval The number of seconds to wait before repeating the block.
7
8
  # @example Run a task until it succeeds
8
9
  # ctrl::do_until() || {
9
- # run_task('test', $target, _catch_errors => true).ok()
10
+ # run_task('test', $target, '_catch_errors' => true).ok()
10
11
  # }
11
- #
12
12
  # @example Run a task until it succeeds or fails 10 times
13
13
  # ctrl::do_until('limit' => 10) || {
14
- # run_task('test', $target, _catch_errors => true).ok()
14
+ # run_task('test', $target, '_catch_errors' => true).ok()
15
+ # }
16
+ # @example Run a task and wait 10 seconds before running it again
17
+ # ctrl::do_until('interval' => 10) || {
18
+ # run_task('test', $target, '_catch_errors' => true).ok()
15
19
  # }
16
- #
17
20
  dispatch :do_until do
18
21
  optional_param 'Hash[String[1], Any]', :options
19
22
  block_param
20
23
  end
21
24
 
22
- def do_until(options = { 'limit' => 0 })
25
+ def do_until(options = {})
23
26
  # Send Analytics Report
24
27
  Puppet.lookup(:bolt_executor) {}&.report_function_call(self.class.name)
25
28
 
26
- limit = options['limit']
29
+ limit = options['limit'] || 0
30
+ interval = options['interval']
31
+
27
32
  i = 0
28
33
  until (x = yield)
29
34
  i += 1
30
35
  break if limit != 0 && i >= limit
36
+ Kernel.sleep(interval) if interval
31
37
  end
32
38
  x
33
39
  end
@@ -12,7 +12,7 @@ Puppet::Functions.create_function(:'out::message') do
12
12
  # @example Print a message
13
13
  # out::message('Something went wrong')
14
14
  dispatch :output_message do
15
- param 'String', :message
15
+ param 'Any', :message
16
16
  return_type 'Undef'
17
17
  end
18
18
 
data/exe/bolt CHANGED
@@ -4,6 +4,7 @@
4
4
  require 'bolt'
5
5
  require 'bolt/cli'
6
6
 
7
+ Thread.current[:name] ||= 'main'
7
8
  cli = Bolt::CLI.new(ARGV)
8
9
  begin
9
10
  opts = cli.parse
@@ -0,0 +1,19 @@
1
+ TOPIC
2
+ inventory
3
+
4
+ DESCRIPTION
5
+ The inventory describes the targets that you run Bolt commands on, along
6
+ with any data and configuration for the targets. Targets in an inventory can
7
+ belong to one or more groups, allowing you to share data and configuration
8
+ across multiple targets and to specify multiple targets for your Bolt
9
+ commands without the need to list each target individually.
10
+
11
+ In most cases, Bolt loads the inventory from an inventory file in your Bolt
12
+ project. The inventory file is a YAML file named 'inventory.yaml'. Because
13
+ Bolt loads the inventory file from a Bolt project, you must have an existing
14
+ project configuration file named 'bolt-project.yaml' alongside the inventory
15
+ file.
16
+
17
+ DOCUMENTATION
18
+ https://pup.pt/bolt-inventory
19
+ https://pup.pt/bolt-inventory-reference
@@ -0,0 +1,22 @@
1
+ TOPIC
2
+ project
3
+
4
+ DESCRIPTION
5
+ A Bolt project is a directory that serves as the launching point for Bolt
6
+ and allows you to create a shareable orchestration application. Projects
7
+ typically include a project configuration file, an inventory file, and any
8
+ content you use in your project workflow, such as tasks and plans.
9
+
10
+ When you run Bolt, it runs in the context of a project. If the directory you
11
+ run Bolt from is not a project, Bolt attempts to find a project by
12
+ traversing the parent directories. If Bolt is unable to find a project, it
13
+ runs from the default project, located at '~/.puppetlabs/bolt'.
14
+
15
+ A directory is only considered a Bolt project when it has a project
16
+ configuration file named 'bolt-project.yaml'. Bolt doesn't load project data
17
+ and content, including inventory files, unless the data and content are part
18
+ of a project.
19
+
20
+ DOCUMENTATION
21
+ https://pup.pt/bolt-projects
22
+ https://pup.pt/bolt-project-reference
@@ -72,7 +72,7 @@ module Bolt
72
72
 
73
73
  def self.load_config(filename, logger)
74
74
  if File.exist?(filename)
75
- YAML.load_file(filename)
75
+ Bolt::Util.read_optional_yaml_hash(filename, 'analytics')
76
76
  else
77
77
  unless ENV['BOLT_DISABLE_ANALYTICS']
78
78
  logger.warn <<~ANALYTICS
@@ -161,9 +161,9 @@ module Bolt
161
161
  # Handle analytics submission in the background to avoid blocking the
162
162
  # app or polluting the log with errors
163
163
  Concurrent::Future.execute(executor: @executor) do
164
- @logger.debug "Submitting analytics: #{JSON.pretty_generate(params)}"
164
+ @logger.trace "Submitting analytics: #{JSON.pretty_generate(params)}"
165
165
  @http.post(TRACKING_URL, params)
166
- @logger.debug "Completed analytics submission"
166
+ @logger.trace "Completed analytics submission"
167
167
  end
168
168
  end
169
169
 
@@ -215,13 +215,13 @@ module Bolt
215
215
  end
216
216
 
217
217
  def screen_view(screen, **_kwargs)
218
- @logger.debug "Skipping submission of '#{screen}' screenview because analytics is disabled"
218
+ @logger.trace "Skipping submission of '#{screen}' screenview because analytics is disabled"
219
219
  end
220
220
 
221
221
  def report_bundled_content(mode, name); end
222
222
 
223
223
  def event(category, action, **_kwargs)
224
- @logger.debug "Skipping submission of '#{category} #{action}' event because analytics is disabled"
224
+ @logger.trace "Skipping submission of '#{category} #{action}' event because analytics is disabled"
225
225
  end
226
226
 
227
227
  def finish; end
@@ -27,7 +27,7 @@ module Bolt
27
27
  @hiera_config = hiera_config ? validate_hiera_config(hiera_config) : nil
28
28
  @apply_settings = apply_settings || {}
29
29
 
30
- @pool = Concurrent::ThreadPoolExecutor.new(max_threads: max_compiles)
30
+ @pool = Concurrent::ThreadPoolExecutor.new(name: 'apply', max_threads: max_compiles)
31
31
  @logger = Logging.logger[self]
32
32
  end
33
33
 
@@ -217,6 +217,7 @@ module Bolt
217
217
  r = @executor.log_action(description, targets) do
218
218
  futures = targets.map do |target|
219
219
  Concurrent::Future.execute(executor: @pool) do
220
+ Thread.current[:name] ||= Thread.current.name
220
221
  @executor.with_node_logging("Compiling manifest block", [target]) do
221
222
  compile(target, scope)
222
223
  end
@@ -300,7 +301,7 @@ module Bolt
300
301
 
301
302
  files.each do |file|
302
303
  tar_path = Pathname.new(file).relative_path_from(parent)
303
- @logger.debug("Packing plugin #{file} to #{tar_path}")
304
+ @logger.trace("Packing plugin #{file} to #{tar_path}")
304
305
  stat = File.stat(file)
305
306
  content = File.binread(file)
306
307
  output.tar.add_file_simple(
@@ -314,7 +315,7 @@ module Bolt
314
315
  end
315
316
 
316
317
  duration = Time.now - start_time
317
- @logger.debug("Packed plugins in #{duration * 1000} ms")
318
+ @logger.trace("Packed plugins in #{duration * 1000} ms")
318
319
 
319
320
  output.close
320
321
  Base64.encode64(sio.string)
@@ -61,11 +61,17 @@ module Bolt
61
61
  { flags: OPTIONS[:global],
62
62
  banner: GROUP_HELP }
63
63
  end
64
+ when 'guide'
65
+ { flags: OPTIONS[:global] + %w[format],
66
+ banner: GUIDE_HELP }
64
67
  when 'plan'
65
68
  case action
66
69
  when 'convert'
67
70
  { flags: OPTIONS[:global] + OPTIONS[:global_config_setters],
68
71
  banner: PLAN_CONVERT_HELP }
72
+ when 'new'
73
+ { flags: OPTIONS[:global] + %w[configfile project],
74
+ banner: PLAN_NEW_HELP }
69
75
  when 'run'
70
76
  { flags: ACTION_OPTS + %w[params compile-concurrency tmpdir hiera-config],
71
77
  banner: PLAN_RUN_HELP }
@@ -82,7 +88,7 @@ module Bolt
82
88
  { flags: OPTIONS[:global] + %w[modules],
83
89
  banner: PROJECT_INIT_HELP }
84
90
  when 'migrate'
85
- { flags: OPTIONS[:global] + %w[inventoryfile boltdir configfile],
91
+ { flags: OPTIONS[:global] + %w[inventoryfile project configfile],
86
92
  banner: PROJECT_MIGRATE_HELP }
87
93
  else
88
94
  { flags: OPTIONS[:global],
@@ -159,15 +165,19 @@ module Bolt
159
165
  SUBCOMMANDS
160
166
  apply Apply Puppet manifest code
161
167
  command Run a command remotely
162
- file Upload a local file or directory
168
+ file Copy files between the controller and targets
163
169
  group Show the list of groups in the inventory
170
+ guide View guides for Bolt concepts and features
164
171
  inventory Show the list of targets an action would run on
165
- plan Convert, show, and run Bolt plans
172
+ plan Convert, create, show, and run Bolt plans
166
173
  project Create and migrate Bolt projects
167
174
  puppetfile Install and list modules and generate type references
168
175
  script Upload a local script and run it remotely
169
176
  secret Create encryption keys and encrypt and decrypt values
170
177
  task Show and run Bolt tasks
178
+
179
+ GUIDES
180
+ For a list of guides on Bolt's concepts and features, run 'bolt guide'.
171
181
  HELP
172
182
 
173
183
  APPLY_HELP = <<~HELP
@@ -286,6 +296,26 @@ module Bolt
286
296
  Show the list of groups in the inventory.
287
297
  HELP
288
298
 
299
+ GUIDE_HELP = <<~HELP
300
+ NAME
301
+ guide
302
+
303
+ USAGE
304
+ bolt guide [topic] [options]
305
+
306
+ DESCRIPTION
307
+ View guides for Bolt's concepts and features.
308
+
309
+ Omitting a topic will display a list of available guides,
310
+ while providing a topic will display the relevant guide.
311
+
312
+ EXAMPLES
313
+ View a list of available guides
314
+ bolt guide
315
+ View the 'project' guide page
316
+ bolt guide project
317
+ HELP
318
+
289
319
  INVENTORY_HELP = <<~HELP
290
320
  NAME
291
321
  inventory
@@ -319,10 +349,11 @@ module Bolt
319
349
  bolt plan <action> [parameters] [options]
320
350
 
321
351
  DESCRIPTION
322
- Convert, show, and run Bolt plans.
352
+ Convert, create, show, and run Bolt plans.
323
353
 
324
354
  ACTIONS
325
355
  convert Convert a YAML plan to a Bolt plan
356
+ new Create a new plan in the current project
326
357
  run Run a plan on the specified targets
327
358
  show Show available plans and plan documentation
328
359
  HELP
@@ -345,6 +376,20 @@ module Bolt
345
376
  bolt plan convert path/to/plan/myplan.yaml
346
377
  HELP
347
378
 
379
+ PLAN_NEW_HELP = <<~HELP
380
+ NAME
381
+ new
382
+
383
+ USAGE
384
+ bolt plan new <plan> [options]
385
+
386
+ DESCRIPTION
387
+ Create a new plan in the current project.
388
+
389
+ EXAMPLES
390
+ bolt plan new myproject::myplan
391
+ HELP
392
+
348
393
  PLAN_RUN_HELP = <<~HELP
349
394
  NAME
350
395
  run
@@ -402,19 +447,18 @@ module Bolt
402
447
  init
403
448
 
404
449
  USAGE
405
- bolt project init [directory] [options]
450
+ bolt project init [name] [options]
406
451
 
407
452
  DESCRIPTION
408
- Create a new Bolt project.
453
+ Create a new Bolt project in the current working directory.
409
454
 
410
- Specify a directory to create a Bolt project in. Defaults to the
411
- curent working directory.
455
+ Specify a name for the Bolt project. Defaults to the basename of the current working directory.
412
456
 
413
457
  EXAMPLES
414
- Create a new Bolt project in the current working directory.
458
+ Create a new Bolt project using the directory as the project name.
415
459
  bolt project init
416
- Create a new Bolt project at a specified path.
417
- bolt project init ~/path/to/project
460
+ Create a new Bolt project with a specified name.
461
+ bolt project init myproject
418
462
  Create a new Bolt project with existing modules.
419
463
  bolt project init --modules puppetlabs-apt,puppetlabs-ntp
420
464
  HELP
@@ -427,10 +471,7 @@ module Bolt
427
471
  bolt project migrate [options]
428
472
 
429
473
  DESCRIPTION
430
- Migrate a Bolt project to the latest version.
431
-
432
- Loads a Bolt project's inventory file and migrates it to the latest version. The
433
- inventory file is modified in place and will not preserve comments or formatting.
474
+ Migrate a Bolt project to use current best practices and the latest version of configuration files.
434
475
  HELP
435
476
 
436
477
  PUPPETFILE_HELP = <<~HELP
@@ -676,9 +717,9 @@ module Bolt
676
717
  @options[:password] = password
677
718
  end
678
719
  define('--password-prompt', 'Prompt for user to input password') do |_password|
679
- STDERR.print "Please enter your password: "
680
- @options[:password] = STDIN.noecho(&:gets).chomp
681
- STDERR.puts
720
+ $stderr.print "Please enter your password: "
721
+ @options[:password] = $stdin.noecho(&:gets).chomp
722
+ $stderr.puts
682
723
  end
683
724
  define('--private-key KEY', 'Path to private ssh key to authenticate with') do |key|
684
725
  @options[:'private-key'] = File.expand_path(key)
@@ -702,9 +743,9 @@ module Bolt
702
743
  @options[:'sudo-password'] = password
703
744
  end
704
745
  define('--sudo-password-prompt', 'Prompt for user to input escalation password') do |_password|
705
- STDERR.print "Please enter your privilege escalation password: "
706
- @options[:'sudo-password'] = STDIN.noecho(&:gets).chomp
707
- STDERR.puts
746
+ $stderr.print "Please enter your privilege escalation password: "
747
+ @options[:'sudo-password'] = $stdin.noecho(&:gets).chomp
748
+ $stderr.puts
708
749
  end
709
750
  define('--sudo-executable EXEC', "Specify an executable for running as another user.",
710
751
  "This option is experimental.") do |exec|
@@ -846,7 +887,7 @@ module Bolt
846
887
  end
847
888
  define('--log-level LEVEL',
848
889
  "Set the log level for the console. Available options are",
849
- "debug, info, notice, warn, error, fatal, any.") do |level|
890
+ "trace, debug, info, warn, error, fatal, any.") do |level|
850
891
  @options[:log] = { 'console' => { 'level' => level } }
851
892
  end
852
893
  define('--plugin PLUGIN', 'Select the plugin to use') do |plug|
@@ -887,7 +928,7 @@ module Bolt
887
928
  file = value.sub(/^@/, '')
888
929
  read_arg_file(file)
889
930
  elsif value == '-'
890
- STDIN.read
931
+ $stdin.read
891
932
  else
892
933
  value
893
934
  end
@@ -76,7 +76,15 @@ module Bolt
76
76
  target = request['target']
77
77
  plan_vars = shadow_vars('plan', request['plan_vars'], target['facts'])
78
78
  target_vars = shadow_vars('target', target['variables'], target['facts'])
79
- topscope_vars = target_vars.merge(plan_vars)
79
+
80
+ # Merge plan vars with target vars, while maintaining the order of the plan
81
+ # vars. It's critical that the order of plan vars is not changed, as Puppet
82
+ # will deserialize the variables in the order they appear. Variables may
83
+ # contain local references to variables that appear earlier in a plan. If
84
+ # these variables are moved before the variable they reference, Puppet will
85
+ # be unable to deserialize the data and raise an error.
86
+ topscope_vars = target_vars.reject { |k, _v| plan_vars.key?(k) }.merge(plan_vars)
87
+
80
88
  env_conf = { modulepath: request['modulepath'],
81
89
  facts: target['facts'],
82
90
  variables: topscope_vars }
@@ -20,6 +20,7 @@ require 'bolt/logger'
20
20
  require 'bolt/outputter'
21
21
  require 'bolt/puppetdb'
22
22
  require 'bolt/plugin'
23
+ require 'bolt/project_migrate'
23
24
  require 'bolt/pal'
24
25
  require 'bolt/target'
25
26
  require 'bolt/version'
@@ -31,14 +32,15 @@ module Bolt
31
32
  COMMANDS = { 'command' => %w[run],
32
33
  'script' => %w[run],
33
34
  'task' => %w[show run],
34
- 'plan' => %w[show run convert],
35
+ 'plan' => %w[show run convert new],
35
36
  'file' => %w[download upload],
36
37
  'puppetfile' => %w[install show-modules generate-types],
37
38
  'secret' => %w[encrypt decrypt createkeys],
38
39
  'inventory' => %w[show],
39
40
  'group' => %w[show],
40
41
  'project' => %w[init migrate],
41
- 'apply' => %w[] }.freeze
42
+ 'apply' => %w[],
43
+ 'guide' => %w[] }.freeze
42
44
 
43
45
  attr_reader :config, :options
44
46
 
@@ -283,6 +285,10 @@ module Bolt
283
285
  raise Bolt::CLIError, "Must specify a value to #{options[:action]}"
284
286
  end
285
287
 
288
+ if options[:subcommand] == 'plan' && options[:action] == 'new' && !options[:object]
289
+ raise Bolt::CLIError, "Must specify a plan name."
290
+ end
291
+
286
292
  if options.key?(:debug) && options.key?(:log)
287
293
  raise Bolt::CLIError, "Only one of '--debug' or '--log-level' may be specified"
288
294
  end
@@ -350,19 +356,11 @@ module Bolt
350
356
  # Initialize inventory and targets. Errors here are better to catch early.
351
357
  # options[:target_args] will contain a string/array version of the targetting options this is passed to plans
352
358
  # options[:targets] will contain a resolved set of Target objects
353
- unless options[:subcommand] == 'puppetfile' ||
354
- options[:subcommand] == 'secret' ||
355
- options[:subcommand] == 'project' ||
356
- options[:action] == 'show' ||
357
- options[:action] == 'convert'
359
+ unless %w[project puppetfile secret guide].include?(options[:subcommand]) ||
360
+ %w[convert new show].include?(options[:action])
358
361
  update_targets(options)
359
362
  end
360
363
 
361
- if options[:action] == 'convert'
362
- convert_plan(options[:object])
363
- return 0
364
- end
365
-
366
364
  screen = "#{options[:subcommand]}_#{options[:action]}"
367
365
  # submit a different screen for `bolt task show` and `bolt task show foo`
368
366
  if options[:action] == 'show' && options[:object]
@@ -414,6 +412,9 @@ module Bolt
414
412
  when 'show-modules'
415
413
  list_modules
416
414
  return 0
415
+ when 'convert'
416
+ pal.convert_plan(options[:object])
417
+ return 0
417
418
  end
418
419
 
419
420
  message = 'There may be processes left executing on some nodes.'
@@ -423,15 +424,28 @@ module Bolt
423
424
  end
424
425
 
425
426
  case options[:subcommand]
427
+ when 'guide'
428
+ code = if options[:object]
429
+ show_guide(options[:object])
430
+ else
431
+ list_topics
432
+ end
426
433
  when 'project'
427
434
  case options[:action]
428
435
  when 'init'
429
436
  code = initialize_project
430
437
  when 'migrate'
431
- code = migrate_project
438
+ inv = config.inventoryfile
439
+ path = config.project.path
440
+ code = Bolt::ProjectMigrate.new(path, outputter, inv).migrate_project
432
441
  end
433
442
  when 'plan'
434
- code = run_plan(options[:object], options[:task_options], options[:target_args], options)
443
+ case options[:action]
444
+ when 'new'
445
+ code = new_plan(options[:object])
446
+ when 'run'
447
+ code = run_plan(options[:object], options[:task_options], options[:target_args], options)
448
+ end
435
449
  when 'puppetfile'
436
450
  case options[:action]
437
451
  when 'generate-types'
@@ -522,7 +536,7 @@ module Bolt
522
536
  tasks = pal.list_tasks
523
537
  tasks.select! { |task| task.first.include?(options[:filter]) } if options[:filter]
524
538
  tasks.select! { |task| config.project.tasks.include?(task.first) } unless config.project.tasks.nil?
525
- outputter.print_tasks(tasks, pal.list_modulepath)
539
+ outputter.print_tasks(tasks, pal.user_modulepath)
526
540
  end
527
541
 
528
542
  def show_plan(plan_name)
@@ -533,7 +547,7 @@ module Bolt
533
547
  plans = pal.list_plans
534
548
  plans.select! { |plan| plan.first.include?(options[:filter]) } if options[:filter]
535
549
  plans.select! { |plan| config.project.plans.include?(plan.first) } unless config.project.plans.nil?
536
- outputter.print_plans(plans, pal.list_modulepath)
550
+ outputter.print_plans(plans, pal.user_modulepath)
537
551
  end
538
552
 
539
553
  def list_targets
@@ -551,6 +565,118 @@ module Bolt
551
565
  outputter.print_groups(groups)
552
566
  end
553
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
+
554
680
  def run_plan(plan_name, plan_arguments, nodes, options)
555
681
  unless nodes.empty?
556
682
  if plan_arguments['nodes'] || plan_arguments['targets']
@@ -654,8 +780,26 @@ module Bolt
654
780
  # Initializes a specified directory as a Bolt project and installs any modules
655
781
  # specified by the user, along with their dependencies
656
782
  def initialize_project
657
- project = Pathname.new(File.expand_path(options[:object] || Dir.pwd))
658
- config = project + 'bolt.yaml'
783
+ # Dir.pwd will return backslashes on Windows, but Pathname always uses
784
+ # forward slashes to concatenate paths. This results in paths like
785
+ # C:\User\Administrator/modules, which fail module install. This ensure
786
+ # forward slashes in the cwd path.
787
+ dir = File.expand_path(Dir.pwd)
788
+ name = options[:object] || File.basename(dir)
789
+ if name !~ Bolt::Module::MODULE_NAME_REGEX
790
+ if options[:object]
791
+ raise Bolt::ValidationError, "The provided project name '#{name}' is invalid; "\
792
+ "project name must begin with a lowercase letter and can include lowercase "\
793
+ "letters, numbers, and underscores."
794
+ else
795
+ raise Bolt::ValidationError, "The current directory name '#{name}' is an invalid "\
796
+ "project name. Please specify a name using 'bolt project init <name>'."
797
+ end
798
+ end
799
+
800
+ project = Pathname.new(dir)
801
+ old_config = project + 'bolt.yaml'
802
+ config = project + 'bolt-project.yaml'
659
803
  puppetfile = project + 'Puppetfile'
660
804
  modulepath = [project + 'modules']
661
805
 
@@ -676,18 +820,24 @@ module Bolt
676
820
 
677
821
  # Warn the user if the project directory already exists. We don't error here since users
678
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
679
826
  if config.exist?
680
- @logger.warn "Found existing project directory at #{project}"
681
- end
682
-
683
- # Create the project directory
684
- FileUtils.mkdir_p(project)
685
-
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}."
686
832
  # Bless the project directory as a...wait for it...project
687
- if FileUtils.touch(config)
688
- outputter.print_message "Successfully created Bolt project at #{project}"
689
833
  else
690
- raise Bolt::FileError.new("Could not create Bolt project directory at #{project}", nil)
834
+ begin
835
+ content = { 'name' => name }
836
+ File.write(config.to_path, content.to_yaml)
837
+ outputter.print_message "Successfully created Bolt project at #{project}"
838
+ rescue StandardError => e
839
+ raise Bolt::FileError.new("Could not create bolt-project.yaml at #{project}: #{e.message}", nil)
840
+ end
691
841
  end
692
842
 
693
843
  # Write the generated Puppetfile to the fancy new project
@@ -763,49 +913,6 @@ module Bolt
763
913
  end
764
914
  end
765
915
 
766
- def migrate_project
767
- inventory_file = config.inventoryfile || config.default_inventoryfile
768
- data = Bolt::Util.read_yaml_hash(inventory_file, 'inventory')
769
-
770
- data.delete('version') if data['version'] != 2
771
-
772
- migrated = migrate_group(data)
773
-
774
- ok = File.write(inventory_file, data.to_yaml) if migrated
775
-
776
- result = if migrated && ok
777
- "Successfully migrated Bolt project to latest version."
778
- elsif !migrated
779
- "Bolt project already on latest version. Nothing to do."
780
- else
781
- "Could not migrate Bolt project to latest version."
782
- end
783
- outputter.print_message result
784
-
785
- ok ? 0 : 1
786
- end
787
-
788
- # Walks an inventory hash and replaces all 'nodes' keys with 'targets' keys
789
- # and all 'name' keys nested in a 'targets' hash with 'uri' keys. Data is
790
- # modified in place.
791
- def migrate_group(group)
792
- migrated = false
793
- if group.key?('nodes')
794
- migrated = true
795
- targets = group['nodes'].map do |target|
796
- target['uri'] = target.delete('name') if target.is_a?(Hash)
797
- target
798
- end
799
- group.delete('nodes')
800
- group['targets'] = targets
801
- end
802
- (group['groups'] || []).each do |subgroup|
803
- migrated_group = migrate_group(subgroup)
804
- migrated ||= migrated_group
805
- end
806
- migrated
807
- end
808
-
809
916
  def install_puppetfile(config, puppetfile, modulepath)
810
917
  require 'r10k/cli'
811
918
  require 'bolt/r10k_log_proxy'
@@ -848,8 +955,46 @@ module Bolt
848
955
  config.project)
849
956
  end
850
957
 
851
- def convert_plan(plan)
852
- pal.convert_plan(plan)
958
+ # Collects the list of Bolt guides and maps them to their topics.
959
+ def guides
960
+ @guides ||= begin
961
+ root_path = File.expand_path(File.join(__dir__, '..', '..', 'guides'))
962
+ files = Dir.children(root_path).sort
963
+
964
+ files.each_with_object({}) do |file, guides|
965
+ next if file !~ /\.txt\z/
966
+ topic = File.basename(file, '.txt')
967
+ guides[topic] = File.join(root_path, file)
968
+ end
969
+ rescue SystemCallError => e
970
+ raise Bolt::FileError.new("#{e.message}: unable to load guides directory", root_path)
971
+ end
972
+ end
973
+
974
+ # Display the list of available Bolt guides.
975
+ def list_topics
976
+ outputter.print_topics(guides.keys)
977
+ 0
978
+ end
979
+
980
+ # Display a specific Bolt guide.
981
+ def show_guide(topic)
982
+ if guides[topic]
983
+ analytics.event('Guide', 'known_topic', label: topic)
984
+
985
+ begin
986
+ guide = File.read(guides[topic])
987
+ rescue SystemCallError => e
988
+ raise Bolt::FileError("#{e.message}: unable to load guide page", filepath)
989
+ end
990
+
991
+ outputter.print_guide(guide, topic)
992
+ else
993
+ analytics.event('Guide', 'unknown_topic', label: topic)
994
+ outputter.print_message("Did not find guide for topic '#{topic}'.\n\n")
995
+ list_topics
996
+ end
997
+ 0
853
998
  end
854
999
 
855
1000
  def validate_file(type, path, allow_dir = false)
@@ -916,7 +1061,7 @@ module Bolt
916
1061
  msg = <<~MSG.chomp
917
1062
  Loaded configuration from: '#{config.config_files.join("', '")}'
918
1063
  MSG
919
- @logger.debug(msg)
1064
+ @logger.info(msg)
920
1065
  end
921
1066
 
922
1067
  # Gem installs include the aggregate, canary, and puppetdb_fact modules, while