kitchen-ec2 3.22.3 → 3.22.4
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/client.rb +23 -1
- data/lib/kitchen/driver/aws/dedicated_hosts.rb +74 -29
- data/lib/kitchen/driver/aws/instance_connect.rb +16 -0
- data/lib/kitchen/driver/aws/instance_generator.rb +42 -20
- data/lib/kitchen/driver/aws/ssm_session_manager.rb +20 -3
- data/lib/kitchen/driver/aws/standard_platform/alma.rb +21 -2
- data/lib/kitchen/driver/aws/standard_platform/amazon.rb +26 -3
- data/lib/kitchen/driver/aws/standard_platform/amazon2.rb +26 -3
- data/lib/kitchen/driver/aws/standard_platform/amazon2023.rb +26 -3
- data/lib/kitchen/driver/aws/standard_platform/centos.rb +35 -2
- data/lib/kitchen/driver/aws/standard_platform/debian.rb +44 -5
- data/lib/kitchen/driver/aws/standard_platform/fedora.rb +21 -2
- data/lib/kitchen/driver/aws/standard_platform/freebsd.rb +27 -2
- data/lib/kitchen/driver/aws/standard_platform/macos.rb +30 -4
- data/lib/kitchen/driver/aws/standard_platform/rhel.rb +40 -2
- data/lib/kitchen/driver/aws/standard_platform/rocky.rb +30 -3
- data/lib/kitchen/driver/aws/standard_platform/ubuntu.rb +21 -2
- data/lib/kitchen/driver/aws/standard_platform/windows.rb +67 -34
- data/lib/kitchen/driver/aws/standard_platform.rb +46 -2
- data/lib/kitchen/driver/ec2.rb +332 -10
- data/lib/kitchen/driver/ec2_version.rb +1 -1
- metadata +2 -2
data/lib/kitchen/driver/ec2.rb
CHANGED
|
@@ -49,6 +49,7 @@ require "socket" unless defined?(Socket)
|
|
|
49
49
|
require "shellwords" unless defined?(Shellwords)
|
|
50
50
|
|
|
51
51
|
module Kitchen
|
|
52
|
+
# Namespace for Test Kitchen driver plugins.
|
|
52
53
|
module Driver
|
|
53
54
|
# Amazon EC2 driver for Test Kitchen.
|
|
54
55
|
#
|
|
@@ -107,15 +108,33 @@ module Kitchen
|
|
|
107
108
|
|
|
108
109
|
include Kitchen::Driver::Mixins::DedicatedHosts
|
|
109
110
|
|
|
111
|
+
# @param args [Array] passed through to {Kitchen::Driver::Base}
|
|
112
|
+
# @param block [Proc] passed through to {Kitchen::Driver::Base}
|
|
110
113
|
def initialize(*args, &block)
|
|
111
114
|
super
|
|
112
115
|
end
|
|
113
116
|
|
|
117
|
+
# Warn that a config key is deprecated but still honored.
|
|
118
|
+
#
|
|
119
|
+
# @param driver [Kitchen::Driver::Ec2] the driver being validated
|
|
120
|
+
# @param old_key [Symbol] the deprecated key
|
|
121
|
+
# @param new_key [String, Symbol] what to use instead
|
|
122
|
+
# @return [void]
|
|
114
123
|
def self.validation_warn(driver, old_key, new_key)
|
|
115
124
|
driver.warn "WARN: The driver[#{driver.class.name}] config key `#{old_key}` " \
|
|
116
125
|
"is deprecated, please use `#{new_key}`"
|
|
117
126
|
end
|
|
118
127
|
|
|
128
|
+
# Report that a config key has been removed, and stop.
|
|
129
|
+
#
|
|
130
|
+
# Continuing would silently ignore a setting the user believes is in
|
|
131
|
+
# effect, which for keys like `ebs_volume_size` changes the shape of the
|
|
132
|
+
# instance that gets built.
|
|
133
|
+
#
|
|
134
|
+
# @param driver [Kitchen::Driver::Ec2] the driver being validated
|
|
135
|
+
# @param old_key [Symbol] the removed key
|
|
136
|
+
# @param new_key [String, Symbol] what to use instead
|
|
137
|
+
# @return [void] never returns; terminates the process with `exit!`
|
|
119
138
|
def self.validation_error(driver, old_key, new_key)
|
|
120
139
|
warn "ERROR: The driver[#{driver.class.name}] config key `#{old_key}` " \
|
|
121
140
|
"has been removed, please use `#{new_key}`"
|
|
@@ -220,6 +239,20 @@ module Kitchen
|
|
|
220
239
|
end
|
|
221
240
|
end
|
|
222
241
|
|
|
242
|
+
# Create an EC2 instance and wait until it can be connected to.
|
|
243
|
+
#
|
|
244
|
+
# Auto-creates a security group and key pair when none were configured,
|
|
245
|
+
# allocates a dedicated host if `tenancy: host` requires one, requests
|
|
246
|
+
# either an on-demand or a spot instance, then waits for the instance to
|
|
247
|
+
# exist, become ready, and accept a transport connection.
|
|
248
|
+
#
|
|
249
|
+
# Any failure destroys the instance and everything auto-created alongside
|
|
250
|
+
# it, so that a failed create does not leave billable resources behind.
|
|
251
|
+
#
|
|
252
|
+
# @param state [Hash] the instance state, updated in place with
|
|
253
|
+
# `:server_id`, `:hostname` and any auto-created credentials
|
|
254
|
+
# @return [void]
|
|
255
|
+
# @raise [RuntimeError] wrapping whatever went wrong, after cleaning up
|
|
223
256
|
def create(state)
|
|
224
257
|
return if state[:server_id]
|
|
225
258
|
|
|
@@ -308,6 +341,15 @@ module Kitchen
|
|
|
308
341
|
raise "#{e.message} in the specified region #{config[:region]}. Please check this AMI is available in this region."
|
|
309
342
|
end
|
|
310
343
|
|
|
344
|
+
# Terminate the instance and clean up everything created alongside it.
|
|
345
|
+
#
|
|
346
|
+
# An instance that no longer exists is treated as success, since `kitchen
|
|
347
|
+
# destroy` is also how a failed create is cleaned up. Termination is
|
|
348
|
+
# waited on only when an auto-created security group needs removing, as
|
|
349
|
+
# the group cannot be deleted while an instance still references it.
|
|
350
|
+
#
|
|
351
|
+
# @param state [Hash] the instance state, cleaned up in place
|
|
352
|
+
# @return [void]
|
|
311
353
|
def destroy(state)
|
|
312
354
|
if state[:server_id]
|
|
313
355
|
server = ec2.get_instance(state[:server_id])
|
|
@@ -350,6 +392,12 @@ module Kitchen
|
|
|
350
392
|
empty_hosts.each { |host| deallocate_host(host.host_id) }
|
|
351
393
|
end
|
|
352
394
|
|
|
395
|
+
# The EC2 image this instance will be created from.
|
|
396
|
+
#
|
|
397
|
+
# @return [Aws::EC2::Image]
|
|
398
|
+
# @raise [RuntimeError] when neither `image_id` nor `image_search` yielded
|
|
399
|
+
# an image, which happens when the platform name is not recognized and
|
|
400
|
+
# no explicit search was configured
|
|
353
401
|
def image
|
|
354
402
|
return @image if defined?(@image)
|
|
355
403
|
|
|
@@ -365,6 +413,12 @@ module Kitchen
|
|
|
365
413
|
@image
|
|
366
414
|
end
|
|
367
415
|
|
|
416
|
+
# The instance type to use when the user did not choose one.
|
|
417
|
+
#
|
|
418
|
+
# t2 instances require a hardware-virtualized image, so a paravirtual
|
|
419
|
+
# image falls back to the older t1 family.
|
|
420
|
+
#
|
|
421
|
+
# @return [String] a free-tier instance type
|
|
368
422
|
def default_instance_type
|
|
369
423
|
@instance_type ||= if image && image.virtualization_type == "hvm"
|
|
370
424
|
info("instance_type not specified. Using free tier t2.micro instance ...")
|
|
@@ -376,11 +430,22 @@ module Kitchen
|
|
|
376
430
|
end
|
|
377
431
|
end
|
|
378
432
|
|
|
379
|
-
# The
|
|
433
|
+
# The platform detected from the image actually being used.
|
|
434
|
+
#
|
|
435
|
+
# This can differ from {#desired_platform}: the user asks for "ubuntu" and
|
|
436
|
+
# gets whichever Ubuntu release the search matched. It is the source of
|
|
437
|
+
# the default SSH username.
|
|
438
|
+
#
|
|
439
|
+
# @return [Kitchen::Driver::Aws::StandardPlatform, nil] nil when no
|
|
440
|
+
# platform recognizes the image
|
|
380
441
|
def actual_platform
|
|
381
442
|
@actual_platform ||= Aws::StandardPlatform.from_image(self, image) if image
|
|
382
443
|
end
|
|
383
444
|
|
|
445
|
+
# The platform requested by the Test Kitchen platform name.
|
|
446
|
+
#
|
|
447
|
+
# @return [Kitchen::Driver::Aws::StandardPlatform, nil] nil when the
|
|
448
|
+
# platform name is not one the driver knows how to search for
|
|
384
449
|
def desired_platform
|
|
385
450
|
@desired_platform ||= begin
|
|
386
451
|
platform = Aws::StandardPlatform.from_platform_string(self, instance.platform.name)
|
|
@@ -392,6 +457,12 @@ module Kitchen
|
|
|
392
457
|
end
|
|
393
458
|
end
|
|
394
459
|
|
|
460
|
+
# Search for an image matching the requested platform.
|
|
461
|
+
#
|
|
462
|
+
# Falls back to searching for Ubuntu when the platform name is not
|
|
463
|
+
# recognized, so that `kitchen create` still does something useful.
|
|
464
|
+
#
|
|
465
|
+
# @return [String, nil] the image ID, or nil when the search matched nothing
|
|
395
466
|
def default_ami
|
|
396
467
|
@default_ami ||= begin
|
|
397
468
|
search_platform = desired_platform ||
|
|
@@ -401,6 +472,13 @@ module Kitchen
|
|
|
401
472
|
end
|
|
402
473
|
end
|
|
403
474
|
|
|
475
|
+
# Record the platform's default SSH username in the instance state.
|
|
476
|
+
#
|
|
477
|
+
# Only applied when the transport is still using its own default username,
|
|
478
|
+
# so that a username the user configured is never overwritten.
|
|
479
|
+
#
|
|
480
|
+
# @param state [Hash] the instance state, updated in place
|
|
481
|
+
# @return [void]
|
|
404
482
|
def update_username(state)
|
|
405
483
|
# BUG: With the following equality condition on username, if the user specifies 'root'
|
|
406
484
|
# as the transport's username then we will overwrite that value with one from the standard
|
|
@@ -416,6 +494,9 @@ module Kitchen
|
|
|
416
494
|
end
|
|
417
495
|
end
|
|
418
496
|
|
|
497
|
+
# The EC2 client wrapper, configured from the driver config.
|
|
498
|
+
#
|
|
499
|
+
# @return [Kitchen::Driver::Aws::Client]
|
|
419
500
|
def ec2
|
|
420
501
|
@ec2 ||= Aws::Client.new(
|
|
421
502
|
config[:region],
|
|
@@ -426,11 +507,20 @@ module Kitchen
|
|
|
426
507
|
)
|
|
427
508
|
end
|
|
428
509
|
|
|
510
|
+
# A generator for the RunInstances payload.
|
|
511
|
+
#
|
|
512
|
+
# @note Deliberately reassigned rather than memoized with `||=`: spot
|
|
513
|
+
# requests retry against a rewritten {#config}, and a cached generator
|
|
514
|
+
# would keep building the payload from the config of the first attempt.
|
|
515
|
+
#
|
|
516
|
+
# @return [Kitchen::Driver::Aws::InstanceGenerator]
|
|
429
517
|
def instance_generator
|
|
430
518
|
@instance_generator = Aws::InstanceGenerator.new(config, ec2, instance.logger)
|
|
431
519
|
end
|
|
432
520
|
|
|
433
|
-
#
|
|
521
|
+
# Request a single on-demand instance.
|
|
522
|
+
#
|
|
523
|
+
# @return [Aws::EC2::Instance] the newly requested instance
|
|
434
524
|
def submit_server
|
|
435
525
|
instance_data = instance_generator.ec2_instance_data
|
|
436
526
|
debug("Creating EC2 instance in region #{config[:region]} with properties:")
|
|
@@ -441,13 +531,29 @@ module Kitchen
|
|
|
441
531
|
ec2.create_instance(instance_data)
|
|
442
532
|
end
|
|
443
533
|
|
|
534
|
+
# The driver config.
|
|
535
|
+
#
|
|
536
|
+
# {#submit_spots} overrides this with a rewritten config while trying each
|
|
537
|
+
# instance type and subnet combination, so the generator and the rest of
|
|
538
|
+
# the driver see the variant currently being attempted.
|
|
539
|
+
#
|
|
540
|
+
# @return [Hash] the config in effect
|
|
444
541
|
def config
|
|
445
542
|
return super unless @config
|
|
446
543
|
|
|
447
544
|
@config
|
|
448
545
|
end
|
|
449
546
|
|
|
450
|
-
#
|
|
547
|
+
# Expand a config whose value for `key` is a list into one config per
|
|
548
|
+
# element.
|
|
549
|
+
#
|
|
550
|
+
# Used to turn `instance_type: [a, b]` into two candidate configs to try
|
|
551
|
+
# in turn. The original config is cloned rather than mutated.
|
|
552
|
+
#
|
|
553
|
+
# @param conf [Hash] the config to expand
|
|
554
|
+
# @param key [Symbol] the key that may hold a list
|
|
555
|
+
# @return [Array<Hash>] one config per value, or `[conf]` when the value
|
|
556
|
+
# is not a list
|
|
451
557
|
def expand_config(conf, key)
|
|
452
558
|
configs = []
|
|
453
559
|
|
|
@@ -465,6 +571,15 @@ module Kitchen
|
|
|
465
571
|
configs
|
|
466
572
|
end
|
|
467
573
|
|
|
574
|
+
# Request a spot instance, trying each viable configuration in turn.
|
|
575
|
+
#
|
|
576
|
+
# Spot capacity is per instance type and per availability zone, so a
|
|
577
|
+
# request can fail for reasons that a different type or subnet would
|
|
578
|
+
# satisfy. Every combination of instance type and subnet is attempted
|
|
579
|
+
# before giving up, and all the failures are reported together.
|
|
580
|
+
#
|
|
581
|
+
# @return [Aws::EC2::Instance] the first instance that could be fulfilled
|
|
582
|
+
# @raise [RuntimeError] listing every failure when none could be fulfilled
|
|
468
583
|
def submit_spots
|
|
469
584
|
configs = [config]
|
|
470
585
|
expanded = []
|
|
@@ -518,6 +633,18 @@ module Kitchen
|
|
|
518
633
|
raise ["Could not create a spot instance:", errs].flatten.join("\n")
|
|
519
634
|
end
|
|
520
635
|
|
|
636
|
+
# Request a single spot instance for the current config.
|
|
637
|
+
#
|
|
638
|
+
# A `spot_price` of "ondemand" or "on-demand" requests a spot instance
|
|
639
|
+
# with no price cap, which EC2 expresses by omitting `max_price`.
|
|
640
|
+
#
|
|
641
|
+
# `create_instances` is used rather than `request_spot_instances` because
|
|
642
|
+
# only the former can tag an instance at creation time; the retry loop
|
|
643
|
+
# compensates for its lack of built-in waiting.
|
|
644
|
+
#
|
|
645
|
+
# @return [Aws::EC2::Instance] the newly requested instance
|
|
646
|
+
# @raise [Aws::EC2::Errors::SpotMaxPriceTooLow] when the price could not be
|
|
647
|
+
# satisfied within `spot_wait` seconds
|
|
521
648
|
def submit_spot
|
|
522
649
|
debug("Creating EC2 Spot Instance..")
|
|
523
650
|
instance_data = instance_generator.ec2_instance_data
|
|
@@ -562,8 +689,16 @@ module Kitchen
|
|
|
562
689
|
end
|
|
563
690
|
end
|
|
564
691
|
|
|
565
|
-
#
|
|
566
|
-
#
|
|
692
|
+
# Wait until an instance is genuinely usable.
|
|
693
|
+
#
|
|
694
|
+
# `server.wait_until_running` is not sufficient: an instance can report
|
|
695
|
+
# running before it has an address, and a Windows instance is not usable
|
|
696
|
+
# until its console output says so. The hostname is stored as soon as it
|
|
697
|
+
# is known so that a later failure still leaves enough state to clean up.
|
|
698
|
+
#
|
|
699
|
+
# @param server [Aws::EC2::Instance] the instance to wait on
|
|
700
|
+
# @param state [Hash] the instance state, updated in place
|
|
701
|
+
# @return [void]
|
|
567
702
|
def wait_until_ready(server, state)
|
|
568
703
|
wait_with_destroy(server, state, "to become ready") do |aws_instance|
|
|
569
704
|
hostname = hostname(aws_instance, config[:interface])
|
|
@@ -598,8 +733,18 @@ module Kitchen
|
|
|
598
733
|
end
|
|
599
734
|
end
|
|
600
735
|
|
|
601
|
-
# Poll a block
|
|
602
|
-
#
|
|
736
|
+
# Poll until a block returns true, destroying the instance if it never does.
|
|
737
|
+
#
|
|
738
|
+
# An instance that never becomes ready would otherwise keep running and
|
|
739
|
+
# accruing charges after Test Kitchen gave up on it.
|
|
740
|
+
#
|
|
741
|
+
# @param server [Aws::EC2::Instance] the instance to wait on
|
|
742
|
+
# @param state [Hash] the instance state
|
|
743
|
+
# @param status_msg [String] what is being waited for, for log messages
|
|
744
|
+
# @yieldparam aws_instance [Aws::EC2::Instance] the instance being polled
|
|
745
|
+
# @yieldreturn [Boolean] true when the wait is over
|
|
746
|
+
# @return [void]
|
|
747
|
+
# @raise [Aws::Waiters::Errors::WaiterFailed] after destroying the instance
|
|
603
748
|
def wait_with_destroy(server, state, status_msg, &block)
|
|
604
749
|
wait_log = proc do |attempts|
|
|
605
750
|
c = attempts * config[:retryable_sleep]
|
|
@@ -623,6 +768,14 @@ module Kitchen
|
|
|
623
768
|
end
|
|
624
769
|
end
|
|
625
770
|
|
|
771
|
+
# Wait for and decrypt the generated Windows administrator password.
|
|
772
|
+
#
|
|
773
|
+
# EC2 returns blank password data until the password is available, so this
|
|
774
|
+
# polls first and then decrypts with the instance's private key.
|
|
775
|
+
#
|
|
776
|
+
# @param server [Aws::EC2::Instance] the instance
|
|
777
|
+
# @param state [Hash] the instance state, updated in place with `:password`
|
|
778
|
+
# @return [void]
|
|
626
779
|
def fetch_windows_admin_password(server, state)
|
|
627
780
|
wait_with_destroy(server, state, "to fetch windows admin password") do |_aws_instance|
|
|
628
781
|
enc = server.client.get_password_data(
|
|
@@ -638,6 +791,14 @@ module Kitchen
|
|
|
638
791
|
info("Retrieved Windows password for instance <#{state[:server_id]}>.")
|
|
639
792
|
end
|
|
640
793
|
|
|
794
|
+
# Retry a block with quadratic backoff when EC2 throttles the request.
|
|
795
|
+
#
|
|
796
|
+
# Only throttling is retried; any other error is re-raised immediately so
|
|
797
|
+
# that a genuine failure is not delayed by five pointless retries.
|
|
798
|
+
#
|
|
799
|
+
# @param state [Hash] the instance state, used for log messages
|
|
800
|
+
# @yieldreturn [Object] the block's value
|
|
801
|
+
# @return [Object] the block's value
|
|
641
802
|
def with_request_limit_backoff(state)
|
|
642
803
|
retries = 0
|
|
643
804
|
begin
|
|
@@ -653,10 +814,11 @@ module Kitchen
|
|
|
653
814
|
end
|
|
654
815
|
end
|
|
655
816
|
|
|
817
|
+
# Mapping from the `interface` config value to the EC2 instance attribute
|
|
818
|
+
# holding that address, in the order they are preferred when no interface
|
|
819
|
+
# was requested.
|
|
656
820
|
#
|
|
657
|
-
#
|
|
658
|
-
# when looking up hostname.
|
|
659
|
-
#
|
|
821
|
+
# @return [Hash{String => String}]
|
|
660
822
|
INTERFACE_TYPES =
|
|
661
823
|
{
|
|
662
824
|
"dns" => "public_dns_name",
|
|
@@ -671,6 +833,16 @@ module Kitchen
|
|
|
671
833
|
# that interface to lookup hostname. Otherwise, try ordered list of
|
|
672
834
|
# options.
|
|
673
835
|
#
|
|
836
|
+
# The address to connect to an instance on.
|
|
837
|
+
#
|
|
838
|
+
# With no interface type, {INTERFACE_TYPES} is walked in order and the
|
|
839
|
+
# first populated value wins. AWS returns an empty string rather than nil
|
|
840
|
+
# for an address that is not assigned yet, so empty values are skipped.
|
|
841
|
+
#
|
|
842
|
+
# @param server [Aws::EC2::Instance] the instance
|
|
843
|
+
# @param interface_type [String, nil] one of the keys of {INTERFACE_TYPES}
|
|
844
|
+
# @return [String, nil] the address, or nil when none is available yet
|
|
845
|
+
# @raise [Kitchen::UserError] when `interface_type` is not recognized
|
|
674
846
|
def hostname(server, interface_type = nil)
|
|
675
847
|
if interface_type
|
|
676
848
|
interface_type = INTERFACE_TYPES.fetch(interface_type) do
|
|
@@ -691,10 +863,20 @@ module Kitchen
|
|
|
691
863
|
#
|
|
692
864
|
# Returns the sudo command to use or empty string if sudo is not configured
|
|
693
865
|
#
|
|
866
|
+
# The command used to elevate privileges, if any.
|
|
867
|
+
#
|
|
868
|
+
# @return [String] the sudo command, or an empty string when sudo is off
|
|
694
869
|
def sudo_command
|
|
695
870
|
instance.provisioner[:sudo] ? instance.provisioner[:sudo_command].to_s : ""
|
|
696
871
|
end
|
|
697
872
|
|
|
873
|
+
# Write the Ohai EC2 hint file on the instance.
|
|
874
|
+
#
|
|
875
|
+
# Chef's `ec2` Ohai plugin only collects EC2 metadata when this hint file
|
|
876
|
+
# is present, so it is created for Chef provisioners.
|
|
877
|
+
#
|
|
878
|
+
# @param state [Hash] the instance state
|
|
879
|
+
# @return [void]
|
|
698
880
|
def create_ec2_json(state)
|
|
699
881
|
if windows_os?
|
|
700
882
|
cmd = 'New-Item -Force C:\\chef\\ohai\\hints\\ec2.json -ItemType File'
|
|
@@ -705,6 +887,18 @@ module Kitchen
|
|
|
705
887
|
instance.transport.connection(state).execute(cmd)
|
|
706
888
|
end
|
|
707
889
|
|
|
890
|
+
# The default PowerShell user data script for Windows instances.
|
|
891
|
+
#
|
|
892
|
+
# Enables PS remoting, opens the WinRM firewall port and configures WinRM
|
|
893
|
+
# limits, without which a freshly created Windows instance cannot be
|
|
894
|
+
# connected to. Handles both EC2Launch (2016+) and the older EC2Config
|
|
895
|
+
# service, which log to different paths.
|
|
896
|
+
#
|
|
897
|
+
# When the transport uses an account other than Administrator, a matching
|
|
898
|
+
# local account is created and password complexity is relaxed first, since
|
|
899
|
+
# a generated password may not satisfy the default policy.
|
|
900
|
+
#
|
|
901
|
+
# @return [String] a PowerShell script wrapped in `<powershell>` tags
|
|
708
902
|
def default_windows_user_data
|
|
709
903
|
base_script = Kitchen::Util.outdent!(<<-EOH)
|
|
710
904
|
$OSVersion = (get-itemproperty -Path "HKLM:\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion" -Name ProductName).ProductName
|
|
@@ -768,6 +962,9 @@ module Kitchen
|
|
|
768
962
|
EOH
|
|
769
963
|
end
|
|
770
964
|
|
|
965
|
+
# Log which image was chosen and what platform was detected on it.
|
|
966
|
+
#
|
|
967
|
+
# @return [void]
|
|
771
968
|
def show_chosen_image
|
|
772
969
|
# Print some debug stuff
|
|
773
970
|
debug("Image for #{instance.name}: #{image.name}. #{image_info(image)}")
|
|
@@ -780,6 +977,10 @@ module Kitchen
|
|
|
780
977
|
end
|
|
781
978
|
end
|
|
782
979
|
|
|
980
|
+
# A one-line summary of the attributes that drive image selection.
|
|
981
|
+
#
|
|
982
|
+
# @param image [Aws::EC2::Image] the image to describe
|
|
983
|
+
# @return [String] architecture, virtualization, storage and creation date
|
|
783
984
|
def image_info(image)
|
|
784
985
|
root_device = image.block_device_mappings
|
|
785
986
|
.find { |b| b.device_name == image.root_device_name }
|
|
@@ -920,6 +1121,15 @@ module Kitchen
|
|
|
920
1121
|
state[:ssh_key] = key_path
|
|
921
1122
|
end
|
|
922
1123
|
|
|
1124
|
+
# Attach a pre-existing elastic network interface to the instance.
|
|
1125
|
+
#
|
|
1126
|
+
# Attached at device index 1, leaving index 0 for the primary interface.
|
|
1127
|
+
# An interface that is already attached is left alone, and one that does
|
|
1128
|
+
# not exist is reported without failing the run, since the instance itself
|
|
1129
|
+
# is already up by this point.
|
|
1130
|
+
#
|
|
1131
|
+
# @param state [Hash] the instance state
|
|
1132
|
+
# @return [void]
|
|
923
1133
|
def attach_network_interface(state)
|
|
924
1134
|
info("Attaching Network interface <#{config[:elastic_network_interface_id]}> with the instance <#{state[:server_id]}> .")
|
|
925
1135
|
client = ::Aws::EC2::Client.new(region: config[:region])
|
|
@@ -972,6 +1182,14 @@ module Kitchen
|
|
|
972
1182
|
File.unlink("#{config[:kitchen_root]}/.kitchen/#{instance.name}.pem")
|
|
973
1183
|
end
|
|
974
1184
|
|
|
1185
|
+
# Finalize the driver config and install transport overrides.
|
|
1186
|
+
#
|
|
1187
|
+
# Instance Connect and SSM Session Manager both work by wrapping the
|
|
1188
|
+
# transport's connection handling, which has to happen before the
|
|
1189
|
+
# transport is first used.
|
|
1190
|
+
#
|
|
1191
|
+
# @param instance [Kitchen::Instance] the instance this driver serves
|
|
1192
|
+
# @return [self]
|
|
975
1193
|
def finalize_config!(instance)
|
|
976
1194
|
super
|
|
977
1195
|
|
|
@@ -991,6 +1209,18 @@ module Kitchen
|
|
|
991
1209
|
|
|
992
1210
|
private
|
|
993
1211
|
|
|
1212
|
+
# Wrap the transport's `connection` method with Instance Connect setup.
|
|
1213
|
+
#
|
|
1214
|
+
# A pushed Instance Connect key expires after about a minute, so the key
|
|
1215
|
+
# is refreshed and the connection mode re-decided before every connection
|
|
1216
|
+
# rather than once at create time.
|
|
1217
|
+
#
|
|
1218
|
+
# Guarded against being applied twice: the override wraps the previous
|
|
1219
|
+
# method, so applying it again would wrap the wrapper and push the key
|
|
1220
|
+
# more than once per connection.
|
|
1221
|
+
#
|
|
1222
|
+
# @param instance [Kitchen::Instance] the instance whose transport to wrap
|
|
1223
|
+
# @return [void]
|
|
994
1224
|
def instance_connect_setup_override(instance)
|
|
995
1225
|
# Prevent double pushing of the SSH public keys
|
|
996
1226
|
return if instance.transport.respond_to?(:instance_connect_override_applied)
|
|
@@ -1029,6 +1259,14 @@ module Kitchen
|
|
|
1029
1259
|
instance.transport.define_singleton_method(:instance_connect_override_applied) { true }
|
|
1030
1260
|
end
|
|
1031
1261
|
|
|
1262
|
+
# Wrap the InSpec verifier's `call` method with Instance Connect setup.
|
|
1263
|
+
#
|
|
1264
|
+
# InSpec builds its own SSH options rather than going through the Test
|
|
1265
|
+
# Kitchen transport, so it needs the proxy command or public DNS injected
|
|
1266
|
+
# separately from {#instance_connect_setup_override}.
|
|
1267
|
+
#
|
|
1268
|
+
# @param instance [Kitchen::Instance] the instance whose verifier to wrap
|
|
1269
|
+
# @return [void] a no-op unless the verifier is InSpec
|
|
1032
1270
|
def instance_connect_setup_inspec_override(instance)
|
|
1033
1271
|
# Only apply to InSpec verifier
|
|
1034
1272
|
return unless instance.verifier.name.downcase == "inspec"
|
|
@@ -1117,6 +1355,13 @@ module Kitchen
|
|
|
1117
1355
|
instance.verifier.define_singleton_method(:instance_connect_inspec_override_applied) { true }
|
|
1118
1356
|
end
|
|
1119
1357
|
|
|
1358
|
+
# Prepare an instance for its first Instance Connect connection.
|
|
1359
|
+
#
|
|
1360
|
+
# Chooses between tunnelling through an Instance Connect endpoint and
|
|
1361
|
+
# connecting directly over public DNS, then pushes the SSH key.
|
|
1362
|
+
#
|
|
1363
|
+
# @param state [Hash] the instance state, updated in place
|
|
1364
|
+
# @return [void]
|
|
1120
1365
|
def instance_connect_setup_ready(state)
|
|
1121
1366
|
# Determine whether to use proxy command or direct SSH based on endpoint availability
|
|
1122
1367
|
if instance_connect_endpoint_available?(state)
|
|
@@ -1133,6 +1378,17 @@ module Kitchen
|
|
|
1133
1378
|
instance_connect_refresh_key(state)
|
|
1134
1379
|
end
|
|
1135
1380
|
|
|
1381
|
+
# Push the SSH public key to the instance again.
|
|
1382
|
+
#
|
|
1383
|
+
# Instance Connect keys are accepted for roughly sixty seconds, so this
|
|
1384
|
+
# runs before each connection. A failure is warned about rather than
|
|
1385
|
+
# raised, because the key may still be valid from a previous push.
|
|
1386
|
+
#
|
|
1387
|
+
# @note Shells out to the AWS CLI rather than using the SDK client in
|
|
1388
|
+
# {Kitchen::Driver::Aws::InstanceConnect}.
|
|
1389
|
+
#
|
|
1390
|
+
# @param state [Hash] the instance state
|
|
1391
|
+
# @return [void] a no-op when no SSH key is known
|
|
1136
1392
|
def instance_connect_refresh_key(state)
|
|
1137
1393
|
# Extract public key from the key that was already set up
|
|
1138
1394
|
key_path = state[:ssh_key] || instance.transport[:ssh_key]
|
|
@@ -1163,6 +1419,14 @@ module Kitchen
|
|
|
1163
1419
|
end
|
|
1164
1420
|
end
|
|
1165
1421
|
|
|
1422
|
+
# Configure SSH to tunnel through an Instance Connect endpoint.
|
|
1423
|
+
#
|
|
1424
|
+
# Used for instances with no public address: the AWS CLI opens a tunnel
|
|
1425
|
+
# that SSH is pointed at as a proxy command.
|
|
1426
|
+
#
|
|
1427
|
+
# @param state [Hash] the instance state, updated in place with
|
|
1428
|
+
# `:ssh_proxy_command` and `:instance_connect_config`
|
|
1429
|
+
# @return [void]
|
|
1166
1430
|
def instance_connect_configure_ssh_proxy_command(state)
|
|
1167
1431
|
info("[AWS EC2 Instance Connect] Configuring proxy command mode (tunnel)")
|
|
1168
1432
|
|
|
@@ -1198,6 +1462,15 @@ module Kitchen
|
|
|
1198
1462
|
}
|
|
1199
1463
|
end
|
|
1200
1464
|
|
|
1465
|
+
# Whether an Instance Connect endpoint can be used for this instance.
|
|
1466
|
+
#
|
|
1467
|
+
# A configured endpoint ID is trusted without a lookup. Otherwise the
|
|
1468
|
+
# instance's VPC is searched for a completed endpoint. Instance Connect
|
|
1469
|
+
# endpoints are not available in every region or to every IAM principal,
|
|
1470
|
+
# so a rejected lookup means "no endpoint" rather than an error.
|
|
1471
|
+
#
|
|
1472
|
+
# @param state [Hash] the instance state
|
|
1473
|
+
# @return [Boolean]
|
|
1201
1474
|
def instance_connect_endpoint_available?(state)
|
|
1202
1475
|
# If explicitly configured, respect that configuration
|
|
1203
1476
|
return true if config[:instance_connect_endpoint_id]
|
|
@@ -1222,6 +1495,10 @@ module Kitchen
|
|
|
1222
1495
|
end
|
|
1223
1496
|
end
|
|
1224
1497
|
|
|
1498
|
+
# The VPC an instance belongs to.
|
|
1499
|
+
#
|
|
1500
|
+
# @param state [Hash] the instance state
|
|
1501
|
+
# @return [String, nil] the VPC ID, or nil when it cannot be determined
|
|
1225
1502
|
def get_vpc_id_for_instance(state)
|
|
1226
1503
|
# Get the instance details to find its VPC
|
|
1227
1504
|
return unless state[:server_id]
|
|
@@ -1237,6 +1514,14 @@ module Kitchen
|
|
|
1237
1514
|
end
|
|
1238
1515
|
end
|
|
1239
1516
|
|
|
1517
|
+
# Configure SSH to connect straight to the instance's public DNS name.
|
|
1518
|
+
#
|
|
1519
|
+
# Used when no Instance Connect endpoint is available. When the instance
|
|
1520
|
+
# has no public DNS name there is nothing to switch to, so the existing
|
|
1521
|
+
# hostname is kept and a warning is logged.
|
|
1522
|
+
#
|
|
1523
|
+
# @param state [Hash] the instance state, updated in place
|
|
1524
|
+
# @return [void]
|
|
1240
1525
|
def instance_connect_configure_direct_ssh(state)
|
|
1241
1526
|
# For direct SSH, we need to ensure the hostname is the public DNS name
|
|
1242
1527
|
# and configure SSH options appropriately
|
|
@@ -1261,6 +1546,15 @@ module Kitchen
|
|
|
1261
1546
|
end
|
|
1262
1547
|
end
|
|
1263
1548
|
|
|
1549
|
+
# The OpenSSH public key matching a private key.
|
|
1550
|
+
#
|
|
1551
|
+
# Prefers an adjacent `.pub` file, and derives the public half from the
|
|
1552
|
+
# private key otherwise -- keys created by {#create_key} are downloaded
|
|
1553
|
+
# from EC2 as a bare private key with no `.pub` alongside.
|
|
1554
|
+
#
|
|
1555
|
+
# @param private_key_path [String] path to the private key
|
|
1556
|
+
# @return [String] the public key in OpenSSH format
|
|
1557
|
+
# @raise [RuntimeError] when the key cannot be read or parsed
|
|
1264
1558
|
def instance_connect_extract_public_key(private_key_path)
|
|
1265
1559
|
public_key_path = "#{private_key_path}.pub"
|
|
1266
1560
|
|
|
@@ -1278,10 +1572,22 @@ module Kitchen
|
|
|
1278
1572
|
|
|
1279
1573
|
# SSM Session Manager Support Methods
|
|
1280
1574
|
|
|
1575
|
+
# The SSM Session Manager helper.
|
|
1576
|
+
#
|
|
1577
|
+
# @return [Kitchen::Driver::Aws::SsmSessionManager]
|
|
1281
1578
|
def ssm_session_manager
|
|
1282
1579
|
@ssm_session_manager ||= Aws::SsmSessionManager.new(config, instance.logger)
|
|
1283
1580
|
end
|
|
1284
1581
|
|
|
1582
|
+
# Wait for an instance to become reachable over SSM.
|
|
1583
|
+
#
|
|
1584
|
+
# The SSM agent registers itself some time after the instance boots, so
|
|
1585
|
+
# this polls for up to two minutes. A timeout is warned about rather than
|
|
1586
|
+
# raised: the usual cause is a missing IAM instance profile, and the
|
|
1587
|
+
# connection attempt itself gives a clearer error.
|
|
1588
|
+
#
|
|
1589
|
+
# @param state [Hash] the instance state
|
|
1590
|
+
# @return [void]
|
|
1285
1591
|
def ssm_session_manager_setup_ready(state)
|
|
1286
1592
|
info("[AWS SSM Session Manager] Setting up SSM Session Manager connection")
|
|
1287
1593
|
|
|
@@ -1314,6 +1620,14 @@ module Kitchen
|
|
|
1314
1620
|
end
|
|
1315
1621
|
end
|
|
1316
1622
|
|
|
1623
|
+
# Wrap the transport's `connection` method with SSM Session Manager setup.
|
|
1624
|
+
#
|
|
1625
|
+
# SSM connections work by pointing SSH at `aws ssm start-session` as a
|
|
1626
|
+
# proxy command, which needs the instance ID and so cannot be built until
|
|
1627
|
+
# the instance exists.
|
|
1628
|
+
#
|
|
1629
|
+
# @param instance [Kitchen::Instance] the instance whose transport to wrap
|
|
1630
|
+
# @return [void]
|
|
1317
1631
|
def ssm_session_manager_setup_override(instance)
|
|
1318
1632
|
# Prevent double setup
|
|
1319
1633
|
return if instance.transport.respond_to?(:ssm_session_manager_override_applied)
|
|
@@ -1359,6 +1673,14 @@ module Kitchen
|
|
|
1359
1673
|
instance.transport.define_singleton_method(:ssm_session_manager_override_applied) { true }
|
|
1360
1674
|
end
|
|
1361
1675
|
|
|
1676
|
+
# Wrap the InSpec verifier's `call` method with SSM Session Manager setup.
|
|
1677
|
+
#
|
|
1678
|
+
# InSpec builds its own SSH options rather than going through the Test
|
|
1679
|
+
# Kitchen transport, so the proxy command has to be injected separately
|
|
1680
|
+
# from {#ssm_session_manager_setup_override}.
|
|
1681
|
+
#
|
|
1682
|
+
# @param instance [Kitchen::Instance] the instance whose verifier to wrap
|
|
1683
|
+
# @return [void] a no-op unless the verifier is InSpec
|
|
1362
1684
|
def ssm_session_manager_setup_inspec_override(instance)
|
|
1363
1685
|
# Only apply to InSpec verifier
|
|
1364
1686
|
return unless instance.verifier.name.downcase == "inspec"
|
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.
|
|
4
|
+
version: 3.22.4
|
|
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-08-23 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: aws-sdk-ec2
|