bolt 2.27.0 → 2.32.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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +13 -12
  3. data/bolt-modules/boltlib/lib/puppet/functions/write_file.rb +2 -2
  4. data/bolt-modules/out/lib/puppet/functions/out/message.rb +44 -1
  5. data/bolt-modules/prompt/lib/puppet/functions/prompt.rb +3 -0
  6. data/guides/module.txt +19 -0
  7. data/guides/modulepath.txt +25 -0
  8. data/lib/bolt/applicator.rb +14 -14
  9. data/lib/bolt/bolt_option_parser.rb +74 -22
  10. data/lib/bolt/catalog.rb +1 -1
  11. data/lib/bolt/cli.rb +178 -127
  12. data/lib/bolt/config.rb +13 -1
  13. data/lib/bolt/config/modulepath.rb +30 -0
  14. data/lib/bolt/config/options.rb +38 -9
  15. data/lib/bolt/config/transport/options.rb +1 -1
  16. data/lib/bolt/executor.rb +1 -1
  17. data/lib/bolt/inventory.rb +11 -10
  18. data/lib/bolt/logger.rb +26 -19
  19. data/lib/bolt/module_installer.rb +197 -0
  20. data/lib/bolt/{puppetfile → module_installer}/installer.rb +3 -2
  21. data/lib/bolt/module_installer/puppetfile.rb +117 -0
  22. data/lib/bolt/module_installer/puppetfile/forge_module.rb +54 -0
  23. data/lib/bolt/module_installer/puppetfile/git_module.rb +37 -0
  24. data/lib/bolt/module_installer/puppetfile/module.rb +26 -0
  25. data/lib/bolt/module_installer/resolver.rb +76 -0
  26. data/lib/bolt/module_installer/specs.rb +93 -0
  27. data/lib/bolt/module_installer/specs/forge_spec.rb +84 -0
  28. data/lib/bolt/module_installer/specs/git_spec.rb +178 -0
  29. data/lib/bolt/outputter.rb +2 -45
  30. data/lib/bolt/outputter/human.rb +78 -18
  31. data/lib/bolt/outputter/json.rb +22 -7
  32. data/lib/bolt/outputter/logger.rb +2 -2
  33. data/lib/bolt/pal.rb +29 -25
  34. data/lib/bolt/plugin.rb +1 -1
  35. data/lib/bolt/plugin/module.rb +1 -1
  36. data/lib/bolt/project.rb +32 -22
  37. data/lib/bolt/project_migrator.rb +80 -0
  38. data/lib/bolt/project_migrator/base.rb +39 -0
  39. data/lib/bolt/project_migrator/config.rb +67 -0
  40. data/lib/bolt/project_migrator/inventory.rb +67 -0
  41. data/lib/bolt/project_migrator/modules.rb +200 -0
  42. data/lib/bolt/shell/bash.rb +4 -3
  43. data/lib/bolt/transport/base.rb +4 -4
  44. data/lib/bolt/transport/ssh/connection.rb +1 -1
  45. data/lib/bolt/util.rb +51 -10
  46. data/lib/bolt/version.rb +1 -1
  47. data/lib/bolt_server/acl.rb +2 -2
  48. data/lib/bolt_server/base_config.rb +3 -3
  49. data/lib/bolt_server/file_cache.rb +11 -11
  50. data/lib/bolt_server/schemas/partials/task.json +17 -2
  51. data/lib/bolt_server/transport_app.rb +93 -13
  52. data/lib/bolt_spec/bolt_context.rb +8 -6
  53. data/lib/bolt_spec/plans.rb +1 -1
  54. data/lib/bolt_spec/plans/mock_executor.rb +1 -1
  55. data/lib/bolt_spec/run.rb +1 -1
  56. metadata +30 -11
  57. data/lib/bolt/project_migrate.rb +0 -138
  58. data/lib/bolt/puppetfile.rb +0 -160
  59. data/lib/bolt/puppetfile/module.rb +0 -66
  60. data/lib/bolt_server/pe/pal.rb +0 -67
@@ -391,6 +391,12 @@ module Bolt
391
391
  @logs << { warn: msg }
392
392
  end
393
393
 
394
+ if @project.modules && @data['modulepath']&.include?(@project.managed_moduledir.to_s)
395
+ raise Bolt::ValidationError,
396
+ "Found invalid path in modulepath: #{@project.managed_moduledir}. This path "\
397
+ "is automatically appended to the modulepath and cannot be configured."
398
+ end
399
+
394
400
  keys = OPTIONS.keys - %w[plugins plugin_hooks puppetdb]
395
401
  keys.each do |key|
396
402
  next unless Bolt::Util.references?(@data[key])
@@ -445,7 +451,13 @@ module Bolt
445
451
  end
446
452
 
447
453
  def modulepath
448
- @data['modulepath'] || @project.modulepath
454
+ path = @data['modulepath'] || @project.modulepath
455
+
456
+ if @project.modules
457
+ path + [@project.managed_moduledir.to_s]
458
+ else
459
+ path
460
+ end
449
461
  end
450
462
 
451
463
  def modulepath=(value)
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/config'
4
+
5
+ module Bolt
6
+ class Config
7
+ class Modulepath
8
+ BOLTLIB_PATH = File.expand_path('../../../bolt-modules', __dir__)
9
+ MODULES_PATH = File.expand_path('../../../modules', __dir__)
10
+
11
+ # The user_modulepath only includes the original modulepath and is used during pluginsync.
12
+ # We don't want to pluginsync any of the content from BOLT_MODULES since that content
13
+ # includes core modules that can conflict with modules installed with an agent.
14
+ attr_reader :user_modulepath
15
+
16
+ def initialize(user_modulepath, boltlib_path: BOLTLIB_PATH, builtin_content_path: MODULES_PATH)
17
+ @user_modulepath = Array(user_modulepath).flatten
18
+ @boltlib_path = Array(boltlib_path).flatten
19
+ @builtin_content_path = Array(builtin_content_path).flatten
20
+ end
21
+
22
+ # The full_modulepath includes both the BOLTLIB
23
+ # path and the MODULES_PATH to ensure bolt functions and
24
+ # built-in content are available in the compliler
25
+ def full_modulepath
26
+ @boltlib_path + @user_modulepath + @builtin_content_path
27
+ end
28
+ end
29
+ end
30
+ end
@@ -233,17 +233,46 @@ module Bolt
233
233
  "install` command.",
234
234
  type: Array,
235
235
  items: {
236
- type: Hash,
237
- required: ["name"],
238
- properties: {
239
- "name" => {
240
- description: "The name of the module.",
241
- type: String
236
+ type: [Hash, String],
237
+ oneOf: [
238
+ {
239
+ required: ["name"],
240
+ properties: {
241
+ "name" => {
242
+ description: "The name of the module.",
243
+ type: String
244
+ },
245
+ "version_requirement" => {
246
+ description: "The version requirement for the module. Accepts a specific version (1.2.3), version "\
247
+ "shorthand (1.2.x), or a version range (>= 1.2.0).",
248
+ type: String
249
+ }
250
+ }
251
+ },
252
+ {
253
+ required: %w[git ref],
254
+ properties: {
255
+ "git" => {
256
+ description: "The URL to the public git repository.",
257
+ type: String
258
+ },
259
+ "ref" => {
260
+ description: "The git reference to check out. Can be either a branch, tag, or commit SHA.",
261
+ type: String
262
+ }
263
+ }
242
264
  }
243
- }
265
+ ]
244
266
  },
245
267
  _plugin: false,
246
- _example: [{ "name" => "puppetlabs-mysql" }, { "name" => "puppetlabs-apache" }]
268
+ _example: [
269
+ "puppetlabs-facts",
270
+ { "name" => "puppetlabs-mysql" },
271
+ { "name" => "puppetlabs-apache", "version_requirement" => "5.5.0" },
272
+ { "name" => "puppetlabs-puppetdb", "version_requirement" => "7.x" },
273
+ { "name" => "puppetlabs-firewall", "version_requirement" => ">= 1.0.0 < 3.0.0" },
274
+ { "git" => "https://github.com/puppetlabs/puppetlabs-apt", "ref" => "7.6.0" }
275
+ ]
247
276
  },
248
277
  "name" => {
249
278
  description: "The name of the Bolt project. When this option is configured, the project is considered a "\
@@ -318,7 +347,7 @@ module Bolt
318
347
  "server_urls" => {
319
348
  description: "An array containing the PuppetDB host to connect to. Include the protocol `https` "\
320
349
  "and the port, which is usually `8081`. For example, "\
321
- "`https://my-master.example.com:8081`.",
350
+ "`https://my-puppetdb-server.com:8081`.",
322
351
  type: Array,
323
352
  _example: ["https://puppet.example.com:8081"]
324
353
  },
@@ -374,7 +374,7 @@ module Bolt
374
374
  },
375
375
  "ssh-command" => {
376
376
  type: [Array, String],
377
- description: "The command and flags to use when SSHing. This option is used when you need support for "\
377
+ description: "The command and options to use when SSHing. This option is used when you need support for "\
378
378
  "features or algorithms that are not supported by the net-ssh Ruby library. **This option "\
379
379
  "is experimental.** You can read more about this option in [Native SSH "\
380
380
  "transport](experimental_features.md#native-ssh-transport).",
@@ -227,7 +227,7 @@ module Bolt
227
227
  data[:resource_mean] = sum / resource_counts.length
228
228
  end
229
229
 
230
- @analytics&.event('Apply', 'ast', data)
230
+ @analytics&.event('Apply', 'ast', **data)
231
231
  end
232
232
 
233
233
  def report_yaml_plan(plan)
@@ -46,7 +46,7 @@ module Bolt
46
46
  end
47
47
 
48
48
  def self.from_config(config, plugins)
49
- logger = Logging.logger[self]
49
+ logger = Bolt::Logger.logger(self)
50
50
 
51
51
  if ENV.include?(ENVIRONMENT_VAR)
52
52
  begin
@@ -56,16 +56,17 @@ module Bolt
56
56
  rescue Psych::Exception
57
57
  raise Bolt::ParseError, "Could not parse inventory from $#{ENVIRONMENT_VAR}"
58
58
  end
59
+ elsif config.inventoryfile
60
+ data = Bolt::Util.read_yaml_hash(config.inventoryfile, 'inventory')
61
+ logger.debug("Loaded inventory from #{config.inventoryfile}")
59
62
  else
60
- data = if config.inventoryfile
61
- Bolt::Util.read_yaml_hash(config.inventoryfile, 'inventory')
62
- else
63
- i = Bolt::Util.read_optional_yaml_hash(config.default_inventoryfile, 'inventory')
64
- logger.debug("Loaded inventory from #{config.default_inventoryfile}") if i
65
- i
66
- end
67
- # This avoids rubocop complaining about identical conditionals
68
- logger.debug("Loaded inventory from #{config.inventoryfile}") if config.inventoryfile
63
+ data = Bolt::Util.read_optional_yaml_hash(config.default_inventoryfile, 'inventory')
64
+
65
+ if config.default_inventoryfile.exist?
66
+ logger.debug("Loaded inventory from #{config.default_inventoryfile}")
67
+ else
68
+ logger.debug("Tried to load inventory from #{config.default_inventoryfile}, but the file does not exist")
69
+ end
69
70
  end
70
71
 
71
72
  # Resolve plugin references from transport config
@@ -4,6 +4,10 @@ require 'logging'
4
4
 
5
5
  module Bolt
6
6
  module Logger
7
+ LEVELS = %w[trace debug info notice warn error fatal].freeze
8
+ @mutex = Mutex.new
9
+ @warnings = Set.new
10
+
7
11
  # This method provides a single point-of-entry to setup logging for both
8
12
  # the CLI and for tests. This is necessary because we define custom log
9
13
  # levels which create corresponding methods on the logger instances;
@@ -11,20 +15,25 @@ module Bolt
11
15
  # will fail.
12
16
  def self.initialize_logging
13
17
  # Initialization isn't idempotent and will result in warnings about const
14
- # redefs, so skip it if it's already been initialized
15
- return if Logging.initialized?
16
-
17
- Logging.init :trace, :debug, :info, :notice, :warn, :error, :fatal, :any
18
- @mutex = Mutex.new
19
-
20
- Logging.color_scheme(
21
- 'bolt',
22
- lines: {
23
- warn: :yellow,
24
- error: :red,
25
- fatal: %i[white on_red]
26
- }
27
- )
18
+ # redefs, so skip it if the log levels we expect are present. If it's
19
+ # already been initialized with an insufficient set of levels, go ahead
20
+ # and call init anyway or we'll have failures when calling log methods
21
+ # for missing levels.
22
+ unless levels & LEVELS == LEVELS
23
+ Logging.init(*LEVELS)
24
+ end
25
+
26
+ # As above, only create the color scheme if we haven't already created it.
27
+ unless Logging.color_scheme('bolt')
28
+ Logging.color_scheme(
29
+ 'bolt',
30
+ lines: {
31
+ warn: :yellow,
32
+ error: :red,
33
+ fatal: %i[white on_red]
34
+ }
35
+ )
36
+ end
28
37
  end
29
38
 
30
39
  def self.configure(destinations, color)
@@ -115,14 +124,12 @@ module Bolt
115
124
  end
116
125
 
117
126
  def self.warn_once(type, msg)
118
- @mutex.synchronize {
119
- @warnings ||= []
127
+ @mutex.synchronize do
120
128
  @logger ||= Bolt::Logger.logger(self)
121
- unless @warnings.include?(type)
129
+ if @warnings.add?(type)
122
130
  @logger.warn(msg)
123
- @warnings << type
124
131
  end
125
- }
132
+ end
126
133
  end
127
134
 
128
135
  def self.deprecation_warning(type, msg)
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/error'
4
+ require 'bolt/logger'
5
+ require 'bolt/module_installer/installer'
6
+ require 'bolt/module_installer/puppetfile'
7
+ require 'bolt/module_installer/resolver'
8
+ require 'bolt/module_installer/specs'
9
+
10
+ module Bolt
11
+ class ModuleInstaller
12
+ def initialize(outputter, pal)
13
+ @outputter = outputter
14
+ @pal = pal
15
+ @logger = Bolt::Logger.logger(self)
16
+ end
17
+
18
+ # Adds a single module to the project.
19
+ #
20
+ def add(name, specs, puppetfile_path, moduledir, config_path)
21
+ project_specs = Specs.new(specs)
22
+
23
+ # Exit early if project config already includes a spec with this name.
24
+ if project_specs.include?(name)
25
+ @outputter.print_message(
26
+ "Project configuration file #{config_path} already includes specification with name "\
27
+ "#{name}. Nothing to do."
28
+ )
29
+ return true
30
+ end
31
+
32
+ @outputter.print_message("Adding module #{name} to project\n\n")
33
+
34
+ # Generate the specs to resolve from. If a Puppetfile exists, parse it and
35
+ # convert the modules to specs. Otherwise, use the project specs.
36
+ resolve_specs = if puppetfile_path.exist?
37
+ existing_puppetfile = Puppetfile.parse(puppetfile_path)
38
+ existing_puppetfile.assert_satisfies(project_specs)
39
+ Specs.from_puppetfile(existing_puppetfile)
40
+ else
41
+ project_specs
42
+ end
43
+
44
+ # Resolve module dependencies. Attempt to first resolve with resolve
45
+ # specss. If that fails, fall back to resolving from project specs.
46
+ # This prevents Bolt from modifying installed modules unless there is
47
+ # a version conflict.
48
+ @outputter.print_action_step("Resolving module dependencies, this may take a moment")
49
+
50
+ begin
51
+ resolve_specs.add_specs('name' => name)
52
+ puppetfile = Resolver.new.resolve(resolve_specs)
53
+ rescue Bolt::Error
54
+ project_specs.add_specs('name' => name)
55
+ puppetfile = Resolver.new.resolve(project_specs)
56
+ end
57
+
58
+ # Display the diff between the existing Puppetfile and the new Puppetfile.
59
+ print_puppetfile_diff(existing_puppetfile, puppetfile)
60
+
61
+ # Add the module to the project configuration.
62
+ @outputter.print_action_step("Updating project configuration file at #{config_path}")
63
+
64
+ data = Bolt::Util.read_yaml_hash(config_path, 'project')
65
+ data['modules'] ||= []
66
+ data['modules'] << name
67
+
68
+ begin
69
+ File.write(config_path, data.to_yaml)
70
+ rescue SystemCallError => e
71
+ raise Bolt::FileError.new(
72
+ "Unable to update project configuration file: #{e.message}",
73
+ config
74
+ )
75
+ end
76
+
77
+ # Write the Puppetfile.
78
+ @outputter.print_action_step("Writing Puppetfile at #{puppetfile_path}")
79
+ puppetfile.write(puppetfile_path, moduledir)
80
+
81
+ # Install the modules.
82
+ install_puppetfile(puppetfile_path, moduledir)
83
+ end
84
+
85
+ # Outputs a diff of an old Puppetfile and a new Puppetfile.
86
+ #
87
+ def print_puppetfile_diff(old, new)
88
+ # Build hashes mapping the module name to the module object. This makes it
89
+ # a little easier to determine which modules have been added, removed, or
90
+ # modified.
91
+ old = (old&.modules || []).each_with_object({}) do |mod, acc|
92
+ next unless mod.type == :forge
93
+ acc[mod.full_name] = mod
94
+ end
95
+
96
+ new = new.modules.each_with_object({}) do |mod, acc|
97
+ next unless mod.type == :forge
98
+ acc[mod.full_name] = mod
99
+ end
100
+
101
+ # New modules are those present in new but not in old.
102
+ added = new.reject { |full_name, _mod| old.include?(full_name) }.values
103
+
104
+ if added.any?
105
+ diff = "Adding the following modules:\n"
106
+ added.each { |mod| diff += "#{mod.full_name} #{mod.version}\n" }
107
+ @outputter.print_action_step(diff)
108
+ end
109
+
110
+ # Upgraded modules are those that have a newer version in new than old.
111
+ upgraded = new.select do |full_name, mod|
112
+ if old.include?(full_name)
113
+ mod.version > old[full_name].version
114
+ end
115
+ end.keys
116
+
117
+ if upgraded.any?
118
+ diff = "Upgrading the following modules:\n"
119
+ upgraded.each { |full_name| diff += "#{full_name} #{old[full_name].version} to #{new[full_name].version}\n" }
120
+ @outputter.print_action_step(diff)
121
+ end
122
+
123
+ # Downgraded modules are those that have an older version in new than old.
124
+ downgraded = new.select do |full_name, mod|
125
+ if old.include?(full_name)
126
+ mod.version < old[full_name].version
127
+ end
128
+ end.keys
129
+
130
+ if downgraded.any?
131
+ diff = "Downgrading the following modules: \n"
132
+ downgraded.each { |full_name| diff += "#{full_name} #{old[full_name].version} to #{new[full_name].version}\n" }
133
+ @outputter.print_action_step(diff)
134
+ end
135
+
136
+ # Removed modules are those present in old but not in new.
137
+ removed = old.reject { |full_name, _mod| new.include?(full_name) }.values
138
+
139
+ if removed.any?
140
+ diff = "Removing the following modules:\n"
141
+ removed.each { |mod| diff += "#{mod.full_name} #{mod.version}\n" }
142
+ @outputter.print_action_step(diff)
143
+ end
144
+ end
145
+
146
+ # Installs a project's module dependencies.
147
+ #
148
+ def install(specs, path, moduledir, force: false, resolve: true)
149
+ @outputter.print_message("Installing project modules\n\n")
150
+
151
+ if resolve != false
152
+ specs = Specs.new(specs)
153
+
154
+ # If forcibly installing or if there is no Puppetfile, resolve
155
+ # and write a Puppetfile.
156
+ if force || !path.exist?
157
+ @outputter.print_action_step("Resolving module dependencies, this may take a moment")
158
+ puppetfile = Resolver.new.resolve(specs)
159
+
160
+ # We get here either through 'bolt module install' which uses the
161
+ # managed modulepath (which isn't configurable) or through bolt
162
+ # project init --modules, which uses the default modulepath. This
163
+ # should be safe to assume that if `.modules/` is the moduledir the
164
+ # user is using the new workflow
165
+ @outputter.print_action_step("Writing Puppetfile at #{path}")
166
+ if moduledir.basename.to_s == '.modules'
167
+ puppetfile.write(path, moduledir)
168
+ else
169
+ puppetfile.write(path)
170
+ end
171
+ # If not forcibly installing and there is a Puppetfile, assert
172
+ # that it satisfies the specs.
173
+ else
174
+ puppetfile = Puppetfile.parse(path)
175
+ puppetfile.assert_satisfies(specs)
176
+ end
177
+ end
178
+
179
+ # Install the modules.
180
+ install_puppetfile(path, moduledir)
181
+ end
182
+
183
+ # Installs the Puppetfile and generates types.
184
+ #
185
+ def install_puppetfile(path, moduledir, config = {})
186
+ @outputter.print_action_step("Syncing modules from #{path} to #{moduledir}")
187
+ ok = Installer.new(config).install(path, moduledir)
188
+
189
+ # Automatically generate types after installing modules
190
+ @pal.generate_types
191
+
192
+ @outputter.print_puppetfile_result(ok, path, moduledir)
193
+
194
+ ok
195
+ end
196
+ end
197
+ end
@@ -1,19 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'r10k/cli'
4
3
  require 'bolt/r10k_log_proxy'
5
4
  require 'bolt/error'
6
5
 
7
6
  # This class is used to install modules from a Puppetfile to a module directory.
8
7
  #
9
8
  module Bolt
10
- class Puppetfile
9
+ class ModuleInstaller
11
10
  class Installer
12
11
  def initialize(config = {})
13
12
  @config = config
14
13
  end
15
14
 
16
15
  def install(path, moduledir)
16
+ require 'r10k/cli'
17
+
17
18
  unless File.exist?(path)
18
19
  raise Bolt::FileError.new(
19
20
  "Could not find a Puppetfile at #{path}",