bolt 2.23.0 → 2.27.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 (61) 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/exe/bolt +1 -0
  7. data/guides/inventory.txt +19 -0
  8. data/guides/project.txt +22 -0
  9. data/lib/bolt/analytics.rb +11 -7
  10. data/lib/bolt/applicator.rb +11 -10
  11. data/lib/bolt/bolt_option_parser.rb +75 -13
  12. data/lib/bolt/catalog.rb +4 -2
  13. data/lib/bolt/cli.rb +156 -176
  14. data/lib/bolt/config.rb +55 -25
  15. data/lib/bolt/config/options.rb +28 -6
  16. data/lib/bolt/executor.rb +5 -3
  17. data/lib/bolt/inventory.rb +8 -1
  18. data/lib/bolt/inventory/group.rb +4 -4
  19. data/lib/bolt/inventory/inventory.rb +1 -1
  20. data/lib/bolt/inventory/target.rb +1 -1
  21. data/lib/bolt/logger.rb +12 -6
  22. data/lib/bolt/outputter/human.rb +10 -0
  23. data/lib/bolt/outputter/json.rb +11 -0
  24. data/lib/bolt/outputter/logger.rb +3 -3
  25. data/lib/bolt/outputter/rainbow.rb +15 -0
  26. data/lib/bolt/pal.rb +23 -12
  27. data/lib/bolt/pal/yaml_plan/evaluator.rb +1 -1
  28. data/lib/bolt/pal/yaml_plan/transpiler.rb +11 -3
  29. data/lib/bolt/plugin/puppetdb.rb +1 -1
  30. data/lib/bolt/project.rb +63 -17
  31. data/lib/bolt/project_migrate.rb +138 -0
  32. data/lib/bolt/puppetdb/client.rb +1 -1
  33. data/lib/bolt/puppetdb/config.rb +1 -1
  34. data/lib/bolt/puppetfile.rb +160 -0
  35. data/lib/bolt/puppetfile/installer.rb +43 -0
  36. data/lib/bolt/puppetfile/module.rb +66 -0
  37. data/lib/bolt/r10k_log_proxy.rb +1 -1
  38. data/lib/bolt/rerun.rb +2 -2
  39. data/lib/bolt/result.rb +23 -0
  40. data/lib/bolt/shell.rb +1 -1
  41. data/lib/bolt/shell/bash.rb +7 -7
  42. data/lib/bolt/task.rb +1 -1
  43. data/lib/bolt/transport/base.rb +1 -1
  44. data/lib/bolt/transport/docker/connection.rb +10 -10
  45. data/lib/bolt/transport/local/connection.rb +3 -3
  46. data/lib/bolt/transport/orch.rb +3 -3
  47. data/lib/bolt/transport/ssh.rb +1 -1
  48. data/lib/bolt/transport/ssh/connection.rb +6 -6
  49. data/lib/bolt/transport/ssh/exec_connection.rb +5 -5
  50. data/lib/bolt/transport/winrm.rb +1 -1
  51. data/lib/bolt/transport/winrm/connection.rb +9 -9
  52. data/lib/bolt/util.rb +2 -2
  53. data/lib/bolt/util/puppet_log_level.rb +4 -3
  54. data/lib/bolt/version.rb +1 -1
  55. data/lib/bolt_server/base_config.rb +2 -2
  56. data/lib/bolt_server/config.rb +1 -1
  57. data/lib/bolt_server/file_cache.rb +1 -1
  58. data/lib/bolt_server/transport_app.rb +189 -14
  59. data/lib/bolt_spec/plans.rb +1 -1
  60. data/lib/bolt_spec/run.rb +3 -0
  61. metadata +12 -12
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f58ca99a18fca1c109d531c61b55bcee45d73c14e55a074575ffaaf1d9f40aac
4
- data.tar.gz: ea55e4c9b9ba4ccd8afed70a6c62c603a119a46f406b573f429bd2cd89983672
3
+ metadata.gz: 851cd71cf3bbdd91a522f123db238205ca21875c7d1c9bf1eefe4850399ffb5d
4
+ data.tar.gz: 4ec2ca689f05d37ccbc1cd3962edae53dfcda79c0a58facd9a2f228af5df914c
5
5
  SHA512:
6
- metadata.gz: dc8845ebe2a171ce79b704c474594a9679e7078feaca324eb7f26b88e9e903de43361a4dbaa357de3cf47be4ae72b4db4690a76e0910c9595f52aab384768fbc
7
- data.tar.gz: 60b30a31debf0d031902d8df21372b2524246ee98749eeccfd588c00b3458bcb9c0ed2a1fbdeab266cce86332537bde1005aead37fece867d440d88989c2486d
6
+ metadata.gz: 3d595d36833a70860c7c997fff4cc55ce682dd3b3b9c4fc5e7b36067cbb73a665e65d473097993963f8409eb5a06a25b5b834de9dff6f42ad8e244972ab0ac4e
7
+ data.tar.gz: d136cd117711e3a103480b7cae52661e8ad3beef5023552f3bbaa8235e8f795410e13a1955290c7313fb6f7fced1485b275b8430a0d5c81297e11e1ff911f458
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
 
data/exe/bolt CHANGED
@@ -4,6 +4,7 @@
4
4
  require 'bolt'
5
5
  require 'bolt/cli'
6
6
 
7
+ Thread.current[:name] ||= 'main'
7
8
  cli = Bolt::CLI.new(ARGV)
8
9
  begin
9
10
  opts = cli.parse
@@ -0,0 +1,19 @@
1
+ TOPIC
2
+ inventory
3
+
4
+ DESCRIPTION
5
+ The inventory describes the targets that you run Bolt commands on, along
6
+ with any data and configuration for the targets. Targets in an inventory can
7
+ belong to one or more groups, allowing you to share data and configuration
8
+ across multiple targets and to specify multiple targets for your Bolt
9
+ commands without the need to list each target individually.
10
+
11
+ In most cases, Bolt loads the inventory from an inventory file in your Bolt
12
+ project. The inventory file is a YAML file named 'inventory.yaml'. Because
13
+ Bolt loads the inventory file from a Bolt project, you must have an existing
14
+ project configuration file named 'bolt-project.yaml' alongside the inventory
15
+ file.
16
+
17
+ DOCUMENTATION
18
+ https://pup.pt/bolt-inventory
19
+ https://pup.pt/bolt-inventory-reference
@@ -0,0 +1,22 @@
1
+ TOPIC
2
+ project
3
+
4
+ DESCRIPTION
5
+ A Bolt project is a directory that serves as the launching point for Bolt
6
+ and allows you to create a shareable orchestration application. Projects
7
+ typically include a project configuration file, an inventory file, and any
8
+ content you use in your project workflow, such as tasks and plans.
9
+
10
+ When you run Bolt, it runs in the context of a project. If the directory you
11
+ run Bolt from is not a project, Bolt attempts to find a project by
12
+ traversing the parent directories. If Bolt is unable to find a project, it
13
+ runs from the default project, located at '~/.puppetlabs/bolt'.
14
+
15
+ A directory is only considered a Bolt project when it has a project
16
+ configuration file named 'bolt-project.yaml'. Bolt doesn't load project data
17
+ and content, including inventory files, unless the data and content are part
18
+ of a project.
19
+
20
+ DOCUMENTATION
21
+ https://pup.pt/bolt-projects
22
+ https://pup.pt/bolt-project-reference
@@ -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
@@ -161,9 +165,9 @@ module Bolt
161
165
  # Handle analytics submission in the background to avoid blocking the
162
166
  # app or polluting the log with errors
163
167
  Concurrent::Future.execute(executor: @executor) do
164
- @logger.debug "Submitting analytics: #{JSON.pretty_generate(params)}"
168
+ @logger.trace "Submitting analytics: #{JSON.pretty_generate(params)}"
165
169
  @http.post(TRACKING_URL, params)
166
- @logger.debug "Completed analytics submission"
170
+ @logger.trace "Completed analytics submission"
167
171
  end
168
172
  end
169
173
 
@@ -210,18 +214,18 @@ 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
 
217
221
  def screen_view(screen, **_kwargs)
218
- @logger.debug "Skipping submission of '#{screen}' screenview because analytics is disabled"
222
+ @logger.trace "Skipping submission of '#{screen}' screenview because analytics is disabled"
219
223
  end
220
224
 
221
225
  def report_bundled_content(mode, name); end
222
226
 
223
227
  def event(category, action, **_kwargs)
224
- @logger.debug "Skipping submission of '#{category} #{action}' event because analytics is disabled"
228
+ @logger.trace "Skipping submission of '#{category} #{action}' event because analytics is disabled"
225
229
  end
226
230
 
227
231
  def finish; end
@@ -27,8 +27,8 @@ module Bolt
27
27
  @hiera_config = hiera_config ? validate_hiera_config(hiera_config) : nil
28
28
  @apply_settings = apply_settings || {}
29
29
 
30
- @pool = Concurrent::ThreadPoolExecutor.new(max_threads: max_compiles)
31
- @logger = Logging.logger[self]
30
+ @pool = Concurrent::ThreadPoolExecutor.new(name: 'apply', max_threads: max_compiles)
31
+ @logger = Bolt::Logger.logger(self)
32
32
  end
33
33
 
34
34
  private def libexec
@@ -75,23 +75,24 @@ 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
@@ -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])
@@ -217,6 +217,7 @@ module Bolt
217
217
  r = @executor.log_action(description, targets) do
218
218
  futures = targets.map do |target|
219
219
  Concurrent::Future.execute(executor: @pool) do
220
+ Thread.current[:name] ||= Thread.current.name
220
221
  @executor.with_node_logging("Compiling manifest block", [target]) do
221
222
  compile(target, scope)
222
223
  end
@@ -300,7 +301,7 @@ module Bolt
300
301
 
301
302
  files.each do |file|
302
303
  tar_path = Pathname.new(file).relative_path_from(parent)
303
- @logger.debug("Packing plugin #{file} to #{tar_path}")
304
+ @logger.trace("Packing plugin #{file} to #{tar_path}")
304
305
  stat = File.stat(file)
305
306
  content = File.binread(file)
306
307
  output.tar.add_file_simple(
@@ -314,7 +315,7 @@ module Bolt
314
315
  end
315
316
 
316
317
  duration = Time.now - start_time
317
- @logger.debug("Packed plugins in #{duration * 1000} ms")
318
+ @logger.trace("Packed plugins in #{duration * 1000} ms")
318
319
 
319
320
  output.close
320
321
  Base64.encode64(sio.string)
@@ -61,6 +61,18 @@ module Bolt
61
61
  { flags: OPTIONS[:global],
62
62
  banner: GROUP_HELP }
63
63
  end
64
+ when 'guide'
65
+ { flags: OPTIONS[:global] + %w[format],
66
+ banner: GUIDE_HELP }
67
+ when 'module'
68
+ case action
69
+ when 'install'
70
+ { flags: OPTIONS[:global] + %w[configfile force project],
71
+ banner: MODULE_INSTALL_HELP }
72
+ else
73
+ { flags: OPTIONS[:global],
74
+ banner: MODULE_HELP }
75
+ end
64
76
  when 'plan'
65
77
  case action
66
78
  when 'convert'
@@ -85,7 +97,7 @@ module Bolt
85
97
  { flags: OPTIONS[:global] + %w[modules],
86
98
  banner: PROJECT_INIT_HELP }
87
99
  when 'migrate'
88
- { flags: OPTIONS[:global] + %w[inventoryfile boltdir configfile],
100
+ { flags: OPTIONS[:global] + %w[inventoryfile project configfile],
89
101
  banner: PROJECT_MIGRATE_HELP }
90
102
  else
91
103
  { flags: OPTIONS[:global],
@@ -164,6 +176,7 @@ module Bolt
164
176
  command Run a command remotely
165
177
  file Copy files between the controller and targets
166
178
  group Show the list of groups in the inventory
179
+ guide View guides for Bolt concepts and features
167
180
  inventory Show the list of targets an action would run on
168
181
  plan Convert, create, show, and run Bolt plans
169
182
  project Create and migrate Bolt projects
@@ -171,6 +184,9 @@ module Bolt
171
184
  script Upload a local script and run it remotely
172
185
  secret Create encryption keys and encrypt and decrypt values
173
186
  task Show and run Bolt tasks
187
+
188
+ GUIDES
189
+ For a list of guides on Bolt's concepts and features, run 'bolt guide'.
174
190
  HELP
175
191
 
176
192
  APPLY_HELP = <<~HELP
@@ -289,6 +305,26 @@ module Bolt
289
305
  Show the list of groups in the inventory.
290
306
  HELP
291
307
 
308
+ GUIDE_HELP = <<~HELP
309
+ NAME
310
+ guide
311
+
312
+ USAGE
313
+ bolt guide [topic] [options]
314
+
315
+ DESCRIPTION
316
+ View guides for Bolt's concepts and features.
317
+
318
+ Omitting a topic will display a list of available guides,
319
+ while providing a topic will display the relevant guide.
320
+
321
+ EXAMPLES
322
+ View a list of available guides
323
+ bolt guide
324
+ View the 'project' guide page
325
+ bolt guide project
326
+ HELP
327
+
292
328
  INVENTORY_HELP = <<~HELP
293
329
  NAME
294
330
  inventory
@@ -314,6 +350,35 @@ module Bolt
314
350
  Show the list of targets an action would run on.
315
351
  HELP
316
352
 
353
+ MODULE_HELP = <<~HELP
354
+ NAME
355
+ module
356
+
357
+ USAGE
358
+ bolt module <action> [options]
359
+
360
+ DESCRIPTION
361
+ Install the project's modules
362
+
363
+ ACTIONS
364
+ install Install the project's modules
365
+ HELP
366
+
367
+ MODULE_INSTALL_HELP = <<~HELP
368
+ NAME
369
+ install
370
+
371
+ USAGE
372
+ bolt module install [options]
373
+
374
+ DESCRIPTION
375
+ Install the project's modules.
376
+
377
+ Module declarations are loaded from the project's configuration
378
+ file. Bolt will automatically resolve all module dependencies,
379
+ generate a Puppetfile, and install the modules.
380
+ HELP
381
+
317
382
  PLAN_HELP = <<~HELP
318
383
  NAME
319
384
  plan
@@ -339,11 +404,11 @@ module Bolt
339
404
  bolt plan convert <path> [options]
340
405
 
341
406
  DESCRIPTION
342
- Convert a YAML plan to a Bolt plan.
407
+ Convert a YAML plan to a Puppet language plan and print the converted plan to stdout.
343
408
 
344
409
  Converting a YAML plan may result in a plan that is syntactically
345
410
  correct but has different behavior. Always verify a converted plan's
346
- functionality.
411
+ functionality. Note that the converted plan is not written to a file.
347
412
 
348
413
  EXAMPLES
349
414
  bolt plan convert path/to/plan/myplan.yaml
@@ -394,9 +459,9 @@ module Bolt
394
459
  the plan, including a list of available parameters.
395
460
 
396
461
  EXAMPLES
397
- Display a list of available tasks
462
+ Display a list of available plans
398
463
  bolt plan show
399
- Display documentation for the canary task
464
+ Display documentation for the aggregate::count plan
400
465
  bolt plan show aggregate::count
401
466
  HELP
402
467
 
@@ -444,10 +509,7 @@ module Bolt
444
509
  bolt project migrate [options]
445
510
 
446
511
  DESCRIPTION
447
- Migrate a Bolt project to the latest version.
448
-
449
- Loads a Bolt project's inventory file and migrates it to the latest version. The
450
- inventory file is modified in place and will not preserve comments or formatting.
512
+ Migrate a Bolt project to use current best practices and the latest version of configuration files.
451
513
  HELP
452
514
 
453
515
  PUPPETFILE_HELP = <<~HELP
@@ -804,7 +866,7 @@ module Bolt
804
866
  "This option is experimental.") do |exec|
805
867
  @options[:'copy-command'] = exec
806
868
  end
807
- define('--connect-timeout TIMEOUT', Integer, 'Connection timeout (defaults vary)') do |timeout|
869
+ define('--connect-timeout TIMEOUT', Integer, 'Connection timeout in seconds (defaults vary)') do |timeout|
808
870
  @options[:'connect-timeout'] = timeout
809
871
  end
810
872
  define('--[no-]tty', 'Request a pseudo TTY on targets that support it') do |tty|
@@ -840,9 +902,9 @@ module Bolt
840
902
  define('--modules MODULES',
841
903
  'A comma-separated list of modules to install from the Puppet Forge',
842
904
  'when initializing a project. Resolves and installs all dependencies.') do |modules|
843
- @options[:modules] = modules.split(',')
905
+ @options[:modules] = modules.split(',').map { |mod| { 'name' => mod } }
844
906
  end
845
- define('--force', 'Overwrite existing key pairs') do |_force|
907
+ define('--force', 'Force a destructive action') do |_force|
846
908
  @options[:force] = true
847
909
  end
848
910
 
@@ -863,7 +925,7 @@ module Bolt
863
925
  end
864
926
  define('--log-level LEVEL',
865
927
  "Set the log level for the console. Available options are",
866
- "debug, info, notice, warn, error, fatal, any.") do |level|
928
+ "trace, debug, info, warn, error, fatal, any.") do |level|
867
929
  @options[:log] = { 'console' => { 'level' => level } }
868
930
  end
869
931
  define('--plugin PLUGIN', 'Select the plugin to use') do |plug|
@@ -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,
@@ -20,6 +20,7 @@ require 'bolt/logger'
20
20
  require 'bolt/outputter'
21
21
  require 'bolt/puppetdb'
22
22
  require 'bolt/plugin'
23
+ require 'bolt/project_migrate'
23
24
  require 'bolt/pal'
24
25
  require 'bolt/target'
25
26
  require 'bolt/version'
@@ -38,13 +39,15 @@ module Bolt
38
39
  'inventory' => %w[show],
39
40
  'group' => %w[show],
40
41
  'project' => %w[init migrate],
41
- 'apply' => %w[] }.freeze
42
+ 'apply' => %w[],
43
+ 'guide' => %w[],
44
+ 'module' => %w[install] }.freeze
42
45
 
43
46
  attr_reader :config, :options
44
47
 
45
48
  def initialize(argv)
46
49
  Bolt::Logger.initialize_logging
47
- @logger = Logging.logger[self]
50
+ @logger = Bolt::Logger.logger(self)
48
51
  @argv = argv
49
52
  @options = {}
50
53
  end
@@ -77,7 +80,7 @@ module Bolt
77
80
 
78
81
  # Wrapper method that is called by the Bolt executable. Parses the command and
79
82
  # then loads the project and config. Once config is loaded, it completes the
80
- # setup process by configuring Bolt and issuing warnings.
83
+ # setup process by configuring Bolt and logging messages.
81
84
  #
82
85
  # This separation is needed since the Bolt::Outputter class that normally handles
83
86
  # printing errors relies on config being loaded. All setup that happens before
@@ -165,20 +168,17 @@ module Bolt
165
168
  raise e
166
169
  end
167
170
 
168
- # Completes the setup process by configuring Bolt and issuing warnings
171
+ # Completes the setup process by configuring Bolt and log messages
169
172
  def finalize_setup
170
173
  Bolt::Logger.configure(config.log, config.color)
171
174
  Bolt::Logger.analytics = analytics
172
175
 
173
- # 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
174
177
  config.check_path_case('modulepath', config.modulepath)
175
178
  config.project.check_deprecated_file
176
179
 
177
- # Log the file paths for loaded config files
178
- config_loaded
179
-
180
- # Display warnings created during parser and config initialization
181
- 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]) }
182
182
  @parser_deprecations.each { |dep| Bolt::Logger.deprecation_warning(dep[:type], dep[:msg]) }
183
183
  config.deprecations.each { |dep| Bolt::Logger.deprecation_warning(dep[:type], dep[:msg]) }
184
184
 
@@ -211,10 +211,14 @@ module Bolt
211
211
  end
212
212
 
213
213
  def validate(options)
214
- 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])
215
219
  raise Bolt::CLIError,
216
220
  "Expected subcommand '#{options[:subcommand]}' to be one of " \
217
- "#{COMMANDS.keys.join(', ')}"
221
+ "#{commands.keys.join(', ')}"
218
222
  end
219
223
 
220
224
  actions = COMMANDS[options[:subcommand]]
@@ -354,7 +358,7 @@ module Bolt
354
358
  # Initialize inventory and targets. Errors here are better to catch early.
355
359
  # options[:target_args] will contain a string/array version of the targetting options this is passed to plans
356
360
  # options[:targets] will contain a resolved set of Target objects
357
- unless %w[project puppetfile secret].include?(options[:subcommand]) ||
361
+ unless %w[guide module project puppetfile secret].include?(options[:subcommand]) ||
358
362
  %w[convert new show].include?(options[:action])
359
363
  update_targets(options)
360
364
  end
@@ -411,7 +415,7 @@ module Bolt
411
415
  list_modules
412
416
  return 0
413
417
  when 'convert'
414
- convert_plan(options[:object])
418
+ pal.convert_plan(options[:object])
415
419
  return 0
416
420
  end
417
421
 
@@ -422,12 +426,20 @@ module Bolt
422
426
  end
423
427
 
424
428
  case options[:subcommand]
429
+ when 'guide'
430
+ code = if options[:object]
431
+ show_guide(options[:object])
432
+ else
433
+ list_topics
434
+ end
425
435
  when 'project'
426
436
  case options[:action]
427
437
  when 'init'
428
438
  code = initialize_project
429
439
  when 'migrate'
430
- code = migrate_project
440
+ inv = config.inventoryfile
441
+ path = config.project.path
442
+ code = Bolt::ProjectMigrate.new(path, outputter, inv).migrate_project
431
443
  end
432
444
  when 'plan'
433
445
  case options[:action]
@@ -436,12 +448,17 @@ module Bolt
436
448
  when 'run'
437
449
  code = run_plan(options[:object], options[:task_options], options[:target_args], options)
438
450
  end
451
+ when 'module'
452
+ case options[:action]
453
+ when 'install'
454
+ code = install_project_modules
455
+ end
439
456
  when 'puppetfile'
440
457
  case options[:action]
441
458
  when 'generate-types'
442
459
  code = generate_types
443
460
  when 'install'
444
- code = install_puppetfile(config.puppetfile_config, config.puppetfile, config.modulepath)
461
+ code = install_puppetfile(config.puppetfile_config, config.puppetfile, config.modulepath.first)
445
462
  end
446
463
  when 'secret'
447
464
  code = Bolt::Secret.execute(plugins, outputter, options)
@@ -791,36 +808,38 @@ module Bolt
791
808
  old_config = project + 'bolt.yaml'
792
809
  config = project + 'bolt-project.yaml'
793
810
  puppetfile = project + 'Puppetfile'
794
- modulepath = [project + 'modules']
811
+ moduledir = project + 'modules'
795
812
 
796
- # If modules were specified, first check if there is already a Puppetfile at the project
797
- # directory, erroring if there is. If there is no Puppetfile, generate the Puppetfile
798
- # content by resolving the specified modules and all their dependencies.
799
- # We generate the Puppetfile first so that any errors in resolving modules and their
800
- # dependencies are caught early and do not create a project directory.
813
+ # Warn the user if the project directory already exists. We don't error
814
+ # here since users might not have installed any modules yet. If both
815
+ # bolt.yaml and bolt-project.yaml exist, this will just warn about
816
+ # bolt-project.yaml and subsequent Bolt actions will warn about both files
817
+ # existing.
818
+ if config.exist?
819
+ @logger.warn "Found existing project directory at #{project}. Skipping file creation."
820
+ elsif old_config.exist?
821
+ @logger.warn "Found existing #{old_config.basename} at #{project}. "\
822
+ "#{old_config.basename} is deprecated, please rename to #{config.basename}."
823
+ end
824
+
825
+ # If modules were specified, first check if there is already a Puppetfile
826
+ # at the project directory, erroring if there is. If there is no
827
+ # Puppetfile, install the specified modules. The module installer will
828
+ # resolve dependencies, generate a Puppetfile, and install the modules.
801
829
  if options[:modules]
802
830
  if puppetfile.exist?
803
831
  raise Bolt::CLIError,
804
- "Found existing Puppetfile at #{puppetfile}, unable to initialize project with "\
805
- "#{options[:modules].join(', ')}"
806
- else
807
- puppetfile_specs = resolve_puppetfile_specs
832
+ "Found existing Puppetfile at #{puppetfile}, unable to initialize "\
833
+ "project with modules."
808
834
  end
835
+
836
+ install_modules(puppetfile, {}, moduledir, options[:modules])
809
837
  end
810
838
 
811
- # Warn the user if the project directory already exists. We don't error here since users
812
- # might not have installed any modules yet.
813
- # If both bolt.yaml and bolt-project.yaml exist, this will just warn
814
- # about bolt-project.yaml and subsequent Bolt actions will warn about
815
- # both files existing
816
- if config.exist?
817
- @logger.warn "Found existing project directory at #{project}. Skipping file creation."
818
- # This won't get called if bolt-project.yaml exists
819
- elsif old_config.exist?
820
- @logger.warn "Found existing #{old_config.basename} at #{project}. "\
821
- "#{old_config.basename} is deprecated, please rename to #{config.basename}."
822
- # Bless the project directory as a...wait for it...project
823
- else
839
+ # If either bolt.yaml or bolt-project.yaml exist, the user has already
840
+ # been warned and we can just finish project creation. Otherwise, create a
841
+ # bolt-project.yaml with the project name in it.
842
+ unless config.exist? || old_config.exist?
824
843
  begin
825
844
  content = { 'name' => name }
826
845
  File.write(config.to_path, content.to_yaml)
@@ -830,152 +849,82 @@ module Bolt
830
849
  end
831
850
  end
832
851
 
833
- # Write the generated Puppetfile to the fancy new project
834
- if puppetfile_specs
835
- File.write(puppetfile, puppetfile_specs.join("\n"))
836
- outputter.print_message "Successfully created Puppetfile at #{puppetfile}"
837
- # Install the modules from our shiny new Puppetfile
838
- if install_puppetfile({}, puppetfile, modulepath)
839
- outputter.print_message "Successfully installed #{options[:modules].join(', ')}"
840
- else
841
- raise Bolt::CLIError, "Could not install #{options[:modules].join(', ')}"
842
- end
843
- end
844
-
845
852
  0
846
853
  end
847
854
 
848
- # Resolves Puppetfile specs from user-specified modules and dependencies resolved
849
- # by the puppetfile-resolver gem.
850
- def resolve_puppetfile_specs
851
- require 'puppetfile-resolver'
852
-
853
- # Build the document model from the module names, defaulting to the latest version of each module
854
- model = PuppetfileResolver::Puppetfile::Document.new('')
855
- options[:modules].each do |mod_name|
856
- model.add_module(
857
- PuppetfileResolver::Puppetfile::ForgeModule.new(mod_name).tap { |mod| mod.version = :latest }
858
- )
859
- end
860
-
861
- # Make sure the Puppetfile model is valid
862
- unless model.valid?
863
- raise Bolt::ValidationError,
864
- "Unable to resolve dependencies for #{options[:modules].join(', ')}"
855
+ # Installs modules declared in the project configuration file.
856
+ #
857
+ def install_project_modules
858
+ if config.project.modules.nil?
859
+ outputter.print_message "Project configuration file '#{config.project.project_file}' "\
860
+ "does not specify any module dependencies. Nothing to do."
861
+ return 0
865
862
  end
866
863
 
867
- # Create the resolver using the Puppetfile model. nil disables Puppet version restrictions.
868
- resolver = PuppetfileResolver::Resolver.new(model, nil)
869
-
870
- # Configure and resolve the dependency graph
871
- result = resolver.resolve(
872
- cache: nil,
873
- ui: nil,
874
- module_paths: [],
875
- allow_missing_modules: true
864
+ install_modules(
865
+ config.puppetfile,
866
+ config.puppetfile_config,
867
+ config.project.path + '.modules',
868
+ config.project.modules
876
869
  )
870
+ end
877
871
 
878
- # Validate that the modules exist
879
- missing_graph = result.specifications.select do |_name, spec|
880
- spec.instance_of? PuppetfileResolver::Models::MissingModuleSpecification
881
- end
882
-
883
- if missing_graph.any?
884
- titles = model.modules.each_with_object({}) do |mod, acc|
885
- acc[mod.name] = mod.title
872
+ # Installs modules declared in the project configuration file.
873
+ #
874
+ def install_modules(puppetfile_path, config, moduledir, modules)
875
+ require 'bolt/puppetfile'
876
+ require 'bolt/puppetfile/installer'
877
+
878
+ puppetfile = Bolt::Puppetfile.new(modules)
879
+
880
+ # If the Puppetfile exists, check if it includes specs for each declared
881
+ # module, erroring if there are any missing. Otherwise, resolve the
882
+ # module dependencies and write a new Puppetfile. Users can forcibly
883
+ # overwrite an existing Puppetfile with the '--force' option.
884
+ if puppetfile_path.exist? && !options[:force]
885
+ outputter.print_message "Parsing existing Puppetfile at #{puppetfile_path}"
886
+ existing = Bolt::Puppetfile.parse(puppetfile_path)
887
+
888
+ unless existing.modules.superset? puppetfile.modules
889
+ missing_modules = puppetfile.modules - existing.modules
890
+
891
+ raise Bolt::Error.new(
892
+ "Puppetfile #{puppetfile_path} is missing specifications for modules: "\
893
+ "#{missing_modules.map(&:title).join(', ')}. This may not be a Puppetfile "\
894
+ "managed by Bolt. To forcibly overwrite the Puppetfile, run with the "\
895
+ "'--force' option.",
896
+ 'bolt/missing-module-specs'
897
+ )
886
898
  end
887
-
888
- names = titles.values_at(*missing_graph.keys)
889
- plural = names.count == 1 ? '' : 's'
890
-
891
- raise Bolt::ValidationError,
892
- "Unknown module name#{plural} #{names.join(', ')}"
893
- end
894
-
895
- # Filter the dependency graph to only include module specifications
896
- spec_graph = result.specifications.select do |_name, spec|
897
- spec.instance_of? PuppetfileResolver::Models::ModuleSpecification
898
- end
899
-
900
- # Map specification models to a Puppetfile specification
901
- spec_graph.values.map do |spec|
902
- "mod '#{spec.owner}-#{spec.name}', '#{spec.version}'"
899
+ else
900
+ outputter.print_message "Resolving module dependencies, this may take a moment"
901
+ puppetfile.resolve
902
+ outputter.print_message "Writing Puppetfile at #{puppetfile_path}"
903
+ puppetfile.write(puppetfile_path, force: true)
903
904
  end
904
- end
905
905
 
906
- def migrate_project
907
- inventory_file = config.inventoryfile || config.default_inventoryfile
908
- data = Bolt::Util.read_yaml_hash(inventory_file, 'inventory')
906
+ outputter.print_message "Syncing modules from #{puppetfile_path} to #{moduledir}"
907
+ ok = Bolt::Puppetfile::Installer.new(config).install(puppetfile_path, moduledir)
909
908
 
910
- data.delete('version') if data['version'] != 2
911
-
912
- migrated = migrate_group(data)
913
-
914
- ok = File.write(inventory_file, data.to_yaml) if migrated
915
-
916
- result = if migrated && ok
917
- "Successfully migrated Bolt project to latest version."
918
- elsif !migrated
919
- "Bolt project already on latest version. Nothing to do."
920
- else
921
- "Could not migrate Bolt project to latest version."
922
- end
923
- outputter.print_message result
909
+ # Automatically generate types after installing modules.
910
+ pal.generate_types
924
911
 
912
+ outputter.print_puppetfile_result(ok, puppetfile_path, moduledir)
925
913
  ok ? 0 : 1
926
914
  end
927
915
 
928
- # Walks an inventory hash and replaces all 'nodes' keys with 'targets' keys
929
- # and all 'name' keys nested in a 'targets' hash with 'uri' keys. Data is
930
- # modified in place.
931
- def migrate_group(group)
932
- migrated = false
933
- if group.key?('nodes')
934
- migrated = true
935
- targets = group['nodes'].map do |target|
936
- target['uri'] = target.delete('name') if target.is_a?(Hash)
937
- target
938
- end
939
- group.delete('nodes')
940
- group['targets'] = targets
941
- end
942
- (group['groups'] || []).each do |subgroup|
943
- migrated_group = migrate_group(subgroup)
944
- migrated ||= migrated_group
945
- end
946
- migrated
947
- end
948
-
949
- def install_puppetfile(config, puppetfile, modulepath)
950
- require 'r10k/cli'
951
- require 'bolt/r10k_log_proxy'
952
-
953
- if puppetfile.exist?
954
- moduledir = modulepath.first.to_s
955
- r10k_opts = {
956
- root: puppetfile.dirname.to_s,
957
- puppetfile: puppetfile.to_s,
958
- moduledir: moduledir
959
- }
960
-
961
- settings = R10K::Settings.global_settings.evaluate(config)
962
- R10K::Initializers::GlobalInitializer.new(settings).call
963
- install_action = R10K::Action::Puppetfile::Install.new(r10k_opts, nil)
916
+ # Loads a Puppetfile and installs its modules.
917
+ #
918
+ def install_puppetfile(config, puppetfile, moduledir)
919
+ require 'bolt/puppetfile/installer'
964
920
 
965
- # Override the r10k logger with a proxy to our own logger
966
- R10K::Logging.instance_variable_set(:@outputter, Bolt::R10KLogProxy.new)
921
+ ok = Bolt::Puppetfile::Installer.new(config).install(puppetfile, moduledir)
967
922
 
968
- ok = install_action.call
969
- outputter.print_puppetfile_result(ok, puppetfile, moduledir)
970
- # Automatically generate types after installing modules
971
- pal.generate_types
923
+ # Automatically generate types after installing modules.
924
+ pal.generate_types
972
925
 
973
- ok ? 0 : 1
974
- else
975
- raise Bolt::FileError.new("Could not find a Puppetfile at #{puppetfile}", puppetfile)
976
- end
977
- rescue R10K::Error => e
978
- raise PuppetfileError, e
926
+ outputter.print_puppetfile_result(ok, puppetfile, moduledir)
927
+ ok ? 0 : 1
979
928
  end
980
929
 
981
930
  def pal
@@ -988,8 +937,46 @@ module Bolt
988
937
  config.project)
989
938
  end
990
939
 
991
- def convert_plan(plan)
992
- pal.convert_plan(plan)
940
+ # Collects the list of Bolt guides and maps them to their topics.
941
+ def guides
942
+ @guides ||= begin
943
+ root_path = File.expand_path(File.join(__dir__, '..', '..', 'guides'))
944
+ files = Dir.children(root_path).sort
945
+
946
+ files.each_with_object({}) do |file, guides|
947
+ next if file !~ /\.txt\z/
948
+ topic = File.basename(file, '.txt')
949
+ guides[topic] = File.join(root_path, file)
950
+ end
951
+ rescue SystemCallError => e
952
+ raise Bolt::FileError.new("#{e.message}: unable to load guides directory", root_path)
953
+ end
954
+ end
955
+
956
+ # Display the list of available Bolt guides.
957
+ def list_topics
958
+ outputter.print_topics(guides.keys)
959
+ 0
960
+ end
961
+
962
+ # Display a specific Bolt guide.
963
+ def show_guide(topic)
964
+ if guides[topic]
965
+ analytics.event('Guide', 'known_topic', label: topic)
966
+
967
+ begin
968
+ guide = File.read(guides[topic])
969
+ rescue SystemCallError => e
970
+ raise Bolt::FileError("#{e.message}: unable to load guide page", filepath)
971
+ end
972
+
973
+ outputter.print_guide(guide, topic)
974
+ else
975
+ analytics.event('Guide', 'unknown_topic', label: topic)
976
+ outputter.print_message("Did not find guide for topic '#{topic}'.\n\n")
977
+ list_topics
978
+ end
979
+ 0
993
980
  end
994
981
 
995
982
  def validate_file(type, path, allow_dir = false)
@@ -1052,13 +1039,6 @@ module Bolt
1052
1039
  content
1053
1040
  end
1054
1041
 
1055
- def config_loaded
1056
- msg = <<~MSG.chomp
1057
- Loaded configuration from: '#{config.config_files.join("', '")}'
1058
- MSG
1059
- @logger.debug(msg)
1060
- end
1061
-
1062
1042
  # Gem installs include the aggregate, canary, and puppetdb_fact modules, while
1063
1043
  # package installs include modules listed in the Bolt repo Puppetfile
1064
1044
  def incomplete_install?