diff --git a/.changes/next-release/enhancement-ec2instanceconnect-78148.json b/.changes/next-release/enhancement-ec2instanceconnect-78148.json new file mode 100644 index 000000000000..913e9c08eaa8 --- /dev/null +++ b/.changes/next-release/enhancement-ec2instanceconnect-78148.json @@ -0,0 +1,5 @@ +{ + "type": "enhancement", + "category": "ec2-instance-connect", + "description": "Allow ec2-instance-connect ssh and open-tunnel commands to connect to update-complete, update-in-progress, and update-failed EC2 Instance Connect Endpoints. Fixes `#9715 `__" +} diff --git a/awscli/customizations/ec2instanceconnect/eicefetcher.py b/awscli/customizations/ec2instanceconnect/eicefetcher.py index 0282d7e0253f..a196d83c86c5 100644 --- a/awscli/customizations/ec2instanceconnect/eicefetcher.py +++ b/awscli/customizations/ec2instanceconnect/eicefetcher.py @@ -51,7 +51,17 @@ def _get_instance_connect_endpoint_by_id( self, ec2_client, instance_connect_endpoint_id ): args = { - "Filters": [{"Name": "state", "Values": ["create-complete"]}], + "Filters": [ + { + "Name": "state", + "Values": [ + "create-complete", + "update-in-progress", + "update-failed", + "update-complete", + ], + } + ], "InstanceConnectEndpointIds": [instance_connect_endpoint_id], } describe_eice_response = ( @@ -72,7 +82,15 @@ def _get_instance_connect_endpoint_by_vpc( ## Describe until subnet match and if none match subnet then return the first one based on vpc-id filter args = { "Filters": [ - {"Name": "state", "Values": ["create-complete"]}, + { + "Name": "state", + "Values": [ + "create-complete", + "update-in-progress", + "update-failed", + "update-complete", + ], + }, {"Name": "vpc-id", "Values": [vpc_id]}, ] } @@ -90,9 +108,7 @@ def _get_instance_connect_endpoint_by_vpc( if page_result: for eice in page_result: if eice['SubnetId'] == subnet_id: - logger.debug( - f"Using EICE based on subnet: {instance_connect_endpoints[0]}" - ) + logger.debug(f"Using EICE based on subnet: {eice}") return eice if instance_connect_endpoints: diff --git a/tests/functional/ec2instanceconnect/test_opentunnel.py b/tests/functional/ec2instanceconnect/test_opentunnel.py index cd85b01c4993..744aea670273 100644 --- a/tests/functional/ec2instanceconnect/test_opentunnel.py +++ b/tests/functional/ec2instanceconnect/test_opentunnel.py @@ -352,7 +352,15 @@ def describe_eice_response_empty_fips_dns(dns_name, fips_dns_name): def request_params_for_describe_eice(): return { 'Filters': [ - {'Name': 'state', 'Values': ['create-complete']}, + { + 'Name': 'state', + 'Values': [ + 'create-complete', + 'update-in-progress', + 'update-failed', + 'update-complete', + ], + }, {'Name': 'vpc-id', 'Values': ['vpc-123']}, ] } @@ -434,6 +442,15 @@ def assert_url(dns_name, url): class TestOpenTunnel: + @pytest.mark.parametrize( + "endpoint_state", + [ + "create-complete", + "update-in-progress", + "update-failed", + "update-complete", + ], + ) def test_single_connection_mode( self, cli_runner, @@ -442,10 +459,11 @@ def test_single_connection_mode( io_patch, describe_instance_response, dns_name, - describe_eice_response, + fips_dns_name, request_params_for_describe_instance, request_params_for_describe_eice, datetime_utcnow_patch, + endpoint_state, ): cli_runner.env["AWS_USE_FIPS_ENDPOINT"] = "false" cmdline = [ @@ -456,8 +474,27 @@ def test_single_connection_mode( "--max-tunnel-duration", "1", ] + + # Create endpoint response with the specified state + describe_eice_response_with_state = f""" + + + + {dns_name} + {fips_dns_name} + eice-123 + {endpoint_state} + subnet-123 + vpc-123 + + + + """ + cli_runner.add_response(HTTPResponse(body=describe_instance_response)) - cli_runner.add_response(HTTPResponse(body=describe_eice_response)) + cli_runner.add_response( + HTTPResponse(body=describe_eice_response_with_state) + ) test_server_input = b"Test Server Output" mock_crt_websocket.add_output_from_server(test_server_input) @@ -469,7 +506,7 @@ def test_single_connection_mode( assert 0 == result.rc assert pop_stdout_content() == test_server_input assert_stdout_empty() - # Order of the query params on the url mater because of sigv4 + # Order of the query params on the url matter because of sigv4 assert ( "eice-123.ec2-instance-connect-endpoint.us-west-2.amazonaws.com/openTunnel?" "instanceConnectEndpointId=eice-123&remotePort=22&privateIpAddress=10.0.0.0&" diff --git a/tests/functional/ec2instanceconnect/test_ssh.py b/tests/functional/ec2instanceconnect/test_ssh.py index 15b64261bd31..05d0ddd30622 100644 --- a/tests/functional/ec2instanceconnect/test_ssh.py +++ b/tests/functional/ec2instanceconnect/test_ssh.py @@ -176,6 +176,39 @@ def get_describe_eice_response(): """ +def get_describe_eice_response_with_state(state): + return f""" + + + + dns.com + fips.dns.com + eice-123 + {state} + subnet-123 + vpc-123 + + + + """ + + +def get_describe_eice_response_create_complete(): + return get_describe_eice_response_with_state("create-complete") + + +def get_describe_eice_response_update_in_progress(): + return get_describe_eice_response_with_state("update-in-progress") + + +def get_describe_eice_response_update_complete(): + return get_describe_eice_response_with_state("update-complete") + + +def get_describe_eice_response_update_failed(): + return get_describe_eice_response_with_state("update-failed") + + @pytest.fixture def describe_eice_response(): return get_describe_eice_response() @@ -184,7 +217,15 @@ def describe_eice_response(): def get_request_params_for_describe_eice(): return { 'Filters': [ - {'Name': 'state', 'Values': ['create-complete']}, + { + 'Name': 'state', + 'Values': [ + 'create-complete', + 'update-in-progress', + 'update-failed', + 'update-complete', + ], + }, {'Name': 'vpc-id', 'Values': ['vpc-123']}, ] } @@ -192,7 +233,17 @@ def get_request_params_for_describe_eice(): def get_request_params_for_describe_eice_with_eice_id(): return { - 'Filters': [{'Name': 'state', 'Values': ['create-complete']}], + 'Filters': [ + { + 'Name': 'state', + 'Values': [ + 'create-complete', + 'update-in-progress', + 'update-failed', + 'update-complete', + ], + } + ], 'InstanceConnectEndpointIds': ['eice-12345'], } @@ -568,6 +619,118 @@ class TestSSHCommand: ], id='Open-Tunnel: use provided ip', ), + pytest.param( + get_describe_private_instance_response(), + get_describe_eice_response_create_complete(), + get_request_params_for_describe_eice(), + [ + "ec2-instance-connect", + "ssh", + "--instance-id", + "i-123", + "--private-key-file", + "/tmp/ssh-file", + ], + [ + 'ssh', + '-o', + 'ServerAliveInterval=5', + '-p', + '22', + '-i', + '/tmp/ssh-file', + '-o', + 'ProxyCommand=aws ec2-instance-connect open-tunnel --instance-id i-123 ' + '--private-ip-address 10.0.0.0 --remote-port 22 ' + '--instance-connect-endpoint-id eice-123 --instance-connect-endpoint-dns-name dns.com', + 'ec2-user@10.0.0.0', + ], + id='Open-Tunnel: connect via eice in create-complete', + ), + pytest.param( + get_describe_private_instance_response(), + get_describe_eice_response_update_in_progress(), + get_request_params_for_describe_eice(), + [ + "ec2-instance-connect", + "ssh", + "--instance-id", + "i-123", + "--private-key-file", + "/tmp/ssh-file", + ], + [ + 'ssh', + '-o', + 'ServerAliveInterval=5', + '-p', + '22', + '-i', + '/tmp/ssh-file', + '-o', + 'ProxyCommand=aws ec2-instance-connect open-tunnel --instance-id i-123 ' + '--private-ip-address 10.0.0.0 --remote-port 22 ' + '--instance-connect-endpoint-id eice-123 --instance-connect-endpoint-dns-name dns.com', + 'ec2-user@10.0.0.0', + ], + id='Open-Tunnel: connect via eice in update-in-progress', + ), + pytest.param( + get_describe_private_instance_response(), + get_describe_eice_response_update_complete(), + get_request_params_for_describe_eice(), + [ + "ec2-instance-connect", + "ssh", + "--instance-id", + "i-123", + "--private-key-file", + "/tmp/ssh-file", + ], + [ + 'ssh', + '-o', + 'ServerAliveInterval=5', + '-p', + '22', + '-i', + '/tmp/ssh-file', + '-o', + 'ProxyCommand=aws ec2-instance-connect open-tunnel --instance-id i-123 ' + '--private-ip-address 10.0.0.0 --remote-port 22 ' + '--instance-connect-endpoint-id eice-123 --instance-connect-endpoint-dns-name dns.com', + 'ec2-user@10.0.0.0', + ], + id='Open-Tunnel: connect via eice in update-complete', + ), + pytest.param( + get_describe_private_instance_response(), + get_describe_eice_response_update_failed(), + get_request_params_for_describe_eice(), + [ + "ec2-instance-connect", + "ssh", + "--instance-id", + "i-123", + "--private-key-file", + "/tmp/ssh-file", + ], + [ + 'ssh', + '-o', + 'ServerAliveInterval=5', + '-p', + '22', + '-i', + '/tmp/ssh-file', + '-o', + 'ProxyCommand=aws ec2-instance-connect open-tunnel --instance-id i-123 ' + '--private-ip-address 10.0.0.0 --remote-port 22 ' + '--instance-connect-endpoint-id eice-123 --instance-connect-endpoint-dns-name dns.com', + 'ec2-user@10.0.0.0', + ], + id='Open-Tunnel: connect via eice in update-failed', + ), pytest.param( get_describe_public_instance_response(), None,