kitchen-ec2 3.19.1 → 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: c3c2ccfd2c5edb694ea8eb944198dd2b801eb00e04cdc4d32eaec63b74e358f6
4
- data.tar.gz: 1fd3e76ada5452e6e104168ef1a34d8b812cd027593f95aff64f81d9b050f0c6
3
+ metadata.gz: 5d6b1c659fad578e57f13b71e617d6b1f8dce59393f17287bb0b33fe4894a5f0
4
+ data.tar.gz: c99cb5f69df034d26af00ae8a78a2bcbe9999ed67065cdf76efd897c80080c69
5
5
  SHA512:
6
- metadata.gz: fe17d7e44c41bfab90ee65056e7f5db0a80cdc852f2b7787f946ae679bd2fdd912d2012b1c1a85420d0a1a3453bf69f5e3fdffdddc3c7332cb255aa967909319
7
- data.tar.gz: ca7eb0f23c34414d75a7c65b1d87aba6f9fbb286e8f626031bbf171697308d5a40c8a55106a96584f6eb9fcfcbc200457b11ff6141dfa6ac09ca47c45af752ff
6
+ metadata.gz: 508886f38d0c6bba27e1724360e4a0ee4587bd92f14e21341e7c3dba1a40a951b69408cea2214b9b9249e911a31e378c12b2fe49334fe35e5df353fc9ba2f98d
7
+ data.tar.gz: d422f9a8542e95c7a3e0fcdcb882898dcbb996a6f556a5445ae96fb63fa149b81cbc39136216cb047886edb215c83f8ed5d090d4811a6aec6758e5fd5f9db7fb
data/LICENSE CHANGED
@@ -6,7 +6,7 @@ Licensed under the Apache License, Version 2.0 (the "License");
6
6
  you may not use this file except in compliance with the License.
7
7
  You may obtain a copy of the License at
8
8
 
9
- http://www.apache.org/licenses/LICENSE-2.0
9
+ https://www.apache.org/licenses/LICENSE-2.0
10
10
 
11
11
  Unless required by applicable law or agreed to in writing, software
12
12
  distributed under the License is distributed on an "AS IS" BASIS,
@@ -8,7 +8,7 @@
8
8
  # you may not use this file except in compliance with the License.
9
9
  # You may obtain a copy of the License at
10
10
  #
11
- # http://www.apache.org/licenses/LICENSE-2.0
11
+ # https://www.apache.org/licenses/LICENSE-2.0
12
12
  #
13
13
  # Unless required by applicable law or agreed to in writing, software
14
14
  # distributed under the License is distributed on an "AS IS" BASIS,
@@ -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
@@ -8,7 +8,7 @@
8
8
  # you may not use this file except in compliance with the License.
9
9
  # You may obtain a copy of the License at
10
10
  #
11
- # http://www.apache.org/licenses/LICENSE-2.0
11
+ # https://www.apache.org/licenses/LICENSE-2.0
12
12
  #
13
13
  # Unless required by applicable law or agreed to in writing, software
14
14
  # distributed under the License is distributed on an "AS IS" BASIS,
@@ -35,8 +35,8 @@ module Kitchen
35
35
  end
36
36
 
37
37
  # Transform the provided kitchen config into the hash we'll use to create the aws instance
38
- # can be passed in null, others need to be ommitted if they are null
39
- # Some fields can be passed in null, others need to be ommitted if they are null
38
+ # can be passed in null, others need to be omitted if they are null
39
+ # Some fields can be passed in null, others need to be omitted if they are null
40
40
  # @return [Hash]
41
41
  def ec2_instance_data
42
42
  # Support for looking up security group id and subnet id using tags.
@@ -173,6 +173,9 @@ module Kitchen
173
173
  if config[:security_group_ids]
174
174
  i[:network_interfaces][0][:groups] = i.delete(:security_group_ids)
175
175
  end
176
+ if config[:associate_ipv6]
177
+ i[:network_interfaces][0][:ipv_6_address_count] = 1
178
+ end
176
179
  end
177
180
  availability_zone = config[:availability_zone]
178
181
  if availability_zone
@@ -5,7 +5,7 @@
5
5
  # you may not use this file except in compliance with the License.
6
6
  # You may obtain a copy of the License at
7
7
  #
8
- # http://www.apache.org/licenses/LICENSE-2.0
8
+ # https://www.apache.org/licenses/LICENSE-2.0
9
9
  #
10
10
  # Unless required by applicable law or agreed to in writing, software
11
11
  # distributed under the License is distributed on an "AS IS" BASIS,
@@ -5,7 +5,7 @@
5
5
  # you may not use this file except in compliance with the License.
6
6
  # You may obtain a copy of the License at
7
7
  #
8
- # http://www.apache.org/licenses/LICENSE-2.0
8
+ # https://www.apache.org/licenses/LICENSE-2.0
9
9
  #
10
10
  # Unless required by applicable law or agreed to in writing, software
11
11
  # distributed under the License is distributed on an "AS IS" BASIS,
@@ -5,7 +5,7 @@
5
5
  # you may not use this file except in compliance with the License.
6
6
  # You may obtain a copy of the License at
7
7
  #
8
- # http://www.apache.org/licenses/LICENSE-2.0
8
+ # https://www.apache.org/licenses/LICENSE-2.0
9
9
  #
10
10
  # Unless required by applicable law or agreed to in writing, software
11
11
  # distributed under the License is distributed on an "AS IS" BASIS,
@@ -5,7 +5,7 @@
5
5
  # you may not use this file except in compliance with the License.
6
6
  # You may obtain a copy of the License at
7
7
  #
8
- # http://www.apache.org/licenses/LICENSE-2.0
8
+ # https://www.apache.org/licenses/LICENSE-2.0
9
9
  #
10
10
  # Unless required by applicable law or agreed to in writing, software
11
11
  # distributed under the License is distributed on an "AS IS" BASIS,
@@ -5,7 +5,7 @@
5
5
  # you may not use this file except in compliance with the License.
6
6
  # You may obtain a copy of the License at
7
7
  #
8
- # http://www.apache.org/licenses/LICENSE-2.0
8
+ # https://www.apache.org/licenses/LICENSE-2.0
9
9
  #
10
10
  # Unless required by applicable law or agreed to in writing, software
11
11
  # distributed under the License is distributed on an "AS IS" BASIS,
@@ -5,7 +5,7 @@
5
5
  # you may not use this file except in compliance with the License.
6
6
  # You may obtain a copy of the License at
7
7
  #
8
- # http://www.apache.org/licenses/LICENSE-2.0
8
+ # https://www.apache.org/licenses/LICENSE-2.0
9
9
  #
10
10
  # Unless required by applicable law or agreed to in writing, software
11
11
  # distributed under the License is distributed on an "AS IS" BASIS,
@@ -5,7 +5,7 @@
5
5
  # you may not use this file except in compliance with the License.
6
6
  # You may obtain a copy of the License at
7
7
  #
8
- # http://www.apache.org/licenses/LICENSE-2.0
8
+ # https://www.apache.org/licenses/LICENSE-2.0
9
9
  #
10
10
  # Unless required by applicable law or agreed to in writing, software
11
11
  # distributed under the License is distributed on an "AS IS" BASIS,
@@ -5,7 +5,7 @@
5
5
  # you may not use this file except in compliance with the License.
6
6
  # You may obtain a copy of the License at
7
7
  #
8
- # http://www.apache.org/licenses/LICENSE-2.0
8
+ # https://www.apache.org/licenses/LICENSE-2.0
9
9
  #
10
10
  # Unless required by applicable law or agreed to in writing, software
11
11
  # distributed under the License is distributed on an "AS IS" BASIS,
@@ -5,7 +5,7 @@
5
5
  # you may not use this file except in compliance with the License.
6
6
  # You may obtain a copy of the License at
7
7
  #
8
- # http://www.apache.org/licenses/LICENSE-2.0
8
+ # https://www.apache.org/licenses/LICENSE-2.0
9
9
  #
10
10
  # Unless required by applicable law or agreed to in writing, software
11
11
  # distributed under the License is distributed on an "AS IS" BASIS,
@@ -5,7 +5,7 @@
5
5
  # you may not use this file except in compliance with the License.
6
6
  # You may obtain a copy of the License at
7
7
  #
8
- # http://www.apache.org/licenses/LICENSE-2.0
8
+ # https://www.apache.org/licenses/LICENSE-2.0
9
9
  #
10
10
  # Unless required by applicable law or agreed to in writing, software
11
11
  # distributed under the License is distributed on an "AS IS" BASIS,
@@ -5,7 +5,7 @@
5
5
  # you may not use this file except in compliance with the License.
6
6
  # You may obtain a copy of the License at
7
7
  #
8
- # http://www.apache.org/licenses/LICENSE-2.0
8
+ # https://www.apache.org/licenses/LICENSE-2.0
9
9
  #
10
10
  # Unless required by applicable law or agreed to in writing, software
11
11
  # distributed under the License is distributed on an "AS IS" BASIS,
@@ -5,7 +5,7 @@
5
5
  # you may not use this file except in compliance with the License.
6
6
  # You may obtain a copy of the License at
7
7
  #
8
- # http://www.apache.org/licenses/LICENSE-2.0
8
+ # https://www.apache.org/licenses/LICENSE-2.0
9
9
  #
10
10
  # Unless required by applicable law or agreed to in writing, software
11
11
  # distributed under the License is distributed on an "AS IS" BASIS,
@@ -5,7 +5,7 @@
5
5
  # you may not use this file except in compliance with the License.
6
6
  # You may obtain a copy of the License at
7
7
  #
8
- # http://www.apache.org/licenses/LICENSE-2.0
8
+ # https://www.apache.org/licenses/LICENSE-2.0
9
9
  #
10
10
  # Unless required by applicable law or agreed to in writing, software
11
11
  # distributed under the License is distributed on an "AS IS" BASIS,
@@ -6,7 +6,7 @@
6
6
  # you may not use this file except in compliance with the License.
7
7
  # You may obtain a copy of the License at
8
8
  #
9
- # http://www.apache.org/licenses/LICENSE-2.0
9
+ # https://www.apache.org/licenses/LICENSE-2.0
10
10
  #
11
11
  # Unless required by applicable law or agreed to in writing, software
12
12
  # distributed under the License is distributed on an "AS IS" BASIS,
@@ -82,7 +82,7 @@ module Kitchen
82
82
  #
83
83
  # @return [String]
84
84
  #
85
- # @see SUPPORTED_ARCHITECTURESS
85
+ # @see SUPPORTED_ARCHITECTURES
86
86
  #
87
87
  attr_reader :architecture
88
88
 
@@ -8,7 +8,7 @@
8
8
  # you may not use this file except in compliance with the License.
9
9
  # You may obtain a copy of the License at
10
10
  #
11
- # http://www.apache.org/licenses/LICENSE-2.0
11
+ # https://www.apache.org/licenses/LICENSE-2.0
12
12
  #
13
13
  # Unless required by applicable law or agreed to in writing, software
14
14
  # distributed under the License is distributed on an "AS IS" BASIS,
@@ -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
@@ -85,6 +88,7 @@ module Kitchen
85
88
  default_config :image_search, nil
86
89
  default_config :username, nil
87
90
  default_config :associate_public_ip, nil
91
+ default_config :associate_ipv6, nil
88
92
  default_config :interface, nil
89
93
  default_config :http_proxy, ENV["HTTPS_PROXY"] || ENV.fetch("HTTP_PROXY", nil)
90
94
  default_config :retry_limit, 3
@@ -94,6 +98,9 @@ module Kitchen
94
98
  default_config :skip_cost_warning, false
95
99
  default_config :allocate_dedicated_host, false
96
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
97
104
 
98
105
  include Kitchen::Driver::Mixins::DedicatedHosts
99
106
 
@@ -225,7 +232,8 @@ module Kitchen
225
232
  case config[:aws_ssh_key_id]
226
233
  when nil
227
234
  create_key(state)
228
- 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]
229
237
  when "_disable"
230
238
  info("Disabling AWS-managed SSH key pairs for this EC2 instance.")
231
239
  info("The key pairs for the kitchen transport config and the AMI must match.")
@@ -271,6 +279,11 @@ module Kitchen
271
279
  end
272
280
 
273
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
+
274
287
  instance.transport.connection(state).wait_until_ready
275
288
  attach_network_interface(state) unless config[:elastic_network_interface_id].nil?
276
289
  create_ec2_json(state) if /chef/i.match?(instance.provisioner.name)
@@ -914,7 +927,7 @@ module Kitchen
914
927
  puts "ENI #{config[:elastic_network_interface_id]} already attached."
915
928
  end
916
929
  rescue ::Aws::EC2::Errors::InvalidNetworkInterfaceIDNotFound => e
917
- warn(e.to_s)
930
+ warn(e)
918
931
  end
919
932
  end
920
933
 
@@ -944,6 +957,307 @@ module Kitchen
944
957
  state.delete(:auto_key_id)
945
958
  File.unlink("#{config[:kitchen_root]}/.kitchen/#{instance.name}.pem")
946
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
947
1261
  end
948
1262
  end
949
1263
  end
@@ -8,7 +8,7 @@
8
8
  # you may not use this file except in compliance with the License.
9
9
  # You may obtain a copy of the License at
10
10
  #
11
- # http://www.apache.org/licenses/LICENSE-2.0
11
+ # https://www.apache.org/licenses/LICENSE-2.0
12
12
  #
13
13
  # Unless required by applicable law or agreed to in writing, software
14
14
  # distributed under the License is distributed on an "AS IS" BASIS,
@@ -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.19.1".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.19.1
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-15 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