bolt 2.25.0 → 2.30.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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +4 -3
  3. data/bolt-modules/boltlib/lib/puppet/datatypes/result.rb +2 -1
  4. data/bolt-modules/boltlib/lib/puppet/functions/download_file.rb +1 -1
  5. data/bolt-modules/dir/lib/puppet/functions/dir/children.rb +1 -1
  6. data/lib/bolt/analytics.rb +7 -3
  7. data/lib/bolt/applicator.rb +21 -21
  8. data/lib/bolt/bolt_option_parser.rb +116 -26
  9. data/lib/bolt/catalog.rb +5 -3
  10. data/lib/bolt/cli.rb +194 -185
  11. data/lib/bolt/config.rb +61 -26
  12. data/lib/bolt/config/options.rb +35 -2
  13. data/lib/bolt/executor.rb +2 -2
  14. data/lib/bolt/inventory.rb +8 -1
  15. data/lib/bolt/inventory/group.rb +1 -1
  16. data/lib/bolt/inventory/inventory.rb +1 -1
  17. data/lib/bolt/inventory/target.rb +1 -1
  18. data/lib/bolt/logger.rb +35 -21
  19. data/lib/bolt/module_installer.rb +172 -0
  20. data/lib/bolt/outputter.rb +4 -0
  21. data/lib/bolt/outputter/human.rb +53 -11
  22. data/lib/bolt/outputter/json.rb +7 -1
  23. data/lib/bolt/outputter/logger.rb +3 -3
  24. data/lib/bolt/pal.rb +29 -20
  25. data/lib/bolt/pal/yaml_plan/evaluator.rb +1 -1
  26. data/lib/bolt/plugin/module.rb +1 -1
  27. data/lib/bolt/plugin/puppetdb.rb +1 -1
  28. data/lib/bolt/project.rb +89 -28
  29. data/lib/bolt/project_migrator.rb +80 -0
  30. data/lib/bolt/project_migrator/base.rb +39 -0
  31. data/lib/bolt/project_migrator/config.rb +67 -0
  32. data/lib/bolt/project_migrator/inventory.rb +67 -0
  33. data/lib/bolt/project_migrator/modules.rb +198 -0
  34. data/lib/bolt/puppetdb/client.rb +1 -1
  35. data/lib/bolt/puppetdb/config.rb +1 -1
  36. data/lib/bolt/puppetfile.rb +142 -0
  37. data/lib/bolt/puppetfile/installer.rb +43 -0
  38. data/lib/bolt/puppetfile/module.rb +90 -0
  39. data/lib/bolt/r10k_log_proxy.rb +1 -1
  40. data/lib/bolt/rerun.rb +2 -2
  41. data/lib/bolt/result.rb +23 -0
  42. data/lib/bolt/shell.rb +1 -1
  43. data/lib/bolt/shell/bash.rb +1 -1
  44. data/lib/bolt/task.rb +1 -1
  45. data/lib/bolt/transport/base.rb +5 -5
  46. data/lib/bolt/transport/docker/connection.rb +1 -1
  47. data/lib/bolt/transport/local/connection.rb +1 -1
  48. data/lib/bolt/transport/ssh.rb +1 -1
  49. data/lib/bolt/transport/ssh/connection.rb +1 -1
  50. data/lib/bolt/transport/ssh/exec_connection.rb +1 -1
  51. data/lib/bolt/transport/winrm.rb +1 -1
  52. data/lib/bolt/transport/winrm/connection.rb +1 -1
  53. data/lib/bolt/util.rb +52 -11
  54. data/lib/bolt/version.rb +1 -1
  55. data/lib/bolt_server/acl.rb +2 -2
  56. data/lib/bolt_server/base_config.rb +3 -3
  57. data/lib/bolt_server/config.rb +1 -1
  58. data/lib/bolt_server/file_cache.rb +12 -12
  59. data/lib/bolt_server/transport_app.rb +125 -26
  60. data/lib/bolt_spec/bolt_context.rb +4 -4
  61. data/lib/bolt_spec/plans/mock_executor.rb +1 -1
  62. metadata +15 -13
  63. data/lib/bolt/project_migrate.rb +0 -138
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8bfb8de382e2a2ab6931f43ffce1eb97187fda6e2e56036b20637afb8ae8da2d
4
- data.tar.gz: 56cf9c3fda7f29434413feb8b7f2eb3b358c1ed4316f2d5f4e1e47875a6919c5
3
+ metadata.gz: c8ca2906e21e39d1da306c9a02ddb970f64955133fcab01574e4138c1e6e154e
4
+ data.tar.gz: 4c875d82bf7ff9d4117440eca9e1276c1b9c2fa842fda4669227ebb0ec56f170
5
5
  SHA512:
6
- metadata.gz: ec5bf195ad19bac051942f2689e58a325982a8c47b12d8970e1ed6a6758e714d8f22ce5275dc3fb7160ea6d06e7ee63bf528b2f18d67ee754b9ae41c3e92a7c5
7
- data.tar.gz: cc6a87ed720484d04c7bb2275cd6812ac211733e2efce265b69cfb5ee5614fa743122ce50369079697ad08374eccea6e9797e8398176d7669846887b0f0832aa
6
+ metadata.gz: bd8e4327ddfeb4d88fdd1f94ab4233bcf1f0d8ce1390769587197dec11bfa024a16889c5693ec4a4333565cd7553a8c13487b6674ba5427a63f9408961e6a3a7
7
+ data.tar.gz: f2bcbe28ba4c26bc36d3c52ab89a8bafa6a132c930c310a92e48b841d0274389022f2480d3ed397bcc4bcf1f77fb272da82697362d33feccfe05ab0ca203cf6c
data/Puppetfile CHANGED
@@ -31,9 +31,10 @@ mod 'puppetlabs-ruby_plugin_helper', '0.1.0'
31
31
  mod 'puppetlabs-stdlib', '6.3.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'
@@ -9,11 +9,12 @@ Puppet::DataTypes.create_type('Result') do
9
9
  functions => {
10
10
  error => Callable[[], Optional[Error]],
11
11
  message => Callable[[], Optional[String]],
12
+ sensitive => Callable[[], Optional[Sensitive[Data]]],
12
13
  action => Callable[[], String],
13
14
  status => Callable[[], String],
14
15
  to_data => Callable[[], Hash],
15
16
  ok => Callable[[], Boolean],
16
- '[]' => Callable[[String[1]], Data]
17
+ '[]' => Callable[[String[1]], Variant[Data, Sensitive[Data]]]
17
18
  }
18
19
  PUPPET
19
20
 
@@ -96,7 +96,7 @@ Puppet::Functions.create_function(:download_file, Puppet::Functions::InternalFun
96
96
 
97
97
  # Paths expand relative to the default downloads directory for the project
98
98
  # e.g. ~/.puppetlabs/bolt/downloads/
99
- destination = Puppet.lookup(:bolt_project_data).downloads + destination
99
+ destination = Puppet.lookup(:bolt_project).downloads + destination
100
100
 
101
101
  # If the destination directory already exists, delete any existing contents
102
102
  if Dir.exist?(destination)
@@ -25,7 +25,7 @@ Puppet::Functions.create_function(:'dir::children', Puppet::Functions::InternalF
25
25
  full_mod_path = File.join(mod_path, subpath || '') if mod_path
26
26
 
27
27
  # Expand relative to the project directory if path is relative
28
- project = Puppet.lookup(:bolt_project_data)
28
+ project = Puppet.lookup(:bolt_project)
29
29
  pathname = Pathname.new(dirname)
30
30
  full_dir = pathname.absolute? ? dirname : File.expand_path(File.join(project.path, dirname))
31
31
 
@@ -27,7 +27,7 @@ module Bolt
27
27
  }.freeze
28
28
 
29
29
  def self.build_client
30
- logger = Logging.logger[self]
30
+ logger = Bolt::Logger.logger(self)
31
31
  begin
32
32
  config_file = config_path(logger)
33
33
  config = load_config(config_file, logger)
@@ -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
@@ -106,7 +110,7 @@ module Bolt
106
110
  require 'httpclient'
107
111
  require 'locale'
108
112
 
109
- @logger = Logging.logger[self]
113
+ @logger = Bolt::Logger.logger(self)
110
114
  @http = HTTPClient.new
111
115
  @user_id = user_id
112
116
  @executor = Concurrent.global_io_executor
@@ -210,7 +214,7 @@ module Bolt
210
214
  attr_accessor :bundled_content
211
215
 
212
216
  def initialize
213
- @logger = Logging.logger[self]
217
+ @logger = Bolt::Logger.logger(self)
214
218
  @bundled_content = []
215
219
  end
216
220
 
@@ -28,7 +28,7 @@ module Bolt
28
28
  @apply_settings = apply_settings || {}
29
29
 
30
30
  @pool = Concurrent::ThreadPoolExecutor.new(name: 'apply', max_threads: max_compiles)
31
- @logger = Logging.logger[self]
31
+ @logger = Bolt::Logger.logger(self)
32
32
  end
33
33
 
34
34
  private def libexec
@@ -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
@@ -183,17 +184,16 @@ module Bolt
183
184
  type_by_reference: true,
184
185
  local_reference: true)
185
186
 
186
- bolt_project = @project if @project&.name
187
187
  scope = {
188
188
  code_ast: ast,
189
189
  modulepath: @modulepath,
190
- project: bolt_project.to_h,
190
+ project: @project.to_h,
191
191
  pdb_config: @pdb_client.config.to_hash,
192
192
  hiera_config: @hiera_config,
193
193
  plan_vars: plan_vars,
194
194
  # This data isn't available on the target config hash
195
195
  config: @inventory.transport_data_get
196
- }
196
+ }.freeze
197
197
  description = options[:description] || 'apply catalog'
198
198
 
199
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
@@ -366,11 +466,11 @@ module Bolt
366
466
  bolt plan convert <path> [options]
367
467
 
368
468
  DESCRIPTION
369
- Convert a YAML plan to a Bolt plan.
469
+ Convert a YAML plan to a Puppet language plan and print the converted plan to stdout.
370
470
 
371
471
  Converting a YAML plan may result in a plan that is syntactically
372
472
  correct but has different behavior. Always verify a converted plan's
373
- functionality.
473
+ functionality. Note that the converted plan is not written to a file.
374
474
 
375
475
  EXAMPLES
376
476
  bolt plan convert path/to/plan/myplan.yaml
@@ -421,9 +521,9 @@ module Bolt
421
521
  the plan, including a list of available parameters.
422
522
 
423
523
  EXAMPLES
424
- Display a list of available tasks
524
+ Display a list of available plans
425
525
  bolt plan show
426
- Display documentation for the canary task
526
+ Display documentation for the aggregate::count plan
427
527
  bolt plan show aggregate::count
428
528
  HELP
429
529
 
@@ -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
@@ -57,8 +57,10 @@ module Bolt
57
57
 
58
58
  def compile_catalog(request)
59
59
  pdb_client = Bolt::PuppetDB::Client.new(Bolt::PuppetDB::Config.new(request['pdb_config']))
60
- project = request['project'] || {}
61
- bolt_project = Struct.new(:name, :path).new(project['name'], project['path']) unless project.empty?
60
+ project = request['project']
61
+ bolt_project = Struct.new(:name, :path, :load_as_module?).new(project['name'],
62
+ project['path'],
63
+ project['load_as_module?'])
62
64
  inv = Bolt::ApplyInventory.new(request['config'])
63
65
  puppet_overrides = {
64
66
  bolt_pdb_client: pdb_client,
@@ -95,7 +97,7 @@ module Bolt
95
97
  }
96
98
 
97
99
  with_puppet_settings(puppet_settings) do
98
- Puppet::Pal.in_tmp_environment('bolt_catalog', env_conf) do |pal|
100
+ Puppet::Pal.in_tmp_environment('bolt_catalog', **env_conf) do |pal|
99
101
  Puppet.override(puppet_overrides) do
100
102
  Puppet.lookup(:pal_current_node).trusted_data = target['trusted']
101
103
  pal.with_catalog_compiler do |compiler|
@@ -20,33 +20,37 @@ 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
 
47
51
  def initialize(argv)
48
52
  Bolt::Logger.initialize_logging
49
- @logger = Logging.logger[self]
53
+ @logger = Bolt::Logger.logger(self)
50
54
  @argv = argv
51
55
  @options = {}
52
56
  end
@@ -79,7 +83,7 @@ module Bolt
79
83
 
80
84
  # Wrapper method that is called by the Bolt executable. Parses the command and
81
85
  # then loads the project and config. Once config is loaded, it completes the
82
- # setup process by configuring Bolt and issuing warnings.
86
+ # setup process by configuring Bolt and logging messages.
83
87
  #
84
88
  # This separation is needed since the Bolt::Outputter class that normally handles
85
89
  # printing errors relies on config being loaded. All setup that happens before
@@ -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 =~ /.+=/ }
@@ -167,25 +180,26 @@ module Bolt
167
180
  raise e
168
181
  end
169
182
 
170
- # Completes the setup process by configuring Bolt and issuing warnings
183
+ # Completes the setup process by configuring Bolt and log messages
171
184
  def finalize_setup
172
185
  Bolt::Logger.configure(config.log, config.color)
173
186
  Bolt::Logger.analytics = analytics
174
187
 
175
- # Logger must be configured before checking path case and project file, otherwise warnings will not display
188
+ # Logger must be configured before checking path case and project file, otherwise logs will not display
176
189
  config.check_path_case('modulepath', config.modulepath)
177
190
  config.project.check_deprecated_file
178
191
 
179
- # Log the file paths for loaded config files
180
- config_loaded
181
-
182
- # Display warnings created during parser and config initialization
183
- config.warnings.each { |warning| @logger.warn(warning[:msg]) }
192
+ # Log messages created during parser and config initialization
193
+ config.logs.each { |log| @logger.send(log.keys[0], log.values[0]) }
184
194
  @parser_deprecations.each { |dep| Bolt::Logger.deprecation_warning(dep[:type], dep[:msg]) }
185
195
  config.deprecations.each { |dep| Bolt::Logger.deprecation_warning(dep[:type], dep[:msg]) }
186
196
 
187
197
  warn_inventory_overrides_cli(options)
188
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
+
189
203
  options
190
204
  rescue Bolt::Error => e
191
205
  outputter.fatal_error(e)
@@ -233,12 +247,6 @@ module Bolt
233
247
  end
234
248
  end
235
249
 
236
- if options[:subcommand] != 'file' && options[:subcommand] != 'script' &&
237
- !options[:leftovers].empty?
238
- raise Bolt::CLIError,
239
- "Unknown argument(s) #{options[:leftovers].join(', ')}"
240
- end
241
-
242
250
  if %w[task plan].include?(options[:subcommand]) && options[:action] == 'run'
243
251
  if options[:object].nil?
244
252
  raise Bolt::CLIError, "Must specify a #{options[:subcommand]} to run"
@@ -250,23 +258,6 @@ module Bolt
250
258
  end
251
259
  end
252
260
 
253
- if options[:boltdir] && options[:configfile]
254
- raise Bolt::CLIError, "Only one of '--boltdir', '--project', or '--configfile' may be specified"
255
- end
256
-
257
- if options[:noop] &&
258
- !(options[:subcommand] == 'task' && options[:action] == 'run') && options[:subcommand] != 'apply'
259
- raise Bolt::CLIError,
260
- "Option '--noop' may only be specified when running a task or applying manifest code"
261
- end
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
-
270
261
  if options[:subcommand] == 'apply' && (options[:object] && options[:code])
271
262
  raise Bolt::CLIError, "--execute is unsupported when specifying a manifest file"
272
263
  end
@@ -289,6 +280,38 @@ module Bolt
289
280
  raise Bolt::CLIError, "Must specify a plan name."
290
281
  end
291
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
+
292
315
  if options.key?(:debug) && options.key?(:log)
293
316
  raise Bolt::CLIError, "Only one of '--debug' or '--log-level' may be specified"
294
317
  end
@@ -356,7 +379,7 @@ module Bolt
356
379
  # Initialize inventory and targets. Errors here are better to catch early.
357
380
  # options[:target_args] will contain a string/array version of the targetting options this is passed to plans
358
381
  # options[:targets] will contain a resolved set of Target objects
359
- unless %w[project puppetfile secret guide].include?(options[:subcommand]) ||
382
+ unless %w[guide module project puppetfile secret].include?(options[:subcommand]) ||
360
383
  %w[convert new show].include?(options[:action])
361
384
  update_targets(options)
362
385
  end
@@ -382,7 +405,7 @@ module Bolt
382
405
  inventory_version: inventory.version)
383
406
  end
384
407
 
385
- analytics.screen_view(screen, screen_view_fields)
408
+ analytics.screen_view(screen, **screen_view_fields)
386
409
 
387
410
  case options[:action]
388
411
  when 'show'
@@ -407,6 +430,8 @@ module Bolt
407
430
  end
408
431
  when 'group'
409
432
  list_groups
433
+ when 'module'
434
+ list_modules
410
435
  end
411
436
  return 0
412
437
  when 'show-modules'
@@ -435,9 +460,7 @@ module Bolt
435
460
  when 'init'
436
461
  code = initialize_project
437
462
  when 'migrate'
438
- inv = config.inventoryfile
439
- path = config.project.path
440
- code = Bolt::ProjectMigrate.new(path, outputter, inv).migrate_project
463
+ code = Bolt::ProjectMigrator.new(config, outputter).migrate
441
464
  end
442
465
  when 'plan'
443
466
  case options[:action]
@@ -446,12 +469,25 @@ module Bolt
446
469
  when 'run'
447
470
  code = run_plan(options[:object], options[:task_options], options[:target_args], options)
448
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
449
481
  when 'puppetfile'
450
482
  case options[:action]
451
483
  when 'generate-types'
452
484
  code = generate_types
453
485
  when 'install'
454
- 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
+ )
455
491
  end
456
492
  when 'secret'
457
493
  code = Bolt::Secret.execute(plugins, outputter, options)
@@ -801,36 +837,39 @@ module Bolt
801
837
  old_config = project + 'bolt.yaml'
802
838
  config = project + 'bolt-project.yaml'
803
839
  puppetfile = project + 'Puppetfile'
804
- modulepath = [project + 'modules']
840
+ moduledir = project + 'modules'
841
+
842
+ # Warn the user if the project directory already exists. We don't error
843
+ # here since users might not have installed any modules yet. If both
844
+ # bolt.yaml and bolt-project.yaml exist, this will just warn about
845
+ # bolt-project.yaml and subsequent Bolt actions will warn about both files
846
+ # existing.
847
+ if config.exist?
848
+ @logger.warn "Found existing project directory at #{project}. Skipping file creation."
849
+ elsif old_config.exist?
850
+ @logger.warn "Found existing #{old_config.basename} at #{project}. "\
851
+ "#{old_config.basename} is deprecated, please rename to #{config.basename}."
852
+ end
805
853
 
806
- # If modules were specified, first check if there is already a Puppetfile at the project
807
- # directory, erroring if there is. If there is no Puppetfile, generate the Puppetfile
808
- # content by resolving the specified modules and all their dependencies.
809
- # We generate the Puppetfile first so that any errors in resolving modules and their
810
- # dependencies are caught early and do not create a project directory.
854
+ # If modules were specified, first check if there is already a Puppetfile
855
+ # at the project directory, erroring if there is. If there is no
856
+ # Puppetfile, install the specified modules. The module installer will
857
+ # resolve dependencies, generate a Puppetfile, and install the modules.
811
858
  if options[:modules]
812
859
  if puppetfile.exist?
813
860
  raise Bolt::CLIError,
814
- "Found existing Puppetfile at #{puppetfile}, unable to initialize project with "\
815
- "#{options[:modules].join(', ')}"
816
- else
817
- puppetfile_specs = resolve_puppetfile_specs
861
+ "Found existing Puppetfile at #{puppetfile}, unable to initialize "\
862
+ "project with modules."
818
863
  end
864
+
865
+ installer = Bolt::ModuleInstaller.new(outputter, pal)
866
+ installer.install(options[:modules], puppetfile, moduledir)
819
867
  end
820
868
 
821
- # Warn the user if the project directory already exists. We don't error here since users
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
826
- if config.exist?
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}."
832
- # Bless the project directory as a...wait for it...project
833
- else
869
+ # If either bolt.yaml or bolt-project.yaml exist, the user has already
870
+ # been warned and we can just finish project creation. Otherwise, create a
871
+ # bolt-project.yaml with the project name in it.
872
+ unless config.exist? || old_config.exist?
834
873
  begin
835
874
  content = { 'name' => name }
836
875
  File.write(config.to_path, content.to_yaml)
@@ -840,109 +879,86 @@ module Bolt
840
879
  end
841
880
  end
842
881
 
843
- # Write the generated Puppetfile to the fancy new project
844
- if puppetfile_specs
845
- File.write(puppetfile, puppetfile_specs.join("\n"))
846
- outputter.print_message "Successfully created Puppetfile at #{puppetfile}"
847
- # Install the modules from our shiny new Puppetfile
848
- if install_puppetfile({}, puppetfile, modulepath)
849
- outputter.print_message "Successfully installed #{options[:modules].join(', ')}"
850
- else
851
- raise Bolt::CLIError, "Could not install #{options[:modules].join(', ')}"
852
- end
853
- end
854
-
855
882
  0
856
883
  end
857
884
 
858
- # Resolves Puppetfile specs from user-specified modules and dependencies resolved
859
- # by the puppetfile-resolver gem.
860
- def resolve_puppetfile_specs
861
- require 'puppetfile-resolver'
862
-
863
- # Build the document model from the module names, defaulting to the latest version of each module
864
- model = PuppetfileResolver::Puppetfile::Document.new('')
865
- options[:modules].each do |mod_name|
866
- model.add_module(
867
- PuppetfileResolver::Puppetfile::ForgeModule.new(mod_name).tap { |mod| mod.version = :latest }
868
- )
869
- end
870
-
871
- # Make sure the Puppetfile model is valid
872
- unless model.valid?
873
- raise Bolt::ValidationError,
874
- "Unable to resolve dependencies for #{options[:modules].join(', ')}"
875
- end
876
-
877
- # Create the resolver using the Puppetfile model. nil disables Puppet version restrictions.
878
- resolver = PuppetfileResolver::Resolver.new(model, nil)
879
-
880
- # Configure and resolve the dependency graph
881
- result = resolver.resolve(
882
- cache: nil,
883
- ui: nil,
884
- module_paths: [],
885
- allow_missing_modules: true
886
- )
885
+ # Installs modules declared in the project configuration file.
886
+ #
887
+ def install_project_modules(project, force, resolve)
888
+ assert_project_file(project)
887
889
 
888
- # Validate that the modules exist
889
- missing_graph = result.specifications.select do |_name, spec|
890
- spec.instance_of? PuppetfileResolver::Models::MissingModuleSpecification
890
+ unless project.modules
891
+ outputter.print_message "Project configuration file #{project.project_file} does not "\
892
+ "specify any module dependencies. Nothing to do."
893
+ return 0
891
894
  end
892
895
 
893
- if missing_graph.any?
894
- titles = model.modules.each_with_object({}) do |mod, acc|
895
- acc[mod.name] = mod.title
896
- end
896
+ installer = Bolt::ModuleInstaller.new(outputter, pal)
897
897
 
898
- names = titles.values_at(*missing_graph.keys)
899
- plural = names.count == 1 ? '' : 's'
898
+ ok = installer.install(project.modules,
899
+ project.puppetfile,
900
+ project.managed_moduledir,
901
+ force: force,
902
+ resolve: resolve)
903
+ ok ? 0 : 1
904
+ end
900
905
 
901
- raise Bolt::ValidationError,
902
- "Unknown module name#{plural} #{names.join(', ')}"
903
- end
906
+ # Adds a single module to the project.
907
+ #
908
+ def add_project_module(name, project)
909
+ assert_project_file(project)
910
+
911
+ modules = project.modules || []
912
+ installer = Bolt::ModuleInstaller.new(outputter, pal)
913
+
914
+ ok = installer.add(name,
915
+ modules,
916
+ project.puppetfile,
917
+ project.managed_moduledir,
918
+ project.project_file)
919
+ ok ? 0 : 1
920
+ end
904
921
 
905
- # Filter the dependency graph to only include module specifications
906
- spec_graph = result.specifications.select do |_name, spec|
907
- spec.instance_of? PuppetfileResolver::Models::ModuleSpecification
908
- end
922
+ # Asserts that there is a project configuration file.
923
+ #
924
+ def assert_project_file(project)
925
+ unless project.project_file?
926
+ msg = if project.config_file.exist?
927
+ "Detected Bolt configuration file #{project.config_file}, unable to install "\
928
+ "modules. To update to a project configuration file, run 'bolt project migrate'."
929
+ else
930
+ "Could not find project configuration file #{project.project_file}, unable "\
931
+ "to install modules. To create a Bolt project, run 'bolt project init'."
932
+ end
909
933
 
910
- # Map specification models to a Puppetfile specification
911
- spec_graph.values.map do |spec|
912
- "mod '#{spec.owner}-#{spec.name}', '#{spec.version}'"
934
+ raise Bolt::Error.new(msg, 'bolt/missing-project-config-error')
913
935
  end
914
936
  end
915
937
 
916
- def install_puppetfile(config, puppetfile, modulepath)
917
- require 'r10k/cli'
918
- require 'bolt/r10k_log_proxy'
919
-
920
- if puppetfile.exist?
921
- moduledir = modulepath.first.to_s
922
- r10k_opts = {
923
- root: puppetfile.dirname.to_s,
924
- puppetfile: puppetfile.to_s,
925
- moduledir: moduledir
926
- }
927
-
928
- settings = R10K::Settings.global_settings.evaluate(config)
929
- R10K::Initializers::GlobalInitializer.new(settings).call
930
- install_action = R10K::Action::Puppetfile::Install.new(r10k_opts, nil)
931
-
932
- # Override the r10k logger with a proxy to our own logger
933
- R10K::Logging.instance_variable_set(:@outputter, Bolt::R10KLogProxy.new)
934
-
935
- ok = install_action.call
936
- outputter.print_puppetfile_result(ok, puppetfile, moduledir)
937
- # Automatically generate types after installing modules
938
- pal.generate_types
938
+ # Loads a Puppetfile and installs its modules.
939
+ #
940
+ def install_puppetfile(config, puppetfile, moduledir)
941
+ installer = Bolt::ModuleInstaller.new(outputter, pal)
942
+ ok = installer.install_puppetfile(puppetfile, moduledir, config)
943
+ ok ? 0 : 1
944
+ end
939
945
 
940
- ok ? 0 : 1
941
- else
942
- raise Bolt::FileError.new("Could not find a Puppetfile at #{puppetfile}", puppetfile)
946
+ # Raises an error if the 'puppetfile install' command is deprecated due to
947
+ # modules being configured.
948
+ #
949
+ def assert_puppetfile_or_module_command(modules)
950
+ if modules && options[:subcommand] == 'puppetfile'
951
+ raise Bolt::CLIError,
952
+ "Unable to use command 'bolt puppetfile #{options[:action]}' when "\
953
+ "'modules' is configured in bolt-project.yaml. Use the 'module' command "\
954
+ "instead. For a list of available actions for the 'module' command, run "\
955
+ "'bolt module --help'."
956
+ elsif modules.nil? && options[:subcommand] == 'module'
957
+ raise Bolt::CLIError,
958
+ "Unable to use command 'bolt module #{options[:action]}'. To use "\
959
+ "this command, update your project configuration to manage module "\
960
+ "dependencies."
943
961
  end
944
- rescue R10K::Error => e
945
- raise PuppetfileError, e
946
962
  end
947
963
 
948
964
  def pal
@@ -958,17 +974,17 @@ module Bolt
958
974
  # Collects the list of Bolt guides and maps them to their topics.
959
975
  def guides
960
976
  @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
977
+ root_path = File.expand_path(File.join(__dir__, '..', '..', 'guides'))
978
+ files = Dir.children(root_path).sort
979
+
980
+ files.each_with_object({}) do |file, guides|
981
+ next if file !~ /\.txt\z/
982
+ topic = File.basename(file, '.txt')
983
+ guides[topic] = File.join(root_path, file)
984
+ end
985
+ rescue SystemCallError => e
986
+ raise Bolt::FileError.new("#{e.message}: unable to load guides directory", root_path)
987
+ end
972
988
  end
973
989
 
974
990
  # Display the list of available Bolt guides.
@@ -1019,10 +1035,10 @@ module Bolt
1019
1035
 
1020
1036
  def analytics
1021
1037
  @analytics ||= begin
1022
- client = Bolt::Analytics.build_client
1023
- client.bundled_content = bundled_content
1024
- client
1025
- end
1038
+ client = Bolt::Analytics.build_client
1039
+ client.bundled_content = bundled_content
1040
+ client
1041
+ end
1026
1042
  end
1027
1043
 
1028
1044
  def bundled_content
@@ -1057,17 +1073,10 @@ module Bolt
1057
1073
  content
1058
1074
  end
1059
1075
 
1060
- def config_loaded
1061
- msg = <<~MSG.chomp
1062
- Loaded configuration from: '#{config.config_files.join("', '")}'
1063
- MSG
1064
- @logger.info(msg)
1065
- end
1066
-
1067
1076
  # Gem installs include the aggregate, canary, and puppetdb_fact modules, while
1068
1077
  # package installs include modules listed in the Bolt repo Puppetfile
1069
1078
  def incomplete_install?
1070
- (Dir.children(Bolt::PAL::MODULES_PATH) - %w[aggregate canary puppetdb_fact]).empty?
1079
+ (Dir.children(Bolt::PAL::MODULES_PATH) - %w[aggregate canary puppetdb_fact secure_env_vars]).empty?
1071
1080
  end
1072
1081
 
1073
1082
  # Mimicks the output from Outputter::Human#fatal_error. This should be used to print