bolt 2.37.0 → 2.38.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.

@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'bolt/error'
4
4
  require 'bolt/util'
5
- require 'bolt/config/validator'
5
+ require 'bolt/validator'
6
6
  require 'bolt/config/transport/options'
7
7
 
8
8
  module Bolt
@@ -125,7 +125,7 @@ module Bolt
125
125
 
126
126
  # Validation defaults to just asserting the option types
127
127
  private def validate
128
- Bolt::Config::Validator.new.validate(@config.compact, self.class.schema.fetch(:properties))
128
+ Bolt::Validator.new.validate(@config.compact, self.class.schema)
129
129
  end
130
130
  end
131
131
  end
@@ -8,6 +8,7 @@ module Bolt
8
8
  module Transport
9
9
  class Local < Base
10
10
  WINDOWS_OPTIONS = %w[
11
+ bundled-ruby
11
12
  cleanup
12
13
  interpreters
13
14
  tmpdir
@@ -6,73 +6,8 @@ module Bolt
6
6
  module Options
7
7
  LOGIN_SHELLS = %w[sh bash zsh dash ksh powershell].freeze
8
8
 
9
- # The following constants define the various configuration options available to Bolt's
10
- # transports. Each constant is a hash where keys are the configuration option and values
11
- # are the option's definition. These options are used in multiple locations:
12
- #
13
- # - Automatic type validation when loading and setting configuration
14
- # - Generating reference documentation for configuration files
15
- # - Generating JSON schemas for configuration files
16
- #
17
- # Data includes keys defined by JSON Schema Draft 07 as well as some metadata used
18
- # by Bolt to generate documentation. The following keys are used:
19
- #
20
- # :description String A detailed description of the option and what it does. This
21
- # field is used in both documentation and the JSON schemas,
22
- # and should provide as much detail as possible, including
23
- # links to relevant documentation.
24
- #
25
- # :type Class The expected type of a value. These should be Ruby classes,
26
- # as this field is used to perform automatic type validation.
27
- # If an option can accept more than one type, this should be
28
- # an array of types. Boolean values should set :type to
29
- # [TrueClass, FalseClass], as Ruby does not have a single
30
- # Boolean class.
31
- #
32
- # :items Hash A definition hash for items in an array. Similar to values
33
- # for top-level options, items can have a :description, :type,
34
- # or any other key in this list.
35
- #
36
- # :uniqueItems Boolean Whether or not an array should contain only unique items.
37
- #
38
- # :properties Hash A hash where keys are sub-options and values are definitions
39
- # for the sub-option. Similar to values for top-level options,
40
- # properties can have a :description, :type, or any other key
41
- # in this list.
42
- #
43
- # :additionalProperties A variation of the :properties key, where the hash is a
44
- # Hash definition for any properties not specified in :properties.
45
- # This can be used to permit arbitrary sub-options, such as
46
- # logs for the 'log' option.
47
- #
48
- # :propertyNames Hash A hash that defines the properties that an option's property
49
- # names must adhere to.
50
- #
51
- # :required Array An array of properties that are required for options that
52
- # accept Hash values.
53
- #
54
- # :minimum Integer The minimum integer value for an option.
55
- #
56
- # :enum Array An array of values that the option recognizes.
57
- #
58
- # :pattern String A JSON regex pattern that the option's vaue should match.
59
- #
60
- # :format String Requires that a string value matches a format defined by the
61
- # JSON Schema draft.
62
- #
63
- # :_plugin Boolean Whether the option accepts a plugin reference. This is used
64
- # when generating the JSON schemas to determine whether or not
65
- # to include a reference to the _plugin definition. If :_plugin
66
- # is set to true, the script that generates JSON schemas will
67
- # automatically recurse through the :items and :properties keys
68
- # and add plugin references if applicable.
69
- #
70
- # :_example Any An example value for the option. This is used to generate
71
- # reference documentation for configuration files.
72
- #
73
- # :_default Any The documented default value for the option. This is only
74
- # used to generate reference documentation for configuration
75
- # files and is not used by Bolt to actually set default values.
9
+ # Definitions used to validate config options.
10
+ # https://github.com/puppetlabs/bolt/blob/main/schemas/README.md
76
11
  TRANSPORT_OPTIONS = {
77
12
  "basic-auth-only" => {
78
13
  type: [TrueClass, FalseClass],
@@ -81,6 +16,13 @@ module Bolt
81
16
  _default: false,
82
17
  _example: true
83
18
  },
19
+ "bundled-ruby" => {
20
+ description: "Whether to use the Ruby bundled with Bolt packages for local targets.",
21
+ type: [TrueClass, FalseClass],
22
+ _plugin: false,
23
+ _example: true,
24
+ _default: false
25
+ },
84
26
  "cacert" => {
85
27
  type: String,
86
28
  description: "The path to the CA certificate.",
@@ -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,25 @@ 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
+ }
60
+
61
+ schema[:definitions]['config'][:properties] = Bolt::Config.transport_definitions
62
+ schema
63
+ end
64
+
48
65
  def self.from_config(config, plugins)
49
66
  logger = Bolt::Logger.logger(self)
50
67
 
51
68
  if ENV.include?(ENVIRONMENT_VAR)
52
69
  begin
70
+ source = ENVIRONMENT_VAR
53
71
  data = YAML.safe_load(ENV[ENVIRONMENT_VAR])
54
72
  raise Bolt::ParseError, "Could not parse inventory from $#{ENVIRONMENT_VAR}" unless data.is_a?(Hash)
55
73
  logger.debug("Loaded inventory from environment variable #{ENVIRONMENT_VAR}")
@@ -57,9 +75,11 @@ module Bolt
57
75
  raise Bolt::ParseError, "Could not parse inventory from $#{ENVIRONMENT_VAR}"
58
76
  end
59
77
  elsif config.inventoryfile
78
+ source = config.inventoryfile
60
79
  data = Bolt::Util.read_yaml_hash(config.inventoryfile, 'inventory')
61
80
  logger.debug("Loaded inventory from #{config.inventoryfile}")
62
81
  else
82
+ source = config.default_inventoryfile
63
83
  data = Bolt::Util.read_optional_yaml_hash(config.default_inventoryfile, 'inventory')
64
84
 
65
85
  if config.default_inventoryfile.exist?
@@ -74,6 +94,11 @@ module Bolt
74
94
  t.resolve(plugins) unless t.resolved?
75
95
  end
76
96
 
97
+ Bolt::Validator.new.tap do |validator|
98
+ validator.validate(data, schema, source)
99
+ validator.warnings.each { |warning| logger.warn(warning) }
100
+ end
101
+
77
102
  inventory = create_version(data, config.transport, config.transports, plugins)
78
103
  inventory.validate
79
104
  inventory
@@ -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
@@ -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")