kitchen-ec2 3.21.0 → 3.22.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: 5d6b1c659fad578e57f13b71e617d6b1f8dce59393f17287bb0b33fe4894a5f0
4
- data.tar.gz: c99cb5f69df034d26af00ae8a78a2bcbe9999ed67065cdf76efd897c80080c69
3
+ metadata.gz: 7c8c56f3f2ff8b26435d3aa00a714f5fd4e2162b55fc6215d0b0fa3e33299b40
4
+ data.tar.gz: bd381092802c18cffd0b8d717a1b841bb36b6592df0b74759b2e94026dbc0ceb
5
5
  SHA512:
6
- metadata.gz: 508886f38d0c6bba27e1724360e4a0ee4587bd92f14e21341e7c3dba1a40a951b69408cea2214b9b9249e911a31e378c12b2fe49334fe35e5df353fc9ba2f98d
7
- data.tar.gz: d422f9a8542e95c7a3e0fcdcb882898dcbb996a6f556a5445ae96fb63fa149b81cbc39136216cb047886edb215c83f8ed5d090d4811a6aec6758e5fd5f9db7fb
6
+ metadata.gz: 93a6301a6baf2e40344114ba80bf384c6a43542c0029118d5e1ac07f553d357f5aaec6ebf85f5274f45242cbee5a93f048539344c456deb1163f3b03d101bdb7
7
+ data.tar.gz: c8ffd30e4895ccf20513d28c20689cbe7fa7b16cc3b31ebde745193439b9429cb9b2005d5279998de1464e0715928c292ee2aa37108f267c7f89f4ac1fadeac4
@@ -0,0 +1,85 @@
1
+ #
2
+ # Author:: GitHub Copilot
3
+ #
4
+ # Copyright:: 2025, GitHub
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # https://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ require "aws-sdk-ssm"
19
+ require "open3"
20
+
21
+ module Kitchen
22
+ module Driver
23
+ class Aws
24
+ # Manages AWS Systems Manager Session Manager connections for Test Kitchen
25
+ class SsmSessionManager
26
+ def initialize(config, logger)
27
+ @config = config
28
+ @logger = logger
29
+ @ssm_client = ::Aws::SSM::Client.new(
30
+ region: config[:region],
31
+ profile: config[:shared_credentials_profile]
32
+ )
33
+ end
34
+
35
+ # Check if SSM agent is running on the instance
36
+ def ssm_agent_available?(instance_id)
37
+ @logger.debug("Checking if SSM agent is available on instance #{instance_id}")
38
+
39
+ begin
40
+ resp = @ssm_client.describe_instance_information(
41
+ filters: [
42
+ {
43
+ key: "InstanceIds",
44
+ values: [instance_id],
45
+ },
46
+ ]
47
+ )
48
+
49
+ available = !resp.instance_information_list.empty? &&
50
+ resp.instance_information_list.first.ping_status == "Online"
51
+
52
+ if available
53
+ @logger.info("SSM agent is available on instance #{instance_id}")
54
+ else
55
+ @logger.warn("SSM agent is not available on instance #{instance_id}")
56
+ end
57
+
58
+ available
59
+ rescue ::Aws::SSM::Errors::ServiceError => e
60
+ @logger.warn("Error checking SSM agent status: #{e.message}")
61
+ false
62
+ end
63
+ end
64
+
65
+ # Verify that the AWS CLI session manager plugin is installed
66
+ def session_manager_plugin_installed?
67
+ _output, status = Open3.capture2e("session-manager-plugin", "--version")
68
+ installed = status.success?
69
+
70
+ if installed
71
+ @logger.debug("Session Manager plugin is installed")
72
+ else
73
+ @logger.warn("Session Manager plugin is not installed. Install it from: " \
74
+ "https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html")
75
+ end
76
+
77
+ installed
78
+ rescue StandardError => e
79
+ @logger.warn("Error checking for session-manager-plugin: #{e.message}")
80
+ false
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -23,17 +23,15 @@ module Kitchen
23
23
  class Debian < StandardPlatform
24
24
  StandardPlatform.platforms["debian"] = self
25
25
 
26
- # 12/13 are listed last since we default to the first item in the hash
27
- # and 12/13 are not released yet. When they're released move them up
28
26
  DEBIAN_CODENAMES = {
27
+ 13 => "trixie",
28
+ 12 => "bookworm",
29
29
  11 => "bullseye",
30
30
  10 => "buster",
31
31
  9 => "stretch",
32
32
  8 => "jessie",
33
33
  7 => "wheezy",
34
34
  6 => "squeeze",
35
- 12 => "bookworm",
36
- 13 => "trixie",
37
35
  }.freeze
38
36
 
39
37
  # default username for this platform's ami
@@ -39,6 +39,7 @@ require_relative "aws/standard_platform/macos"
39
39
  require_relative "aws/standard_platform/ubuntu"
40
40
  require_relative "aws/standard_platform/windows"
41
41
  require_relative "aws/instance_connect"
42
+ require_relative "aws/ssm_session_manager"
42
43
  require "aws-sdk-ec2"
43
44
  require "aws-sdk-core/waiters/errors"
44
45
  require "retryable" unless defined?(Retryable)
@@ -101,6 +102,8 @@ module Kitchen
101
102
  default_config :use_instance_connect, false
102
103
  default_config :instance_connect_endpoint_id, nil
103
104
  default_config :instance_connect_max_tunnel_duration, 3600
105
+ default_config :use_ssm_session_manager, false
106
+ default_config :ssm_session_manager_document_name, nil
104
107
 
105
108
  include Kitchen::Driver::Mixins::DedicatedHosts
106
109
 
@@ -197,6 +200,15 @@ module Kitchen
197
200
  end
198
201
  end
199
202
 
203
+ # Ensure use_instance_connect and use_ssm_session_manager are not both enabled
204
+ validations[:use_ssm_session_manager] = lambda do |_attr, val, driver|
205
+ if val && driver[:use_instance_connect]
206
+ warn "Cannot use both 'use_instance_connect' and 'use_ssm_session_manager' at the same time. " \
207
+ "Please enable only one transport method."
208
+ exit!
209
+ end
210
+ end
211
+
200
212
  # empty keys cause failures when tagging and they make no sense
201
213
  validations[:tags] = lambda do |_attr, val, _driver|
202
214
  # if someone puts the tags each on their own line it's an array not a hash
@@ -272,7 +284,7 @@ module Kitchen
272
284
  # Waiting can fail, so we have to retry on that.
273
285
  Retryable.retryable(
274
286
  tries: 10,
275
- sleep: lambda { |n| [2**n, 30].min },
287
+ sleep: ->(n) { [2**n, 30].min },
276
288
  on: ::Aws::EC2::Errors::InvalidInstanceIDNotFound
277
289
  ) do |_r, _|
278
290
  wait_until_ready(server, state)
@@ -282,6 +294,8 @@ module Kitchen
282
294
 
283
295
  if config[:use_instance_connect]
284
296
  instance_connect_setup_ready(state)
297
+ elsif config[:use_ssm_session_manager]
298
+ ssm_session_manager_setup_ready(state)
285
299
  end
286
300
 
287
301
  instance.transport.connection(state).wait_until_ready
@@ -456,10 +470,7 @@ module Kitchen
456
470
  expanded = []
457
471
  keys = %i{instance_type}
458
472
 
459
- unless config[:subnet_filter]
460
- # => Use explicitly specified subnets
461
- keys << :subnet_id
462
- else
473
+ if config[:subnet_filter]
463
474
  # => Enable cascading through matching subnets
464
475
  client = ::Aws::EC2::Client.new(region: config[:region])
465
476
 
@@ -484,6 +495,9 @@ module Kitchen
484
495
  new_config[:subnet_filter] = nil
485
496
  new_config
486
497
  end
498
+ else
499
+ # => Use explicitly specified subnets
500
+ keys << :subnet_id
487
501
  end
488
502
 
489
503
  keys.each do |key|
@@ -538,7 +552,7 @@ module Kitchen
538
552
  # not retry if the price could not be satisfied immediately.
539
553
  Retryable.retryable(
540
554
  tries: config[:spot_wait] / config[:retryable_sleep],
541
- sleep: lambda { |_n| config[:retryable_sleep] },
555
+ sleep: ->(_n) { config[:retryable_sleep] },
542
556
  on: ::Aws::EC2::Errors::SpotMaxPriceTooLow
543
557
  ) do |retries|
544
558
  c = retries * config[:retryable_sleep]
@@ -572,12 +586,12 @@ module Kitchen
572
586
  # supplied, try to fetch it from the AWS instance
573
587
  fetch_windows_admin_password(server, state)
574
588
  else
575
- output = server.console_output.output
589
+ output = server.console_output.output || ""
576
590
  unless output.nil?
577
591
  output = Base64.decode64(output)
578
592
  debug "Console output: --- \n#{output}"
579
593
  end
580
- ready = !!(output.include?("Windows is Ready to use"))
594
+ ready = !!output.include?("Windows is Ready to use")
581
595
  end
582
596
  end
583
597
  ready
@@ -683,7 +697,7 @@ module Kitchen
683
697
 
684
698
  def create_ec2_json(state)
685
699
  if windows_os?
686
- cmd = "New-Item -Force C:\\chef\\ohai\\hints\\ec2.json -ItemType File"
700
+ cmd = 'New-Item -Force C:\\chef\\ohai\\hints\\ec2.json -ItemType File'
687
701
  else
688
702
  debug "Using sudo_command='#{sudo_command}' for ohai hints"
689
703
  cmd = "#{sudo_command} mkdir -p /etc/chef/ohai/hints; #{sudo_command} touch /etc/chef/ohai/hints/ec2.json"
@@ -911,16 +925,16 @@ module Kitchen
911
925
  client = ::Aws::EC2::Client.new(region: config[:region])
912
926
  begin
913
927
  check_eni = client.describe_network_interface_attribute({
914
- attribute: "attachment",
915
- network_interface_id: config[:elastic_network_interface_id],
916
- })
928
+ attribute: "attachment",
929
+ network_interface_id: config[:elastic_network_interface_id],
930
+ })
917
931
  if check_eni.attachment.nil?
918
932
  unless state[:server_id].nil?
919
933
  client.attach_network_interface({
920
- device_index: 1,
921
- instance_id: state[:server_id],
922
- network_interface_id: config[:elastic_network_interface_id],
923
- })
934
+ device_index: 1,
935
+ instance_id: state[:server_id],
936
+ network_interface_id: config[:elastic_network_interface_id],
937
+ })
924
938
  info("Attached Network interface <#{config[:elastic_network_interface_id]}> with the instance <#{state[:server_id]}> .")
925
939
  end
926
940
  else
@@ -966,6 +980,10 @@ module Kitchen
966
980
  debug("[AWS EC2 Instance Connect] Setting up Instance Connect overrides")
967
981
  instance_connect_setup_override(instance)
968
982
  instance_connect_setup_inspec_override(instance)
983
+ elsif config[:use_ssm_session_manager]
984
+ debug("[AWS SSM Session Manager] Setting up SSM Session Manager overrides")
985
+ ssm_session_manager_setup_override(instance)
986
+ ssm_session_manager_setup_inspec_override(instance)
969
987
  end
970
988
 
971
989
  self
@@ -1037,7 +1055,6 @@ module Kitchen
1037
1055
 
1038
1056
  # Override runner_options_for_ssh
1039
1057
  define_singleton_method(:runner_options_for_ssh) do |config_data|
1040
-
1041
1058
  # Get the original options
1042
1059
  opts = instance_connect_original_runner_options_for_ssh(config_data)
1043
1060
 
@@ -1076,8 +1093,8 @@ module Kitchen
1076
1093
  if public_dns && !public_dns.empty?
1077
1094
  opts["host"] = public_dns
1078
1095
  opts["ssh_options"] = (opts["ssh_options"] || {}).merge({
1079
- "IdentitiesOnly" => "yes",
1080
- })
1096
+ "IdentitiesOnly" => "yes",
1097
+ })
1081
1098
  driver_instance.info("[AWS EC2 Instance Connect] InSpec using direct SSH to #{public_dns} with IdentitiesOnly=yes")
1082
1099
  else
1083
1100
  driver_instance.warn("[AWS EC2 Instance Connect] No public DNS available for direct SSH mode")
@@ -1207,11 +1224,11 @@ module Kitchen
1207
1224
 
1208
1225
  def get_vpc_id_for_instance(state)
1209
1226
  # Get the instance details to find its VPC
1210
- return nil unless state[:server_id]
1227
+ return unless state[:server_id]
1211
1228
 
1212
1229
  begin
1213
1230
  instance_info = ec2.client.describe_instances(instance_ids: [state[:server_id]]).reservations.first&.instances&.first
1214
- return nil unless instance_info
1231
+ return unless instance_info
1215
1232
 
1216
1233
  instance_info.vpc_id
1217
1234
  rescue => e
@@ -1258,6 +1275,152 @@ module Kitchen
1258
1275
  raise "Unable to extract public key from #{private_key_path}: #{e.message}"
1259
1276
  end
1260
1277
  end
1278
+
1279
+ # SSM Session Manager Support Methods
1280
+
1281
+ def ssm_session_manager
1282
+ @ssm_session_manager ||= Aws::SsmSessionManager.new(config, instance.logger)
1283
+ end
1284
+
1285
+ def ssm_session_manager_setup_ready(state)
1286
+ info("[AWS SSM Session Manager] Setting up SSM Session Manager connection")
1287
+
1288
+ # Verify session manager plugin is installed
1289
+ unless ssm_session_manager.session_manager_plugin_installed?
1290
+ warn("[AWS SSM Session Manager] Session Manager plugin not found. Please install it from: " \
1291
+ "https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html")
1292
+ end
1293
+
1294
+ # Wait for SSM agent to be available
1295
+ max_retries = 12
1296
+ retry_delay = 10
1297
+ retries = 0
1298
+
1299
+ loop do
1300
+ if ssm_session_manager.ssm_agent_available?(state[:server_id])
1301
+ info("[AWS SSM Session Manager] SSM agent is available on instance #{state[:server_id]}")
1302
+ break
1303
+ end
1304
+
1305
+ retries += 1
1306
+ if retries >= max_retries
1307
+ warn("[AWS SSM Session Manager] SSM agent did not become available after #{max_retries * retry_delay} seconds. " \
1308
+ "Ensure the instance has an IAM instance profile with SSM permissions and the SSM agent is running.")
1309
+ break
1310
+ end
1311
+
1312
+ info("[AWS SSM Session Manager] Waiting for SSM agent to be available (attempt #{retries}/#{max_retries})...")
1313
+ sleep retry_delay
1314
+ end
1315
+ end
1316
+
1317
+ def ssm_session_manager_setup_override(instance)
1318
+ # Prevent double setup
1319
+ return if instance.transport.respond_to?(:ssm_session_manager_override_applied)
1320
+
1321
+ # Store reference to driver for use in override
1322
+ driver_instance = self
1323
+ use_ssm = config[:use_ssm_session_manager]
1324
+
1325
+ # Override the transport's connection method to inject SSM setup
1326
+ original_connection = instance.transport.method(:connection)
1327
+
1328
+ instance.transport.define_singleton_method(:connection) do |state, &block|
1329
+ if use_ssm && state[:server_id]
1330
+ # Build SSM start-session command
1331
+ cmd = [
1332
+ "aws", "ssm", "start-session",
1333
+ "--target", state[:server_id],
1334
+ "--region", driver_instance.config[:region]
1335
+ ]
1336
+
1337
+ # Add document name if specified
1338
+ if driver_instance.config[:ssm_session_manager_document_name]
1339
+ cmd += ["--document-name", driver_instance.config[:ssm_session_manager_document_name]]
1340
+ end
1341
+
1342
+ # Add AWS profile if specified
1343
+ if driver_instance.config[:shared_credentials_profile]
1344
+ cmd += ["--profile", driver_instance.config[:shared_credentials_profile]]
1345
+ end
1346
+
1347
+ proxy_command = cmd.join(" ")
1348
+ driver_instance.info("[AWS SSM Session Manager] Using proxy command: #{proxy_command}")
1349
+
1350
+ # Set proxy command for SSH transport
1351
+ state[:ssh_proxy_command] = proxy_command
1352
+ end
1353
+
1354
+ # Call original connection method
1355
+ original_connection.call(state, &block)
1356
+ end
1357
+
1358
+ # Mark as applied
1359
+ instance.transport.define_singleton_method(:ssm_session_manager_override_applied) { true }
1360
+ end
1361
+
1362
+ def ssm_session_manager_setup_inspec_override(instance)
1363
+ # Only apply to InSpec verifier
1364
+ return unless instance.verifier.name.downcase == "inspec"
1365
+ return if instance.verifier.respond_to?(:ssm_session_manager_inspec_override_applied)
1366
+
1367
+ # Store reference to driver for use in override
1368
+ driver_instance = self
1369
+ use_ssm = config[:use_ssm_session_manager]
1370
+
1371
+ # Override the verifier's call method to inject SSM setup
1372
+ original_call = instance.verifier.method(:call)
1373
+
1374
+ instance.verifier.define_singleton_method(:call) do |state|
1375
+ driver_instance.debug("[AWS SSM Session Manager] InSpec call method intercepted")
1376
+
1377
+ # Set up SSM for InSpec if enabled
1378
+ if use_ssm && state[:server_id]
1379
+ # Check if we already have the override method defined
1380
+ unless respond_to?(:ssm_original_runner_options_for_ssh)
1381
+ # Store the original method
1382
+ define_singleton_method(:ssm_original_runner_options_for_ssh, method(:runner_options_for_ssh))
1383
+
1384
+ # Override runner_options_for_ssh
1385
+ define_singleton_method(:runner_options_for_ssh) do |config_data|
1386
+ # Get the original options
1387
+ opts = ssm_original_runner_options_for_ssh(config_data)
1388
+
1389
+ # Inject SSM Session Manager configuration if enabled
1390
+ if use_ssm && config_data[:server_id]
1391
+ # Build SSM start-session command
1392
+ cmd = [
1393
+ "aws", "ssm", "start-session",
1394
+ "--target", config_data[:server_id],
1395
+ "--region", driver_instance.config[:region]
1396
+ ]
1397
+
1398
+ # Add document name if specified
1399
+ if driver_instance.config[:ssm_session_manager_document_name]
1400
+ cmd += ["--document-name", driver_instance.config[:ssm_session_manager_document_name]]
1401
+ end
1402
+
1403
+ # Add AWS profile if specified
1404
+ if driver_instance.config[:shared_credentials_profile]
1405
+ cmd += ["--profile", driver_instance.config[:shared_credentials_profile]]
1406
+ end
1407
+
1408
+ opts["proxy_command"] = cmd.join(" ")
1409
+ driver_instance.info("[AWS SSM Session Manager] InSpec using proxy command: #{opts["proxy_command"]}")
1410
+ end
1411
+
1412
+ opts
1413
+ end
1414
+ end
1415
+ end
1416
+
1417
+ # Call the original method
1418
+ original_call.call(state)
1419
+ end
1420
+
1421
+ # Mark as applied
1422
+ instance.verifier.define_singleton_method(:ssm_session_manager_inspec_override_applied) { true }
1423
+ end
1261
1424
  end
1262
1425
  end
1263
1426
  end
@@ -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.21.0".freeze
22
+ EC2_VERSION = "3.22.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.21.0
4
+ version: 3.22.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: 2025-09-09 00:00:00.000000000 Z
11
+ date: 2026-01-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aws-sdk-ec2
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '1.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: aws-sdk-ssm
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.0'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: retryable
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -81,7 +95,7 @@ dependencies:
81
95
  version: 3.9.0
82
96
  - - "<"
83
97
  - !ruby/object:Gem::Version
84
- version: '4'
98
+ version: '5'
85
99
  type: :runtime
86
100
  prerelease: false
87
101
  version_requirements: !ruby/object:Gem::Requirement
@@ -91,7 +105,7 @@ dependencies:
91
105
  version: 3.9.0
92
106
  - - "<"
93
107
  - !ruby/object:Gem::Version
94
- version: '4'
108
+ version: '5'
95
109
  description: A Test Kitchen Driver for Amazon EC2
96
110
  email:
97
111
  - help@sous-chefs.org
@@ -104,6 +118,7 @@ files:
104
118
  - lib/kitchen/driver/aws/dedicated_hosts.rb
105
119
  - lib/kitchen/driver/aws/instance_connect.rb
106
120
  - lib/kitchen/driver/aws/instance_generator.rb
121
+ - lib/kitchen/driver/aws/ssm_session_manager.rb
107
122
  - lib/kitchen/driver/aws/standard_platform.rb
108
123
  - lib/kitchen/driver/aws/standard_platform/alma.rb
109
124
  - lib/kitchen/driver/aws/standard_platform/amazon.rb