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.

@@ -9,7 +9,7 @@ module Bolt
9
9
  class Resolver
10
10
  # Resolves module specs and returns a Puppetfile object.
11
11
  #
12
- def resolve(specs)
12
+ def resolve(specs, _config = {})
13
13
  require 'puppetfile-resolver'
14
14
 
15
15
  # Build the document model from the specs.
@@ -2,24 +2,25 @@
2
2
 
3
3
  module Bolt
4
4
  class Outputter
5
- def self.for_format(format, color, verbose, trace)
5
+ def self.for_format(format, color, verbose, trace, spin)
6
6
  case format
7
7
  when 'human'
8
- Bolt::Outputter::Human.new(color, verbose, trace)
8
+ Bolt::Outputter::Human.new(color, verbose, trace, spin)
9
9
  when 'json'
10
- Bolt::Outputter::JSON.new(color, verbose, trace)
10
+ Bolt::Outputter::JSON.new(color, verbose, trace, false)
11
11
  when 'rainbow'
12
- Bolt::Outputter::Rainbow.new(color, verbose, trace)
12
+ Bolt::Outputter::Rainbow.new(color, verbose, trace, spin)
13
13
  when nil
14
14
  raise "Cannot use outputter before parsing."
15
15
  end
16
16
  end
17
17
 
18
- def initialize(color, verbose, trace, stream = $stdout)
18
+ def initialize(color, verbose, trace, spin, stream = $stdout)
19
19
  @color = color
20
20
  @verbose = verbose
21
21
  @trace = trace
22
22
  @stream = stream
23
+ @spin = spin
23
24
  end
24
25
 
25
26
  def indent(indent, string)
@@ -34,6 +35,19 @@ module Bolt
34
35
  def print_error
35
36
  raise NotImplementedError, "print_error() must be implemented by the outputter class"
36
37
  end
38
+
39
+ def start_spin; end
40
+
41
+ def stop_spin; end
42
+
43
+ def spin
44
+ start_spin
45
+ begin
46
+ yield
47
+ ensure
48
+ stop_spin
49
+ end
50
+ end
37
51
  end
38
52
  end
39
53
 
@@ -14,12 +14,13 @@ module Bolt
14
14
 
15
15
  def print_head; end
16
16
 
17
- def initialize(color, verbose, trace, stream = $stdout)
17
+ def initialize(color, verbose, trace, spin, stream = $stdout)
18
18
  super
19
19
  # Plans and without_default_logging() calls can both be nested, so we
20
20
  # track each of them with a "stack" consisting of an integer.
21
21
  @plan_depth = 0
22
22
  @disable_depth = 0
23
+ @pinwheel = %w[- \\ | /]
23
24
  end
24
25
 
25
26
  def colorize(color, string)
@@ -30,6 +31,24 @@ module Bolt
30
31
  end
31
32
  end
32
33
 
34
+ def start_spin
35
+ return unless @spin
36
+ @spin = true
37
+ @spin_thread = Thread.new do
38
+ loop do
39
+ sleep(0.1)
40
+ @stream.print(colorize(:cyan, @pinwheel.rotate!.first + "\b"))
41
+ end
42
+ end
43
+ end
44
+
45
+ def stop_spin
46
+ return unless @spin
47
+ @spin_thread.terminate
48
+ @spin = false
49
+ @stream.print("\b")
50
+ end
51
+
33
52
  def remove_trail(string)
34
53
  string.sub(/\s\z/, '')
35
54
  end
@@ -3,7 +3,7 @@
3
3
  module Bolt
4
4
  class Outputter
5
5
  class JSON < Bolt::Outputter
6
- def initialize(color, verbose, trace, stream = $stdout)
6
+ def initialize(color, verbose, trace, spin, stream = $stdout)
7
7
  super
8
8
  @items_open = false
9
9
  @object_open = false
@@ -6,7 +6,7 @@ module Bolt
6
6
  class Outputter
7
7
  class Logger < Bolt::Outputter
8
8
  def initialize(verbose, trace)
9
- super(false, verbose, trace)
9
+ super(false, verbose, trace, false)
10
10
  @logger = Bolt::Logger.logger(self)
11
11
  end
12
12
 
@@ -5,7 +5,7 @@ require 'bolt/pal'
5
5
  module Bolt
6
6
  class Outputter
7
7
  class Rainbow < Bolt::Outputter::Human
8
- def initialize(color, verbose, trace, stream = $stdout)
8
+ def initialize(color, verbose, trace, spin, stream = $stdout)
9
9
  begin
10
10
  require 'paint'
11
11
  if Bolt::Util.windows?
@@ -62,6 +62,17 @@ module Bolt
62
62
  end
63
63
  end
64
64
 
65
+ def start_spin
66
+ return unless @spin
67
+ @spin = true
68
+ @spin_thread = Thread.new do
69
+ loop do
70
+ @stream.print(colorize(:rainbow, @pinwheel.rotate!.first + "\b"))
71
+ sleep(0.1)
72
+ end
73
+ end
74
+ end
75
+
65
76
  def print_summary(results, elapsed_time = nil)
66
77
  ok_set = results.ok_set
67
78
  unless ok_set.empty?
@@ -12,7 +12,7 @@ module Bolt
12
12
  end
13
13
 
14
14
  TEMPLATE_OPTS = %w[alias config facts features name uri vars].freeze
15
- PLUGIN_OPTS = %w[_plugin query target_mapping].freeze
15
+ PLUGIN_OPTS = %w[_plugin _cache query target_mapping].freeze
16
16
 
17
17
  attr_reader :puppetdb_client
18
18
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'pathname'
4
4
  require 'bolt/config'
5
- require 'bolt/config/validator'
5
+ require 'bolt/validator'
6
6
  require 'bolt/pal'
7
7
  require 'bolt/module'
8
8
 
@@ -83,11 +83,7 @@ module Bolt
83
83
 
84
84
  logs << { info: "Loaded #{default}project from '#{fullpath}'" } if exist
85
85
 
86
- # Validate the config against the schema. This will raise a single error
87
- # with all validation errors.
88
- schema = Bolt::Config::OPTIONS.slice(*Bolt::Config::BOLT_PROJECT_OPTIONS)
89
-
90
- Bolt::Config::Validator.new.tap do |validator|
86
+ Bolt::Validator.new.tap do |validator|
91
87
  validator.validate(data, schema, project_file)
92
88
 
93
89
  validator.warnings.each { |warning| logs << { warn: warning } }
@@ -100,6 +96,16 @@ module Bolt
100
96
  new(data, path, type, logs, deprecations)
101
97
  end
102
98
 
99
+ # Builds the schema for bolt-project.yaml used by the validator.
100
+ #
101
+ def self.schema
102
+ {
103
+ type: Hash,
104
+ properties: Bolt::Config::BOLT_PROJECT_OPTIONS.map { |opt| [opt, _ref: opt] }.to_h,
105
+ definitions: Bolt::Config::OPTIONS
106
+ }
107
+ end
108
+
103
109
  def initialize(raw_data, path, type = 'option', logs = [], deprecations = [])
104
110
  @path = Pathname.new(path).expand_path
105
111
  @project_file = @path + CONFIG_NAME
@@ -196,6 +202,10 @@ module Bolt
196
202
  @data['plugin-cache']
197
203
  end
198
204
 
205
+ def module_install
206
+ @data['module-install']
207
+ end
208
+
199
209
  def modules
200
210
  @modules ||= @data['modules']&.map do |mod|
201
211
  if mod.is_a?(String)
@@ -217,7 +227,10 @@ module Bolt
217
227
  raise Bolt::ValidationError, "The project '#{name}' will not be loaded. The project name conflicts "\
218
228
  "with a built-in Bolt module of the same name."
219
229
  end
220
- else
230
+ elsif name.nil? &&
231
+ (File.directory?(plans_path) ||
232
+ File.directory?(@path + 'tasks') ||
233
+ File.directory?(@path + 'files'))
221
234
  message = "No project name is specified in bolt-project.yaml. Project-level content will not be available."
222
235
  @logs << { warn: message }
223
236
  end
@@ -103,7 +103,9 @@ module Bolt
103
103
  # early here and not initialize the project if the modules cannot be
104
104
  # resolved and installed.
105
105
  if modules
106
+ @outputter.start_spin
106
107
  Bolt::ModuleInstaller.new(@outputter, @pal).install(modules, puppetfile, moduledir)
108
+ @outputter.stop_spin
107
109
  end
108
110
 
109
111
  data = { 'name' => project_name }
@@ -72,7 +72,15 @@ module Bolt
72
72
  data = Bolt::Util.read_yaml_hash(project_file, 'config')
73
73
  modified = false
74
74
 
75
- [%w[apply_settings apply-settings], %w[plugin_hooks plugin-hooks]].each do |old, new|
75
+ # Keys to update. The first element is the old key, while the second is
76
+ # the key update it to.
77
+ to_update = [
78
+ %w[apply_settings apply-settings],
79
+ %w[puppetfile module-install],
80
+ %w[plugin_hooks plugin-hooks]
81
+ ]
82
+
83
+ to_update.each do |old, new|
76
84
  next unless data.key?(old)
77
85
 
78
86
  if data.key?(new)
@@ -61,6 +61,7 @@ module Bolt
61
61
  # Create specs to resolve from
62
62
  specs = Bolt::ModuleInstaller::Specs.new(modules.map(&:to_hash))
63
63
 
64
+ @outputter.start_spin
64
65
  # Attempt to resolve dependencies
65
66
  begin
66
67
  @outputter.print_message('')
@@ -72,6 +73,7 @@ module Bolt
72
73
  end
73
74
 
74
75
  migrate_managed_modules(puppetfile, puppetfile_path, managed_moduledir)
76
+ @outputter.stop_spin
75
77
 
76
78
  # Move remaining modules to 'modules'
77
79
  consolidate_modules(modulepath)
@@ -18,6 +18,7 @@ module Bolt
18
18
  def query_certnames(query)
19
19
  return [] unless query
20
20
 
21
+ @logger.debug("Querying certnames")
21
22
  results = make_query(query)
22
23
 
23
24
  if results&.first && !results.first&.key?('certname')
@@ -34,6 +35,8 @@ module Bolt
34
35
  certnames.uniq!
35
36
  name_query = certnames.map { |c| ["=", "certname", c] }
36
37
  name_query.insert(0, "or")
38
+
39
+ @logger.debug("Querying certnames")
37
40
  result = make_query(name_query, 'inventory')
38
41
 
39
42
  result&.each_with_object({}) do |node, coll|
@@ -52,6 +55,8 @@ module Bolt
52
55
  facts_query.insert(0, "or")
53
56
 
54
57
  query = ['and', name_query, facts_query]
58
+
59
+ @logger.debug("Querying certnames")
55
60
  result = make_query(query, 'fact-contents')
56
61
  result.map! { |h| h.delete_if { |k, _v| %w[environment name].include?(k) } }
57
62
  result.group_by { |c| c['certname'] }
@@ -63,11 +68,13 @@ module Bolt
63
68
  url += "/#{path}" if path
64
69
 
65
70
  begin
71
+ @logger.debug("Sending PuppetDB query to #{url}")
66
72
  response = http_client.post(url, body: body, header: headers)
67
73
  rescue StandardError => e
68
74
  raise Bolt::PuppetDBFailoverError, "Failed to query PuppetDB: #{e}"
69
75
  end
70
76
 
77
+ @logger.debug("Got response code #{response.code} from PuppetDB")
71
78
  if response.code != 200
72
79
  msg = "Failed to query PuppetDB: #{response.body}"
73
80
  if response.code == 400
@@ -92,6 +99,7 @@ module Bolt
92
99
  return @http if @http
93
100
  # lazy-load expensive gem code
94
101
  require 'httpclient'
102
+ @logger.trace("Creating HTTP Client")
95
103
  @http = HTTPClient.new
96
104
  @http.ssl_config.set_client_cert_file(@config.cert, @config.key) if @config.cert
97
105
  @http.ssl_config.add_trust_ca(@config.cacert)
@@ -193,7 +193,8 @@ module Bolt
193
193
  def run_command(command, options = {}, position = [])
194
194
  command = [*env_declarations(options[:env_vars]), command].join("\r\n") if options[:env_vars]
195
195
 
196
- output = execute(command)
196
+ wrap_command = conn.is_a?(Bolt::Transport::Local::Connection)
197
+ output = execute(command, wrap_command)
197
198
  Bolt::Result.for_command(target,
198
199
  output.stdout.string,
199
200
  output.stderr.string,
@@ -284,8 +285,9 @@ module Bolt
284
285
  end
285
286
  end
286
287
 
287
- def execute(command)
288
- if conn.max_command_length && command.length > conn.max_command_length
288
+ def execute(command, wrap_command = false)
289
+ if (conn.max_command_length && command.length > conn.max_command_length) ||
290
+ wrap_command
289
291
  return with_tmpdir do |dir|
290
292
  command += "\r\nif (!$?) { if($LASTEXITCODE) { exit $LASTEXITCODE } else { exit 1 } }"
291
293
  script_file = File.join(dir, "#{SecureRandom.uuid}_wrapper.ps1")
@@ -80,6 +80,10 @@ module Bolt
80
80
  inventory_target.resources
81
81
  end
82
82
 
83
+ def set_local_defaults
84
+ inventory_target.set_local_defaults
85
+ end
86
+
83
87
  # rubocop:disable Naming/AccessorMethodName
84
88
  def set_resource(resource)
85
89
  inventory_target.set_resource(resource)
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'bolt/logger'
3
4
  require 'bolt/transport/simple'
4
5
 
5
6
  module Bolt
@@ -10,6 +11,18 @@ module Bolt
10
11
  end
11
12
 
12
13
  def with_connection(target)
14
+ if target.transport_config['bundled-ruby'] || target.name == 'localhost'
15
+ target.set_local_defaults
16
+ end
17
+
18
+ if target.name != 'localhost' &&
19
+ !target.transport_config.key?('bundled-ruby')
20
+ msg = "The local transport will default to using Bolt's Ruby interpreter and "\
21
+ "setting the 'puppet-agent' feature in Bolt 3.0. Enable or disable these "\
22
+ "defaults by setting 'bundled-ruby' in the local transport config."
23
+ Bolt::Logger.warn_once('local default config', msg)
24
+ end
25
+
13
26
  yield Connection.new(target)
14
27
  end
15
28
  end
@@ -0,0 +1,226 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/error'
4
+
5
+ # This class validates config against a schema, raising an error that includes
6
+ # details about any invalid configuration.
7
+ #
8
+ module Bolt
9
+ class Validator
10
+ attr_reader :deprecations, :warnings
11
+
12
+ def initialize
13
+ @errors = []
14
+ @deprecations = []
15
+ @warnings = []
16
+ @path = []
17
+ end
18
+
19
+ # This is the entry method for validating data against the schema.
20
+ #
21
+ def validate(data, schema, location = nil)
22
+ @schema = schema
23
+ @location = location
24
+
25
+ validate_value(data, schema)
26
+
27
+ raise_error
28
+ end
29
+
30
+ # Raises a ValidationError if there are any errors. All error messages
31
+ # created during validation are concatenated into a single error
32
+ # message.
33
+ #
34
+ private def raise_error
35
+ return unless @errors.any?
36
+
37
+ message = "Invalid configuration"
38
+ message += " at #{@location}" if @location
39
+ message += ":\n"
40
+ message += @errors.map { |error| "\s\s#{error}" }.join("\n")
41
+
42
+ raise Bolt::ValidationError, message
43
+ end
44
+
45
+ # Validate an individual value. This performs validation that is
46
+ # common to all values, including type validation. After validating
47
+ # the value's type, the value is passed off to an individual
48
+ # validation method for the value's type.
49
+ #
50
+ private def validate_value(value, definition)
51
+ definition = @schema.dig(:definitions, definition[:_ref]) if definition[:_ref]
52
+
53
+ return if plugin_reference?(value, definition)
54
+ return unless valid_type?(value, definition)
55
+
56
+ case value
57
+ when Hash
58
+ validate_hash(value, definition)
59
+ when Array
60
+ validate_array(value, definition)
61
+ when String
62
+ validate_string(value, definition)
63
+ when Numeric
64
+ validate_number(value, definition)
65
+ end
66
+ end
67
+
68
+ # Validates a hash value, logging errors for any validations that fail.
69
+ # This will enumerate each key-value pair in the hash and validate each
70
+ # value individually.
71
+ #
72
+ private def validate_hash(value, definition)
73
+ properties = definition[:properties] ? definition[:properties].keys : []
74
+
75
+ if definition[:properties] && definition[:additionalProperties].nil?
76
+ validate_keys(value.keys, properties)
77
+ end
78
+
79
+ if definition[:required] && (definition[:required] - value.keys).any?
80
+ missing = definition[:required] - value.keys
81
+ @errors << "Value at '#{path}' is missing required keys #{missing.join(', ')}"
82
+ end
83
+
84
+ value.each_pair do |key, val|
85
+ @path.push(key)
86
+
87
+ if properties.include?(key)
88
+ check_deprecated(key, definition[:properties][key])
89
+ validate_value(val, definition[:properties][key])
90
+ elsif definition[:additionalProperties]
91
+ validate_value(val, definition[:additionalProperties])
92
+ end
93
+ ensure
94
+ @path.pop
95
+ end
96
+ end
97
+
98
+ # Validates an array value, logging errors for any validations that fail.
99
+ # This will enumerate the items in the array and validate each item
100
+ # individually.
101
+ #
102
+ private def validate_array(value, definition)
103
+ if definition[:uniqueItems] && value.size != value.uniq.size
104
+ @errors << "Value at '#{path}' must not include duplicate elements"
105
+ return
106
+ end
107
+
108
+ return unless definition.key?(:items)
109
+
110
+ value.each_with_index do |item, index|
111
+ @path.push(index)
112
+ validate_value(item, definition[:items])
113
+ ensure
114
+ @path.pop
115
+ end
116
+ end
117
+
118
+ # Validates a string value, logging errors for any validations that fail.
119
+ #
120
+ private def validate_string(value, definition)
121
+ if definition.key?(:enum) && !definition[:enum].include?(value)
122
+ message = "Value at '#{path}' must be "
123
+ message += "one of " if definition[:enum].count > 1
124
+ message += definition[:enum].join(', ')
125
+ multitype_error(message, value, definition)
126
+ end
127
+ end
128
+
129
+ # Validates a numeric value, logging errors for any validations that fail.
130
+ #
131
+ private def validate_number(value, definition)
132
+ if definition.key?(:minimum) && value < definition[:minimum]
133
+ @errors << "Value at '#{path}' must be a minimum of #{definition[:minimum]}"
134
+ end
135
+
136
+ if definition.key?(:maximum) && value > definition[:maximum]
137
+ @errors << "Value at '#{path}' must be a maximum of #{definition[:maximum]}"
138
+ end
139
+ end
140
+
141
+ # Adds warnings for unknown config options.
142
+ #
143
+ private def validate_keys(keys, known_keys)
144
+ (keys - known_keys).each do |key|
145
+ message = "Unknown option '#{key}'"
146
+ message += " at '#{path}'" if @path.any?
147
+ message += " at #{@location}" if @location
148
+ message += "."
149
+ @warnings << message
150
+ end
151
+ end
152
+
153
+ # Adds a warning if the given option is deprecated.
154
+ #
155
+ private def check_deprecated(key, definition)
156
+ definition = @schema.dig(:definitions, definition[:_ref]) if definition[:_ref]
157
+
158
+ if definition.key?(:_deprecation)
159
+ message = "Option '#{path}' "
160
+ message += "at #{@location} " if @location
161
+ message += "is deprecated. #{definition[:_deprecation]}"
162
+ @deprecations << { option: key, message: message }
163
+ end
164
+ end
165
+
166
+ # Returns true if a value is a plugin reference. This also validates whether
167
+ # a value can be a plugin reference in the first place. If the value is a
168
+ # plugin reference but cannot be one according to the schema, then this will
169
+ # log an error.
170
+ #
171
+ private def plugin_reference?(value, definition)
172
+ if value.is_a?(Hash) && value.key?('_plugin')
173
+ unless definition[:_plugin]
174
+ @errors << "Value at '#{path}' is a plugin reference, which is unsupported at "\
175
+ "this location"
176
+ end
177
+
178
+ true
179
+ else
180
+ false
181
+ end
182
+ end
183
+
184
+ # Asserts the type for each option against the type specified in the schema
185
+ # definition. The schema definition can specify multiple valid types, so the
186
+ # value needs to only match one of the types to be valid. Returns early if
187
+ # there is no type in the definition (in practice this shouldn't happen, but
188
+ # this will safeguard against any dev mistakes).
189
+ #
190
+ private def valid_type?(value, definition)
191
+ return unless definition.key?(:type)
192
+
193
+ types = Array(definition[:type])
194
+
195
+ if types.include?(value.class)
196
+ true
197
+ else
198
+ if types.include?(TrueClass) || types.include?(FalseClass)
199
+ types = types - [TrueClass, FalseClass] + ['Boolean']
200
+ end
201
+
202
+ @errors << "Value at '#{path}' must be of type #{types.join(' or ')}"
203
+
204
+ false
205
+ end
206
+ end
207
+
208
+ # Adds an error that includes additional helpful information for values
209
+ # that accept multiple types.
210
+ #
211
+ private def multitype_error(message, value, definition)
212
+ if Array(definition[:type]).count > 1
213
+ types = Array(definition[:type]) - [value.class]
214
+ message += " or must be of type #{types.join(' or ')}"
215
+ end
216
+
217
+ @errors << message
218
+ end
219
+
220
+ # Returns the formatted path for the key.
221
+ #
222
+ private def path
223
+ @path.join('.')
224
+ end
225
+ end
226
+ end