bolt 2.29.0 → 2.30.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.

@@ -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)
@@ -233,7 +233,7 @@ module Bolt
233
233
  "install` command.",
234
234
  type: Array,
235
235
  items: {
236
- type: Hash,
236
+ type: [Hash, String],
237
237
  required: ["name"],
238
238
  properties: {
239
239
  "name" => {
@@ -250,6 +250,7 @@ module Bolt
250
250
  _plugin: false,
251
251
  _example: [
252
252
  { "name" => "puppetlabs-mysql" },
253
+ "puppetlabs-facts",
253
254
  { "name" => "puppetlabs-apache", "version_requirement" => "5.5.0" },
254
255
  { "name" => "puppetlabs-puppetdb", "version_requirement" => "7.x" },
255
256
  { "name" => "puppetlabs-firewall", "version_requirement" => ">= 1.0.0 < 3.0.0" }
@@ -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)
@@ -0,0 +1,172 @@
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
+ # If the project configuration file already includes this module,
20
+ # exit early.
21
+ puppetfile = Bolt::Puppetfile.new(modules)
22
+ new_module = Bolt::Puppetfile::Module.from_hash('name' => name)
23
+
24
+ if puppetfile.modules.include?(new_module)
25
+ @outputter.print_message "Project configuration file #{config_path} already "\
26
+ "includes module #{new_module}. Nothing to do."
27
+ return true
28
+ end
29
+
30
+ # If the Puppetfile exists, make sure it's managed by Bolt.
31
+ if puppetfile_path.exist?
32
+ assert_managed_puppetfile(puppetfile, puppetfile_path)
33
+ end
34
+
35
+ # Create a Puppetfile object that includes the new module and its
36
+ # dependencies. We error early here so we don't add the new module to the
37
+ # project config or modify the Puppetfile.
38
+ puppetfile = add_new_module_to_puppetfile(new_module, modules, puppetfile_path)
39
+
40
+ # Add the module to the project configuration.
41
+ @outputter.print_message "Updating project configuration file at #{config_path}"
42
+
43
+ data = Bolt::Util.read_yaml_hash(config_path, 'project')
44
+ data['modules'] ||= []
45
+ data['modules'] << { 'name' => new_module.title }
46
+
47
+ begin
48
+ File.write(config_path, data.to_yaml)
49
+ rescue SystemCallError => e
50
+ raise Bolt::FileError.new(
51
+ "Unable to update project configuration file: #{e.message}",
52
+ config
53
+ )
54
+ end
55
+
56
+ # Write the Puppetfile.
57
+ @outputter.print_message "Writing Puppetfile at #{puppetfile_path}"
58
+ puppetfile.write(puppetfile_path, moduledir)
59
+
60
+ # Install the modules.
61
+ install_puppetfile(puppetfile_path, moduledir)
62
+ end
63
+
64
+ # Creates a new Puppetfile that includes the new module and its dependencies.
65
+ #
66
+ private def add_new_module_to_puppetfile(new_module, modules, path)
67
+ @outputter.print_message "Resolving module dependencies, this may take a moment"
68
+
69
+ # If there is an existing Puppetfile, add the new module and attempt
70
+ # to resolve. This will not update the versions of any installed modules.
71
+ if path.exist?
72
+ puppetfile = Bolt::Puppetfile.parse(path)
73
+ puppetfile.add_modules(new_module)
74
+
75
+ begin
76
+ puppetfile.resolve
77
+ return puppetfile
78
+ rescue Bolt::Error
79
+ @logger.debug "Unable to find a version of #{new_module} compatible "\
80
+ "with installed modules. Attempting to re-resolve modules "\
81
+ "from project configuration; some versions of installed "\
82
+ "modules may change."
83
+ end
84
+ end
85
+
86
+ # If there is not an existing Puppetfile, or resolving with pinned
87
+ # modules fails, resolve all of the module declarations with the new
88
+ # module.
89
+ puppetfile = Bolt::Puppetfile.new(modules)
90
+ puppetfile.add_modules(new_module)
91
+ puppetfile.resolve
92
+ puppetfile
93
+ end
94
+
95
+ # Installs a project's module dependencies.
96
+ #
97
+ def install(modules, path, moduledir, force: false, resolve: true)
98
+ require 'bolt/puppetfile'
99
+
100
+ puppetfile = Bolt::Puppetfile.new(modules)
101
+
102
+ # If the Puppetfile exists, check if it includes specs for each declared
103
+ # module, erroring if there are any missing. Otherwise, resolve the
104
+ # module dependencies and write a new Puppetfile. Users can forcibly
105
+ # overwrite an existing Puppetfile with the '--force' option, or opt to
106
+ # install the Puppetfile as-is with --no-resolve.
107
+ #
108
+ # This is just if resolve is not false (nil should default to true)
109
+ if resolve != false
110
+ if path.exist? && !force
111
+ assert_managed_puppetfile(puppetfile, path)
112
+ else
113
+ @outputter.print_message "Resolving module dependencies, this may take a moment"
114
+ puppetfile.resolve
115
+
116
+ @outputter.print_message "Writing Puppetfile at #{path}"
117
+ # We get here either through 'bolt module install' which uses the
118
+ # managed modulepath (which isn't configurable) or through bolt
119
+ # project init --modules, which uses the default modulepath. This
120
+ # should be safe to assume that if `.modules/` is the moduledir the
121
+ # user is using the new workflow
122
+ if moduledir.basename == '.modules'
123
+ puppetfile.write(path, moduledir)
124
+ else
125
+ puppetfile.write(path)
126
+ end
127
+ end
128
+ end
129
+
130
+ # Install the modules.
131
+ install_puppetfile(path, moduledir)
132
+ end
133
+
134
+ # Installs the Puppetfile and generates types.
135
+ #
136
+ def install_puppetfile(path, moduledir, config = {})
137
+ require 'bolt/puppetfile/installer'
138
+
139
+ @outputter.print_message "Syncing modules from #{path} to #{moduledir}"
140
+ ok = Bolt::Puppetfile::Installer.new(config).install(path, moduledir)
141
+
142
+ # Automatically generate types after installing modules
143
+ @pal.generate_types
144
+
145
+ @outputter.print_puppetfile_result(ok, path, moduledir)
146
+
147
+ ok
148
+ end
149
+
150
+ # Asserts that an existing Puppetfile is managed by Bolt.
151
+ #
152
+ private def assert_managed_puppetfile(puppetfile, path)
153
+ existing_puppetfile = Bolt::Puppetfile.parse(path)
154
+
155
+ unless existing_puppetfile.modules.superset? puppetfile.modules
156
+ missing_modules = puppetfile.modules - existing_puppetfile.modules
157
+
158
+ message = <<~MESSAGE.chomp
159
+ Puppetfile #{path} is missing specifications for the following
160
+ module declarations:
161
+
162
+ #{missing_modules.map(&:to_hash).to_yaml.lines.drop(1).join.chomp}
163
+
164
+ This may not be a Puppetfile managed by Bolt. To forcibly overwrite the
165
+ Puppetfile, run 'bolt module install --force'.
166
+ MESSAGE
167
+
168
+ raise Bolt::Error.new(message, 'bolt/missing-module-specs')
169
+ end
170
+ end
171
+ end
172
+ end
@@ -35,6 +35,10 @@ module Bolt
35
35
  raise NotImplementedError, "print_message() must be implemented by the outputter class"
36
36
  end
37
37
 
38
+ def print_error
39
+ raise NotImplementedError, "print_error() must be implemented by the outputter class"
40
+ end
41
+
38
42
  def stringify(message)
39
43
  formatted = format_message(message)
40
44
  if formatted.is_a?(Hash) || formatted.is_a?(Array)
@@ -5,9 +5,12 @@ require 'bolt/pal'
5
5
  module Bolt
6
6
  class Outputter
7
7
  class Human < Bolt::Outputter
8
- COLORS = { red: "31",
9
- green: "32",
10
- yellow: "33" }.freeze
8
+ COLORS = {
9
+ red: "31",
10
+ green: "32",
11
+ yellow: "33",
12
+ cyan: "36"
13
+ }.freeze
11
14
 
12
15
  def print_head; end
13
16
 
@@ -31,6 +34,10 @@ module Bolt
31
34
  string.sub(/\s\z/, '')
32
35
  end
33
36
 
37
+ def wrap(string, width = 80)
38
+ string.gsub(/(.{1,#{width}})(\s+|\Z)/, "\\1\n")
39
+ end
40
+
34
41
  def handle_event(event)
35
42
  case event[:type]
36
43
  when :enable_default_output
@@ -48,9 +55,9 @@ module Bolt
48
55
  when :node_result
49
56
  print_result(event[:result]) if @verbose
50
57
  when :step_start
51
- print_step_start(event) if plan_logging?
58
+ print_step_start(**event) if plan_logging?
52
59
  when :step_finish
53
- print_step_finish(event) if plan_logging?
60
+ print_step_finish(**event) if plan_logging?
54
61
  when :plan_start
55
62
  print_plan_start(event)
56
63
  when :plan_finish
@@ -188,7 +195,7 @@ module Bolt
188
195
  @stream.puts total_msg
189
196
  end
190
197
 
191
- def print_table(results)
198
+ def print_table(results, padding_left = 0, padding_right = 3)
192
199
  # lazy-load expensive gem code
193
200
  require 'terminal-table'
194
201
 
@@ -198,8 +205,8 @@ module Bolt
198
205
  border_x: '',
199
206
  border_y: '',
200
207
  border_i: '',
201
- padding_left: 0,
202
- padding_right: 3,
208
+ padding_left: padding_left,
209
+ padding_right: padding_right,
203
210
  border_top: false,
204
211
  border_bottom: false
205
212
  }
@@ -299,9 +306,9 @@ module Bolt
299
306
  def print_module_list(module_list)
300
307
  module_list.each do |path, modules|
301
308
  if (mod = modules.find { |m| m[:internal_module_group] })
302
- @stream.puts(mod[:internal_module_group])
309
+ @stream.puts(colorize(:cyan, mod[:internal_module_group]))
303
310
  else
304
- @stream.puts(path)
311
+ @stream.puts(colorize(:cyan, path))
305
312
  end
306
313
 
307
314
  if modules.empty?
@@ -317,7 +324,7 @@ module Bolt
317
324
  [m[:name], version]
318
325
  end
319
326
 
320
- print_table(module_info)
327
+ print_table(module_info, 2, 1)
321
328
  end
322
329
 
323
330
  @stream.write("\n")
@@ -394,6 +401,41 @@ module Bolt
394
401
  @stream.puts(message)
395
402
  end
396
403
 
404
+ def print_error(message)
405
+ @stream.puts(colorize(:red, message))
406
+ end
407
+
408
+ def print_prompt(prompt)
409
+ @stream.print(colorize(:cyan, indent(4, prompt)))
410
+ end
411
+
412
+ def print_prompt_error(message)
413
+ @stream.puts(colorize(:red, indent(4, message)))
414
+ end
415
+
416
+ def print_migrate_step(step)
417
+ first, *remaining = wrap(step, 76).lines
418
+
419
+ first = indent(2, "→ #{first}")
420
+ remaining = remaining.map { |line| indent(4, line) }
421
+ step = [first, *remaining, "\n"].join
422
+
423
+ @stream.puts(step)
424
+ end
425
+
426
+ def print_migrate_error(error)
427
+ # Running everything through 'wrap' messes with newlines. Separating
428
+ # into lines and wrapping each individually ensures separate errors are
429
+ # distinguishable.
430
+ first, *remaining = error.lines
431
+ first = colorize(:red, indent(2, "→ #{wrap(first, 76)}"))
432
+ wrapped = remaining.map { |l| wrap(l) }
433
+ to_print = wrapped.map { |line| colorize(:red, indent(4, line)) }
434
+ step = [first, *to_print, "\n"].join
435
+
436
+ @stream.puts(step)
437
+ end
438
+
397
439
  def duration_to_string(duration)
398
440
  hrs = (duration / 3600).floor
399
441
  mins = ((duration % 3600) / 60).floor
@@ -97,7 +97,7 @@ module Bolt
97
97
  def print_puppetfile_result(success, puppetfile, moduledir)
98
98
  @stream.puts({ "success": success,
99
99
  "puppetfile": puppetfile,
100
- "moduledir": moduledir }.to_json)
100
+ "moduledir": moduledir.to_s }.to_json)
101
101
  end
102
102
 
103
103
  def print_targets(targets)
@@ -135,6 +135,12 @@ module Bolt
135
135
  def print_message(message)
136
136
  $stderr.puts(message)
137
137
  end
138
+ alias print_error print_message
139
+
140
+ def print_migrate_step(step)
141
+ $stderr.puts(step)
142
+ end
143
+ alias print_migrate_error print_migrate_step
138
144
  end
139
145
  end
140
146
  end
@@ -13,9 +13,9 @@ module Bolt
13
13
  def handle_event(event)
14
14
  case event[:type]
15
15
  when :step_start
16
- log_step_start(event)
16
+ log_step_start(**event)
17
17
  when :step_finish
18
- log_step_finish(event)
18
+ log_step_finish(**event)
19
19
  when :plan_start
20
20
  log_plan_start(event)
21
21
  when :plan_finish
@@ -29,15 +29,12 @@ module Bolt
29
29
  message = err.cause ? err.cause.message : err.message
30
30
 
31
31
  # Provide the location of an error if it came from a plan
32
- details = if defined?(err.file) && err.file
33
- { file: err.file,
34
- line: err.line,
35
- column: err.pos }.compact
36
- else
37
- {}
38
- end
32
+ details = {}
33
+ details[:file] = err.file if defined?(err.file)
34
+ details[:line] = err.line if defined?(err.line)
35
+ details[:column] = err.pos if defined?(err.pos)
39
36
 
40
- e = new(message, details)
37
+ e = new(message, details.compact)
41
38
 
42
39
  e.set_backtrace(err.backtrace)
43
40
  e
@@ -441,13 +438,14 @@ module Bolt
441
438
  # Returns a mapping of all modules available to the Bolt compiler
442
439
  #
443
440
  # @return [Hash{String => Array<Hash{Symbol => String,nil}>}]
444
- # A hash that associates each directory on the module path with an array
441
+ # A hash that associates each directory on the modulepath with an array
445
442
  # containing a hash of information for each module in that directory.
446
443
  # The information hash provides the name, version, and a string
447
444
  # indicating whether the module belongs to an internal module group.
448
445
  def list_modules
449
446
  internal_module_groups = { BOLTLIB_PATH => 'Plan Language Modules',
450
- MODULES_PATH => 'Packaged Modules' }
447
+ MODULES_PATH => 'Packaged Modules',
448
+ @project.managed_moduledir.to_s => 'Project Dependencies' }
451
449
 
452
450
  in_bolt_compiler do
453
451
  # NOTE: Can replace map+to_h with transform_values when Ruby 2.4
@@ -16,7 +16,7 @@ module Bolt
16
16
  mod = modules[name]
17
17
  if mod&.plugin?
18
18
  opts[:mod] = mod
19
- plugin = Bolt::Plugin::Module.new(opts)
19
+ plugin = Bolt::Plugin::Module.new(**opts)
20
20
  plugin.setup
21
21
  plugin
22
22
  else
@@ -16,9 +16,10 @@ module Bolt
16
16
  "These tasks are included in `bolt task show` output"
17
17
  }.freeze
18
18
 
19
- attr_reader :path, :data, :config_file, :inventory_file, :modulepath, :hiera_config,
19
+ attr_reader :path, :data, :config_file, :inventory_file, :hiera_config,
20
20
  :puppetfile, :rerunfile, :type, :resource_types, :logs, :project_file,
21
- :deprecations, :downloads, :plans_path
21
+ :deprecations, :downloads, :plans_path, :modulepath, :managed_moduledir,
22
+ :backup_dir
22
23
 
23
24
  def self.default_project(logs = [])
24
25
  create_project(File.expand_path(File.join('~', '.puppetlabs', 'bolt')), 'user', logs)
@@ -94,15 +95,16 @@ module Bolt
94
95
  @deprecations << { type: 'Using bolt.yaml for project configuration', msg: msg }
95
96
  end
96
97
 
97
- @inventory_file = @path + 'inventory.yaml'
98
- @modulepath = [(@path + 'modules').to_s, (@path + 'site-modules').to_s, (@path + 'site').to_s]
99
- @hiera_config = @path + 'hiera.yaml'
100
- @puppetfile = @path + 'Puppetfile'
101
- @rerunfile = @path + '.rerun.json'
102
- @resource_types = @path + '.resource_types'
103
- @type = type
104
- @downloads = @path + 'downloads'
105
- @plans_path = @path + 'plans'
98
+ @inventory_file = @path + 'inventory.yaml'
99
+ @hiera_config = @path + 'hiera.yaml'
100
+ @puppetfile = @path + 'Puppetfile'
101
+ @rerunfile = @path + '.rerun.json'
102
+ @resource_types = @path + '.resource_types'
103
+ @type = type
104
+ @downloads = @path + 'downloads'
105
+ @plans_path = @path + 'plans'
106
+ @managed_moduledir = @path + '.modules'
107
+ @backup_dir = @path + '.bolt-bak'
106
108
 
107
109
  tc = Bolt::Config::INVENTORY_OPTIONS.keys & raw_data.keys
108
110
  if tc.any?
@@ -112,6 +114,14 @@ module Bolt
112
114
 
113
115
  @data = raw_data.reject { |k, _| Bolt::Config::INVENTORY_OPTIONS.include?(k) }
114
116
 
117
+ # If the 'modules' key is present in the project configuration file,
118
+ # use the new, shorter modulepath.
119
+ @modulepath = if @data.key?('modules')
120
+ [(@path + 'modules').to_s]
121
+ else
122
+ [(@path + 'modules').to_s, (@path + 'site-modules').to_s, (@path + 'site').to_s]
123
+ end
124
+
115
125
  # Once bolt.yaml deprecation is removed, this attribute should be removed
116
126
  # and replaced with .project_file in lib/bolt/config.rb
117
127
  @config_file = if (Bolt::Config::BOLT_OPTIONS & @data.keys).any?
@@ -164,7 +174,13 @@ module Bolt
164
174
  end
165
175
 
166
176
  def modules
167
- @data['modules']
177
+ @modules ||= @data['modules']&.map do |mod|
178
+ if mod.is_a?(String)
179
+ { 'name' => mod }
180
+ else
181
+ mod
182
+ end
183
+ end
168
184
  end
169
185
 
170
186
  def validate
@@ -195,11 +211,11 @@ module Bolt
195
211
  end
196
212
 
197
213
  @data['modules'].each do |mod|
198
- next if mod.is_a?(Hash)
199
- raise Bolt::ValidationError, "Module declaration #{mod.inspect} must be a hash"
214
+ next if (mod.is_a?(Hash) && mod.key?('name')) || mod.is_a?(String)
215
+ raise Bolt::ValidationError, "Module declaration #{mod.inspect} must be a hash with a name key"
200
216
  end
201
217
 
202
- unknown_keys = data['modules'].flat_map(&:keys).uniq - %w[name version_requirement]
218
+ unknown_keys = modules.flat_map(&:keys).uniq - %w[name version_requirement]
203
219
  if unknown_keys.any?
204
220
  @logs << { warn: "Ignoring unknown keys in module declarations: #{unknown_keys.join(', ')}." }
205
221
  end