Skip to content

Commit 3c74396

Browse files
authored
Add stale time config to InstanceProfileCredentialsProvider (#5758)
* Add stale time config to InstanceProfileCredentialsProvider to allow for refreshing credentials earlier due to stale value. This will help prevent returning invalid credentials when an error is encountered during asynchronous refresh. * fix spotbug * add configurable retry policy * javadoc * remove InstanceProfileCredentialsRetryPolicy * default retryPolicy set in StaticResourcesEndpointProvider ctor
1 parent 943db51 commit 3c74396

File tree

5 files changed

+122
-4
lines changed

5 files changed

+122
-4
lines changed

core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ public final class InstanceProfileCredentialsProvider
8888

8989
private final String profileName;
9090

91+
private final Duration staleTime;
92+
9193
/**
9294
* @see #builder()
9395
*/
@@ -108,6 +110,8 @@ private InstanceProfileCredentialsProvider(BuilderImpl builder) {
108110
.profileName(profileName)
109111
.build();
110112

113+
this.staleTime = Validate.getOrDefault(builder.staleTime, () -> Duration.ofSeconds(1));
114+
111115
if (Boolean.TRUE.equals(builder.asyncCredentialUpdateEnabled)) {
112116
Validate.paramNotBlank(builder.asyncThreadName, "asyncThreadName");
113117
this.credentialsCache = CachedSupplier.builder(this::refreshCredentials)
@@ -174,7 +178,7 @@ private Instant staleTime(Instant expiration) {
174178
return null;
175179
}
176180

177-
return expiration.minusSeconds(1);
181+
return expiration.minus(staleTime);
178182
}
179183

180184
private Instant prefetchTime(Instant expiration) {
@@ -340,6 +344,18 @@ public interface Builder extends HttpCredentialsProvider.Builder<InstanceProfile
340344
*/
341345
Builder profileName(String profileName);
342346

347+
/**
348+
* Configure the amount of time before the moment of expiration of credentials for which to consider the credentials to
349+
* be stale. A higher value can lead to a higher rate of request being made to the Amazon EC2 Instance Metadata Service.
350+
* The default is 1 sec.
351+
* <p>Increasing this value to a higher value (10s or more) may help with situations where a higher load on the instance
352+
* metadata service causes it to return 503s error, for which the SDK may not be able to recover fast enough and
353+
* returns expired credentials.
354+
*
355+
* @param duration the amount of time before expiration for when to consider the credentials to be stale and need refresh
356+
*/
357+
Builder staleTime(Duration duration);
358+
343359
/**
344360
* Build a {@link InstanceProfileCredentialsProvider} from the provided configuration.
345361
*/
@@ -355,6 +371,7 @@ static final class BuilderImpl implements Builder {
355371
private String asyncThreadName;
356372
private Supplier<ProfileFile> profileFile;
357373
private String profileName;
374+
private Duration staleTime;
358375

359376
private BuilderImpl() {
360377
asyncThreadName("instance-profile-credentials-provider");
@@ -367,6 +384,7 @@ private BuilderImpl(InstanceProfileCredentialsProvider provider) {
367384
this.asyncThreadName = provider.asyncThreadName;
368385
this.profileFile = provider.profileFile;
369386
this.profileName = provider.profileName;
387+
this.staleTime = provider.staleTime;
370388
}
371389

372390
Builder clock(Clock clock) {
@@ -435,6 +453,16 @@ public void setProfileName(String profileName) {
435453
profileName(profileName);
436454
}
437455

456+
@Override
457+
public Builder staleTime(Duration duration) {
458+
this.staleTime = duration;
459+
return this;
460+
}
461+
462+
public void setStaleTime(Duration duration) {
463+
staleTime(duration);
464+
}
465+
438466
@Override
439467
public InstanceProfileCredentialsProvider build() {
440468
return new InstanceProfileCredentialsProvider(this);

core/auth/src/main/java/software/amazon/awssdk/auth/credentials/internal/ContainerCredentialsRetryPolicy.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,15 @@
1717

1818
import java.io.IOException;
1919
import software.amazon.awssdk.annotations.SdkInternalApi;
20+
import software.amazon.awssdk.auth.credentials.ContainerCredentialsProvider;
21+
import software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider;
2022
import software.amazon.awssdk.http.HttpStatusFamily;
2123
import software.amazon.awssdk.regions.util.ResourcesEndpointRetryParameters;
2224
import software.amazon.awssdk.regions.util.ResourcesEndpointRetryPolicy;
2325

26+
/**
27+
* Retry policy shared by {@link InstanceProfileCredentialsProvider} and {@link ContainerCredentialsProvider#}.
28+
*/
2429
@SdkInternalApi
2530
public final class ContainerCredentialsRetryPolicy implements ResourcesEndpointRetryPolicy {
2631

core/auth/src/main/java/software/amazon/awssdk/auth/credentials/internal/StaticResourcesEndpointProvider.java

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,23 +24,27 @@
2424
import java.util.Optional;
2525
import software.amazon.awssdk.annotations.SdkInternalApi;
2626
import software.amazon.awssdk.regions.util.ResourcesEndpointProvider;
27+
import software.amazon.awssdk.regions.util.ResourcesEndpointRetryPolicy;
2728
import software.amazon.awssdk.utils.Validate;
2829

2930
@SdkInternalApi
3031
public final class StaticResourcesEndpointProvider implements ResourcesEndpointProvider {
3132
private final URI endpoint;
3233
private final Map<String, String> headers;
3334
private final Duration connectionTimeout;
35+
private final ResourcesEndpointRetryPolicy retryPolicy;
3436

3537
private StaticResourcesEndpointProvider(URI endpoint,
36-
Map<String, String> additionalHeaders,
37-
Duration customTimeout) {
38+
Map<String, String> additionalHeaders,
39+
Duration customTimeout,
40+
ResourcesEndpointRetryPolicy retryPolicy) {
3841
this.endpoint = Validate.paramNotNull(endpoint, "endpoint");
3942
this.headers = ResourcesEndpointProvider.super.headers();
4043
if (additionalHeaders != null) {
4144
this.headers.putAll(additionalHeaders);
4245
}
4346
this.connectionTimeout = customTimeout;
47+
this.retryPolicy = Validate.getOrDefault(retryPolicy, () -> ResourcesEndpointRetryPolicy.NO_RETRY);
4448
}
4549

4650
@Override
@@ -58,10 +62,16 @@ public Map<String, String> headers() {
5862
return Collections.unmodifiableMap(headers);
5963
}
6064

65+
@Override
66+
public ResourcesEndpointRetryPolicy retryPolicy() {
67+
return this.retryPolicy;
68+
}
69+
6170
public static class Builder {
6271
private URI endpoint;
6372
private Map<String, String> additionalHeaders = new HashMap<>();
6473
private Duration customTimeout;
74+
private ResourcesEndpointRetryPolicy retryPolicy;
6575

6676
public Builder endpoint(URI endpoint) {
6777
this.endpoint = Validate.paramNotNull(endpoint, "endpoint");
@@ -80,8 +90,13 @@ public Builder connectionTimeout(Duration timeout) {
8090
return this;
8191
}
8292

93+
public Builder retryPolicy(ResourcesEndpointRetryPolicy retryPolicy) {
94+
this.retryPolicy = retryPolicy;
95+
return this;
96+
}
97+
8398
public StaticResourcesEndpointProvider build() {
84-
return new StaticResourcesEndpointProvider(endpoint, additionalHeaders, customTimeout);
99+
return new StaticResourcesEndpointProvider(endpoint, additionalHeaders, customTimeout, retryPolicy);
85100
}
86101
}
87102

core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderTest.java

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@
1717

1818
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
1919
import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
20+
import static com.github.tomakehurst.wiremock.client.WireMock.exactly;
2021
import static com.github.tomakehurst.wiremock.client.WireMock.get;
2122
import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
2223
import static com.github.tomakehurst.wiremock.client.WireMock.put;
2324
import static com.github.tomakehurst.wiremock.client.WireMock.putRequestedFor;
2425
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
2526
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
2627
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
28+
import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED;
2729
import static java.time.temporal.ChronoUnit.HOURS;
2830
import static java.time.temporal.ChronoUnit.MINUTES;
2931
import static java.time.temporal.ChronoUnit.SECONDS;
@@ -55,6 +57,7 @@
5557
import org.junit.jupiter.api.extension.RegisterExtension;
5658
import org.junit.jupiter.params.ParameterizedTest;
5759
import org.junit.jupiter.params.provider.ValueSource;
60+
import org.mockito.Mockito;
5861
import software.amazon.awssdk.core.SdkSystemSetting;
5962
import software.amazon.awssdk.core.exception.SdkClientException;
6063
import software.amazon.awssdk.core.util.SdkUserAgent;
@@ -596,13 +599,74 @@ void imdsCallFrequencyIsLimited() {
596599
}
597600
}
598601

602+
@Test
603+
void testErrorWhileCacheIsStale_shouldRecover() {
604+
AdjustableClock clock = new AdjustableClock();
605+
606+
Instant now = Instant.now();
607+
Instant expiration = now.plus(Duration.ofHours(6));
608+
609+
String successfulCredentialsResponse =
610+
"{"
611+
+ "\"AccessKeyId\":\"ACCESS_KEY_ID\","
612+
+ "\"SecretAccessKey\":\"SECRET_ACCESS_KEY\","
613+
+ "\"Expiration\":\"" + DateUtils.formatIso8601Date(expiration) + '"'
614+
+ "}";
615+
616+
String staleResponse =
617+
"{"
618+
+ "\"AccessKeyId\":\"ACCESS_KEY_ID_2\","
619+
+ "\"SecretAccessKey\":\"SECRET_ACCESS_KEY_2\","
620+
+ "\"Expiration\":\"" + DateUtils.formatIso8601Date(Instant.now()) + '"'
621+
+ "}";
622+
623+
624+
Duration staleTime = Duration.ofMinutes(5);
625+
AwsCredentialsProvider provider = credentialsProviderWithClock(clock, staleTime);
626+
627+
// cache expiration with expiration = 6 hours
628+
clock.time = now;
629+
stubSecureCredentialsResponse(aResponse().withBody(successfulCredentialsResponse));
630+
AwsCredentials validCreds = provider.resolveCredentials();
631+
632+
// failure while cache is stale
633+
clock.time = expiration.minus(staleTime.minus(Duration.ofMinutes(2)));
634+
stubTokenFetchErrorResponse(aResponse().withFixedDelay(2000).withBody(STUB_CREDENTIALS), 500);
635+
stubSecureCredentialsResponse(aResponse().withBody(staleResponse));
636+
AwsCredentials refreshedWhileStale = provider.resolveCredentials();
637+
638+
assertThat(refreshedWhileStale).isNotEqualTo(validCreds);
639+
assertThat(refreshedWhileStale.secretAccessKey()).isEqualTo("SECRET_ACCESS_KEY_2");
640+
}
641+
642+
@Test
643+
void shouldNotRetry_whenSucceeds() {
644+
stubSecureCredentialsResponse(aResponse().withBody(STUB_CREDENTIALS));
645+
InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider.builder().build();
646+
AwsCredentials credentials = provider.resolveCredentials();
647+
assertThat(credentials.accessKeyId()).isEqualTo("ACCESS_KEY_ID");
648+
assertThat(credentials.secretAccessKey()).isEqualTo("SECRET_ACCESS_KEY");
649+
assertThat(credentials.providerName()).isPresent().contains("InstanceProfileCredentialsProvider");
650+
verifyImdsCallWithToken();
651+
WireMock.verify(exactly(1), getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + "some-profile")));
652+
}
653+
599654
private AwsCredentialsProvider credentialsProviderWithClock(Clock clock) {
600655
InstanceProfileCredentialsProvider.BuilderImpl builder =
601656
(InstanceProfileCredentialsProvider.BuilderImpl) InstanceProfileCredentialsProvider.builder();
602657
builder.clock(clock);
603658
return builder.build();
604659
}
605660

661+
private AwsCredentialsProvider credentialsProviderWithClock(Clock clock, Duration staleTime) {
662+
InstanceProfileCredentialsProvider.BuilderImpl builder =
663+
(InstanceProfileCredentialsProvider.BuilderImpl) InstanceProfileCredentialsProvider.builder();
664+
builder.clock(clock);
665+
builder.staleTime(staleTime);
666+
return builder.build();
667+
}
668+
669+
606670
private static class AdjustableClock extends Clock {
607671
private Instant time;
608672

core/auth/src/test/resources/log4j2.properties

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,9 @@ rootLogger.appenderRef.stdout.ref = ConsoleAppender
3636
#
3737
#logger.netty.name = io.netty.handler.logging
3838
#logger.netty.level = debug
39+
40+
#logger.cache.name = software.amazon.awssdk.utils.cache.CachedSupplier
41+
#logger.cache.level = DEBUG
42+
43+
#logger.instance.name = software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider
44+
#logger.instance.level = DEBUG

0 commit comments

Comments
 (0)