bolt 2.24.0 → 2.28.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 +75 -7
  9. data/lib/bolt/catalog.rb +4 -2
  10. data/lib/bolt/cli.rb +126 -147
  11. data/lib/bolt/config.rb +56 -26
  12. data/lib/bolt/config/options.rb +24 -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 +9 -2
  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 +63 -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 +66 -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 +11 -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 +9 -12
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1e572a2c5aec9f127f48764b6a2fe903c91566c8aa0f3a1db33e40347bd1630b
4
- data.tar.gz: 1445543fb941e0b8c12ec60900975a45e368a08e593ebe75c87ee946317f57b7
3
+ metadata.gz: 03de2a7e4d6fa0b9d4120fff783ccb009665077c7b00e910f5e2de14c03a1767
4
+ data.tar.gz: 511e18c31228338f47519ee02a7b248f4d264b93516a58aa1b063a41bb835e80
5
5
  SHA512:
6
- metadata.gz: 4cdee0a056e3c248a72ea3a4cb1872453611ca241d19ad3c98f5e0e89c550368ab19e34999e121f0e98c5e5cac353fed32eb174fb54fd149bd4558bd84e5706e
7
- data.tar.gz: d505e8baf7c6ce5b21c20e27d7979c39c871e5bc69ea7bcf5d898ac94261850afea03028569ed60e192196d215837a2b339fef24231ea9491bc4901e93cba3dd
6
+ metadata.gz: 1956edc3051eae34d25957c7827ce1e1f12bfc7e322d9305a79d9a2cb32cf144309413626cfb9db6667193a8853810dabf8d6004924ad3899633a220f34cecb7
7
+ data.tar.gz: af2e40875cb54c1e2b4153605ffc6abb6b11a65e2f0abcc30f7eb87701cb3c5c023905dc1709ff0bf7261e856ceaae29fad9df13e4b9b51b9f74a4ef65fb0b74
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
 
@@ -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
 
@@ -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
@@ -167,20 +168,17 @@ module Bolt
167
168
  raise e
168
169
  end
169
170
 
170
- # Completes the setup process by configuring Bolt and issuing warnings
171
+ # Completes the setup process by configuring Bolt and log messages
171
172
  def finalize_setup
172
173
  Bolt::Logger.configure(config.log, config.color)
173
174
  Bolt::Logger.analytics = analytics
174
175
 
175
- # Logger must be configured before checking path case and project file, otherwise warnings will not display
176
+ # Logger must be configured before checking path case and project file, otherwise logs will not display
176
177
  config.check_path_case('modulepath', config.modulepath)
177
178
  config.project.check_deprecated_file
178
179
 
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]) }
180
+ # Log messages created during parser and config initialization
181
+ config.logs.each { |log| @logger.send(log.keys[0], log.values[0]) }
184
182
  @parser_deprecations.each { |dep| Bolt::Logger.deprecation_warning(dep[:type], dep[:msg]) }
185
183
  config.deprecations.each { |dep| Bolt::Logger.deprecation_warning(dep[:type], dep[:msg]) }
186
184
 
@@ -213,10 +211,14 @@ module Bolt
213
211
  end
214
212
 
215
213
  def validate(options)
216
- unless COMMANDS.include?(options[:subcommand])
214
+ # Disables the 'module' subcommand unless the module feature flag is set.
215
+ commands = COMMANDS.dup
216
+ commands.delete('module') unless ENV['BOLT_MODULE_FEATURE']
217
+
218
+ unless commands.include?(options[:subcommand])
217
219
  raise Bolt::CLIError,
218
220
  "Expected subcommand '#{options[:subcommand]}' to be one of " \
219
- "#{COMMANDS.keys.join(', ')}"
221
+ "#{commands.keys.join(', ')}"
220
222
  end
221
223
 
222
224
  actions = COMMANDS[options[:subcommand]]
@@ -356,7 +358,7 @@ module Bolt
356
358
  # Initialize inventory and targets. Errors here are better to catch early.
357
359
  # options[:target_args] will contain a string/array version of the targetting options this is passed to plans
358
360
  # options[:targets] will contain a resolved set of Target objects
359
- unless %w[project puppetfile secret guide].include?(options[:subcommand]) ||
361
+ unless %w[guide module project puppetfile secret].include?(options[:subcommand]) ||
360
362
  %w[convert new show].include?(options[:action])
361
363
  update_targets(options)
362
364
  end
@@ -407,6 +409,8 @@ module Bolt
407
409
  end
408
410
  when 'group'
409
411
  list_groups
412
+ when 'module'
413
+ list_modules
410
414
  end
411
415
  return 0
412
416
  when 'show-modules'
@@ -446,12 +450,19 @@ module Bolt
446
450
  when 'run'
447
451
  code = run_plan(options[:object], options[:task_options], options[:target_args], options)
448
452
  end
453
+ when 'module'
454
+ case options[:action]
455
+ when 'install'
456
+ code = install_project_modules
457
+ when 'generate-types'
458
+ code = generate_types
459
+ end
449
460
  when 'puppetfile'
450
461
  case options[:action]
451
462
  when 'generate-types'
452
463
  code = generate_types
453
464
  when 'install'
454
- code = install_puppetfile(config.puppetfile_config, config.puppetfile, config.modulepath)
465
+ code = install_puppetfile(config.puppetfile_config, config.puppetfile, config.modulepath.first)
455
466
  end
456
467
  when 'secret'
457
468
  code = Bolt::Secret.execute(plugins, outputter, options)
@@ -801,36 +812,38 @@ module Bolt
801
812
  old_config = project + 'bolt.yaml'
802
813
  config = project + 'bolt-project.yaml'
803
814
  puppetfile = project + 'Puppetfile'
804
- modulepath = [project + 'modules']
815
+ moduledir = project + 'modules'
805
816
 
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.
817
+ # Warn the user if the project directory already exists. We don't error
818
+ # here since users might not have installed any modules yet. If both
819
+ # bolt.yaml and bolt-project.yaml exist, this will just warn about
820
+ # bolt-project.yaml and subsequent Bolt actions will warn about both files
821
+ # existing.
822
+ if config.exist?
823
+ @logger.warn "Found existing project directory at #{project}. Skipping file creation."
824
+ elsif old_config.exist?
825
+ @logger.warn "Found existing #{old_config.basename} at #{project}. "\
826
+ "#{old_config.basename} is deprecated, please rename to #{config.basename}."
827
+ end
828
+
829
+ # If modules were specified, first check if there is already a Puppetfile
830
+ # at the project directory, erroring if there is. If there is no
831
+ # Puppetfile, install the specified modules. The module installer will
832
+ # resolve dependencies, generate a Puppetfile, and install the modules.
811
833
  if options[:modules]
812
834
  if puppetfile.exist?
813
835
  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
836
+ "Found existing Puppetfile at #{puppetfile}, unable to initialize "\
837
+ "project with modules."
818
838
  end
839
+
840
+ install_modules(puppetfile, {}, moduledir, options[:modules])
819
841
  end
820
842
 
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
843
+ # If either bolt.yaml or bolt-project.yaml exist, the user has already
844
+ # been warned and we can just finish project creation. Otherwise, create a
845
+ # bolt-project.yaml with the project name in it.
846
+ unless config.exist? || old_config.exist?
834
847
  begin
835
848
  content = { 'name' => name }
836
849
  File.write(config.to_path, content.to_yaml)
@@ -840,109 +853,82 @@ module Bolt
840
853
  end
841
854
  end
842
855
 
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
856
  0
856
857
  end
857
858
 
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(', ')}"
859
+ # Installs modules declared in the project configuration file.
860
+ #
861
+ def install_project_modules
862
+ if config.project.modules.nil?
863
+ outputter.print_message "Project configuration file '#{config.project.project_file}' "\
864
+ "does not specify any module dependencies. Nothing to do."
865
+ return 0
875
866
  end
876
867
 
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
868
+ install_modules(
869
+ config.puppetfile,
870
+ config.puppetfile_config,
871
+ config.project.path + '.modules',
872
+ config.project.modules
886
873
  )
874
+ end
887
875
 
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
876
+ # Installs modules declared in the project configuration file.
877
+ #
878
+ def install_modules(puppetfile_path, config, moduledir, modules)
879
+ require 'bolt/puppetfile'
880
+ require 'bolt/puppetfile/installer'
881
+
882
+ puppetfile = Bolt::Puppetfile.new(modules)
883
+
884
+ # If the Puppetfile exists, check if it includes specs for each declared
885
+ # module, erroring if there are any missing. Otherwise, resolve the
886
+ # module dependencies and write a new Puppetfile. Users can forcibly
887
+ # overwrite an existing Puppetfile with the '--force' option.
888
+ if puppetfile_path.exist? && !options[:force]
889
+ outputter.print_message "Parsing existing Puppetfile at #{puppetfile_path}"
890
+ existing = Bolt::Puppetfile.parse(puppetfile_path)
891
+
892
+ unless existing.modules.superset? puppetfile.modules
893
+ missing_modules = puppetfile.modules - existing.modules
894
+
895
+ raise Bolt::Error.new(
896
+ "Puppetfile #{puppetfile_path} is missing specifications for modules: "\
897
+ "#{missing_modules.map(&:title).join(', ')}. This may not be a Puppetfile "\
898
+ "managed by Bolt. To forcibly overwrite the Puppetfile, run with the "\
899
+ "'--force' option.",
900
+ 'bolt/missing-module-specs'
901
+ )
896
902
  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
+ else
904
+ outputter.print_message "Resolving module dependencies, this may take a moment"
905
+ puppetfile.resolve
906
+ outputter.print_message "Writing Puppetfile at #{puppetfile_path}"
907
+ puppetfile.write(puppetfile_path, force: true)
903
908
  end
904
909
 
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
910
+ outputter.print_message "Syncing modules from #{puppetfile_path} to #{moduledir}"
911
+ ok = Bolt::Puppetfile::Installer.new(config).install(puppetfile_path, moduledir)
909
912
 
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
915
-
916
- def install_puppetfile(config, puppetfile, modulepath)
917
- require 'r10k/cli'
918
- require 'bolt/r10k_log_proxy'
913
+ # Automatically generate types after installing modules.
914
+ pal.generate_types
919
915
 
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
- }
916
+ outputter.print_puppetfile_result(ok, puppetfile_path, moduledir)
917
+ ok ? 0 : 1
918
+ end
927
919
 
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)
920
+ # Loads a Puppetfile and installs its modules.
921
+ #
922
+ def install_puppetfile(config, puppetfile, moduledir)
923
+ require 'bolt/puppetfile/installer'
931
924
 
932
- # Override the r10k logger with a proxy to our own logger
933
- R10K::Logging.instance_variable_set(:@outputter, Bolt::R10KLogProxy.new)
925
+ ok = Bolt::Puppetfile::Installer.new(config).install(puppetfile, moduledir)
934
926
 
935
- ok = install_action.call
936
- outputter.print_puppetfile_result(ok, puppetfile, moduledir)
937
- # Automatically generate types after installing modules
938
- pal.generate_types
927
+ # Automatically generate types after installing modules.
928
+ pal.generate_types
939
929
 
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
930
+ outputter.print_puppetfile_result(ok, puppetfile, moduledir)
931
+ ok ? 0 : 1
946
932
  end
947
933
 
948
934
  def pal
@@ -958,17 +944,17 @@ module Bolt
958
944
  # Collects the list of Bolt guides and maps them to their topics.
959
945
  def guides
960
946
  @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
947
+ root_path = File.expand_path(File.join(__dir__, '..', '..', 'guides'))
948
+ files = Dir.children(root_path).sort
949
+
950
+ files.each_with_object({}) do |file, guides|
951
+ next if file !~ /\.txt\z/
952
+ topic = File.basename(file, '.txt')
953
+ guides[topic] = File.join(root_path, file)
954
+ end
955
+ rescue SystemCallError => e
956
+ raise Bolt::FileError.new("#{e.message}: unable to load guides directory", root_path)
957
+ end
972
958
  end
973
959
 
974
960
  # Display the list of available Bolt guides.
@@ -1019,10 +1005,10 @@ module Bolt
1019
1005
 
1020
1006
  def analytics
1021
1007
  @analytics ||= begin
1022
- client = Bolt::Analytics.build_client
1023
- client.bundled_content = bundled_content
1024
- client
1025
- end
1008
+ client = Bolt::Analytics.build_client
1009
+ client.bundled_content = bundled_content
1010
+ client
1011
+ end
1026
1012
  end
1027
1013
 
1028
1014
  def bundled_content
@@ -1057,17 +1043,10 @@ module Bolt
1057
1043
  content
1058
1044
  end
1059
1045
 
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
1046
  # Gem installs include the aggregate, canary, and puppetdb_fact modules, while
1068
1047
  # package installs include modules listed in the Bolt repo Puppetfile
1069
1048
  def incomplete_install?
1070
- (Dir.children(Bolt::PAL::MODULES_PATH) - %w[aggregate canary puppetdb_fact]).empty?
1049
+ (Dir.children(Bolt::PAL::MODULES_PATH) - %w[aggregate canary puppetdb_fact secure_env_vars]).empty?
1071
1050
  end
1072
1051
 
1073
1052
  # Mimicks the output from Outputter::Human#fatal_error. This should be used to print