kitchen-ec2 3.20.0 → 3.21.0

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: f47ffd935db82650266d977423d84a3361466c34385adb49dcb782ce8f584b4d
4
- data.tar.gz: 11a0789de1befe3293786fe5d4dcd305cda95517bdc5195738bffb1eefede547
3
+ metadata.gz: 5d6b1c659fad578e57f13b71e617d6b1f8dce59393f17287bb0b33fe4894a5f0
4
+ data.tar.gz: c99cb5f69df034d26af00ae8a78a2bcbe9999ed67065cdf76efd897c80080c69
5
5
  SHA512:
6
- metadata.gz: 37f1555b120b84df599b4c3378c8ac4dc49ca485d08fdaa491c3ffa8ef11e175c4d1cfad534f8529eec4f71683e276062ed4454116279de82ab9ce3638d38426
7
- data.tar.gz: b3c8845339ffc8f823881c4e336114ddd94139b193a192e5cef0fb0e25cd50b57f7bf69a03735f727d1407937527389337ef75aaa609339f891f6fc48f961a82
6
+ metadata.gz: 508886f38d0c6bba27e1724360e4a0ee4587bd92f14e21341e7c3dba1a40a951b69408cea2214b9b9249e911a31e378c12b2fe49334fe35e5df353fc9ba2f98d
7
+ data.tar.gz: d422f9a8542e95c7a3e0fcdcb882898dcbb996a6f556a5445ae96fb63fa149b81cbc39136216cb047886edb215c83f8ed5d090d4811a6aec6758e5fd5f9db7fb
@@ -0,0 +1,44 @@
1
+ #
2
+ # Author:: Alex Kokkinos
3
+ #
4
+ # Copyright:: 2025, Alex Kokkinos
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
+ require "aws-sdk-ec2instanceconnect"
18
+
19
+ module Kitchen
20
+ module Driver
21
+ class Aws
22
+ class InstanceConnect
23
+ def initialize(config, logger)
24
+ @config = config
25
+ @logger = logger
26
+ @client = ::Aws::EC2InstanceConnect::Client.new(region: config[:region])
27
+ end
28
+
29
+ def send_ssh_public_key(instance_id, username, public_key)
30
+ @logger.info("Sending SSH public key to instance #{instance_id} for user #{username}")
31
+
32
+ @client.send_ssh_public_key({
33
+ instance_id: instance_id,
34
+ instance_os_user: username,
35
+ ssh_public_key: public_key,
36
+ availability_zone: @config[:availability_zone],
37
+ })
38
+
39
+ @logger.debug("SSH public key successfully sent to instance")
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -16,6 +16,7 @@
16
16
  # See the License for the specific language governing permissions and
17
17
  # limitations under the License.
18
18
 
19
+ require "sshkey" unless defined?(SSHKey)
19
20
  require "benchmark" unless defined?(Benchmark)
20
21
  require "json" unless defined?(JSON)
21
22
  require "kitchen"
@@ -37,12 +38,14 @@ require_relative "aws/standard_platform/freebsd"
37
38
  require_relative "aws/standard_platform/macos"
38
39
  require_relative "aws/standard_platform/ubuntu"
39
40
  require_relative "aws/standard_platform/windows"
41
+ require_relative "aws/instance_connect"
40
42
  require "aws-sdk-ec2"
41
43
  require "aws-sdk-core/waiters/errors"
42
44
  require "retryable" unless defined?(Retryable)
43
45
  require "time" unless defined?(Time)
44
46
  require "etc" unless defined?(Etc)
45
47
  require "socket" unless defined?(Socket)
48
+ require "shellwords" unless defined?(Shellwords)
46
49
 
47
50
  module Kitchen
48
51
  module Driver
@@ -95,6 +98,9 @@ module Kitchen
95
98
  default_config :skip_cost_warning, false
96
99
  default_config :allocate_dedicated_host, false
97
100
  default_config :deallocate_dedicated_host, false
101
+ default_config :use_instance_connect, false
102
+ default_config :instance_connect_endpoint_id, nil
103
+ default_config :instance_connect_max_tunnel_duration, 3600
98
104
 
99
105
  include Kitchen::Driver::Mixins::DedicatedHosts
100
106
 
@@ -226,7 +232,8 @@ module Kitchen
226
232
  case config[:aws_ssh_key_id]
227
233
  when nil
228
234
  create_key(state)
229
- config[:aws_ssh_key_id] = state[:auto_key_id]
235
+ # Don't set aws_ssh_key_id if using Instance Connect
236
+ config[:aws_ssh_key_id] = state[:auto_key_id] unless config[:use_instance_connect]
230
237
  when "_disable"
231
238
  info("Disabling AWS-managed SSH key pairs for this EC2 instance.")
232
239
  info("The key pairs for the kitchen transport config and the AMI must match.")
@@ -272,6 +279,11 @@ module Kitchen
272
279
  end
273
280
 
274
281
  info("EC2 instance <#{state[:server_id]}> ready (hostname: #{state[:hostname]}).")
282
+
283
+ if config[:use_instance_connect]
284
+ instance_connect_setup_ready(state)
285
+ end
286
+
275
287
  instance.transport.connection(state).wait_until_ready
276
288
  attach_network_interface(state) unless config[:elastic_network_interface_id].nil?
277
289
  create_ec2_json(state) if /chef/i.match?(instance.provisioner.name)
@@ -945,6 +957,307 @@ module Kitchen
945
957
  state.delete(:auto_key_id)
946
958
  File.unlink("#{config[:kitchen_root]}/.kitchen/#{instance.name}.pem")
947
959
  end
960
+
961
+ def finalize_config!(instance)
962
+ super
963
+
964
+ # Set up Instance Connect transport override if configured
965
+ if config[:use_instance_connect]
966
+ debug("[AWS EC2 Instance Connect] Setting up Instance Connect overrides")
967
+ instance_connect_setup_override(instance)
968
+ instance_connect_setup_inspec_override(instance)
969
+ end
970
+
971
+ self
972
+ end
973
+
974
+ private
975
+
976
+ def instance_connect_setup_override(instance)
977
+ # Prevent double pushing of the SSH public keys
978
+ return if instance.transport.respond_to?(:instance_connect_override_applied)
979
+
980
+ # Store reference to driver for use in override
981
+ driver_instance = self
982
+ use_instance_connect = config[:use_instance_connect]
983
+
984
+ # Override the transport's connection method to inject Instance Connect setup
985
+ original_connection = instance.transport.method(:connection)
986
+
987
+ instance.transport.define_singleton_method(:connection) do |state, &block|
988
+ # Set up Instance Connect configuration before every connection
989
+ if use_instance_connect
990
+ # Refresh Instance Connect SSH key
991
+ driver_instance.send(:instance_connect_refresh_key, state)
992
+
993
+ # Configure connection mode based on endpoint availability
994
+ if driver_instance.send(:instance_connect_endpoint_available?, state)
995
+ # Proxy command mode - ensure ssh_proxy_command is set
996
+ unless state[:ssh_proxy_command]
997
+ driver_instance.send(:instance_connect_configure_ssh_proxy_command, state)
998
+ end
999
+ driver_instance.debug("[AWS EC2 Instance Connect] Transport using proxy command mode")
1000
+ else
1001
+ # Direct SSH mode - ensure hostname is set to public DNS
1002
+ driver_instance.send(:instance_connect_configure_direct_ssh, state)
1003
+ driver_instance.debug("[AWS EC2 Instance Connect] Transport using direct SSH mode")
1004
+ end
1005
+ end
1006
+ # Call original connection method
1007
+ original_connection.call(state, &block)
1008
+ end
1009
+
1010
+ # Mark as applied to prevent double pushing of the SSH public keys
1011
+ instance.transport.define_singleton_method(:instance_connect_override_applied) { true }
1012
+ end
1013
+
1014
+ def instance_connect_setup_inspec_override(instance)
1015
+ # Only apply to InSpec verifier
1016
+ return unless instance.verifier.name.downcase == "inspec"
1017
+ return if instance.verifier.respond_to?(:instance_connect_inspec_override_applied)
1018
+
1019
+ # Store reference to driver for use in override
1020
+ driver_instance = self
1021
+ use_instance_connect = config[:use_instance_connect]
1022
+
1023
+ # Override the verifier's call method to inject proxy command setup
1024
+ original_call = instance.verifier.method(:call)
1025
+
1026
+ instance.verifier.define_singleton_method(:call) do |state|
1027
+ driver_instance.debug("[AWS EC2 Instance Connect] InSpec call method intercepted, connecting using kitchen-ec2 driver AWS EC2 Instance Connect")
1028
+ driver_instance.debug("[AWS EC2 Instance Connect] Instance ID: #{state[:server_id]}")
1029
+
1030
+ # If using Instance Connect, set up the override just before the call
1031
+ if use_instance_connect && state[:server_id]
1032
+
1033
+ # Check if we already have the override method defined
1034
+ unless respond_to?(:instance_connect_original_runner_options_for_ssh)
1035
+ # Store the original method
1036
+ define_singleton_method(:instance_connect_original_runner_options_for_ssh, method(:runner_options_for_ssh))
1037
+
1038
+ # Override runner_options_for_ssh
1039
+ define_singleton_method(:runner_options_for_ssh) do |config_data|
1040
+
1041
+ # Get the original options
1042
+ opts = instance_connect_original_runner_options_for_ssh(config_data)
1043
+
1044
+ # Inject Instance Connect configuration if enabled
1045
+ if use_instance_connect && config_data[:server_id]
1046
+ # Refresh Instance Connect SSH key
1047
+ driver_instance.send(:instance_connect_refresh_key, config_data)
1048
+
1049
+ # Check if we should use proxy command or direct SSH
1050
+ if driver_instance.send(:instance_connect_endpoint_available?, config_data)
1051
+ # Build proxy command with the instance ID from state
1052
+ proxy_command = [
1053
+ "aws", "ec2-instance-connect", "open-tunnel",
1054
+ "--instance-id", config_data[:server_id]
1055
+ ]
1056
+
1057
+ # Add optional parameters
1058
+ if driver_instance.config[:instance_connect_endpoint_id]
1059
+ proxy_command += ["--instance-connect-endpoint-id", driver_instance.config[:instance_connect_endpoint_id]]
1060
+ end
1061
+ if driver_instance.config[:instance_connect_max_tunnel_duration]
1062
+ proxy_command += ["--max-tunnel-duration", driver_instance.config[:instance_connect_max_tunnel_duration].to_s]
1063
+ end
1064
+ if driver_instance.config[:shared_credentials_profile]
1065
+ proxy_command += ["--profile", driver_instance.config[:shared_credentials_profile]]
1066
+ end
1067
+ proxy_command += ["--region", driver_instance.config[:region]]
1068
+
1069
+ opts["proxy_command"] = proxy_command.join(" ")
1070
+ driver_instance.info("[AWS EC2 Instance Connect] InSpec using proxy command: #{opts["proxy_command"]}")
1071
+ else
1072
+ # Direct SSH mode - ensure we're using the public DNS and proper SSH options
1073
+ server = driver_instance.ec2.get_instance(config_data[:server_id])
1074
+ public_dns = server&.public_dns_name
1075
+
1076
+ if public_dns && !public_dns.empty?
1077
+ opts["host"] = public_dns
1078
+ opts["ssh_options"] = (opts["ssh_options"] || {}).merge({
1079
+ "IdentitiesOnly" => "yes",
1080
+ })
1081
+ driver_instance.info("[AWS EC2 Instance Connect] InSpec using direct SSH to #{public_dns} with IdentitiesOnly=yes")
1082
+ else
1083
+ driver_instance.warn("[AWS EC2 Instance Connect] No public DNS available for direct SSH mode")
1084
+ end
1085
+ end
1086
+ else
1087
+ driver_instance.info("[AWS EC2 Instance Connect] Not configuring Instance Connect - use_instance_connect: #{use_instance_connect}, server_id present: #{!!config_data[:server_id]}")
1088
+ end
1089
+
1090
+ opts
1091
+ end
1092
+ end
1093
+ end
1094
+
1095
+ # Call the original method
1096
+ original_call.call(state)
1097
+ end
1098
+
1099
+ # Mark as applied to prevent double setup
1100
+ instance.verifier.define_singleton_method(:instance_connect_inspec_override_applied) { true }
1101
+ end
1102
+
1103
+ def instance_connect_setup_ready(state)
1104
+ # Determine whether to use proxy command or direct SSH based on endpoint availability
1105
+ if instance_connect_endpoint_available?(state)
1106
+ # Configure SSH proxy command if not already done
1107
+ instance_connect_configure_ssh_proxy_command(state) unless state[:ssh_proxy_command]
1108
+ info("[AWS EC2 Instance Connect] Using tunnel mode - Instance Connect endpoint available")
1109
+ else
1110
+ # Configure direct SSH with public DNS
1111
+ instance_connect_configure_direct_ssh(state)
1112
+ info("[AWS EC2 Instance Connect] Using direct SSH mode - no Instance Connect endpoint")
1113
+ end
1114
+
1115
+ # Refresh Instance Connect SSH key before connection
1116
+ instance_connect_refresh_key(state)
1117
+ end
1118
+
1119
+ def instance_connect_refresh_key(state)
1120
+ # Extract public key from the key that was already set up
1121
+ key_path = state[:ssh_key] || instance.transport[:ssh_key]
1122
+ return unless key_path
1123
+
1124
+ public_key = instance_connect_extract_public_key(key_path)
1125
+ return unless public_key
1126
+
1127
+ username = state[:username] || actual_platform&.username
1128
+
1129
+ # Build AWS CLI command to send public key
1130
+ cmd = [
1131
+ "aws", "ec2-instance-connect", "send-ssh-public-key",
1132
+ "--instance-id", state[:server_id],
1133
+ "--instance-os-user", username,
1134
+ "--ssh-public-key", public_key,
1135
+ "--region", config[:region]
1136
+ ]
1137
+
1138
+ cmd += ["--profile", config[:shared_credentials_profile]] if config[:shared_credentials_profile]
1139
+
1140
+ # Execute the command with proper shell escaping
1141
+ debug("[AWS EC2 Instance Connect] Refreshing SSH public key for #{state[:server_id]}")
1142
+ escaped_cmd = cmd.map { |arg| Shellwords.escape(arg) }.join(" ")
1143
+ result = `#{escaped_cmd} 2>&1`
1144
+ unless $?.success?
1145
+ warn("[AWS EC2 Instance Connect] Failed to refresh SSH key: #{result}")
1146
+ end
1147
+ end
1148
+
1149
+ def instance_connect_configure_ssh_proxy_command(state)
1150
+ info("[AWS EC2 Instance Connect] Configuring proxy command mode (tunnel)")
1151
+
1152
+ # Build the AWS CLI command for the tunnel
1153
+ proxy_command = [
1154
+ "aws", "ec2-instance-connect", "open-tunnel",
1155
+ "--instance-id", state[:server_id]
1156
+ ]
1157
+
1158
+ # Add optional parameters
1159
+ if config[:instance_connect_endpoint_id]
1160
+ proxy_command += ["--instance-connect-endpoint-id", config[:instance_connect_endpoint_id]]
1161
+ end
1162
+ if config[:instance_connect_max_tunnel_duration]
1163
+ proxy_command += ["--max-tunnel-duration", config[:instance_connect_max_tunnel_duration].to_s]
1164
+ end
1165
+ if config[:shared_credentials_profile]
1166
+ proxy_command += ["--profile", config[:shared_credentials_profile]]
1167
+ end
1168
+ proxy_command += ["--region", config[:region]]
1169
+ proxy_command_str = proxy_command.join(" ")
1170
+
1171
+ info("Configuring SSH to use Instance Connect tunnel: #{proxy_command_str}")
1172
+ state[:ssh_proxy_command] = proxy_command_str
1173
+
1174
+ # Store Instance Connect details for the transport to use
1175
+ state[:instance_connect_config] = {
1176
+ server_id: state[:server_id],
1177
+ username: state[:username] || actual_platform&.username,
1178
+ region: config[:region],
1179
+ profile: config[:shared_credentials_profile],
1180
+ tunnel_mode: true,
1181
+ }
1182
+ end
1183
+
1184
+ def instance_connect_endpoint_available?(state)
1185
+ # If explicitly configured, respect that configuration
1186
+ return true if config[:instance_connect_endpoint_id]
1187
+
1188
+ # Check if there are any instance connect endpoints in the VPC
1189
+ vpc_id = get_vpc_id_for_instance(state)
1190
+ return false unless vpc_id
1191
+
1192
+ begin
1193
+ endpoints = ec2.client.describe_instance_connect_endpoints(
1194
+ filters: [
1195
+ { name: "vpc-id", values: [vpc_id] },
1196
+ { name: "state", values: ["create-complete"] },
1197
+ ]
1198
+ ).instance_connect_endpoints
1199
+
1200
+ !endpoints.empty?
1201
+ rescue ::Aws::EC2::Errors::InvalidAction, ::Aws::EC2::Errors::UnauthorizedOperation => e
1202
+ # Instance Connect endpoints may not be available in this region or account
1203
+ debug("[AWS EC2 Instance Connect] Cannot check for endpoints: #{e.message}")
1204
+ false
1205
+ end
1206
+ end
1207
+
1208
+ def get_vpc_id_for_instance(state)
1209
+ # Get the instance details to find its VPC
1210
+ return nil unless state[:server_id]
1211
+
1212
+ begin
1213
+ instance_info = ec2.client.describe_instances(instance_ids: [state[:server_id]]).reservations.first&.instances&.first
1214
+ return nil unless instance_info
1215
+
1216
+ instance_info.vpc_id
1217
+ rescue => e
1218
+ debug("[AWS EC2 Instance Connect] Error getting VPC ID for instance: #{e.message}")
1219
+ nil
1220
+ end
1221
+ end
1222
+
1223
+ def instance_connect_configure_direct_ssh(state)
1224
+ # For direct SSH, we need to ensure the hostname is the public DNS name
1225
+ # and configure SSH options appropriately
1226
+ server = ec2.get_instance(state[:server_id])
1227
+ public_dns = server.public_dns_name
1228
+
1229
+ if public_dns && !public_dns.empty?
1230
+ info("[AWS EC2 Instance Connect] Configuring direct SSH to #{public_dns}")
1231
+ state[:hostname] = public_dns
1232
+
1233
+ # Store Instance Connect details for direct SSH mode
1234
+ state[:instance_connect_config] = {
1235
+ server_id: state[:server_id],
1236
+ username: state[:username] || actual_platform&.username,
1237
+ region: config[:region],
1238
+ profile: config[:shared_credentials_profile],
1239
+ direct_ssh: true,
1240
+ hostname: public_dns,
1241
+ }
1242
+ else
1243
+ warn("[AWS EC2 Instance Connect] No public DNS available for direct SSH, falling back to existing hostname")
1244
+ end
1245
+ end
1246
+
1247
+ def instance_connect_extract_public_key(private_key_path)
1248
+ public_key_path = "#{private_key_path}.pub"
1249
+
1250
+ if File.exist?(public_key_path)
1251
+ return File.read(public_key_path).strip
1252
+ end
1253
+
1254
+ begin
1255
+ key = SSHKey.new(File.read(private_key_path))
1256
+ key.ssh_public_key
1257
+ rescue => e
1258
+ raise "Unable to extract public key from #{private_key_path}: #{e.message}"
1259
+ end
1260
+ end
948
1261
  end
949
1262
  end
950
1263
  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.20.0".freeze
22
+ EC2_VERSION = "3.21.0".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.20.0
4
+ version: 3.21.0
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-06-16 00:00:00.000000000 Z
11
+ date: 2025-09-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aws-sdk-ec2
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: aws-sdk-ec2instanceconnect
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.0'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: retryable
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -44,13 +58,27 @@ dependencies:
44
58
  - - "<"
45
59
  - !ruby/object:Gem::Version
46
60
  version: '4.0'
61
+ - !ruby/object:Gem::Dependency
62
+ name: sshkey
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '2.0'
68
+ type: :runtime
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '2.0'
47
75
  - !ruby/object:Gem::Dependency
48
76
  name: test-kitchen
49
77
  requirement: !ruby/object:Gem::Requirement
50
78
  requirements:
51
79
  - - ">="
52
80
  - !ruby/object:Gem::Version
53
- version: 1.4.1
81
+ version: 3.9.0
54
82
  - - "<"
55
83
  - !ruby/object:Gem::Version
56
84
  version: '4'
@@ -60,7 +88,7 @@ dependencies:
60
88
  requirements:
61
89
  - - ">="
62
90
  - !ruby/object:Gem::Version
63
- version: 1.4.1
91
+ version: 3.9.0
64
92
  - - "<"
65
93
  - !ruby/object:Gem::Version
66
94
  version: '4'
@@ -74,6 +102,7 @@ files:
74
102
  - LICENSE
75
103
  - lib/kitchen/driver/aws/client.rb
76
104
  - lib/kitchen/driver/aws/dedicated_hosts.rb
105
+ - lib/kitchen/driver/aws/instance_connect.rb
77
106
  - lib/kitchen/driver/aws/instance_generator.rb
78
107
  - lib/kitchen/driver/aws/standard_platform.rb
79
108
  - lib/kitchen/driver/aws/standard_platform/alma.rb