test-kitchen 3.9.1 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +8 -5
  3. data/lib/kitchen/cli.rb +33 -8
  4. data/lib/kitchen/collection.rb +1 -1
  5. data/lib/kitchen/command/list.rb +49 -9
  6. data/lib/kitchen/command/logs.rb +185 -0
  7. data/lib/kitchen/command/test.rb +2 -1
  8. data/lib/kitchen/command.rb +22 -9
  9. data/lib/kitchen/config.rb +8 -0
  10. data/lib/kitchen/configurable.rb +4 -4
  11. data/lib/kitchen/data_munger.rb +54 -21
  12. data/lib/kitchen/driver/base.rb +19 -0
  13. data/lib/kitchen/driver/dummy.rb +27 -0
  14. data/lib/kitchen/driver/proxy.rb +11 -6
  15. data/lib/kitchen/errors.rb +1 -0
  16. data/lib/kitchen/generator/init.rb +1 -1
  17. data/lib/kitchen/instance.rb +248 -182
  18. data/lib/kitchen/lazy_hash.rb +1 -1
  19. data/lib/kitchen/lifecycle_hook/remote.rb +5 -2
  20. data/lib/kitchen/logger.rb +407 -74
  21. data/lib/kitchen/logging.rb +2 -2
  22. data/lib/kitchen/metadata_chopper.rb +1 -1
  23. data/lib/kitchen/provisioner/base.rb +27 -35
  24. data/lib/kitchen/provisioner/external.rb +420 -0
  25. data/lib/kitchen/provisioner.rb +1 -2
  26. data/lib/kitchen/shell_out.rb +1 -1
  27. data/lib/kitchen/state_file.rb +12 -2
  28. data/lib/kitchen/transport/base.rb +1 -1
  29. data/lib/kitchen/transport/ssh.rb +5 -5
  30. data/lib/kitchen/transport/winrm.rb +8 -8
  31. data/lib/kitchen/util.rb +1 -1
  32. data/lib/kitchen/verifier/base.rb +17 -5
  33. data/lib/kitchen/version.rb +1 -1
  34. data/lib/kitchen.rb +21 -4
  35. data/lib/vendor/hash_recursive_merge.rb +2 -2
  36. data/templates/driver/driver.rb.erb +1 -1
  37. data/test-kitchen.gemspec +5 -7
  38. metadata +36 -41
  39. data/lib/kitchen/driver/ssh_base.rb +0 -346
  40. data/lib/kitchen/provisioner/chef/berkshelf.rb +0 -116
  41. data/lib/kitchen/provisioner/chef/common_sandbox.rb +0 -352
  42. data/lib/kitchen/provisioner/chef/policyfile.rb +0 -173
  43. data/lib/kitchen/provisioner/chef_apply.rb +0 -121
  44. data/lib/kitchen/provisioner/chef_base.rb +0 -723
  45. data/lib/kitchen/provisioner/chef_infra.rb +0 -167
  46. data/lib/kitchen/provisioner/chef_solo.rb +0 -82
  47. data/lib/kitchen/provisioner/chef_target.rb +0 -130
  48. data/lib/kitchen/provisioner/chef_zero.rb +0 -12
  49. data/lib/kitchen/ssh.rb +0 -296
@@ -105,18 +105,33 @@ module Kitchen
105
105
  debug("Executing prepare command: #{prepare_command}")
106
106
  conn.execute(prepare_command)
107
107
  debug("Executing run command: #{run_command}")
108
- conn.execute_with_retry(
109
- encode_for_powershell(run_command),
110
- config[:retry_on_exit_code],
111
- config[:max_retries],
112
- config[:wait_for_retry]
113
- )
114
- info("Downloading files from #{instance.to_str}")
115
- config[:downloads].to_h.each do |remotes, local|
116
- debug("Downloading #{Array(remotes).join(", ")} to #{local}")
117
- conn.download(remotes, local)
108
+ begin
109
+ conn.execute_with_retry(
110
+ encode_for_powershell(run_command),
111
+ config[:retry_on_exit_code],
112
+ config[:max_retries],
113
+ config[:wait_for_retry]
114
+ )
115
+ ensure
116
+ # Retrieval is best-effort: a file we could not fetch must not
117
+ # fail an otherwise successful converge, and must never replace
118
+ # an error already in flight from the run command. A transport
119
+ # that cannot download at all is a bug, not a missing file, so
120
+ # it still raises when there is no converge error to protect.
121
+ run_error = $!
122
+ begin
123
+ info("Downloading files from #{instance.to_str}")
124
+ config[:downloads].to_h.each do |remotes, local|
125
+ debug("Downloading #{Array(remotes).join(", ")} to #{local}")
126
+ conn.download(remotes, local)
127
+ end
128
+ debug("Download complete")
129
+ rescue => ex
130
+ raise unless run_error || ex.is_a?(Kitchen::Transport::TransportFailed)
131
+
132
+ warn("Failed to download files from #{instance.to_str}: #{ex.message}")
133
+ end
118
134
  end
119
- debug("Download complete")
120
135
  end
121
136
  rescue Kitchen::Transport::TransportFailed => ex
122
137
  raise ActionFailed, ex.message
@@ -247,24 +262,6 @@ module Kitchen
247
262
 
248
263
  private
249
264
 
250
- # Builds a complete command given a variables String preamble and a file
251
- # containing shell code.
252
- #
253
- # @param vars [String] shell variables, as a String
254
- # @param file [String] file basename (without extension) containing
255
- # shell code
256
- # @return [String] command
257
- # @api private
258
- def shell_code_from_file(vars, file)
259
- src_file = File.join(
260
- File.dirname(__FILE__),
261
- %w{.. .. .. support},
262
- file + (powershell_shell? ? ".ps1" : ".sh")
263
- )
264
-
265
- wrap_shell_code([vars, "", File.read(src_file)].join("\n"))
266
- end
267
-
268
265
  # Conditionally prefixes a command with a sudo command.
269
266
  #
270
267
  # @param command [String] command to be prefixed
@@ -357,13 +354,8 @@ module Kitchen
357
354
 
358
355
  # Replace $env:TEMP with the actual Windows temp directory based on the transport username
359
356
  if resolved_path.include?("$env:TEMP")
360
- # Try to get username from transport configuration
361
357
  # For Windows systems, fallback to "Administrator" if not found
362
- username = begin
363
- instance.transport[:username]
364
- rescue
365
- nil
366
- end
358
+ username = instance.transport[:username] if instance.transport.respond_to?(:[])
367
359
  username ||= "Administrator"
368
360
 
369
361
  temp_path = "C:/Users/#{username}/AppData/Local/Temp"
@@ -0,0 +1,420 @@
1
+ #
2
+ # Copyright:: (C) 2026
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # https://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ require "json" unless defined?(JSON)
17
+ require "open3"
18
+ require "rbconfig" unless defined?(RbConfig)
19
+ require "shellwords" unless defined?(Shellwords)
20
+
21
+ require_relative "base"
22
+ require_relative "../version"
23
+
24
+ module Kitchen
25
+ module Provisioner
26
+ # Executes an external provisioner provider over the v1 stdio protocol.
27
+ class External < Base
28
+ PROTOCOL_VERSION = "1.0".freeze
29
+ RESULT_STATUSES = %w{passed failed skipped}.freeze
30
+ LOG_LEVELS = %w{debug info warn error}.freeze
31
+ INTERNAL_CONFIG_KEYS = %i{
32
+ command provider pass_env kitchen_root test_base_path
33
+ }.freeze
34
+ REDACTED_KEYS = %w{password ssh_http_proxy_password}.freeze
35
+
36
+ kitchen_provisioner_api_version 2
37
+
38
+ plugin_version Kitchen::VERSION
39
+
40
+ required_config :command
41
+
42
+ default_config :provider, &:provider_name_from_command
43
+ default_config :pass_env, []
44
+
45
+ # (see Base#call)
46
+ def call(state)
47
+ verify_capabilities
48
+ validate_provider(state)
49
+ result = run_provider(state)
50
+ persist_provider_state(state, result)
51
+ end
52
+
53
+ def provider_name_from_command
54
+ first_part = Shellwords.split(config[:command].to_s).first.to_s
55
+ File.basename(first_part).sub(/^kitchen-provider-/, "")
56
+ end
57
+
58
+ private
59
+
60
+ def verify_capabilities
61
+ response = invoke_json("capabilities", {})
62
+ protocol_version = response.fetch("protocol_version", nil)
63
+ unless protocol_version == PROTOCOL_VERSION
64
+ raise_protocol_error(
65
+ "Unsupported external provider protocol version #{protocol_version.inspect}"
66
+ )
67
+ end
68
+
69
+ commands = Array(response["commands"])
70
+ missing_commands = %w{validate run} - commands
71
+ unless missing_commands.empty?
72
+ raise_protocol_error(
73
+ "External provider does not support required command(s): #{missing_commands.join(", ")}"
74
+ )
75
+ end
76
+
77
+ transports = Array(response["supported_transports"])
78
+ transport_type = transport_component({}).fetch(:type)
79
+ return if transports.include?(transport_type)
80
+
81
+ raise_protocol_error(
82
+ "External provider does not support #{transport_type.inspect} transport"
83
+ )
84
+ end
85
+
86
+ def validate_provider(state)
87
+ response = invoke_json("validate", request_envelope("validate", state))
88
+ require_protocol_version(response, "validation result")
89
+
90
+ return if response["status"] == "ok"
91
+
92
+ messages = Array(response["errors"]).map { |error| error["message"] }.compact
93
+ message = messages.empty? ? "external provider validation failed" : messages.join("; ")
94
+ raise ActionFailed, redact(message)
95
+ end
96
+
97
+ def run_provider(state)
98
+ stdout, stderr, status = invoke("run", request_envelope("run", state))
99
+ result = nil
100
+
101
+ stdout.each_line.with_index(1) do |line, index|
102
+ next if line.strip.empty?
103
+
104
+ event = parse_event(line, index)
105
+ require_protocol_version(event, "event")
106
+
107
+ case event["type"]
108
+ when "log"
109
+ handle_log_event(event)
110
+ when "progress"
111
+ handle_progress_event(event)
112
+ when "result"
113
+ result = event
114
+ else
115
+ raise_protocol_error("unknown external provider event type #{event["type"].inspect}")
116
+ end
117
+ end
118
+
119
+ unless status.success?
120
+ raise_protocol_error("external provider exited with #{status.exitstatus}", stderr)
121
+ end
122
+ raise_protocol_error("external provider did not emit a final result") if result.nil?
123
+
124
+ handle_result_event(result)
125
+ result
126
+ end
127
+
128
+ def handle_log_event(event)
129
+ level = event["level"]
130
+ raise_protocol_error("external provider log event has invalid level #{level.inspect}") unless LOG_LEVELS.include?(level)
131
+
132
+ logger.public_send(level, redact(event.fetch("message", "")))
133
+ end
134
+
135
+ def handle_progress_event(event)
136
+ stage = event.fetch("stage", "provider")
137
+ message = event.fetch("message", "")
138
+ info(redact("[#{stage}] #{message}"))
139
+ end
140
+
141
+ def handle_result_event(event)
142
+ status = event["status"]
143
+ unless RESULT_STATUSES.include?(status)
144
+ raise_protocol_error("external provider result has invalid status #{status.inspect}")
145
+ end
146
+
147
+ return if %w{passed skipped}.include?(status)
148
+
149
+ message = event["message"] || "external provider reported #{status}"
150
+ raise ActionFailed, redact(message)
151
+ end
152
+
153
+ def persist_provider_state(state, result)
154
+ provider_state = result.dig("state", "provider")
155
+ return if provider_state.nil?
156
+
157
+ state[:providers] ||= {}
158
+ state[:providers][config[:provider]] = provider_state
159
+ end
160
+
161
+ def invoke_json(operation, request)
162
+ stdout, stderr, status = invoke(operation, request)
163
+ unless status.success?
164
+ raise_protocol_error("external provider #{operation} exited with #{status.exitstatus}", stderr)
165
+ end
166
+
167
+ JSON.parse(stdout)
168
+ rescue JSON::ParserError => error
169
+ raise_protocol_error("malformed external provider #{operation} response: #{error.message}")
170
+ end
171
+
172
+ def invoke(operation, request)
173
+ Open3.capture3(
174
+ provider_environment,
175
+ *provider_command_parts,
176
+ operation,
177
+ stdin_data: JSON.generate(request)
178
+ )
179
+ rescue Errno::ENOENT => error
180
+ raise ActionFailed, redact("external provider command failed: #{error.message}")
181
+ end
182
+
183
+ def provider_command_parts
184
+ parts = Shellwords.split(config[:command].to_s)
185
+ raise ActionFailed, "external provider command cannot be blank" if parts.empty?
186
+
187
+ parts
188
+ end
189
+
190
+ def provider_environment
191
+ pass_env.each_with_object({}) do |name, env|
192
+ env[name] = ENV[name] if ENV.key?(name)
193
+ end
194
+ end
195
+
196
+ def request_envelope(operation, state)
197
+ {
198
+ protocol_version: PROTOCOL_VERSION,
199
+ operation: operation,
200
+ kitchen: kitchen_context,
201
+ instance: instance_context(state),
202
+ platform: platform_context,
203
+ suite: suite_context,
204
+ components: components_context(state),
205
+ executor: executor_context,
206
+ environment: environment_context,
207
+ state: state_context(state),
208
+ }
209
+ end
210
+
211
+ def kitchen_context
212
+ {
213
+ version: Kitchen::VERSION,
214
+ root_path: File.expand_path(config[:kitchen_root] || Dir.pwd),
215
+ }
216
+ end
217
+
218
+ def instance_context(state)
219
+ {
220
+ name: instance.name,
221
+ last_action: state[:last_action],
222
+ last_attempted_action: state[:last_attempted_action],
223
+ }
224
+ end
225
+
226
+ def platform_context
227
+ {
228
+ name: component_name(instance.platform),
229
+ os_type: component_attr(instance.platform, :os_type),
230
+ shell_type: component_attr(instance.platform, :shell_type),
231
+ }
232
+ end
233
+
234
+ def suite_context
235
+ { name: instance.suite.name }
236
+ end
237
+
238
+ def components_context(state)
239
+ {
240
+ driver: resolved_component(instance.driver, {}),
241
+ transport: transport_component(state),
242
+ provisioner: {
243
+ name: "external",
244
+ provider: config[:provider],
245
+ command: config[:command],
246
+ config: provider_config,
247
+ },
248
+ verifier: resolved_component(instance.verifier, {}),
249
+ }
250
+ end
251
+
252
+ def resolved_component(component, state)
253
+ {
254
+ name: component_name(component),
255
+ config: {},
256
+ state: state,
257
+ }
258
+ end
259
+
260
+ def transport_component(state)
261
+ name = component_name(instance.transport)
262
+ type = transport_type(name)
263
+ base = {
264
+ name: name,
265
+ type: type,
266
+ config: {},
267
+ }
268
+
269
+ case type
270
+ when "exec"
271
+ base.merge(
272
+ target: { kind: "local", platform: component_attr(instance.platform, :name) },
273
+ config: {
274
+ kitchen_root: File.expand_path(config[:kitchen_root] || Dir.pwd),
275
+ host_os: RbConfig::CONFIG["host_os"],
276
+ }
277
+ )
278
+ when "ssh"
279
+ base.merge(
280
+ target: machine_target(state),
281
+ config: compact_hash(
282
+ username: component_config(:username, state, "root"),
283
+ port: component_config(:port, state, 22),
284
+ connection_timeout: component_config(:connection_timeout, state),
285
+ connection_retries: component_config(:connection_retries, state),
286
+ connection_retry_sleep: component_config(:connection_retry_sleep, state),
287
+ proxy_command: component_config(:ssh_proxy_command, state),
288
+ gateway: component_config(:ssh_gateway, state),
289
+ gateway_port: component_config(:ssh_gateway_port, state),
290
+ gateway_username: component_config(:ssh_gateway_username, state)
291
+ )
292
+ )
293
+ when "winrm"
294
+ base.merge(
295
+ target: machine_target(state),
296
+ config: compact_hash(
297
+ username: component_config(:username, state, "administrator"),
298
+ port: component_config(:port, state, 5985),
299
+ scheme: component_config(:scheme, state, "http"),
300
+ transport: component_config(:winrm_transport, state, "negotiate").to_s,
301
+ elevated: component_config(:elevated, state),
302
+ operation_timeout: component_config(:operation_timeout, state),
303
+ receive_timeout: component_config(:receive_timeout, state),
304
+ connection_retries: component_config(:connection_retries, state),
305
+ connection_retry_sleep: component_config(:connection_retry_sleep, state)
306
+ )
307
+ )
308
+ else
309
+ base.merge(target: { kind: "custom" })
310
+ end
311
+ end
312
+
313
+ def transport_type(name)
314
+ case name.to_s
315
+ when "ssh", "winrm", "exec", "docker_exec"
316
+ name.to_s
317
+ else
318
+ "custom"
319
+ end
320
+ end
321
+
322
+ def executor_context
323
+ {
324
+ model: "exec",
325
+ command: config[:command],
326
+ input: "stdin",
327
+ request_file_argument: nil,
328
+ event_stream: "ndjson",
329
+ }
330
+ end
331
+
332
+ def environment_context
333
+ passed = pass_env.select { |name| ENV.key?(name) }
334
+ {
335
+ passed: passed,
336
+ user_allowed: pass_env,
337
+ kitchen_reserved: [],
338
+ }
339
+ end
340
+
341
+ def state_context(state)
342
+ providers = state.fetch(:providers, {})
343
+ { provider: providers.fetch(config[:provider], {}) }
344
+ end
345
+
346
+ def machine_target(state)
347
+ {
348
+ kind: "machine",
349
+ hostname: state.fetch(:hostname, state.fetch("hostname", "unknown")),
350
+ platform: component_attr(instance.platform, :name),
351
+ }
352
+ end
353
+
354
+ def component_config(key, state, default = nil)
355
+ return state[key] if state.key?(key)
356
+ return state[key.to_s] if state.key?(key.to_s)
357
+ return instance.transport[key] if instance.transport.respond_to?(:[]) && !instance.transport[key].nil?
358
+
359
+ default
360
+ end
361
+
362
+ def compact_hash(hash)
363
+ hash.compact
364
+ end
365
+
366
+ def provider_config
367
+ config.each_with_object({}) do |(key, value), result|
368
+ next if INTERNAL_CONFIG_KEYS.include?(key)
369
+
370
+ result[key.to_s] = value
371
+ end
372
+ end
373
+
374
+ def pass_env
375
+ Array(config[:pass_env]).map(&:to_s)
376
+ end
377
+
378
+ def component_name(component)
379
+ return component.name if component.respond_to?(:name)
380
+
381
+ component.class.name.split("::").last.downcase
382
+ end
383
+
384
+ def component_attr(component, attr)
385
+ component.public_send(attr) if component.respond_to?(attr)
386
+ end
387
+
388
+ def parse_event(line, index)
389
+ JSON.parse(line)
390
+ rescue JSON::ParserError => error
391
+ raise_protocol_error(
392
+ "malformed external provider event at line #{index}: #{error.message}"
393
+ )
394
+ end
395
+
396
+ def require_protocol_version(payload, payload_type)
397
+ return if payload["protocol_version"] == PROTOCOL_VERSION
398
+
399
+ raise_protocol_error(
400
+ "external provider #{payload_type} used unsupported protocol version #{payload["protocol_version"].inspect}"
401
+ )
402
+ end
403
+
404
+ def raise_protocol_error(message, stderr = nil)
405
+ details = [message]
406
+ details << stderr.to_s.strip unless stderr.to_s.strip.empty?
407
+ raise ActionFailed, redact(details.join(": "))
408
+ end
409
+
410
+ def redact(message)
411
+ redacted = Util.mask_values(message.to_s.dup, REDACTED_KEYS)
412
+ pass_env.each do |name|
413
+ value = ENV[name].to_s
414
+ redacted.gsub!(value, "******") unless value.empty?
415
+ end
416
+ redacted
417
+ end
418
+ end
419
+ end
420
+ end
@@ -26,7 +26,7 @@ module Kitchen
26
26
  # @author Fletcher Nichol <fnichol@nichol.ca>
27
27
  module Provisioner
28
28
  # Default provisioner to use
29
- DEFAULT_PLUGIN = "chef_infra".freeze
29
+ DEFAULT_PLUGIN = "shell".freeze
30
30
 
31
31
  # Returns an instance of a provisioner given a plugin type string.
32
32
  #
@@ -35,7 +35,6 @@ module Kitchen
35
35
  # @return [Provisioner::Base] a provisioner instance
36
36
  # @raise [ClientError] if a provisioner instance could not be created
37
37
  def self.for_plugin(plugin, config)
38
- plugin, config[:name] = "chef_infra", "chef_infra" if plugin == "chef_zero"
39
38
  Kitchen::Plugin.load(self, plugin, config)
40
39
  end
41
40
  end
@@ -68,7 +68,7 @@ module Kitchen
68
68
  sh.stdout
69
69
  rescue Mixlib::ShellOut::ShellCommandFailed => ex
70
70
  raise ShellCommandFailed, ex.message
71
- rescue Exception => error # rubocop:disable Lint/RescueException
71
+ rescue ::StandardError => error
72
72
  error.extend(Kitchen::Error)
73
73
  raise
74
74
  end
@@ -76,6 +76,13 @@ module Kitchen
76
76
  result
77
77
  end
78
78
 
79
+ # Returns the absolute path to the state file.
80
+ #
81
+ # @return [String] absolute path to the state file on disk
82
+ def path
83
+ file_name
84
+ end
85
+
79
86
  private
80
87
 
81
88
  # @return [String] absolute path to the yaml state file on disk
@@ -95,8 +102,11 @@ module Kitchen
95
102
  # @raise [StateFileLoadError] if the string document cannot be parsed
96
103
  # @api private
97
104
  def deserialize_string(string)
98
- YAML.safe_load(string)
99
- rescue SyntaxError, Psych::SyntaxError, Psych::DisallowedClass => ex
105
+ result = YAML.safe_load(string)
106
+ return result if result.is_a?(Hash)
107
+
108
+ raise StateFileLoadError, "Error parsing #{file_name} (expected a Hash)"
109
+ rescue SyntaxError, Psych::Exception => ex
100
110
  raise StateFileLoadError, "Error parsing #{file_name} (#{ex.message})"
101
111
  end
102
112
 
@@ -128,7 +128,7 @@ module Kitchen
128
128
  tries += 1
129
129
  debug("Attempting to execute command - try #{tries} of #{max_retries}.")
130
130
  execute(command)
131
- rescue Exception => e
131
+ rescue ::StandardError => e
132
132
  if retry?(tries, max_retries, retryable_exit_codes, e)
133
133
  close
134
134
  sleep wait_time
@@ -211,14 +211,14 @@ module Kitchen
211
211
  Array(remotes).each do |file|
212
212
  logger.debug("Attempting to download '#{file}' as file")
213
213
  session.scp.download!(file, local)
214
- rescue Net::SCP::Error
214
+ rescue Net::SCP::Error => file_error
215
215
  begin
216
216
  logger.debug("Attempting to download '#{file}' as directory")
217
217
  session.scp.download!(file, local, recursive: true)
218
- rescue Net::SCP::Error
219
- logger.warn(
220
- "SCP download failed for file or directory '#{file}', perhaps it does not exist?"
221
- )
218
+ rescue Net::SCP::Error => directory_error
219
+ raise SshFailed,
220
+ "SCP download failed for file or directory '#{file}' " \
221
+ "(file: #{file_error.message}; directory: #{directory_error.message})"
222
222
  end
223
223
  end
224
224
  rescue Net::SSH::Exception => ex
@@ -21,8 +21,8 @@ require "rbconfig" unless defined?(RbConfig)
21
21
  require "uri" unless defined?(URI)
22
22
  require_relative "../../kitchen"
23
23
  require_relative "../util"
24
- require "winrm" unless defined?(WinRM::Connection)
25
- require "winrm/exceptions" unless defined?(WinRM::WinRMHTTPTransportError)
24
+ require "chef-winrm" unless defined?(WinRM::Connection)
25
+ require "chef-winrm/exceptions" unless defined?(WinRM::WinRMHTTPTransportError)
26
26
 
27
27
  module Kitchen
28
28
  module Transport
@@ -420,9 +420,9 @@ module Kitchen
420
420
 
421
421
  private
422
422
 
423
- WINRM_SPEC_VERSION = ["~> 2.0"].freeze
424
- WINRM_FS_SPEC_VERSION = ["~> 1.0"].freeze
425
- WINRM_ELEVATED_SPEC_VERSION = ["~> 1.0"].freeze
423
+ CHEF_WINRM_SPEC_VERSION = ["~> 2.0"].freeze
424
+ CHEF_WINRM_FS_SPEC_VERSION = ["~> 1.0"].freeze
425
+ CHEF_WINRM_ELEVATED_SPEC_VERSION = ["~> 1.0"].freeze
426
426
 
427
427
  # Builds the hash of options needed by the Connection object on
428
428
  # construction.
@@ -515,9 +515,9 @@ module Kitchen
515
515
  # (see Base#load_needed_dependencies!)
516
516
  def load_needed_dependencies!
517
517
  super
518
- load_with_rescue!("winrm", WINRM_SPEC_VERSION.dup)
519
- load_with_rescue!("winrm-fs", WINRM_FS_SPEC_VERSION.dup)
520
- load_with_rescue!("winrm-elevated", WINRM_ELEVATED_SPEC_VERSION.dup) if config[:elevated]
518
+ load_with_rescue!("chef-winrm", CHEF_WINRM_SPEC_VERSION.dup)
519
+ load_with_rescue!("chef-winrm-fs", CHEF_WINRM_FS_SPEC_VERSION.dup)
520
+ load_with_rescue!("chef-winrm-elevated", CHEF_WINRM_ELEVATED_SPEC_VERSION.dup) if config[:elevated]
521
521
  end
522
522
 
523
523
  def load_with_rescue!(gem_name, spec_version)
data/lib/kitchen/util.rb CHANGED
@@ -128,7 +128,7 @@ module Kitchen
128
128
  "sh -c '\n#{cmd}\n'"
129
129
  end
130
130
 
131
- # Modifes the given string to strip leading whitespace on each line, the
131
+ # Modifies the given string to strip leading whitespace on each line, the
132
132
  # amount which is calculated by using the first line of text.
133
133
  #
134
134
  # @example
@@ -77,12 +77,24 @@ module Kitchen
77
77
  begin
78
78
  conn.execute(run_command)
79
79
  ensure
80
- info("Downloading files from #{instance.to_str}")
81
- config[:downloads].to_h.each do |remotes, local|
82
- debug("Downloading #{Array(remotes).join(", ")} to #{local}")
83
- conn.download(remotes, local)
80
+ # Retrieval is best-effort: a file we could not fetch must not
81
+ # fail an otherwise successful verify, and must never replace
82
+ # an error already in flight from the run command. A transport
83
+ # that cannot download at all is a bug, not a missing file, so
84
+ # it still raises when there is no verify error to protect.
85
+ run_error = $!
86
+ begin
87
+ info("Downloading files from #{instance.to_str}")
88
+ config[:downloads].to_h.each do |remotes, local|
89
+ debug("Downloading #{Array(remotes).join(", ")} to #{local}")
90
+ conn.download(remotes, local)
91
+ end
92
+ debug("Download complete")
93
+ rescue => ex
94
+ raise unless run_error || ex.is_a?(Kitchen::Transport::TransportFailed)
95
+
96
+ warn("Failed to download files from #{instance.to_str}: #{ex.message}")
84
97
  end
85
- debug("Download complete")
86
98
  end
87
99
  end
88
100
  rescue Kitchen::Transport::TransportFailed => ex
@@ -16,5 +16,5 @@
16
16
  # limitations under the License.
17
17
 
18
18
  module Kitchen
19
- VERSION = "3.9.1".freeze
19
+ VERSION = "4.1.0".freeze
20
20
  end