Skip to content

Commit 3f67bf7

Browse files
authored
Improve availability of instance profile credentials provider during outages. (#2989)
This is done by allowing services to validate credential expiration time during IMDS outages instead of assuming the SDK has access to the latest valid expiration time. Additional changes: 1. Removed unnecessary intellij inspections that have frequent false positives. 2. Include SDK user-agent in container credential provider calls. 3. Allow specifying the profile file and name used by the instance profile credentials provider. 4. Use the client's profile file and name for instance profile credentials when the default credentials provider is not overridden. 5. Convert the HttpCredentialsProvider protected API to a public API (with some breaking changes), so that there's fewer public classes extending internal classes.
1 parent e2a4cc1 commit 3f67bf7

19 files changed

+753
-394
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"category": "AWS SDK for Java v2",
3+
"contributor": "",
4+
"type": "bugfix",
5+
"description": "Moved HttpCredentialsProvider (base class of ContainerCredentialsProvider and InstanceProfileCredentialsProvider) from private to public. This fixes an issue where public classes extended an internal class. Some components of this type were modified to allow it to be public."
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"category": "AWS SDK for Java v2",
3+
"contributor": "",
4+
"type": "feature",
5+
"description": "Include SDK user-agent in container credential provider calls."
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"category": "AWS SDK for Java v2",
3+
"contributor": "",
4+
"type": "feature",
5+
"description": "Allow specifying the profile file and name used by the instance profile credentials provider."
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"category": "AWS SDK for Java v2",
3+
"contributor": "",
4+
"type": "feature",
5+
"description": "Improve resilience of instance profile credentials provider to short-term outages. Credentials that are close to expiration or expired can still be used to sign calls when the instance metadata service appears to be having issues. Services are now responsible for determining whether the credentials have actually expired."
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"category": "AWS SDK for Java v2",
3+
"contributor": "",
4+
"type": "feature",
5+
"description": "Use the client's profile file and name for instance profile credentials when the default credentials provider is not overridden."
6+
}

.idea/inspectionProfiles/AWS_Java_SDK_2_0.xml

Lines changed: 4 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 116 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -15,25 +15,33 @@
1515

1616
package software.amazon.awssdk.auth.credentials;
1717

18-
import static java.util.Collections.singletonMap;
1918
import static java.util.Collections.unmodifiableSet;
2019

2120
import java.io.IOException;
2221
import java.net.URI;
22+
import java.time.Instant;
23+
import java.time.temporal.ChronoUnit;
2324
import java.util.Arrays;
24-
import java.util.Collections;
25+
import java.util.HashMap;
2526
import java.util.HashSet;
2627
import java.util.Map;
2728
import java.util.Set;
2829
import software.amazon.awssdk.annotations.SdkPublicApi;
29-
import software.amazon.awssdk.annotations.SdkTestInternalApi;
3030
import software.amazon.awssdk.auth.credentials.internal.ContainerCredentialsRetryPolicy;
31+
import software.amazon.awssdk.auth.credentials.internal.HttpCredentialsLoader;
32+
import software.amazon.awssdk.auth.credentials.internal.HttpCredentialsLoader.LoadedCredentials;
3133
import software.amazon.awssdk.core.SdkSystemSetting;
3234
import software.amazon.awssdk.core.exception.SdkClientException;
35+
import software.amazon.awssdk.core.util.SdkUserAgent;
3336
import software.amazon.awssdk.regions.util.ResourcesEndpointProvider;
3437
import software.amazon.awssdk.regions.util.ResourcesEndpointRetryPolicy;
38+
import software.amazon.awssdk.utils.ComparableUtils;
3539
import software.amazon.awssdk.utils.StringUtils;
3640
import software.amazon.awssdk.utils.ToString;
41+
import software.amazon.awssdk.utils.Validate;
42+
import software.amazon.awssdk.utils.cache.CachedSupplier;
43+
import software.amazon.awssdk.utils.cache.NonBlocking;
44+
import software.amazon.awssdk.utils.cache.RefreshResult;
3745

3846
/**
3947
* {@link AwsCredentialsProvider} implementation that loads credentials from a local metadata service.
@@ -52,15 +60,28 @@
5260
* Service (ECS)</a>
5361
*/
5462
@SdkPublicApi
55-
public final class ContainerCredentialsProvider extends HttpCredentialsProvider {
56-
private final ResourcesEndpointProvider credentialsEndpointProvider;
63+
public final class ContainerCredentialsProvider implements HttpCredentialsProvider {
64+
private static final Set<String> ALLOWED_HOSTS = unmodifiableSet(new HashSet<>(Arrays.asList("localhost", "127.0.0.1")));
65+
66+
private final String endpoint;
67+
private final HttpCredentialsLoader httpCredentialsLoader;
68+
private final CachedSupplier<AwsCredentials> credentialsCache;
5769

5870
/**
5971
* @see #builder()
6072
*/
6173
private ContainerCredentialsProvider(BuilderImpl builder) {
62-
super(builder);
63-
this.credentialsEndpointProvider = builder.credentialsEndpointProvider;
74+
this.endpoint = builder.endpoint;
75+
this.httpCredentialsLoader = HttpCredentialsLoader.create();
76+
77+
if (Boolean.TRUE.equals(builder.asyncCredentialUpdateEnabled)) {
78+
Validate.paramNotBlank(builder.asyncThreadName, "asyncThreadName");
79+
this.credentialsCache = CachedSupplier.builder(this::refreshCredentials)
80+
.prefetchStrategy(new NonBlocking(builder.asyncThreadName))
81+
.build();
82+
} else {
83+
this.credentialsCache = CachedSupplier.builder(this::refreshCredentials).build();
84+
}
6485
}
6586

6687
/**
@@ -71,21 +92,60 @@ public static Builder builder() {
7192
}
7293

7394
@Override
74-
protected ResourcesEndpointProvider getCredentialsEndpointProvider() {
75-
return credentialsEndpointProvider;
95+
public String toString() {
96+
return ToString.create("ContainerCredentialsProvider");
97+
}
98+
99+
private RefreshResult<AwsCredentials> refreshCredentials() {
100+
LoadedCredentials loadedCredentials =
101+
httpCredentialsLoader.loadCredentials(new ContainerCredentialsEndpointProvider(endpoint));
102+
Instant expiration = loadedCredentials.getExpiration().orElse(null);
103+
104+
return RefreshResult.builder(loadedCredentials.getAwsCredentials())
105+
.staleTime(staleTime(expiration))
106+
.prefetchTime(prefetchTime(expiration))
107+
.build();
108+
}
109+
110+
private Instant staleTime(Instant expiration) {
111+
if (expiration == null) {
112+
return null;
113+
}
114+
115+
return expiration.minus(1, ChronoUnit.MINUTES);
116+
}
117+
118+
private Instant prefetchTime(Instant expiration) {
119+
Instant oneHourFromNow = Instant.now().plus(1, ChronoUnit.HOURS);
120+
121+
if (expiration == null) {
122+
return oneHourFromNow;
123+
}
124+
125+
Instant fifteenMinutesBeforeExpiration = expiration.minus(15, ChronoUnit.MINUTES);
126+
127+
return ComparableUtils.minimum(oneHourFromNow, fifteenMinutesBeforeExpiration);
76128
}
77129

78130
@Override
79-
public String toString() {
80-
return ToString.create("ContainerCredentialsProvider");
131+
public AwsCredentials resolveCredentials() {
132+
return credentialsCache.get();
133+
}
134+
135+
@Override
136+
public void close() {
137+
credentialsCache.close();
81138
}
82139

83140
static final class ContainerCredentialsEndpointProvider implements ResourcesEndpointProvider {
84-
private static final Set<String> ALLOWED_HOSTS = unmodifiableSet(new HashSet<>(Arrays.asList("localhost", "127.0.0.1")));
141+
private final String endpoint;
142+
143+
ContainerCredentialsEndpointProvider(String endpoint) {
144+
this.endpoint = endpoint;
145+
}
85146

86147
@Override
87148
public URI endpoint() throws IOException {
88-
89149
if (!SdkSystemSetting.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI.getStringValue().isPresent() &&
90150
!SdkSystemSetting.AWS_CONTAINER_CREDENTIALS_FULL_URI.getStringValue().isPresent()) {
91151
throw SdkClientException.builder()
@@ -117,26 +177,28 @@ public ResourcesEndpointRetryPolicy retryPolicy() {
117177

118178
@Override
119179
public Map<String, String> headers() {
120-
return SdkSystemSetting.AWS_CONTAINER_AUTHORIZATION_TOKEN.getStringValue()
121-
.filter(StringUtils::isNotBlank)
122-
.map(t -> singletonMap("Authorization", t))
123-
.orElseGet(Collections::emptyMap);
180+
Map<String, String> requestHeaders = new HashMap<>();
181+
requestHeaders.put("User-Agent", SdkUserAgent.create().userAgent());
182+
SdkSystemSetting.AWS_CONTAINER_AUTHORIZATION_TOKEN.getStringValue()
183+
.filter(StringUtils::isNotBlank)
184+
.ifPresent(t -> requestHeaders.put("Authorization", t));
185+
return requestHeaders;
124186
}
125187

126188
private URI createUri(String relativeUri) {
127-
return URI.create(SdkSystemSetting.AWS_CONTAINER_SERVICE_ENDPOINT.getStringValueOrThrow() + relativeUri);
189+
String host = endpoint != null ? endpoint : SdkSystemSetting.AWS_CONTAINER_SERVICE_ENDPOINT.getStringValueOrThrow();
190+
return URI.create(host + relativeUri);
128191
}
129192

130193
private URI createGenericContainerUrl() {
131194
URI uri = URI.create(SdkSystemSetting.AWS_CONTAINER_CREDENTIALS_FULL_URI.getStringValueOrThrow());
132195
if (!ALLOWED_HOSTS.contains(uri.getHost())) {
133-
196+
String envVarName = SdkSystemSetting.AWS_CONTAINER_CREDENTIALS_FULL_URI.environmentVariable();
134197
throw SdkClientException.builder()
135198
.message(String.format("The full URI (%s) contained within environment " +
136-
"variable %s has an invalid host. Host can only be one of [%s].",
137-
uri,
138-
SdkSystemSetting.AWS_CONTAINER_CREDENTIALS_FULL_URI
139-
.environmentVariable(),
199+
"variable %s has an invalid host. Host can only be one of [%s].",
200+
uri,
201+
envVarName,
140202
String.join(",", ALLOWED_HOSTS)))
141203
.build();
142204
}
@@ -148,29 +210,47 @@ private URI createGenericContainerUrl() {
148210
* A builder for creating a custom a {@link ContainerCredentialsProvider}.
149211
*/
150212
public interface Builder extends HttpCredentialsProvider.Builder<ContainerCredentialsProvider, Builder> {
213+
}
214+
215+
private static final class BuilderImpl implements Builder {
216+
private String endpoint;
217+
private Boolean asyncCredentialUpdateEnabled;
218+
private String asyncThreadName;
219+
220+
BuilderImpl() {
221+
asyncThreadName("container-credentials-provider");
222+
}
151223

152-
/**
153-
* Build a {@link ContainerCredentialsProvider} from the provided configuration.
154-
*/
155224
@Override
156-
ContainerCredentialsProvider build();
157-
}
225+
public Builder endpoint(String endpoint) {
226+
this.endpoint = endpoint;
227+
return this;
228+
}
158229

159-
static final class BuilderImpl extends HttpCredentialsProvider.BuilderImpl<ContainerCredentialsProvider, Builder>
160-
implements Builder {
230+
public void setEndpoint(String endpoint) {
231+
endpoint(endpoint);
232+
}
161233

162-
private ResourcesEndpointProvider credentialsEndpointProvider = new ContainerCredentialsEndpointProvider();
234+
@Override
235+
public Builder asyncCredentialUpdateEnabled(Boolean asyncCredentialUpdateEnabled) {
236+
this.asyncCredentialUpdateEnabled = asyncCredentialUpdateEnabled;
237+
return this;
238+
}
163239

164-
BuilderImpl() {
165-
super.asyncThreadName("container-credentials-provider");
240+
public void setAsyncCredentialUpdateEnabled(boolean asyncCredentialUpdateEnabled) {
241+
asyncCredentialUpdateEnabled(asyncCredentialUpdateEnabled);
166242
}
167243

168-
@SdkTestInternalApi
169-
Builder credentialsEndpointProvider(ResourcesEndpointProvider credentialsEndpointProvider) {
170-
this.credentialsEndpointProvider = credentialsEndpointProvider;
244+
@Override
245+
public Builder asyncThreadName(String asyncThreadName) {
246+
this.asyncThreadName = asyncThreadName;
171247
return this;
172248
}
173249

250+
public void setAsyncThreadName(String asyncThreadName) {
251+
asyncThreadName(asyncThreadName);
252+
}
253+
174254
@Override
175255
public ContainerCredentialsProvider build() {
176256
return new ContainerCredentialsProvider(this);

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ private static LazyAwsCredentialsProvider createChain(Builder builder) {
8383
.build(),
8484
InstanceProfileCredentialsProvider.builder()
8585
.asyncCredentialUpdateEnabled(asyncCredentialUpdateEnabled)
86+
.profileFile(builder.profileFile)
87+
.profileName(builder.profileName)
8688
.build()
8789
};
8890

0 commit comments

Comments
 (0)