kitchen-ec2 3.22.10 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: adbbc52b9c77d773ae78ebd08798d629a6781793d0e91ff46fb14eb080c70595
4
- data.tar.gz: 8df7160b05b65d0187b24d9525ea458593db0c95cf9cef261683229752ca1cf6
3
+ metadata.gz: 4b31b976c77faa84d3828219b396b07d52790a806d0b2a0fc72b95f8dabbe548
4
+ data.tar.gz: 52cb6998a147c6ca43e638f80a16f3b461f3ed25d7999695c42197a1900c34e1
5
5
  SHA512:
6
- metadata.gz: 956f2322fd4e6bb42ad179c677159be0acb108b30c651c940e95b3c6e266466a548a982f9bb9bcc6edeea3e3ae9734ddc37d80dad14cce80a78c268d733a97ba
7
- data.tar.gz: f98fe3846db116988011feefebae241fec7145cbbc49aa45d3e82620c1ba5fa36ae15222125144cf37405d7f373db0c5fbcf32505b62453b9f677e84cfa3c765
6
+ metadata.gz: 0d57aa3da2611170b0e00e140e9d671c1a7585048020da316c1194d238e9c3fd41ef4ffc62ed1e4375d1862983220cd6c67e48ca25f98777adc677e0d6987f87
7
+ data.tar.gz: 68faeb3e26106d8df47a4896b7d6f1ef18c3c30cbbfd2d12e1cac6dcd2dddacd208f60d92493647ec19fba347e50f14691c279ad33999bf109a5ca949817e7cc
@@ -88,7 +88,7 @@ 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.describe_subnets(subnet_ids: [config[:subnet_id]]).subnets[0].vpc_id
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|
@@ -112,7 +112,10 @@ module Kitchen
112
112
  "#{sg_filter.inspect} has neither."
113
113
  end
114
114
 
115
- criteria << { name: "vpc-id", values: [vpc_id] }
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
116
119
 
117
120
  security_group = client.describe_security_groups(filters: criteria).security_groups
118
121
 
@@ -243,6 +246,46 @@ module Kitchen
243
246
  i
244
247
  end
245
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
+
246
289
  # The user data script, base64 encoded as EC2 requires.
247
290
  #
248
291
  # The configured value is treated as a file path when it names an
@@ -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" => "100343932686",
65
+ "owner-id" => MACOS_OWNER_ID,
48
66
  "name" => version ? "amzn-ec2-macos-#{version}*" : "amzn-ec2-macos-*",
49
67
  }
50
- search["architecture"] = architecture if architecture
51
- search["architecture"] = "arm64_mac" if architecture == "arm64"
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::Ec2::Image] The EC2 Image object.
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[Aws::Ec2::Image]] The list of images to sort
226
+ # @param images [Array<Aws::EC2::Image>] The list of images to sort
227
227
  #
228
- # @return [Array[Aws::Ec2::Image]] A sorted list.
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
@@ -256,6 +256,72 @@ module Kitchen
256
256
 
257
257
  # Create an EC2 instance and wait until it can be connected to.
258
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
+ #
259
325
  # Auto-creates a security group and key pair when none were configured,
260
326
  # allocates a dedicated host if `tenancy: host` requires one, requests
261
327
  # either an on-demand or a spot instance, then waits for the instance to
@@ -270,9 +336,7 @@ module Kitchen
270
336
  # @return [void]
271
337
  # @raise [Kitchen::ActionFailed] wrapping whatever went wrong, after
272
338
  # cleaning up
273
- def create(state)
274
- return if state[:server_id]
275
-
339
+ def launch_instance(state)
276
340
  update_username(state)
277
341
 
278
342
  info(Kitchen::Util.outdent!(<<-END)) unless config[:skip_cost_warning]
@@ -389,7 +453,10 @@ module Kitchen
389
453
  # @see https://github.com/test-kitchen/kitchen-ec2/issues/606
390
454
  def create_failure_message(error)
391
455
  message = "Failed to create the EC2 instance: #{error.class}: #{error.message}"
392
- return message unless image_related_error?(error)
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)
393
460
 
394
461
  "#{message} Check that image #{config[:image_id]} exists and is available " \
395
462
  "in region #{config[:region]}."
@@ -397,14 +464,19 @@ module Kitchen
397
464
 
398
465
  # Whether a failure is about the AMI rather than something else entirely.
399
466
  #
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.
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.
403
475
  #
404
476
  # @param error [Exception] the underlying failure
405
477
  # @return [Boolean]
406
478
  def image_related_error?(error)
407
- return true if error.respond_to?(:code) && error.code.to_s.start_with?("InvalidAMI")
479
+ return error.code.to_s.start_with?("InvalidAMI") if error.respond_to?(:code)
408
480
 
409
481
  error.message.to_s.match?(/\bAMI\b/i)
410
482
  end
@@ -429,10 +501,17 @@ module Kitchen
429
501
  warn("Received #{e}, instance was probably already destroyed. Ignoring")
430
502
  end
431
503
  end
432
- # If we are going to clean up an automatic security group, we need
433
- # to wait for the instance to shut down. This slightly breaks the
434
- # subsystem encapsulation, sorry not sorry.
435
- if state[:auto_security_group_id] && server && ec2.instance_exists?(state[:server_id])
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])
436
515
  wait_log = proc do |attempts|
437
516
  c = attempts * config[:retryable_sleep]
438
517
  t = config[:retryable_tries] * config[:retryable_sleep]
@@ -468,7 +547,73 @@ module Kitchen
468
547
  return unless host_id
469
548
 
470
549
  host = host_for_id(host_id)
471
- deallocate_host(host_id) if host && host_unused?(host)
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
+ )
472
617
  end
473
618
 
474
619
  # The EC2 image this instance will be created from.
@@ -484,6 +629,14 @@ module Kitchen
484
629
  @image = ec2.resource.image(config[:image_id])
485
630
  show_chosen_image
486
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."
487
640
  else
488
641
  raise "Neither image_id nor an image_search specified for instance #{instance.name}!" \
489
642
  " Please specify one or the other."
@@ -492,16 +645,33 @@ module Kitchen
492
645
  @image
493
646
  end
494
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
+
495
659
  # The instance type to use when the user did not choose one.
496
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
+ #
497
668
  # t3 instances require a hardware-virtualized image, so a paravirtual
498
669
  # image falls back to the older t1 family.
499
670
  #
500
- # @return [String] a free-tier instance type
671
+ # @return [String] a free-tier instance type matching the image
501
672
  def default_instance_type
502
673
  @instance_type ||= if image && image.virtualization_type == "hvm"
503
- info("instance_type not specified. Using free tier t3.micro instance ...")
504
- "t3.micro"
674
+ hvm_default_instance_type
505
675
  else
506
676
  info("instance_type not specified. Using free tier t1.micro instance since" \
507
677
  " image is paravirtual (pick an hvm image to use the superior t3.micro!) ...")
@@ -509,6 +679,25 @@ module Kitchen
509
679
  end
510
680
  end
511
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
+
512
701
  # The platform detected from the image actually being used.
513
702
  #
514
703
  # This can differ from {#desired_platform}: the user asks for "ubuntu" and
@@ -820,6 +1009,7 @@ module Kitchen
820
1009
  # @param server [Aws::EC2::Instance] the instance to wait on
821
1010
  # @param state [Hash] the instance state
822
1011
  # @param status_msg [String] what is being waited for, for log messages
1012
+ # @param block [Proc] the readiness check polled against the instance
823
1013
  # @yieldparam aws_instance [Aws::EC2::Instance] the instance being polled
824
1014
  # @yieldreturn [Boolean] true when the wait is over
825
1015
  # @return [void]
@@ -1003,9 +1193,15 @@ module Kitchen
1003
1193
  # so it does not matter which agent is installed. Only RAW disks are
1004
1194
  # touched, so a volume that already carries a filesystem is never
1005
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.
1006
1202
  "Initializing any uninitialized volumes" >> $logfile
1007
1203
  Get-Disk | Where-Object PartitionStyle -eq 'RAW' |
1008
- Initialize-Disk -PartitionStyle MBR -PassThru |
1204
+ Initialize-Disk -PartitionStyle GPT -PassThru |
1009
1205
  New-Partition -AssignDriveLetter -UseMaximumSize |
1010
1206
  Format-Volume -FileSystem NTFS -Confirm:$false >> $logfile
1011
1207
 
@@ -1307,6 +1503,28 @@ module Kitchen
1307
1503
 
1308
1504
  private
1309
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
+
1310
1528
  # Wrap the transport's `connection` method with Instance Connect setup.
1311
1529
  #
1312
1530
  # A pushed Instance Connect key expires after about a minute, so the key
@@ -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.10".freeze
22
+ EC2_VERSION = "3.23.1".freeze
23
23
  end
24
24
  end
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.22.10
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-08-23 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: aws-sdk-ec2