diff --git a/.changes/next-release/feature-AWSSDKforJavav2-8c69562.json b/.changes/next-release/feature-AWSSDKforJavav2-8c69562.json new file mode 100644 index 000000000000..dc9a27333838 --- /dev/null +++ b/.changes/next-release/feature-AWSSDKforJavav2-8c69562.json @@ -0,0 +1,6 @@ +{ + "type": "feature", + "category": "AWS SDK for Java v2", + "contributor": "", + "description": "Updated AutoDefaultsModeDiscovery from using EC2MetadataUtils to Ec2MetadataClient" +} diff --git a/core/aws-core/pom.xml b/core/aws-core/pom.xml index 72f3c2004bbd..2b46f89ef8ac 100644 --- a/core/aws-core/pom.xml +++ b/core/aws-core/pom.xml @@ -206,6 +206,12 @@ rxjava test + + software.amazon.awssdk + imds + ${awsjavasdk.version} + compile + diff --git a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/internal/defaultsmode/AutoDefaultsModeDiscovery.java b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/internal/defaultsmode/AutoDefaultsModeDiscovery.java index 5f4193147857..e79a1dff0a95 100644 --- a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/internal/defaultsmode/AutoDefaultsModeDiscovery.java +++ b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/internal/defaultsmode/AutoDefaultsModeDiscovery.java @@ -19,8 +19,11 @@ import software.amazon.awssdk.annotations.SdkProtectedApi; import software.amazon.awssdk.awscore.defaultsmode.DefaultsMode; import software.amazon.awssdk.core.SdkSystemSetting; +import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.imds.Ec2MetadataClient; +import software.amazon.awssdk.imds.Ec2MetadataRetryPolicy; +import software.amazon.awssdk.imds.internal.Ec2MetadataSharedClient; import software.amazon.awssdk.regions.Region; -import software.amazon.awssdk.regions.internal.util.EC2MetadataUtils; import software.amazon.awssdk.utils.JavaSystemSetting; import software.amazon.awssdk.utils.OptionalUtils; import software.amazon.awssdk.utils.SystemSetting; @@ -81,12 +84,25 @@ private static DefaultsMode compareRegion(String region, Region clientRegion) { } private static Optional queryImdsV2() { + + if (SdkSystemSetting.AWS_EC2_METADATA_DISABLED.getBooleanValueOrThrow()) { + return Optional.empty(); + } + + Ec2MetadataClient client = null; try { - String ec2InstanceRegion = EC2MetadataUtils.fetchData(EC2_METADATA_REGION_PATH, false, 1); - // ec2InstanceRegion could be null + client = Ec2MetadataSharedClient.builder() + .retryPolicy(Ec2MetadataRetryPolicy.none()) + .build(); + + String ec2InstanceRegion = client.get(EC2_METADATA_REGION_PATH).asString(); return Optional.ofNullable(ec2InstanceRegion); - } catch (Exception exception) { + } catch (SdkClientException e) { return Optional.empty(); + } finally { + if (client != null) { + Ec2MetadataSharedClient.decrementAndClose(); + } } } diff --git a/core/aws-core/src/test/java/software/amazon/awssdk/awscore/internal/defaultsmode/AutoDefaultsModeDiscoveryEc2MetadataClientTest.java b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/internal/defaultsmode/AutoDefaultsModeDiscoveryEc2MetadataClientTest.java new file mode 100644 index 000000000000..b76324b47073 --- /dev/null +++ b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/internal/defaultsmode/AutoDefaultsModeDiscoveryEc2MetadataClientTest.java @@ -0,0 +1,276 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.awscore.internal.defaultsmode; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.put; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.putRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; +import static com.github.tomakehurst.wiremock.client.WireMock.matching; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static org.assertj.core.api.Assertions.assertThat; + +import com.github.tomakehurst.wiremock.junit5.WireMockExtension; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import software.amazon.awssdk.awscore.defaultsmode.DefaultsMode; +import software.amazon.awssdk.core.SdkSystemSetting; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.testutils.EnvironmentVariableHelper; + +/** + * Tests specifically for AutoDefaultsModeDiscovery's migration to use Ec2MetadataClient. + * These tests verify that the migration from EC2MetadataUtils to Ec2MetadataClient works correctly. + */ +public class AutoDefaultsModeDiscoveryEc2MetadataClientTest { + private static final EnvironmentVariableHelper ENVIRONMENT_VARIABLE_HELPER = new EnvironmentVariableHelper(); + + @RegisterExtension + static WireMockExtension wireMock = WireMockExtension.newInstance() + .options(wireMockConfig().dynamicPort().dynamicPort()) + .configureStaticDsl(true) + .build(); + + @BeforeAll + static void setupClass() { + System.setProperty(SdkSystemSetting.AWS_EC2_METADATA_SERVICE_ENDPOINT.property(), + "http://localhost:" + wireMock.getPort()); + } + + @AfterAll + static void cleanupClass() { + System.clearProperty(SdkSystemSetting.AWS_EC2_METADATA_SERVICE_ENDPOINT.property()); + } + + @BeforeEach + public void setup() { + clearEnvironmentVariable("AWS_EXECUTION_ENV"); + clearEnvironmentVariable("AWS_REGION"); + clearEnvironmentVariable("AWS_DEFAULT_REGION"); + } + + @AfterEach + public void cleanup() { + wireMock.resetAll(); + ENVIRONMENT_VARIABLE_HELPER.reset(); + } + + // Clear an environment variable by setting it to null. + private void clearEnvironmentVariable(String name) { + try { + ENVIRONMENT_VARIABLE_HELPER.set(name, null); + } catch (Exception e) { + // Ignore + } + } + + @Test + public void autoDefaultsModeDiscovery_shouldUseSharedHttpClient() throws Exception { + // Stub successful IMDS responses + stubFor(put("/latest/api/token") + .willReturn(aResponse() + .withStatus(200) + .withHeader("x-aws-ec2-metadata-token-ttl-seconds", "21600") + .withBody("test-token"))); + stubFor(get("/latest/meta-data/placement/region") + .willReturn(aResponse().withStatus(200).withBody("us-east-1"))); + + AutoDefaultsModeDiscovery discovery = new AutoDefaultsModeDiscovery(); + DefaultsMode result = discovery.discover(Region.US_EAST_1); + + // Should return IN_REGION since client region matches IMDS region + assertThat(result).isEqualTo(DefaultsMode.IN_REGION); + + // Verify token request was made + verify(putRequestedFor(urlEqualTo("/latest/api/token"))); + + // Verify region request was made with token header - IMDSv2 + verify(getRequestedFor(urlEqualTo("/latest/meta-data/placement/region")) + .withHeader("x-aws-ec2-metadata-token", matching("test-token"))); + + // Verify no IMDSv1 requests were made + verify(0, getRequestedFor(urlEqualTo("/latest/meta-data/placement/region")) + .withoutHeader("x-aws-ec2-metadata-token")); + } + + @Test + public void multipleDiscoveryInstances_shouldShareSameHttpClient() throws Exception { + stubFor(put("/latest/api/token") + .willReturn(aResponse() + .withStatus(200) + .withHeader("x-aws-ec2-metadata-token-ttl-seconds", "21600") + .withBody("test-token"))); + stubFor(get("/latest/meta-data/placement/region") + .willReturn(aResponse().withStatus(200).withBody("us-west-2"))); + + // Create multiple discovery instances + AutoDefaultsModeDiscovery discovery1 = new AutoDefaultsModeDiscovery(); + AutoDefaultsModeDiscovery discovery2 = new AutoDefaultsModeDiscovery(); + + // Both should use the same shared HTTP client + DefaultsMode result1 = discovery1.discover(Region.US_EAST_1); + DefaultsMode result2 = discovery2.discover(Region.US_EAST_1); + + // Both should return CROSS_REGION + assertThat(result1).isEqualTo(DefaultsMode.CROSS_REGION); + assertThat(result2).isEqualTo(DefaultsMode.CROSS_REGION); + + // Verify token request was made + verify(putRequestedFor(urlEqualTo("/latest/api/token"))); + + // Verify region request was made with token header - IMDSv2 + verify(getRequestedFor(urlEqualTo("/latest/meta-data/placement/region")) + .withHeader("x-aws-ec2-metadata-token", matching("test-token"))); + + // Verify no IMDSv1 requests were made + verify(0, getRequestedFor(urlEqualTo("/latest/meta-data/placement/region")) + .withoutHeader("x-aws-ec2-metadata-token")); + } + + @Test + public void awsEc2MetadataDisabled_shouldSkipImdsAndUseStandardMode() { + // Disable IMDS + ENVIRONMENT_VARIABLE_HELPER.set(SdkSystemSetting.AWS_EC2_METADATA_DISABLED.environmentVariable(), "true"); + + AutoDefaultsModeDiscovery discovery = new AutoDefaultsModeDiscovery(); + DefaultsMode result = discovery.discover(Region.US_EAST_1); + + // Should return STANDARD mode without making IMDS calls + assertThat(result).isEqualTo(DefaultsMode.STANDARD); + + // Verify no IMDS requests were made + verify(0, putRequestedFor(urlEqualTo("/latest/api/token"))); + verify(0, getRequestedFor(urlEqualTo("/latest/meta-data/placement/region"))); + } + + @Test + public void imdsFailure_shouldFallbackToStandardMode() { + // Stub IMDS to fail + stubFor(put("/latest/api/token") + .willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))); + stubFor(get("/latest/meta-data/placement/region") + .willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))); + + AutoDefaultsModeDiscovery discovery = new AutoDefaultsModeDiscovery(); + DefaultsMode result = discovery.discover(Region.US_EAST_1); + + // Should fall back to STANDARD mode when IMDS fails + assertThat(result).isEqualTo(DefaultsMode.STANDARD); + + // Verify IMDS requests were attempted + verify(putRequestedFor(urlEqualTo("/latest/api/token"))); + } + + @Test + public void noRetryPolicy_shouldBeUsedByDefault() { + // Stub token to succeed but region to fail with retryable error + stubFor(put("/latest/api/token") + .willReturn(aResponse() + .withStatus(200) + .withHeader("x-aws-ec2-metadata-token-ttl-seconds", "21600") + .withBody("test-token"))); + stubFor(get("/latest/meta-data/placement/region") + .willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))); + + AutoDefaultsModeDiscovery discovery = new AutoDefaultsModeDiscovery(); + DefaultsMode result = discovery.discover(Region.US_EAST_1); + + // Should fail immediately without retries and fallback to STANDARD + assertThat(result).isEqualTo(DefaultsMode.STANDARD); + + // Verify requests were made once (no retries) + verify(1, putRequestedFor(urlEqualTo("/latest/api/token"))); + + // Verify region request was made with token header - IMDSv2 + verify(1, getRequestedFor(urlEqualTo("/latest/meta-data/placement/region")) + .withHeader("x-aws-ec2-metadata-token", matching("test-token"))); + + // Verify no IMDSv1 requests were made + verify(0, getRequestedFor(urlEqualTo("/latest/meta-data/placement/region")) + .withoutHeader("x-aws-ec2-metadata-token")); + } + + @Test + public void imdsV1Fallback_shouldWorkWhenTokenFails() { + // Stub token request to fail + stubFor(put("/latest/api/token") + .willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))); + + // Stub successful IMDSv1 request + stubFor(get("/latest/meta-data/placement/region") + .willReturn(aResponse().withStatus(200).withBody("us-east-1"))); + + AutoDefaultsModeDiscovery discovery = new AutoDefaultsModeDiscovery(); + DefaultsMode result = discovery.discover(Region.US_EAST_1); + + // Should fall back to IMDSv1 and return IN_REGION + assertThat(result).isEqualTo(DefaultsMode.IN_REGION); + + // Verify token request was attempted + verify(putRequestedFor(urlEqualTo("/latest/api/token"))); + + // Verify region request was made without token header - IMDSv1 fallback + verify(getRequestedFor(urlEqualTo("/latest/meta-data/placement/region")) + .withoutHeader("x-aws-ec2-metadata-token")); + + // Verify no IMDSv2 requests were made + verify(0, getRequestedFor(urlEqualTo("/latest/meta-data/placement/region")) + .withHeader("x-aws-ec2-metadata-token", matching(".*"))); + } + + @Test + public void imdsV1Fallback_shouldNotWorkWhenV1Disabled() { + // Disable IMDSv1 fallback + ENVIRONMENT_VARIABLE_HELPER.set(SdkSystemSetting.AWS_EC2_METADATA_V1_DISABLED.environmentVariable(), "true"); + + // Stub token request to fail + stubFor(put("/latest/api/token") + .willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))); + + AutoDefaultsModeDiscovery discovery = new AutoDefaultsModeDiscovery(); + DefaultsMode result = discovery.discover(Region.US_EAST_1); + + // Should fail without fallback to IMDSv1 and return STANDARD + assertThat(result).isEqualTo(DefaultsMode.STANDARD); + + // Verify only token request was made + verify(putRequestedFor(urlEqualTo("/latest/api/token"))); + } + + @Test + public void tokenRequest400Error_shouldNotFallbackToV1() { + // Stub token request to fail with 400 + stubFor(put("/latest/api/token") + .willReturn(aResponse().withStatus(400).withBody("Bad Request"))); + + AutoDefaultsModeDiscovery discovery = new AutoDefaultsModeDiscovery(); + DefaultsMode result = discovery.discover(Region.US_EAST_1); + + // Should fail without attempting IMDSv1 fallback and return STANDARD + assertThat(result).isEqualTo(DefaultsMode.STANDARD); + + // Verify only token request was made + verify(putRequestedFor(urlEqualTo("/latest/api/token"))); + } +} diff --git a/core/imds/src/main/java/software/amazon/awssdk/imds/internal/BaseEc2MetadataClient.java b/core/imds/src/main/java/software/amazon/awssdk/imds/internal/BaseEc2MetadataClient.java index 2698ad083fcb..fbb7d24bb2cf 100644 --- a/core/imds/src/main/java/software/amazon/awssdk/imds/internal/BaseEc2MetadataClient.java +++ b/core/imds/src/main/java/software/amazon/awssdk/imds/internal/BaseEc2MetadataClient.java @@ -44,8 +44,8 @@ public abstract class BaseEc2MetadataClient { protected final RequestMarshaller requestMarshaller; protected final Duration tokenTtl; - private BaseEc2MetadataClient(Ec2MetadataRetryPolicy retryPolicy, Duration tokenTtl, URI endpoint, - EndpointMode endpointMode) { + protected BaseEc2MetadataClient(Ec2MetadataRetryPolicy retryPolicy, Duration tokenTtl, URI endpoint, + EndpointMode endpointMode) { this.retryPolicy = Validate.getOrDefault(retryPolicy, Ec2MetadataRetryPolicy.builder()::build); this.tokenTtl = Validate.getOrDefault(tokenTtl, () -> DEFAULT_TOKEN_TTL); this.endpoint = getEndpoint(endpoint, endpointMode); @@ -60,6 +60,7 @@ protected BaseEc2MetadataClient(DefaultEc2MetadataAsyncClient.Ec2MetadataAsyncBu this(builder.getRetryPolicy(), builder.getTokenTtl(), builder.getEndpoint(), builder.getEndpointMode()); } + private URI getEndpoint(URI builderEndpoint, EndpointMode builderEndpointMode) { Validate.mutuallyExclusive("Only one of 'endpoint' or 'endpointMode' must be specified, but not both", builderEndpoint, builderEndpointMode); diff --git a/core/imds/src/main/java/software/amazon/awssdk/imds/internal/DefaultEc2MetadataClientWithFallback.java b/core/imds/src/main/java/software/amazon/awssdk/imds/internal/DefaultEc2MetadataClientWithFallback.java new file mode 100644 index 000000000000..3f1a1f7c4d45 --- /dev/null +++ b/core/imds/src/main/java/software/amazon/awssdk/imds/internal/DefaultEc2MetadataClientWithFallback.java @@ -0,0 +1,405 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.imds.internal; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.URI; +import java.time.Duration; +import java.time.Instant; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.Supplier; +import software.amazon.awssdk.annotations.Immutable; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.annotations.ThreadSafe; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.core.SdkSystemSetting; +import software.amazon.awssdk.core.exception.RetryableException; +import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.core.internal.http.loader.DefaultSdkHttpClientBuilder; +import software.amazon.awssdk.core.retry.RetryPolicyContext; +import software.amazon.awssdk.http.AbortableInputStream; +import software.amazon.awssdk.http.HttpExecuteRequest; +import software.amazon.awssdk.http.HttpExecuteResponse; +import software.amazon.awssdk.http.HttpStatusFamily; +import software.amazon.awssdk.http.SdkHttpClient; +import software.amazon.awssdk.imds.Ec2MetadataClient; +import software.amazon.awssdk.imds.Ec2MetadataClientException; +import software.amazon.awssdk.imds.Ec2MetadataResponse; +import software.amazon.awssdk.imds.Ec2MetadataRetryPolicy; +import software.amazon.awssdk.imds.EndpointMode; +import software.amazon.awssdk.profiles.ProfileProperty; +import software.amazon.awssdk.utils.Either; +import software.amazon.awssdk.utils.Logger; +import software.amazon.awssdk.utils.OptionalUtils; +import software.amazon.awssdk.utils.ToString; +import software.amazon.awssdk.utils.Validate; +import software.amazon.awssdk.utils.cache.CachedSupplier; +import software.amazon.awssdk.utils.cache.RefreshResult; + +/** + * An Implementation of the Ec2Metadata Interface with IMDSv1 fallback support. + * This client is identical to DefaultEc2MetadataClient but provides automatic fallback + * to IMDSv1 when IMDSv2 token retrieval fails (except for 400 errors). + */ +@SdkInternalApi +@Immutable +@ThreadSafe +public final class DefaultEc2MetadataClientWithFallback extends BaseEc2MetadataClient implements Ec2MetadataClient { + + private static final Logger log = Logger.loggerFor(DefaultEc2MetadataClientWithFallback.class); + + private final SdkHttpClient httpClient; + private final Supplier tokenCache; + private final boolean httpClientIsInternal; + private final boolean imdsV1FallbackEnabled; + + private DefaultEc2MetadataClientWithFallback(Ec2MetadataBuilder builder) { + super(builder.getRetryPolicy(), builder.getTokenTtl(), builder.getEndpoint(), builder.getEndpointMode()); + + Validate.isTrue(builder.httpClient == null || builder.httpClientBuilder == null, + "The httpClient and the httpClientBuilder can't both be configured."); + this.httpClient = Either + .fromNullable(builder.httpClient, builder.httpClientBuilder) + .map(e -> e.map(Function.identity(), SdkHttpClient.Builder::build)) + .orElseGet(() -> new DefaultSdkHttpClientBuilder().buildWithDefaults(imdsHttpDefaults())); + this.httpClientIsInternal = builder.httpClient == null; + + this.imdsV1FallbackEnabled = !resolveImdsV1Disabled(); + + this.tokenCache = CachedSupplier.builder(() -> RefreshResult.builder(this.getTokenWithFallback()) + .staleTime(Instant.now().plus(tokenTtl)) + .build()) + .cachedValueName(toString()) + .build(); + } + + @Override + public String toString() { + return ToString.create("Ec2MetadataClientWithFallback"); + } + + @Override + public void close() { + if (httpClientIsInternal) { + httpClient.close(); + } + } + + public static Ec2MetadataBuilder builder() { + return new DefaultEc2MetadataClientWithFallback.Ec2MetadataBuilder(); + } + + /** + * Gets the specified instance metadata value by the given path with IMDSv1 fallback support. + * Will retry based on the {@link Ec2MetadataRetryPolicy retry policy} provided. + * Follows the same behavior as EC2MetadataUtils: if IMDSv2 token retrieval fails with 400 error, + * throws exception; otherwise falls back to IMDSv1 (null token). + * + * @param path Input path of the resource to get. + * @return Instance metadata value as part of MetadataResponse Object + * @throws SdkClientException if the request fails after all retries and fallback attempts + */ + @Override + public Ec2MetadataResponse get(String path) { + Throwable lastCause = null; + // 3 retries means 4 total attempts + Token token = null; + for (int attempt = 0; attempt < retryPolicy.numRetries() + 1; attempt++) { + RetryPolicyContext retryPolicyContext = RetryPolicyContext.builder().retriesAttempted(attempt).build(); + try { + if (token == null || token.isExpired()) { + token = tokenCache.get(); + } + return sendRequest(path, token == null ? null : token.value()); + } catch (UncheckedIOException | RetryableException e) { + lastCause = e; + int currentTry = attempt; + log.debug(() -> "Error while executing EC2Metadata request, attempting retry. Current attempt: " + currentTry); + } catch (SdkClientException sdkClientException) { + int totalTries = attempt + 1; + log.debug(() -> String.format("Error while executing EC2Metadata request. Total attempts: %d. %s", + totalTries, + sdkClientException.getMessage())); + throw sdkClientException; + } catch (IOException ioe) { + lastCause = new UncheckedIOException(ioe); + int currentTry = attempt; + log.debug(() -> "Error while executing EC2Metadata request, attempting retry. Current attempt: " + currentTry); + } + pauseBeforeRetryIfNeeded(retryPolicyContext); + } + + SdkClientException.Builder sdkClientExceptionBuilder = SdkClientException + .builder() + .message("Exceeded maximum number of retries. Total retry attempts: " + retryPolicy.numRetries()); + if (lastCause != null) { + String msg = sdkClientExceptionBuilder.message(); + sdkClientExceptionBuilder.cause(lastCause).message(msg); + } + throw sdkClientExceptionBuilder.build(); + } + + /** + * Gets token with fallback logic that can be cached. + * If token retrieval fails with 400 error, throws exception. + * Otherwise, returns null to indicate fallback to IMDSv1. + */ + private Token getTokenWithFallback() { + try { + return getToken(); + } catch (Exception e) { + boolean is400ServiceException = e instanceof Ec2MetadataClientException + && ((Ec2MetadataClientException) e).statusCode() == 400; + + // metadata resolution must not continue to the token-less flow for a 400 + if (is400ServiceException) { + throw SdkClientException.builder() + .message("Unable to fetch metadata token") + .cause(e) + .build(); + } + return handleTokenErrorResponse(e); + } + } + + /** + * Handles token error response following EC2MetadataUtils pattern. + */ + private Token handleTokenErrorResponse(Exception e) { + if (!imdsV1FallbackEnabled) { + String message = String.format("Failed to retrieve IMDS token, and fallback to IMDS v1 is disabled via the " + + "%s system property, %s environment variable, or %s configuration file profile" + + " setting.", + SdkSystemSetting.AWS_EC2_METADATA_V1_DISABLED.environmentVariable(), + SdkSystemSetting.AWS_EC2_METADATA_V1_DISABLED.property(), + ProfileProperty.EC2_METADATA_V1_DISABLED); + throw SdkClientException.builder() + .message(message) + .cause(e) + .build(); + } + return null; // null token indicates fallback to IMDSv1 + } + + + /** + * Resolves whether IMDSv1 is disabled from system settings and profile file. + */ + private boolean resolveImdsV1Disabled() { + return OptionalUtils.firstPresent( + fromSystemSettingsMetadataV1Disabled(), + () -> fromProfileFileMetadataV1Disabled() + ) + .orElse(false); + } + + private Optional fromSystemSettingsMetadataV1Disabled() { + return SdkSystemSetting.AWS_EC2_METADATA_V1_DISABLED.getStringValue() + .map(Boolean::parseBoolean); + } + + private Optional fromProfileFileMetadataV1Disabled() { + return Ec2MetadataConfigProvider.instance() + .resolveProfile() + .flatMap(p -> p.property(ProfileProperty.EC2_METADATA_V1_DISABLED)) + .map(Boolean::parseBoolean); + } + + private void handleUnsuccessfulResponse(int statusCode, Optional responseBody, + HttpExecuteResponse response, Supplier errorMessageSupplier) { + String responseContent = responseBody.map(BaseEc2MetadataClient::uncheckedInputStreamToUtf8) + .orElse(""); + + throw Ec2MetadataClientException.builder() + .statusCode(statusCode) + .sdkHttpResponse(response.httpResponse()) + .rawResponse(SdkBytes.fromUtf8String(responseContent)) + .message(errorMessageSupplier.get()) + .build(); + } + + private Ec2MetadataResponse sendRequest(String path, String token) throws IOException { + HttpExecuteRequest httpExecuteRequest = + HttpExecuteRequest.builder() + .request(requestMarshaller.createDataRequest(path, token, tokenTtl)) + .build(); + HttpExecuteResponse response = httpClient.prepareRequest(httpExecuteRequest).call(); + + int statusCode = response.httpResponse().statusCode(); + Optional responseBody = response.responseBody(); + + if (HttpStatusFamily.of(statusCode).isOneOf(HttpStatusFamily.SERVER_ERROR)) { + responseBody.map(BaseEc2MetadataClient::uncheckedInputStreamToUtf8) + .ifPresent(str -> log.debug(() -> "Metadata request response body: " + str)); + throw RetryableException + .builder() + .message(String.format("The requested metadata at path '%s' returned Http code %s", path, statusCode)) + .build(); + } + + if (!HttpStatusFamily.of(statusCode).isOneOf(HttpStatusFamily.SUCCESSFUL)) { + handleUnsuccessfulResponse(statusCode, responseBody, response, + () -> String.format("The requested metadata at path '%s' returned Http code %s", path, statusCode) + ); + } + + AbortableInputStream abortableInputStream = responseBody.orElseThrow( + SdkClientException.builder().message("Response body empty with Status Code " + statusCode)::build); + String data = uncheckedInputStreamToUtf8(abortableInputStream); + return Ec2MetadataResponse.create(data); + } + + private void pauseBeforeRetryIfNeeded(RetryPolicyContext retryPolicyContext) { + long backoffTimeMillis = retryPolicy.backoffStrategy() + .computeDelayBeforeNextRetry(retryPolicyContext) + .toMillis(); + if (backoffTimeMillis > 0) { + try { + TimeUnit.MILLISECONDS.sleep(backoffTimeMillis); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw SdkClientException.builder().message("Thread interrupted while trying to sleep").cause(e).build(); + } + } + } + + private Token getToken() { + HttpExecuteRequest httpExecuteRequest = HttpExecuteRequest.builder() + .request(requestMarshaller.createTokenRequest(tokenTtl)) + .build(); + HttpExecuteResponse response = null; + try { + response = httpClient.prepareRequest(httpExecuteRequest).call(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + + int statusCode = response.httpResponse().statusCode(); + + if (HttpStatusFamily.of(statusCode).isOneOf(HttpStatusFamily.SERVER_ERROR)) { + response.responseBody().map(BaseEc2MetadataClient::uncheckedInputStreamToUtf8) + .ifPresent(str -> log.debug(() -> "Metadata request response body: " + str)); + throw RetryableException.builder() + .message("Could not retrieve token, " + statusCode + " error occurred").build(); + } + + if (!HttpStatusFamily.of(statusCode).isOneOf(HttpStatusFamily.SUCCESSFUL)) { + handleUnsuccessfulResponse(statusCode, response.responseBody(), response, + () -> String.format("Could not retrieve token, %d error occurred", statusCode) + ); + } + + String ttl = response.httpResponse() + .firstMatchingHeader(RequestMarshaller.EC2_METADATA_TOKEN_TTL_HEADER) + .orElseThrow(() -> SdkClientException + .builder() + .message(RequestMarshaller.EC2_METADATA_TOKEN_TTL_HEADER + " header not found in token response") + .build()); + java.time.Duration ttlDuration; + try { + ttlDuration = java.time.Duration.ofSeconds(Long.parseLong(ttl)); + } catch (NumberFormatException nfe) { + throw SdkClientException.create("Invalid token format received from IMDS server", nfe); + } + + AbortableInputStream abortableInputStream = response.responseBody().orElseThrow( + SdkClientException.builder().message("Empty response body")::build); + + String value = uncheckedInputStreamToUtf8(abortableInputStream); + return new Token(value, ttlDuration); + } + + protected static final class Ec2MetadataBuilder implements Ec2MetadataClient.Builder { + + private Ec2MetadataRetryPolicy retryPolicy; + private URI endpoint; + private Duration tokenTtl; + private EndpointMode endpointMode; + private SdkHttpClient httpClient; + private SdkHttpClient.Builder httpClientBuilder; + + private Ec2MetadataBuilder() { + } + + @Override + public Ec2MetadataBuilder retryPolicy(Ec2MetadataRetryPolicy retryPolicy) { + this.retryPolicy = retryPolicy; + return this; + } + + @Override + public Builder retryPolicy(java.util.function.Consumer builderConsumer) { + Validate.notNull(builderConsumer, "builderConsumer must not be null"); + Ec2MetadataRetryPolicy.Builder builder = Ec2MetadataRetryPolicy.builder(); + builderConsumer.accept(builder); + return retryPolicy(builder.build()); + } + + @Override + public Ec2MetadataBuilder endpoint(java.net.URI endpoint) { + this.endpoint = endpoint; + return this; + } + + @Override + public Ec2MetadataBuilder tokenTtl(java.time.Duration tokenTtl) { + this.tokenTtl = tokenTtl; + return this; + } + + @Override + public Ec2MetadataBuilder endpointMode(EndpointMode endpointMode) { + this.endpointMode = endpointMode; + return this; + } + + @Override + public Ec2MetadataBuilder httpClient(SdkHttpClient httpClient) { + this.httpClient = httpClient; + return this; + } + + @Override + public Builder httpClient(SdkHttpClient.Builder builder) { + this.httpClientBuilder = builder; + return this; + } + + public Ec2MetadataRetryPolicy getRetryPolicy() { + return this.retryPolicy; + } + + public java.net.URI getEndpoint() { + return this.endpoint; + } + + public java.time.Duration getTokenTtl() { + return this.tokenTtl; + } + + public EndpointMode getEndpointMode() { + return this.endpointMode; + } + + @Override + public Ec2MetadataClient build() { + return new DefaultEc2MetadataClientWithFallback(this); + } + } +} diff --git a/core/imds/src/main/java/software/amazon/awssdk/imds/internal/Ec2MetadataSharedClient.java b/core/imds/src/main/java/software/amazon/awssdk/imds/internal/Ec2MetadataSharedClient.java new file mode 100644 index 000000000000..a9eebadb9d70 --- /dev/null +++ b/core/imds/src/main/java/software/amazon/awssdk/imds/internal/Ec2MetadataSharedClient.java @@ -0,0 +1,114 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.imds.internal; + +import java.time.Duration; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import software.amazon.awssdk.annotations.SdkProtectedApi; +import software.amazon.awssdk.core.internal.http.loader.DefaultSdkHttpClientBuilder; +import software.amazon.awssdk.http.SdkHttpClient; +import software.amazon.awssdk.http.SdkHttpConfigurationOption; +import software.amazon.awssdk.imds.Ec2MetadataClient; +import software.amazon.awssdk.imds.Ec2MetadataRetryPolicy; +import software.amazon.awssdk.utils.AttributeMap; + +/** + * Creates Ec2MetadataClient instances using a shared HTTP client internally. + * This provides resource efficiency by sharing a single HTTP client across all IMDS-backed providers + */ +@SdkProtectedApi +public final class Ec2MetadataSharedClient { + + private static final Lock LOCK = new ReentrantLock(); + private static volatile SdkHttpClient sharedHttpClient; + private static int referenceCount = 0; + + private Ec2MetadataSharedClient() { + // Prevent instantiation + } + + /** + * Creates a builder for configuring Ec2MetadataClient with shared HTTP client. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Creates a new Ec2MetadataClient instance using the shared HTTP client + * with default configuration. + */ + public static Ec2MetadataClient create() { + return builder().build(); + } + + /** + * Decrements the reference count and closes the shared HTTP client if no more references exist. + */ + public static void decrementAndClose() { + LOCK.lock(); + try { + referenceCount--; + if (referenceCount == 0 && sharedHttpClient != null) { + sharedHttpClient.close(); + sharedHttpClient = null; + } + } finally { + LOCK.unlock(); + } + } + + private static SdkHttpClient createImdsHttpClient() { + Duration metadataServiceTimeout = Ec2MetadataConfigProvider.instance().resolveServiceTimeout(); + AttributeMap imdsHttpDefaults = AttributeMap.builder() + .put(SdkHttpConfigurationOption.CONNECTION_TIMEOUT, metadataServiceTimeout) + .put(SdkHttpConfigurationOption.READ_TIMEOUT, metadataServiceTimeout) + .build(); + + return new DefaultSdkHttpClientBuilder().buildWithDefaults(imdsHttpDefaults); + } + + public static final class Builder { + private Ec2MetadataRetryPolicy retryPolicy; + + private Builder() { + } + + public Builder retryPolicy(Ec2MetadataRetryPolicy retryPolicy) { + this.retryPolicy = retryPolicy; + return this; + } + + public Ec2MetadataClient build() { + LOCK.lock(); + try { + if (sharedHttpClient == null) { + sharedHttpClient = createImdsHttpClient(); + } + + referenceCount++; + + return DefaultEc2MetadataClientWithFallback.builder() + .httpClient(sharedHttpClient) + .retryPolicy(retryPolicy) + .build(); + } finally { + LOCK.unlock(); + } + } + } +} diff --git a/core/imds/src/main/java/software/amazon/awssdk/imds/internal/RequestMarshaller.java b/core/imds/src/main/java/software/amazon/awssdk/imds/internal/RequestMarshaller.java index 516def7dd985..cc721143da36 100644 --- a/core/imds/src/main/java/software/amazon/awssdk/imds/internal/RequestMarshaller.java +++ b/core/imds/src/main/java/software/amazon/awssdk/imds/internal/RequestMarshaller.java @@ -50,7 +50,7 @@ public RequestMarshaller(URI basePath) { } public SdkHttpFullRequest createTokenRequest(Duration tokenTtl) { - return defaulttHttpBuilder() + return defaultHttpBuilder() .method(SdkHttpMethod.PUT) .uri(tokenPath) .putHeader(EC2_METADATA_TOKEN_TTL_HEADER, String.valueOf(tokenTtl.getSeconds())) @@ -59,15 +59,17 @@ public SdkHttpFullRequest createTokenRequest(Duration tokenTtl) { public SdkHttpFullRequest createDataRequest(String path, String token, Duration tokenTtl) { URI resourcePath = URI.create(basePath + path); - return defaulttHttpBuilder() + SdkHttpFullRequest.Builder builder = defaultHttpBuilder() .method(SdkHttpMethod.GET) .uri(resourcePath) - .putHeader(EC2_METADATA_TOKEN_TTL_HEADER, String.valueOf(tokenTtl.getSeconds())) - .putHeader(TOKEN_HEADER, token) - .build(); + .putHeader(EC2_METADATA_TOKEN_TTL_HEADER, String.valueOf(tokenTtl.getSeconds())); + if (token != null) { + builder.putHeader(TOKEN_HEADER, token); + } + return builder.build(); } - private SdkHttpFullRequest.Builder defaulttHttpBuilder() { + private SdkHttpFullRequest.Builder defaultHttpBuilder() { return SdkHttpFullRequest.builder() .putHeader(USER_AGENT, SystemUserAgent.getOrCreate().userAgentString()) .putHeader(ACCEPT, "*/*") diff --git a/core/imds/src/test/java/software/amazon/awssdk/imds/internal/DefaultEc2MetadataClientWithFallbackTest.java b/core/imds/src/test/java/software/amazon/awssdk/imds/internal/DefaultEc2MetadataClientWithFallbackTest.java new file mode 100644 index 000000000000..0d1a70747b62 --- /dev/null +++ b/core/imds/src/test/java/software/amazon/awssdk/imds/internal/DefaultEc2MetadataClientWithFallbackTest.java @@ -0,0 +1,238 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.imds.internal; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.put; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.putRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; +import static com.github.tomakehurst.wiremock.client.WireMock.matching; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.github.tomakehurst.wiremock.junit5.WireMockExtension; +import java.net.URI; +import java.time.Duration; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import software.amazon.awssdk.core.SdkSystemSetting; +import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.imds.Ec2MetadataClient; +import software.amazon.awssdk.imds.Ec2MetadataResponse; +import software.amazon.awssdk.imds.Ec2MetadataRetryPolicy; +import software.amazon.awssdk.testutils.EnvironmentVariableHelper; + +/** + * Tests for DefaultEc2MetadataClientWithFallback to verify IMDSv1 fallback behavior. + */ +public class DefaultEc2MetadataClientWithFallbackTest { + private static final EnvironmentVariableHelper ENVIRONMENT_VARIABLE_HELPER = new EnvironmentVariableHelper(); + + @RegisterExtension + static WireMockExtension wireMock = WireMockExtension.newInstance() + .options(wireMockConfig().dynamicPort().dynamicPort()) + .configureStaticDsl(true) + .build(); + + private Ec2MetadataClient client; + + @BeforeEach + public void setup() { + clearEnvironmentVariable(SdkSystemSetting.AWS_EC2_METADATA_V1_DISABLED.environmentVariable()); + + // Create client with WireMock endpoint + client = DefaultEc2MetadataClientWithFallback.builder() + .endpoint(URI.create("http://localhost:" + wireMock.getPort())) + .retryPolicy(Ec2MetadataRetryPolicy.builder().numRetries(0).build()) + .tokenTtl(Duration.ofSeconds(21600)) + .build(); + } + + @AfterEach + public void cleanup() { + if (client != null) { + client.close(); + } + wireMock.resetAll(); + ENVIRONMENT_VARIABLE_HELPER.reset(); + } + + private void clearEnvironmentVariable(String name) { + try { + ENVIRONMENT_VARIABLE_HELPER.set(name, null); + } catch (Exception e) { + //Ignore + } + } + + @Test + public void imdsV2Success_shouldUseTokenAndReturnData() { + // Stub successful IMDSv2 flow + stubFor(put("/latest/api/token") + .willReturn(aResponse() + .withStatus(200) + .withHeader("x-aws-ec2-metadata-token-ttl-seconds", "21600") + .withBody("test-token"))); + + stubFor(get("/latest/meta-data/placement/region") + .withHeader("x-aws-ec2-metadata-token", matching("test-token")) + .willReturn(aResponse().withStatus(200).withBody("us-east-1"))); + + Ec2MetadataResponse response = client.get("/latest/meta-data/placement/region"); + + assertThat(response.asString()).isEqualTo("us-east-1"); + + // Verify both token and data requests were made + verify(putRequestedFor(urlEqualTo("/latest/api/token"))); + verify(getRequestedFor(urlEqualTo("/latest/meta-data/placement/region")) + .withHeader("x-aws-ec2-metadata-token", matching("test-token"))); + } + + @Test + public void imdsV1Fallback_shouldWorkWhenTokenFails() { + // Stub token request to fail with 500 + stubFor(put("/latest/api/token") + .willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))); + + // Stub successful IMDSv1 request + stubFor(get("/latest/meta-data/placement/region") + .willReturn(aResponse().withStatus(200).withBody("us-west-2"))); + + Ec2MetadataResponse response = client.get("/latest/meta-data/placement/region"); + + assertThat(response.asString()).isEqualTo("us-west-2"); + + verify(putRequestedFor(urlEqualTo("/latest/api/token"))); + verify(getRequestedFor(urlEqualTo("/latest/meta-data/placement/region")) + .withoutHeader("x-aws-ec2-metadata-token")); + + // Verify no IMDSv2 requests were made + verify(0, getRequestedFor(urlEqualTo("/latest/meta-data/placement/region")) + .withHeader("x-aws-ec2-metadata-token", matching(".*"))); + } + + @Test + public void imdsV1Fallback_shouldNotWorkWith400Error() { + // Stub token request to fail with 400 + stubFor(put("/latest/api/token") + .willReturn(aResponse().withStatus(400).withBody("Bad Request"))); + + assertThatThrownBy(() -> client.get("/latest/meta-data/placement/region")) + .isInstanceOf(SdkClientException.class) + .hasMessageContaining("Unable to fetch metadata token"); + + verify(putRequestedFor(urlEqualTo("/latest/api/token"))); + verify(0, getRequestedFor(urlEqualTo("/latest/meta-data/placement/region"))); + } + + @Test + public void imdsV1Fallback_shouldNotWorkWhenDisabled() { + // Disable IMDSv1 fallback + ENVIRONMENT_VARIABLE_HELPER.set(SdkSystemSetting.AWS_EC2_METADATA_V1_DISABLED.environmentVariable(), "true"); + + // Recreate client to pick up environment variable + client.close(); + client = DefaultEc2MetadataClientWithFallback.builder() + .endpoint(URI.create("http://localhost:" + wireMock.getPort())) + .retryPolicy(Ec2MetadataRetryPolicy.builder().numRetries(0).build()) + .tokenTtl(Duration.ofSeconds(21600)) + .build(); + + stubFor(put("/latest/api/token") + .willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))); + + // Should throw exception without attempting IMDSv1 fallback + assertThatThrownBy(() -> client.get("/latest/meta-data/placement/region")) + .isInstanceOf(SdkClientException.class) + .hasMessageContaining("fallback to IMDS v1 is disabled"); + + + verify(putRequestedFor(urlEqualTo("/latest/api/token"))); + verify(0, getRequestedFor(urlEqualTo("/latest/meta-data/placement/region"))); + } + + @Test + public void tokenCaching_shouldReuseValidToken() { + // Stub successful token request + stubFor(put("/latest/api/token") + .willReturn(aResponse() + .withStatus(200) + .withHeader("x-aws-ec2-metadata-token-ttl-seconds", "21600") + .withBody("cached-token"))); + + // Stub successful data requests + stubFor(get("/latest/meta-data/placement/region") + .withHeader("x-aws-ec2-metadata-token", matching("cached-token")) + .willReturn(aResponse().withStatus(200).withBody("us-east-1"))); + + stubFor(get("/latest/meta-data/instance-id") + .withHeader("x-aws-ec2-metadata-token", matching("cached-token")) + .willReturn(aResponse().withStatus(200).withBody("i1234"))); + + // Make two requests + Ec2MetadataResponse response1 = client.get("/latest/meta-data/placement/region"); + Ec2MetadataResponse response2 = client.get("/latest/meta-data/instance-id"); + + assertThat(response1.asString()).isEqualTo("us-east-1"); + assertThat(response2.asString()).isEqualTo("i1234"); + + // Verify token was requested only once + verify(1, putRequestedFor(urlEqualTo("/latest/api/token"))); + + // Verify both data requests were made with the same token + verify(getRequestedFor(urlEqualTo("/latest/meta-data/placement/region")) + .withHeader("x-aws-ec2-metadata-token", matching("cached-token"))); + verify(getRequestedFor(urlEqualTo("/latest/meta-data/instance-id")) + .withHeader("x-aws-ec2-metadata-token", matching("cached-token"))); + } + + @Test + public void fallbackAfterTokenFailure_shouldUseImdsV1ForSubsequentRequests() { + // Stub token request to fail + stubFor(put("/latest/api/token") + .willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))); + + // Stub successful IMDSv1 requests + stubFor(get("/latest/meta-data/placement/region") + .willReturn(aResponse().withStatus(200).withBody("us-west-2"))); + + stubFor(get("/latest/meta-data/instance-id") + .willReturn(aResponse().withStatus(200).withBody("i-123"))); + + // Make two requests - both should use IMDSv1 fallback + Ec2MetadataResponse response1 = client.get("/latest/meta-data/placement/region"); + Ec2MetadataResponse response2 = client.get("/latest/meta-data/instance-id"); + + assertThat(response1.asString()).isEqualTo("us-west-2"); + assertThat(response2.asString()).isEqualTo("i-123"); + + // Verify token was requested only once + verify(1, putRequestedFor(urlEqualTo("/latest/api/token"))); + + + verify(getRequestedFor(urlEqualTo("/latest/meta-data/placement/region")) + .withoutHeader("x-aws-ec2-metadata-token")); + verify(getRequestedFor(urlEqualTo("/latest/meta-data/instance-id")) + .withoutHeader("x-aws-ec2-metadata-token")); + } +} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/loader/DefaultSdkHttpClientBuilder.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/loader/DefaultSdkHttpClientBuilder.java index 5fc4b4b45ec7..00539ab7b506 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/loader/DefaultSdkHttpClientBuilder.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/loader/DefaultSdkHttpClientBuilder.java @@ -15,7 +15,7 @@ package software.amazon.awssdk.core.internal.http.loader; -import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.annotations.SdkProtectedApi; import software.amazon.awssdk.core.exception.SdkClientException; import software.amazon.awssdk.http.SdkHttpClient; import software.amazon.awssdk.http.SdkHttpService; @@ -23,8 +23,11 @@ /** * Utility to load the default HTTP client factory and create an instance of {@link SdkHttpClient}. + * + * Implementation notes: this class should've been outside internal package, + * but we can't fix it due to backwards compatibility reasons. */ -@SdkInternalApi +@SdkProtectedApi public final class DefaultSdkHttpClientBuilder implements SdkHttpClient.Builder { private static final SdkHttpServiceProvider DEFAULT_CHAIN = new CachingSdkHttpServiceProvider<>( diff --git a/test/architecture-tests/archunit_store/c898eee1-aeb5-4355-bb3b-ea56bf58cacb b/test/architecture-tests/archunit_store/c898eee1-aeb5-4355-bb3b-ea56bf58cacb index 62572136f3db..afb6739bc1fe 100644 --- a/test/architecture-tests/archunit_store/c898eee1-aeb5-4355-bb3b-ea56bf58cacb +++ b/test/architecture-tests/archunit_store/c898eee1-aeb5-4355-bb3b-ea56bf58cacb @@ -27,3 +27,5 @@ Class does not reside outsid Class does not reside outside of package '..internal..' in (EnumUtils.java:0) Class does not reside outside of package '..internal..' in (MappingSubscriber.java:0) Class does not reside outside of package '..internal..' in (SystemSettingUtils.java:0) +Class does not reside outside of package '..internal..' in (DefaultSdkHttpClientBuilder.java:0) +Class does not reside outside of package '..internal..' in (Ec2MetadataSharedClient.java:0)