kitchen-ec2 3.22.9 → 3.23.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 +4 -4
- data/lib/kitchen/driver/aws/instance_generator.rb +62 -24
- data/lib/kitchen/driver/aws/standard_platform/debian.rb +23 -0
- data/lib/kitchen/driver/aws/standard_platform/macos.rb +22 -3
- data/lib/kitchen/driver/aws/standard_platform.rb +4 -3
- data/lib/kitchen/driver/ec2.rb +322 -32
- data/lib/kitchen/driver/ec2_version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4b31b976c77faa84d3828219b396b07d52790a806d0b2a0fc72b95f8dabbe548
|
|
4
|
+
data.tar.gz: 52cb6998a147c6ca43e638f80a16f3b461f3ed25d7999695c42197a1900c34e1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0d57aa3da2611170b0e00e140e9d671c1a7585048020da316c1194d238e9c3fd41ef4ffc62ed1e4375d1862983220cd6c67e48ca25f98777adc677e0d6987f87
|
|
7
|
+
data.tar.gz: 68faeb3e26106d8df47a4896b7d6f1ef18c3c30cbbfd2d12e1cac6dcd2dddacd208f60d92493647ec19fba347e50f14691c279ad33999bf109a5ca949817e7cc
|
|
@@ -88,38 +88,36 @@ module Kitchen
|
|
|
88
88
|
|
|
89
89
|
if config[:security_group_ids].nil? && config[:security_group_filter]
|
|
90
90
|
# => Grab the VPC in the case a Subnet ID rather than Filter was set
|
|
91
|
-
vpc_id ||= client
|
|
91
|
+
vpc_id ||= security_group_search_vpc_id(client)
|
|
92
92
|
security_groups = []
|
|
93
93
|
filters = [config[:security_group_filter]].flatten
|
|
94
94
|
filters.each do |sg_filter|
|
|
95
|
-
|
|
95
|
+
# Built up rather than assigned, so that a filter carrying both a
|
|
96
|
+
# name and a tag searches on both. Assigning meant the tag
|
|
97
|
+
# replaced the name outright, silently widening the search to
|
|
98
|
+
# whatever else carried that tag.
|
|
99
|
+
criteria = []
|
|
96
100
|
if sg_filter[:name]
|
|
97
|
-
|
|
98
|
-
{
|
|
99
|
-
name: "group-name",
|
|
100
|
-
values: [sg_filter[:name]],
|
|
101
|
-
},
|
|
102
|
-
{
|
|
103
|
-
name: "vpc-id",
|
|
104
|
-
values: [vpc_id],
|
|
105
|
-
},
|
|
106
|
-
]
|
|
101
|
+
criteria << { name: "group-name", values: [sg_filter[:name]] }
|
|
107
102
|
end
|
|
108
|
-
|
|
109
103
|
if sg_filter[:tag]
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
]
|
|
104
|
+
criteria << { name: "tag:#{sg_filter[:tag]}", values: [sg_filter[:value]] }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Refused rather than sent: describe_security_groups with no
|
|
108
|
+
# filters returns every security group in the region, and all of
|
|
109
|
+
# them were then attached to the instance.
|
|
110
|
+
if criteria.empty?
|
|
111
|
+
raise "A security_group_filter needs a `name` or a `tag`, but " \
|
|
112
|
+
"#{sg_filter.inspect} has neither."
|
|
120
113
|
end
|
|
121
114
|
|
|
122
|
-
|
|
115
|
+
# Only when a VPC is known. A name or tag has already narrowed the
|
|
116
|
+
# search, so leaving this off is not the unfiltered request the
|
|
117
|
+
# check above exists to prevent.
|
|
118
|
+
criteria << { name: "vpc-id", values: [vpc_id] } if vpc_id
|
|
119
|
+
|
|
120
|
+
security_group = client.describe_security_groups(filters: criteria).security_groups
|
|
123
121
|
|
|
124
122
|
if security_group.any?
|
|
125
123
|
security_group.each { |sg| security_groups.push(sg.group_id) }
|
|
@@ -248,6 +246,46 @@ module Kitchen
|
|
|
248
246
|
i
|
|
249
247
|
end
|
|
250
248
|
|
|
249
|
+
# The VPC to look for security groups in.
|
|
250
|
+
#
|
|
251
|
+
# Security group names are unique only within a VPC, so the search is
|
|
252
|
+
# scoped to the VPC the instance will launch into. Which VPC that is
|
|
253
|
+
# depends on how the subnet was chosen:
|
|
254
|
+
#
|
|
255
|
+
# - a named `subnet_id` decides it, so the subnet is described for it
|
|
256
|
+
# - no subnet at all means EC2 launches into the account's default VPC,
|
|
257
|
+
# so that is what gets searched
|
|
258
|
+
#
|
|
259
|
+
# The second case used to describe a subnet with an ID of nil and then
|
|
260
|
+
# read `vpc_id` off the empty result, so a `security_group_filter` on
|
|
261
|
+
# its own -- a documented combination, and the natural one in a default
|
|
262
|
+
# VPC account -- died with `undefined method 'vpc_id' for nil` before
|
|
263
|
+
# anything was launched.
|
|
264
|
+
#
|
|
265
|
+
# An account with no default VPC returns nil, and the caller then
|
|
266
|
+
# searches on the name or tag alone.
|
|
267
|
+
#
|
|
268
|
+
# @param client [Aws::EC2::Client] the client to query with
|
|
269
|
+
# @return [String, nil] the VPC ID, or nil when there is no default VPC
|
|
270
|
+
# @raise [RuntimeError] when a named subnet does not exist, which would
|
|
271
|
+
# otherwise surface as the same nil dereference
|
|
272
|
+
def security_group_search_vpc_id(client)
|
|
273
|
+
unless config[:subnet_id]
|
|
274
|
+
default_vpc = client.describe_vpcs(
|
|
275
|
+
filters: [{ name: "isDefault", values: %w{true} }]
|
|
276
|
+
).vpcs.first
|
|
277
|
+
|
|
278
|
+
return default_vpc&.vpc_id
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
subnet = client.describe_subnets(subnet_ids: [config[:subnet_id]]).subnets.first
|
|
282
|
+
unless subnet
|
|
283
|
+
raise "Subnet #{config[:subnet_id]} not found while resolving security_group_filter."
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
subnet.vpc_id
|
|
287
|
+
end
|
|
288
|
+
|
|
251
289
|
# The user data script, base64 encoded as EC2 requires.
|
|
252
290
|
#
|
|
253
291
|
# The configured value is treated as a file path when it names an
|
|
@@ -98,6 +98,29 @@ module Kitchen
|
|
|
98
98
|
search
|
|
99
99
|
end
|
|
100
100
|
|
|
101
|
+
# Sort images newest release first, keeping backports images last.
|
|
102
|
+
#
|
|
103
|
+
# Debian publishes a backports image alongside each release, from the
|
|
104
|
+
# same account and under the same "debian-<release>-" prefix,
|
|
105
|
+
# differing only by the word "backports" in the name:
|
|
106
|
+
#
|
|
107
|
+
# debian-12-backports-amd64-20260821-2577
|
|
108
|
+
# debian-12-amd64-20260821-2577
|
|
109
|
+
#
|
|
110
|
+
# It runs the backports kernel rather than the release's own -- 6.12
|
|
111
|
+
# against 6.1 for Debian 12 -- and is often published minutes after
|
|
112
|
+
# its plain counterpart, so a tie broken on creation date handed
|
|
113
|
+
# every Debian platform the backports image.
|
|
114
|
+
#
|
|
115
|
+
# This is a preference rather than a filter, so a release with only
|
|
116
|
+
# backports images published is still selectable.
|
|
117
|
+
#
|
|
118
|
+
# @param images [Array<Aws::EC2::Image>] the images to sort
|
|
119
|
+
# @return [Array<Aws::EC2::Image>] the images, newest release first
|
|
120
|
+
def sort_by_version(images)
|
|
121
|
+
prefer(super) { |image| !image.name.include?("backports") }
|
|
122
|
+
end
|
|
123
|
+
|
|
101
124
|
# Detect this platform from an EC2 image.
|
|
102
125
|
#
|
|
103
126
|
# Matching is done on the image name, which is the only reliable signal
|
|
@@ -34,6 +34,24 @@ module Kitchen
|
|
|
34
34
|
"ec2-user"
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
# The AWS account Amazon publishes its macOS AMIs under.
|
|
38
|
+
#
|
|
39
|
+
# @return [String]
|
|
40
|
+
MACOS_OWNER_ID = "628277914472".freeze
|
|
41
|
+
|
|
42
|
+
# EC2's architecture values for Mac instances, keyed by the name the
|
|
43
|
+
# platform string uses.
|
|
44
|
+
#
|
|
45
|
+
# EC2 reports a Mac image's architecture as "x86_64_mac" or
|
|
46
|
+
# "arm64_mac", never the bare "x86_64" or "arm64" every other
|
|
47
|
+
# platform uses, so an unmapped value matches nothing at all.
|
|
48
|
+
#
|
|
49
|
+
# @return [Hash{String => String}]
|
|
50
|
+
MAC_ARCHITECTURES = {
|
|
51
|
+
"arm64" => "arm64_mac",
|
|
52
|
+
"x86_64" => "x86_64_mac",
|
|
53
|
+
}.freeze
|
|
54
|
+
|
|
37
55
|
# EC2 image filters that select Amazon's macOS AMIs, which run only on dedicated Mac hosts.
|
|
38
56
|
#
|
|
39
57
|
# A filter is added for {StandardPlatform#architecture} only when one was
|
|
@@ -44,11 +62,12 @@ module Kitchen
|
|
|
44
62
|
# @see StandardPlatform#find_image
|
|
45
63
|
def image_search
|
|
46
64
|
search = {
|
|
47
|
-
"owner-id" =>
|
|
65
|
+
"owner-id" => MACOS_OWNER_ID,
|
|
48
66
|
"name" => version ? "amzn-ec2-macos-#{version}*" : "amzn-ec2-macos-*",
|
|
49
67
|
}
|
|
50
|
-
|
|
51
|
-
|
|
68
|
+
if architecture
|
|
69
|
+
search["architecture"] = MAC_ARCHITECTURES.fetch(architecture, architecture)
|
|
70
|
+
end
|
|
52
71
|
search
|
|
53
72
|
end
|
|
54
73
|
|
|
@@ -176,7 +176,7 @@ module Kitchen
|
|
|
176
176
|
# Detect platform from an image.
|
|
177
177
|
#
|
|
178
178
|
# @param driver [Kitchen::Driver::Ec2] The driver.
|
|
179
|
-
# @param image [Aws::
|
|
179
|
+
# @param image [Aws::EC2::Image] The EC2 Image object.
|
|
180
180
|
#
|
|
181
181
|
# @return [Kitchen::Driver::Aws::StandardPlatform]
|
|
182
182
|
#
|
|
@@ -223,9 +223,9 @@ module Kitchen
|
|
|
223
223
|
# Used by the default find_image. The default version calls platform_from_image()
|
|
224
224
|
# on each image, and interprets the versions as floats (7 < 7.1 < 8).
|
|
225
225
|
#
|
|
226
|
-
# @param images [Array
|
|
226
|
+
# @param images [Array<Aws::EC2::Image>] The list of images to sort
|
|
227
227
|
#
|
|
228
|
-
# @return [Array
|
|
228
|
+
# @return [Array<Aws::EC2::Image>] A sorted list.
|
|
229
229
|
#
|
|
230
230
|
def sort_by_version(images)
|
|
231
231
|
# 7.1 -> [ img1, img2, img3 ]
|
|
@@ -246,6 +246,7 @@ module Kitchen
|
|
|
246
246
|
# preferences.
|
|
247
247
|
#
|
|
248
248
|
# @param images [Array<Aws::EC2::Image>] the images to reorder
|
|
249
|
+
# @param block [Proc] the predicate each image is partitioned by
|
|
249
250
|
# @yieldparam image [Aws::EC2::Image] an image to test
|
|
250
251
|
# @yieldreturn [Boolean] true when the image is preferred
|
|
251
252
|
# @return [Array<Aws::EC2::Image>] preferred images first
|
data/lib/kitchen/driver/ec2.rb
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
require "fileutils" unless defined?(FileUtils)
|
|
20
20
|
require "sshkey" unless defined?(SSHKey)
|
|
21
21
|
require "benchmark" unless defined?(Benchmark)
|
|
22
|
+
require "open3" unless defined?(Open3)
|
|
22
23
|
require "json" unless defined?(JSON)
|
|
23
24
|
require "kitchen"
|
|
24
25
|
require_relative "ec2_version"
|
|
@@ -229,6 +230,19 @@ module Kitchen
|
|
|
229
230
|
end
|
|
230
231
|
end
|
|
231
232
|
|
|
233
|
+
# A placement group is named either by ID or by name, never both: EC2
|
|
234
|
+
# rejects a RunInstances call carrying the pair. The payload generator
|
|
235
|
+
# therefore only applies each one when the other is absent, which means
|
|
236
|
+
# setting both silently drops both and the instance launches in no
|
|
237
|
+
# placement group at all, with nothing in the output to say so.
|
|
238
|
+
validations[:placement] = lambda do |_attr, val, _driver|
|
|
239
|
+
if val.is_a?(Hash) && val[:group_id] && val[:group_name]
|
|
240
|
+
warn "Cannot set both 'group_id' and 'group_name' under 'placement'. " \
|
|
241
|
+
"A placement group is identified by one or the other, so please set only one."
|
|
242
|
+
exit!
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
232
246
|
# empty keys cause failures when tagging and they make no sense
|
|
233
247
|
validations[:tags] = lambda do |_attr, val, _driver|
|
|
234
248
|
# if someone puts the tags each on their own line it's an array not a hash
|
|
@@ -242,6 +256,72 @@ module Kitchen
|
|
|
242
256
|
|
|
243
257
|
# Create an EC2 instance and wait until it can be connected to.
|
|
244
258
|
#
|
|
259
|
+
# Configuration that cannot possibly work is rejected first, before
|
|
260
|
+
# anything billable is launched; {#launch_instance} does the real work.
|
|
261
|
+
#
|
|
262
|
+
# @param state [Hash] the instance state, updated in place with
|
|
263
|
+
# `:server_id`, `:hostname` and any auto-created credentials
|
|
264
|
+
# @return [void]
|
|
265
|
+
# @raise [Kitchen::UserError] when the platform is misconfigured such
|
|
266
|
+
# that the run could not succeed
|
|
267
|
+
def create(state)
|
|
268
|
+
return if state[:server_id]
|
|
269
|
+
|
|
270
|
+
# Deliberately checked here rather than inside #launch_instance: that
|
|
271
|
+
# method rewrites every exception it sees into an "is this AMI available
|
|
272
|
+
# in this region" message, which would bury this one.
|
|
273
|
+
assert_powershell_shell_type!
|
|
274
|
+
|
|
275
|
+
launch_instance(state)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Refuse to launch a Windows instance that would be driven with a Bourne
|
|
279
|
+
# shell type.
|
|
280
|
+
#
|
|
281
|
+
# Test Kitchen infers `shell_type` from the platform *name*, not from
|
|
282
|
+
# `os_type`, so a Windows platform named something that does not begin
|
|
283
|
+
# with "windows" silently ends up as a Bourne host. The provisioner then
|
|
284
|
+
# generates Bourne commands, WinRM executes them in PowerShell, and the
|
|
285
|
+
# run fails deep into converge with an error that points nowhere near the
|
|
286
|
+
# real cause -- typically a `New-Item` complaint that the sandbox
|
|
287
|
+
# directory already exists.
|
|
288
|
+
#
|
|
289
|
+
# Failing here costs the user nothing; letting it through costs them a
|
|
290
|
+
# billable instance and a confusing converge failure.
|
|
291
|
+
#
|
|
292
|
+
# @raise [Kitchen::UserError] when the platform is Windows but its shell
|
|
293
|
+
# type is not PowerShell
|
|
294
|
+
# @return [void]
|
|
295
|
+
# @see https://github.com/test-kitchen/kitchen-ec2/issues/621
|
|
296
|
+
def assert_powershell_shell_type!
|
|
297
|
+
return unless windows_os?
|
|
298
|
+
return if powershell_shell?
|
|
299
|
+
|
|
300
|
+
shell_type = instance.platform.respond_to?(:shell_type) ? instance.platform.shell_type : nil
|
|
301
|
+
|
|
302
|
+
raise Kitchen::UserError, <<~MESSAGE
|
|
303
|
+
Platform '#{instance.platform.name}' sets os_type 'windows' but its shell_type is '#{shell_type}'.
|
|
304
|
+
|
|
305
|
+
Test Kitchen infers shell_type from the platform name, so a Windows platform
|
|
306
|
+
whose name does not begin with 'windows' is treated as a Bourne shell host.
|
|
307
|
+
Provisioner commands would be generated as Bourne syntax and then executed by
|
|
308
|
+
PowerShell over WinRM, failing during converge with an unrelated error such as
|
|
309
|
+
'Cannot create ... because a file or directory with the same name already exists'.
|
|
310
|
+
|
|
311
|
+
Set shell_type explicitly on the platform:
|
|
312
|
+
|
|
313
|
+
platforms:
|
|
314
|
+
- name: #{instance.platform.name}
|
|
315
|
+
os_type: windows
|
|
316
|
+
shell_type: powershell
|
|
317
|
+
|
|
318
|
+
Renaming the platform to start with 'windows' also works, as Test Kitchen then
|
|
319
|
+
infers both os_type and shell_type from the name.
|
|
320
|
+
MESSAGE
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# Request the instance and wait until it can be connected to.
|
|
324
|
+
#
|
|
245
325
|
# Auto-creates a security group and key pair when none were configured,
|
|
246
326
|
# allocates a dedicated host if `tenancy: host` requires one, requests
|
|
247
327
|
# either an on-demand or a spot instance, then waits for the instance to
|
|
@@ -256,9 +336,7 @@ module Kitchen
|
|
|
256
336
|
# @return [void]
|
|
257
337
|
# @raise [Kitchen::ActionFailed] wrapping whatever went wrong, after
|
|
258
338
|
# cleaning up
|
|
259
|
-
def
|
|
260
|
-
return if state[:server_id]
|
|
261
|
-
|
|
339
|
+
def launch_instance(state)
|
|
262
340
|
update_username(state)
|
|
263
341
|
|
|
264
342
|
info(Kitchen::Util.outdent!(<<-END)) unless config[:skip_cost_warning]
|
|
@@ -295,7 +373,8 @@ module Kitchen
|
|
|
295
373
|
exit!
|
|
296
374
|
end
|
|
297
375
|
|
|
298
|
-
|
|
376
|
+
# Remembered so that destroy releases this host and no other.
|
|
377
|
+
state[:allocated_host_id] = allocate_host unless host_available?
|
|
299
378
|
|
|
300
379
|
info("Auto placement on one dedicated host out of: #{hosts_with_capacity.map(&:host_id).join(", ")}")
|
|
301
380
|
end
|
|
@@ -374,7 +453,10 @@ module Kitchen
|
|
|
374
453
|
# @see https://github.com/test-kitchen/kitchen-ec2/issues/606
|
|
375
454
|
def create_failure_message(error)
|
|
376
455
|
message = "Failed to create the EC2 instance: #{error.class}: #{error.message}"
|
|
377
|
-
|
|
456
|
+
# The hint names the image, so it has nothing to say when there is no
|
|
457
|
+
# image to name. Without this it rendered as "Check that image exists"
|
|
458
|
+
# on exactly the failure where no image was ever resolved.
|
|
459
|
+
return message unless config[:image_id] && image_related_error?(error)
|
|
378
460
|
|
|
379
461
|
"#{message} Check that image #{config[:image_id]} exists and is available " \
|
|
380
462
|
"in region #{config[:region]}."
|
|
@@ -382,14 +464,19 @@ module Kitchen
|
|
|
382
464
|
|
|
383
465
|
# Whether a failure is about the AMI rather than something else entirely.
|
|
384
466
|
#
|
|
385
|
-
# EC2 reports every image problem with a code beginning "InvalidAMI"
|
|
386
|
-
# message check
|
|
387
|
-
# plain strings with no code attached
|
|
467
|
+
# EC2 reports every image problem with a code beginning "InvalidAMI". The
|
|
468
|
+
# message check is for errors raised by the driver itself, which are
|
|
469
|
+
# plain strings with no code attached -- but it only applies to those,
|
|
470
|
+
# because plenty of AWS errors merely *mention* the AMI while being about
|
|
471
|
+
# something else. The clearest example is an instance type whose
|
|
472
|
+
# architecture does not match the image's: its message names the AMI
|
|
473
|
+
# twice, and the old check duly told the user to go and check whether an
|
|
474
|
+
# image that exists, exists.
|
|
388
475
|
#
|
|
389
476
|
# @param error [Exception] the underlying failure
|
|
390
477
|
# @return [Boolean]
|
|
391
478
|
def image_related_error?(error)
|
|
392
|
-
return
|
|
479
|
+
return error.code.to_s.start_with?("InvalidAMI") if error.respond_to?(:code)
|
|
393
480
|
|
|
394
481
|
error.message.to_s.match?(/\bAMI\b/i)
|
|
395
482
|
end
|
|
@@ -414,10 +501,17 @@ module Kitchen
|
|
|
414
501
|
warn("Received #{e}, instance was probably already destroyed. Ignoring")
|
|
415
502
|
end
|
|
416
503
|
end
|
|
417
|
-
#
|
|
418
|
-
#
|
|
419
|
-
#
|
|
420
|
-
|
|
504
|
+
# Two cleanups below cannot succeed while the instance is still
|
|
505
|
+
# shutting down, so either of them means waiting termination out:
|
|
506
|
+
# an auto-created security group cannot be deleted while an instance
|
|
507
|
+
# still references it, and a dedicated host goes on listing a
|
|
508
|
+
# terminating instance, which blocks its release.
|
|
509
|
+
#
|
|
510
|
+
# The host case used to be missing, so a run that supplied its own
|
|
511
|
+
# `security_group_ids` skipped the wait, found the host still
|
|
512
|
+
# occupied, and silently left it allocated and billing.
|
|
513
|
+
if (state[:auto_security_group_id] || state[:allocated_host_id]) &&
|
|
514
|
+
server && ec2.instance_exists?(state[:server_id])
|
|
421
515
|
wait_log = proc do |attempts|
|
|
422
516
|
c = attempts * config[:retryable_sleep]
|
|
423
517
|
t = config[:retryable_tries] * config[:retryable_sleep]
|
|
@@ -438,11 +532,88 @@ module Kitchen
|
|
|
438
532
|
delete_security_group(state)
|
|
439
533
|
delete_key(state)
|
|
440
534
|
|
|
441
|
-
#
|
|
535
|
+
# Release the dedicated host this instance's create allocated, if it
|
|
536
|
+
# allocated one and nothing else is left running on it.
|
|
537
|
+
#
|
|
538
|
+
# Only that host. Dedicated hosts are a shared pool -- create places
|
|
539
|
+
# onto any managed host with room rather than always allocating, so
|
|
540
|
+
# most runs allocate nothing -- and releasing every empty managed host
|
|
541
|
+
# tore down hosts belonging to other suites, including one allocated
|
|
542
|
+
# seconds earlier by a concurrent run whose instance had not launched
|
|
543
|
+
# onto it yet.
|
|
442
544
|
return unless config[:tenancy] == "host" && allow_deallocate_host?
|
|
443
545
|
|
|
444
|
-
|
|
445
|
-
|
|
546
|
+
host_id = state.delete(:allocated_host_id)
|
|
547
|
+
return unless host_id
|
|
548
|
+
|
|
549
|
+
host = host_for_id(host_id)
|
|
550
|
+
# Already gone, or never usable: nothing to release either way.
|
|
551
|
+
return if host.nil? || host.state != "available"
|
|
552
|
+
|
|
553
|
+
unless host_unused?(host)
|
|
554
|
+
# Test Kitchen deletes the state file once destroy returns, taking
|
|
555
|
+
# the host ID with it, so there is no later run that could pick this
|
|
556
|
+
# up -- say so loudly and give the command to finish the job.
|
|
557
|
+
error("Dedicated host #{host_id} still has instances on it and was not released. " \
|
|
558
|
+
"A dedicated host bills from allocation until it is released. Release it with: " \
|
|
559
|
+
"aws ec2 release-hosts --region #{config[:region]} --host-ids #{host_id}")
|
|
560
|
+
return
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
deallocate_host(host_id)
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
# EC2 instance states in which the instance still exists as a resource.
|
|
567
|
+
#
|
|
568
|
+
# "shutting-down" and "terminated" are left out deliberately. Neither can
|
|
569
|
+
# be brought back and neither leaves anything to destroy, so counting
|
|
570
|
+
# them as live would defeat the point of asking.
|
|
571
|
+
#
|
|
572
|
+
# @return [Array<String>]
|
|
573
|
+
LIVE_INSTANCE_STATES = %w{pending running stopping stopped}.freeze
|
|
574
|
+
|
|
575
|
+
# Whether the instance Test Kitchen recorded is still there.
|
|
576
|
+
#
|
|
577
|
+
# Answers `kitchen list --live` by asking EC2 rather than trusting the
|
|
578
|
+
# state file, which is the whole point: the state file records what Test
|
|
579
|
+
# Kitchen last did, not what survived. An instance terminated in the
|
|
580
|
+
# console, reaped by an account policy, or orphaned by a run that was
|
|
581
|
+
# killed mid-create all leave a state file claiming the instance exists.
|
|
582
|
+
#
|
|
583
|
+
# Kept to a single describe call, since `kitchen list --live` makes one
|
|
584
|
+
# of these per instance.
|
|
585
|
+
#
|
|
586
|
+
# @param state [Hash] the instance state
|
|
587
|
+
# @return [Hash] status data, normalized by Kitchen::Instance
|
|
588
|
+
# @see https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_InstanceState.html
|
|
589
|
+
def status(state)
|
|
590
|
+
unless state[:server_id]
|
|
591
|
+
return status_report(
|
|
592
|
+
live: false,
|
|
593
|
+
instance_state: "not_created",
|
|
594
|
+
message: "No EC2 instance has been created for this suite yet"
|
|
595
|
+
)
|
|
596
|
+
end
|
|
597
|
+
|
|
598
|
+
instance_state = ec2.get_instance(state[:server_id]).state.name
|
|
599
|
+
|
|
600
|
+
status_report(
|
|
601
|
+
live: LIVE_INSTANCE_STATES.include?(instance_state),
|
|
602
|
+
instance_state: instance_state,
|
|
603
|
+
resource_id: state[:server_id],
|
|
604
|
+
message: "EC2 instance #{state[:server_id]} is #{instance_state}"
|
|
605
|
+
)
|
|
606
|
+
rescue ::Aws::EC2::Errors::InvalidInstanceIDNotFound
|
|
607
|
+
# The reconciliation this check exists for: Test Kitchen still believes
|
|
608
|
+
# it has an instance, and EC2 has never heard of it.
|
|
609
|
+
status_report(
|
|
610
|
+
live: false,
|
|
611
|
+
instance_state: "not_found",
|
|
612
|
+
resource_id: state[:server_id],
|
|
613
|
+
message: "EC2 does not know instance #{state[:server_id]}. It was terminated " \
|
|
614
|
+
"outside Test Kitchen, or has aged out of EC2's terminated instance list. " \
|
|
615
|
+
"Run `kitchen destroy` to clear the stale state."
|
|
616
|
+
)
|
|
446
617
|
end
|
|
447
618
|
|
|
448
619
|
# The EC2 image this instance will be created from.
|
|
@@ -458,6 +629,14 @@ module Kitchen
|
|
|
458
629
|
@image = ec2.resource.image(config[:image_id])
|
|
459
630
|
show_chosen_image
|
|
460
631
|
|
|
632
|
+
elsif searched_for_image?
|
|
633
|
+
# A search ran and matched nothing. Saying "specify image_id or
|
|
634
|
+
# image_search" here sent people to set an option they had already
|
|
635
|
+
# set, or that the platform sets for them -- the real problem is
|
|
636
|
+
# that the filters matched no image in this region.
|
|
637
|
+
raise "The image search for #{desired_platform || instance.platform.name} matched no " \
|
|
638
|
+
"image in region #{config[:region]}. Set image_id to an AMI, or image_search to " \
|
|
639
|
+
"filters that match one."
|
|
461
640
|
else
|
|
462
641
|
raise "Neither image_id nor an image_search specified for instance #{instance.name}!" \
|
|
463
642
|
" Please specify one or the other."
|
|
@@ -466,16 +645,33 @@ module Kitchen
|
|
|
466
645
|
@image
|
|
467
646
|
end
|
|
468
647
|
|
|
648
|
+
# Whether an image search ran and came back empty.
|
|
649
|
+
#
|
|
650
|
+
# Distinguishes "there was nothing to search for" -- an unrecognized
|
|
651
|
+
# platform name and no `image_search` -- from "the search matched
|
|
652
|
+
# nothing", which are different mistakes with different fixes.
|
|
653
|
+
#
|
|
654
|
+
# @return [Boolean]
|
|
655
|
+
def searched_for_image?
|
|
656
|
+
!config[:image_search].nil? || !desired_platform.nil?
|
|
657
|
+
end
|
|
658
|
+
|
|
469
659
|
# The instance type to use when the user did not choose one.
|
|
470
660
|
#
|
|
661
|
+
# An instance type runs one processor architecture and EC2 rejects a
|
|
662
|
+
# RunInstances call pairing it with an image built for another, so the
|
|
663
|
+
# default has to follow the image: t4g is the Graviton counterpart of
|
|
664
|
+
# t3. Picking t3.micro for every HVM image made every arm64 platform --
|
|
665
|
+
# "ubuntu-24.04-arm64" and friends, an architecture this driver parses
|
|
666
|
+
# and searches for -- fail to launch outright.
|
|
667
|
+
#
|
|
471
668
|
# t3 instances require a hardware-virtualized image, so a paravirtual
|
|
472
669
|
# image falls back to the older t1 family.
|
|
473
670
|
#
|
|
474
|
-
# @return [String] a free-tier instance type
|
|
671
|
+
# @return [String] a free-tier instance type matching the image
|
|
475
672
|
def default_instance_type
|
|
476
673
|
@instance_type ||= if image && image.virtualization_type == "hvm"
|
|
477
|
-
|
|
478
|
-
"t3.micro"
|
|
674
|
+
hvm_default_instance_type
|
|
479
675
|
else
|
|
480
676
|
info("instance_type not specified. Using free tier t1.micro instance since" \
|
|
481
677
|
" image is paravirtual (pick an hvm image to use the superior t3.micro!) ...")
|
|
@@ -483,6 +679,25 @@ module Kitchen
|
|
|
483
679
|
end
|
|
484
680
|
end
|
|
485
681
|
|
|
682
|
+
# The free-tier instance type matching a hardware-virtualized image.
|
|
683
|
+
#
|
|
684
|
+
# Only arm64 is special-cased. EC2's Mac architectures ("arm64_mac" and
|
|
685
|
+
# "x86_64_mac") are deliberately not defaulted: they run only on
|
|
686
|
+
# dedicated Mac hosts, which carry a 24-hour minimum allocation, so
|
|
687
|
+
# guessing one would be an expensive surprise rather than a convenience.
|
|
688
|
+
#
|
|
689
|
+
# @return [String] the instance type to launch
|
|
690
|
+
def hvm_default_instance_type
|
|
691
|
+
if image.architecture == "arm64"
|
|
692
|
+
info("instance_type not specified. Using free tier t4g.micro instance" \
|
|
693
|
+
" since image is arm64 ...")
|
|
694
|
+
"t4g.micro"
|
|
695
|
+
else
|
|
696
|
+
info("instance_type not specified. Using free tier t3.micro instance ...")
|
|
697
|
+
"t3.micro"
|
|
698
|
+
end
|
|
699
|
+
end
|
|
700
|
+
|
|
486
701
|
# The platform detected from the image actually being used.
|
|
487
702
|
#
|
|
488
703
|
# This can differ from {#desired_platform}: the user asks for "ubuntu" and
|
|
@@ -794,6 +1009,7 @@ module Kitchen
|
|
|
794
1009
|
# @param server [Aws::EC2::Instance] the instance to wait on
|
|
795
1010
|
# @param state [Hash] the instance state
|
|
796
1011
|
# @param status_msg [String] what is being waited for, for log messages
|
|
1012
|
+
# @param block [Proc] the readiness check polled against the instance
|
|
797
1013
|
# @yieldparam aws_instance [Aws::EC2::Instance] the instance being polled
|
|
798
1014
|
# @yieldreturn [Boolean] true when the wait is over
|
|
799
1015
|
# @return [void]
|
|
@@ -954,20 +1170,41 @@ module Kitchen
|
|
|
954
1170
|
# @return [String] a PowerShell script wrapped in `<powershell>` tags
|
|
955
1171
|
def default_windows_user_data
|
|
956
1172
|
base_script = Kitchen::Util.outdent!(<<-EOH)
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1173
|
+
# Log where the installed launch agent already logs, chosen by looking
|
|
1174
|
+
# for it rather than by matching the OS against known release names.
|
|
1175
|
+
# Writing into the directory of an agent that is not installed would
|
|
1176
|
+
# invent a misleading empty tree.
|
|
1177
|
+
$logdir = If (Test-Path 'C:\\ProgramData\\Amazon\\EC2Launch') {
|
|
1178
|
+
'C:\\ProgramData\\Amazon\\EC2Launch\\log'
|
|
1179
|
+
} ElseIf (Test-Path 'C:\\ProgramData\\Amazon\\EC2-Windows\\Launch') {
|
|
1180
|
+
'C:\\ProgramData\\Amazon\\EC2-Windows\\Launch\\Log'
|
|
1181
|
+
} ElseIf (Test-Path 'C:\\Program Files\\Amazon\\Ec2ConfigService') {
|
|
1182
|
+
'C:\\Program Files\\Amazon\\Ec2ConfigService\\Logs'
|
|
1183
|
+
} Else {
|
|
1184
|
+
Join-Path $env:ProgramData 'Amazon\\kitchen-ec2'
|
|
1185
|
+
}
|
|
1186
|
+
New-Item -ItemType Directory -Force -Path $logdir | Out-Null
|
|
1187
|
+
$logfile = Join-Path $logdir 'kitchen-ec2.log'
|
|
969
1188
|
New-Item $logfile -Type file -Force
|
|
970
1189
|
|
|
1190
|
+
# Extra EBS volumes are attached but left uninitialized: no launch
|
|
1191
|
+
# agent partitions them by default, on any release. Done with the
|
|
1192
|
+
# storage cmdlets rather than by calling a particular agent's script,
|
|
1193
|
+
# so it does not matter which agent is installed. Only RAW disks are
|
|
1194
|
+
# touched, so a volume that already carries a filesystem is never
|
|
1195
|
+
# reformatted.
|
|
1196
|
+
#
|
|
1197
|
+
# GPT, not MBR: an MBR disk cannot address beyond 2 TiB, and
|
|
1198
|
+
# Initialize-Disk does not fail on a larger one -- it caps it, so a
|
|
1199
|
+
# 2600 GB volume came up as a 2 TiB filesystem with the remainder
|
|
1200
|
+
# unreachable and still billed. GPT is supported by every Windows
|
|
1201
|
+
# release this driver can launch.
|
|
1202
|
+
"Initializing any uninitialized volumes" >> $logfile
|
|
1203
|
+
Get-Disk | Where-Object PartitionStyle -eq 'RAW' |
|
|
1204
|
+
Initialize-Disk -PartitionStyle GPT -PassThru |
|
|
1205
|
+
New-Partition -AssignDriveLetter -UseMaximumSize |
|
|
1206
|
+
Format-Volume -FileSystem NTFS -Confirm:$false >> $logfile
|
|
1207
|
+
|
|
971
1208
|
# Allow script execution
|
|
972
1209
|
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Force
|
|
973
1210
|
#PS Remoting and & winrm.cmd basic config
|
|
@@ -980,7 +1217,6 @@ module Kitchen
|
|
|
980
1217
|
& winrm.cmd set winrm/config '@{MaxTimeoutms="1800000"}' >> $logfile
|
|
981
1218
|
& winrm.cmd set winrm/config/winrs '@{MaxMemoryPerShellMB="1024"}' >> $logfile
|
|
982
1219
|
& winrm.cmd set winrm/config/winrs '@{MaxShellsPerUser="50"}' >> $logfile
|
|
983
|
-
& winrm.cmd set winrm/config/winrs '@{MaxMemoryPerShellMB="1024"}' >> $logfile
|
|
984
1220
|
#Firewall Config
|
|
985
1221
|
& netsh advfirewall firewall set rule name="Windows Remote Management (HTTP-In)" profile=public protocol=tcp localport=5985 remoteip=localsubnet new remoteip=any >> $logfile
|
|
986
1222
|
Set-ItemProperty -Name LocalAccountTokenFilterPolicy -Path HKLM:\\software\\Microsoft\\Windows\\CurrentVersion\\Policies\\system -Value 1
|
|
@@ -1267,6 +1503,28 @@ module Kitchen
|
|
|
1267
1503
|
|
|
1268
1504
|
private
|
|
1269
1505
|
|
|
1506
|
+
# Build a status hash in the shape Kitchen::Instance normalizes.
|
|
1507
|
+
#
|
|
1508
|
+
# `checked_at` is stamped here rather than left out so that the timestamp
|
|
1509
|
+
# reflects when EC2 was actually asked, not when the answer was rendered.
|
|
1510
|
+
#
|
|
1511
|
+
# @param live [Boolean] whether the instance still exists
|
|
1512
|
+
# @param instance_state [String] the EC2 state name, or a driver-level
|
|
1513
|
+
# one such as "not_created" for the cases EC2 was never asked about
|
|
1514
|
+
# @param message [String] a human-readable explanation
|
|
1515
|
+
# @param resource_id [String, nil] the EC2 instance ID, when there is one
|
|
1516
|
+
# @return [Hash]
|
|
1517
|
+
def status_report(live:, instance_state:, message:, resource_id: nil)
|
|
1518
|
+
{
|
|
1519
|
+
live: live,
|
|
1520
|
+
state: instance_state,
|
|
1521
|
+
source: "driver",
|
|
1522
|
+
resource_id: resource_id,
|
|
1523
|
+
message: message,
|
|
1524
|
+
checked_at: Time.now.utc.iso8601,
|
|
1525
|
+
}
|
|
1526
|
+
end
|
|
1527
|
+
|
|
1270
1528
|
# Wrap the transport's `connection` method with Instance Connect setup.
|
|
1271
1529
|
#
|
|
1272
1530
|
# A pushed Instance Connect key expires after about a minute, so the key
|
|
@@ -1620,6 +1878,9 @@ module Kitchen
|
|
|
1620
1878
|
return File.read(public_key_path).strip
|
|
1621
1879
|
end
|
|
1622
1880
|
|
|
1881
|
+
public_key = instance_connect_public_key_via_ssh_keygen(private_key_path)
|
|
1882
|
+
return public_key if public_key
|
|
1883
|
+
|
|
1623
1884
|
begin
|
|
1624
1885
|
key = SSHKey.new(File.read(private_key_path))
|
|
1625
1886
|
key.ssh_public_key
|
|
@@ -1628,6 +1889,35 @@ module Kitchen
|
|
|
1628
1889
|
end
|
|
1629
1890
|
end
|
|
1630
1891
|
|
|
1892
|
+
# Derive the public half of a private key with ssh-keygen.
|
|
1893
|
+
#
|
|
1894
|
+
# OpenSSH reads every key type EC2 can create. The sshkey gem handles
|
|
1895
|
+
# only RSA and DSA, and raises "Neither PUB key nor PRIV key" on an
|
|
1896
|
+
# ed25519 key -- which is exactly what `aws_ssh_key_type: ed25519`
|
|
1897
|
+
# produces, so that documented setting could not be combined with
|
|
1898
|
+
# Instance Connect at all.
|
|
1899
|
+
#
|
|
1900
|
+
# Falling back to the gem rather than requiring ssh-keygen keeps the RSA
|
|
1901
|
+
# default working where OpenSSH is not installed.
|
|
1902
|
+
#
|
|
1903
|
+
# @param private_key_path [String] path to the private key
|
|
1904
|
+
# @return [String, nil] the public key in OpenSSH format, or nil when
|
|
1905
|
+
# ssh-keygen is unavailable or could not read the key
|
|
1906
|
+
def instance_connect_public_key_via_ssh_keygen(private_key_path)
|
|
1907
|
+
output, status = Open3.capture2e("ssh-keygen", "-y", "-f", private_key_path)
|
|
1908
|
+
return output.strip if status.success?
|
|
1909
|
+
|
|
1910
|
+
debug("ssh-keygen could not read #{private_key_path}: #{output.strip}")
|
|
1911
|
+
nil
|
|
1912
|
+
# ::StandardError, not StandardError: this file is nested inside `module
|
|
1913
|
+
# Kitchen`, which defines Kitchen::StandardError. An unqualified constant
|
|
1914
|
+
# resolves to that one, letting the Errno::ENOENT raised by a missing
|
|
1915
|
+
# ssh-keygen escape the check meant to detect it.
|
|
1916
|
+
rescue ::StandardError => e
|
|
1917
|
+
debug("Could not run ssh-keygen: #{e.message}")
|
|
1918
|
+
nil
|
|
1919
|
+
end
|
|
1920
|
+
|
|
1631
1921
|
# SSM Session Manager Support Methods
|
|
1632
1922
|
|
|
1633
1923
|
# The SSM Session Manager helper.
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kitchen-ec2
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.23.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Test Kitchen Team
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-10-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: aws-sdk-ec2
|