bolt 2.26.0 → 2.31.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +13 -12
  3. data/bolt-modules/boltlib/lib/puppet/functions/write_file.rb +2 -2
  4. data/lib/bolt/analytics.rb +4 -0
  5. data/lib/bolt/applicator.rb +19 -18
  6. data/lib/bolt/bolt_option_parser.rb +112 -22
  7. data/lib/bolt/catalog.rb +1 -1
  8. data/lib/bolt/cli.rb +210 -174
  9. data/lib/bolt/config.rb +22 -2
  10. data/lib/bolt/config/modulepath.rb +30 -0
  11. data/lib/bolt/config/options.rb +30 -0
  12. data/lib/bolt/config/transport/options.rb +1 -1
  13. data/lib/bolt/executor.rb +1 -1
  14. data/lib/bolt/inventory.rb +11 -10
  15. data/lib/bolt/logger.rb +26 -19
  16. data/lib/bolt/module_installer.rb +242 -0
  17. data/lib/bolt/outputter.rb +4 -0
  18. data/lib/bolt/outputter/human.rb +77 -17
  19. data/lib/bolt/outputter/json.rb +21 -6
  20. data/lib/bolt/outputter/logger.rb +2 -2
  21. data/lib/bolt/pal.rb +46 -25
  22. data/lib/bolt/plugin.rb +1 -1
  23. data/lib/bolt/plugin/module.rb +1 -1
  24. data/lib/bolt/project.rb +62 -12
  25. data/lib/bolt/project_migrator.rb +80 -0
  26. data/lib/bolt/project_migrator/base.rb +39 -0
  27. data/lib/bolt/project_migrator/config.rb +67 -0
  28. data/lib/bolt/project_migrator/inventory.rb +67 -0
  29. data/lib/bolt/project_migrator/modules.rb +198 -0
  30. data/lib/bolt/puppetfile.rb +149 -0
  31. data/lib/bolt/puppetfile/installer.rb +43 -0
  32. data/lib/bolt/puppetfile/module.rb +93 -0
  33. data/lib/bolt/rerun.rb +1 -1
  34. data/lib/bolt/result.rb +15 -0
  35. data/lib/bolt/shell/bash.rb +4 -3
  36. data/lib/bolt/transport/base.rb +4 -4
  37. data/lib/bolt/transport/ssh/connection.rb +1 -1
  38. data/lib/bolt/util.rb +51 -10
  39. data/lib/bolt/version.rb +1 -1
  40. data/lib/bolt_server/acl.rb +2 -2
  41. data/lib/bolt_server/base_config.rb +3 -3
  42. data/lib/bolt_server/config.rb +1 -1
  43. data/lib/bolt_server/file_cache.rb +11 -11
  44. data/lib/bolt_server/transport_app.rb +206 -27
  45. data/lib/bolt_spec/bolt_context.rb +8 -6
  46. data/lib/bolt_spec/plans.rb +1 -1
  47. data/lib/bolt_spec/plans/mock_executor.rb +1 -1
  48. data/lib/bolt_spec/run.rb +1 -1
  49. metadata +14 -6
  50. data/lib/bolt/project_migrate.rb +0 -138
  51. data/lib/bolt_server/pe/pal.rb +0 -67
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 195c9ad94788e4811c9a90accb11515fc707346106827e15ef71995b016efbcc
4
- data.tar.gz: 7259f2a7b9af58dab4b4f8224360fe8f1570e042c43b6becb183a0dd1257d91e
3
+ metadata.gz: 4b9f29b79c8544029c9a69f6dc76b083857a87e178c9aae0359e49c59ce3cfdb
4
+ data.tar.gz: 02166a6dda2dc381365b7556f506a419e2717267e88f6015af430f7f725d75d0
5
5
  SHA512:
6
- metadata.gz: 2092172d760e7a806e865a3a1315ec2687cff1835b8b44addec9de208711a16be2193ff570a9cae65a620b6d9203575ccb64c2798c71509dd2fc1a0d41ca3399
7
- data.tar.gz: 77828dd923626abe08c0bbf8ef0f30175462aab55a8584127359dde412f37b17d7cd952a4bd1be2ce115f47db4f8d6f4269eb455f063ff01ee1297a4637c03c8
6
+ metadata.gz: 3a282194ca9ccf988e7282afc3a3a0b57b4edc16bb5bf61790fb0eb0ef234e0c7a76c0ea6cd44915047a99e6d423311be1307f435d8000c568d0552268f6afa6
7
+ data.tar.gz: 957e6be1a72af03bd448fe070e6f9bb0b78aa1bb65e90a8bda251ef0f6c56e5ffaa8ff64aa0c5852963d9fecb87144b61ffd24494fe0aabcbdff43ecde9947a3
data/Puppetfile CHANGED
@@ -7,33 +7,34 @@ moduledir File.join(File.dirname(__FILE__), 'modules')
7
7
  # Core modules used by 'apply'
8
8
  mod 'puppetlabs-service', '1.3.0'
9
9
  mod 'puppetlabs-puppet_agent', '4.1.1'
10
- mod 'puppetlabs-facts', '1.0.0'
10
+ mod 'puppetlabs-facts', '1.1.0'
11
11
 
12
12
  # Core types and providers for Puppet 6
13
- mod 'puppetlabs-augeas_core', '1.0.5'
13
+ mod 'puppetlabs-augeas_core', '1.1.1'
14
14
  mod 'puppetlabs-host_core', '1.0.3'
15
- mod 'puppetlabs-scheduled_task', '2.0.1'
16
- mod 'puppetlabs-sshkeys_core', '1.0.3'
17
- mod 'puppetlabs-zfs_core', '1.0.4'
18
- mod 'puppetlabs-cron_core', '1.0.3'
15
+ mod 'puppetlabs-scheduled_task', '2.2.1'
16
+ mod 'puppetlabs-sshkeys_core', '2.1.0'
17
+ mod 'puppetlabs-zfs_core', '1.1.0'
18
+ mod 'puppetlabs-cron_core', '1.0.4'
19
19
  mod 'puppetlabs-mount_core', '1.0.4'
20
20
  mod 'puppetlabs-selinux_core', '1.0.4'
21
- mod 'puppetlabs-yumrepo_core', '1.0.6'
21
+ mod 'puppetlabs-yumrepo_core', '1.0.7'
22
22
  mod 'puppetlabs-zone_core', '1.0.3'
23
23
 
24
24
  # Useful additional modules
25
- mod 'puppetlabs-package', '1.1.0'
25
+ mod 'puppetlabs-package', '1.3.0'
26
26
  mod 'puppetlabs-puppet_conf', '0.6.0'
27
27
  mod 'puppetlabs-python_task_helper', '0.4.3'
28
28
  mod 'puppetlabs-reboot', '3.0.0'
29
29
  mod 'puppetlabs-ruby_task_helper', '0.5.1'
30
30
  mod 'puppetlabs-ruby_plugin_helper', '0.1.0'
31
- mod 'puppetlabs-stdlib', '6.3.0'
31
+ mod 'puppetlabs-stdlib', '6.5.0'
32
32
 
33
33
  # Plugin modules
34
- mod 'puppetlabs-aws_inventory', '0.5.0'
35
- mod 'puppetlabs-azure_inventory', '0.3.0'
36
- mod 'puppetlabs-gcloud_inventory', '0.1.1'
34
+ mod 'puppetlabs-aws_inventory', '0.5.2'
35
+ mod 'puppetlabs-azure_inventory', '0.4.1'
36
+ mod 'puppetlabs-gcloud_inventory', '0.1.3'
37
+ mod 'puppetlabs-http_request', '0.1.0'
37
38
  mod 'puppetlabs-pkcs7', '0.1.1'
38
39
  mod 'puppetlabs-terraform', '0.5.0'
39
40
  mod 'puppetlabs-vault', '0.3.0'
@@ -6,15 +6,15 @@ require 'tempfile'
6
6
  #
7
7
  # > **Note:** Not available in apply block
8
8
  Puppet::Functions.create_function(:write_file) do
9
- # @param targets A pattern identifying zero or more targets. See {get_targets} for accepted patterns.
10
9
  # @param content File content to write.
11
10
  # @param destination An absolute path on the target(s).
11
+ # @param targets A pattern identifying zero or more targets. See {get_targets} for accepted patterns.
12
12
  # @option options [Boolean] _catch_errors Whether to catch raised errors.
13
13
  # @option options [String] _run_as User to run as using privilege escalation.
14
14
  # @return A list of results, one entry per target.
15
15
  # @example Write a file to a target
16
16
  # $content = 'Hello, world!'
17
- # write_file($targets, $content, '/Users/me/hello.txt')
17
+ # write_file($content, '/Users/me/hello.txt', $targets)
18
18
  dispatch :write_file do
19
19
  required_param 'String', :content
20
20
  required_param 'String[1]', :destination
@@ -93,6 +93,10 @@ module Bolt
93
93
  def self.write_config(filename, config)
94
94
  FileUtils.mkdir_p(File.dirname(filename))
95
95
  File.write(filename, config.to_yaml)
96
+ rescue StandardError => e
97
+ Bolt::Logger.warn_once('unwriteable_file', "Could not write analytics configuration to #{filename}.")
98
+ # This will get caught by build_client and create a NoopClient
99
+ raise e
96
100
  end
97
101
 
98
102
  class Client
@@ -50,15 +50,15 @@ module Bolt
50
50
 
51
51
  def catalog_apply_task
52
52
  @catalog_apply_task ||= begin
53
- path = File.join(libexec, 'apply_catalog.rb')
54
- file = { 'name' => 'apply_catalog.rb', 'path' => path }
55
- metadata = { 'supports_noop' => true, 'input_method' => 'stdin',
56
- 'implementations' => [
57
- { 'name' => 'apply_catalog.rb' },
58
- { 'name' => 'apply_catalog.rb', 'remote' => true }
59
- ] }
60
- Bolt::Task.new('apply_helpers::apply_catalog', metadata, [file])
61
- end
53
+ path = File.join(libexec, 'apply_catalog.rb')
54
+ file = { 'name' => 'apply_catalog.rb', 'path' => path }
55
+ metadata = { 'supports_noop' => true, 'input_method' => 'stdin',
56
+ 'implementations' => [
57
+ { 'name' => 'apply_catalog.rb' },
58
+ { 'name' => 'apply_catalog.rb', 'remote' => true }
59
+ ] }
60
+ Bolt::Task.new('apply_helpers::apply_catalog', metadata, [file])
61
+ end
62
62
  end
63
63
 
64
64
  def query_resources_task
@@ -75,34 +75,35 @@ module Bolt
75
75
  end
76
76
  end
77
77
 
78
- def compile(target, catalog_input)
78
+ def compile(target, scope)
79
79
  # This simplified Puppet node object is what .local uses to determine the
80
80
  # certname of the target
81
81
  node = Puppet::Node.from_data_hash('name' => target.name,
82
82
  'parameters' => { 'clientcert' => target.name })
83
83
  trusted = Puppet::Context::TrustedInformation.local(node)
84
- catalog_input[:target] = {
84
+ target_data = {
85
85
  name: target.name,
86
86
  facts: @inventory.facts(target).merge('bolt' => true),
87
87
  variables: @inventory.vars(target),
88
88
  trusted: trusted.to_h
89
89
  }
90
+ catalog_request = scope.merge(target: target_data)
90
91
 
91
92
  bolt_catalog_exe = File.join(libexec, 'bolt_catalog')
92
93
  old_path = ENV['PATH']
93
94
  ENV['PATH'] = "#{RbConfig::CONFIG['bindir']}#{File::PATH_SEPARATOR}#{old_path}"
94
- out, err, stat = Open3.capture3('ruby', bolt_catalog_exe, 'compile', stdin_data: catalog_input.to_json)
95
+ out, err, stat = Open3.capture3('ruby', bolt_catalog_exe, 'compile', stdin_data: catalog_request.to_json)
95
96
  ENV['PATH'] = old_path
96
97
 
97
98
  # If bolt_catalog does not return valid JSON, we should print stderr to
98
99
  # see what happened
99
100
  print_logs = stat.success?
100
101
  result = begin
101
- JSON.parse(out)
102
- rescue JSON::ParserError
103
- print_logs = true
104
- { 'message' => "Something's gone terribly wrong! STDERR is logged." }
105
- end
102
+ JSON.parse(out)
103
+ rescue JSON::ParserError
104
+ print_logs = true
105
+ { 'message' => "Something's gone terribly wrong! STDERR is logged." }
106
+ end
106
107
 
107
108
  # Any messages logged by Puppet will be on stderr as JSON hashes, so we
108
109
  # parse those and store them here. Any message on stderr that is not
@@ -192,7 +193,7 @@ module Bolt
192
193
  plan_vars: plan_vars,
193
194
  # This data isn't available on the target config hash
194
195
  config: @inventory.transport_data_get
195
- }
196
+ }.freeze
196
197
  description = options[:description] || 'apply catalog'
197
198
 
198
199
  required_modules = options[:required_modules].nil? ? nil : Array(options[:required_modules])
@@ -64,6 +64,24 @@ module Bolt
64
64
  when 'guide'
65
65
  { flags: OPTIONS[:global] + %w[format],
66
66
  banner: GUIDE_HELP }
67
+ when 'module'
68
+ case action
69
+ when 'add'
70
+ { flags: OPTIONS[:global] + %w[configfile project],
71
+ banner: MODULE_ADD_HELP }
72
+ when 'generate-types'
73
+ { flags: OPTIONS[:global] + OPTIONS[:global_config_setters],
74
+ banner: MODULE_GENERATETYPES_HELP }
75
+ when 'install'
76
+ { flags: OPTIONS[:global] + %w[configfile force project resolve],
77
+ banner: MODULE_INSTALL_HELP }
78
+ when 'show'
79
+ { flags: OPTIONS[:global] + OPTIONS[:global_config_setters],
80
+ banner: MODULE_SHOW_HELP }
81
+ else
82
+ { flags: OPTIONS[:global],
83
+ banner: MODULE_HELP }
84
+ end
67
85
  when 'plan'
68
86
  case action
69
87
  when 'convert'
@@ -169,6 +187,7 @@ module Bolt
169
187
  group Show the list of groups in the inventory
170
188
  guide View guides for Bolt concepts and features
171
189
  inventory Show the list of targets an action would run on
190
+ module Manage Bolt project modules
172
191
  plan Convert, create, show, and run Bolt plans
173
192
  project Create and migrate Bolt projects
174
193
  puppetfile Install and list modules and generate type references
@@ -341,6 +360,87 @@ module Bolt
341
360
  Show the list of targets an action would run on.
342
361
  HELP
343
362
 
363
+ MODULE_HELP = <<~HELP
364
+ NAME
365
+ module
366
+
367
+ USAGE
368
+ bolt module <action> [options]
369
+
370
+ DESCRIPTION
371
+ Manage Bolt project modules
372
+
373
+ The module command is only supported when a project is configured
374
+ with the 'modules' key.
375
+
376
+ ACTIONS
377
+ add Add a module to the project
378
+ generate-types Generate type references to register in plans
379
+ install Install the project's modules
380
+ show List modules available to the Bolt project
381
+ HELP
382
+
383
+ MODULE_ADD_HELP = <<~HELP
384
+ NAME
385
+ add
386
+
387
+ USAGE
388
+ bolt module add <module> [options]
389
+
390
+ DESCRIPTION
391
+ Add a module to the project.
392
+
393
+ Module declarations are loaded from the project's configuration
394
+ file. Bolt will automatically resolve all module dependencies,
395
+ generate a Puppetfile, and install the modules.
396
+
397
+ The module command is only supported when a project is configured
398
+ with the 'modules' key.
399
+ HELP
400
+
401
+ MODULE_GENERATETYPES_HELP = <<~HELP
402
+ NAME
403
+ generate-types
404
+
405
+ USAGE
406
+ bolt module generate-types [options]
407
+
408
+ DESCRIPTION
409
+ Generate type references to register in plans.
410
+
411
+ The module command is only supported when a project is configured
412
+ with the 'modules' key.
413
+ HELP
414
+
415
+ MODULE_INSTALL_HELP = <<~HELP
416
+ NAME
417
+ install
418
+
419
+ USAGE
420
+ bolt module install [options]
421
+
422
+ DESCRIPTION
423
+ Install the project's modules.
424
+
425
+ Module declarations are loaded from the project's configuration
426
+ file. Bolt will automatically resolve all module dependencies,
427
+ generate a Puppetfile, and install the modules.
428
+ HELP
429
+
430
+ MODULE_SHOW_HELP = <<~HELP
431
+ NAME
432
+ show
433
+
434
+ USAGE
435
+ bolt module show [options]
436
+
437
+ DESCRIPTION
438
+ List modules available to the Bolt project.
439
+
440
+ The module command is only supported when a project is configured
441
+ with the 'modules' key.
442
+ HELP
443
+
344
444
  PLAN_HELP = <<~HELP
345
445
  NAME
346
446
  plan
@@ -678,7 +778,7 @@ module Bolt
678
778
  'For SSH, port defaults to `22`',
679
779
  'For WinRM, port defaults to `5985` or `5986` based on the --[no-]ssl setting') do |targets|
680
780
  @options[:targets] ||= []
681
- @options[:targets] << get_arg_input(targets)
781
+ @options[:targets] << Bolt::Util.get_arg_input(targets)
682
782
  end
683
783
  define('-q', '--query QUERY', 'Query PuppetDB to determine the targets') do |query|
684
784
  @options[:query] = query
@@ -828,7 +928,7 @@ module Bolt
828
928
  "This option is experimental.") do |exec|
829
929
  @options[:'copy-command'] = exec
830
930
  end
831
- define('--connect-timeout TIMEOUT', Integer, 'Connection timeout (defaults vary)') do |timeout|
931
+ define('--connect-timeout TIMEOUT', Integer, 'Connection timeout in seconds (defaults vary)') do |timeout|
832
932
  @options[:'connect-timeout'] = timeout
833
933
  end
834
934
  define('--[no-]tty', 'Request a pseudo TTY on targets that support it') do |tty|
@@ -838,6 +938,13 @@ module Bolt
838
938
  @options[:tmpdir] = tmpdir
839
939
  end
840
940
 
941
+ separator "\nMODULE OPTIONS"
942
+ define('--[no-]resolve',
943
+ 'Use --no-resolve to install modules listed in the Puppetfile without resolving modules configured',
944
+ 'in Bolt project configuration') do |resolve|
945
+ @options[:resolve] = resolve
946
+ end
947
+
841
948
  separator "\nDISPLAY OPTIONS"
842
949
  define('--filter FILTER', 'Filter tasks and plans by a matching substring') do |filter|
843
950
  unless /^[a-z0-9_:]+$/.match(filter)
@@ -864,9 +971,9 @@ module Bolt
864
971
  define('--modules MODULES',
865
972
  'A comma-separated list of modules to install from the Puppet Forge',
866
973
  'when initializing a project. Resolves and installs all dependencies.') do |modules|
867
- @options[:modules] = modules.split(',')
974
+ @options[:modules] = modules.split(',').map { |mod| { 'name' => mod } }
868
975
  end
869
- define('--force', 'Overwrite existing key pairs') do |_force|
976
+ define('--force', 'Force a destructive action') do |_force|
870
977
  @options[:force] = true
871
978
  end
872
979
 
@@ -917,27 +1024,10 @@ module Bolt
917
1024
  end
918
1025
 
919
1026
  def parse_params(params)
920
- json = get_arg_input(params)
1027
+ json = Bolt::Util.get_arg_input(params)
921
1028
  JSON.parse(json)
922
1029
  rescue JSON::ParserError => e
923
1030
  raise Bolt::CLIError, "Unable to parse --params value as JSON: #{e}"
924
1031
  end
925
-
926
- def get_arg_input(value)
927
- if value.start_with?('@')
928
- file = value.sub(/^@/, '')
929
- read_arg_file(file)
930
- elsif value == '-'
931
- $stdin.read
932
- else
933
- value
934
- end
935
- end
936
-
937
- def read_arg_file(file)
938
- File.read(File.expand_path(file))
939
- rescue StandardError => e
940
- raise Bolt::FileError.new("Error attempting to read #{file}: #{e}", file)
941
- end
942
1032
  end
943
1033
  end
@@ -97,7 +97,7 @@ module Bolt
97
97
  }
98
98
 
99
99
  with_puppet_settings(puppet_settings) do
100
- Puppet::Pal.in_tmp_environment('bolt_catalog', env_conf) do |pal|
100
+ Puppet::Pal.in_tmp_environment('bolt_catalog', **env_conf) do |pal|
101
101
  Puppet.override(puppet_overrides) do
102
102
  Puppet.lookup(:pal_current_node).trusted_data = target['trusted']
103
103
  pal.with_catalog_compiler do |compiler|
@@ -20,27 +20,31 @@ require 'bolt/logger'
20
20
  require 'bolt/outputter'
21
21
  require 'bolt/puppetdb'
22
22
  require 'bolt/plugin'
23
- require 'bolt/project_migrate'
23
+ require 'bolt/project_migrator'
24
24
  require 'bolt/pal'
25
25
  require 'bolt/target'
26
26
  require 'bolt/version'
27
27
  require 'bolt/secret'
28
+ require 'bolt/module_installer'
28
29
 
29
30
  module Bolt
30
31
  class CLIExit < StandardError; end
31
32
  class CLI
32
- COMMANDS = { 'command' => %w[run],
33
- 'script' => %w[run],
34
- 'task' => %w[show run],
35
- 'plan' => %w[show run convert new],
36
- 'file' => %w[download upload],
37
- 'puppetfile' => %w[install show-modules generate-types],
38
- 'secret' => %w[encrypt decrypt createkeys],
39
- 'inventory' => %w[show],
40
- 'group' => %w[show],
41
- 'project' => %w[init migrate],
42
- 'apply' => %w[],
43
- 'guide' => %w[] }.freeze
33
+ COMMANDS = {
34
+ 'command' => %w[run],
35
+ 'script' => %w[run],
36
+ 'task' => %w[show run],
37
+ 'plan' => %w[show run convert new],
38
+ 'file' => %w[download upload],
39
+ 'puppetfile' => %w[install show-modules generate-types],
40
+ 'secret' => %w[encrypt decrypt createkeys],
41
+ 'inventory' => %w[show],
42
+ 'group' => %w[show],
43
+ 'project' => %w[init migrate],
44
+ 'module' => %w[add generate-types install show],
45
+ 'apply' => %w[],
46
+ 'guide' => %w[]
47
+ }.freeze
44
48
 
45
49
  attr_reader :config, :options
46
50
 
@@ -98,6 +102,10 @@ module Bolt
98
102
  # This part aims to handle both `bolt <mode> --help` and `bolt help <mode>`.
99
103
  remaining = handle_parser_errors { parser.permute(@argv) } unless @argv.empty?
100
104
  if @argv.empty? || help?(remaining)
105
+ # If the subcommand is not enabled, display the default
106
+ # help text
107
+ options[:subcommand] = nil unless COMMANDS.include?(options[:subcommand])
108
+
101
109
  # Update the parser for the subcommand (or lack thereof)
102
110
  parser.update
103
111
  puts parser.help
@@ -106,6 +114,11 @@ module Bolt
106
114
 
107
115
  options[:object] = remaining.shift
108
116
 
117
+ # Handle reading a command from a file
118
+ if options[:subcommand] == 'command' && options[:object]
119
+ options[:object] = Bolt::Util.get_arg_input(options[:object])
120
+ end
121
+
109
122
  # Only parse task_options for task or plan
110
123
  if %w[task plan].include?(options[:subcommand])
111
124
  task_options, remaining = remaining.partition { |s| s =~ /.+=/ }
@@ -183,6 +196,10 @@ module Bolt
183
196
 
184
197
  warn_inventory_overrides_cli(options)
185
198
 
199
+ # Assert whether the puppetfile/module commands are available depending
200
+ # on whether 'modules' is configured.
201
+ assert_puppetfile_or_module_command(config.project.modules)
202
+
186
203
  options
187
204
  rescue Bolt::Error => e
188
205
  outputter.fatal_error(e)
@@ -230,12 +247,6 @@ module Bolt
230
247
  end
231
248
  end
232
249
 
233
- if options[:subcommand] != 'file' && options[:subcommand] != 'script' &&
234
- !options[:leftovers].empty?
235
- raise Bolt::CLIError,
236
- "Unknown argument(s) #{options[:leftovers].join(', ')}"
237
- end
238
-
239
250
  if %w[task plan].include?(options[:subcommand]) && options[:action] == 'run'
240
251
  if options[:object].nil?
241
252
  raise Bolt::CLIError, "Must specify a #{options[:subcommand]} to run"
@@ -247,23 +258,6 @@ module Bolt
247
258
  end
248
259
  end
249
260
 
250
- if options[:boltdir] && options[:configfile]
251
- raise Bolt::CLIError, "Only one of '--boltdir', '--project', or '--configfile' may be specified"
252
- end
253
-
254
- if options[:noop] &&
255
- !(options[:subcommand] == 'task' && options[:action] == 'run') && options[:subcommand] != 'apply'
256
- raise Bolt::CLIError,
257
- "Option '--noop' may only be specified when running a task or applying manifest code"
258
- end
259
-
260
- if options[:env_vars]
261
- unless %w[command script].include?(options[:subcommand]) && options[:action] == 'run'
262
- raise Bolt::CLIError,
263
- "Option '--env-var' may only be specified when running a command or script"
264
- end
265
- end
266
-
267
261
  if options[:subcommand] == 'apply' && (options[:object] && options[:code])
268
262
  raise Bolt::CLIError, "--execute is unsupported when specifying a manifest file"
269
263
  end
@@ -286,6 +280,38 @@ module Bolt
286
280
  raise Bolt::CLIError, "Must specify a plan name."
287
281
  end
288
282
 
283
+ if options[:subcommand] == 'module' && options[:action] == 'add' && !options[:object]
284
+ raise Bolt::CLIError, "Must specify a module name."
285
+ end
286
+
287
+ if options[:subcommand] == 'module' && options[:action] == 'install' && options[:object]
288
+ raise Bolt::CLIError, "Invalid argument '#{options[:object]}'. To add a new module to "\
289
+ "the project, run 'bolt module add #{options[:object]}'."
290
+ end
291
+
292
+ if options[:subcommand] != 'file' && options[:subcommand] != 'script' &&
293
+ !options[:leftovers].empty?
294
+ raise Bolt::CLIError,
295
+ "Unknown argument(s) #{options[:leftovers].join(', ')}"
296
+ end
297
+
298
+ if options[:boltdir] && options[:configfile]
299
+ raise Bolt::CLIError, "Only one of '--boltdir', '--project', or '--configfile' may be specified"
300
+ end
301
+
302
+ if options[:noop] &&
303
+ !(options[:subcommand] == 'task' && options[:action] == 'run') && options[:subcommand] != 'apply'
304
+ raise Bolt::CLIError,
305
+ "Option '--noop' may only be specified when running a task or applying manifest code"
306
+ end
307
+
308
+ if options[:env_vars]
309
+ unless %w[command script].include?(options[:subcommand]) && options[:action] == 'run'
310
+ raise Bolt::CLIError,
311
+ "Option '--env-var' may only be specified when running a command or script"
312
+ end
313
+ end
314
+
289
315
  if options.key?(:debug) && options.key?(:log)
290
316
  raise Bolt::CLIError, "Only one of '--debug' or '--log-level' may be specified"
291
317
  end
@@ -353,7 +379,7 @@ module Bolt
353
379
  # Initialize inventory and targets. Errors here are better to catch early.
354
380
  # options[:target_args] will contain a string/array version of the targetting options this is passed to plans
355
381
  # options[:targets] will contain a resolved set of Target objects
356
- unless %w[project puppetfile secret guide].include?(options[:subcommand]) ||
382
+ unless %w[guide module project puppetfile secret].include?(options[:subcommand]) ||
357
383
  %w[convert new show].include?(options[:action])
358
384
  update_targets(options)
359
385
  end
@@ -379,7 +405,7 @@ module Bolt
379
405
  inventory_version: inventory.version)
380
406
  end
381
407
 
382
- analytics.screen_view(screen, screen_view_fields)
408
+ analytics.screen_view(screen, **screen_view_fields)
383
409
 
384
410
  case options[:action]
385
411
  when 'show'
@@ -404,6 +430,8 @@ module Bolt
404
430
  end
405
431
  when 'group'
406
432
  list_groups
433
+ when 'module'
434
+ list_modules
407
435
  end
408
436
  return 0
409
437
  when 'show-modules'
@@ -432,9 +460,7 @@ module Bolt
432
460
  when 'init'
433
461
  code = initialize_project
434
462
  when 'migrate'
435
- inv = config.inventoryfile
436
- path = config.project.path
437
- code = Bolt::ProjectMigrate.new(path, outputter, inv).migrate_project
463
+ code = Bolt::ProjectMigrator.new(config, outputter).migrate
438
464
  end
439
465
  when 'plan'
440
466
  case options[:action]
@@ -443,12 +469,25 @@ module Bolt
443
469
  when 'run'
444
470
  code = run_plan(options[:object], options[:task_options], options[:target_args], options)
445
471
  end
472
+ when 'module'
473
+ case options[:action]
474
+ when 'add'
475
+ code = add_project_module(options[:object], config.project)
476
+ when 'install'
477
+ code = install_project_modules(config.project, options[:force], options[:resolve])
478
+ when 'generate-types'
479
+ code = generate_types
480
+ end
446
481
  when 'puppetfile'
447
482
  case options[:action]
448
483
  when 'generate-types'
449
484
  code = generate_types
450
485
  when 'install'
451
- code = install_puppetfile(config.puppetfile_config, config.puppetfile, config.modulepath)
486
+ code = install_puppetfile(
487
+ config.puppetfile_config,
488
+ config.puppetfile,
489
+ config.modulepath.first
490
+ )
452
491
  end
453
492
  when 'secret'
454
493
  code = Bolt::Secret.execute(plugins, outputter, options)
@@ -548,8 +587,24 @@ module Bolt
548
587
  end
549
588
 
550
589
  def list_targets
590
+ inventoryfile = config.inventoryfile || config.default_inventoryfile
591
+
592
+ # Retrieve the known group and target names. This needs to be done before
593
+ # updating targets, as that will add adhoc targets to the inventory.
594
+ known_names = inventory.target_names
595
+
551
596
  update_targets(options)
552
- outputter.print_targets(options[:targets])
597
+
598
+ inventory_targets, adhoc_targets = options[:targets].partition do |target|
599
+ known_names.include?(target.name)
600
+ end
601
+
602
+ target_list = {
603
+ inventory: inventory_targets,
604
+ adhoc: adhoc_targets
605
+ }
606
+
607
+ outputter.print_targets(target_list, inventoryfile)
553
608
  end
554
609
 
555
610
  def show_targets
@@ -578,10 +633,10 @@ module Bolt
578
633
  message = <<~MESSAGE.chomp
579
634
  Invalid plan name '#{plan_name}'. Plan names are composed of one or more name segments
580
635
  separated by double colons '::'.
581
-
636
+
582
637
  Each name segment must begin with a lowercase letter, and may only include lowercase
583
638
  letters, digits, and underscores.
584
-
639
+
585
640
  Examples of valid plan names:
586
641
  - #{config.project.name}
587
642
  - #{config.project.name}::my_plan
@@ -798,36 +853,39 @@ module Bolt
798
853
  old_config = project + 'bolt.yaml'
799
854
  config = project + 'bolt-project.yaml'
800
855
  puppetfile = project + 'Puppetfile'
801
- modulepath = [project + 'modules']
856
+ moduledir = project + 'modules'
802
857
 
803
- # If modules were specified, first check if there is already a Puppetfile at the project
804
- # directory, erroring if there is. If there is no Puppetfile, generate the Puppetfile
805
- # content by resolving the specified modules and all their dependencies.
806
- # We generate the Puppetfile first so that any errors in resolving modules and their
807
- # dependencies are caught early and do not create a project directory.
858
+ # Warn the user if the project directory already exists. We don't error
859
+ # here since users might not have installed any modules yet. If both
860
+ # bolt.yaml and bolt-project.yaml exist, this will just warn about
861
+ # bolt-project.yaml and subsequent Bolt actions will warn about both files
862
+ # existing.
863
+ if config.exist?
864
+ @logger.warn "Found existing project directory at #{project}. Skipping file creation."
865
+ elsif old_config.exist?
866
+ @logger.warn "Found existing #{old_config.basename} at #{project}. "\
867
+ "#{old_config.basename} is deprecated, please rename to #{config.basename}."
868
+ end
869
+
870
+ # If modules were specified, first check if there is already a Puppetfile
871
+ # at the project directory, erroring if there is. If there is no
872
+ # Puppetfile, install the specified modules. The module installer will
873
+ # resolve dependencies, generate a Puppetfile, and install the modules.
808
874
  if options[:modules]
809
875
  if puppetfile.exist?
810
876
  raise Bolt::CLIError,
811
- "Found existing Puppetfile at #{puppetfile}, unable to initialize project with "\
812
- "#{options[:modules].join(', ')}"
813
- else
814
- puppetfile_specs = resolve_puppetfile_specs
877
+ "Found existing Puppetfile at #{puppetfile}, unable to initialize "\
878
+ "project with modules."
815
879
  end
880
+
881
+ installer = Bolt::ModuleInstaller.new(outputter, pal)
882
+ installer.install(options[:modules], puppetfile, moduledir)
816
883
  end
817
884
 
818
- # Warn the user if the project directory already exists. We don't error here since users
819
- # might not have installed any modules yet.
820
- # If both bolt.yaml and bolt-project.yaml exist, this will just warn
821
- # about bolt-project.yaml and subsequent Bolt actions will warn about
822
- # both files existing
823
- if config.exist?
824
- @logger.warn "Found existing project directory at #{project}. Skipping file creation."
825
- # This won't get called if bolt-project.yaml exists
826
- elsif old_config.exist?
827
- @logger.warn "Found existing #{old_config.basename} at #{project}. "\
828
- "#{old_config.basename} is deprecated, please rename to #{config.basename}."
829
- # Bless the project directory as a...wait for it...project
830
- else
885
+ # If either bolt.yaml or bolt-project.yaml exist, the user has already
886
+ # been warned and we can just finish project creation. Otherwise, create a
887
+ # bolt-project.yaml with the project name in it.
888
+ unless config.exist? || old_config.exist?
831
889
  begin
832
890
  content = { 'name' => name }
833
891
  File.write(config.to_path, content.to_yaml)
@@ -837,113 +895,91 @@ module Bolt
837
895
  end
838
896
  end
839
897
 
840
- # Write the generated Puppetfile to the fancy new project
841
- if puppetfile_specs
842
- File.write(puppetfile, puppetfile_specs.join("\n"))
843
- outputter.print_message "Successfully created Puppetfile at #{puppetfile}"
844
- # Install the modules from our shiny new Puppetfile
845
- if install_puppetfile({}, puppetfile, modulepath)
846
- outputter.print_message "Successfully installed #{options[:modules].join(', ')}"
847
- else
848
- raise Bolt::CLIError, "Could not install #{options[:modules].join(', ')}"
849
- end
850
- end
851
-
852
898
  0
853
899
  end
854
900
 
855
- # Resolves Puppetfile specs from user-specified modules and dependencies resolved
856
- # by the puppetfile-resolver gem.
857
- def resolve_puppetfile_specs
858
- require 'puppetfile-resolver'
859
-
860
- # Build the document model from the module names, defaulting to the latest version of each module
861
- model = PuppetfileResolver::Puppetfile::Document.new('')
862
- options[:modules].each do |mod_name|
863
- model.add_module(
864
- PuppetfileResolver::Puppetfile::ForgeModule.new(mod_name).tap { |mod| mod.version = :latest }
865
- )
866
- end
867
-
868
- # Make sure the Puppetfile model is valid
869
- unless model.valid?
870
- raise Bolt::ValidationError,
871
- "Unable to resolve dependencies for #{options[:modules].join(', ')}"
872
- end
873
-
874
- # Create the resolver using the Puppetfile model. nil disables Puppet version restrictions.
875
- resolver = PuppetfileResolver::Resolver.new(model, nil)
876
-
877
- # Configure and resolve the dependency graph
878
- result = resolver.resolve(
879
- cache: nil,
880
- ui: nil,
881
- module_paths: [],
882
- allow_missing_modules: true
883
- )
901
+ # Installs modules declared in the project configuration file.
902
+ #
903
+ def install_project_modules(project, force, resolve)
904
+ assert_project_file(project)
884
905
 
885
- # Validate that the modules exist
886
- missing_graph = result.specifications.select do |_name, spec|
887
- spec.instance_of? PuppetfileResolver::Models::MissingModuleSpecification
906
+ unless project.modules
907
+ outputter.print_message "Project configuration file #{project.project_file} does not "\
908
+ "specify any module dependencies. Nothing to do."
909
+ return 0
888
910
  end
889
911
 
890
- if missing_graph.any?
891
- titles = model.modules.each_with_object({}) do |mod, acc|
892
- acc[mod.name] = mod.title
893
- end
912
+ installer = Bolt::ModuleInstaller.new(outputter, pal)
894
913
 
895
- names = titles.values_at(*missing_graph.keys)
896
- plural = names.count == 1 ? '' : 's'
914
+ ok = installer.install(project.modules,
915
+ project.puppetfile,
916
+ project.managed_moduledir,
917
+ force: force,
918
+ resolve: resolve)
919
+ ok ? 0 : 1
920
+ end
897
921
 
898
- raise Bolt::ValidationError,
899
- "Unknown module name#{plural} #{names.join(', ')}"
900
- end
922
+ # Adds a single module to the project.
923
+ #
924
+ def add_project_module(name, project)
925
+ assert_project_file(project)
926
+
927
+ modules = project.modules || []
928
+ installer = Bolt::ModuleInstaller.new(outputter, pal)
929
+
930
+ ok = installer.add(name,
931
+ modules,
932
+ project.puppetfile,
933
+ project.managed_moduledir,
934
+ project.project_file)
935
+ ok ? 0 : 1
936
+ end
901
937
 
902
- # Filter the dependency graph to only include module specifications
903
- spec_graph = result.specifications.select do |_name, spec|
904
- spec.instance_of? PuppetfileResolver::Models::ModuleSpecification
905
- end
938
+ # Asserts that there is a project configuration file.
939
+ #
940
+ def assert_project_file(project)
941
+ unless project.project_file?
942
+ msg = if project.config_file.exist?
943
+ "Detected Bolt configuration file #{project.config_file}, unable to install "\
944
+ "modules. To update to a project configuration file, run 'bolt project migrate'."
945
+ else
946
+ "Could not find project configuration file #{project.project_file}, unable "\
947
+ "to install modules. To create a Bolt project, run 'bolt project init'."
948
+ end
906
949
 
907
- # Map specification models to a Puppetfile specification
908
- spec_graph.values.map do |spec|
909
- "mod '#{spec.owner}-#{spec.name}', '#{spec.version}'"
950
+ raise Bolt::Error.new(msg, 'bolt/missing-project-config-error')
910
951
  end
911
952
  end
912
953
 
913
- def install_puppetfile(config, puppetfile, modulepath)
914
- require 'r10k/cli'
915
- require 'bolt/r10k_log_proxy'
916
-
917
- if puppetfile.exist?
918
- moduledir = modulepath.first.to_s
919
- r10k_opts = {
920
- root: puppetfile.dirname.to_s,
921
- puppetfile: puppetfile.to_s,
922
- moduledir: moduledir
923
- }
924
-
925
- settings = R10K::Settings.global_settings.evaluate(config)
926
- R10K::Initializers::GlobalInitializer.new(settings).call
927
- install_action = R10K::Action::Puppetfile::Install.new(r10k_opts, nil)
928
-
929
- # Override the r10k logger with a proxy to our own logger
930
- R10K::Logging.instance_variable_set(:@outputter, Bolt::R10KLogProxy.new)
931
-
932
- ok = install_action.call
933
- outputter.print_puppetfile_result(ok, puppetfile, moduledir)
934
- # Automatically generate types after installing modules
935
- pal.generate_types
954
+ # Loads a Puppetfile and installs its modules.
955
+ #
956
+ def install_puppetfile(config, puppetfile, moduledir)
957
+ outputter.print_message("Installing modules from Puppetfile")
958
+ installer = Bolt::ModuleInstaller.new(outputter, pal)
959
+ ok = installer.install_puppetfile(puppetfile, moduledir, config)
960
+ ok ? 0 : 1
961
+ end
936
962
 
937
- ok ? 0 : 1
938
- else
939
- raise Bolt::FileError.new("Could not find a Puppetfile at #{puppetfile}", puppetfile)
963
+ # Raises an error if the 'puppetfile install' command is deprecated due to
964
+ # modules being configured.
965
+ #
966
+ def assert_puppetfile_or_module_command(modules)
967
+ if modules && options[:subcommand] == 'puppetfile'
968
+ raise Bolt::CLIError,
969
+ "Unable to use command 'bolt puppetfile #{options[:action]}' when "\
970
+ "'modules' is configured in bolt-project.yaml. Use the 'module' command "\
971
+ "instead. For a list of available actions for the 'module' command, run "\
972
+ "'bolt module --help'."
973
+ elsif modules.nil? && options[:subcommand] == 'module'
974
+ raise Bolt::CLIError,
975
+ "Unable to use command 'bolt module #{options[:action]}'. To use "\
976
+ "this command, update your project configuration to manage module "\
977
+ "dependencies."
940
978
  end
941
- rescue R10K::Error => e
942
- raise PuppetfileError, e
943
979
  end
944
980
 
945
981
  def pal
946
- @pal ||= Bolt::PAL.new(config.modulepath,
982
+ @pal ||= Bolt::PAL.new(Bolt::Config::Modulepath.new(config.modulepath),
947
983
  config.hiera_config,
948
984
  config.project.resource_types,
949
985
  config.compile_concurrency,
@@ -955,17 +991,17 @@ module Bolt
955
991
  # Collects the list of Bolt guides and maps them to their topics.
956
992
  def guides
957
993
  @guides ||= begin
958
- root_path = File.expand_path(File.join(__dir__, '..', '..', 'guides'))
959
- files = Dir.children(root_path).sort
960
-
961
- files.each_with_object({}) do |file, guides|
962
- next if file !~ /\.txt\z/
963
- topic = File.basename(file, '.txt')
964
- guides[topic] = File.join(root_path, file)
965
- end
966
- rescue SystemCallError => e
967
- raise Bolt::FileError.new("#{e.message}: unable to load guides directory", root_path)
968
- end
994
+ root_path = File.expand_path(File.join(__dir__, '..', '..', 'guides'))
995
+ files = Dir.children(root_path).sort
996
+
997
+ files.each_with_object({}) do |file, guides|
998
+ next if file !~ /\.txt\z/
999
+ topic = File.basename(file, '.txt')
1000
+ guides[topic] = File.join(root_path, file)
1001
+ end
1002
+ rescue SystemCallError => e
1003
+ raise Bolt::FileError.new("#{e.message}: unable to load guides directory", root_path)
1004
+ end
969
1005
  end
970
1006
 
971
1007
  # Display the list of available Bolt guides.
@@ -1016,10 +1052,10 @@ module Bolt
1016
1052
 
1017
1053
  def analytics
1018
1054
  @analytics ||= begin
1019
- client = Bolt::Analytics.build_client
1020
- client.bundled_content = bundled_content
1021
- client
1022
- end
1055
+ client = Bolt::Analytics.build_client
1056
+ client.bundled_content = bundled_content
1057
+ client
1058
+ end
1023
1059
  end
1024
1060
 
1025
1061
  def bundled_content
@@ -1042,7 +1078,7 @@ module Bolt
1042
1078
  'Task' => [],
1043
1079
  'Plugin' => Bolt::Plugin::BUILTIN_PLUGINS }
1044
1080
  if %w[plan task].include?(options[:subcommand]) && options[:action] == 'run'
1045
- default_content = Bolt::PAL.new([], nil, nil)
1081
+ default_content = Bolt::PAL.new(Bolt::Config::Modulepath.new([]), nil, nil)
1046
1082
  content['Plan'] = default_content.list_plans.each_with_object([]) do |iter, col|
1047
1083
  col << iter&.first
1048
1084
  end
@@ -1057,7 +1093,7 @@ module Bolt
1057
1093
  # Gem installs include the aggregate, canary, and puppetdb_fact modules, while
1058
1094
  # package installs include modules listed in the Bolt repo Puppetfile
1059
1095
  def incomplete_install?
1060
- (Dir.children(Bolt::PAL::MODULES_PATH) - %w[aggregate canary puppetdb_fact]).empty?
1096
+ (Dir.children(Bolt::Config::Modulepath::MODULES_PATH) - %w[aggregate canary puppetdb_fact secure_env_vars]).empty?
1061
1097
  end
1062
1098
 
1063
1099
  # Mimicks the output from Outputter::Human#fatal_error. This should be used to print