bolt 2.26.0 → 2.31.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 (51) 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/lib/bolt/analytics.rb +4 -0
  5. data/lib/bolt/applicator.rb +19 -18
  6. data/lib/bolt/bolt_option_parser.rb +112 -22
  7. data/lib/bolt/catalog.rb +1 -1
  8. data/lib/bolt/cli.rb +210 -174
  9. data/lib/bolt/config.rb +22 -2
  10. data/lib/bolt/config/modulepath.rb +30 -0
  11. data/lib/bolt/config/options.rb +30 -0
  12. data/lib/bolt/config/transport/options.rb +1 -1
  13. data/lib/bolt/executor.rb +1 -1
  14. data/lib/bolt/inventory.rb +11 -10
  15. data/lib/bolt/logger.rb +26 -19
  16. data/lib/bolt/module_installer.rb +242 -0
  17. data/lib/bolt/outputter.rb +4 -0
  18. data/lib/bolt/outputter/human.rb +77 -17
  19. data/lib/bolt/outputter/json.rb +21 -6
  20. data/lib/bolt/outputter/logger.rb +2 -2
  21. data/lib/bolt/pal.rb +46 -25
  22. data/lib/bolt/plugin.rb +1 -1
  23. data/lib/bolt/plugin/module.rb +1 -1
  24. data/lib/bolt/project.rb +62 -12
  25. data/lib/bolt/project_migrator.rb +80 -0
  26. data/lib/bolt/project_migrator/base.rb +39 -0
  27. data/lib/bolt/project_migrator/config.rb +67 -0
  28. data/lib/bolt/project_migrator/inventory.rb +67 -0
  29. data/lib/bolt/project_migrator/modules.rb +198 -0
  30. data/lib/bolt/puppetfile.rb +149 -0
  31. data/lib/bolt/puppetfile/installer.rb +43 -0
  32. data/lib/bolt/puppetfile/module.rb +93 -0
  33. data/lib/bolt/rerun.rb +1 -1
  34. data/lib/bolt/result.rb +15 -0
  35. data/lib/bolt/shell/bash.rb +4 -3
  36. data/lib/bolt/transport/base.rb +4 -4
  37. data/lib/bolt/transport/ssh/connection.rb +1 -1
  38. data/lib/bolt/util.rb +51 -10
  39. data/lib/bolt/version.rb +1 -1
  40. data/lib/bolt_server/acl.rb +2 -2
  41. data/lib/bolt_server/base_config.rb +3 -3
  42. data/lib/bolt_server/config.rb +1 -1
  43. data/lib/bolt_server/file_cache.rb +11 -11
  44. data/lib/bolt_server/transport_app.rb +206 -27
  45. data/lib/bolt_spec/bolt_context.rb +8 -6
  46. data/lib/bolt_spec/plans.rb +1 -1
  47. data/lib/bolt_spec/plans/mock_executor.rb +1 -1
  48. data/lib/bolt_spec/run.rb +1 -1
  49. metadata +14 -6
  50. data/lib/bolt/project_migrate.rb +0 -138
  51. data/lib/bolt_server/pe/pal.rb +0 -67
@@ -64,7 +64,7 @@ module Bolt
64
64
  end
65
65
 
66
66
  data = load_defaults(project).push(
67
- filepath: project.config_file,
67
+ filepath: configfile,
68
68
  data: conf,
69
69
  logs: logs,
70
70
  deprecations: []
@@ -344,6 +344,14 @@ module Bolt
344
344
  end
345
345
 
346
346
  private def update_logs(logs)
347
+ begin
348
+ if logs['bolt-debug.log'] && logs['bolt-debug.log'] != 'disable'
349
+ FileUtils.touch(File.expand_path('bolt-debug.log', @project.path))
350
+ end
351
+ rescue StandardError
352
+ logs.delete('bolt-debug.log')
353
+ end
354
+
347
355
  logs.each_with_object({}) do |(key, val), acc|
348
356
  # Remove any disabled logs
349
357
  next if val == 'disable'
@@ -383,6 +391,12 @@ module Bolt
383
391
  @logs << { warn: msg }
384
392
  end
385
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
+
386
400
  keys = OPTIONS.keys - %w[plugins plugin_hooks puppetdb]
387
401
  keys.each do |key|
388
402
  next unless Bolt::Util.references?(@data[key])
@@ -437,7 +451,13 @@ module Bolt
437
451
  end
438
452
 
439
453
  def modulepath
440
- @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
441
461
  end
442
462
 
443
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
@@ -227,6 +227,35 @@ module Bolt
227
227
  _example: ["~/.puppetlabs/bolt/modules", "~/.puppetlabs/bolt/site-modules"],
228
228
  _default: ["project/modules", "project/site-modules", "project/site"]
229
229
  },
230
+ "modules" => {
231
+ description: "A list of module dependencies for the project. Each dependency is a map of data specifying "\
232
+ "the module to install. To install the project's module dependencies, run the `bolt module "\
233
+ "install` command.",
234
+ type: Array,
235
+ items: {
236
+ type: [Hash, String],
237
+ required: ["name"],
238
+ properties: {
239
+ "name" => {
240
+ description: "The name of the module.",
241
+ type: String
242
+ },
243
+ "version_requirement" => {
244
+ description: "The version requirement for the module. Accepts a specific version (1.2.3), version "\
245
+ "shorthand (1.2.x), or a version range (>= 1.2.0).",
246
+ type: String
247
+ }
248
+ }
249
+ },
250
+ _plugin: false,
251
+ _example: [
252
+ { "name" => "puppetlabs-mysql" },
253
+ "puppetlabs-facts",
254
+ { "name" => "puppetlabs-apache", "version_requirement" => "5.5.0" },
255
+ { "name" => "puppetlabs-puppetdb", "version_requirement" => "7.x" },
256
+ { "name" => "puppetlabs-firewall", "version_requirement" => ">= 1.0.0 < 3.0.0" }
257
+ ]
258
+ },
230
259
  "name" => {
231
260
  description: "The name of the Bolt project. When this option is configured, the project is considered a "\
232
261
  "[Bolt project](experimental_features.md#bolt-projects), allowing Bolt to load content from "\
@@ -476,6 +505,7 @@ module Bolt
476
505
  inventoryfile
477
506
  log
478
507
  modulepath
508
+ modules
479
509
  name
480
510
  plans
481
511
  plugin_hooks
@@ -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,242 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/error'
4
+ require 'bolt/logger'
5
+
6
+ module Bolt
7
+ class ModuleInstaller
8
+ def initialize(outputter, pal)
9
+ @outputter = outputter
10
+ @pal = pal
11
+ @logger = Bolt::Logger.logger(self)
12
+ end
13
+
14
+ # Adds a single module to the project.
15
+ #
16
+ def add(name, modules, puppetfile_path, moduledir, config_path)
17
+ require 'bolt/puppetfile'
18
+
19
+ @outputter.print_message("Adding module #{name} to project\n\n")
20
+
21
+ # If the project configuration file already includes this module,
22
+ # exit early.
23
+ puppetfile = Bolt::Puppetfile.new(modules)
24
+ new_module = Bolt::Puppetfile::Module.from_hash('name' => name)
25
+
26
+ if puppetfile.modules.include?(new_module)
27
+ @outputter.print_action_step(
28
+ "Project configuration file #{config_path} already includes module #{new_module}. Nothing to do."
29
+ )
30
+ return true
31
+ end
32
+
33
+ # If the Puppetfile exists, make sure it's managed by Bolt.
34
+ if puppetfile_path.exist?
35
+ assert_managed_puppetfile(puppetfile, puppetfile_path)
36
+ existing = Bolt::Puppetfile.parse(puppetfile_path)
37
+ else
38
+ existing = Bolt::Puppetfile.new
39
+ end
40
+
41
+ # Create a Puppetfile object that includes the new module and its
42
+ # dependencies. We error early here so we don't add the new module to the
43
+ # project config or modify the Puppetfile.
44
+ puppetfile = add_new_module_to_puppetfile(new_module, modules, puppetfile_path)
45
+
46
+ # Display the diff between the existing Puppetfile and the new Puppetfile.
47
+ print_puppetfile_diff(existing, puppetfile)
48
+
49
+ # Add the module to the project configuration.
50
+ @outputter.print_action_step("Updating project configuration file at #{config_path}")
51
+
52
+ data = Bolt::Util.read_yaml_hash(config_path, 'project')
53
+ data['modules'] ||= []
54
+ data['modules'] << { 'name' => new_module.title }
55
+
56
+ begin
57
+ File.write(config_path, data.to_yaml)
58
+ rescue SystemCallError => e
59
+ raise Bolt::FileError.new(
60
+ "Unable to update project configuration file: #{e.message}",
61
+ config
62
+ )
63
+ end
64
+
65
+ # Write the Puppetfile.
66
+ @outputter.print_action_step("Writing Puppetfile at #{puppetfile_path}")
67
+ puppetfile.write(puppetfile_path, moduledir)
68
+
69
+ # Install the modules.
70
+ install_puppetfile(puppetfile_path, moduledir)
71
+ end
72
+
73
+ # Creates a new Puppetfile that includes the new module and its dependencies.
74
+ #
75
+ private def add_new_module_to_puppetfile(new_module, modules, path)
76
+ @outputter.print_action_step("Resolving module dependencies, this may take a moment")
77
+
78
+ # If there is an existing Puppetfile, add the new module and attempt
79
+ # to resolve. This will not update the versions of any installed modules.
80
+ if path.exist?
81
+ puppetfile = Bolt::Puppetfile.parse(path)
82
+ puppetfile.add_modules(new_module)
83
+
84
+ begin
85
+ puppetfile.resolve
86
+ return puppetfile
87
+ rescue Bolt::Error
88
+ @logger.debug "Unable to find a version of #{new_module} compatible "\
89
+ "with installed modules. Attempting to re-resolve modules "\
90
+ "from project configuration; some versions of installed "\
91
+ "modules may change."
92
+ end
93
+ end
94
+
95
+ # If there is not an existing Puppetfile, or resolving with pinned
96
+ # modules fails, resolve all of the module declarations with the new
97
+ # module.
98
+ puppetfile = Bolt::Puppetfile.new(modules)
99
+ puppetfile.add_modules(new_module)
100
+ puppetfile.resolve
101
+ puppetfile
102
+ end
103
+
104
+ # Outputs a diff of an old Puppetfile and a new Puppetfile.
105
+ #
106
+ def print_puppetfile_diff(old, new)
107
+ # Build hashes mapping the module title to the module object. This makes it
108
+ # a little easier to determine which modules have been added, removed, or
109
+ # modified.
110
+ old = old.modules.each_with_object({}) do |mod, acc|
111
+ acc[mod.title] = mod
112
+ end
113
+
114
+ new = new.modules.each_with_object({}) do |mod, acc|
115
+ acc[mod.title] = mod
116
+ end
117
+
118
+ # New modules are those present in new but not in old.
119
+ added = new.reject { |title, _mod| old.include?(title) }.values
120
+
121
+ if added.any?
122
+ diff = "Adding the following modules:\n"
123
+ added.each { |mod| diff += "#{mod.title} #{mod.version}\n" }
124
+ @outputter.print_action_step(diff)
125
+ end
126
+
127
+ # Upgraded modules are those that have a newer version in new than old.
128
+ upgraded = new.select do |title, mod|
129
+ if old.include?(title)
130
+ SemanticPuppet::Version.parse(mod.version) > SemanticPuppet::Version.parse(old[title].version)
131
+ end
132
+ end.keys
133
+
134
+ if upgraded.any?
135
+ diff = "Upgrading the following modules:\n"
136
+ upgraded.each { |title| diff += "#{title} #{old[title].version} to #{new[title].version}\n" }
137
+ @outputter.print_action_step(diff)
138
+ end
139
+
140
+ # Downgraded modules are those that have an older version in new than old.
141
+ downgraded = new.select do |title, mod|
142
+ if old.include?(title)
143
+ SemanticPuppet::Version.parse(mod.version) < SemanticPuppet::Version.parse(old[title].version)
144
+ end
145
+ end.keys
146
+
147
+ if downgraded.any?
148
+ diff = "Downgrading the following modules: \n"
149
+ downgraded.each { |title| diff += "#{title} #{old[title].version} to #{new[title].version}\n" }
150
+ @outputter.print_action_step(diff)
151
+ end
152
+
153
+ # Removed modules are those present in old but not in new.
154
+ removed = old.reject { |title, _mod| new.include?(title) }.values
155
+
156
+ if removed.any?
157
+ diff = "Removing the following modules:\n"
158
+ removed.each { |mod| diff += "#{mod.title} #{mod.version}\n" }
159
+ @outputter.print_action_step(diff)
160
+ end
161
+ end
162
+
163
+ # Installs a project's module dependencies.
164
+ #
165
+ def install(modules, path, moduledir, force: false, resolve: true)
166
+ require 'bolt/puppetfile'
167
+
168
+ @outputter.print_message("Installing project modules\n\n")
169
+
170
+ puppetfile = Bolt::Puppetfile.new(modules)
171
+
172
+ # If the Puppetfile exists, check if it includes specs for each declared
173
+ # module, erroring if there are any missing. Otherwise, resolve the
174
+ # module dependencies and write a new Puppetfile. Users can forcibly
175
+ # overwrite an existing Puppetfile with the '--force' option, or opt to
176
+ # install the Puppetfile as-is with --no-resolve.
177
+ #
178
+ # This is just if resolve is not false (nil should default to true)
179
+ if resolve != false
180
+ if path.exist? && !force
181
+ assert_managed_puppetfile(puppetfile, path)
182
+ else
183
+ @outputter.print_action_step("Resolving module dependencies, this may take a moment")
184
+ puppetfile.resolve
185
+
186
+ @outputter.print_action_step("Writing Puppetfile at #{path}")
187
+ # We get here either through 'bolt module install' which uses the
188
+ # managed modulepath (which isn't configurable) or through bolt
189
+ # project init --modules, which uses the default modulepath. This
190
+ # should be safe to assume that if `.modules/` is the moduledir the
191
+ # user is using the new workflow
192
+ if moduledir.basename.to_s == '.modules'
193
+ puppetfile.write(path, moduledir)
194
+ else
195
+ puppetfile.write(path)
196
+ end
197
+ end
198
+ end
199
+
200
+ # Install the modules.
201
+ install_puppetfile(path, moduledir)
202
+ end
203
+
204
+ # Installs the Puppetfile and generates types.
205
+ #
206
+ def install_puppetfile(path, moduledir, config = {})
207
+ require 'bolt/puppetfile/installer'
208
+
209
+ @outputter.print_action_step("Syncing modules from #{path} to #{moduledir}")
210
+ ok = Bolt::Puppetfile::Installer.new(config).install(path, moduledir)
211
+
212
+ # Automatically generate types after installing modules
213
+ @pal.generate_types
214
+
215
+ @outputter.print_puppetfile_result(ok, path, moduledir)
216
+
217
+ ok
218
+ end
219
+
220
+ # Asserts that an existing Puppetfile is managed by Bolt.
221
+ #
222
+ private def assert_managed_puppetfile(puppetfile, path)
223
+ existing_puppetfile = Bolt::Puppetfile.parse(path)
224
+
225
+ unless existing_puppetfile.modules.superset? puppetfile.modules
226
+ missing_modules = puppetfile.modules - existing_puppetfile.modules
227
+
228
+ message = <<~MESSAGE.chomp
229
+ Puppetfile #{path} is missing specifications for the following
230
+ module declarations:
231
+
232
+ #{missing_modules.map(&:to_hash).to_yaml.lines.drop(1).join.chomp}
233
+
234
+ This may not be a Puppetfile managed by Bolt. To forcibly overwrite the
235
+ Puppetfile, run 'bolt module install --force'.
236
+ MESSAGE
237
+
238
+ raise Bolt::Error.new(message, 'bolt/missing-module-specs')
239
+ end
240
+ end
241
+ end
242
+ end