kitchen-ec2 3.22.7 → 3.22.10

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: 54005acc701e50833f4592f90156aa8d41f74ceba73c824df2903940afd4232d
4
- data.tar.gz: e1c55129e883dfa0e92f0c2510a47d4719fe7ceb2f2b3c9db4bcb766c05fe18c
3
+ metadata.gz: adbbc52b9c77d773ae78ebd08798d629a6781793d0e91ff46fb14eb080c70595
4
+ data.tar.gz: 8df7160b05b65d0187b24d9525ea458593db0c95cf9cef261683229752ca1cf6
5
5
  SHA512:
6
- metadata.gz: 61ddb4833e791864530709dba1bd080519a7e8e8d18268ee147f6cf8f2bfa212218e81f40990e8fc7e13288abb2d77edfba580a757cb8488fc7738189cfb940c
7
- data.tar.gz: 415028713bf3467ed42152c5249730212d0124e5a0424867cd1580f55f1b23dbaef199bc8aa82cc88fa2e0e0e85e7ae1de30d3f55de7e967588fcabbda333450
6
+ metadata.gz: 956f2322fd4e6bb42ad179c677159be0acb108b30c651c940e95b3c6e266466a548a982f9bb9bcc6edeea3e3ae9734ddc37d80dad14cce80a78c268d733a97ba
7
+ data.tar.gz: f98fe3846db116988011feefebae241fec7145cbbc49aa45d3e82620c1ba5fa36ae15222125144cf37405d7f373db0c5fbcf32505b62453b9f677e84cfa3c765
@@ -91,7 +91,9 @@ module Kitchen
91
91
  # host costs money whether or not it is used.
92
92
  def allocate_host
93
93
  unless allow_allocate_host?
94
- warn "ERROR: Attempted to allocate dedicated host but need environment variable TK_ALLOCATE_DEDICATED_HOST to be set"
94
+ warn "ERROR: Attempted to allocate a dedicated host, but the driver setting `allocate_dedicated_host` is not enabled. " \
95
+ "Set `allocate_dedicated_host: true` to allow it, remembering that a dedicated host is billed from allocation " \
96
+ "until it is released."
95
97
  exit!
96
98
  end
97
99
 
@@ -92,34 +92,29 @@ module Kitchen
92
92
  security_groups = []
93
93
  filters = [config[:security_group_filter]].flatten
94
94
  filters.each do |sg_filter|
95
- r = {}
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
- r[:filters] = [
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
- r[:filters] = [
111
- {
112
- name: "tag:#{sg_filter[:tag]}",
113
- values: [sg_filter[:value]],
114
- },
115
- {
116
- name: "vpc-id",
117
- values: [vpc_id],
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
- security_group = client.describe_security_groups(r).security_groups
115
+ criteria << { name: "vpc-id", values: [vpc_id] }
116
+
117
+ security_group = client.describe_security_groups(filters: criteria).security_groups
123
118
 
124
119
  if security_group.any?
125
120
  security_group.each { |sg| security_groups.push(sg.group_id) }
@@ -232,11 +227,13 @@ module Kitchen
232
227
  i[:placement][:tenancy] = placement[:tenancy]
233
228
  end
234
229
  end
230
+ # RunInstances calls this `license_specifications`. The driver option
231
+ # is `licenses`, and the payload key used to match the option rather
232
+ # than the API, which no EC2 API accepts.
235
233
  license_specifications = config[:licenses]
236
234
  if license_specifications
237
- i[:licenses] = []
238
- license_specifications.each do |license_configuration_arn|
239
- i[:licenses].append({ license_configuration_arn: license_configuration_arn[:license_configuration_arn] })
235
+ i[:license_specifications] = license_specifications.map do |license|
236
+ { license_configuration_arn: license[:license_configuration_arn] }
240
237
  end
241
238
  end
242
239
  unless config[:instance_initiated_shutdown_behavior].nil? ||
@@ -50,6 +50,23 @@ module Kitchen
50
50
  search
51
51
  end
52
52
 
53
+ # Sort images newest release first, keeping Kitten builds last.
54
+ #
55
+ # AlmaLinux Kitten is AlmaLinux's development distribution, published
56
+ # from the same account and under the same "AlmaLinux OS" prefix as
57
+ # the releases. Its name carries no release number, so {.from_image}
58
+ # reads the major straight into the build date: "AlmaLinux OS Kitten
59
+ # 10.20260727.0" yields "10.20260727", a far larger number than the
60
+ # "10.2" of an actual release, which sorts Kitten ahead of
61
+ # everything. A versioned search is unaffected, since "AlmaLinux OS
62
+ # 10*" does not match "AlmaLinux OS Kitten 10*".
63
+ #
64
+ # @param images [Array<Aws::EC2::Image>] the images to sort
65
+ # @return [Array<Aws::EC2::Image>] the images, newest release first
66
+ def sort_by_version(images)
67
+ prefer(super) { |image| !image.name.include?("Kitten") }
68
+ end
69
+
53
70
  # Detect this platform from an EC2 image.
54
71
  #
55
72
  # Matching is done on the image name, which is the only reliable signal
@@ -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
@@ -23,6 +23,12 @@ module Kitchen
23
23
  class Fedora < StandardPlatform
24
24
  StandardPlatform.platforms["fedora"] = self
25
25
 
26
+ # Name fragments marking an image as a development stream rather
27
+ # than a Fedora release: Rawhide (the rolling development branch),
28
+ # ELN (Enterprise Linux Next) and the Prerelease builds published
29
+ # while a release is still stabilising.
30
+ DEVELOPMENT_STREAMS = /-(?:Rawhide|ELN|Prerelease)-/i
31
+
26
32
  # The account EC2 creates on this platform's official AMIs.
27
33
  #
28
34
  # Used as the SSH username when the transport does not specify one.
@@ -43,12 +49,36 @@ module Kitchen
43
49
  def image_search
44
50
  search = {
45
51
  "owner-id" => "125523088429",
46
- "name" => version ? "Fedora-Cloud-Base-#{version}-*" : "Fedora-Cloud-Base-*",
52
+ "name" => if version
53
+ # Both naming schemes are searched because the older
54
+ # one is still present in some regions.
55
+ ["Fedora-Cloud-Base-AmazonEC2.*-#{version}-*",
56
+ "Fedora-Cloud-Base-#{version}-*"]
57
+ else
58
+ "Fedora-Cloud-Base-*"
59
+ end,
47
60
  }
48
61
  search["architecture"] = architecture if architecture
49
62
  search
50
63
  end
51
64
 
65
+ # Sort images newest release first, keeping development streams last.
66
+ #
67
+ # Fedora publishes Rawhide, ELN and Prerelease images from the same
68
+ # account and under the same "Fedora-Cloud-Base-" prefix as its
69
+ # releases. Those names carry a build date where a release carries a
70
+ # release number, so {.from_image} reads a version like "20250820.0"
71
+ # off them -- larger than any real release, which sorts them to the
72
+ # front. They are pushed to the back afterwards, the same way RHEL
73
+ # handles its Beta images.
74
+ #
75
+ # @param images [Array<Aws::EC2::Image>] the images to sort
76
+ # @return [Array<Aws::EC2::Image>] the images, newest release first
77
+ def sort_by_version(images)
78
+ images = super
79
+ prefer(images) { |image| !DEVELOPMENT_STREAMS.match?(image.name) }
80
+ end
81
+
52
82
  # Detect this platform from an EC2 image.
53
83
  #
54
84
  # Matching is done on the image name, which is the only reliable signal
@@ -57,6 +57,28 @@ module Kitchen
57
57
  search
58
58
  end
59
59
 
60
+ # Sort images newest release first, preferring the cloud-init flavor.
61
+ #
62
+ # FreeBSD publishes several flavors of each release, distinguished
63
+ # only by a word at the end of the image name, and only the
64
+ # cloud-init flavor is a general-purpose cloud image. "base" and
65
+ # "small" ship without sudo, so a converge fails on them; "builder"
66
+ # is a developer image that does not boot to a usable state on a
67
+ # small instance -- launched on a t3.micro it reaches "impaired" and
68
+ # never opens port 22. Nothing in the name distinguishes them by
69
+ # date or version, so without a preference the flavor selected is
70
+ # effectively arbitrary.
71
+ #
72
+ # Releases old enough to predate the flavored names publish a single
73
+ # image each. Because this is a stable partition rather than a
74
+ # filter, those are left where the version sort put them.
75
+ #
76
+ # @param images [Array<Aws::EC2::Image>] the images to sort
77
+ # @return [Array<Aws::EC2::Image>] the images, best match first
78
+ def sort_by_version(images)
79
+ prefer(super) { |image| image.name.include?("cloud-init") }
80
+ end
81
+
60
82
  # Detect this platform from an EC2 image.
61
83
  #
62
84
  # Matching is done on the image name, which is the only reliable signal
@@ -90,10 +90,11 @@ module Kitchen
90
90
  # @param images [Array<Aws::EC2::Image>] the images to sort
91
91
  # @return [Array<Aws::EC2::Image>] the images, best match first
92
92
  def sort_by_version(images)
93
- # First do a normal version sort
94
- super(images)
95
- # Now sort again, shunning Beta releases.
96
- prefer(images) { |image| !image.name.match(/_Beta-/i) }
93
+ # `prefer(super)`, not `super(images)` followed by
94
+ # `prefer(images)`: `prefer` returns a new array rather than
95
+ # reordering in place, so applying it to the original argument
96
+ # discarded the version sort entirely.
97
+ prefer(super) { |image| !image.name.match?(/_Beta-/i) }
97
98
  end
98
99
  end
99
100
  end
@@ -153,6 +153,20 @@ module Kitchen
153
153
  #
154
154
  def self.from_platform_string(driver, platform_string)
155
155
  platform, version, architecture = parse_platform_string(platform_string)
156
+
157
+ # A few platform names end in what looks like a version but is
158
+ # really part of the distribution's name: "amazon2023" and "amazon2"
159
+ # are Amazon Linux 2023 and Amazon Linux 2, not releases 2023 and 2
160
+ # of "amazon" (which is the long-EOL Amazon Linux 1). Written with a
161
+ # dash -- the spelling the documentation uses -- the split above
162
+ # lands on the "amazon" platform searching for a release that never
163
+ # existed, and no image matches. Fold the release back into the name
164
+ # whenever the two together name a platform that is registered.
165
+ if version && platforms["#{platform}#{version}"]
166
+ platform = "#{platform}#{version}"
167
+ version = nil
168
+ end
169
+
156
170
  return unless platform && platforms[platform]
157
171
 
158
172
  platforms[platform].new(driver, platform, version, architecture)
@@ -16,8 +16,10 @@
16
16
  # See the License for the specific language governing permissions and
17
17
  # limitations under the License.
18
18
 
19
+ require "fileutils" unless defined?(FileUtils)
19
20
  require "sshkey" unless defined?(SSHKey)
20
21
  require "benchmark" unless defined?(Benchmark)
22
+ require "open3" unless defined?(Open3)
21
23
  require "json" unless defined?(JSON)
22
24
  require "kitchen"
23
25
  require_relative "ec2_version"
@@ -228,6 +230,19 @@ module Kitchen
228
230
  end
229
231
  end
230
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
+
231
246
  # empty keys cause failures when tagging and they make no sense
232
247
  validations[:tags] = lambda do |_attr, val, _driver|
233
248
  # if someone puts the tags each on their own line it's an array not a hash
@@ -248,11 +263,13 @@ module Kitchen
248
263
  #
249
264
  # Any failure destroys the instance and everything auto-created alongside
250
265
  # it, so that a failed create does not leave billable resources behind.
266
+ # Interrupts are re-raised untouched once that cleanup has run.
251
267
  #
252
268
  # @param state [Hash] the instance state, updated in place with
253
269
  # `:server_id`, `:hostname` and any auto-created credentials
254
270
  # @return [void]
255
- # @raise [RuntimeError] wrapping whatever went wrong, after cleaning up
271
+ # @raise [Kitchen::ActionFailed] wrapping whatever went wrong, after
272
+ # cleaning up
256
273
  def create(state)
257
274
  return if state[:server_id]
258
275
 
@@ -292,7 +309,8 @@ module Kitchen
292
309
  exit!
293
310
  end
294
311
 
295
- allocate_host unless host_available?
312
+ # Remembered so that destroy releases this host and no other.
313
+ state[:allocated_host_id] = allocate_host unless host_available?
296
314
 
297
315
  info("Auto placement on one dedicated host out of: #{hosts_with_capacity.map(&:host_id).join(", ")}")
298
316
  end
@@ -335,10 +353,60 @@ module Kitchen
335
353
  attach_network_interface(state) unless config[:elastic_network_interface_id].nil?
336
354
  create_ec2_json(state) if /chef/i.match?(instance.provisioner.name)
337
355
  debug("ec2:create '#{state[:hostname]}'")
338
- rescue Exception => e
339
- # Clean up the instance and any auto-created security groups or keys on the way out.
356
+ rescue ::Exception => e
357
+ # Deliberately ::Exception, and deliberately broad: a create that is
358
+ # interrupted partway through must still clean up, or the user is left
359
+ # paying for an instance Test Kitchen has forgotten about.
360
+ #
361
+ # Root-qualified because this file is nested inside `module Kitchen`.
362
+ # There is no Kitchen::Exception today, but Kitchen::StandardError does
363
+ # exist, and an unqualified constant would silently pick it up if one
364
+ # were ever added.
340
365
  destroy(state)
341
- raise "#{e.message} in the specified region #{config[:region]}. Please check this AMI is available in this region."
366
+
367
+ # Signals and exits are re-raised untouched. Wrapping a Ctrl-C in an
368
+ # ActionFailed would report the user's own interrupt as a driver bug.
369
+ raise if e.is_a?(::SignalException) || e.is_a?(::SystemExit)
370
+
371
+ raise Kitchen::ActionFailed, create_failure_message(e), e.backtrace
372
+ end
373
+
374
+ # Describe a failed create without discarding what actually went wrong.
375
+ #
376
+ # This message used to append "in the specified region <region>. Please
377
+ # check this AMI is available in this region" to *every* failure, and to
378
+ # raise a bare RuntimeError, so the original exception class and
379
+ # backtrace were lost. A local OpenSSL fault, an expired credential or a
380
+ # missing subnet all arrived looking like an AMI problem, sending users
381
+ # to check something that was never wrong.
382
+ #
383
+ # The hint is worth keeping -- an AMI that does not exist in the region
384
+ # really is a common mistake -- but only when the failure is about the
385
+ # image.
386
+ #
387
+ # @param error [Exception] the underlying failure
388
+ # @return [String]
389
+ # @see https://github.com/test-kitchen/kitchen-ec2/issues/606
390
+ def create_failure_message(error)
391
+ message = "Failed to create the EC2 instance: #{error.class}: #{error.message}"
392
+ return message unless image_related_error?(error)
393
+
394
+ "#{message} Check that image #{config[:image_id]} exists and is available " \
395
+ "in region #{config[:region]}."
396
+ end
397
+
398
+ # Whether a failure is about the AMI rather than something else entirely.
399
+ #
400
+ # EC2 reports every image problem with a code beginning "InvalidAMI"; the
401
+ # message check catches errors raised by the driver itself, which are
402
+ # plain strings with no code attached.
403
+ #
404
+ # @param error [Exception] the underlying failure
405
+ # @return [Boolean]
406
+ def image_related_error?(error)
407
+ return true if error.respond_to?(:code) && error.code.to_s.start_with?("InvalidAMI")
408
+
409
+ error.message.to_s.match?(/\bAMI\b/i)
342
410
  end
343
411
 
344
412
  # Terminate the instance and clean up everything created alongside it.
@@ -385,11 +453,22 @@ module Kitchen
385
453
  delete_security_group(state)
386
454
  delete_key(state)
387
455
 
388
- # Clean up dedicated hosts matching instance_type and unused (if allowed)
456
+ # Release the dedicated host this instance's create allocated, if it
457
+ # allocated one and nothing else is left running on it.
458
+ #
459
+ # Only that host. Dedicated hosts are a shared pool -- create places
460
+ # onto any managed host with room rather than always allocating, so
461
+ # most runs allocate nothing -- and releasing every empty managed host
462
+ # tore down hosts belonging to other suites, including one allocated
463
+ # seconds earlier by a concurrent run whose instance had not launched
464
+ # onto it yet.
389
465
  return unless config[:tenancy] == "host" && allow_deallocate_host?
390
466
 
391
- empty_hosts = hosts_with_capacity.select { |host| host_unused?(host) }
392
- empty_hosts.each { |host| deallocate_host(host.host_id) }
467
+ host_id = state.delete(:allocated_host_id)
468
+ return unless host_id
469
+
470
+ host = host_for_id(host_id)
471
+ deallocate_host(host_id) if host && host_unused?(host)
393
472
  end
394
473
 
395
474
  # The EC2 image this instance will be created from.
@@ -901,20 +980,35 @@ module Kitchen
901
980
  # @return [String] a PowerShell script wrapped in `<powershell>` tags
902
981
  def default_windows_user_data
903
982
  base_script = Kitchen::Util.outdent!(<<-EOH)
904
- $OSVersion = (get-itemproperty -Path "HKLM:\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion" -Name ProductName).ProductName
905
- If($OSVersion.contains('2016') -Or $OSVersion.contains('2019') -Or $OSVersion -eq 'Windows Server Datacenter') {
906
- New-Item -ItemType Directory -Force -Path 'C:\\ProgramData\\Amazon\\EC2-Windows\\Launch\\Log'
907
- $logfile='C:\\ProgramData\\Amazon\\EC2-Windows\\Launch\\Log\\kitchen-ec2.log'
908
- # EC2Launch doesn't init extra disks by default
909
- C:\\ProgramData\\Amazon\\EC2-Windows\\Launch\\Scripts\\InitializeDisks.ps1
910
- } Else {
911
- New-Item -ItemType Directory -Force -Path 'C:\\Program Files\\Amazon\\Ec2ConfigService\\Logs'
912
- $logfile='C:\\Program Files\\Amazon\\Ec2ConfigService\\Logs\\kitchen-ec2.log'
913
- }
914
-
915
- # Logfile fail-safe in case the directory does not exist
983
+ # Log where the installed launch agent already logs, chosen by looking
984
+ # for it rather than by matching the OS against known release names.
985
+ # Writing into the directory of an agent that is not installed would
986
+ # invent a misleading empty tree.
987
+ $logdir = If (Test-Path 'C:\\ProgramData\\Amazon\\EC2Launch') {
988
+ 'C:\\ProgramData\\Amazon\\EC2Launch\\log'
989
+ } ElseIf (Test-Path 'C:\\ProgramData\\Amazon\\EC2-Windows\\Launch') {
990
+ 'C:\\ProgramData\\Amazon\\EC2-Windows\\Launch\\Log'
991
+ } ElseIf (Test-Path 'C:\\Program Files\\Amazon\\Ec2ConfigService') {
992
+ 'C:\\Program Files\\Amazon\\Ec2ConfigService\\Logs'
993
+ } Else {
994
+ Join-Path $env:ProgramData 'Amazon\\kitchen-ec2'
995
+ }
996
+ New-Item -ItemType Directory -Force -Path $logdir | Out-Null
997
+ $logfile = Join-Path $logdir 'kitchen-ec2.log'
916
998
  New-Item $logfile -Type file -Force
917
999
 
1000
+ # Extra EBS volumes are attached but left uninitialized: no launch
1001
+ # agent partitions them by default, on any release. Done with the
1002
+ # storage cmdlets rather than by calling a particular agent's script,
1003
+ # so it does not matter which agent is installed. Only RAW disks are
1004
+ # touched, so a volume that already carries a filesystem is never
1005
+ # reformatted.
1006
+ "Initializing any uninitialized volumes" >> $logfile
1007
+ Get-Disk | Where-Object PartitionStyle -eq 'RAW' |
1008
+ Initialize-Disk -PartitionStyle MBR -PassThru |
1009
+ New-Partition -AssignDriveLetter -UseMaximumSize |
1010
+ Format-Volume -FileSystem NTFS -Confirm:$false >> $logfile
1011
+
918
1012
  # Allow script execution
919
1013
  Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Force
920
1014
  #PS Remoting and & winrm.cmd basic config
@@ -927,7 +1021,6 @@ module Kitchen
927
1021
  & winrm.cmd set winrm/config '@{MaxTimeoutms="1800000"}' >> $logfile
928
1022
  & winrm.cmd set winrm/config/winrs '@{MaxMemoryPerShellMB="1024"}' >> $logfile
929
1023
  & winrm.cmd set winrm/config/winrs '@{MaxShellsPerUser="50"}' >> $logfile
930
- & winrm.cmd set winrm/config/winrs '@{MaxMemoryPerShellMB="1024"}' >> $logfile
931
1024
  #Firewall Config
932
1025
  & netsh advfirewall firewall set rule name="Windows Remote Management (HTTP-In)" profile=public protocol=tcp localport=5985 remoteip=localsubnet new remoteip=any >> $logfile
933
1026
  Set-ItemProperty -Name LocalAccountTokenFilterPolicy -Path HKLM:\\software\\Microsoft\\Windows\\CurrentVersion\\Policies\\system -Value 1
@@ -1179,7 +1272,12 @@ module Kitchen
1179
1272
  info("Removing automatic key pair #{state[:auto_key_id]}")
1180
1273
  ec2.client.delete_key_pair(key_name: state[:auto_key_id])
1181
1274
  state.delete(:auto_key_id)
1182
- File.unlink("#{config[:kitchen_root]}/.kitchen/#{instance.name}.pem")
1275
+ # The file is not always still there: it may have been cleaned up by
1276
+ # hand, wiped along with .kitchen, or never written at all, since
1277
+ # create records the key pair in the state before writing it. This is
1278
+ # the last thing destroy does, so raising here fails the whole action
1279
+ # after every AWS resource has already been cleaned up successfully.
1280
+ FileUtils.rm_f("#{config[:kitchen_root]}/.kitchen/#{instance.name}.pem")
1183
1281
  end
1184
1282
 
1185
1283
  # Finalize the driver config and install transport overrides.
@@ -1562,6 +1660,9 @@ module Kitchen
1562
1660
  return File.read(public_key_path).strip
1563
1661
  end
1564
1662
 
1663
+ public_key = instance_connect_public_key_via_ssh_keygen(private_key_path)
1664
+ return public_key if public_key
1665
+
1565
1666
  begin
1566
1667
  key = SSHKey.new(File.read(private_key_path))
1567
1668
  key.ssh_public_key
@@ -1570,6 +1671,35 @@ module Kitchen
1570
1671
  end
1571
1672
  end
1572
1673
 
1674
+ # Derive the public half of a private key with ssh-keygen.
1675
+ #
1676
+ # OpenSSH reads every key type EC2 can create. The sshkey gem handles
1677
+ # only RSA and DSA, and raises "Neither PUB key nor PRIV key" on an
1678
+ # ed25519 key -- which is exactly what `aws_ssh_key_type: ed25519`
1679
+ # produces, so that documented setting could not be combined with
1680
+ # Instance Connect at all.
1681
+ #
1682
+ # Falling back to the gem rather than requiring ssh-keygen keeps the RSA
1683
+ # default working where OpenSSH is not installed.
1684
+ #
1685
+ # @param private_key_path [String] path to the private key
1686
+ # @return [String, nil] the public key in OpenSSH format, or nil when
1687
+ # ssh-keygen is unavailable or could not read the key
1688
+ def instance_connect_public_key_via_ssh_keygen(private_key_path)
1689
+ output, status = Open3.capture2e("ssh-keygen", "-y", "-f", private_key_path)
1690
+ return output.strip if status.success?
1691
+
1692
+ debug("ssh-keygen could not read #{private_key_path}: #{output.strip}")
1693
+ nil
1694
+ # ::StandardError, not StandardError: this file is nested inside `module
1695
+ # Kitchen`, which defines Kitchen::StandardError. An unqualified constant
1696
+ # resolves to that one, letting the Errno::ENOENT raised by a missing
1697
+ # ssh-keygen escape the check meant to detect it.
1698
+ rescue ::StandardError => e
1699
+ debug("Could not run ssh-keygen: #{e.message}")
1700
+ nil
1701
+ end
1702
+
1573
1703
  # SSM Session Manager Support Methods
1574
1704
 
1575
1705
  # The SSM Session Manager helper.
@@ -1648,9 +1778,18 @@ module Kitchen
1648
1778
  "--region", driver_instance.config[:region]
1649
1779
  ]
1650
1780
 
1651
- # Add document name if specified
1781
+ # Tunnelling SSH over SSM needs a session document that forwards a
1782
+ # port. Without one, `start-session` opens an interactive shell
1783
+ # session instead, which speaks nothing SSH understands: pointed at
1784
+ # that as a ProxyCommand, SSH waits for a banner that never comes
1785
+ # and the create hangs until it is interrupted. AWS-StartSSHSession
1786
+ # is the document AWS publishes for this, and `%p` is substituted by
1787
+ # Net::SSH::Proxy::Command with the port being connected on, so a
1788
+ # transport using a port other than 22 tunnels to that port.
1652
1789
  if driver_instance.config[:ssm_session_manager_document_name]
1653
1790
  cmd += ["--document-name", driver_instance.config[:ssm_session_manager_document_name]]
1791
+ else
1792
+ cmd += ["--document-name", "AWS-StartSSHSession", "--parameters", "portNumber=%p"]
1654
1793
  end
1655
1794
 
1656
1795
  # Add AWS profile if specified
@@ -19,6 +19,6 @@
19
19
  module Kitchen
20
20
  module Driver
21
21
  # Version string for EC2 Test Kitchen driver
22
- EC2_VERSION = "3.22.7".freeze
22
+ EC2_VERSION = "3.22.10".freeze
23
23
  end
24
24
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kitchen-ec2
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.22.7
4
+ version: 3.22.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Test Kitchen Team