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
@@ -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
@@ -5,18 +5,16 @@ require 'bolt/executor'
5
5
  require 'bolt/error'
6
6
  require 'bolt/plan_result'
7
7
  require 'bolt/util'
8
+ require 'bolt/config/modulepath'
8
9
  require 'etc'
9
10
 
10
11
  module Bolt
11
12
  class PAL
12
- BOLTLIB_PATH = File.expand_path('../../bolt-modules', __dir__)
13
- MODULES_PATH = File.expand_path('../../modules', __dir__)
14
-
15
13
  # PALError is used to convert errors from executing puppet code into
16
14
  # Bolt::Errors
17
15
  class PALError < Bolt::Error
18
16
  def self.from_preformatted_error(err)
19
- if err.cause&.is_a? Bolt::Error
17
+ if err.cause.is_a? Bolt::Error
20
18
  err.cause
21
19
  else
22
20
  from_error(err)
@@ -29,15 +27,12 @@ module Bolt
29
27
  message = err.cause ? err.cause.message : err.message
30
28
 
31
29
  # 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
30
+ details = {}
31
+ details[:file] = err.file if defined?(err.file)
32
+ details[:line] = err.line if defined?(err.line)
33
+ details[:column] = err.pos if defined?(err.pos)
39
34
 
40
- e = new(message, details)
35
+ e = new(message, details.compact)
41
36
 
42
37
  e.set_backtrace(err.backtrace)
43
38
  e
@@ -48,16 +43,16 @@ module Bolt
48
43
  end
49
44
  end
50
45
 
51
- attr_reader :modulepath, :user_modulepath
52
-
53
46
  def initialize(modulepath, hiera_config, resource_types, max_compiles = Etc.nprocessors,
54
47
  trusted_external = nil, apply_settings = {}, project = nil)
48
+ unless modulepath.is_a?(Bolt::Config::Modulepath)
49
+ msg = "Type error in PAL: modulepath must be a Bolt::Config::Modulepath"
50
+ raise Bolt::Error.new(msg, "bolt/execution-error")
51
+ end
55
52
  # Nothing works without initialized this global state. Reinitializing
56
53
  # is safe and in practice only happens in tests
57
54
  self.class.load_puppet
58
-
59
- @user_modulepath = modulepath
60
- @modulepath = [BOLTLIB_PATH, *modulepath, MODULES_PATH]
55
+ @modulepath = modulepath
61
56
  @hiera_config = hiera_config
62
57
  @trusted_external = trusted_external
63
58
  @apply_settings = apply_settings
@@ -66,13 +61,21 @@ module Bolt
66
61
  @project = project
67
62
 
68
63
  @logger = Bolt::Logger.logger(self)
69
- if modulepath && !modulepath.empty?
70
- @logger.debug("Loading modules from #{@modulepath.join(File::PATH_SEPARATOR)}")
64
+ unless user_modulepath.empty?
65
+ @logger.debug("Loading modules from #{full_modulepath.join(File::PATH_SEPARATOR)}")
71
66
  end
72
67
 
73
68
  @loaded = false
74
69
  end
75
70
 
71
+ def full_modulepath
72
+ @modulepath.full_modulepath
73
+ end
74
+
75
+ def user_modulepath
76
+ @modulepath.user_modulepath
77
+ end
78
+
76
79
  # Puppet logging is global so this is class method to avoid confusion
77
80
  def self.configure_logging
78
81
  Puppet::Util::Log.destinations.clear
@@ -160,7 +163,7 @@ module Bolt
160
163
  def in_bolt_compiler
161
164
  # TODO: If we always call this inside a bolt_executor we can remove this here
162
165
  setup
163
- r = Puppet::Pal.in_tmp_environment('bolt', modulepath: @modulepath, facts: {}) do |pal|
166
+ r = Puppet::Pal.in_tmp_environment('bolt', modulepath: full_modulepath, facts: {}) do |pal|
164
167
  # Only load the project if it a) exists, b) has a name it can be loaded with
165
168
  Puppet.override(bolt_project: @project,
166
169
  yaml_plan_instantiator: Bolt::PAL::YamlPlan::Loader) do
@@ -215,11 +218,11 @@ module Bolt
215
218
  apply_executor: applicator || Applicator.new(
216
219
  inventory,
217
220
  executor,
218
- @modulepath,
221
+ full_modulepath,
219
222
  # Skip syncing built-in plugins, since we vendor some Puppet 6
220
223
  # versions of "core" types, which are already present on the agent,
221
224
  # but may cause issues on Puppet 5 agents.
222
- @user_modulepath,
225
+ user_modulepath,
223
226
  @project,
224
227
  pdb_client,
225
228
  @hiera_config,
@@ -441,13 +444,14 @@ module Bolt
441
444
  # Returns a mapping of all modules available to the Bolt compiler
442
445
  #
443
446
  # @return [Hash{String => Array<Hash{Symbol => String,nil}>}]
444
- # A hash that associates each directory on the module path with an array
447
+ # A hash that associates each directory on the modulepath with an array
445
448
  # containing a hash of information for each module in that directory.
446
449
  # The information hash provides the name, version, and a string
447
450
  # indicating whether the module belongs to an internal module group.
448
451
  def list_modules
449
- internal_module_groups = { BOLTLIB_PATH => 'Plan Language Modules',
450
- MODULES_PATH => 'Packaged Modules' }
452
+ internal_module_groups = { Bolt::Config::Modulepath::BOLTLIB_PATH => 'Plan Language Modules',
453
+ Bolt::Config::Modulepath::MODULES_PATH => 'Packaged Modules',
454
+ @project.managed_moduledir.to_s => 'Project Dependencies' }
451
455
 
452
456
  in_bolt_compiler do
453
457
  # NOTE: Can replace map+to_h with transform_values when Ruby 2.4
@@ -168,7 +168,7 @@ module Bolt
168
168
  end
169
169
 
170
170
  def modules
171
- @modules ||= Bolt::Module.discover(@pal.modulepath)
171
+ @modules ||= Bolt::Module.discover(@pal.full_modulepath)
172
172
  end
173
173
 
174
174
  def add_plugin(plugin)
@@ -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
@@ -174,7 +190,7 @@ module Bolt
174
190
  Invalid project name '#{name}' in bolt-project.yaml; project name must begin with a lowercase letter
175
191
  and can include lowercase letters, numbers, and underscores.
176
192
  ERROR_STRING
177
- elsif Dir.children(Bolt::PAL::BOLTLIB_PATH).include?(name)
193
+ elsif Dir.children(Bolt::Config::Modulepath::BOLTLIB_PATH).include?(name)
178
194
  raise Bolt::ValidationError, "The project '#{name}' will not be loaded. The project name conflicts "\
179
195
  "with a built-in Bolt module of the same name."
180
196
  end
@@ -194,15 +210,9 @@ module Bolt
194
210
  raise Bolt::ValidationError, "'modules' in bolt-project.yaml must be an array"
195
211
  end
196
212
 
197
- @data['modules'].each do |mod|
198
- next if mod.is_a?(Hash)
199
- raise Bolt::ValidationError, "Module declaration #{mod.inspect} must be a hash"
200
- end
201
-
202
- unknown_keys = data['modules'].flat_map(&:keys).uniq - ['name']
203
- if unknown_keys.any?
204
- @logs << { warn: "Module declarations in bolt-project.yaml only support a name key. Ignoring "\
205
- "unsupported keys: #{unknown_keys.join(', ')}." }
213
+ @data['modules'].each do |spec|
214
+ next if spec.is_a?(Hash) || spec.is_a?(String)
215
+ raise Bolt::ValidationError, "Module specification #{spec.inspect} must be a hash or string"
206
216
  end
207
217
  end
208
218
  end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/project_migrator/config'
4
+ require 'bolt/project_migrator/inventory'
5
+ require 'bolt/project_migrator/modules'
6
+
7
+ module Bolt
8
+ class ProjectMigrator
9
+ def initialize(config, outputter)
10
+ @config = config
11
+ @outputter = outputter
12
+ end
13
+
14
+ def migrate
15
+ unless $stdin.tty?
16
+ raise Bolt::Error.new(
17
+ "stdin is not a tty, unable to migrate project",
18
+ 'bolt/stdin-not-a-tty-error'
19
+ )
20
+ end
21
+
22
+ @outputter.print_message("Migrating project #{@config.project.path}\n\n")
23
+
24
+ @outputter.print_action_step(
25
+ "Migrating a Bolt project may make irreversible changes to the project's "\
26
+ "configuration and inventory files. Before continuing, make sure the "\
27
+ "project has a backup or uses a version control system."
28
+ )
29
+
30
+ return 0 unless Bolt::Util.prompt_yes_no("Continue with project migration?", @outputter)
31
+
32
+ @outputter.print_message('')
33
+
34
+ ok = migrate_inventory && migrate_config && migrate_modules
35
+
36
+ if ok
37
+ @outputter.print_message("Project successfully migrated")
38
+ else
39
+ @outputter.print_error("Project could not be migrated completely")
40
+ end
41
+
42
+ ok ? 0 : 1
43
+ end
44
+
45
+ # Migrates the project-level configuration file to the latest version.
46
+ #
47
+ private def migrate_config
48
+ migrator = Bolt::ProjectMigrator::Config.new(@outputter)
49
+
50
+ migrator.migrate(
51
+ @config.project.config_file,
52
+ @config.project.project_file,
53
+ @config.inventoryfile || @config.project.inventory_file,
54
+ @config.project.backup_dir
55
+ )
56
+ end
57
+
58
+ # Migrates the inventory file to the latest version.
59
+ #
60
+ private def migrate_inventory
61
+ migrator = Bolt::ProjectMigrator::Inventory.new(@outputter)
62
+
63
+ migrator.migrate(
64
+ @config.inventoryfile || @config.project.inventory_file,
65
+ @config.project.backup_dir
66
+ )
67
+ end
68
+
69
+ # Migrates the project's modules to use current best practices.
70
+ #
71
+ private def migrate_modules
72
+ migrator = Bolt::ProjectMigrator::Modules.new(@outputter)
73
+
74
+ migrator.migrate(
75
+ @config.project,
76
+ @config.modulepath
77
+ )
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'bolt/error'
5
+
6
+ module Bolt
7
+ class ProjectMigrator
8
+ class Base
9
+ def initialize(outputter)
10
+ @outputter = outputter
11
+ end
12
+
13
+ protected def backup_file(origin_path, backup_dir)
14
+ unless File.exist?(origin_path)
15
+ @outputter.print_action_step(
16
+ "Could not find file #{origin_path}, skipping backup."
17
+ )
18
+ return
19
+ end
20
+
21
+ date = Time.new.strftime("%Y%m%d_%H%M%S%L")
22
+ FileUtils.mkdir_p(backup_dir)
23
+
24
+ filename = File.basename(origin_path)
25
+ backup_path = File.join(backup_dir, "#{filename}.#{date}.bak")
26
+
27
+ @outputter.print_action_step(
28
+ "Backing up #{filename} from #{origin_path} to #{backup_path}"
29
+ )
30
+
31
+ begin
32
+ FileUtils.cp(origin_path, backup_path)
33
+ rescue StandardError => e
34
+ raise Bolt::FileError.new("#{e.message}; unable to create backup of #{filename}.", origin_path)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/project_migrator/base'
4
+
5
+ module Bolt
6
+ class ProjectMigrator
7
+ class Config < Base
8
+ def migrate(config_file, project_file, inventory_file, backup_dir)
9
+ bolt_yaml_to_bolt_project(config_file, project_file, inventory_file, backup_dir)
10
+ end
11
+
12
+ private def bolt_yaml_to_bolt_project(config_file, project_file, inventory_file, backup_dir)
13
+ if File.exist?(project_file)
14
+ return true
15
+ end
16
+
17
+ unless File.exist?(config_file)
18
+ return true
19
+ end
20
+
21
+ @outputter.print_message "Migrating project configuration\n\n"
22
+
23
+ config_data = Bolt::Util.read_optional_yaml_hash(config_file, 'config')
24
+ transport_data, project_data = config_data.partition do |k, _|
25
+ Bolt::Config::INVENTORY_OPTIONS.keys.include?(k)
26
+ end.map(&:to_h)
27
+
28
+ if transport_data.any?
29
+ if File.exist?(inventory_file)
30
+ inventory_data = Bolt::Util.read_yaml_hash(inventory_file, 'inventory')
31
+ merged = Bolt::Util.deep_merge(transport_data, inventory_data['config'] || {})
32
+ inventory_data['config'] = merged
33
+ backup_file(inventory_file, backup_dir)
34
+ else
35
+ FileUtils.touch(inventory_file)
36
+ inventory_data = { 'config' => transport_data }
37
+ end
38
+
39
+ backup_file(config_file, backup_dir)
40
+
41
+ begin
42
+ @outputter.print_action_step(
43
+ "Moving transportation configuration options '#{transport_data.keys.join(', ')}' "\
44
+ "from bolt.yaml to inventory.yaml"
45
+ )
46
+
47
+ File.write(inventory_file, inventory_data.to_yaml)
48
+ File.write(config_file, project_data.to_yaml)
49
+ rescue StandardError => e
50
+ raise Bolt::FileError.new("#{e.message}; unable to write inventory.", inventory_file)
51
+ end
52
+ end
53
+
54
+ @outputter.print_action_step("Renaming bolt.yaml to bolt-project.yaml")
55
+ FileUtils.mv(config_file, project_file)
56
+
57
+ @outputter.print_action_step(
58
+ "Successfully migrated config. Please add a 'name' key to bolt-project.yaml "\
59
+ "to use project-level tasks and plans. Learn more about projects by running "\
60
+ "'bolt guide project'."
61
+ )
62
+
63
+ true
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/project_migrator/base'
4
+
5
+ module Bolt
6
+ class ProjectMigrator
7
+ class Inventory < Base
8
+ def migrate(inventory_file, backup_dir)
9
+ inventory_1_to_2(inventory_file, backup_dir)
10
+ end
11
+
12
+ # Migrates an inventory v1 file to inventory v2.
13
+ #
14
+ private def inventory_1_to_2(inventory_file, backup_dir)
15
+ unless File.exist?(inventory_file)
16
+ return true
17
+ end
18
+
19
+ data = Bolt::Util.read_yaml_hash(inventory_file, 'inventory')
20
+ data.delete('version') if data['version'] != 2
21
+ migrated = migrate_group(data)
22
+
23
+ return true unless migrated
24
+
25
+ @outputter.print_message "Migrating inventory\n\n"
26
+
27
+ backup_file(inventory_file, backup_dir)
28
+
29
+ begin
30
+ File.write(inventory_file, data.to_yaml)
31
+ @outputter.print_action_step(
32
+ "Successfully migrated Bolt inventory to the latest version."
33
+ )
34
+ true
35
+ rescue StandardError => e
36
+ raise Bolt::FileError.new(
37
+ "Unable to write to #{inventory_file}: #{e.message}. See "\
38
+ "http://pup.pt/bolt-inventory to manually update.",
39
+ inventory_file
40
+ )
41
+ end
42
+ end
43
+
44
+ # Walks an inventory hash and replaces all 'nodes' keys with 'targets'
45
+ # keys and all 'name' keys nested in a 'targets' hash with 'uri' keys.
46
+ # Data is modified in place.
47
+ #
48
+ private def migrate_group(group)
49
+ migrated = false
50
+ if group.key?('nodes')
51
+ migrated = true
52
+ targets = group['nodes'].map do |target|
53
+ target['uri'] = target.delete('name') if target.is_a?(Hash)
54
+ target
55
+ end
56
+ group.delete('nodes')
57
+ group['targets'] = targets
58
+ end
59
+ (group['groups'] || []).each do |subgroup|
60
+ migrated_group = migrate_group(subgroup)
61
+ migrated ||= migrated_group
62
+ end
63
+ migrated
64
+ end
65
+ end
66
+ end
67
+ end