bolt 2.36.0 → 2.42.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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +8 -8
  3. data/lib/bolt/bolt_option_parser.rb +7 -3
  4. data/lib/bolt/cli.rb +67 -23
  5. data/lib/bolt/config.rb +70 -45
  6. data/lib/bolt/config/options.rb +104 -79
  7. data/lib/bolt/config/transport/base.rb +2 -2
  8. data/lib/bolt/config/transport/local.rb +1 -0
  9. data/lib/bolt/config/transport/options.rb +11 -68
  10. data/lib/bolt/config/transport/ssh.rb +0 -5
  11. data/lib/bolt/inventory.rb +26 -0
  12. data/lib/bolt/inventory/group.rb +29 -9
  13. data/lib/bolt/inventory/inventory.rb +1 -1
  14. data/lib/bolt/inventory/options.rb +130 -0
  15. data/lib/bolt/inventory/target.rb +10 -11
  16. data/lib/bolt/module.rb +10 -2
  17. data/lib/bolt/module_installer.rb +21 -13
  18. data/lib/bolt/module_installer/resolver.rb +13 -5
  19. data/lib/bolt/outputter.rb +19 -5
  20. data/lib/bolt/outputter/human.rb +20 -1
  21. data/lib/bolt/outputter/json.rb +1 -1
  22. data/lib/bolt/outputter/logger.rb +1 -1
  23. data/lib/bolt/outputter/rainbow.rb +12 -1
  24. data/lib/bolt/pal/yaml_plan/transpiler.rb +5 -1
  25. data/lib/bolt/plugin.rb +42 -6
  26. data/lib/bolt/plugin/cache.rb +76 -0
  27. data/lib/bolt/plugin/module.rb +4 -4
  28. data/lib/bolt/plugin/puppetdb.rb +1 -1
  29. data/lib/bolt/project.rb +38 -13
  30. data/lib/bolt/project_manager.rb +2 -0
  31. data/lib/bolt/project_manager/config_migrator.rb +9 -1
  32. data/lib/bolt/project_manager/module_migrator.rb +2 -0
  33. data/lib/bolt/puppetdb/client.rb +8 -0
  34. data/lib/bolt/rerun.rb +1 -5
  35. data/lib/bolt/shell/bash.rb +7 -1
  36. data/lib/bolt/shell/powershell.rb +21 -3
  37. data/lib/bolt/target.rb +4 -0
  38. data/lib/bolt/transport/local.rb +13 -0
  39. data/lib/bolt/util.rb +22 -0
  40. data/lib/bolt/validator.rb +227 -0
  41. data/lib/bolt/version.rb +1 -1
  42. data/lib/bolt_server/plugin.rb +13 -0
  43. data/lib/bolt_server/plugin/puppet_connect_data.rb +37 -0
  44. data/lib/bolt_server/schemas/connect-data.json +22 -0
  45. data/lib/bolt_server/schemas/partials/task.json +1 -1
  46. data/lib/bolt_server/transport_app.rb +64 -36
  47. metadata +24 -5
  48. data/lib/bolt/config/validator.rb +0 -231
@@ -111,11 +111,6 @@ module Bolt
111
111
  @config['interpreters'] = normalize_interpreters(@config['interpreters'])
112
112
  end
113
113
 
114
- if @config['login-shell'] && !LOGIN_SHELLS.include?(@config['login-shell'])
115
- raise Bolt::ValidationError,
116
- "Unsupported login-shell #{@config['login-shell']}. Supported shells are #{LOGIN_SHELLS.join(', ')}"
117
- end
118
-
119
114
  if @config['login-shell'] == 'powershell'
120
115
  %w[tty run-as].each do |key|
121
116
  if @config[key]
@@ -4,13 +4,17 @@ require 'set'
4
4
  require 'bolt/config'
5
5
  require 'bolt/inventory/group'
6
6
  require 'bolt/inventory/inventory'
7
+ require 'bolt/inventory/options'
7
8
  require 'bolt/target'
8
9
  require 'bolt/util'
9
10
  require 'bolt/plugin'
11
+ require 'bolt/validator'
10
12
  require 'yaml'
11
13
 
12
14
  module Bolt
13
15
  class Inventory
16
+ include Bolt::Inventory::Options
17
+
14
18
  ENVIRONMENT_VAR = 'BOLT_INVENTORY'
15
19
 
16
20
  class ValidationError < Bolt::Error
@@ -45,11 +49,26 @@ module Bolt
45
49
  end
46
50
  end
47
51
 
52
+ # Builds the schema used by the validator.
53
+ #
54
+ def self.schema
55
+ schema = {
56
+ type: Hash,
57
+ properties: OPTIONS.map { |opt| [opt, _ref: opt] }.to_h,
58
+ definitions: DEFINITIONS,
59
+ _plugin: true
60
+ }
61
+
62
+ schema[:definitions]['config'][:properties] = Bolt::Config.transport_definitions
63
+ schema
64
+ end
65
+
48
66
  def self.from_config(config, plugins)
49
67
  logger = Bolt::Logger.logger(self)
50
68
 
51
69
  if ENV.include?(ENVIRONMENT_VAR)
52
70
  begin
71
+ source = ENVIRONMENT_VAR
53
72
  data = YAML.safe_load(ENV[ENVIRONMENT_VAR])
54
73
  raise Bolt::ParseError, "Could not parse inventory from $#{ENVIRONMENT_VAR}" unless data.is_a?(Hash)
55
74
  logger.debug("Loaded inventory from environment variable #{ENVIRONMENT_VAR}")
@@ -57,9 +76,11 @@ module Bolt
57
76
  raise Bolt::ParseError, "Could not parse inventory from $#{ENVIRONMENT_VAR}"
58
77
  end
59
78
  elsif config.inventoryfile
79
+ source = config.inventoryfile
60
80
  data = Bolt::Util.read_yaml_hash(config.inventoryfile, 'inventory')
61
81
  logger.debug("Loaded inventory from #{config.inventoryfile}")
62
82
  else
83
+ source = config.default_inventoryfile
63
84
  data = Bolt::Util.read_optional_yaml_hash(config.default_inventoryfile, 'inventory')
64
85
 
65
86
  if config.default_inventoryfile.exist?
@@ -74,6 +95,11 @@ module Bolt
74
95
  t.resolve(plugins) unless t.resolved?
75
96
  end
76
97
 
98
+ Bolt::Validator.new.tap do |validator|
99
+ validator.validate(data, schema, source)
100
+ validator.warnings.each { |warning| logger.warn(warning) }
101
+ end
102
+
77
103
  inventory = create_version(data, config.transport, config.transports, plugins)
78
104
  inventory.validate
79
105
  inventory
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'bolt/config/options'
3
4
  require 'bolt/inventory/group'
4
5
  require 'bolt/inventory/inventory'
5
6
  require 'bolt/inventory/target'
@@ -18,12 +19,20 @@ module Bolt
18
19
  GROUP_KEYS = DATA_KEYS + %w[name groups targets]
19
20
  CONFIG_KEYS = Bolt::Config::INVENTORY_OPTIONS.keys
20
21
 
21
- def initialize(input, plugins)
22
+ def initialize(input, plugins, all_group: false)
22
23
  @logger = Bolt::Logger.logger(self)
23
24
  @plugins = plugins
24
25
 
25
26
  input = @plugins.resolve_top_level_references(input) if @plugins.reference?(input)
26
27
 
28
+ if all_group
29
+ if input.key?('name') && input['name'] != 'all'
30
+ @logger.warn("Top-level group '#{input['name']}' cannot specify a name, using 'all' instead.")
31
+ end
32
+
33
+ input = input.merge('name' => 'all')
34
+ end
35
+
27
36
  raise ValidationError.new("Group does not have a name", nil) unless input.key?('name')
28
37
 
29
38
  @name = @plugins.resolve_references(input['name'])
@@ -254,14 +263,6 @@ module Bolt
254
263
  msg = "Found unexpected key(s) #{unexpected_keys.join(', ')} in group #{@name}"
255
264
  @logger.warn(msg)
256
265
  end
257
-
258
- Bolt::Util.walk_keys(input) do |key|
259
- if @plugins.reference?(key)
260
- raise ValidationError.new("Group keys cannot be specified as _plugin references", @name)
261
- else
262
- key
263
- end
264
- end
265
266
  end
266
267
 
267
268
  def validate(used_group_names = Set.new, used_target_names = Set.new, used_aliases = {})
@@ -323,7 +324,26 @@ module Bolt
323
324
  'features' => @plugins.resolve_references(data.fetch('features', [])),
324
325
  'plugin_hooks' => @plugins.resolve_references(data.fetch('plugin_hooks', {}))
325
326
  }
327
+
326
328
  validate_data_keys(result, target)
329
+
330
+ Bolt::Config::Options::TRANSPORT_CONFIG.each_key do |transport|
331
+ next unless result['config'].key?(transport)
332
+ transport_config = result['config'][transport]
333
+ next unless transport_config.is_a?(Hash)
334
+ transport_config = Bolt::Util.postwalk_vals(transport_config) do |val|
335
+ if val.is_a?(Hash)
336
+ val = val.compact
337
+ val = nil if val.empty?
338
+ end
339
+ val
340
+ end
341
+ # the transport config is user-specified data so we
342
+ # still want to preserve it even if it exclusively
343
+ # contains nil-resolved keys
344
+ result['config'][transport] = transport_config || {}
345
+ end
346
+
327
347
  result['features'] = Set.new(result['features'].flatten)
328
348
  result
329
349
  end
@@ -21,7 +21,7 @@ module Bolt
21
21
  @transport = transport
22
22
  @config = transports
23
23
  @plugins = plugins
24
- @groups = Group.new(@data.merge('name' => 'all'), plugins)
24
+ @groups = Group.new(@data, plugins, all_group: true)
25
25
  @group_lookup = {}
26
26
  @targets = {}
27
27
 
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/config/options'
4
+
5
+ module Bolt
6
+ class Inventory
7
+ module Options
8
+ # Top-level options available in the inventory.
9
+ OPTIONS = %w[
10
+ config
11
+ facts
12
+ features
13
+ groups
14
+ targets
15
+ vars
16
+ ].freeze
17
+
18
+ # Definitions used to validate the data.
19
+ # https://github.com/puppetlabs/bolt/blob/main/schemas/README.md
20
+ DEFINITIONS = {
21
+ "alias" => {
22
+ description: "A unique alias to refer to the target. Aliases cannot conflict "\
23
+ "with the name of a group, the name of a target, or another alias.",
24
+ type: [String, Array],
25
+ uniqueItems: true,
26
+ items: {
27
+ type: String,
28
+ _plugin: true
29
+ },
30
+ _plugin: true
31
+ },
32
+ "config" => {
33
+ description: "A map of configuration options.",
34
+ type: Hash,
35
+ # These properties are populated as part of Bolt::Inventory.schema
36
+ properties: {},
37
+ _plugin: true
38
+ },
39
+ "facts" => {
40
+ description: "A map of system information, also known as facts, for the target.",
41
+ type: Hash,
42
+ _plugin: true
43
+ },
44
+ "features" => {
45
+ description: "A list of available features for the target.",
46
+ type: Array,
47
+ uniqueItems: true,
48
+ items: {
49
+ type: String,
50
+ _plugin: true
51
+ },
52
+ _plugin: true
53
+ },
54
+ "groups" => {
55
+ description: "A list of groups and their associated configuration.",
56
+ type: Array,
57
+ items: {
58
+ type: Hash,
59
+ required: ["name"],
60
+ properties: {
61
+ "config" => { _ref: "config" },
62
+ "facts" => { _ref: "facts" },
63
+ "features" => { _ref: "features" },
64
+ "groups" => { _ref: "groups" },
65
+ "name" => { _ref: "name" },
66
+ "plugin_hooks" => { _ref: "plugin_hooks" },
67
+ "targets" => { _ref: "targets" },
68
+ "vars" => { _ref: "vars" }
69
+ },
70
+ _plugin: true
71
+ },
72
+ _plugin: true
73
+ },
74
+ "name" => {
75
+ description: "A human-readable name to refer to the group or target. Names "\
76
+ "cannot conflict with the name of a group, the name of a target, "\
77
+ "or the alias of a target. A name is required for a group and is "\
78
+ "required for a target unless the uri option is set.",
79
+ type: String,
80
+ _plugin: true
81
+ },
82
+ "plugin_hooks" => {
83
+ description: "Configuration for the Puppet library plugin used to install the "\
84
+ "Puppet agent on the target. For more information, see "\
85
+ "https://pup.pt/bolt-plugin-hooks",
86
+ type: Hash,
87
+ properties: {
88
+ "puppet_library" => {
89
+ description: "Configuration for the Puppet library plugin.",
90
+ type: Hash,
91
+ _plugin: true
92
+ }
93
+ },
94
+ _plugin: true
95
+ },
96
+ "targets" => {
97
+ description: "A list of targets and their associated configuration.",
98
+ type: Array,
99
+ items: {
100
+ type: [String, Hash],
101
+ properties: {
102
+ "alias" => { _ref: "alias" },
103
+ "config" => { _ref: "config" },
104
+ "facts" => { _ref: "facts" },
105
+ "features" => { _ref: "features" },
106
+ "name" => { _ref: "name" },
107
+ "plugin_hooks" => { _ref: "plugin_hooks" },
108
+ "uri" => { _ref: "uri" },
109
+ "vars" => { _ref: "vars" }
110
+ },
111
+ _plugin: true
112
+ },
113
+ _plugin: true
114
+ },
115
+ "uri" => {
116
+ description: "The URI of the target. This option is required unless the name "\
117
+ "option is set.",
118
+ type: String,
119
+ format: "uri",
120
+ _plugin: true
121
+ },
122
+ "vars" => {
123
+ description: "A map of variables for the group or target.",
124
+ type: Hash,
125
+ _plugin: true
126
+ }
127
+ }.freeze
128
+ end
129
+ end
130
+ end
@@ -31,7 +31,8 @@ module Bolt
31
31
  end
32
32
 
33
33
  if @name == 'localhost'
34
- target_data = localhost_defaults(target_data)
34
+ default = { 'config' => { 'transport' => 'local' } }
35
+ target_data = Bolt::Util.deep_merge(default, target_data)
35
36
  end
36
37
 
37
38
  @config = target_data['config'] || {}
@@ -49,18 +50,16 @@ module Bolt
49
50
  validate
50
51
  end
51
52
 
52
- def localhost_defaults(data)
53
+ def set_local_defaults
54
+ return if @set_local_default
53
55
  defaults = {
54
- 'config' => {
55
- 'transport' => 'local',
56
- 'local' => { 'interpreters' => { '.rb' => RbConfig.ruby } }
57
- },
58
- 'features' => ['puppet-agent']
56
+ 'local' => { 'interpreters' => { '.rb' => RbConfig.ruby } }
59
57
  }
60
- data = Bolt::Util.deep_merge(defaults, data)
61
- # If features is an empty array deep_merge won't add the puppet-agent
62
- data['features'] += ['puppet-agent'] if data['features'].empty?
63
- data
58
+ old_config = @config
59
+ @config = Bolt::Util.deep_merge(defaults, @config)
60
+ invalidate_config_cache! if old_config != @config
61
+ set_feature('puppet-agent')
62
+ @set_local_default = true
64
63
  end
65
64
 
66
65
  # rubocop:disable Naming/AccessorMethodName
@@ -6,8 +6,14 @@ module Bolt
6
6
  CONTENT_NAME_REGEX = /\A[a-z][a-z0-9_]*(::[a-z][a-z0-9_]*)*\z/.freeze
7
7
  MODULE_NAME_REGEX = /\A[a-z][a-z0-9_]*\z/.freeze
8
8
 
9
- def self.discover(modulepath)
10
- modulepath.each_with_object({}) do |path, mods|
9
+ def self.discover(modulepath, project)
10
+ mods = {}
11
+
12
+ if project.load_as_module?
13
+ mods[project.name] = Bolt::Module.new(project.name, project.path.to_s)
14
+ end
15
+
16
+ modulepath.each do |path|
11
17
  next unless File.exist?(path) && File.directory?(path)
12
18
  Dir.children(path)
13
19
  .map { |dir| File.join(path, dir) }
@@ -20,6 +26,8 @@ module Bolt
20
26
  end
21
27
  end
22
28
  end
29
+
30
+ mods
23
31
  end
24
32
 
25
33
  attr_reader :name, :path
@@ -17,14 +17,14 @@ module Bolt
17
17
 
18
18
  # Adds a single module to the project.
19
19
  #
20
- def add(name, specs, puppetfile_path, moduledir, config_path)
20
+ def add(name, specs, puppetfile_path, moduledir, project_file, config)
21
21
  project_specs = Specs.new(specs)
22
22
 
23
23
  # Exit early if project config already includes a spec with this name.
24
24
  if project_specs.include?(name)
25
25
  @outputter.print_message(
26
- "Project configuration file #{config_path} already includes specification with name "\
27
- "#{name}. Nothing to do."
26
+ "Project configuration file #{project_file} already includes specification "\
27
+ "with name #{name}. Nothing to do."
28
28
  )
29
29
  return true
30
30
  end
@@ -47,30 +47,32 @@ module Bolt
47
47
  # a version conflict.
48
48
  @outputter.print_action_step("Resolving module dependencies, this may take a moment")
49
49
 
50
+ @outputter.start_spin
50
51
  begin
51
52
  resolve_specs.add_specs('name' => name)
52
- puppetfile = Resolver.new.resolve(resolve_specs)
53
+ puppetfile = Resolver.new.resolve(resolve_specs, config)
53
54
  rescue Bolt::Error
54
55
  project_specs.add_specs('name' => name)
55
- puppetfile = Resolver.new.resolve(project_specs)
56
+ puppetfile = Resolver.new.resolve(project_specs, config)
56
57
  end
58
+ @outputter.stop_spin
57
59
 
58
60
  # Display the diff between the existing Puppetfile and the new Puppetfile.
59
61
  print_puppetfile_diff(existing_puppetfile, puppetfile)
60
62
 
61
63
  # Add the module to the project configuration.
62
- @outputter.print_action_step("Updating project configuration file at #{config_path}")
64
+ @outputter.print_action_step("Updating project configuration file at #{project_file}")
63
65
 
64
- data = Bolt::Util.read_yaml_hash(config_path, 'project')
66
+ data = Bolt::Util.read_yaml_hash(project_file, 'project')
65
67
  data['modules'] ||= []
66
68
  data['modules'] << name.tr('-', '/')
67
69
 
68
70
  begin
69
- File.write(config_path, data.to_yaml)
71
+ File.write(project_file, data.to_yaml)
70
72
  rescue SystemCallError => e
71
73
  raise Bolt::FileError.new(
72
74
  "Unable to update project configuration file: #{e.message}",
73
- config
75
+ project_file
74
76
  )
75
77
  end
76
78
 
@@ -79,7 +81,7 @@ module Bolt
79
81
  puppetfile.write(puppetfile_path, moduledir)
80
82
 
81
83
  # Install the modules.
82
- install_puppetfile(puppetfile_path, moduledir)
84
+ install_puppetfile(puppetfile_path, moduledir, config)
83
85
  end
84
86
 
85
87
  # Outputs a diff of an old Puppetfile and a new Puppetfile.
@@ -145,7 +147,7 @@ module Bolt
145
147
 
146
148
  # Installs a project's module dependencies.
147
149
  #
148
- def install(specs, path, moduledir, force: false, resolve: true)
150
+ def install(specs, path, moduledir, config = {}, force: false, resolve: true)
149
151
  @outputter.print_message("Installing project modules\n\n")
150
152
 
151
153
  if resolve != false
@@ -155,7 +157,11 @@ module Bolt
155
157
  # and write a Puppetfile.
156
158
  if force || !path.exist?
157
159
  @outputter.print_action_step("Resolving module dependencies, this may take a moment")
158
- puppetfile = Resolver.new.resolve(specs)
160
+
161
+ # This doesn't use the block as it's more testable to just mock *_spin
162
+ @outputter.start_spin
163
+ puppetfile = Resolver.new.resolve(specs, config)
164
+ @outputter.stop_spin
159
165
 
160
166
  # We get here either through 'bolt module install' which uses the
161
167
  # managed modulepath (which isn't configurable) or through bolt
@@ -177,14 +183,16 @@ module Bolt
177
183
  end
178
184
 
179
185
  # Install the modules.
180
- install_puppetfile(path, moduledir)
186
+ install_puppetfile(path, moduledir, config)
181
187
  end
182
188
 
183
189
  # Installs the Puppetfile and generates types.
184
190
  #
185
191
  def install_puppetfile(path, moduledir, config = {})
186
192
  @outputter.print_action_step("Syncing modules from #{path} to #{moduledir}")
193
+ @outputter.start_spin
187
194
  ok = Installer.new(config).install(path, moduledir)
195
+ @outputter.stop_spin
188
196
 
189
197
  # Automatically generate types after installing modules
190
198
  @outputter.print_action_step("Generating type references")