bolt 2.24.1 → 2.29.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/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 +77 -26
  9. data/lib/bolt/catalog.rb +4 -2
  10. data/lib/bolt/cli.rb +135 -147
  11. data/lib/bolt/config.rb +48 -25
  12. data/lib/bolt/config/options.rb +34 -2
  13. data/lib/bolt/executor.rb +1 -1
  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/outputter/logger.rb +1 -1
  20. data/lib/bolt/pal.rb +21 -10
  21. data/lib/bolt/pal/yaml_plan/evaluator.rb +1 -1
  22. data/lib/bolt/plugin/puppetdb.rb +1 -1
  23. data/lib/bolt/project.rb +62 -17
  24. data/lib/bolt/puppetdb/client.rb +1 -1
  25. data/lib/bolt/puppetdb/config.rb +1 -1
  26. data/lib/bolt/puppetfile.rb +160 -0
  27. data/lib/bolt/puppetfile/installer.rb +43 -0
  28. data/lib/bolt/puppetfile/module.rb +89 -0
  29. data/lib/bolt/r10k_log_proxy.rb +1 -1
  30. data/lib/bolt/rerun.rb +2 -2
  31. data/lib/bolt/result.rb +23 -0
  32. data/lib/bolt/shell.rb +1 -1
  33. data/lib/bolt/task.rb +1 -1
  34. data/lib/bolt/transport/base.rb +5 -5
  35. data/lib/bolt/transport/docker/connection.rb +1 -1
  36. data/lib/bolt/transport/local/connection.rb +1 -1
  37. data/lib/bolt/transport/ssh.rb +1 -1
  38. data/lib/bolt/transport/ssh/connection.rb +1 -1
  39. data/lib/bolt/transport/ssh/exec_connection.rb +1 -1
  40. data/lib/bolt/transport/winrm.rb +1 -1
  41. data/lib/bolt/transport/winrm/connection.rb +1 -1
  42. data/lib/bolt/util.rb +30 -11
  43. data/lib/bolt/version.rb +1 -1
  44. data/lib/bolt_server/base_config.rb +1 -1
  45. data/lib/bolt_server/config.rb +1 -1
  46. data/lib/bolt_server/file_cache.rb +12 -12
  47. data/lib/bolt_server/transport_app.rb +125 -26
  48. data/lib/bolt_spec/bolt_context.rb +4 -4
  49. data/lib/bolt_spec/run.rb +3 -0
  50. metadata +11 -14
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4a424969e4d64b199e77f75c0d67f2f8d668124dea01c8feaec98f7c8d6acd8a
4
- data.tar.gz: c877c94c963b4dcca169b8bad3cf85fe547c1ca9a6050c9554fcc0685cb324b5
3
+ metadata.gz: ec167b23fc4ceec9d9b3048ae64582a3fcf2baa1eceb8100fb26cc3c6df95cb6
4
+ data.tar.gz: 4621d274f8c14f4986dae819755c0db162cd056fbd9412275a0bcffc2169edee
5
5
  SHA512:
6
- metadata.gz: 7720901d8ad877de79b073b3d2d845e78845366bd3e556b4e9c7f4c3be7907e4f9bbf95de06a2583fa2b8d773494855bc3ff1d03fbd3a05217afa969caa1b703
7
- data.tar.gz: f9f4ffb0512f5be6962497bec3f64f69e58f79862fb0337c7d7d67d8964a124afc01ea99cec2f39c12952943e06006d8bc99bb2651e592b909f599791d2b0b4d
6
+ metadata.gz: 1af9a8df65a1a95a7404a0b94f74bfeb9086e3a97ba03e1f19a53acbb626757249b69d812f7afe87e2da05a06d698f515b05313018a649621d98269eabfe2f93
7
+ data.tar.gz: 24f0b0f56451570d5e97d74ddb66556b48fd8c14a79fd7a0c09f10a4ef1c8590684ccee1675c4dd4126cdbcbc781bad6bf83ba2aa4d8ada0cd13279c15f53876
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
@@ -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,21 @@ 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 'install'
70
+ { flags: OPTIONS[:global] + %w[configfile force project],
71
+ banner: MODULE_INSTALL_HELP }
72
+ when 'show'
73
+ { flags: OPTIONS[:global] + OPTIONS[:global_config_setters],
74
+ banner: MODULE_SHOW_HELP }
75
+ when 'generate-types'
76
+ { flags: OPTIONS[:global] + OPTIONS[:global_config_setters],
77
+ banner: MODULE_GENERATETYPES_HELP }
78
+ else
79
+ { flags: OPTIONS[:global],
80
+ banner: MODULE_HELP }
81
+ end
67
82
  when 'plan'
68
83
  case action
69
84
  when 'convert'
@@ -341,6 +356,59 @@ module Bolt
341
356
  Show the list of targets an action would run on.
342
357
  HELP
343
358
 
359
+ MODULE_HELP = <<~HELP
360
+ NAME
361
+ module
362
+
363
+ USAGE
364
+ bolt module <action> [options]
365
+
366
+ DESCRIPTION
367
+ Install and list modules and generate type references
368
+
369
+ ACTIONS
370
+ generate-types Generate type references to register in plans
371
+ install Install the project's modules
372
+ show List modules available to the Bolt project
373
+ HELP
374
+
375
+ MODULE_INSTALL_HELP = <<~HELP
376
+ NAME
377
+ install
378
+
379
+ USAGE
380
+ bolt module install [options]
381
+
382
+ DESCRIPTION
383
+ Install the project's modules.
384
+
385
+ Module declarations are loaded from the project's configuration
386
+ file. Bolt will automatically resolve all module dependencies,
387
+ generate a Puppetfile, and install the modules.
388
+ HELP
389
+
390
+ MODULE_GENERATETYPES_HELP = <<~HELP
391
+ NAME
392
+ generate-types
393
+
394
+ USAGE
395
+ bolt module generate-types [options]
396
+
397
+ DESCRIPTION
398
+ Generate type references to register in plans.
399
+ HELP
400
+
401
+ MODULE_SHOW_HELP = <<~HELP
402
+ NAME
403
+ show
404
+
405
+ USAGE
406
+ bolt module show [options]
407
+
408
+ DESCRIPTION
409
+ List modules available to the Bolt project.
410
+ HELP
411
+
344
412
  PLAN_HELP = <<~HELP
345
413
  NAME
346
414
  plan
@@ -366,11 +434,11 @@ module Bolt
366
434
  bolt plan convert <path> [options]
367
435
 
368
436
  DESCRIPTION
369
- Convert a YAML plan to a Bolt plan.
437
+ Convert a YAML plan to a Puppet language plan and print the converted plan to stdout.
370
438
 
371
439
  Converting a YAML plan may result in a plan that is syntactically
372
440
  correct but has different behavior. Always verify a converted plan's
373
- functionality.
441
+ functionality. Note that the converted plan is not written to a file.
374
442
 
375
443
  EXAMPLES
376
444
  bolt plan convert path/to/plan/myplan.yaml
@@ -421,9 +489,9 @@ module Bolt
421
489
  the plan, including a list of available parameters.
422
490
 
423
491
  EXAMPLES
424
- Display a list of available tasks
492
+ Display a list of available plans
425
493
  bolt plan show
426
- Display documentation for the canary task
494
+ Display documentation for the aggregate::count plan
427
495
  bolt plan show aggregate::count
428
496
  HELP
429
497
 
@@ -678,7 +746,7 @@ module Bolt
678
746
  'For SSH, port defaults to `22`',
679
747
  'For WinRM, port defaults to `5985` or `5986` based on the --[no-]ssl setting') do |targets|
680
748
  @options[:targets] ||= []
681
- @options[:targets] << get_arg_input(targets)
749
+ @options[:targets] << Bolt::Util.get_arg_input(targets)
682
750
  end
683
751
  define('-q', '--query QUERY', 'Query PuppetDB to determine the targets') do |query|
684
752
  @options[:query] = query
@@ -828,7 +896,7 @@ module Bolt
828
896
  "This option is experimental.") do |exec|
829
897
  @options[:'copy-command'] = exec
830
898
  end
831
- define('--connect-timeout TIMEOUT', Integer, 'Connection timeout (defaults vary)') do |timeout|
899
+ define('--connect-timeout TIMEOUT', Integer, 'Connection timeout in seconds (defaults vary)') do |timeout|
832
900
  @options[:'connect-timeout'] = timeout
833
901
  end
834
902
  define('--[no-]tty', 'Request a pseudo TTY on targets that support it') do |tty|
@@ -864,9 +932,9 @@ module Bolt
864
932
  define('--modules MODULES',
865
933
  'A comma-separated list of modules to install from the Puppet Forge',
866
934
  'when initializing a project. Resolves and installs all dependencies.') do |modules|
867
- @options[:modules] = modules.split(',')
935
+ @options[:modules] = modules.split(',').map { |mod| { 'name' => mod } }
868
936
  end
869
- define('--force', 'Overwrite existing key pairs') do |_force|
937
+ define('--force', 'Force a destructive action') do |_force|
870
938
  @options[:force] = true
871
939
  end
872
940
 
@@ -917,27 +985,10 @@ module Bolt
917
985
  end
918
986
 
919
987
  def parse_params(params)
920
- json = get_arg_input(params)
988
+ json = Bolt::Util.get_arg_input(params)
921
989
  JSON.parse(json)
922
990
  rescue JSON::ParserError => e
923
991
  raise Bolt::CLIError, "Unable to parse --params value as JSON: #{e}"
924
992
  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
993
  end
943
994
  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,
@@ -40,13 +40,14 @@ module Bolt
40
40
  'group' => %w[show],
41
41
  'project' => %w[init migrate],
42
42
  'apply' => %w[],
43
- 'guide' => %w[] }.freeze
43
+ 'guide' => %w[],
44
+ 'module' => %w[install show generate-types] }.freeze
44
45
 
45
46
  attr_reader :config, :options
46
47
 
47
48
  def initialize(argv)
48
49
  Bolt::Logger.initialize_logging
49
- @logger = Logging.logger[self]
50
+ @logger = Bolt::Logger.logger(self)
50
51
  @argv = argv
51
52
  @options = {}
52
53
  end
@@ -79,7 +80,7 @@ module Bolt
79
80
 
80
81
  # Wrapper method that is called by the Bolt executable. Parses the command and
81
82
  # then loads the project and config. Once config is loaded, it completes the
82
- # setup process by configuring Bolt and issuing warnings.
83
+ # setup process by configuring Bolt and logging messages.
83
84
  #
84
85
  # This separation is needed since the Bolt::Outputter class that normally handles
85
86
  # printing errors relies on config being loaded. All setup that happens before
@@ -106,6 +107,11 @@ module Bolt
106
107
 
107
108
  options[:object] = remaining.shift
108
109
 
110
+ # Handle reading a command from a file
111
+ if options[:subcommand] == 'command' && options[:object]
112
+ options[:object] = Bolt::Util.get_arg_input(options[:object])
113
+ end
114
+
109
115
  # Only parse task_options for task or plan
110
116
  if %w[task plan].include?(options[:subcommand])
111
117
  task_options, remaining = remaining.partition { |s| s =~ /.+=/ }
@@ -167,20 +173,17 @@ module Bolt
167
173
  raise e
168
174
  end
169
175
 
170
- # Completes the setup process by configuring Bolt and issuing warnings
176
+ # Completes the setup process by configuring Bolt and log messages
171
177
  def finalize_setup
172
178
  Bolt::Logger.configure(config.log, config.color)
173
179
  Bolt::Logger.analytics = analytics
174
180
 
175
- # Logger must be configured before checking path case and project file, otherwise warnings will not display
181
+ # Logger must be configured before checking path case and project file, otherwise logs will not display
176
182
  config.check_path_case('modulepath', config.modulepath)
177
183
  config.project.check_deprecated_file
178
184
 
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]) }
185
+ # Log messages created during parser and config initialization
186
+ config.logs.each { |log| @logger.send(log.keys[0], log.values[0]) }
184
187
  @parser_deprecations.each { |dep| Bolt::Logger.deprecation_warning(dep[:type], dep[:msg]) }
185
188
  config.deprecations.each { |dep| Bolt::Logger.deprecation_warning(dep[:type], dep[:msg]) }
186
189
 
@@ -213,10 +216,14 @@ module Bolt
213
216
  end
214
217
 
215
218
  def validate(options)
216
- unless COMMANDS.include?(options[:subcommand])
219
+ # Disables the 'module' subcommand unless the module feature flag is set.
220
+ commands = COMMANDS.dup
221
+ commands.delete('module') unless ENV['BOLT_MODULE_FEATURE']
222
+
223
+ unless commands.include?(options[:subcommand])
217
224
  raise Bolt::CLIError,
218
225
  "Expected subcommand '#{options[:subcommand]}' to be one of " \
219
- "#{COMMANDS.keys.join(', ')}"
226
+ "#{commands.keys.join(', ')}"
220
227
  end
221
228
 
222
229
  actions = COMMANDS[options[:subcommand]]
@@ -356,7 +363,7 @@ module Bolt
356
363
  # Initialize inventory and targets. Errors here are better to catch early.
357
364
  # options[:target_args] will contain a string/array version of the targetting options this is passed to plans
358
365
  # options[:targets] will contain a resolved set of Target objects
359
- unless %w[project puppetfile secret guide].include?(options[:subcommand]) ||
366
+ unless %w[guide module project puppetfile secret].include?(options[:subcommand]) ||
360
367
  %w[convert new show].include?(options[:action])
361
368
  update_targets(options)
362
369
  end
@@ -407,6 +414,8 @@ module Bolt
407
414
  end
408
415
  when 'group'
409
416
  list_groups
417
+ when 'module'
418
+ list_modules
410
419
  end
411
420
  return 0
412
421
  when 'show-modules'
@@ -446,12 +455,19 @@ module Bolt
446
455
  when 'run'
447
456
  code = run_plan(options[:object], options[:task_options], options[:target_args], options)
448
457
  end
458
+ when 'module'
459
+ case options[:action]
460
+ when 'install'
461
+ code = install_project_modules
462
+ when 'generate-types'
463
+ code = generate_types
464
+ end
449
465
  when 'puppetfile'
450
466
  case options[:action]
451
467
  when 'generate-types'
452
468
  code = generate_types
453
469
  when 'install'
454
- code = install_puppetfile(config.puppetfile_config, config.puppetfile, config.modulepath)
470
+ code = install_puppetfile(config.puppetfile_config, config.puppetfile, config.modulepath.first)
455
471
  end
456
472
  when 'secret'
457
473
  code = Bolt::Secret.execute(plugins, outputter, options)
@@ -801,36 +817,38 @@ module Bolt
801
817
  old_config = project + 'bolt.yaml'
802
818
  config = project + 'bolt-project.yaml'
803
819
  puppetfile = project + 'Puppetfile'
804
- modulepath = [project + 'modules']
820
+ moduledir = project + 'modules'
821
+
822
+ # Warn the user if the project directory already exists. We don't error
823
+ # here since users might not have installed any modules yet. If both
824
+ # bolt.yaml and bolt-project.yaml exist, this will just warn about
825
+ # bolt-project.yaml and subsequent Bolt actions will warn about both files
826
+ # existing.
827
+ if config.exist?
828
+ @logger.warn "Found existing project directory at #{project}. Skipping file creation."
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
+ end
805
833
 
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.
834
+ # If modules were specified, first check if there is already a Puppetfile
835
+ # at the project directory, erroring if there is. If there is no
836
+ # Puppetfile, install the specified modules. The module installer will
837
+ # resolve dependencies, generate a Puppetfile, and install the modules.
811
838
  if options[:modules]
812
839
  if puppetfile.exist?
813
840
  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
841
+ "Found existing Puppetfile at #{puppetfile}, unable to initialize "\
842
+ "project with modules."
818
843
  end
844
+
845
+ install_modules(puppetfile, {}, moduledir, options[:modules])
819
846
  end
820
847
 
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
848
+ # If either bolt.yaml or bolt-project.yaml exist, the user has already
849
+ # been warned and we can just finish project creation. Otherwise, create a
850
+ # bolt-project.yaml with the project name in it.
851
+ unless config.exist? || old_config.exist?
834
852
  begin
835
853
  content = { 'name' => name }
836
854
  File.write(config.to_path, content.to_yaml)
@@ -840,109 +858,86 @@ module Bolt
840
858
  end
841
859
  end
842
860
 
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
861
  0
856
862
  end
857
863
 
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(', ')}"
864
+ # Installs modules declared in the project configuration file.
865
+ #
866
+ def install_project_modules
867
+ if config.project.modules.nil?
868
+ outputter.print_message "Project configuration file '#{config.project.project_file}' "\
869
+ "does not specify any module dependencies. Nothing to do."
870
+ return 0
875
871
  end
876
872
 
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
873
+ install_modules(
874
+ config.puppetfile,
875
+ config.puppetfile_config,
876
+ config.project.path + '.modules',
877
+ config.project.modules
886
878
  )
879
+ end
887
880
 
888
- # Validate that the modules exist
889
- missing_graph = result.specifications.select do |_name, spec|
890
- spec.instance_of? PuppetfileResolver::Models::MissingModuleSpecification
891
- end
892
-
893
- if missing_graph.any?
894
- titles = model.modules.each_with_object({}) do |mod, acc|
895
- acc[mod.name] = mod.title
881
+ # Installs modules declared in the project configuration file.
882
+ #
883
+ def install_modules(puppetfile_path, config, moduledir, modules)
884
+ require 'bolt/puppetfile'
885
+ require 'bolt/puppetfile/installer'
886
+
887
+ puppetfile = Bolt::Puppetfile.new(modules)
888
+
889
+ # If the Puppetfile exists, check if it includes specs for each declared
890
+ # module, erroring if there are any missing. Otherwise, resolve the
891
+ # module dependencies and write a new Puppetfile. Users can forcibly
892
+ # overwrite an existing Puppetfile with the '--force' option.
893
+ if puppetfile_path.exist? && !options[:force]
894
+ outputter.print_message "Parsing existing Puppetfile at #{puppetfile_path}"
895
+ existing = Bolt::Puppetfile.parse(puppetfile_path)
896
+
897
+ unless existing.modules.superset? puppetfile.modules
898
+ missing_modules = puppetfile.modules - existing.modules
899
+
900
+ message = <<~MESSAGE.chomp
901
+ Puppetfile #{puppetfile_path} is missing specifications for the following
902
+ module declarations:
903
+
904
+ #{missing_modules.map(&:to_hash).to_yaml.lines.drop(1).join.chomp}
905
+
906
+ This may not be a Puppetfile managed by Bolt. To forcibly overwrite the
907
+ Puppetfile, run 'bolt module install --force'.
908
+ MESSAGE
909
+
910
+ raise Bolt::Error.new(message, 'bolt/missing-module-specs')
896
911
  end
897
-
898
- names = titles.values_at(*missing_graph.keys)
899
- plural = names.count == 1 ? '' : 's'
900
-
901
- raise Bolt::ValidationError,
902
- "Unknown module name#{plural} #{names.join(', ')}"
903
- end
904
-
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
912
+ else
913
+ outputter.print_message "Resolving module dependencies, this may take a moment"
914
+ puppetfile.resolve
915
+ outputter.print_message "Writing Puppetfile at #{puppetfile_path}"
916
+ puppetfile.write(puppetfile_path, force: true)
908
917
  end
909
918
 
910
- # Map specification models to a Puppetfile specification
911
- spec_graph.values.map do |spec|
912
- "mod '#{spec.owner}-#{spec.name}', '#{spec.version}'"
913
- end
914
- end
919
+ outputter.print_message "Syncing modules from #{puppetfile_path} to #{moduledir}"
920
+ ok = Bolt::Puppetfile::Installer.new(config).install(puppetfile_path, moduledir)
915
921
 
916
- def install_puppetfile(config, puppetfile, modulepath)
917
- require 'r10k/cli'
918
- require 'bolt/r10k_log_proxy'
922
+ # Automatically generate types after installing modules.
923
+ pal.generate_types
919
924
 
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
- }
925
+ outputter.print_puppetfile_result(ok, puppetfile_path, moduledir)
926
+ ok ? 0 : 1
927
+ end
927
928
 
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)
929
+ # Loads a Puppetfile and installs its modules.
930
+ #
931
+ def install_puppetfile(config, puppetfile, moduledir)
932
+ require 'bolt/puppetfile/installer'
931
933
 
932
- # Override the r10k logger with a proxy to our own logger
933
- R10K::Logging.instance_variable_set(:@outputter, Bolt::R10KLogProxy.new)
934
+ ok = Bolt::Puppetfile::Installer.new(config).install(puppetfile, moduledir)
934
935
 
935
- ok = install_action.call
936
- outputter.print_puppetfile_result(ok, puppetfile, moduledir)
937
- # Automatically generate types after installing modules
938
- pal.generate_types
936
+ # Automatically generate types after installing modules.
937
+ pal.generate_types
939
938
 
940
- ok ? 0 : 1
941
- else
942
- raise Bolt::FileError.new("Could not find a Puppetfile at #{puppetfile}", puppetfile)
943
- end
944
- rescue R10K::Error => e
945
- raise PuppetfileError, e
939
+ outputter.print_puppetfile_result(ok, puppetfile, moduledir)
940
+ ok ? 0 : 1
946
941
  end
947
942
 
948
943
  def pal
@@ -958,17 +953,17 @@ module Bolt
958
953
  # Collects the list of Bolt guides and maps them to their topics.
959
954
  def guides
960
955
  @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
956
+ root_path = File.expand_path(File.join(__dir__, '..', '..', 'guides'))
957
+ files = Dir.children(root_path).sort
958
+
959
+ files.each_with_object({}) do |file, guides|
960
+ next if file !~ /\.txt\z/
961
+ topic = File.basename(file, '.txt')
962
+ guides[topic] = File.join(root_path, file)
963
+ end
964
+ rescue SystemCallError => e
965
+ raise Bolt::FileError.new("#{e.message}: unable to load guides directory", root_path)
966
+ end
972
967
  end
973
968
 
974
969
  # Display the list of available Bolt guides.
@@ -1019,10 +1014,10 @@ module Bolt
1019
1014
 
1020
1015
  def analytics
1021
1016
  @analytics ||= begin
1022
- client = Bolt::Analytics.build_client
1023
- client.bundled_content = bundled_content
1024
- client
1025
- end
1017
+ client = Bolt::Analytics.build_client
1018
+ client.bundled_content = bundled_content
1019
+ client
1020
+ end
1026
1021
  end
1027
1022
 
1028
1023
  def bundled_content
@@ -1057,17 +1052,10 @@ module Bolt
1057
1052
  content
1058
1053
  end
1059
1054
 
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
1055
  # Gem installs include the aggregate, canary, and puppetdb_fact modules, while
1068
1056
  # package installs include modules listed in the Bolt repo Puppetfile
1069
1057
  def incomplete_install?
1070
- (Dir.children(Bolt::PAL::MODULES_PATH) - %w[aggregate canary puppetdb_fact]).empty?
1058
+ (Dir.children(Bolt::PAL::MODULES_PATH) - %w[aggregate canary puppetdb_fact secure_env_vars]).empty?
1071
1059
  end
1072
1060
 
1073
1061
  # Mimicks the output from Outputter::Human#fatal_error. This should be used to print