From 97f9314f8169fe523aa4f8b4d13a54f66da40def Mon Sep 17 00:00:00 2001 From: Saranya Somepalli Date: Thu, 12 Jun 2025 08:45:45 -0700 Subject: [PATCH 01/12] EC2 IMDS Changes to Support Account ID --- .../next-release/feature-AWSEC2-9b178a4.json | 6 + ...ileCredentialsProviderIntegrationTest.java | 2 +- .../InstanceProfileCredentialsProvider.java | 73 +++++-- .../internal/HttpCredentialsLoader.java | 17 +- ...ofileCredentialsProviderAccountIDTest.java | 181 ++++++++++++++++++ core/regions/pom.xml | 5 + .../regions/util/HttpResourcesUtils.java | 7 +- 7 files changed, 271 insertions(+), 20 deletions(-) create mode 100644 .changes/next-release/feature-AWSEC2-9b178a4.json create mode 100644 core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderAccountIDTest.java diff --git a/.changes/next-release/feature-AWSEC2-9b178a4.json b/.changes/next-release/feature-AWSEC2-9b178a4.json new file mode 100644 index 000000000000..f4c5695426a8 --- /dev/null +++ b/.changes/next-release/feature-AWSEC2-9b178a4.json @@ -0,0 +1,6 @@ +{ + "type": "feature", + "category": "AWS EC2", + "contributor": "", + "description": "EC2 IMDS Changes to Support Account ID" +} diff --git a/core/auth/src/it/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderIntegrationTest.java b/core/auth/src/it/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderIntegrationTest.java index ad54813ec782..c3a6e0dbf874 100644 --- a/core/auth/src/it/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderIntegrationTest.java +++ b/core/auth/src/it/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderIntegrationTest.java @@ -35,7 +35,7 @@ public class InstanceProfileCredentialsProviderIntegrationTest { /** Starts up the mock EC2 Instance Metadata Service. */ @Before public void setUp() throws Exception { - mockServer = new EC2MetadataServiceMock("/latest/meta-data/iam/security-credentials/"); + mockServer = new EC2MetadataServiceMock("/latest/meta-data/iam/security-credentials-extended/"); mockServer.start(); } diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java index b1ddc5d7faef..661d64b7b260 100644 --- a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java +++ b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java @@ -37,12 +37,14 @@ import software.amazon.awssdk.core.SdkSystemSetting; import software.amazon.awssdk.core.exception.SdkClientException; import software.amazon.awssdk.core.exception.SdkServiceException; +import software.amazon.awssdk.imds.Ec2MetadataClientException; import software.amazon.awssdk.profiles.ProfileFile; import software.amazon.awssdk.profiles.ProfileFileSupplier; import software.amazon.awssdk.profiles.ProfileFileSystemSetting; import software.amazon.awssdk.profiles.ProfileProperty; import software.amazon.awssdk.regions.util.HttpResourcesUtils; import software.amazon.awssdk.regions.util.ResourcesEndpointProvider; +import software.amazon.awssdk.utils.Lazy; import software.amazon.awssdk.utils.Logger; import software.amazon.awssdk.utils.ToString; import software.amazon.awssdk.utils.Validate; @@ -70,9 +72,19 @@ public final class InstanceProfileCredentialsProvider private static final String PROVIDER_NAME = "InstanceProfileCredentialsProvider"; private static final String EC2_METADATA_TOKEN_HEADER = "x-aws-ec2-metadata-token"; private static final String SECURITY_CREDENTIALS_RESOURCE = "/latest/meta-data/iam/security-credentials/"; + private static final String SECURITY_CREDENTIALS_EXTENDED_RESOURCE = "/latest/meta-data/iam/security-credentials-extended/"; private static final String TOKEN_RESOURCE = "/latest/api/token"; + + private enum ApiVersion { + UNKNOWN, + LEGACY, + EXTENDED + } + private static final String EC2_METADATA_TOKEN_TTL_HEADER = "x-aws-ec2-metadata-token-ttl-seconds"; private static final String DEFAULT_TOKEN_TTL = "21600"; + private Lazy apiVersion = new Lazy<>(() -> ApiVersion.UNKNOWN); + private Lazy resolvedProfile = new Lazy<>(() -> null); private final Clock clock; private final String endpoint; @@ -157,6 +169,11 @@ private RefreshResult refreshCredentials() { try { LoadedCredentials credentials = httpCredentialsLoader.loadCredentials(createEndpointProvider()); + ApiVersion currentVersion = apiVersion.getValue(); + if (currentVersion == ApiVersion.UNKNOWN) { + apiVersion = Lazy.withValue(ApiVersion.EXTENDED); + } + Instant expiration = credentials.getExpiration().orElse(null); log.debug(() -> "Loaded credentials from IMDS with expiration time of " + expiration); @@ -164,7 +181,15 @@ private RefreshResult refreshCredentials() { .staleTime(staleTime(expiration)) .prefetchTime(prefetchTime(expiration)) .build(); + } catch (Ec2MetadataClientException e) { + if (apiVersion.getValue() == ApiVersion.UNKNOWN) { + apiVersion = Lazy.withValue(ApiVersion.LEGACY); + resolvedProfile = new Lazy<>(() -> null); + return refreshCredentials(); + } + throw SdkClientException.create("Failed to load credentials from IMDS.", e); } catch (RuntimeException e) { + resolvedProfile = new Lazy<>(() -> null); throw SdkClientException.create("Failed to load credentials from IMDS.", e); } } @@ -207,14 +232,20 @@ public String toString() { return ToString.create(PROVIDER_NAME); } + private String getSecurityCredentialsResource() { + return apiVersion.getValue() == ApiVersion.LEGACY ? + SECURITY_CREDENTIALS_RESOURCE : + SECURITY_CREDENTIALS_EXTENDED_RESOURCE; + } + private ResourcesEndpointProvider createEndpointProvider() { String imdsHostname = getImdsEndpoint(); String token = getToken(imdsHostname); String[] securityCredentials = getSecurityCredentials(imdsHostname, token); - + String urlBase = getSecurityCredentialsResource(); + return StaticResourcesEndpointProvider.builder() - .endpoint(URI.create(imdsHostname + SECURITY_CREDENTIALS_RESOURCE - + securityCredentials[0])) + .endpoint(URI.create(imdsHostname + urlBase + securityCredentials[0])) .headers(getTokenHeaders(token)) .connectionTimeout(Duration.ofMillis( this.configProvider.serviceTimeout())) @@ -285,21 +316,41 @@ private boolean isInsecureFallbackDisabled() { } private String[] getSecurityCredentials(String imdsHostname, String metadataToken) { + if (resolvedProfile.hasValue()) { + return new String[]{resolvedProfile.getValue()}; + } + + String urlBase = getSecurityCredentialsResource(); ResourcesEndpointProvider securityCredentialsEndpoint = StaticResourcesEndpointProvider.builder() - .endpoint(URI.create(imdsHostname + SECURITY_CREDENTIALS_RESOURCE)) + .endpoint(URI.create(imdsHostname + urlBase)) .headers(getTokenHeaders(metadataToken)) - .connectionTimeout(Duration.ofMillis(this.configProvider.serviceTimeout())) + .connectionTimeout(Duration.ofMillis(this.configProvider.serviceTimeout())) .build(); - String securityCredentialsList = - invokeSafely(() -> HttpResourcesUtils.instance().readResource(securityCredentialsEndpoint)); - String[] securityCredentials = securityCredentialsList.trim().split("\n"); + try { + String securityCredentialsList = + invokeSafely(() -> HttpResourcesUtils.instance().readResource(securityCredentialsEndpoint)); + String[] securityCredentials = securityCredentialsList.trim().split("\n"); + + if (securityCredentials.length == 0) { + throw SdkClientException.builder().message("Unable to load credentials path").build(); + } - if (securityCredentials.length == 0) { - throw SdkClientException.builder().message("Unable to load credentials path").build(); + ApiVersion currentVersion = apiVersion.getValue(); + if (currentVersion == ApiVersion.UNKNOWN) { + apiVersion = Lazy.withValue(ApiVersion.EXTENDED); + } + resolvedProfile = new Lazy<>(() -> securityCredentials[0]); + return securityCredentials; + + } catch (Ec2MetadataClientException e) { + if (apiVersion.getValue() == ApiVersion.UNKNOWN) { + apiVersion = Lazy.withValue(ApiVersion.LEGACY); + return getSecurityCredentials(imdsHostname, metadataToken); + } + throw SdkClientException.create("Failed to load credentials from IMDS.", e); } - return securityCredentials; } private Map getTokenHeaders(String metadataToken) { diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/internal/HttpCredentialsLoader.java b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/internal/HttpCredentialsLoader.java index 507ae7c6f44f..364dcf131eb7 100644 --- a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/internal/HttpCredentialsLoader.java +++ b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/internal/HttpCredentialsLoader.java @@ -64,15 +64,17 @@ public LoadedCredentials loadCredentials(ResourcesEndpointProvider endpoint) { JsonNode secretKey = node.get("SecretAccessKey"); JsonNode token = node.get("Token"); JsonNode expiration = node.get("Expiration"); + JsonNode accountId = node.get("AccountId"); Validate.notNull(accessKey, "Failed to load access key from metadata service."); Validate.notNull(secretKey, "Failed to load secret key from metadata service."); return new LoadedCredentials(accessKey.text(), - secretKey.text(), - token != null ? token.text() : null, - expiration != null ? expiration.text() : null, - providerName); + secretKey.text(), + token != null ? token.text() : null, + expiration != null ? expiration.text() : null, + accountId != null ? accountId.text() : null, + providerName); } catch (SdkClientException e) { throw e; } catch (RuntimeException | IOException e) { @@ -89,12 +91,15 @@ public static final class LoadedCredentials { private final String token; private final Instant expiration; private final String providerName; + private final String accountId; - private LoadedCredentials(String accessKeyId, String secretKey, String token, String expiration, String providerName) { + private LoadedCredentials(String accessKeyId, String secretKey, String token, + String expiration, String accountId, String providerName) { this.accessKeyId = Validate.paramNotBlank(accessKeyId, "accessKeyId"); this.secretKey = Validate.paramNotBlank(secretKey, "secretKey"); this.token = token; this.expiration = expiration == null ? null : parseExpiration(expiration); + this.accountId = accountId; this.providerName = providerName; } @@ -105,11 +110,13 @@ public AwsCredentials getAwsCredentials() { .secretAccessKey(secretKey) .sessionToken(token) .providerName(providerName) + .accountId(accountId) .build() : AwsBasicCredentials.builder() .accessKeyId(accessKeyId) .secretAccessKey(secretKey) .providerName(providerName) + .accountId(accountId) .build(); } diff --git a/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderAccountIDTest.java b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderAccountIDTest.java new file mode 100644 index 000000000000..22a875ecf003 --- /dev/null +++ b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderAccountIDTest.java @@ -0,0 +1,181 @@ +/* + * 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.auth.credentials; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.put; +import static com.github.tomakehurst.wiremock.client.WireMock.putRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; +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 com.github.tomakehurst.wiremock.junit5.WireMockTest; +import org.junit.jupiter.api.AfterAll; +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.testutils.EnvironmentVariableHelper; +import software.amazon.awssdk.utils.DateUtils; + +import java.time.Duration; +import java.time.Instant; + +/** + * Tests verifying IMDS credential resolution with account ID support. + */ +@WireMockTest +public class InstanceProfileCredentialsProviderAccountIDTest { + private static final String TOKEN_RESOURCE_PATH = "/latest/api/token"; + private static final String CREDENTIALS_RESOURCE_PATH = "/latest/meta-data/iam/security-credentials/"; + private static final String CREDENTIALS_EXTENDED_RESOURCE_PATH = "/latest/meta-data/iam/security-credentials-extended/"; + private static final String TOKEN_HEADER = "x-aws-ec2-metadata-token"; + private static final String TOKEN_STUB = "some-token"; + private static final String PROFILE_NAME = "some-profile"; + private static final String EC2_METADATA_TOKEN_TTL_HEADER = "x-aws-ec2-metadata-token-ttl-seconds"; + private static final String ACCOUNT_ID = "123456789012"; + private static final EnvironmentVariableHelper environmentVariableHelper = new EnvironmentVariableHelper(); + + @RegisterExtension + static WireMockExtension wireMockServer = WireMockExtension.newInstance() + .options(wireMockConfig().dynamicPort()) + .configureStaticDsl(true) + .build(); + + @BeforeEach + public void methodSetup() { + environmentVariableHelper.reset(); + System.setProperty(SdkSystemSetting.AWS_EC2_METADATA_SERVICE_ENDPOINT.property(), + "http://localhost:" + wireMockServer.getPort()); + } + + @AfterAll + public static void teardown() { + System.clearProperty(SdkSystemSetting.AWS_EC2_METADATA_SERVICE_ENDPOINT.property()); + environmentVariableHelper.reset(); + } + + @Test + void resolveCredentials_usesExtendedEndpoint_withAccountId() { + String credentialsWithAccountId = String.format( + "{\"AccessKeyId\":\"ACCESS_KEY_ID\"," + + "\"SecretAccessKey\":\"SECRET_ACCESS_KEY\"," + + "\"Token\":\"SESSION_TOKEN\"," + + "\"Expiration\":\"%s\"," + + "\"AccountId\":\"%s\"}", + DateUtils.formatIso8601Date(Instant.now().plus(Duration.ofDays(1))), + ACCOUNT_ID + ); + + stubSecureCredentialsResponse(aResponse().withBody(credentialsWithAccountId), true); + InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider.builder().build(); + AwsCredentials credentials = provider.resolveCredentials(); + + assertThat(credentials.accessKeyId()).isEqualTo("ACCESS_KEY_ID"); + assertThat(credentials.secretAccessKey()).isEqualTo("SECRET_ACCESS_KEY"); + assertThat(((AwsSessionCredentials)credentials).sessionToken()).isEqualTo("SESSION_TOKEN"); + assertThat(credentials.accountId()).hasValue(ACCOUNT_ID); + verifyImdsCallWithToken(true); + } + + @Test + void resolveCredentials_fallsBackToLegacy_noAccountId() { + String credentialsWithoutAccountId = String.format( + "{\"AccessKeyId\":\"ACCESS_KEY_ID\"," + + "\"SecretAccessKey\":\"SECRET_ACCESS_KEY\"," + + "\"Token\":\"SESSION_TOKEN\"," + + "\"Expiration\":\"%s\"," + + "\"Code\":\"Success\"}", // No AccountId field at all + DateUtils.formatIso8601Date(Instant.now().plus(Duration.ofDays(1))) + ); + + stubSecureCredentialsResponse(aResponse().withBody(credentialsWithoutAccountId), false); + InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider.builder().build(); + AwsCredentials credentials = provider.resolveCredentials(); + + assertThat(credentials.accessKeyId()).isEqualTo("ACCESS_KEY_ID"); + assertThat(credentials.secretAccessKey()).isEqualTo("SECRET_ACCESS_KEY"); + assertThat(((AwsSessionCredentials)credentials).sessionToken()).isEqualTo("SESSION_TOKEN"); + verifyImdsCallWithToken(false); + } + + @Test + void resolveCredentials_cachesProfile_maintainsAccountId() { + String credentialsWithAccountId = String.format( + "{\"AccessKeyId\":\"ACCESS_KEY_ID\"," + + "\"SecretAccessKey\":\"SECRET_ACCESS_KEY\"," + + "\"Token\":\"SESSION_TOKEN\"," + + "\"Expiration\":\"%s\"," + + "\"AccountId\":\"%s\"}", + DateUtils.formatIso8601Date(Instant.now().plus(Duration.ofDays(1))), + ACCOUNT_ID + ); + + stubSecureCredentialsResponse(aResponse().withBody(credentialsWithAccountId), true); + InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider.builder().build(); + + // First call + AwsCredentials creds1 = provider.resolveCredentials(); + assertThat(creds1.accountId()).hasValue(ACCOUNT_ID); + + // Second call - should use cached profile + AwsCredentials creds2 = provider.resolveCredentials(); + assertThat(creds2.accountId()).hasValue(ACCOUNT_ID); + + // Verify profile discovery only called once + verify(1, getRequestedFor(urlPathEqualTo(CREDENTIALS_EXTENDED_RESOURCE_PATH))); + } + + private void stubSecureCredentialsResponse(com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder responseDefinitionBuilder, boolean useExtended) { + wireMockServer.stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody(TOKEN_STUB))); + String path = useExtended ? CREDENTIALS_EXTENDED_RESOURCE_PATH : CREDENTIALS_RESOURCE_PATH; + + if (useExtended) { + wireMockServer.stubFor(get(urlPathEqualTo(path)).willReturn(aResponse().withBody(PROFILE_NAME))); + wireMockServer.stubFor(get(urlPathEqualTo(path + PROFILE_NAME)).willReturn(responseDefinitionBuilder)); + } else { + // Extended endpoint fails, fallback to legacy + wireMockServer.stubFor(get(urlPathEqualTo(CREDENTIALS_EXTENDED_RESOURCE_PATH)) + .willReturn(aResponse().withStatus(404))); + wireMockServer.stubFor(get(urlPathEqualTo(CREDENTIALS_EXTENDED_RESOURCE_PATH + PROFILE_NAME)) + .willReturn(aResponse().withStatus(404))); + wireMockServer.stubFor(get(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH)).willReturn(aResponse().withBody(PROFILE_NAME))); + wireMockServer.stubFor(get(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + PROFILE_NAME)).willReturn(responseDefinitionBuilder)); + } + } + + private void verifyImdsCallWithToken(boolean useExtended) { + verify(putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)) + .withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600"))); + + String path = useExtended ? CREDENTIALS_EXTENDED_RESOURCE_PATH : CREDENTIALS_RESOURCE_PATH; + verify(getRequestedFor(urlPathEqualTo(path)) + .withHeader(TOKEN_HEADER, equalTo(TOKEN_STUB))); + verify(getRequestedFor(urlPathEqualTo(path + PROFILE_NAME)) + .withHeader(TOKEN_HEADER, equalTo(TOKEN_STUB))); + + if (useExtended) { + // Verify extended endpoint was tried first + verify(getRequestedFor(urlPathEqualTo(CREDENTIALS_EXTENDED_RESOURCE_PATH))); + verify(getRequestedFor(urlPathEqualTo(CREDENTIALS_EXTENDED_RESOURCE_PATH + PROFILE_NAME))); + } + } +} diff --git a/core/regions/pom.xml b/core/regions/pom.xml index e0f4f0ace379..b025854e0afb 100644 --- a/core/regions/pom.xml +++ b/core/regions/pom.xml @@ -33,6 +33,11 @@ annotations ${awsjavasdk.version} + + software.amazon.awssdk + imds + ${awsjavasdk.version} + software.amazon.awssdk utils diff --git a/core/regions/src/main/java/software/amazon/awssdk/regions/util/HttpResourcesUtils.java b/core/regions/src/main/java/software/amazon/awssdk/regions/util/HttpResourcesUtils.java index 44c80de7db0b..7bc2e2f3c569 100644 --- a/core/regions/src/main/java/software/amazon/awssdk/regions/util/HttpResourcesUtils.java +++ b/core/regions/src/main/java/software/amazon/awssdk/regions/util/HttpResourcesUtils.java @@ -25,6 +25,7 @@ import software.amazon.awssdk.annotations.SdkProtectedApi; import software.amazon.awssdk.core.exception.SdkClientException; import software.amazon.awssdk.core.exception.SdkServiceException; +import software.amazon.awssdk.imds.Ec2MetadataClientException; import software.amazon.awssdk.protocols.jsoncore.JsonNode; import software.amazon.awssdk.protocols.jsoncore.JsonNodeParser; import software.amazon.awssdk.regions.internal.util.ConnectionUtils; @@ -118,9 +119,9 @@ public String readResource(ResourcesEndpointProvider endpointProvider, String me return IoUtils.toUtf8String(inputStream); } else if (statusCode == HttpURLConnection.HTTP_NOT_FOUND) { // This is to preserve existing behavior of EC2 Instance metadata service. - throw SdkClientException.builder() - .message("The requested metadata is not found at " + connection.getURL()) - .build(); + throw Ec2MetadataClientException.builder() + .message("The requested metadata is not found at " + connection.getURL()) + .build(); } else { if (!endpointProvider.retryPolicy().shouldRetry(retriesAttempted++, ResourcesEndpointRetryParameters.builder() From 5079073480b2dbd3d76d0fef35b7c8cf540e2054 Mon Sep 17 00:00:00 2001 From: Saranya Somepalli Date: Thu, 12 Jun 2025 10:11:37 -0700 Subject: [PATCH 02/12] Fixing dependency issue --- core/auth/pom.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/auth/pom.xml b/core/auth/pom.xml index fe8725734410..65f469c9b6af 100644 --- a/core/auth/pom.xml +++ b/core/auth/pom.xml @@ -58,6 +58,11 @@ regions ${awsjavasdk.version} + + software.amazon.awssdk + imds + ${awsjavasdk.version} + software.amazon.awssdk profiles From e2b31bb97ccdcffb290f7377c22d91c97cb83c2b Mon Sep 17 00:00:00 2001 From: Saranya Somepalli Date: Fri, 13 Jun 2025 15:07:04 -0700 Subject: [PATCH 03/12] Addition additional tests and replacing initialization --- .../InstanceProfileCredentialsProvider.java | 34 ++--- ...ofileCredentialsProviderAccountIDTest.java | 130 ++++++++++++++++++ 2 files changed, 145 insertions(+), 19 deletions(-) diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java index 661d64b7b260..a3acea6e2050 100644 --- a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java +++ b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java @@ -44,7 +44,6 @@ import software.amazon.awssdk.profiles.ProfileProperty; import software.amazon.awssdk.regions.util.HttpResourcesUtils; import software.amazon.awssdk.regions.util.ResourcesEndpointProvider; -import software.amazon.awssdk.utils.Lazy; import software.amazon.awssdk.utils.Logger; import software.amazon.awssdk.utils.ToString; import software.amazon.awssdk.utils.Validate; @@ -83,8 +82,8 @@ private enum ApiVersion { private static final String EC2_METADATA_TOKEN_TTL_HEADER = "x-aws-ec2-metadata-token-ttl-seconds"; private static final String DEFAULT_TOKEN_TTL = "21600"; - private Lazy apiVersion = new Lazy<>(() -> ApiVersion.UNKNOWN); - private Lazy resolvedProfile = new Lazy<>(() -> null); + private ApiVersion apiVersion = ApiVersion.UNKNOWN; + private String resolvedProfile = null; private final Clock clock; private final String endpoint; @@ -169,9 +168,8 @@ private RefreshResult refreshCredentials() { try { LoadedCredentials credentials = httpCredentialsLoader.loadCredentials(createEndpointProvider()); - ApiVersion currentVersion = apiVersion.getValue(); - if (currentVersion == ApiVersion.UNKNOWN) { - apiVersion = Lazy.withValue(ApiVersion.EXTENDED); + if (apiVersion == ApiVersion.UNKNOWN) { + apiVersion = ApiVersion.EXTENDED; } Instant expiration = credentials.getExpiration().orElse(null); @@ -182,14 +180,13 @@ private RefreshResult refreshCredentials() { .prefetchTime(prefetchTime(expiration)) .build(); } catch (Ec2MetadataClientException e) { - if (apiVersion.getValue() == ApiVersion.UNKNOWN) { - apiVersion = Lazy.withValue(ApiVersion.LEGACY); - resolvedProfile = new Lazy<>(() -> null); + if (apiVersion == ApiVersion.UNKNOWN) { + apiVersion = ApiVersion.LEGACY; + resolvedProfile = null; return refreshCredentials(); } throw SdkClientException.create("Failed to load credentials from IMDS.", e); } catch (RuntimeException e) { - resolvedProfile = new Lazy<>(() -> null); throw SdkClientException.create("Failed to load credentials from IMDS.", e); } } @@ -233,7 +230,7 @@ public String toString() { } private String getSecurityCredentialsResource() { - return apiVersion.getValue() == ApiVersion.LEGACY ? + return apiVersion == ApiVersion.LEGACY ? SECURITY_CREDENTIALS_RESOURCE : SECURITY_CREDENTIALS_EXTENDED_RESOURCE; } @@ -316,8 +313,8 @@ private boolean isInsecureFallbackDisabled() { } private String[] getSecurityCredentials(String imdsHostname, String metadataToken) { - if (resolvedProfile.hasValue()) { - return new String[]{resolvedProfile.getValue()}; + if (resolvedProfile != null) { + return new String[]{resolvedProfile}; } String urlBase = getSecurityCredentialsResource(); @@ -337,16 +334,15 @@ private String[] getSecurityCredentials(String imdsHostname, String metadataToke throw SdkClientException.builder().message("Unable to load credentials path").build(); } - ApiVersion currentVersion = apiVersion.getValue(); - if (currentVersion == ApiVersion.UNKNOWN) { - apiVersion = Lazy.withValue(ApiVersion.EXTENDED); + if (apiVersion == ApiVersion.UNKNOWN) { + apiVersion = ApiVersion.EXTENDED; } - resolvedProfile = new Lazy<>(() -> securityCredentials[0]); + resolvedProfile = securityCredentials[0]; return securityCredentials; } catch (Ec2MetadataClientException e) { - if (apiVersion.getValue() == ApiVersion.UNKNOWN) { - apiVersion = Lazy.withValue(ApiVersion.LEGACY); + if (apiVersion == ApiVersion.UNKNOWN) { + apiVersion = ApiVersion.LEGACY; return getSecurityCredentials(imdsHostname, metadataToken); } throw SdkClientException.create("Failed to load credentials from IMDS.", e); diff --git a/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderAccountIDTest.java b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderAccountIDTest.java index 22a875ecf003..0e378a34ddc8 100644 --- a/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderAccountIDTest.java +++ b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderAccountIDTest.java @@ -25,6 +25,7 @@ import static com.github.tomakehurst.wiremock.client.WireMock.verify; 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 com.github.tomakehurst.wiremock.junit5.WireMockTest; @@ -33,6 +34,7 @@ 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.testutils.EnvironmentVariableHelper; import software.amazon.awssdk.utils.DateUtils; @@ -117,6 +119,134 @@ void resolveCredentials_fallsBackToLegacy_noAccountId() { verifyImdsCallWithToken(false); } + @Test + void resolveCredentials_withImdsDisabled_returnsNoCredentials() { + environmentVariableHelper.set(SdkSystemSetting.AWS_EC2_METADATA_DISABLED.environmentVariable(), "true"); + InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider.builder().build(); + + assertThatThrownBy(() -> provider.resolveCredentials()) + .isInstanceOf(SdkClientException.class) + .hasMessageContaining("IMDS credentials have been disabled"); + + verify(0, putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH))); + verify(0, getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH))); + verify(0, getRequestedFor(urlPathEqualTo(CREDENTIALS_EXTENDED_RESOURCE_PATH))); + } + + + @Test + void resolveCredentials_withInvalidProfile_throwsException() { + String invalidProfile = "my-profile-0004"; + + wireMockServer.stubFor(get(urlPathEqualTo(CREDENTIALS_EXTENDED_RESOURCE_PATH)) + .willReturn(aResponse().withBody(invalidProfile))); + wireMockServer.stubFor(get(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH)) + .willReturn(aResponse().withBody(invalidProfile))); + + wireMockServer.stubFor(get(urlPathEqualTo(CREDENTIALS_EXTENDED_RESOURCE_PATH + invalidProfile)) + .willReturn(aResponse().withStatus(404))); + wireMockServer.stubFor(get(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + invalidProfile)) + .willReturn(aResponse().withStatus(404))); + + wireMockServer.stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)) + .willReturn(aResponse().withBody(TOKEN_STUB))); + + InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider.builder().build(); + + assertThatThrownBy(() -> provider.resolveCredentials()) + .isInstanceOf(SdkClientException.class) + .hasMessageContaining("Failed to load credentials from IMDS."); + } + + @Test + void resolveCredentials_withUnstableProfile_noAccountId_refreshesCredentials() { + String firstCredentials = String.format( + "{\"AccessKeyId\":\"ASIAIOSFODNN7EXAMPLE\"," + + "\"SecretAccessKey\":\"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\"," + + "\"Token\":\"AQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT+FvwqnKw\"," + + "\"Expiration\":\"%s\"," + + "\"Code\":\"Success\"," + + "\"Type\":\"AWS-HMAC\"," + + "\"LastUpdated\":\"2025-03-18T20:53:17.832308Z\"," + + "\"UnexpectedElement7\":{\"Name\":\"ignore-me-7\"}}", + DateUtils.formatIso8601Date(Instant.now().plus(Duration.ofDays(1))) + ); + + String secondCredentials = String.format( + "{\"AccessKeyId\":\"ASIAIOSFODNN7EXAMPLE\"," + + "\"SecretAccessKey\":\"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\"," + + "\"Token\":\"AQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT+FvwqnKw\"," + + "\"Expiration\":\"%s\"," + + "\"Code\":\"Success\"," + + "\"Type\":\"AWS-HMAC\"," + + "\"LastUpdated\":\"2025-03-18T20:53:17.832308Z\"," + + "\"UnexpectedElement7\":{\"Name\":\"ignore-me-7\"}}", + DateUtils.formatIso8601Date(Instant.now().plus(Duration.ofDays(1))) + ); + + wireMockServer.stubFor(get(urlPathEqualTo(CREDENTIALS_EXTENDED_RESOURCE_PATH)) + .inScenario("Profile Change No AccountId") + .whenScenarioStateIs("Started") + .willReturn(aResponse().withBody("my-profile-0007")) + .willSetStateTo("First Profile")); + + wireMockServer.stubFor(get(urlPathEqualTo(CREDENTIALS_EXTENDED_RESOURCE_PATH + "my-profile-0007")) + .inScenario("Profile Change No AccountId") + .whenScenarioStateIs("First Profile") + .willReturn(aResponse().withBody(firstCredentials)) + .willSetStateTo("First Profile Done")); + + wireMockServer.stubFor(get(urlPathEqualTo(CREDENTIALS_EXTENDED_RESOURCE_PATH + "my-profile-0007")) + .inScenario("Profile Change No AccountId") + .whenScenarioStateIs("First Profile Done") + .willReturn(aResponse().withStatus(404)) + .willSetStateTo("Profile Changed")); + + wireMockServer.stubFor(get(urlPathEqualTo(CREDENTIALS_EXTENDED_RESOURCE_PATH)) + .inScenario("Profile Change No AccountId") + .whenScenarioStateIs("Profile Changed") + .willReturn(aResponse().withBody("my-profile-0007-b"))); + + wireMockServer.stubFor(get(urlPathEqualTo(CREDENTIALS_EXTENDED_RESOURCE_PATH + "my-profile-0007-b")) + .willReturn(aResponse().withBody(secondCredentials))); + + wireMockServer.stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)) + .willReturn(aResponse().withBody(TOKEN_STUB))); + + InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider.builder().build(); + + AwsCredentials creds1 = provider.resolveCredentials(); + assertThat(creds1.accountId()).isEmpty(); + assertThat(creds1.accessKeyId()).isEqualTo("ASIAIOSFODNN7EXAMPLE"); + + AwsCredentials creds2 = provider.resolveCredentials(); + assertThat(creds2.accountId()).isEmpty(); + assertThat(creds2.accessKeyId()).isEqualTo("ASIAIOSFODNN7EXAMPLE"); + } + + @Test + void resolveCredentials_withDiscoveredInvalidProfile_noAccountId_throwsException() { + String invalidProfile = "my-profile-0008"; + + wireMockServer.stubFor(get(urlPathEqualTo(CREDENTIALS_EXTENDED_RESOURCE_PATH)) + .willReturn(aResponse().withBody(invalidProfile))); + + wireMockServer.stubFor(get(urlPathEqualTo(CREDENTIALS_EXTENDED_RESOURCE_PATH + invalidProfile)) + .willReturn(aResponse().withStatus(404))); + + wireMockServer.stubFor(get(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + invalidProfile)) + .willReturn(aResponse().withStatus(404))); + + wireMockServer.stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)) + .willReturn(aResponse().withBody(TOKEN_STUB))); + + InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider.builder().build(); + + assertThatThrownBy(() -> provider.resolveCredentials()) + .isInstanceOf(SdkClientException.class) + .hasMessageContaining("Failed to load credentials from IMDS"); + } + @Test void resolveCredentials_cachesProfile_maintainsAccountId() { String credentialsWithAccountId = String.format( From 76be4657706d5679e5ec0afcaa35d57010146ef2 Mon Sep 17 00:00:00 2001 From: Saranya Somepalli Date: Fri, 13 Jun 2025 15:14:33 -0700 Subject: [PATCH 04/12] Updating the test file name --- ...a => InstanceProfileCredentialsProviderExtendedApiTest.java} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename core/auth/src/test/java/software/amazon/awssdk/auth/credentials/{InstanceProfileCredentialsProviderAccountIDTest.java => InstanceProfileCredentialsProviderExtendedApiTest.java} (99%) diff --git a/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderAccountIDTest.java b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderExtendedApiTest.java similarity index 99% rename from core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderAccountIDTest.java rename to core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderExtendedApiTest.java index 0e378a34ddc8..a2bd3b4d6cf8 100644 --- a/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderAccountIDTest.java +++ b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderExtendedApiTest.java @@ -45,7 +45,7 @@ * Tests verifying IMDS credential resolution with account ID support. */ @WireMockTest -public class InstanceProfileCredentialsProviderAccountIDTest { +public class InstanceProfileCredentialsProviderExtendedApiTest { private static final String TOKEN_RESOURCE_PATH = "/latest/api/token"; private static final String CREDENTIALS_RESOURCE_PATH = "/latest/meta-data/iam/security-credentials/"; private static final String CREDENTIALS_EXTENDED_RESOURCE_PATH = "/latest/meta-data/iam/security-credentials-extended/"; From 4a763671c0faabe3999a6d9fd5dff9f68bf46a57 Mon Sep 17 00:00:00 2001 From: Saranya Somepalli Date: Mon, 16 Jun 2025 11:24:45 -0700 Subject: [PATCH 05/12] Additional Changes -created a new integration test file for IMDS extended url separating it from legacy -Included the status code to the fallback logic --- .../next-release/feature-AWSEC2-9b178a4.json | 2 +- ...ileCredentialsProviderIntegrationTest.java | 2 +- .../InstanceProfileCredentialsProvider.java | 6 +- .../credentials/EC2MetadataServiceMock.java | 10 ++ ...ntialsProviderExtendedIntegrationTest.java | 126 ++++++++++++++++++ .../core/auth/sessionResponseExtended.json | 10 ++ .../regions/util/HttpResourcesUtils.java | 1 + 7 files changed, 150 insertions(+), 7 deletions(-) create mode 100644 core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderExtendedIntegrationTest.java create mode 100644 core/auth/src/test/resources/software/amazon/awssdk/core/auth/sessionResponseExtended.json diff --git a/.changes/next-release/feature-AWSEC2-9b178a4.json b/.changes/next-release/feature-AWSEC2-9b178a4.json index f4c5695426a8..80d8abbafc0f 100644 --- a/.changes/next-release/feature-AWSEC2-9b178a4.json +++ b/.changes/next-release/feature-AWSEC2-9b178a4.json @@ -1,6 +1,6 @@ { "type": "feature", - "category": "AWS EC2", + "category": "AWS SDK for Java v2", "contributor": "", "description": "EC2 IMDS Changes to Support Account ID" } diff --git a/core/auth/src/it/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderIntegrationTest.java b/core/auth/src/it/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderIntegrationTest.java index c3a6e0dbf874..ad54813ec782 100644 --- a/core/auth/src/it/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderIntegrationTest.java +++ b/core/auth/src/it/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderIntegrationTest.java @@ -35,7 +35,7 @@ public class InstanceProfileCredentialsProviderIntegrationTest { /** Starts up the mock EC2 Instance Metadata Service. */ @Before public void setUp() throws Exception { - mockServer = new EC2MetadataServiceMock("/latest/meta-data/iam/security-credentials-extended/"); + mockServer = new EC2MetadataServiceMock("/latest/meta-data/iam/security-credentials/"); mockServer.start(); } diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java index a3acea6e2050..b192747a5b4f 100644 --- a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java +++ b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java @@ -168,10 +168,6 @@ private RefreshResult refreshCredentials() { try { LoadedCredentials credentials = httpCredentialsLoader.loadCredentials(createEndpointProvider()); - if (apiVersion == ApiVersion.UNKNOWN) { - apiVersion = ApiVersion.EXTENDED; - } - Instant expiration = credentials.getExpiration().orElse(null); log.debug(() -> "Loaded credentials from IMDS with expiration time of " + expiration); @@ -180,7 +176,7 @@ private RefreshResult refreshCredentials() { .prefetchTime(prefetchTime(expiration)) .build(); } catch (Ec2MetadataClientException e) { - if (apiVersion == ApiVersion.UNKNOWN) { + if (e.statusCode() == 404 && apiVersion == ApiVersion.EXTENDED) { apiVersion = ApiVersion.LEGACY; resolvedProfile = null; return refreshCredentials(); diff --git a/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/EC2MetadataServiceMock.java b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/EC2MetadataServiceMock.java index 495fff4f943d..c380d00af8b6 100644 --- a/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/EC2MetadataServiceMock.java +++ b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/EC2MetadataServiceMock.java @@ -42,6 +42,7 @@ public class EC2MetadataServiceMock { "Content-Type: text/html\r\n" + "Content-Length: "; private static final String OUTPUT_END_OF_HEADERS = "\r\n\r\n"; + private static final String EXTENDED_PATH = "/latest/meta-data/iam/security-credentials-extended/"; private final String securityCredentialsResource; private EC2MockMetadataServiceListenerThread hosmMockServerThread; @@ -140,6 +141,15 @@ public void run() { String[] strings = requestLine.split(" "); String resourcePath = strings[1]; + // Return 404 for extended path when in legacy mode + if (!credentialsResource.equals(EXTENDED_PATH) && + (resourcePath.equals(EXTENDED_PATH) || resourcePath.startsWith(EXTENDED_PATH))) { + String notFound = "HTTP/1.1 404 Not Found\r\n" + + "Content-Length: 0\r\n" + + "\r\n"; + outputStream.write(notFound.getBytes()); + continue; + } String httpResponse = null; diff --git a/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderExtendedIntegrationTest.java b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderExtendedIntegrationTest.java new file mode 100644 index 000000000000..7bea20f3ed03 --- /dev/null +++ b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderExtendedIntegrationTest.java @@ -0,0 +1,126 @@ +/* + * 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.auth.credentials; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import software.amazon.awssdk.core.SdkSystemSetting; +import software.amazon.awssdk.core.exception.SdkClientException; + +/** + * Tests for {@link InstanceProfileCredentialsProvider} using the extended IMDS path. + */ +public class InstanceProfileCredentialsProviderExtendedIntegrationTest { + private static final String EXTENDED_PATH = "/latest/meta-data/iam/security-credentials-extended/"; + private EC2MetadataServiceMock mockServer; + + @Before + public void setUp() throws Exception { + mockServer = new EC2MetadataServiceMock(EXTENDED_PATH); + mockServer.start(); + } + + @After + public void tearDown() { + mockServer.stop(); + } + + @Test + public void resolveCredentials_withExtendedPath_includesAccountId() { + mockServer.setAvailableSecurityCredentials("test-role"); + mockServer.setResponseFileName("sessionResponseExtended"); + + InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider.builder().build(); + AwsCredentials credentials = provider.resolveCredentials(); + + assertThat(credentials.accountId()).isPresent(); + assertThat(credentials.accountId().get()).isEqualTo("123456789012"); + } + + @Test + public void resolveCredentials_withExtendedPath_hasCorrectCredentials() { + mockServer.setAvailableSecurityCredentials("test-role"); + mockServer.setResponseFileName("sessionResponseExtended"); + + InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider.builder().build(); + AwsCredentials credentials = provider.resolveCredentials(); + + assertThat(credentials).isInstanceOf(AwsSessionCredentials.class); + assertThat(credentials.accessKeyId()).isEqualTo("ACCESS_KEY_ID"); + assertThat(credentials.secretAccessKey()).isEqualTo("SECRET_ACCESS_KEY"); + assertThat(((AwsSessionCredentials) credentials).sessionToken()).isEqualTo("TOKEN"); + } + + @Test + public void testSessionCredentials_MultipleInstanceProfiles() { + mockServer.setAvailableSecurityCredentials("test-credentials"); + mockServer.setResponseFileName("sessionResponseExtended"); + + InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider.builder().build(); + AwsCredentials credentials = provider.resolveCredentials(); + + assertThat(credentials).isInstanceOf(AwsSessionCredentials.class); + assertThat(credentials.accessKeyId()).isEqualTo("ACCESS_KEY_ID"); + assertThat(credentials.secretAccessKey()).isEqualTo("SECRET_ACCESS_KEY"); + assertThat(((AwsSessionCredentials) credentials).sessionToken()).isEqualTo("TOKEN"); + } + + @Test + public void testNoInstanceProfiles() throws Exception { + mockServer.setResponseFileName("sessionResponseExtended"); + mockServer.setAvailableSecurityCredentials(""); + + try (InstanceProfileCredentialsProvider credentialsProvider = InstanceProfileCredentialsProvider.create()) { + + try { + credentialsProvider.resolveCredentials(); + fail("Expected an SdkClientException, but wasn't thrown"); + } catch (SdkClientException ace) { + assertNotNull(ace.getMessage()); + } + } + } + + @Test + public void resolveCredentials_withDisabledMetadata_throwsException() { + System.setProperty(SdkSystemSetting.AWS_EC2_METADATA_DISABLED.property(), "true"); + try { + InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider.builder().build(); + assertThatThrownBy(() -> provider.resolveCredentials()) + .isInstanceOf(SdkClientException.class) + .hasMessageContaining("IMDS credentials have been disabled"); + } finally { + System.clearProperty(SdkSystemSetting.AWS_EC2_METADATA_DISABLED.property()); + } + } + + @Test + public void resolveCredentials_withNoAvailableCredentials_throwsException() { + mockServer.setAvailableSecurityCredentials(""); + mockServer.setResponseFileName("sessionResponseExtended"); + + InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider.builder().build(); + assertThatThrownBy(() -> provider.resolveCredentials()) + .isInstanceOf(SdkClientException.class) + .hasMessageContaining("Failed to load credentials from IMDS."); + } +} diff --git a/core/auth/src/test/resources/software/amazon/awssdk/core/auth/sessionResponseExtended.json b/core/auth/src/test/resources/software/amazon/awssdk/core/auth/sessionResponseExtended.json new file mode 100644 index 000000000000..4912f823945c --- /dev/null +++ b/core/auth/src/test/resources/software/amazon/awssdk/core/auth/sessionResponseExtended.json @@ -0,0 +1,10 @@ +{ + "Code" : "Success", + "LastUpdated" : "2025-02-13T18:00:00Z", + "Type" : "AWS-HMAC", + "AccessKeyId" : "ACCESS_KEY_ID", + "SecretAccessKey" : "SECRET_ACCESS_KEY", + "Token" : "TOKEN", + "Expiration" : "2025-02-13T19:00:00Z", + "AccountId" : "123456789012" +} diff --git a/core/regions/src/main/java/software/amazon/awssdk/regions/util/HttpResourcesUtils.java b/core/regions/src/main/java/software/amazon/awssdk/regions/util/HttpResourcesUtils.java index 7bc2e2f3c569..8b46c40cf47d 100644 --- a/core/regions/src/main/java/software/amazon/awssdk/regions/util/HttpResourcesUtils.java +++ b/core/regions/src/main/java/software/amazon/awssdk/regions/util/HttpResourcesUtils.java @@ -120,6 +120,7 @@ public String readResource(ResourcesEndpointProvider endpointProvider, String me } else if (statusCode == HttpURLConnection.HTTP_NOT_FOUND) { // This is to preserve existing behavior of EC2 Instance metadata service. throw Ec2MetadataClientException.builder() + .statusCode(404) .message("The requested metadata is not found at " + connection.getURL()) .build(); } else { From c3d02a59696134a984ca5022f38610f529b79574 Mon Sep 17 00:00:00 2001 From: Saranya Somepalli Date: Mon, 16 Jun 2025 14:33:28 -0700 Subject: [PATCH 06/12] Additional Changes - Removed the duplicate test files - Make ApiVersion Volatile --- .../InstanceProfileCredentialsProvider.java | 2 +- ...ntialsProviderExtendedIntegrationTest.java | 126 ------------------ .../core/auth/sessionResponseExtended.json | 10 -- 3 files changed, 1 insertion(+), 137 deletions(-) delete mode 100644 core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderExtendedIntegrationTest.java delete mode 100644 core/auth/src/test/resources/software/amazon/awssdk/core/auth/sessionResponseExtended.json diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java index b192747a5b4f..f8bb7050de55 100644 --- a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java +++ b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java @@ -82,7 +82,7 @@ private enum ApiVersion { private static final String EC2_METADATA_TOKEN_TTL_HEADER = "x-aws-ec2-metadata-token-ttl-seconds"; private static final String DEFAULT_TOKEN_TTL = "21600"; - private ApiVersion apiVersion = ApiVersion.UNKNOWN; + private volatile ApiVersion apiVersion = ApiVersion.UNKNOWN; private String resolvedProfile = null; private final Clock clock; diff --git a/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderExtendedIntegrationTest.java b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderExtendedIntegrationTest.java deleted file mode 100644 index 7bea20f3ed03..000000000000 --- a/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderExtendedIntegrationTest.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * 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.auth.credentials; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.fail; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import software.amazon.awssdk.core.SdkSystemSetting; -import software.amazon.awssdk.core.exception.SdkClientException; - -/** - * Tests for {@link InstanceProfileCredentialsProvider} using the extended IMDS path. - */ -public class InstanceProfileCredentialsProviderExtendedIntegrationTest { - private static final String EXTENDED_PATH = "/latest/meta-data/iam/security-credentials-extended/"; - private EC2MetadataServiceMock mockServer; - - @Before - public void setUp() throws Exception { - mockServer = new EC2MetadataServiceMock(EXTENDED_PATH); - mockServer.start(); - } - - @After - public void tearDown() { - mockServer.stop(); - } - - @Test - public void resolveCredentials_withExtendedPath_includesAccountId() { - mockServer.setAvailableSecurityCredentials("test-role"); - mockServer.setResponseFileName("sessionResponseExtended"); - - InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider.builder().build(); - AwsCredentials credentials = provider.resolveCredentials(); - - assertThat(credentials.accountId()).isPresent(); - assertThat(credentials.accountId().get()).isEqualTo("123456789012"); - } - - @Test - public void resolveCredentials_withExtendedPath_hasCorrectCredentials() { - mockServer.setAvailableSecurityCredentials("test-role"); - mockServer.setResponseFileName("sessionResponseExtended"); - - InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider.builder().build(); - AwsCredentials credentials = provider.resolveCredentials(); - - assertThat(credentials).isInstanceOf(AwsSessionCredentials.class); - assertThat(credentials.accessKeyId()).isEqualTo("ACCESS_KEY_ID"); - assertThat(credentials.secretAccessKey()).isEqualTo("SECRET_ACCESS_KEY"); - assertThat(((AwsSessionCredentials) credentials).sessionToken()).isEqualTo("TOKEN"); - } - - @Test - public void testSessionCredentials_MultipleInstanceProfiles() { - mockServer.setAvailableSecurityCredentials("test-credentials"); - mockServer.setResponseFileName("sessionResponseExtended"); - - InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider.builder().build(); - AwsCredentials credentials = provider.resolveCredentials(); - - assertThat(credentials).isInstanceOf(AwsSessionCredentials.class); - assertThat(credentials.accessKeyId()).isEqualTo("ACCESS_KEY_ID"); - assertThat(credentials.secretAccessKey()).isEqualTo("SECRET_ACCESS_KEY"); - assertThat(((AwsSessionCredentials) credentials).sessionToken()).isEqualTo("TOKEN"); - } - - @Test - public void testNoInstanceProfiles() throws Exception { - mockServer.setResponseFileName("sessionResponseExtended"); - mockServer.setAvailableSecurityCredentials(""); - - try (InstanceProfileCredentialsProvider credentialsProvider = InstanceProfileCredentialsProvider.create()) { - - try { - credentialsProvider.resolveCredentials(); - fail("Expected an SdkClientException, but wasn't thrown"); - } catch (SdkClientException ace) { - assertNotNull(ace.getMessage()); - } - } - } - - @Test - public void resolveCredentials_withDisabledMetadata_throwsException() { - System.setProperty(SdkSystemSetting.AWS_EC2_METADATA_DISABLED.property(), "true"); - try { - InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider.builder().build(); - assertThatThrownBy(() -> provider.resolveCredentials()) - .isInstanceOf(SdkClientException.class) - .hasMessageContaining("IMDS credentials have been disabled"); - } finally { - System.clearProperty(SdkSystemSetting.AWS_EC2_METADATA_DISABLED.property()); - } - } - - @Test - public void resolveCredentials_withNoAvailableCredentials_throwsException() { - mockServer.setAvailableSecurityCredentials(""); - mockServer.setResponseFileName("sessionResponseExtended"); - - InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider.builder().build(); - assertThatThrownBy(() -> provider.resolveCredentials()) - .isInstanceOf(SdkClientException.class) - .hasMessageContaining("Failed to load credentials from IMDS."); - } -} diff --git a/core/auth/src/test/resources/software/amazon/awssdk/core/auth/sessionResponseExtended.json b/core/auth/src/test/resources/software/amazon/awssdk/core/auth/sessionResponseExtended.json deleted file mode 100644 index 4912f823945c..000000000000 --- a/core/auth/src/test/resources/software/amazon/awssdk/core/auth/sessionResponseExtended.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "Code" : "Success", - "LastUpdated" : "2025-02-13T18:00:00Z", - "Type" : "AWS-HMAC", - "AccessKeyId" : "ACCESS_KEY_ID", - "SecretAccessKey" : "SECRET_ACCESS_KEY", - "Token" : "TOKEN", - "Expiration" : "2025-02-13T19:00:00Z", - "AccountId" : "123456789012" -} From e42ad38f43ca255cd1725fe20bd39b77954da306 Mon Sep 17 00:00:00 2001 From: Saranya Somepalli Date: Tue, 17 Jun 2025 13:09:26 -0700 Subject: [PATCH 07/12] Additional Changes: -Adding additional tests -Updating to use AtomicReference --- .../InstanceProfileCredentialsProvider.java | 28 ++-- .../internal/HttpCredentialsLoader.java | 10 +- ...ileCredentialsProviderExtendedApiTest.java | 135 +++++++----------- 3 files changed, 68 insertions(+), 105 deletions(-) diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java index f8bb7050de55..008dbc7e8240 100644 --- a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java +++ b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java @@ -27,6 +27,7 @@ import java.util.Collections; import java.util.Map; import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.annotations.SdkTestInternalApi; @@ -82,8 +83,8 @@ private enum ApiVersion { private static final String EC2_METADATA_TOKEN_TTL_HEADER = "x-aws-ec2-metadata-token-ttl-seconds"; private static final String DEFAULT_TOKEN_TTL = "21600"; - private volatile ApiVersion apiVersion = ApiVersion.UNKNOWN; - private String resolvedProfile = null; + private final AtomicReference apiVersion = new AtomicReference<>(ApiVersion.UNKNOWN); + private final AtomicReference resolvedProfile = new AtomicReference<>(); private final Clock clock; private final String endpoint; @@ -176,9 +177,9 @@ private RefreshResult refreshCredentials() { .prefetchTime(prefetchTime(expiration)) .build(); } catch (Ec2MetadataClientException e) { - if (e.statusCode() == 404 && apiVersion == ApiVersion.EXTENDED) { - apiVersion = ApiVersion.LEGACY; - resolvedProfile = null; + if (e.statusCode() == 404 && apiVersion.compareAndSet(ApiVersion.EXTENDED, ApiVersion.LEGACY)) { + log.debug(() -> "Unable to load credential path from extended API. Falling back to legacy API."); + resolvedProfile.set(null); return refreshCredentials(); } throw SdkClientException.create("Failed to load credentials from IMDS.", e); @@ -226,7 +227,7 @@ public String toString() { } private String getSecurityCredentialsResource() { - return apiVersion == ApiVersion.LEGACY ? + return apiVersion.get() == ApiVersion.LEGACY ? SECURITY_CREDENTIALS_RESOURCE : SECURITY_CREDENTIALS_EXTENDED_RESOURCE; } @@ -309,8 +310,9 @@ private boolean isInsecureFallbackDisabled() { } private String[] getSecurityCredentials(String imdsHostname, String metadataToken) { - if (resolvedProfile != null) { - return new String[]{resolvedProfile}; + String profile = resolvedProfile.get(); + if (profile != null) { + return new String[]{profile}; } String urlBase = getSecurityCredentialsResource(); @@ -330,15 +332,13 @@ private String[] getSecurityCredentials(String imdsHostname, String metadataToke throw SdkClientException.builder().message("Unable to load credentials path").build(); } - if (apiVersion == ApiVersion.UNKNOWN) { - apiVersion = ApiVersion.EXTENDED; - } - resolvedProfile = securityCredentials[0]; + apiVersion.compareAndSet(ApiVersion.UNKNOWN, ApiVersion.EXTENDED); + resolvedProfile.set(securityCredentials[0]); return securityCredentials; } catch (Ec2MetadataClientException e) { - if (apiVersion == ApiVersion.UNKNOWN) { - apiVersion = ApiVersion.LEGACY; + if (e.statusCode() == 404 && apiVersion.compareAndSet(ApiVersion.UNKNOWN, ApiVersion.LEGACY)) { + log.debug(() -> "Unable to load credential path from extended API. Falling back to legacy API."); return getSecurityCredentials(imdsHostname, metadataToken); } throw SdkClientException.create("Failed to load credentials from IMDS.", e); diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/internal/HttpCredentialsLoader.java b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/internal/HttpCredentialsLoader.java index 364dcf131eb7..05c2fb7ffdc1 100644 --- a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/internal/HttpCredentialsLoader.java +++ b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/internal/HttpCredentialsLoader.java @@ -70,11 +70,11 @@ public LoadedCredentials loadCredentials(ResourcesEndpointProvider endpoint) { Validate.notNull(secretKey, "Failed to load secret key from metadata service."); return new LoadedCredentials(accessKey.text(), - secretKey.text(), - token != null ? token.text() : null, - expiration != null ? expiration.text() : null, - accountId != null ? accountId.text() : null, - providerName); + secretKey.text(), + token != null ? token.text() : null, + expiration != null ? expiration.text() : null, + accountId != null ? accountId.text() : null, + providerName); } catch (SdkClientException e) { throw e; } catch (RuntimeException | IOException e) { diff --git a/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderExtendedApiTest.java b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderExtendedApiTest.java index a2bd3b4d6cf8..8e9da6179cf2 100644 --- a/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderExtendedApiTest.java +++ b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderExtendedApiTest.java @@ -105,7 +105,7 @@ void resolveCredentials_fallsBackToLegacy_noAccountId() { "\"SecretAccessKey\":\"SECRET_ACCESS_KEY\"," + "\"Token\":\"SESSION_TOKEN\"," + "\"Expiration\":\"%s\"," + - "\"Code\":\"Success\"}", // No AccountId field at all + "\"Code\":\"Success\"}", DateUtils.formatIso8601Date(Instant.now().plus(Duration.ofDays(1))) ); @@ -159,119 +159,82 @@ void resolveCredentials_withInvalidProfile_throwsException() { } @Test - void resolveCredentials_withUnstableProfile_noAccountId_refreshesCredentials() { - String firstCredentials = String.format( - "{\"AccessKeyId\":\"ASIAIOSFODNN7EXAMPLE\"," + - "\"SecretAccessKey\":\"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\"," + - "\"Token\":\"AQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT+FvwqnKw\"," + - "\"Expiration\":\"%s\"," + - "\"Code\":\"Success\"," + - "\"Type\":\"AWS-HMAC\"," + - "\"LastUpdated\":\"2025-03-18T20:53:17.832308Z\"," + - "\"UnexpectedElement7\":{\"Name\":\"ignore-me-7\"}}", - DateUtils.formatIso8601Date(Instant.now().plus(Duration.ofDays(1))) - ); - - String secondCredentials = String.format( - "{\"AccessKeyId\":\"ASIAIOSFODNN7EXAMPLE\"," + - "\"SecretAccessKey\":\"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\"," + - "\"Token\":\"AQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT+FvwqnKw\"," + + void resolveCredentials_cachesProfile_maintainsAccountId() { + String credentialsWithAccountId = String.format( + "{\"AccessKeyId\":\"ACCESS_KEY_ID\"," + + "\"SecretAccessKey\":\"SECRET_ACCESS_KEY\"," + + "\"Token\":\"SESSION_TOKEN\"," + "\"Expiration\":\"%s\"," + - "\"Code\":\"Success\"," + - "\"Type\":\"AWS-HMAC\"," + - "\"LastUpdated\":\"2025-03-18T20:53:17.832308Z\"," + - "\"UnexpectedElement7\":{\"Name\":\"ignore-me-7\"}}", - DateUtils.formatIso8601Date(Instant.now().plus(Duration.ofDays(1))) + "\"AccountId\":\"%s\"}", + DateUtils.formatIso8601Date(Instant.now().plus(Duration.ofDays(1))), + ACCOUNT_ID ); - wireMockServer.stubFor(get(urlPathEqualTo(CREDENTIALS_EXTENDED_RESOURCE_PATH)) - .inScenario("Profile Change No AccountId") - .whenScenarioStateIs("Started") - .willReturn(aResponse().withBody("my-profile-0007")) - .willSetStateTo("First Profile")); - - wireMockServer.stubFor(get(urlPathEqualTo(CREDENTIALS_EXTENDED_RESOURCE_PATH + "my-profile-0007")) - .inScenario("Profile Change No AccountId") - .whenScenarioStateIs("First Profile") - .willReturn(aResponse().withBody(firstCredentials)) - .willSetStateTo("First Profile Done")); - - wireMockServer.stubFor(get(urlPathEqualTo(CREDENTIALS_EXTENDED_RESOURCE_PATH + "my-profile-0007")) - .inScenario("Profile Change No AccountId") - .whenScenarioStateIs("First Profile Done") - .willReturn(aResponse().withStatus(404)) - .willSetStateTo("Profile Changed")); - - wireMockServer.stubFor(get(urlPathEqualTo(CREDENTIALS_EXTENDED_RESOURCE_PATH)) - .inScenario("Profile Change No AccountId") - .whenScenarioStateIs("Profile Changed") - .willReturn(aResponse().withBody("my-profile-0007-b"))); - - wireMockServer.stubFor(get(urlPathEqualTo(CREDENTIALS_EXTENDED_RESOURCE_PATH + "my-profile-0007-b")) - .willReturn(aResponse().withBody(secondCredentials))); - - wireMockServer.stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)) - .willReturn(aResponse().withBody(TOKEN_STUB))); - + stubSecureCredentialsResponse(aResponse().withBody(credentialsWithAccountId), true); InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider.builder().build(); + // First call AwsCredentials creds1 = provider.resolveCredentials(); - assertThat(creds1.accountId()).isEmpty(); - assertThat(creds1.accessKeyId()).isEqualTo("ASIAIOSFODNN7EXAMPLE"); + assertThat(creds1.accountId()).hasValue(ACCOUNT_ID); + // Second call - should use cached profile AwsCredentials creds2 = provider.resolveCredentials(); - assertThat(creds2.accountId()).isEmpty(); - assertThat(creds2.accessKeyId()).isEqualTo("ASIAIOSFODNN7EXAMPLE"); + assertThat(creds2.accountId()).hasValue(ACCOUNT_ID); + + // Verify profile discovery only called once + verify(1, getRequestedFor(urlPathEqualTo(CREDENTIALS_EXTENDED_RESOURCE_PATH))); } @Test - void resolveCredentials_withDiscoveredInvalidProfile_noAccountId_throwsException() { - String invalidProfile = "my-profile-0008"; + void resolveCredentials_withNon404Error_doesNotFallbackToLegacy() { + wireMockServer.stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)) + .willReturn(aResponse().withBody(TOKEN_STUB))); wireMockServer.stubFor(get(urlPathEqualTo(CREDENTIALS_EXTENDED_RESOURCE_PATH)) - .willReturn(aResponse().withBody(invalidProfile))); + .willReturn(aResponse().withBody(PROFILE_NAME))); - wireMockServer.stubFor(get(urlPathEqualTo(CREDENTIALS_EXTENDED_RESOURCE_PATH + invalidProfile)) - .willReturn(aResponse().withStatus(404))); + wireMockServer.stubFor(get(urlPathEqualTo(CREDENTIALS_EXTENDED_RESOURCE_PATH + PROFILE_NAME)) + .willReturn(aResponse().withStatus(500).withBody("Internal Server Error"))); - wireMockServer.stubFor(get(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + invalidProfile)) - .willReturn(aResponse().withStatus(404))); - - wireMockServer.stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)) - .willReturn(aResponse().withBody(TOKEN_STUB))); InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider.builder().build(); assertThatThrownBy(() -> provider.resolveCredentials()) .isInstanceOf(SdkClientException.class) .hasMessageContaining("Failed to load credentials from IMDS"); - } + // Verify extended endpoint was called + verify(1, getRequestedFor(urlPathEqualTo(CREDENTIALS_EXTENDED_RESOURCE_PATH))); + verify(1, getRequestedFor(urlPathEqualTo(CREDENTIALS_EXTENDED_RESOURCE_PATH + PROFILE_NAME))); + + // Verify legacy endpoint was NOT called + verify(0, getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH))); + verify(0, getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + PROFILE_NAME))); + } + @Test - void resolveCredentials_cachesProfile_maintainsAccountId() { - String credentialsWithAccountId = String.format( - "{\"AccessKeyId\":\"ACCESS_KEY_ID\"," + - "\"SecretAccessKey\":\"SECRET_ACCESS_KEY\"," + - "\"Token\":\"SESSION_TOKEN\"," + - "\"Expiration\":\"%s\"," + - "\"AccountId\":\"%s\"}", - DateUtils.formatIso8601Date(Instant.now().plus(Duration.ofDays(1))), - ACCOUNT_ID - ); + void resolveCredentials_withNon404ErrorOnProfileDiscovery_doesNotFallbackToLegacy() { + wireMockServer.stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)) + .willReturn(aResponse().withBody(TOKEN_STUB))); - stubSecureCredentialsResponse(aResponse().withBody(credentialsWithAccountId), true); - InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider.builder().build(); + wireMockServer.stubFor(get(urlPathEqualTo(CREDENTIALS_EXTENDED_RESOURCE_PATH)) + .willReturn(aResponse().withStatus(403).withBody("Forbidden"))); - // First call - AwsCredentials creds1 = provider.resolveCredentials(); - assertThat(creds1.accountId()).hasValue(ACCOUNT_ID); + InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider.builder().build(); - // Second call - should use cached profile - AwsCredentials creds2 = provider.resolveCredentials(); - assertThat(creds2.accountId()).hasValue(ACCOUNT_ID); + assertThatThrownBy(() -> provider.resolveCredentials()) + .isInstanceOf(SdkClientException.class) + .hasMessageContaining("Failed to load credentials from IMDS"); - // Verify profile discovery only called once + // Verify extended endpoint was called verify(1, getRequestedFor(urlPathEqualTo(CREDENTIALS_EXTENDED_RESOURCE_PATH))); + + // Verify profile-specific endpoint was NOT called + verify(0, getRequestedFor(urlPathEqualTo(CREDENTIALS_EXTENDED_RESOURCE_PATH + PROFILE_NAME))); + + // Verify legacy endpoint was NOT called + verify(0, getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH))); + verify(0, getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + PROFILE_NAME))); } private void stubSecureCredentialsResponse(com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder responseDefinitionBuilder, boolean useExtended) { From c5e41b5c180ca79036b6819f9765a928d870ebaf Mon Sep 17 00:00:00 2001 From: Saranya Somepalli Date: Tue, 17 Jun 2025 16:03:54 -0700 Subject: [PATCH 08/12] Address PR feedback: Updating the debug logging message Modified the fallback logic in refresh credentials --- .changes/next-release/feature-AWSEC2-9b178a4.json | 2 +- .../credentials/InstanceProfileCredentialsProvider.java | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.changes/next-release/feature-AWSEC2-9b178a4.json b/.changes/next-release/feature-AWSEC2-9b178a4.json index 80d8abbafc0f..b93df556becd 100644 --- a/.changes/next-release/feature-AWSEC2-9b178a4.json +++ b/.changes/next-release/feature-AWSEC2-9b178a4.json @@ -2,5 +2,5 @@ "type": "feature", "category": "AWS SDK for Java v2", "contributor": "", - "description": "EC2 IMDS Changes to Support Account ID" + "description": "Include the account ID associated with the credentials retrieved from IMDS when available." } diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java index 008dbc7e8240..640b7298dc9a 100644 --- a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java +++ b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java @@ -177,8 +177,8 @@ private RefreshResult refreshCredentials() { .prefetchTime(prefetchTime(expiration)) .build(); } catch (Ec2MetadataClientException e) { - if (e.statusCode() == 404 && apiVersion.compareAndSet(ApiVersion.EXTENDED, ApiVersion.LEGACY)) { - log.debug(() -> "Unable to load credential path from extended API. Falling back to legacy API."); + if (e.statusCode() == 404) { + log.debug(() -> "Resolved profile is no longer available. Resetting it and trying again."); resolvedProfile.set(null); return refreshCredentials(); } @@ -338,7 +338,7 @@ private String[] getSecurityCredentials(String imdsHostname, String metadataToke } catch (Ec2MetadataClientException e) { if (e.statusCode() == 404 && apiVersion.compareAndSet(ApiVersion.UNKNOWN, ApiVersion.LEGACY)) { - log.debug(() -> "Unable to load credential path from extended API. Falling back to legacy API."); + log.debug(() -> "Instance does not support IMDS extended API. Falling back to legacy API."); return getSecurityCredentials(imdsHostname, metadataToken); } throw SdkClientException.create("Failed to load credentials from IMDS.", e); From f41795bce7293e7fcada19eb2364d889ae87023f Mon Sep 17 00:00:00 2001 From: Saranya Somepalli Date: Wed, 18 Jun 2025 09:34:23 -0700 Subject: [PATCH 09/12] Replacing AtomicReference fields and updating test file --- .../InstanceProfileCredentialsProvider.java | 25 +++++++++++-------- ...ileCredentialsProviderExtendedApiTest.java | 25 ------------------- 2 files changed, 14 insertions(+), 36 deletions(-) diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java index 640b7298dc9a..c6f2a5360916 100644 --- a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java +++ b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java @@ -27,7 +27,6 @@ import java.util.Collections; import java.util.Map; import java.util.Optional; -import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.annotations.SdkTestInternalApi; @@ -83,8 +82,10 @@ private enum ApiVersion { private static final String EC2_METADATA_TOKEN_TTL_HEADER = "x-aws-ec2-metadata-token-ttl-seconds"; private static final String DEFAULT_TOKEN_TTL = "21600"; - private final AtomicReference apiVersion = new AtomicReference<>(ApiVersion.UNKNOWN); - private final AtomicReference resolvedProfile = new AtomicReference<>(); + + // These fields are accessed from methods called by CachedSupplier which provides thread safety through its ReentrantLock + private ApiVersion apiVersion = ApiVersion.UNKNOWN; + private String resolvedProfile = null; private final Clock clock; private final String endpoint; @@ -179,7 +180,7 @@ private RefreshResult refreshCredentials() { } catch (Ec2MetadataClientException e) { if (e.statusCode() == 404) { log.debug(() -> "Resolved profile is no longer available. Resetting it and trying again."); - resolvedProfile.set(null); + resolvedProfile = null; return refreshCredentials(); } throw SdkClientException.create("Failed to load credentials from IMDS.", e); @@ -227,7 +228,7 @@ public String toString() { } private String getSecurityCredentialsResource() { - return apiVersion.get() == ApiVersion.LEGACY ? + return apiVersion == ApiVersion.LEGACY ? SECURITY_CREDENTIALS_RESOURCE : SECURITY_CREDENTIALS_EXTENDED_RESOURCE; } @@ -310,9 +311,8 @@ private boolean isInsecureFallbackDisabled() { } private String[] getSecurityCredentials(String imdsHostname, String metadataToken) { - String profile = resolvedProfile.get(); - if (profile != null) { - return new String[]{profile}; + if (resolvedProfile != null) { + return new String[]{resolvedProfile}; } String urlBase = getSecurityCredentialsResource(); @@ -332,12 +332,15 @@ private String[] getSecurityCredentials(String imdsHostname, String metadataToke throw SdkClientException.builder().message("Unable to load credentials path").build(); } - apiVersion.compareAndSet(ApiVersion.UNKNOWN, ApiVersion.EXTENDED); - resolvedProfile.set(securityCredentials[0]); + if (apiVersion == ApiVersion.UNKNOWN) { + apiVersion = ApiVersion.EXTENDED; + } + resolvedProfile = securityCredentials[0]; return securityCredentials; } catch (Ec2MetadataClientException e) { - if (e.statusCode() == 404 && apiVersion.compareAndSet(ApiVersion.UNKNOWN, ApiVersion.LEGACY)) { + if (e.statusCode() == 404 && apiVersion == ApiVersion.UNKNOWN) { + apiVersion = ApiVersion.LEGACY; log.debug(() -> "Instance does not support IMDS extended API. Falling back to legacy API."); return getSecurityCredentials(imdsHostname, metadataToken); } diff --git a/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderExtendedApiTest.java b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderExtendedApiTest.java index 8e9da6179cf2..1a3704bc6f42 100644 --- a/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderExtendedApiTest.java +++ b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderExtendedApiTest.java @@ -133,31 +133,6 @@ void resolveCredentials_withImdsDisabled_returnsNoCredentials() { verify(0, getRequestedFor(urlPathEqualTo(CREDENTIALS_EXTENDED_RESOURCE_PATH))); } - - @Test - void resolveCredentials_withInvalidProfile_throwsException() { - String invalidProfile = "my-profile-0004"; - - wireMockServer.stubFor(get(urlPathEqualTo(CREDENTIALS_EXTENDED_RESOURCE_PATH)) - .willReturn(aResponse().withBody(invalidProfile))); - wireMockServer.stubFor(get(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH)) - .willReturn(aResponse().withBody(invalidProfile))); - - wireMockServer.stubFor(get(urlPathEqualTo(CREDENTIALS_EXTENDED_RESOURCE_PATH + invalidProfile)) - .willReturn(aResponse().withStatus(404))); - wireMockServer.stubFor(get(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + invalidProfile)) - .willReturn(aResponse().withStatus(404))); - - wireMockServer.stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)) - .willReturn(aResponse().withBody(TOKEN_STUB))); - - InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider.builder().build(); - - assertThatThrownBy(() -> provider.resolveCredentials()) - .isInstanceOf(SdkClientException.class) - .hasMessageContaining("Failed to load credentials from IMDS."); - } - @Test void resolveCredentials_cachesProfile_maintainsAccountId() { String credentialsWithAccountId = String.format( From 0968af7218283d1e813352e9c495a8286b832424 Mon Sep 17 00:00:00 2001 From: Saranya Somepalli Date: Wed, 18 Jun 2025 11:58:56 -0700 Subject: [PATCH 10/12] Adding retry counter to the fallback logic --- .../InstanceProfileCredentialsProvider.java | 8 ++ ...ileCredentialsProviderExtendedApiTest.java | 77 +++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java index c6f2a5360916..b01c7662b148 100644 --- a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java +++ b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java @@ -82,10 +82,12 @@ private enum ApiVersion { private static final String EC2_METADATA_TOKEN_TTL_HEADER = "x-aws-ec2-metadata-token-ttl-seconds"; private static final String DEFAULT_TOKEN_TTL = "21600"; + private static final int MAX_PROFILE_RETRIES = 3; // These fields are accessed from methods called by CachedSupplier which provides thread safety through its ReentrantLock private ApiVersion apiVersion = ApiVersion.UNKNOWN; private String resolvedProfile = null; + private int profileRetryCount = 0; private final Clock clock; private final String endpoint; @@ -173,6 +175,8 @@ private RefreshResult refreshCredentials() { Instant expiration = credentials.getExpiration().orElse(null); log.debug(() -> "Loaded credentials from IMDS with expiration time of " + expiration); + profileRetryCount = 0; + return RefreshResult.builder(credentials.getAwsCredentials()) .staleTime(staleTime(expiration)) .prefetchTime(prefetchTime(expiration)) @@ -181,6 +185,10 @@ private RefreshResult refreshCredentials() { if (e.statusCode() == 404) { log.debug(() -> "Resolved profile is no longer available. Resetting it and trying again."); resolvedProfile = null; + profileRetryCount++; + if (profileRetryCount > MAX_PROFILE_RETRIES) { + throw SdkClientException.create("Failed to load credentials from IMDS.", e); + } return refreshCredentials(); } throw SdkClientException.create("Failed to load credentials from IMDS.", e); diff --git a/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderExtendedApiTest.java b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderExtendedApiTest.java index 1a3704bc6f42..a13c97ef60e2 100644 --- a/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderExtendedApiTest.java +++ b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderExtendedApiTest.java @@ -26,6 +26,7 @@ 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 static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import com.github.tomakehurst.wiremock.junit5.WireMockExtension; import com.github.tomakehurst.wiremock.junit5.WireMockTest; @@ -212,6 +213,82 @@ void resolveCredentials_withNon404ErrorOnProfileDiscovery_doesNotFallbackToLegac verify(0, getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + PROFILE_NAME))); } + @Test + void resolveCredentials_withUnstableProfile_ReturnsCredentials() { + String initialProfile = "my-profile-0007"; + String newProfile = "my-profile-0007-b"; + String credentialsJson = String.format( + "{\"Code\":\"Success\"," + + "\"LastUpdated\":\"2025-03-18T20:53:17.832308Z\"," + + "\"Type\":\"AWS-HMAC\"," + + "\"AccessKeyId\":\"ASIAIOSFODNN7EXAMPLE\"," + + "\"SecretAccessKey\":\"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\"," + + "\"Token\":\"AQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT+FvwqnKw...\"," + + "\"Expiration\":\"2025-03-18T21:53:17.832308Z\"," + + "\"UnexpectedElement7\":{\"Name\":\"ignore-me-7\"}}"); + + wireMockServer.stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)) + .willReturn(aResponse().withBody(TOKEN_STUB))); + + wireMockServer.stubFor(get(urlPathEqualTo(CREDENTIALS_EXTENDED_RESOURCE_PATH)) + .inScenario("unstable-profile") + .whenScenarioStateIs("Started") + .willReturn(aResponse().withBody(initialProfile)) + .willSetStateTo("initial-profile-discovered")); + + + wireMockServer.stubFor(get(urlPathEqualTo(CREDENTIALS_EXTENDED_RESOURCE_PATH + initialProfile)) + .inScenario("unstable-profile") + .whenScenarioStateIs("initial-profile-discovered") + .willReturn(aResponse().withStatus(404)) + .willSetStateTo("profile-not-found")); + + wireMockServer.stubFor(get(urlPathEqualTo(CREDENTIALS_EXTENDED_RESOURCE_PATH)) + .inScenario("unstable-profile") + .whenScenarioStateIs("profile-not-found") + .willReturn(aResponse().withBody(newProfile)) + .willSetStateTo("new-profile-discovered")); + + wireMockServer.stubFor(get(urlPathEqualTo(CREDENTIALS_EXTENDED_RESOURCE_PATH + newProfile)) + .inScenario("unstable-profile") + .whenScenarioStateIs("new-profile-discovered") + .willReturn(aResponse().withBody(credentialsJson))); + + InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider.builder().build(); + + AwsCredentials creds1 = provider.resolveCredentials(); + assertThat(creds1.accessKeyId()).isEqualTo("ASIAIOSFODNN7EXAMPLE"); + + AwsCredentials creds2 = assertDoesNotThrow(() -> provider.resolveCredentials()); + assertThat(creds2.accessKeyId()).isEqualTo("ASIAIOSFODNN7EXAMPLE"); + + verify(1, getRequestedFor(urlPathEqualTo(CREDENTIALS_EXTENDED_RESOURCE_PATH + initialProfile))); + verify(1, getRequestedFor(urlPathEqualTo(CREDENTIALS_EXTENDED_RESOURCE_PATH + newProfile))); + } + + @Test + void resolveCredentials_withTooManyProfileFailures_throwsException() { + String profile = "unstable-profile"; + + wireMockServer.stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)) + .willReturn(aResponse().withBody(TOKEN_STUB))); + + wireMockServer.stubFor(get(urlPathEqualTo(CREDENTIALS_EXTENDED_RESOURCE_PATH)) + .willReturn(aResponse().withBody(profile))); + + wireMockServer.stubFor(get(urlPathEqualTo(CREDENTIALS_EXTENDED_RESOURCE_PATH + profile)) + .willReturn(aResponse().withStatus(404))); + + InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider.builder().build(); + + assertThatThrownBy(() -> provider.resolveCredentials()) + .isInstanceOf(SdkClientException.class) + .hasMessageContaining("Failed to load credentials from IMDS."); + + verify(4, getRequestedFor(urlPathEqualTo(CREDENTIALS_EXTENDED_RESOURCE_PATH))); + verify(4, getRequestedFor(urlPathEqualTo(CREDENTIALS_EXTENDED_RESOURCE_PATH + profile))); + } + private void stubSecureCredentialsResponse(com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder responseDefinitionBuilder, boolean useExtended) { wireMockServer.stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody(TOKEN_STUB))); String path = useExtended ? CREDENTIALS_EXTENDED_RESOURCE_PATH : CREDENTIALS_RESOURCE_PATH; From 8708d802df30d2e414bdd7468584e730d701e838 Mon Sep 17 00:00:00 2001 From: Saranya Somepalli Date: Wed, 18 Jun 2025 15:21:05 -0700 Subject: [PATCH 11/12] Updating the fallback logic --- .../InstanceProfileCredentialsProvider.java | 22 +++++++++++++------ ...ileCredentialsProviderExtendedApiTest.java | 4 ++-- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java index b01c7662b148..76d9de8f8994 100644 --- a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java +++ b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java @@ -73,6 +73,7 @@ public final class InstanceProfileCredentialsProvider private static final String SECURITY_CREDENTIALS_RESOURCE = "/latest/meta-data/iam/security-credentials/"; private static final String SECURITY_CREDENTIALS_EXTENDED_RESOURCE = "/latest/meta-data/iam/security-credentials-extended/"; private static final String TOKEN_RESOURCE = "/latest/api/token"; + private static final String FAILED_TO_LOAD_CREDENTIALS_ERROR = "Failed to load credentials from IMDS."; private enum ApiVersion { UNKNOWN, @@ -82,7 +83,7 @@ private enum ApiVersion { private static final String EC2_METADATA_TOKEN_TTL_HEADER = "x-aws-ec2-metadata-token-ttl-seconds"; private static final String DEFAULT_TOKEN_TTL = "21600"; - private static final int MAX_PROFILE_RETRIES = 3; + private static final int MAX_PROFILE_RETRIES = 1; // These fields are accessed from methods called by CachedSupplier which provides thread safety through its ReentrantLock private ApiVersion apiVersion = ApiVersion.UNKNOWN; @@ -185,15 +186,22 @@ private RefreshResult refreshCredentials() { if (e.statusCode() == 404) { log.debug(() -> "Resolved profile is no longer available. Resetting it and trying again."); resolvedProfile = null; + + if (apiVersion == ApiVersion.UNKNOWN) { + apiVersion = ApiVersion.LEGACY; + return refreshCredentials(); + } + profileRetryCount++; - if (profileRetryCount > MAX_PROFILE_RETRIES) { - throw SdkClientException.create("Failed to load credentials from IMDS.", e); + if (profileRetryCount <= MAX_PROFILE_RETRIES) { + log.debug(() -> "Retrying fetching the profile name again"); + return refreshCredentials(); } - return refreshCredentials(); + throw SdkClientException.create(FAILED_TO_LOAD_CREDENTIALS_ERROR, e); } - throw SdkClientException.create("Failed to load credentials from IMDS.", e); + throw SdkClientException.create(FAILED_TO_LOAD_CREDENTIALS_ERROR, e); } catch (RuntimeException e) { - throw SdkClientException.create("Failed to load credentials from IMDS.", e); + throw SdkClientException.create(FAILED_TO_LOAD_CREDENTIALS_ERROR, e); } } @@ -352,7 +360,7 @@ private String[] getSecurityCredentials(String imdsHostname, String metadataToke log.debug(() -> "Instance does not support IMDS extended API. Falling back to legacy API."); return getSecurityCredentials(imdsHostname, metadataToken); } - throw SdkClientException.create("Failed to load credentials from IMDS.", e); + throw SdkClientException.create(FAILED_TO_LOAD_CREDENTIALS_ERROR, e); } } diff --git a/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderExtendedApiTest.java b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderExtendedApiTest.java index a13c97ef60e2..a8b3aa5bf603 100644 --- a/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderExtendedApiTest.java +++ b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderExtendedApiTest.java @@ -285,8 +285,8 @@ void resolveCredentials_withTooManyProfileFailures_throwsException() { .isInstanceOf(SdkClientException.class) .hasMessageContaining("Failed to load credentials from IMDS."); - verify(4, getRequestedFor(urlPathEqualTo(CREDENTIALS_EXTENDED_RESOURCE_PATH))); - verify(4, getRequestedFor(urlPathEqualTo(CREDENTIALS_EXTENDED_RESOURCE_PATH + profile))); + verify(2, getRequestedFor(urlPathEqualTo(CREDENTIALS_EXTENDED_RESOURCE_PATH))); + verify(2, getRequestedFor(urlPathEqualTo(CREDENTIALS_EXTENDED_RESOURCE_PATH + profile))); } private void stubSecureCredentialsResponse(com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder responseDefinitionBuilder, boolean useExtended) { From b912ba91c64607a602fca94abe40c40d31fb090b Mon Sep 17 00:00:00 2001 From: Saranya Somepalli Date: Wed, 18 Jun 2025 16:13:36 -0700 Subject: [PATCH 12/12] Adding comment --- .../auth/credentials/InstanceProfileCredentialsProvider.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java index 76d9de8f8994..9751477b9f14 100644 --- a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java +++ b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java @@ -176,6 +176,7 @@ private RefreshResult refreshCredentials() { Instant expiration = credentials.getExpiration().orElse(null); log.debug(() -> "Loaded credentials from IMDS with expiration time of " + expiration); + // Reset profile retry count after successful credential fetch profileRetryCount = 0; return RefreshResult.builder(credentials.getAwsCredentials()) @@ -194,7 +195,7 @@ private RefreshResult refreshCredentials() { profileRetryCount++; if (profileRetryCount <= MAX_PROFILE_RETRIES) { - log.debug(() -> "Retrying fetching the profile name again"); + log.debug(() -> "Profile name not found, retrying fetching the profile name again."); return refreshCredentials(); } throw SdkClientException.create(FAILED_TO_LOAD_CREDENTIALS_ERROR, e);