kitchen-vra 3.4.0 → 3.4.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c3ad4a4ab887a646b1856e8329bfa319f85c85adf5ab1d55785ed443e31ea623
4
- data.tar.gz: b178306e4e6e20e88f27d1274913a6bf7c2be4ba9871fda69b8373938550783f
3
+ metadata.gz: 3077884f3c8f5e434889905e6c4d47e06ecf308fe810948c79d644e5f13086bd
4
+ data.tar.gz: addc3a87013a8834a75392625e026b5efdf1cfaff8009e0eaa8e690236b0be84
5
5
  SHA512:
6
- metadata.gz: 4e2cbf243619ad72b38384a2520e7ccc68646e16c472e1356e088f00058ebf707e86aa558470dd75fb17b13a0742448ea1c822b73e1e38482340e1296b9a0686
7
- data.tar.gz: 1a85f3b0bb3cbd9fa872a057225e0ab9494940830bbb68833b81968fc197b2e883bf32163022dbe83cb8eba5abce8212ba9467e8e5dd62c8462185a4236d92b6
6
+ metadata.gz: 2d6521702e1cc486f91d1aa2c0d1c17fdd1feaece55a8b634aaea65950282bde3b327e4bb79e7c256a6683ba8128b36ef5b59852b2c1f0de763d64dc4aa359a2
7
+ data.tar.gz: 190fa3d7bbfc3fd0db0220934390545f9b28cd4565126648d0503459de10035962f795b69275599409671aa15d2f5d31a018074f7b59d4a092f83437b027d8cd
data/.gitignore CHANGED
@@ -2,7 +2,6 @@
2
2
  /.yardoc
3
3
  /Gemfile.lock
4
4
  /_yardoc/
5
- /coverage/
6
5
  /doc/
7
6
  /pkg/
8
7
  /spec/reports/
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "3.4.0"
2
+ ".": "3.4.1"
3
3
  }
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --require spec_helper
2
+ --color
data/.rubocop.yml CHANGED
@@ -6,4 +6,3 @@ AllCops:
6
6
  TargetRubyVersion: 3.1
7
7
  Exclude:
8
8
  - "vendor/**/*"
9
- - "spec/**/*"
data/.yardopts CHANGED
@@ -8,4 +8,4 @@ lib/**/*.rb
8
8
  -
9
9
  CHANGELOG.md
10
10
  CONTRIBUTING.md
11
- LICENSE.txt
11
+ LICENSE
data/CHANGELOG.md CHANGED
@@ -10,6 +10,13 @@
10
10
  * Docs: rewrite README for new users and split contributor docs ([#80](https://github.com/test-kitchen/kitchen-vra/pull/80)) ([d9ebc45](https://github.com/test-kitchen/kitchen-vra/commit/d9ebc45))
11
11
  * Standardize renovate config and remove dependabot ([#81](https://github.com/test-kitchen/kitchen-vra/pull/81)) ([be552bd](https://github.com/test-kitchen/kitchen-vra/commit/be552bd))
12
12
 
13
+ ## [3.4.1](https://github.com/test-kitchen/kitchen-vra/compare/v3.4.0...v3.4.1) (2026-08-30)
14
+
15
+
16
+ ### Bug Fixes
17
+
18
+ * stop prompting for credentials on every run with cache_credentials ([#91](https://github.com/test-kitchen/kitchen-vra/issues/91)) ([e1c185f](https://github.com/test-kitchen/kitchen-vra/commit/e1c185f8b38700abd98824cae67f01a33d36b710))
19
+
13
20
  ## [3.4.0](https://github.com/test-kitchen/kitchen-vra/compare/v3.3.4...v3.4.0) (2026-08-24)
14
21
 
15
22
 
@@ -383,8 +383,10 @@ module Kitchen
383
383
  wait_for_request(destroy_request)
384
384
  info("Destroy request complete.")
385
385
 
386
- File.delete(".kitchen/cached_vra") if File.exist?(".kitchen/cached_vra")
387
- info("Removed cached file")
386
+ return unless File.exist?(CREDENTIALS_CACHE_FILE)
387
+
388
+ File.delete(CREDENTIALS_CACHE_FILE)
389
+ info("Removed the cached credentials in #{CREDENTIALS_CACHE_FILE}.")
388
390
  end
389
391
 
390
392
  # Builds the vRA catalog request for the configured blueprint.
@@ -431,21 +433,43 @@ module Kitchen
431
433
 
432
434
  # The vRA API client, built from the resolved credentials.
433
435
  #
434
- # On any failure the credentials are re-prompted, on the assumption that
435
- # what failed was authentication.
436
+ # Credentials are resolved once, when the client is first built. A
437
+ # failure to build it is assumed to be an authentication problem, so the
438
+ # credentials are asked for again and the client built a second time. A
439
+ # second failure is raised rather than swallowed.
436
440
  #
437
441
  # @return [Vra::Client]
442
+ # @raise [ArgumentError] if the credentials are still not usable after
443
+ # re-prompting
438
444
  def vra_client
439
- check_config config[:cache_credentials]
440
- @client ||= ::Vra::Client.new(
441
- base_url: config[:base_url],
442
- username: config[:username],
443
- password: config[:password],
444
- domain: config[:domain],
445
- verify_ssl: config[:verify_ssl]
446
- )
447
- rescue => _e
448
- check_config true
445
+ @client ||= build_client
446
+ end
447
+
448
+ # Resolves the credentials and builds the client, asking for the
449
+ # credentials a second time if the first attempt fails.
450
+ #
451
+ # @return [Vra::Client]
452
+ # @raise [ArgumentError] if the second attempt fails as well
453
+ # @api private
454
+ def build_client
455
+ retried = false
456
+
457
+ begin
458
+ check_config(retried)
459
+ ::Vra::Client.new(
460
+ base_url: config[:base_url],
461
+ username: config[:username],
462
+ password: config[:password],
463
+ domain: config[:domain],
464
+ verify_ssl: config[:verify_ssl]
465
+ )
466
+ rescue => e
467
+ raise if retried
468
+
469
+ warn("Unable to build a vRA client: #{e.message}")
470
+ retried = true
471
+ retry
472
+ end
449
473
  end
450
474
 
451
475
  # Polls a vRA request until it completes or times out.
@@ -457,8 +481,6 @@ module Kitchen
457
481
  # @return [void]
458
482
  # @raise [Timeout::Error] if the request does not complete in time
459
483
  def wait_for_request(request)
460
- # config = check_config config
461
-
462
484
  last_status = ""
463
485
  wait_time = config[:request_timeout]
464
486
  sleep_time = config[:request_refresh_rate]
@@ -23,6 +23,6 @@ module Kitchen
23
23
  # Test Kitchen's driver plugins.
24
24
  module Driver
25
25
  # The version of the kitchen-vra gem.
26
- VRA_VERSION = "3.4.0"
26
+ VRA_VERSION = "3.4.1"
27
27
  end
28
28
  end
data/spec/spec_helper.rb CHANGED
@@ -19,13 +19,51 @@
19
19
  #
20
20
 
21
21
  require "webmock/rspec"
22
+ require "tmpdir"
23
+ require "fileutils"
22
24
 
25
+ require "kitchen/driver/vra"
26
+ require "kitchen/provisioner/dummy"
27
+ require "kitchen/transport/dummy"
28
+ require "kitchen/verifier/dummy"
29
+
30
+ Dir[File.join(__dir__, "support", "**", "*.rb")].sort.each { |file| require file }
31
+
32
+ # These specs never reach a vRA appliance. Everything the driver does is turn
33
+ # kitchen.yml configuration into a vRA catalog request, and turn vRA's answers
34
+ # back into Test Kitchen state, so an outbound request from a spec is a mistake
35
+ # rather than a slow test.
23
36
  WebMock.disable_net_connect!(allow_localhost: true)
24
37
 
25
- if ENV["COVERAGE"]
26
- require "simplecov"
27
- SimpleCov.profiles.define "gem" do
28
- command_name "Specs"
38
+ RSpec.configure do |config|
39
+ config.expect_with :rspec do |expectations|
40
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
29
41
  end
30
- SimpleCov.start "gem"
42
+
43
+ config.mock_with :rspec do |mocks|
44
+ # Fail if an example stubs a method the real object does not have. Almost
45
+ # every driver method is a call into the vmware-vra gem, so without this a
46
+ # rename there leaves the specs stubbing a method that no longer exists,
47
+ # passing while the driver is broken.
48
+ mocks.verify_partial_doubles = true
49
+ end
50
+
51
+ config.shared_context_metadata_behavior = :apply_to_host_groups
52
+
53
+ # No `describe` on bare objects and no `should` -- the RSpec 2 DSL, kept
54
+ # alive only by monkey patches on Object and BasicObject.
55
+ config.disable_monkey_patching!
56
+
57
+ # Surface deprecations as failures rather than warnings that scroll past.
58
+ config.raise_errors_for_deprecations!
59
+
60
+ config.include VraApi
61
+
62
+ config.filter_run_when_matching :focus
63
+
64
+ # Run specs in random order to surface order dependencies. To debug one, fix
65
+ # the order by passing the seed printed after each run:
66
+ # --seed 1234
67
+ config.order = :random
68
+ Kernel.srand config.seed
31
69
  end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
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
+ # http://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
+
17
+ # Stand-ins for the vmware-vra objects the driver talks to.
18
+ #
19
+ # The driver is a thin layer over the vmware-vra gem: nearly every method
20
+ # either asks a Vra object something or hands it a request. A bare `double`
21
+ # answers whatever it is told to answer, so a suite built on them keeps passing
22
+ # after vmware-vra renames a method -- which is exactly the change most likely
23
+ # to break the driver. Everything here is a verifying double checked against
24
+ # the real class instead, so the specs fail when the gem moves underneath them.
25
+ #
26
+ # Each helper takes a hash of overrides so an example can say only what it
27
+ # cares about:
28
+ #
29
+ # vra_deployment(status: "CREATE_FAILED", failed?: true)
30
+ module VraApi
31
+ # A VM inside a deployment, as returned by Vra::Deployment#resources.
32
+ #
33
+ # @param overrides [Hash] stubs to replace the defaults with
34
+ # @return [InstanceDouble<Vra::Resource>]
35
+ def vra_resource(overrides = {})
36
+ instance_double(
37
+ Vra::Resource,
38
+ {
39
+ id: "res-1",
40
+ deployment_id: "dep-1",
41
+ name: "test-server",
42
+ ip_address: "1.2.3.4",
43
+ vm?: true,
44
+ }.merge(overrides)
45
+ )
46
+ end
47
+
48
+ # A deployment. This is also what Vra::DeploymentRequest#submit hands back,
49
+ # which is why the driver waits on it and then asks it for its resources.
50
+ #
51
+ # @param overrides [Hash] stubs to replace the defaults with
52
+ # @return [InstanceDouble<Vra::Deployment>]
53
+ def vra_deployment(overrides = {})
54
+ instance_double(
55
+ Vra::Deployment,
56
+ {
57
+ id: "dep-1",
58
+ name: "test-instance",
59
+ status: "CREATE_SUCCESSFUL",
60
+ failed?: false,
61
+ completed?: true,
62
+ completion_details: nil,
63
+ refresh: nil,
64
+ resources: [vra_resource],
65
+ }.merge(overrides)
66
+ )
67
+ end
68
+
69
+ # A request against a deployment, such as the destroy action.
70
+ #
71
+ # @param overrides [Hash] stubs to replace the defaults with
72
+ # @return [InstanceDouble<Vra::Request>]
73
+ def vra_request(overrides = {})
74
+ instance_double(
75
+ Vra::Request,
76
+ {
77
+ id: "req-1",
78
+ status: "SUCCESSFUL",
79
+ completed?: true,
80
+ refresh: nil,
81
+ details: nil,
82
+ }.merge(overrides)
83
+ )
84
+ end
85
+
86
+ # A catalog item, as returned by Vra::Catalog#fetch_catalog_items.
87
+ #
88
+ # @param overrides [Hash] stubs to replace the defaults with
89
+ # @return [InstanceDouble<Vra::CatalogItem>]
90
+ def vra_catalog_item(overrides = {})
91
+ instance_double(
92
+ Vra::CatalogItem,
93
+ { id: "cat-1", name: "Ubuntu Server" }.merge(overrides)
94
+ )
95
+ end
96
+
97
+ # An unsubmitted catalog request.
98
+ #
99
+ # @param overrides [Hash] stubs to replace the defaults with
100
+ # @return [InstanceDouble<Vra::DeploymentRequest>]
101
+ def vra_deployment_request(overrides = {})
102
+ instance_double(
103
+ Vra::DeploymentRequest,
104
+ { submit: vra_deployment, set_parameters: nil }.merge(overrides)
105
+ )
106
+ end
107
+
108
+ # A vRA API client, wired to a catalog and a deployments collection.
109
+ #
110
+ # @param catalog [InstanceDouble<Vra::Catalog>, nil]
111
+ # @param deployments [InstanceDouble<Vra::Deployments>, nil]
112
+ # @return [InstanceDouble<Vra::Client>]
113
+ def vra_client(catalog: nil, deployments: nil)
114
+ instance_double(
115
+ Vra::Client,
116
+ catalog: catalog || instance_double(Vra::Catalog),
117
+ deployments: deployments || instance_double(Vra::Deployments)
118
+ )
119
+ end
120
+ end
data/spec/vra_spec.rb CHANGED
@@ -18,43 +18,35 @@
18
18
  # limitations under the License.
19
19
  #
20
20
 
21
- require "spec_helper"
22
- require "tmpdir"
23
- require "fileutils"
24
- require "kitchen/driver/vra"
25
- require "kitchen/provisioner/dummy"
26
- require "kitchen/transport/dummy"
27
- require "kitchen/verifier/dummy"
28
-
29
- describe Kitchen::Driver::Vra do
21
+ RSpec.describe Kitchen::Driver::Vra do
30
22
  let(:logged_output) { StringIO.new }
31
23
  let(:logger) { Logger.new(logged_output) }
32
24
  let(:platform) { Kitchen::Platform.new(name: "fake_platform") }
33
25
  let(:transport) { Kitchen::Transport::Dummy.new }
34
- let(:driver) { Kitchen::Driver::Vra.new(config) }
26
+ let(:driver) { described_class.new(config) }
35
27
 
36
28
  let(:config) do
37
29
  {
38
- base_url: "https://vra.corp.local",
39
- username: "myuser",
40
- password: "mypassword",
41
- domain: "mytenant.corp.com",
42
- project_id: "6ba69375-79d5-42c3-a099-7d32739f71a7",
43
- image_mapping: "VRA-nc-lnx-ce8.4-Docker",
44
- flavor_mapping: "Small",
45
- verify_ssl: true,
46
- use_dns: false,
30
+ base_url: "https://vra.corp.local",
31
+ username: "myuser",
32
+ password: "mypassword",
33
+ domain: "mytenant.corp.com",
34
+ project_id: "6ba69375-79d5-42c3-a099-7d32739f71a7",
35
+ image_mapping: "VRA-nc-lnx-ce8.4-Docker",
36
+ flavor_mapping: "Small",
37
+ verify_ssl: true,
38
+ use_dns: false,
47
39
  deployment_name: "test-instance",
48
- catalog_id: "1536b95d-68fe-3c68-9417-46ed24bee3ef",
40
+ catalog_id: "1536b95d-68fe-3c68-9417-46ed24bee3ef",
49
41
  }
50
42
  end
51
43
 
52
44
  let(:instance) do
53
45
  instance_double(Kitchen::Instance,
54
- logger: logger,
55
- transport: transport,
56
- platform: platform,
57
- to_str: "instance_str")
46
+ logger: logger,
47
+ transport: transport,
48
+ platform: platform,
49
+ to_str: "instance_str")
58
50
  end
59
51
 
60
52
  before do
@@ -71,6 +63,54 @@ describe Kitchen::Driver::Vra do
71
63
  end
72
64
  end
73
65
 
66
+ describe "configuration defaults" do
67
+ let(:config) do
68
+ {
69
+ base_url: "https://vra.corp.local",
70
+ domain: "mytenant.corp.com",
71
+ project_id: "6ba69375-79d5-42c3-a099-7d32739f71a7",
72
+ image_mapping: "VRA-nc-lnx-ce8.4-Docker",
73
+ flavor_mapping: "Small",
74
+ }
75
+ end
76
+
77
+ it "names the deployment after the platform under test" do
78
+ expect(driver[:deployment_name]).to eq("fake_platform")
79
+ end
80
+
81
+ it "connects by IP address rather than by name" do
82
+ expect(driver[:use_dns]).to be false
83
+ end
84
+
85
+ it "verifies the appliance certificate" do
86
+ expect(driver[:verify_ssl]).to be true
87
+ end
88
+
89
+ describe "the private key" do
90
+ let(:id_rsa) { File.expand_path("~/.ssh/id_rsa") }
91
+ let(:id_dsa) { File.expand_path("~/.ssh/id_dsa") }
92
+
93
+ before { allow(File).to receive(:exist?).and_return(false) }
94
+
95
+ it "prefers an RSA key" do
96
+ allow(File).to receive(:exist?).with(id_rsa).and_return(true)
97
+ allow(File).to receive(:exist?).with(id_dsa).and_return(true)
98
+
99
+ expect(driver[:private_key_path]).to eq(id_rsa)
100
+ end
101
+
102
+ it "falls back to a DSA key" do
103
+ allow(File).to receive(:exist?).with(id_dsa).and_return(true)
104
+
105
+ expect(driver[:private_key_path]).to eq(id_dsa)
106
+ end
107
+
108
+ it "is left unset when neither key exists" do
109
+ expect(driver[:private_key_path]).to be_nil
110
+ end
111
+ end
112
+ end
113
+
74
114
  describe "#create" do
75
115
  context "when the server is already created" do
76
116
  let(:state) { { deployment_id: "48959518-6a0d-46e1-8415-3749696b65f4" } }
@@ -81,14 +121,8 @@ describe Kitchen::Driver::Vra do
81
121
  end
82
122
  end
83
123
 
84
- let(:state) { {} }
85
- let(:resource) do
86
- double("server1",
87
- deployment_id: "e8706351-cf4c-4c12-acb7-c90cc683b22c",
88
- name: "test-server",
89
- ip_address: "1.2.3.4",
90
- vm?: true)
91
- end
124
+ let(:state) { {} }
125
+ let(:resource) { vra_resource(deployment_id: "e8706351-cf4c-4c12-acb7-c90cc683b22c") }
92
126
 
93
127
  before do
94
128
  allow(driver).to receive(:request_server).and_return(resource)
@@ -115,22 +149,34 @@ describe Kitchen::Driver::Vra do
115
149
  expect(driver).to receive(:wait_for_server)
116
150
  driver.create(state)
117
151
  end
152
+
153
+ context "when a private key is configured" do
154
+ let(:config) { super().merge(private_key_path: "/home/me/.ssh/id_vra") }
155
+
156
+ it "tells the transport which key to connect with" do
157
+ driver.create(state)
158
+ expect(state[:ssh_key]).to eq("/home/me/.ssh/id_vra")
159
+ end
160
+ end
161
+
162
+ context "when no private key was found" do
163
+ let(:config) { super().merge(private_key_path: nil) }
164
+
165
+ it "leaves the transport to work out how to connect" do
166
+ driver.create(state)
167
+ expect(state).not_to have_key(:ssh_key)
168
+ end
169
+ end
118
170
  end
119
171
 
120
172
  describe "#hostname_for" do
121
- let(:server) do
122
- double("server",
123
- deployment_id: "test_id",
124
- name: "test_hostname",
125
- ip_address: "1.2.3.4",
126
- vm?: true)
127
- end
173
+ let(:server) { vra_resource(deployment_id: "test_id", name: "test_hostname") }
128
174
 
129
175
  context "when use_dns is true and dns_suffix is defined" do
130
176
  let(:config) do
131
177
  {
132
- use_dns: true,
133
- dns_suffix: "my.com",
178
+ use_dns: true,
179
+ dns_suffix: "my.com",
134
180
  }
135
181
  end
136
182
 
@@ -143,20 +189,23 @@ describe Kitchen::Driver::Vra do
143
189
  let(:config) { { use_dns: true } }
144
190
 
145
191
  it "raises an exception if the server name is nil" do
146
- allow(server).to receive(:name).and_return(nil)
147
- expect { driver.hostname_for(server) }.to raise_error(RuntimeError)
192
+ expect { driver.hostname_for(vra_resource(name: nil)) }.to raise_error(RuntimeError)
148
193
  end
149
194
 
150
195
  it "returns the server name" do
151
196
  expect(driver.hostname_for(server)).to eq("test_hostname")
152
197
  end
198
+
199
+ it "does not go looking for an IP address" do
200
+ expect(server).not_to receive(:ip_address)
201
+ driver.hostname_for(server)
202
+ end
153
203
  end
154
204
 
155
205
  context "when use_dns is false" do
156
206
  it "falls back to the server name if no IP address exists" do
157
- allow(server).to receive(:ip_address).and_return(nil)
158
207
  expect(driver).to receive(:warn)
159
- expect(driver.hostname_for(server)).to eq("test_hostname")
208
+ expect(driver.hostname_for(vra_resource(ip_address: nil))).to eq("test-server")
160
209
  end
161
210
 
162
211
  it "returns the IP address if it exists" do
@@ -166,47 +215,32 @@ describe Kitchen::Driver::Vra do
166
215
  end
167
216
 
168
217
  describe "#request_server" do
169
- let(:submitted_request) { double("submitted_request") }
170
- let(:catalog_request) { double("catalog_request") }
171
- let(:resource1) do
172
- double("server1",
173
- id: "e8706351-cf4c-4c12-acb7-c90cc683b22c",
174
- name: "server1",
175
- ip_address: "1.2.3.4",
176
- vm?: true)
177
- end
178
- let(:resource2) do
179
- double("server2",
180
- id: "9e2364cf-7af4-4b85-93fd-1f03ee2ac865",
181
- name: "server2",
182
- ip_address: "4.3.2.1",
183
- vm?: true)
184
- end
185
- let(:resources) { [resource1] }
218
+ let(:deployment) { vra_deployment(id: "74e26af9-2d2f-4889-a472-95dbcedb70b8", resources: resources) }
219
+ let(:catalog_request) { vra_deployment_request(submit: deployment) }
220
+ let(:resource1) { vra_resource(id: "e8706351-cf4c-4c12-acb7-c90cc683b22c", name: "server1") }
221
+ let(:resource2) { vra_resource(id: "9e2364cf-7af4-4b85-93fd-1f03ee2ac865", name: "server2") }
222
+ let(:resources) { [resource1] }
186
223
 
187
224
  before do
188
225
  allow(driver).to receive(:catalog_request).and_return(catalog_request)
189
- allow(catalog_request).to receive(:submit).and_return(submitted_request)
190
- allow(submitted_request).to receive(:id).and_return("74e26af9-2d2f-4889-a472-95dbcedb70b8")
191
- allow(submitted_request).to receive(:resources).and_return(resources)
192
- allow(submitted_request).to receive(:failed?).and_return(false)
193
- allow(driver).to receive(:wait_for_request).with(submitted_request)
226
+ allow(driver).to receive(:wait_for_request).with(deployment)
194
227
  end
195
228
 
196
229
  it "submits a catalog request" do
197
- expect(driver.catalog_request).to receive(:submit).and_return(submitted_request)
230
+ expect(catalog_request).to receive(:submit).and_return(deployment)
198
231
  driver.request_server
199
232
  end
200
233
 
201
234
  it "waits for the request to complete" do
202
- expect(driver).to receive(:wait_for_request).with(submitted_request)
235
+ expect(driver).to receive(:wait_for_request).with(deployment)
203
236
  driver.request_server
204
237
  end
205
238
 
206
239
  it "raises an exception if the request failed" do
207
- allow(submitted_request).to receive(:failed?).and_return(true)
208
- allow(submitted_request).to receive(:completion_details).and_return("it failed")
209
- expect { driver.request_server }.to raise_error(RuntimeError)
240
+ allow(deployment).to receive(:failed?).and_return(true)
241
+ allow(deployment).to receive(:completion_details).and_return("it failed")
242
+
243
+ expect { driver.request_server }.to raise_error(RuntimeError, /it failed/)
210
244
  end
211
245
 
212
246
  describe "getting the server from the request" do
@@ -217,16 +251,26 @@ describe Kitchen::Driver::Vra do
217
251
  end
218
252
 
219
253
  context "when multiple servers are returned" do
254
+ let(:resources) { [resource1, resource2] }
255
+
220
256
  it "raises an exception" do
221
- allow(submitted_request).to receive(:resources).and_return([ resource1, resource2 ])
222
- expect { driver.request_server }.to raise_error(RuntimeError)
257
+ expect { driver.request_server }.to raise_error(RuntimeError, /more than one server/)
223
258
  end
224
259
  end
225
260
 
226
261
  context "when no servers are returned" do
262
+ let(:resources) { [] }
263
+
227
264
  it "raises an exception" do
228
- allow(submitted_request).to receive(:resources).and_return([])
229
- expect { driver.request_server }.to raise_error(RuntimeError)
265
+ expect { driver.request_server }.to raise_error(RuntimeError, /did not create any servers/)
266
+ end
267
+ end
268
+
269
+ context "when the deployment also contains things that are not VMs" do
270
+ let(:resources) { [vra_resource(id: "net-1", vm?: false), resource1] }
271
+
272
+ it "picks out the one VM" do
273
+ expect(driver.request_server).to eq(resource1)
230
274
  end
231
275
  end
232
276
  end
@@ -234,18 +278,25 @@ describe Kitchen::Driver::Vra do
234
278
  it "returns the the single server resource object" do
235
279
  expect(driver.request_server).to eq(resource1)
236
280
  end
281
+
282
+ context "when unique_name is set" do
283
+ let(:config) { super().merge(unique_name: true) }
284
+
285
+ it "reports the name vRA will give the deployment" do
286
+ allow(driver).to receive(:info)
287
+
288
+ driver.request_server
289
+
290
+ expect(driver).to have_received(:info)
291
+ .with("Deployment name is deployment_74e26af9-2d2f-4889-a472-95dbcedb70b8")
292
+ end
293
+ end
237
294
  end
238
295
 
239
296
  describe "#wait_for_server" do
240
297
  let(:connection) { instance.transport.connection(state) }
241
298
  let(:state) { {} }
242
- let(:resource1) do
243
- double("server1",
244
- id: "test_id",
245
- name: "server1",
246
- ip_address: "1.2.3.4",
247
- vm?: true)
248
- end
299
+ let(:resource1) { vra_resource(id: "test_id", name: "server1") }
249
300
 
250
301
  before do
251
302
  allow(transport).to receive(:connection).and_return(connection)
@@ -331,10 +382,10 @@ describe Kitchen::Driver::Vra do
331
382
  end
332
383
 
333
384
  describe "#status" do
334
- let(:deployments) { double("deployments") }
335
- let(:vra_client) { double("vra_client", deployments: deployments) }
385
+ let(:deployments) { instance_double(Vra::Deployments) }
386
+ let(:client) { vra_client(deployments: deployments) }
336
387
 
337
- before { allow(driver).to receive(:vra_client).and_return(vra_client) }
388
+ before { allow(driver).to receive(:vra_client).and_return(client) }
338
389
 
339
390
  it "reports an unknown status when state names no deployment" do
340
391
  expect(driver.status({})).to include(live: nil, state: "unknown")
@@ -348,8 +399,7 @@ describe Kitchen::Driver::Vra do
348
399
  end
349
400
 
350
401
  it "reports a successfully created deployment as live" do
351
- allow(deployments).to receive(:by_id).with("dep-1")
352
- .and_return(double(status: "CREATE_SUCCESSFUL"))
402
+ allow(deployments).to receive(:by_id).with("dep-1").and_return(vra_deployment)
353
403
 
354
404
  expect(driver.status(deployment_id: "dep-1")).to include(
355
405
  live: true, state: "CREATE_SUCCESSFUL", source: "driver",
@@ -358,24 +408,21 @@ describe Kitchen::Driver::Vra do
358
408
  end
359
409
 
360
410
  it "stamps when the check happened" do
361
- allow(deployments).to receive(:by_id)
362
- .and_return(double(status: "CREATE_SUCCESSFUL"))
411
+ allow(deployments).to receive(:by_id).and_return(vra_deployment)
363
412
 
364
413
  expect(driver.status(deployment_id: "dep-1")[:checked_at])
365
414
  .to match(/\A\d{4}-\d{2}-\d{2}T/)
366
415
  end
367
416
 
368
417
  it "reports a deployment still building as not live" do
369
- allow(deployments).to receive(:by_id)
370
- .and_return(double(status: "CREATE_INPROGRESS"))
418
+ allow(deployments).to receive(:by_id).and_return(vra_deployment(status: "CREATE_INPROGRESS"))
371
419
 
372
420
  expect(driver.status(deployment_id: "dep-1"))
373
421
  .to include(live: false, state: "CREATE_INPROGRESS")
374
422
  end
375
423
 
376
424
  it "names a failed deployment rather than reporting a bare false" do
377
- allow(deployments).to receive(:by_id)
378
- .and_return(double(status: "CREATE_FAILED"))
425
+ allow(deployments).to receive(:by_id).and_return(vra_deployment(status: "CREATE_FAILED", failed?: true))
379
426
 
380
427
  expect(driver.status(deployment_id: "dep-1"))
381
428
  .to include(live: false, state: "CREATE_FAILED")
@@ -389,44 +436,36 @@ describe Kitchen::Driver::Vra do
389
436
  end
390
437
 
391
438
  describe "#destroy" do
392
- let(:deployment_id) { "8c1a833a-5844-4100-b58c-9cab3543c958" }
439
+ let(:deployment_id) { "8c1a833a-5844-4100-b58c-9cab3543c958" }
393
440
  let(:state) { { deployment_id: deployment_id } }
394
- let(:vra_client) { double("vra_client") }
395
- let(:deployments) { double("resources") }
396
- let(:destroy_request) { double("destroy_request") }
397
- let(:resource) do
398
- double("server1",
399
- deployment_id: deployment_id,
400
- name: "server1",
401
- ip_address: "5.6.7.8",
402
- vm?: true)
403
- end
441
+ let(:deployments) { instance_double(Vra::Deployments) }
442
+ let(:client) { vra_client(deployments: deployments) }
443
+ let(:destroy_request) { vra_request(id: "6da65982-7c33-4e6e-b346-fdf4bcbf01ab") }
444
+ let(:deployment) { vra_deployment(id: deployment_id, destroy: destroy_request) }
404
445
 
405
446
  before do
406
- allow(driver).to receive(:vra_client).and_return(vra_client)
447
+ allow(driver).to receive(:vra_client).and_return(client)
407
448
  allow(driver).to receive(:wait_for_request).with(destroy_request)
408
- allow(vra_client).to receive(:deployments).and_return(deployments)
409
- allow(deployments).to receive(:by_id).and_return(resource)
410
- allow(resource).to receive(:destroy).and_return(destroy_request)
411
- allow(destroy_request).to receive(:id).and_return("6da65982-7c33-4e6e-b346-fdf4bcbf01ab")
449
+ allow(deployments).to receive(:by_id).and_return(deployment)
412
450
  end
413
451
 
414
452
  context "when the resource is not created" do
415
453
  let(:state) { {} }
454
+
416
455
  it "does not look up the resource if no resource ID exists" do
417
- expect(vra_client.deployments).not_to receive(:by_id)
456
+ expect(deployments).not_to receive(:by_id)
418
457
  driver.destroy(state)
419
458
  end
420
459
  end
421
460
 
422
461
  it "looks up the resource record" do
423
- expect(vra_client.deployments).to receive(:by_id).with(deployment_id).and_return(resource)
462
+ expect(deployments).to receive(:by_id).with(deployment_id).and_return(deployment)
424
463
  driver.destroy(state)
425
464
  end
426
465
 
427
466
  context "when the resource record cannot be found" do
428
467
  it "does not raise an exception" do
429
- allow(vra_client.deployments).to receive(:by_id).with(deployment_id).and_raise(Vra::Exception::NotFound)
468
+ allow(deployments).to receive(:by_id).with(deployment_id).and_raise(Vra::Exception::NotFound)
430
469
  expect { driver.destroy(state) }.not_to raise_error
431
470
  end
432
471
  end
@@ -434,13 +473,13 @@ describe Kitchen::Driver::Vra do
434
473
  describe "creating the destroy request" do
435
474
  context "when the destroy method or server is not found" do
436
475
  it "does not raise an exception" do
437
- allow(resource).to receive(:destroy).and_raise(Vra::Exception::NotFound)
476
+ allow(deployment).to receive(:destroy).and_raise(Vra::Exception::NotFound)
438
477
  expect { driver.destroy(state) }.not_to raise_error
439
478
  end
440
479
  end
441
480
 
442
481
  it "calls #destroy on the server" do
443
- expect(resource).to receive(:destroy).and_return(destroy_request)
482
+ expect(deployment).to receive(:destroy).and_return(destroy_request)
444
483
  driver.destroy(state)
445
484
  end
446
485
  end
@@ -449,81 +488,201 @@ describe Kitchen::Driver::Vra do
449
488
  expect(driver).to receive(:wait_for_request).with(destroy_request)
450
489
  driver.destroy(state)
451
490
  end
452
- end
453
491
 
454
- describe "#catalog_request" do
455
- let(:catalog_request) { double("catalog_request") }
456
- let(:vra_client) { double("vra_client") }
457
- let(:catalog) { double("catalog") }
458
- before do
459
- allow(driver).to receive(:vra_client).and_return(vra_client)
460
- allow(vra_client).to receive(:catalog).and_return(catalog)
461
- allow(catalog).to receive(:request).and_return(catalog_request)
462
- %i{catalog_id= set_parameter}.each do |method|
463
- allow(catalog_request).to receive(method)
492
+ describe "the cached credentials" do
493
+ let(:cache_dir) { Dir.mktmpdir("kitchen-vra-spec") }
494
+
495
+ around { |example| Dir.chdir(cache_dir) { example.run } }
496
+ after { FileUtils.remove_entry(cache_dir) }
497
+
498
+ it "removes the cache file left behind by a previous run" do
499
+ FileUtils.mkdir_p(".kitchen")
500
+ File.write(".kitchen/cached_vra", "cached")
501
+
502
+ driver.destroy(state)
503
+
504
+ expect(File.exist?(".kitchen/cached_vra")).to be false
505
+ end
506
+
507
+ it "does not claim to have removed a cache file that was never written" do
508
+ expect(driver).not_to receive(:info).with(/Removed.*cached/i)
509
+
510
+ driver.destroy(state)
464
511
  end
465
512
  end
513
+ end
514
+
515
+ describe "#catalog_request" do
516
+ let(:deployment_request) { vra_deployment_request }
517
+ let(:catalog) { instance_double(Vra::Catalog, request: deployment_request) }
518
+ let(:client) { vra_client(catalog: catalog) }
519
+
520
+ before { allow(driver).to receive(:vra_client).and_return(client) }
466
521
 
467
522
  it "creates a catalog_request" do
468
- expect(vra_client.catalog).to receive(:request).and_return(catalog_request)
523
+ expect(catalog).to receive(:request).and_return(deployment_request)
469
524
  driver.catalog_request
470
525
  end
471
526
 
472
- context "when option parameters are not supplied" do
527
+ it "asks for the configured image, flavor, project and version" do
528
+ expect(catalog).to receive(:request).with(
529
+ "1536b95d-68fe-3c68-9417-46ed24bee3ef",
530
+ hash_including(
531
+ image_mapping: "VRA-nc-lnx-ce8.4-Docker",
532
+ flavor_mapping: "Small",
533
+ project_id: "6ba69375-79d5-42c3-a099-7d32739f71a7",
534
+ version: nil
535
+ )
536
+ ).and_return(deployment_request)
537
+
538
+ driver.catalog_request
539
+ end
540
+
541
+ it "names the deployment" do
542
+ expect(catalog).to receive(:request)
543
+ .with(anything, hash_including(name: "test-instance"))
544
+ .and_return(deployment_request)
545
+
546
+ driver.catalog_request
547
+ end
548
+
549
+ context "when unique_name is set" do
550
+ let(:config) { super().merge(unique_name: true) }
551
+
552
+ it "leaves vRA to name the deployment after the request" do
553
+ expect(catalog).to receive(:request)
554
+ .with(anything, hash_excluding(:name))
555
+ .and_return(deployment_request)
556
+
557
+ driver.catalog_request
558
+ end
559
+ end
560
+
561
+ context "when the catalog item is named rather than identified" do
473
562
  let(:config) do
474
- {
475
- base_url: "https://vra.corp.local",
476
- username: "myuser",
477
- password: "mypassword",
478
- domain: "domain.corp.com",
479
- verify_ssl: true,
480
- project_id: "6ba69375-79d5-42c3-a099-7d32739f71a7",
481
- image_mapping: "VRA-nc-lnx-ce8.4-Docker",
482
- flavor_mapping: "Small",
483
- catalog_id: "2kabc423-af89-56fc-990a-82fab34ed12",
484
- }
563
+ super().reject { |key, _value| key == :catalog_id }
564
+ .merge(catalog_name: "Ubuntu Server")
485
565
  end
486
566
 
567
+ it "looks the name up and requests the ID it resolves to" do
568
+ allow(catalog).to receive(:fetch_catalog_items).with("Ubuntu Server")
569
+ .and_return([vra_catalog_item(id: "cat-9")])
570
+ expect(catalog).to receive(:request).with("cat-9", anything).and_return(deployment_request)
571
+
572
+ driver.catalog_request
573
+ end
574
+
575
+ context "when the name matches nothing in the catalog" do
576
+ before do
577
+ allow(catalog).to receive(:fetch_catalog_items).and_return([])
578
+ allow(driver).to receive(:error)
579
+ end
580
+
581
+ it "fails rather than requesting an unknown catalog item" do
582
+ expect { driver.catalog_request }
583
+ .to raise_error(Kitchen::InstanceFailure, /without a valid catalog/)
584
+ end
585
+
586
+ it "says which name it could not resolve" do
587
+ expect { driver.catalog_request }.to raise_error(Kitchen::InstanceFailure)
588
+
589
+ expect(driver).to have_received(:error).with(/Ubuntu Server/)
590
+ end
591
+ end
592
+ end
593
+
594
+ context "when neither a catalog ID nor a catalog name is given" do
595
+ let(:config) { super().reject { |key, _value| key == :catalog_id } }
596
+
597
+ it "fails before talking to vRA" do
598
+ expect(catalog).not_to receive(:request)
599
+ expect { driver.catalog_request }.to raise_error(Kitchen::InstanceFailure)
600
+ end
601
+ end
602
+
603
+ context "when option parameters are not supplied" do
487
604
  it "does not attempt to set params on the catalog_request" do
488
- expect(catalog_request).not_to receive(:set_parameters)
605
+ expect(deployment_request).not_to receive(:set_parameters)
489
606
  driver.catalog_request
490
607
  end
491
608
  end
492
609
 
493
610
  context "when extra parameters are set" do
494
611
  let(:config) do
495
- {
496
- base_url: "https://vra.corp.local",
497
- username: "myuser",
498
- password: "mypassword",
499
- domain: "mydomain.corp.com",
500
- verify_ssl: true,
501
- project_id: "6ba69375-79d5-42c3-a099-7d32739f71a7",
502
- image_mapping: "VRA-nc-lnx-ce8.4-Docker",
503
- flavor_mapping: "Small",
504
- catalog_id: "2kabc423-af89-56fc-990a-82fab34ed12",
505
- extra_parameters: { "key1" => { type: "string", value: "value1" },
506
- "key2" => { type: "integer", value: 123 } },
507
- }
612
+ super().merge(
613
+ extra_parameters: {
614
+ "key1" => { type: "string", value: "value1" },
615
+ "key2" => { type: "integer", value: 123 },
616
+ }
617
+ )
508
618
  end
509
619
 
510
620
  it "sets extra parameters" do
511
- expect(catalog_request).to receive(:set_parameters).with("key1", { type: "string", value: "value1" })
512
- expect(catalog_request).to receive(:set_parameters).with("key2", { type: "integer", value: 123 })
621
+ expect(deployment_request).to receive(:set_parameters).with("key1", { type: "string", value: "value1" })
622
+ expect(deployment_request).to receive(:set_parameters).with("key2", { type: "integer", value: 123 })
513
623
  driver.catalog_request
514
624
  end
515
625
  end
516
626
  end
517
627
 
518
628
  describe "#vra_client" do
629
+ let(:client) { vra_client }
630
+
519
631
  it "sets up a client object" do
520
632
  expect(Vra::Client).to receive(:new).with(base_url: config[:base_url],
521
- username: config[:username],
522
- password: config[:password],
523
- domain: config[:domain],
524
- verify_ssl: config[:verify_ssl])
633
+ username: config[:username],
634
+ password: config[:password],
635
+ domain: config[:domain],
636
+ verify_ssl: config[:verify_ssl])
525
637
  driver.vra_client
526
638
  end
639
+
640
+ it "builds the client once and hands the same one back" do
641
+ expect(Vra::Client).to receive(:new).once.and_return(client)
642
+
643
+ expect(driver.vra_client).to eq(client)
644
+ expect(driver.vra_client).to eq(client)
645
+ end
646
+
647
+ context "when cache_credentials is enabled" do
648
+ let(:config) { super().merge(cache_credentials: true) }
649
+
650
+ before { allow(driver).to receive(:c_save) }
651
+
652
+ it "does not prompt for credentials it already has" do
653
+ allow(Vra::Client).to receive(:new).and_return(client)
654
+ expect(driver).not_to receive(:ask)
655
+
656
+ driver.vra_client
657
+ end
658
+ end
659
+
660
+ context "when the client cannot be built" do
661
+ before do
662
+ allow(driver).to receive(:warn)
663
+ allow(driver).to receive(:check_config)
664
+ end
665
+
666
+ it "re-prompts for the credentials and tries once more" do
667
+ attempts = 0
668
+ allow(Vra::Client).to receive(:new) do
669
+ attempts += 1
670
+ raise ArgumentError, "Username and password are required" if attempts == 1
671
+
672
+ client
673
+ end
674
+
675
+ expect(driver.vra_client).to eq(client)
676
+ expect(driver).to have_received(:check_config).with(true)
677
+ end
678
+
679
+ it "raises rather than handing back something that is not a client" do
680
+ allow(Vra::Client).to receive(:new)
681
+ .and_raise(ArgumentError, "Username and password are required")
682
+
683
+ expect { driver.vra_client }.to raise_error(ArgumentError)
684
+ end
685
+ end
527
686
  end
528
687
 
529
688
  describe "#wait_for_request" do
@@ -534,21 +693,42 @@ describe Kitchen::Driver::Vra do
534
693
 
535
694
  context "when the requests completes normally, 3 loops" do
536
695
  it "only refreshes the request 3 times" do
537
- request = double("request")
538
- allow(request).to receive(:status)
539
- allow(request).to receive(:completed?).exactly(3).times.and_return(false, false, true)
540
- expect(request).to receive(:refresh).exactly(3).times
696
+ request = vra_request(status: nil)
697
+ allow(request).to receive(:completed?).and_return(false, false, true)
541
698
 
542
699
  driver.wait_for_request(request)
700
+
701
+ expect(request).to have_received(:refresh).exactly(3).times
543
702
  end
544
703
  end
545
704
 
546
705
  context "when the request is completed on the first loop" do
547
706
  it "only refreshes the request 1 time" do
548
- request = double("request")
549
- allow(request).to receive(:status)
550
- allow(request).to receive(:completed?).once.and_return(true)
551
- expect(request).to receive(:refresh).once
707
+ request = vra_request(status: nil)
708
+
709
+ driver.wait_for_request(request)
710
+
711
+ expect(request).to have_received(:refresh).once
712
+ end
713
+ end
714
+
715
+ it "reports a status once, not on every poll" do
716
+ request = vra_request(status: "IN_PROGRESS")
717
+ allow(request).to receive(:completed?).and_return(false, false, true)
718
+ allow(driver).to receive(:info)
719
+
720
+ driver.wait_for_request(request)
721
+
722
+ expect(driver).to have_received(:info).with("Current request status: IN_PROGRESS").once
723
+ end
724
+
725
+ context "with a slower refresh rate configured" do
726
+ let(:config) { super().merge(request_refresh_rate: 10) }
727
+
728
+ it "waits that long between polls" do
729
+ request = vra_request(status: nil)
730
+ allow(request).to receive(:completed?).and_return(false, true)
731
+ expect(driver).to receive(:sleep).with(10)
552
732
 
553
733
  driver.wait_for_request(request)
554
734
  end
@@ -556,16 +736,19 @@ describe Kitchen::Driver::Vra do
556
736
 
557
737
  context "when the timeout is exceeded" do
558
738
  it "prints a warning and exits" do
559
- request = double("request")
560
739
  allow(Timeout).to receive(:timeout).and_raise(Timeout::Error)
561
- expect { driver.wait_for_request(request) }.to raise_error(Timeout::Error)
740
+ allow(driver).to receive(:error)
741
+
742
+ expect { driver.wait_for_request(vra_request) }.to raise_error(Timeout::Error)
743
+ expect(driver).to have_received(:error).with(/did not complete in 600 seconds/)
562
744
  end
563
745
  end
564
746
 
565
747
  context "when a non-timeout exception is raised" do
566
748
  it "raises the original exception" do
567
- request = double("request")
749
+ request = vra_request
568
750
  allow(request).to receive(:refresh).and_raise(RuntimeError)
751
+
569
752
  expect { driver.wait_for_request(request) }.to raise_error(RuntimeError)
570
753
  end
571
754
  end
@@ -583,6 +766,13 @@ describe Kitchen::Driver::Vra do
583
766
 
584
767
  after { FileUtils.remove_entry(cache_dir) }
585
768
 
769
+ # A second driver reading the cache the first one wrote, which is what the
770
+ # next `kitchen` run is.
771
+ def reader(overrides = {})
772
+ described_class.new(config.merge(overrides).reject { |k, _v| %i{username password}.include?(k) })
773
+ .tap { |driver| allow(driver).to receive(:instance).and_return(instance) }
774
+ end
775
+
586
776
  describe "#c_save" do
587
777
  it "creates the cache file" do
588
778
  driver.c_save
@@ -602,18 +792,29 @@ describe Kitchen::Driver::Vra do
602
792
  driver.c_save
603
793
  expect(File.read(cache_file)).not_to include("mypassword")
604
794
  end
795
+
796
+ it "does not write the username in the clear either" do
797
+ driver.c_save
798
+ expect(File.read(cache_file)).not_to include("myuser")
799
+ end
800
+
801
+ it "reports a cache it could not write rather than failing the run" do
802
+ allow(FileUtils).to receive(:mkdir_p).and_raise(Errno::EACCES)
803
+ expect(driver).to receive(:warn).with(/Unable to save credentials/)
804
+
805
+ expect { driver.c_save }.not_to raise_error
806
+ end
605
807
  end
606
808
 
607
809
  describe "#c_load" do
608
810
  it "restores credentials written by #c_save" do
609
811
  driver.c_save
610
812
 
611
- reader = Kitchen::Driver::Vra.new(config.reject { |k, _v| %i{username password}.include?(k) })
612
- allow(reader).to receive(:instance).and_return(instance)
613
- reader.c_load
813
+ loader = reader
814
+ loader.c_load
614
815
 
615
- expect(reader[:username]).to eq("myuser")
616
- expect(reader[:password]).to eq("mypassword")
816
+ expect(loader[:username]).to eq("myuser")
817
+ expect(loader[:password]).to eq("mypassword")
617
818
  end
618
819
 
619
820
  it "does nothing when no cache file exists" do
@@ -624,9 +825,7 @@ describe Kitchen::Driver::Vra do
624
825
  it "refuses a cache written against a different base_url" do
625
826
  driver.c_save
626
827
 
627
- other = Kitchen::Driver::Vra.new(config.merge(base_url: "https://other.corp.local")
628
- .reject { |k, _v| %i{username password}.include?(k) })
629
- allow(other).to receive(:instance).and_return(instance)
828
+ other = reader(base_url: "https://other.corp.local")
630
829
  expect(other).to receive(:warn).with(/Failed to load cached credentials/)
631
830
  other.c_load
632
831
 
@@ -638,30 +837,97 @@ describe Kitchen::Driver::Vra do
638
837
  driver.c_save
639
838
  File.write(cache_file, File.read(cache_file).succ)
640
839
 
641
- loader = Kitchen::Driver::Vra.new(config.reject { |k, _v| %i{username password}.include?(k) })
642
- allow(loader).to receive(:instance).and_return(instance)
840
+ loader = reader
643
841
  expect(loader).to receive(:warn).with(/Failed to load cached credentials/)
644
842
  loader.c_load
645
843
 
646
844
  expect(loader[:username]).to be_nil
647
845
  end
846
+
847
+ it "refuses a cache written in a layout it does not recognize" do
848
+ FileUtils.mkdir_p(File.dirname(cache_file))
849
+ File.write(cache_file, "v0:whatever")
850
+
851
+ loader = reader
852
+ expect(loader).to receive(:warn).with(/unrecognized cache format/)
853
+ loader.c_load
854
+
855
+ expect(loader[:username]).to be_nil
856
+ end
648
857
  end
649
858
 
650
859
  describe "#check_config" do
651
- it "uses the cache instead of prompting" do
652
- driver.c_save
860
+ it "keeps the credentials it was configured with" do
861
+ allow(driver).to receive(:ask)
862
+
863
+ driver.check_config
864
+
865
+ expect(driver[:username]).to eq("myuser")
866
+ expect(driver[:password]).to eq("mypassword")
867
+ expect(driver).not_to have_received(:ask)
868
+ end
653
869
 
654
- reader = Kitchen::Driver::Vra.new(config.reject { |k, _v| %i{username password}.include?(k) })
655
- allow(reader).to receive(:instance).and_return(instance)
656
- allow(ENV).to receive(:[]).and_call_original
657
- allow(ENV).to receive(:[]).with("VRA_USER_NAME").and_return(nil)
658
- allow(ENV).to receive(:[]).with("VRA_USER_PASSWORD").and_return(nil)
659
- expect(reader).not_to receive(:ask)
870
+ it "asks again when told to, even though it has credentials" do
871
+ allow(driver).to receive(:ask).and_return("newuser", "newpass")
660
872
 
661
- reader.check_config
873
+ driver.check_config(true)
662
874
 
663
- expect(reader[:username]).to eq("myuser")
664
- expect(reader[:password]).to eq("mypassword")
875
+ expect(driver[:username]).to eq("newuser")
876
+ expect(driver[:password]).to eq("newpass")
877
+ end
878
+
879
+ it "does not write a cache file unless asked to" do
880
+ driver.check_config
881
+
882
+ expect(File.exist?(cache_file)).to be false
883
+ end
884
+
885
+ context "when cache_credentials is enabled" do
886
+ let(:config) { super().merge(cache_credentials: true) }
887
+
888
+ it "writes the credentials out for the next run" do
889
+ driver.check_config
890
+
891
+ expect(File.exist?(cache_file)).to be true
892
+ end
893
+ end
894
+
895
+ context "when the config names no credentials" do
896
+ it "reads them from the environment" do
897
+ stub_const("ENV", ENV.to_hash.merge("VRA_USER_NAME" => "envuser",
898
+ "VRA_USER_PASSWORD" => "envpass"))
899
+ loader = reader
900
+ expect(loader).not_to receive(:ask)
901
+
902
+ loader.check_config
903
+
904
+ expect(loader[:username]).to eq("envuser")
905
+ expect(loader[:password]).to eq("envpass")
906
+ end
907
+
908
+ it "uses the cache instead of prompting" do
909
+ driver.c_save
910
+
911
+ stub_const("ENV", ENV.to_hash.reject { |key, _v| key.start_with?("VRA_USER_") })
912
+ loader = reader
913
+ expect(loader).not_to receive(:ask)
914
+
915
+ loader.check_config
916
+
917
+ expect(loader[:username]).to eq("myuser")
918
+ expect(loader[:password]).to eq("mypassword")
919
+ end
920
+
921
+ it "prompts as a last resort" do
922
+ stub_const("ENV", ENV.to_hash.reject { |key, _v| key.start_with?("VRA_USER_") })
923
+ loader = reader
924
+ allow(loader).to receive(:ask).and_return("askeduser", "askedpass")
925
+
926
+ loader.check_config
927
+
928
+ expect(loader[:username]).to eq("askeduser")
929
+ expect(loader[:password]).to eq("askedpass")
930
+ end
665
931
  end
666
932
  end
667
933
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kitchen-vra
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.4.0
4
+ version: 3.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chef Community Tools Team
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-08-24 00:00:00.000000000 Z
11
+ date: 2026-10-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: test-kitchen
@@ -97,13 +97,14 @@ files:
97
97
  - ".gitignore"
98
98
  - ".markdownlint.yaml"
99
99
  - ".release-please-manifest.json"
100
+ - ".rspec"
100
101
  - ".rubocop.yml"
101
102
  - ".yamllint"
102
103
  - ".yardopts"
103
104
  - CHANGELOG.md
104
105
  - CONTRIBUTING.md
105
106
  - Gemfile
106
- - LICENSE.txt
107
+ - LICENSE
107
108
  - README.md
108
109
  - Rakefile
109
110
  - kitchen-vra.gemspec
@@ -112,6 +113,7 @@ files:
112
113
  - release-please-config.json
113
114
  - renovate.json
114
115
  - spec/spec_helper.rb
116
+ - spec/support/vra_api.rb
115
117
  - spec/vra_spec.rb
116
118
  homepage: https://github.com/test-kitchen/kitchen-vra
117
119
  licenses:
File without changes