kitchen-vra 3.3.3 → 3.4.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.
- checksums.yaml +4 -4
- data/.github/workflows/publish.yml +2 -2
- data/.gitignore +3 -1
- data/.release-please-manifest.json +1 -1
- data/.rubocop.yml +7 -1
- data/.yamllint +6 -0
- data/.yardopts +11 -0
- data/CHANGELOG.md +105 -49
- data/CONTRIBUTING.md +84 -0
- data/Gemfile +5 -6
- data/README.md +260 -31
- data/Rakefile +20 -2
- data/kitchen-vra.gemspec +2 -4
- data/lib/kitchen/driver/vra.rb +224 -32
- data/lib/kitchen/driver/vra_version.rb +4 -1
- data/renovate.json +3 -1
- data/spec/vra_spec.rb +156 -1
- metadata +16 -30
- data/.github/dependabot.yml +0 -8
data/lib/kitchen/driver/vra.rb
CHANGED
|
@@ -22,16 +22,48 @@ require "kitchen"
|
|
|
22
22
|
require "highline/import"
|
|
23
23
|
require "openssl" unless defined?(OpenSSL)
|
|
24
24
|
require "base64" unless defined?(Base64)
|
|
25
|
-
require "digest
|
|
25
|
+
require "digest" unless defined?(Digest)
|
|
26
|
+
require "fileutils" unless defined?(FileUtils)
|
|
26
27
|
require "vra"
|
|
28
|
+
require "time" unless defined?(Time.now.iso8601)
|
|
27
29
|
require_relative "vra_version"
|
|
28
30
|
|
|
29
31
|
module Kitchen
|
|
32
|
+
# Test Kitchen's driver plugins.
|
|
30
33
|
module Driver
|
|
34
|
+
# Test Kitchen driver for VMware vRealize Automation (vRA) 8.x.
|
|
35
|
+
#
|
|
36
|
+
# Unlike a cloud driver that creates a machine directly, this driver
|
|
37
|
+
# submits a request against a vRA catalog item and waits for vRA's own
|
|
38
|
+
# automation to build a deployment. The blueprint behind that catalog item
|
|
39
|
+
# decides what gets built; this driver only supplies the image, flavor,
|
|
40
|
+
# and project, then waits for a single VM to come back.
|
|
41
|
+
#
|
|
42
|
+
# The blueprint must return exactly one VM. A blueprint returning several,
|
|
43
|
+
# or none, fails the +create+ rather than guessing which one to test.
|
|
44
|
+
#
|
|
45
|
+
# @see https://www.vmware.com/products/vrealize-automation.html vRealize Automation
|
|
31
46
|
class Vra < Kitchen::Driver::Base # rubocop:disable Metrics/ClassLength
|
|
32
47
|
kitchen_driver_api_version 2
|
|
33
48
|
plugin_version Kitchen::Driver::VRA_VERSION
|
|
34
49
|
|
|
50
|
+
# Deployment statuses vRA reports for a deployment that is up.
|
|
51
|
+
#
|
|
52
|
+
# @return [Array<String>]
|
|
53
|
+
LIVE_STATUSES = %w{CREATE_SUCCESSFUL}.freeze
|
|
54
|
+
|
|
55
|
+
# Location of the credential cache, relative to the working directory.
|
|
56
|
+
CREDENTIALS_CACHE_FILE = ".kitchen/cached_vra"
|
|
57
|
+
|
|
58
|
+
# Cipher used for the credential cache. GCM is authenticated, so a cache
|
|
59
|
+
# written for a different +base_url+, or one that has been altered on
|
|
60
|
+
# disk, fails to decrypt rather than yielding garbage credentials.
|
|
61
|
+
CREDENTIALS_CIPHER = "aes-256-gcm"
|
|
62
|
+
|
|
63
|
+
# Marker for the cache file layout, so a later format change can reject
|
|
64
|
+
# old files instead of misreading them.
|
|
65
|
+
CREDENTIALS_CACHE_VERSION = "v1"
|
|
66
|
+
|
|
35
67
|
default_config :username, nil
|
|
36
68
|
default_config :password, nil
|
|
37
69
|
required_config :base_url
|
|
@@ -68,10 +100,20 @@ module Kitchen
|
|
|
68
100
|
In vRA 8.x, the 'tenant' configuration is no longer relevant for authentication.
|
|
69
101
|
Please use the 'domain' configuration in its place.".dup)
|
|
70
102
|
|
|
103
|
+
# @return [String] the driver's display name in `kitchen list`
|
|
71
104
|
def name
|
|
72
105
|
"vRA"
|
|
73
106
|
end
|
|
74
107
|
|
|
108
|
+
# Resolves the vRA username and password, prompting if necessary.
|
|
109
|
+
#
|
|
110
|
+
# Sources are tried in order: explicit config, then the +VRA_USER_NAME+
|
|
111
|
+
# and +VRA_USER_PASSWORD+ environment variables, then the credential
|
|
112
|
+
# cache, then an interactive prompt.
|
|
113
|
+
#
|
|
114
|
+
# @param force_change [Boolean] prompt even if credentials already
|
|
115
|
+
# resolved, used to re-ask after an authentication failure
|
|
116
|
+
# @return [void]
|
|
75
117
|
def check_config(force_change = false)
|
|
76
118
|
config[:username] = config[:username] || ENV["VRA_USER_NAME"]
|
|
77
119
|
config[:password] = config[:password] || ENV["VRA_USER_PASSWORD"]
|
|
@@ -82,43 +124,104 @@ module Kitchen
|
|
|
82
124
|
c_save if config[:cache_credentials]
|
|
83
125
|
end
|
|
84
126
|
|
|
127
|
+
# Writes the resolved credentials to {CREDENTIALS_CACHE_FILE}.
|
|
128
|
+
#
|
|
129
|
+
# The file is obfuscated rather than secured: the key is derived from
|
|
130
|
+
# +base_url+, which is not a secret, so anyone holding both the file and
|
|
131
|
+
# the kitchen config can recover the credentials. It keeps passwords out
|
|
132
|
+
# of plain sight on disk; it is not a substitute for a secret store.
|
|
133
|
+
#
|
|
134
|
+
# @return [void]
|
|
85
135
|
def c_save
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
file.write(output)
|
|
98
|
-
file.close
|
|
99
|
-
rescue
|
|
100
|
-
puts "Unable to save credentials"
|
|
136
|
+
FileUtils.mkdir_p(File.dirname(CREDENTIALS_CACHE_FILE))
|
|
137
|
+
fields = [config[:username], config[:password]].flat_map { |value| encrypt_credential(value) }
|
|
138
|
+
|
|
139
|
+
File.open(CREDENTIALS_CACHE_FILE, File::WRONLY | File::CREAT | File::TRUNC, 0o600) do |file|
|
|
140
|
+
file.write(([CREDENTIALS_CACHE_VERSION] + fields).join(":"))
|
|
141
|
+
end
|
|
142
|
+
# The mode above only applies when the file is created, so narrow the
|
|
143
|
+
# permissions of a cache left behind by an earlier run as well.
|
|
144
|
+
File.chmod(0o600, CREDENTIALS_CACHE_FILE)
|
|
145
|
+
rescue => e
|
|
146
|
+
warn("Unable to save credentials to #{CREDENTIALS_CACHE_FILE}: #{e.message}")
|
|
101
147
|
end
|
|
102
148
|
|
|
149
|
+
# Reads credentials back from {CREDENTIALS_CACHE_FILE}.
|
|
150
|
+
#
|
|
151
|
+
# A cache that cannot be decrypted -- a different +base_url+, a truncated
|
|
152
|
+
# or edited file, an older layout -- is reported and ignored, leaving the
|
|
153
|
+
# credentials unset so the caller falls through to prompting.
|
|
154
|
+
#
|
|
155
|
+
# @return [void]
|
|
103
156
|
def c_load
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
rescue
|
|
119
|
-
puts "Failed to load cached credentials"
|
|
157
|
+
return unless File.exist?(CREDENTIALS_CACHE_FILE)
|
|
158
|
+
|
|
159
|
+
version, *fields = File.read(CREDENTIALS_CACHE_FILE).strip.split(":")
|
|
160
|
+
raise "unrecognized cache format" unless version == CREDENTIALS_CACHE_VERSION && fields.length == 6
|
|
161
|
+
|
|
162
|
+
# Decrypt both before assigning either, so a partially readable cache
|
|
163
|
+
# cannot leave half the credentials set.
|
|
164
|
+
username = decrypt_credential(*fields[0, 3])
|
|
165
|
+
password = decrypt_credential(*fields[3, 3])
|
|
166
|
+
|
|
167
|
+
config[:username] = username
|
|
168
|
+
config[:password] = password
|
|
169
|
+
rescue => e
|
|
170
|
+
warn("Failed to load cached credentials from #{CREDENTIALS_CACHE_FILE}: #{e.message}")
|
|
120
171
|
end
|
|
121
172
|
|
|
173
|
+
# Encrypts one credential for the cache file.
|
|
174
|
+
#
|
|
175
|
+
# @param value [String] the credential to encrypt
|
|
176
|
+
# @return [Array<String>] Base64-encoded IV, authentication tag, and
|
|
177
|
+
# ciphertext, in that order
|
|
178
|
+
def encrypt_credential(value)
|
|
179
|
+
cipher = OpenSSL::Cipher.new(CREDENTIALS_CIPHER)
|
|
180
|
+
cipher.encrypt
|
|
181
|
+
cipher.key = credentials_key
|
|
182
|
+
iv = cipher.random_iv
|
|
183
|
+
encrypted = cipher.update(value.to_s) + cipher.final
|
|
184
|
+
|
|
185
|
+
[iv, cipher.auth_tag, encrypted].map { |part| Base64.strict_encode64(part) }
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Decrypts one credential from the cache file.
|
|
189
|
+
#
|
|
190
|
+
# @param iv [String] Base64-encoded initialization vector
|
|
191
|
+
# @param auth_tag [String] Base64-encoded GCM authentication tag
|
|
192
|
+
# @param encrypted [String] Base64-encoded ciphertext
|
|
193
|
+
# @return [String] the decrypted credential
|
|
194
|
+
# @raise [OpenSSL::Cipher::CipherError] if the key or tag does not match
|
|
195
|
+
def decrypt_credential(iv, auth_tag, encrypted)
|
|
196
|
+
cipher = OpenSSL::Cipher.new(CREDENTIALS_CIPHER)
|
|
197
|
+
cipher.decrypt
|
|
198
|
+
cipher.key = credentials_key
|
|
199
|
+
cipher.iv = Base64.strict_decode64(iv)
|
|
200
|
+
cipher.auth_tag = Base64.strict_decode64(auth_tag)
|
|
201
|
+
|
|
202
|
+
cipher.update(Base64.strict_decode64(encrypted)) + cipher.final
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Derives the cache key from +base_url+.
|
|
206
|
+
#
|
|
207
|
+
# SHA-256 is used for its digest length: {CREDENTIALS_CIPHER} requires a
|
|
208
|
+
# 32-byte key, which +Digest::SHA256.digest+ returns exactly.
|
|
209
|
+
#
|
|
210
|
+
# @return [String] a 32-byte key
|
|
211
|
+
def credentials_key
|
|
212
|
+
Digest::SHA256.digest(config[:base_url].to_s)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Requests a deployment from vRA and waits until it can be logged into.
|
|
216
|
+
#
|
|
217
|
+
# Returns immediately if the state already names a deployment, so a
|
|
218
|
+
# re-run does not build a second one.
|
|
219
|
+
#
|
|
220
|
+
# @param state [Hash] mutable instance state; gains +deployment_id+,
|
|
221
|
+
# +hostname+, and +ssh_key+
|
|
222
|
+
# @return [void]
|
|
223
|
+
# @raise [RuntimeError] if the vRA request fails or does not yield
|
|
224
|
+
# exactly one VM
|
|
122
225
|
def create(state)
|
|
123
226
|
return if state[:deployment_id]
|
|
124
227
|
|
|
@@ -131,6 +234,15 @@ module Kitchen
|
|
|
131
234
|
info("Server #{server.deployment_id} (#{server.name}) ready.")
|
|
132
235
|
end
|
|
133
236
|
|
|
237
|
+
# Works out the address Test Kitchen should connect to.
|
|
238
|
+
#
|
|
239
|
+
# With +use_dns+ set, the server's name is used, optionally suffixed with
|
|
240
|
+
# +dns_suffix+. Otherwise the IP address is preferred, falling back to
|
|
241
|
+
# the name with a warning when vRA reports no address.
|
|
242
|
+
#
|
|
243
|
+
# @param server [Vra::Resource] the VM vRA built
|
|
244
|
+
# @return [String] a hostname or IP address
|
|
245
|
+
# @raise [RuntimeError] if +use_dns+ is set but vRA returned no name
|
|
134
246
|
def hostname_for(server)
|
|
135
247
|
if config[:use_dns]
|
|
136
248
|
raise "No server name returned for the vRA request" if server.name.nil?
|
|
@@ -147,6 +259,11 @@ module Kitchen
|
|
|
147
259
|
end
|
|
148
260
|
end
|
|
149
261
|
|
|
262
|
+
# Submits the catalog request and waits for vRA to finish building it.
|
|
263
|
+
#
|
|
264
|
+
# @return [Vra::Resource] the single VM the blueprint produced
|
|
265
|
+
# @raise [RuntimeError] if the request failed, or produced zero or more
|
|
266
|
+
# than one VM
|
|
150
267
|
def request_server
|
|
151
268
|
info("Building vRA catalog request...")
|
|
152
269
|
|
|
@@ -167,6 +284,16 @@ module Kitchen
|
|
|
167
284
|
servers.first
|
|
168
285
|
end
|
|
169
286
|
|
|
287
|
+
# Waits for the transport to accept a connection, retrying on failure.
|
|
288
|
+
#
|
|
289
|
+
# Backs off in five-second steps up to thirty seconds between attempts.
|
|
290
|
+
# Once +server_ready_retries+ is exceeded the deployment is destroyed
|
|
291
|
+
# before the error is re-raised, so a machine that never comes up is not
|
|
292
|
+
# left running and billable.
|
|
293
|
+
#
|
|
294
|
+
# @param state [Hash] instance state describing how to connect
|
|
295
|
+
# @param server [Vra::Resource] the VM being waited on
|
|
296
|
+
# @return [void]
|
|
170
297
|
def wait_for_server(state, server)
|
|
171
298
|
info("Server #{server.id} (#{server.name}) created. Waiting until ready...")
|
|
172
299
|
|
|
@@ -193,6 +320,49 @@ module Kitchen
|
|
|
193
320
|
end
|
|
194
321
|
end
|
|
195
322
|
|
|
323
|
+
# Reports what vRA currently thinks of the deployment.
|
|
324
|
+
#
|
|
325
|
+
# @param state [Hash] instance state naming the deployment
|
|
326
|
+
# @return [Hash] a Test Kitchen status hash, or the base implementation's
|
|
327
|
+
# answer when there is no deployment or vRA does not know it
|
|
328
|
+
def status(state)
|
|
329
|
+
return super unless state[:deployment_id]
|
|
330
|
+
|
|
331
|
+
deployment = lookup_deployment(state[:deployment_id])
|
|
332
|
+
return super unless deployment
|
|
333
|
+
|
|
334
|
+
deployment_status = deployment.status.to_s
|
|
335
|
+
{
|
|
336
|
+
live: LIVE_STATUSES.include?(deployment_status),
|
|
337
|
+
state: deployment_status,
|
|
338
|
+
source: "driver",
|
|
339
|
+
resource_id: state[:deployment_id],
|
|
340
|
+
message: "vRA reports the deployment as #{deployment_status}",
|
|
341
|
+
checked_at: Time.now.utc.iso8601,
|
|
342
|
+
}
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
# Looks a deployment up without turning an unreachable vRA into a
|
|
346
|
+
# failure. A deployment that has already been destroyed answers with
|
|
347
|
+
# NotFound, which is a real answer rather than an error.
|
|
348
|
+
#
|
|
349
|
+
# @param deployment_id [String] the vRA deployment ID
|
|
350
|
+
# @return [Vra::Deployment, nil] the deployment, or nil when vRA does not
|
|
351
|
+
# know it or cannot be reached
|
|
352
|
+
def lookup_deployment(deployment_id)
|
|
353
|
+
vra_client.deployments.by_id(deployment_id)
|
|
354
|
+
rescue ::StandardError
|
|
355
|
+
nil
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# Destroys the vRA deployment, if one exists.
|
|
359
|
+
#
|
|
360
|
+
# A deployment that vRA no longer knows about, or that offers no destroy
|
|
361
|
+
# action, is treated as already gone rather than an error. The cached
|
|
362
|
+
# credentials file is removed afterwards.
|
|
363
|
+
#
|
|
364
|
+
# @param state [Hash] instance state naming the deployment
|
|
365
|
+
# @return [void]
|
|
196
366
|
def destroy(state)
|
|
197
367
|
return if state[:deployment_id].nil?
|
|
198
368
|
|
|
@@ -217,6 +387,14 @@ module Kitchen
|
|
|
217
387
|
info("Removed cached file")
|
|
218
388
|
end
|
|
219
389
|
|
|
390
|
+
# Builds the vRA catalog request for the configured blueprint.
|
|
391
|
+
#
|
|
392
|
+
# When +catalog_name+ is given it is resolved to a catalog ID first.
|
|
393
|
+
# A deployment name is only sent when +unique_name+ is false; otherwise
|
|
394
|
+
# vRA is left to name the deployment after its request ID.
|
|
395
|
+
#
|
|
396
|
+
# @return [Vra::CatalogRequest] a request ready to submit
|
|
397
|
+
# @raise [Kitchen::InstanceFailure] if no catalog could be resolved
|
|
220
398
|
def catalog_request # rubocop:disable Metrics/MethodLength
|
|
221
399
|
unless config[:catalog_name].nil?
|
|
222
400
|
info("Fetching Catalog ID by Catalog Name")
|
|
@@ -251,6 +429,12 @@ module Kitchen
|
|
|
251
429
|
catalog_request
|
|
252
430
|
end
|
|
253
431
|
|
|
432
|
+
# The vRA API client, built from the resolved credentials.
|
|
433
|
+
#
|
|
434
|
+
# On any failure the credentials are re-prompted, on the assumption that
|
|
435
|
+
# what failed was authentication.
|
|
436
|
+
#
|
|
437
|
+
# @return [Vra::Client]
|
|
254
438
|
def vra_client
|
|
255
439
|
check_config config[:cache_credentials]
|
|
256
440
|
@client ||= ::Vra::Client.new(
|
|
@@ -264,6 +448,14 @@ module Kitchen
|
|
|
264
448
|
check_config true
|
|
265
449
|
end
|
|
266
450
|
|
|
451
|
+
# Polls a vRA request until it completes or times out.
|
|
452
|
+
#
|
|
453
|
+
# Polls every +request_refresh_rate+ seconds, logging each change of
|
|
454
|
+
# status, and gives up after +request_timeout+ seconds.
|
|
455
|
+
#
|
|
456
|
+
# @param request [Vra::Request] the request to poll
|
|
457
|
+
# @return [void]
|
|
458
|
+
# @raise [Timeout::Error] if the request does not complete in time
|
|
267
459
|
def wait_for_request(request)
|
|
268
460
|
# config = check_config config
|
|
269
461
|
|
|
@@ -18,8 +18,11 @@
|
|
|
18
18
|
# limitations under the License.
|
|
19
19
|
#
|
|
20
20
|
|
|
21
|
+
# Test Kitchen's top-level namespace.
|
|
21
22
|
module Kitchen
|
|
23
|
+
# Test Kitchen's driver plugins.
|
|
22
24
|
module Driver
|
|
23
|
-
|
|
25
|
+
# The version of the kitchen-vra gem.
|
|
26
|
+
VRA_VERSION = "3.4.0"
|
|
24
27
|
end
|
|
25
28
|
end
|
data/renovate.json
CHANGED
data/spec/vra_spec.rb
CHANGED
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
#
|
|
20
20
|
|
|
21
21
|
require "spec_helper"
|
|
22
|
+
require "tmpdir"
|
|
23
|
+
require "fileutils"
|
|
22
24
|
require "kitchen/driver/vra"
|
|
23
25
|
require "kitchen/provisioner/dummy"
|
|
24
26
|
require "kitchen/transport/dummy"
|
|
@@ -328,6 +330,64 @@ describe Kitchen::Driver::Vra do
|
|
|
328
330
|
end
|
|
329
331
|
end
|
|
330
332
|
|
|
333
|
+
describe "#status" do
|
|
334
|
+
let(:deployments) { double("deployments") }
|
|
335
|
+
let(:vra_client) { double("vra_client", deployments: deployments) }
|
|
336
|
+
|
|
337
|
+
before { allow(driver).to receive(:vra_client).and_return(vra_client) }
|
|
338
|
+
|
|
339
|
+
it "reports an unknown status when state names no deployment" do
|
|
340
|
+
expect(driver.status({})).to include(live: nil, state: "unknown")
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
it "reports an unknown status when vRA has never heard of the deployment" do
|
|
344
|
+
allow(deployments).to receive(:by_id).with("gone")
|
|
345
|
+
.and_raise(Vra::Exception::NotFound.new("nope"))
|
|
346
|
+
|
|
347
|
+
expect(driver.status(deployment_id: "gone")).to include(state: "unknown")
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
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"))
|
|
353
|
+
|
|
354
|
+
expect(driver.status(deployment_id: "dep-1")).to include(
|
|
355
|
+
live: true, state: "CREATE_SUCCESSFUL", source: "driver",
|
|
356
|
+
resource_id: "dep-1"
|
|
357
|
+
)
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
it "stamps when the check happened" do
|
|
361
|
+
allow(deployments).to receive(:by_id)
|
|
362
|
+
.and_return(double(status: "CREATE_SUCCESSFUL"))
|
|
363
|
+
|
|
364
|
+
expect(driver.status(deployment_id: "dep-1")[:checked_at])
|
|
365
|
+
.to match(/\A\d{4}-\d{2}-\d{2}T/)
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
it "reports a deployment still building as not live" do
|
|
369
|
+
allow(deployments).to receive(:by_id)
|
|
370
|
+
.and_return(double(status: "CREATE_INPROGRESS"))
|
|
371
|
+
|
|
372
|
+
expect(driver.status(deployment_id: "dep-1"))
|
|
373
|
+
.to include(live: false, state: "CREATE_INPROGRESS")
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
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"))
|
|
379
|
+
|
|
380
|
+
expect(driver.status(deployment_id: "dep-1"))
|
|
381
|
+
.to include(live: false, state: "CREATE_FAILED")
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
it "reports an unknown status when vRA cannot be reached" do
|
|
385
|
+
allow(deployments).to receive(:by_id).and_raise(StandardError.new("boom"))
|
|
386
|
+
|
|
387
|
+
expect(driver.status(deployment_id: "dep-1")).to include(state: "unknown")
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
|
|
331
391
|
describe "#destroy" do
|
|
332
392
|
let(:deployment_id) { "8c1a833a-5844-4100-b58c-9cab3543c958" }
|
|
333
393
|
let(:state) { { deployment_id: deployment_id } }
|
|
@@ -447,7 +507,7 @@ describe Kitchen::Driver::Vra do
|
|
|
447
507
|
}
|
|
448
508
|
end
|
|
449
509
|
|
|
450
|
-
it "sets extra
|
|
510
|
+
it "sets extra parameters" do
|
|
451
511
|
expect(catalog_request).to receive(:set_parameters).with("key1", { type: "string", value: "value1" })
|
|
452
512
|
expect(catalog_request).to receive(:set_parameters).with("key2", { type: "integer", value: 123 })
|
|
453
513
|
driver.catalog_request
|
|
@@ -510,4 +570,99 @@ describe Kitchen::Driver::Vra do
|
|
|
510
570
|
end
|
|
511
571
|
end
|
|
512
572
|
end
|
|
573
|
+
|
|
574
|
+
describe "credential caching" do
|
|
575
|
+
let(:cache_dir) { Dir.mktmpdir("kitchen-vra-spec") }
|
|
576
|
+
let(:cache_file) { File.join(cache_dir, ".kitchen", "cached_vra") }
|
|
577
|
+
|
|
578
|
+
# c_save/c_load address the cache by a path relative to the working
|
|
579
|
+
# directory, so each example runs inside a throwaway directory.
|
|
580
|
+
around do |example|
|
|
581
|
+
Dir.chdir(cache_dir) { example.run }
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
after { FileUtils.remove_entry(cache_dir) }
|
|
585
|
+
|
|
586
|
+
describe "#c_save" do
|
|
587
|
+
it "creates the cache file" do
|
|
588
|
+
driver.c_save
|
|
589
|
+
expect(File.exist?(cache_file)).to be true
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
it "creates the .kitchen directory when it does not exist" do
|
|
593
|
+
expect { driver.c_save }.to change { Dir.exist?(File.join(cache_dir, ".kitchen")) }.from(false).to(true)
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
it "writes the file readable only by its owner" do
|
|
597
|
+
driver.c_save
|
|
598
|
+
expect(File.stat(cache_file).mode & 0o777).to eq(0o600)
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
it "does not write the password in the clear" do
|
|
602
|
+
driver.c_save
|
|
603
|
+
expect(File.read(cache_file)).not_to include("mypassword")
|
|
604
|
+
end
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
describe "#c_load" do
|
|
608
|
+
it "restores credentials written by #c_save" do
|
|
609
|
+
driver.c_save
|
|
610
|
+
|
|
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
|
|
614
|
+
|
|
615
|
+
expect(reader[:username]).to eq("myuser")
|
|
616
|
+
expect(reader[:password]).to eq("mypassword")
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
it "does nothing when no cache file exists" do
|
|
620
|
+
driver.c_load
|
|
621
|
+
expect(driver[:username]).to eq("myuser")
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
it "refuses a cache written against a different base_url" do
|
|
625
|
+
driver.c_save
|
|
626
|
+
|
|
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)
|
|
630
|
+
expect(other).to receive(:warn).with(/Failed to load cached credentials/)
|
|
631
|
+
other.c_load
|
|
632
|
+
|
|
633
|
+
expect(other[:username]).to be_nil
|
|
634
|
+
expect(other[:password]).to be_nil
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
it "refuses a corrupted cache rather than returning garbage" do
|
|
638
|
+
driver.c_save
|
|
639
|
+
File.write(cache_file, File.read(cache_file).succ)
|
|
640
|
+
|
|
641
|
+
loader = Kitchen::Driver::Vra.new(config.reject { |k, _v| %i{username password}.include?(k) })
|
|
642
|
+
allow(loader).to receive(:instance).and_return(instance)
|
|
643
|
+
expect(loader).to receive(:warn).with(/Failed to load cached credentials/)
|
|
644
|
+
loader.c_load
|
|
645
|
+
|
|
646
|
+
expect(loader[:username]).to be_nil
|
|
647
|
+
end
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
describe "#check_config" do
|
|
651
|
+
it "uses the cache instead of prompting" do
|
|
652
|
+
driver.c_save
|
|
653
|
+
|
|
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)
|
|
660
|
+
|
|
661
|
+
reader.check_config
|
|
662
|
+
|
|
663
|
+
expect(reader[:username]).to eq("myuser")
|
|
664
|
+
expect(reader[:password]).to eq("mypassword")
|
|
665
|
+
end
|
|
666
|
+
end
|
|
667
|
+
end
|
|
513
668
|
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
|
+
version: 3.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
|
-
- Chef
|
|
7
|
+
- Chef Community Tools Team
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-08-24 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: test-kitchen
|
|
@@ -16,14 +16,20 @@ dependencies:
|
|
|
16
16
|
requirements:
|
|
17
17
|
- - ">="
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '0'
|
|
19
|
+
version: '3.0'
|
|
20
|
+
- - "<"
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: '5'
|
|
20
23
|
type: :runtime
|
|
21
24
|
prerelease: false
|
|
22
25
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
26
|
requirements:
|
|
24
27
|
- - ">="
|
|
25
28
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '0'
|
|
29
|
+
version: '3.0'
|
|
30
|
+
- - "<"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '5'
|
|
27
33
|
- !ruby/object:Gem::Dependency
|
|
28
34
|
name: vmware-vra
|
|
29
35
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -58,26 +64,6 @@ dependencies:
|
|
|
58
64
|
- - ">="
|
|
59
65
|
- !ruby/object:Gem::Version
|
|
60
66
|
version: '0'
|
|
61
|
-
- !ruby/object:Gem::Dependency
|
|
62
|
-
name: rack
|
|
63
|
-
requirement: !ruby/object:Gem::Requirement
|
|
64
|
-
requirements:
|
|
65
|
-
- - ">="
|
|
66
|
-
- !ruby/object:Gem::Version
|
|
67
|
-
version: '1.6'
|
|
68
|
-
- - "<"
|
|
69
|
-
- !ruby/object:Gem::Version
|
|
70
|
-
version: '4.0'
|
|
71
|
-
type: :runtime
|
|
72
|
-
prerelease: false
|
|
73
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
74
|
-
requirements:
|
|
75
|
-
- - ">="
|
|
76
|
-
- !ruby/object:Gem::Version
|
|
77
|
-
version: '1.6'
|
|
78
|
-
- - "<"
|
|
79
|
-
- !ruby/object:Gem::Version
|
|
80
|
-
version: '4.0'
|
|
81
67
|
- !ruby/object:Gem::Dependency
|
|
82
68
|
name: ffi-yajl
|
|
83
69
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -106,14 +92,16 @@ extensions: []
|
|
|
106
92
|
extra_rdoc_files: []
|
|
107
93
|
files:
|
|
108
94
|
- ".github/CODEOWNERS"
|
|
109
|
-
- ".github/dependabot.yml"
|
|
110
95
|
- ".github/workflows/linters.yml"
|
|
111
96
|
- ".github/workflows/publish.yml"
|
|
112
97
|
- ".gitignore"
|
|
113
98
|
- ".markdownlint.yaml"
|
|
114
99
|
- ".release-please-manifest.json"
|
|
115
100
|
- ".rubocop.yml"
|
|
101
|
+
- ".yamllint"
|
|
102
|
+
- ".yardopts"
|
|
116
103
|
- CHANGELOG.md
|
|
104
|
+
- CONTRIBUTING.md
|
|
117
105
|
- Gemfile
|
|
118
106
|
- LICENSE.txt
|
|
119
107
|
- README.md
|
|
@@ -144,10 +132,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
144
132
|
- !ruby/object:Gem::Version
|
|
145
133
|
version: '0'
|
|
146
134
|
requirements: []
|
|
147
|
-
rubygems_version: 3.
|
|
135
|
+
rubygems_version: 3.5.9
|
|
148
136
|
signing_key:
|
|
149
137
|
specification_version: 4
|
|
150
138
|
summary: A Test Kitchen driver for VMware vRealize Automation (vRA)
|
|
151
|
-
test_files:
|
|
152
|
-
- spec/spec_helper.rb
|
|
153
|
-
- spec/vra_spec.rb
|
|
139
|
+
test_files: []
|