Skip to content

Commit 17822d7

Browse files
authored
[IMDS] Surface area review comments (#3681)
* Surface area review - removed getters on client builder public interface - rename MetadataResponse to Ec2MetadataResponse - add builder-consumer for retry policy - add DefaultSdkAsyncHttpClientBuilder and DefaultSdkHttpClientBuilder to client builders - remove duplication in BaseEc2MetadataClient constructors - TokenResponseHandler cleanup - Updated ttl header logic if not found in response headers - use SdkHttpClient.Builder in IMDS Client Builder interface - Javadoc - Async Http Request Handlers refactor - add test for mutual exclusion of http client and http client builder in IMDS client builders - dont retry on bad response format from server
1 parent 556f341 commit 17822d7

19 files changed

+358
-275
lines changed

core/imds/src/main/java/software/amazon/awssdk/imds/Ec2MetadataAsyncClient.java

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,22 +31,27 @@
3131
public interface Ec2MetadataAsyncClient extends SdkAutoCloseable {
3232

3333
/**
34-
* Gets the specified instance metadata value by the given path.
34+
* Gets the specified instance metadata value by the given path. For more information about instance metadata, check the
35+
* <a href=https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html>Instance metadata documentation</a>.
3536
*
3637
* @param path Input path
3738
* @return A CompletableFuture that completes when the MetadataResponse is made available.
3839
*/
39-
CompletableFuture<MetadataResponse> get(String path);
40+
CompletableFuture<Ec2MetadataResponse> get(String path);
4041

4142
/**
4243
* Create an {@link Ec2MetadataAsyncClient} instance using the default values.
4344
*
44-
* @return
45+
* @return the client instance.
4546
*/
4647
static Ec2MetadataAsyncClient create() {
4748
return builder().build();
4849
}
4950

51+
/**
52+
* Creates a builder for an async client instance.
53+
* @return the newly created builder instance.
54+
*/
5055
static Ec2MetadataAsyncClient.Builder builder() {
5156
return DefaultEc2MetadataAsyncClient.builder();
5257
}
@@ -84,5 +89,15 @@ interface Builder extends Ec2MetadataClientBuilder<Ec2MetadataAsyncClient.Builde
8489
* @return a reference to this builder
8590
*/
8691
Builder httpClient(SdkAsyncHttpClient httpClient);
92+
93+
/**
94+
* An http client builder used to retrieve an instance of an {@link SdkAsyncHttpClient}. If specified, the Ec2
95+
* Metadata Client will use the instance returned by the builder and manage its lifetime by closing the http client
96+
* once the Ec2 Client itself is closed.
97+
*
98+
* @param builder the builder to used to retrieve an instance.
99+
* @return a reference to this builder
100+
*/
101+
Builder httpClient(SdkAsyncHttpClient.Builder<?> builder);
87102
}
88103
}

core/imds/src/main/java/software/amazon/awssdk/imds/Ec2MetadataClient.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,18 @@
3030
public interface Ec2MetadataClient extends SdkAutoCloseable {
3131

3232
/**
33-
* Gets the specified instance metadata value by the given path.
33+
* Gets the specified instance metadata value by the given path. For more information about instance metadata, check the
34+
* <a href=https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html>Instance metadata documentation</a>
35+
*
3436
* @param path Input path
3537
* @return Instance metadata value as part of MetadataResponse Object
3638
*/
37-
MetadataResponse get(String path);
39+
Ec2MetadataResponse get(String path);
3840

3941
/**
4042
* Create an {@link Ec2MetadataClient} instance using the default values.
43+
*
44+
* @return the client instance.
4145
*/
4246
static Ec2MetadataClient create() {
4347
return builder().build();
@@ -69,6 +73,15 @@ interface Builder extends Ec2MetadataClientBuilder<Ec2MetadataClient.Builder, Ec
6973
*/
7074
Builder httpClient(SdkHttpClient httpClient);
7175

76+
/**
77+
* A http client builder used to retrieve an instance of an {@link SdkHttpClient}. If specified, the Ec2 Metadata Client
78+
* will use the instance returned by the builder and manage its lifetime by closing the http client once the Ec2 Client
79+
* itself is closed.
80+
*
81+
* @param builder the builder to used to retrieve an instance.
82+
* @return a reference to this builder
83+
*/
84+
Builder httpClient(SdkHttpClient.Builder<?> builder);
7285
}
7386

7487
}

core/imds/src/main/java/software/amazon/awssdk/imds/Ec2MetadataClientBuilder.java

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,15 @@
1717

1818
import java.net.URI;
1919
import java.time.Duration;
20+
import java.util.function.Consumer;
2021
import software.amazon.awssdk.annotations.SdkPublicApi;
2122
import software.amazon.awssdk.core.retry.RetryMode;
2223
import software.amazon.awssdk.core.retry.backoff.BackoffStrategy;
2324
import software.amazon.awssdk.imds.internal.Ec2MetadataEndpointProvider;
2425
import software.amazon.awssdk.utils.builder.SdkBuilder;
2526

2627
/**
27-
* Base shared builder interface for Ec2MetadataClient
28+
* Base shared builder interface for Ec2MetadataClients, sync and async.
2829
* @param <B> the Builder Type
2930
* @param <T> the Ec2MetadataClient Type
3031
*/
@@ -33,15 +34,30 @@ public interface Ec2MetadataClientBuilder<B, T> extends SdkBuilder<Ec2MetadataCl
3334
/**
3435
* Define the retry policy which includes the number of retry attempts for any failed request.
3536
* <p>
36-
* If not specified, defaults to 3 retry attempts and a {@link BackoffStrategy#defaultStrategy()} backoff strategy} that
37-
* uses {@link RetryMode#STANDARD}
37+
* If not specified, defaults to 3 retry attempts and a {@link BackoffStrategy#defaultStrategy()} backoff strategy} that
38+
* uses {@link RetryMode#STANDARD}. Can be also specified by using the
39+
* {@link Ec2MetadataClientBuilder#retryPolicy(Consumer)} method. if any of the retryPolicy methods are called multiple times,
40+
* only the last invocation will be considered.
3841
*
3942
* @param retryPolicy The retry policy which includes the number of retry attempts for any failed request.
4043
* @return a reference to this builder
4144
*/
4245
B retryPolicy(Ec2MetadataRetryPolicy retryPolicy);
4346

44-
Ec2MetadataRetryPolicy getRetryPolicy();
47+
/**
48+
* Define the retry policy which includes the number of retry attempts for any failed request. Can be used instead of
49+
* {@link Ec2MetadataClientBuilder#retryPolicy(Ec2MetadataRetryPolicy)} to use a "fluent consumer" syntax. User
50+
* <em>should not</em> manually build the builder in the consumer.
51+
* <p>
52+
* If not specified, defaults to 3 retry attempts and a {@link BackoffStrategy#defaultStrategy()} backoff strategy} that
53+
* uses {@link RetryMode#STANDARD}. Can be also specified by using the
54+
* {@link Ec2MetadataClientBuilder#retryPolicy(Ec2MetadataRetryPolicy)} method. if any of the retryPolicy methods are
55+
* called multiple times, only the last invocation will be considered.
56+
*
57+
* @param builderConsumer the consumer
58+
* @return a reference to this builder
59+
*/
60+
B retryPolicy(Consumer<Ec2MetadataRetryPolicy.Builder> builderConsumer);
4561

4662
/**
4763
* Define the endpoint of IMDS.
@@ -54,8 +70,6 @@ public interface Ec2MetadataClientBuilder<B, T> extends SdkBuilder<Ec2MetadataCl
5470
*/
5571
B endpoint(URI endpoint);
5672

57-
URI getEndpoint();
58-
5973
/**
6074
* Define the Time to live (TTL) of the token. The token represents a logical session. The TTL specifies the length of time
6175
* that the token is valid and, therefore, the duration of the session. Defaults to 21,600 seconds (6 hours) if not specified.
@@ -65,8 +79,6 @@ public interface Ec2MetadataClientBuilder<B, T> extends SdkBuilder<Ec2MetadataCl
6579
*/
6680
B tokenTtl(Duration tokenTtl);
6781

68-
Duration getTokenTtl();
69-
7082
/**
7183
* Define the endpoint mode of IMDS. Supported values include IPv4 and IPv6. Used to determine the endpoint of the IMDS
7284
* Client only if {@link Ec2MetadataClientBuilder#endpoint(URI)} is not specified. Only one of both endpoint or endpoint mode
@@ -80,7 +92,4 @@ public interface Ec2MetadataClientBuilder<B, T> extends SdkBuilder<Ec2MetadataCl
8092
*/
8193
B endpointMode(EndpointMode endpointMode);
8294

83-
EndpointMode getEndpointMode();
84-
85-
8695
}

core/imds/src/main/java/software/amazon/awssdk/imds/MetadataResponse.java renamed to core/imds/src/main/java/software/amazon/awssdk/imds/Ec2MetadataResponse.java

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,28 +28,28 @@
2828
import software.amazon.awssdk.utils.Validate;
2929

3030
/**
31-
* The class is used for response handling and parsing the metadata fetched by the get call in the {@link Ec2MetadataClient}
32-
* interface.
33-
* The class provides convenience methods to the users to parse the metadata as a String, List and Document.
31+
* This class is used for response handling and parsing the metadata fetched by the get call in the {@link Ec2MetadataClient}
32+
* interface. It provides convenience methods to the users to parse the metadata as a String and List. Also provides
33+
* ways to parse the metadata as Document type if it is in the json format.
3434
*/
3535
@SdkPublicApi
36-
public final class MetadataResponse {
36+
public final class Ec2MetadataResponse {
3737

3838
private static final JsonNodeParser JSON_NODE_PARSER = JsonNode.parserBuilder().removeErrorLocations(true).build();
3939

4040
private final String body;
4141

42-
private MetadataResponse(String body) {
42+
private Ec2MetadataResponse(String body) {
4343
this.body = Validate.notNull(body, "Metadata is null");
4444
}
4545

4646
/**
47-
* Create a {@link MetadataResponse} with the given body as it's content.
47+
* Create a {@link Ec2MetadataResponse} with the given body as it's content.
4848
* @param body the content of the response
49-
* @return a {@link MetadataResponse} with the given body as it's content.
49+
* @return a {@link Ec2MetadataResponse} with the given body as it's content.
5050
*/
51-
public static MetadataResponse create(String body) {
52-
return new MetadataResponse(body);
51+
public static Ec2MetadataResponse create(String body) {
52+
return new Ec2MetadataResponse(body);
5353
}
5454

5555
/**
@@ -86,7 +86,7 @@ public boolean equals(Object o) {
8686
if (o == null || getClass() != o.getClass()) {
8787
return false;
8888
}
89-
MetadataResponse that = (MetadataResponse) o;
89+
Ec2MetadataResponse that = (Ec2MetadataResponse) o;
9090
return body.equals(that.body);
9191
}
9292

core/imds/src/main/java/software/amazon/awssdk/imds/Ec2MetadataRetryPolicy.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public final class Ec2MetadataRetryPolicy implements ToCopyableBuilder<Ec2Metada
3838
private static final int DEFAULT_RETRY_ATTEMPTS = 3;
3939

4040
private final BackoffStrategy backoffStrategy;
41-
private final int numRetries;
41+
private final Integer numRetries;
4242

4343
private Ec2MetadataRetryPolicy(BuilderImpl builder) {
4444

@@ -58,7 +58,7 @@ public boolean equals(Object obj) {
5858
}
5959
Ec2MetadataRetryPolicy ec2MetadataRetryPolicy = (Ec2MetadataRetryPolicy) obj;
6060

61-
if (numRetries != ec2MetadataRetryPolicy.numRetries) {
61+
if (!Objects.equals(numRetries, ec2MetadataRetryPolicy.numRetries)) {
6262
return false;
6363
}
6464
return Objects.equals(backoffStrategy, ec2MetadataRetryPolicy.backoffStrategy);
@@ -122,8 +122,6 @@ public interface Builder extends CopyableBuilder<Ec2MetadataRetryPolicy.Builder,
122122
*/
123123
Builder numRetries(Integer numRetries);
124124

125-
@Override
126-
Ec2MetadataRetryPolicy build();
127125
}
128126

129127
private static final class BuilderImpl implements Builder {

core/imds/src/main/java/software/amazon/awssdk/imds/internal/AsyncHttpRequestHelper.java

Lines changed: 50 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,25 @@
1515

1616
package software.amazon.awssdk.imds.internal;
1717

18+
import static software.amazon.awssdk.imds.internal.BaseEc2MetadataClient.uncheckedInputStreamToUtf8;
19+
import static software.amazon.awssdk.imds.internal.RequestMarshaller.EC2_METADATA_TOKEN_TTL_HEADER;
20+
1821
import java.time.Duration;
22+
import java.util.Optional;
1923
import java.util.concurrent.CompletableFuture;
20-
import java.util.function.Consumer;
2124
import java.util.function.Function;
2225
import software.amazon.awssdk.annotations.SdkInternalApi;
26+
import software.amazon.awssdk.core.exception.RetryableException;
27+
import software.amazon.awssdk.core.exception.SdkClientException;
2328
import software.amazon.awssdk.core.http.HttpResponseHandler;
2429
import software.amazon.awssdk.core.interceptor.ExecutionAttributes;
2530
import software.amazon.awssdk.core.internal.http.TransformingAsyncResponseHandler;
2631
import software.amazon.awssdk.core.internal.http.async.AsyncResponseHandler;
2732
import software.amazon.awssdk.core.internal.http.async.SimpleHttpContentPublisher;
33+
import software.amazon.awssdk.http.AbortableInputStream;
34+
import software.amazon.awssdk.http.HttpStatusFamily;
2835
import software.amazon.awssdk.http.SdkHttpFullRequest;
36+
import software.amazon.awssdk.http.SdkHttpFullResponse;
2937
import software.amazon.awssdk.http.async.AsyncExecuteRequest;
3038
import software.amazon.awssdk.http.async.SdkAsyncHttpClient;
3139
import software.amazon.awssdk.http.async.SdkHttpContentPublisher;
@@ -41,28 +49,22 @@ private AsyncHttpRequestHelper() {
4149
public static CompletableFuture<String> sendAsyncMetadataRequest(SdkAsyncHttpClient httpClient,
4250
SdkHttpFullRequest baseRequest,
4351
CompletableFuture<?> parentFuture) {
44-
StringResponseHandler stringResponseHandler = new StringResponseHandler();
45-
return sendAsync(httpClient, baseRequest, stringResponseHandler, stringResponseHandler::setFuture, parentFuture);
52+
return sendAsync(httpClient, baseRequest, AsyncHttpRequestHelper::handleResponse, parentFuture);
4653
}
4754

48-
public static CompletableFuture<Token> sendAsyncTokenRequest(Duration ttlSeconds,
49-
SdkAsyncHttpClient httpClient,
55+
public static CompletableFuture<Token> sendAsyncTokenRequest(SdkAsyncHttpClient httpClient,
5056
SdkHttpFullRequest baseRequest) {
51-
TokenResponseHandler tokenResponseHandler = new TokenResponseHandler(ttlSeconds.getSeconds());
52-
return sendAsync(httpClient, baseRequest, tokenResponseHandler, tokenResponseHandler::setFuture, null);
57+
return sendAsync(httpClient, baseRequest, AsyncHttpRequestHelper::handleTokenResponse, null);
5358
}
5459

55-
static <T> CompletableFuture<T> sendAsync(SdkAsyncHttpClient client,
56-
SdkHttpFullRequest request,
57-
HttpResponseHandler<T> handler,
58-
Consumer<CompletableFuture<T>> withFuture,
59-
CompletableFuture<?> parentFuture) {
60+
private static <T> CompletableFuture<T> sendAsync(SdkAsyncHttpClient client,
61+
SdkHttpFullRequest request,
62+
HttpResponseHandler<T> handler,
63+
CompletableFuture<?> parentFuture) {
6064
SdkHttpContentPublisher requestContentPublisher = new SimpleHttpContentPublisher(request);
61-
TransformingAsyncResponseHandler<T> responseHandler = new AsyncResponseHandler<>(handler,
62-
Function.identity(),
63-
new ExecutionAttributes());
65+
TransformingAsyncResponseHandler<T> responseHandler =
66+
new AsyncResponseHandler<>(handler, Function.identity(), new ExecutionAttributes());
6467
CompletableFuture<T> responseHandlerFuture = responseHandler.prepare();
65-
withFuture.accept(responseHandlerFuture);
6668
AsyncExecuteRequest metadataRequest = AsyncExecuteRequest.builder()
6769
.request(request)
6870
.requestContentPublisher(requestContentPublisher)
@@ -74,7 +76,39 @@ static <T> CompletableFuture<T> sendAsync(SdkAsyncHttpClient client,
7476
CompletableFutureUtils.forwardExceptionTo(parentFuture, responseHandlerFuture);
7577
}
7678
return responseHandlerFuture;
79+
}
80+
81+
private static String handleResponse(SdkHttpFullResponse response, ExecutionAttributes executionAttributes) {
82+
HttpStatusFamily statusCode = HttpStatusFamily.of(response.statusCode());
83+
AbortableInputStream inputStream =
84+
response.content().orElseThrow(() -> SdkClientException.create("Unexpected error: empty response content"));
85+
String responseContent = uncheckedInputStreamToUtf8(inputStream);
86+
87+
// non-retryable error
88+
if (statusCode.isOneOf(HttpStatusFamily.CLIENT_ERROR)) {
89+
throw SdkClientException.builder().message(responseContent).build();
90+
}
7791

92+
// retryable error
93+
if (statusCode.isOneOf(HttpStatusFamily.SERVER_ERROR)) {
94+
throw RetryableException.create(responseContent);
95+
}
96+
return responseContent;
7897
}
7998

99+
private static Token handleTokenResponse(SdkHttpFullResponse response, ExecutionAttributes executionAttributes) {
100+
String tokenValue = handleResponse(response, executionAttributes);
101+
Optional<String> ttl = response.firstMatchingHeader(EC2_METADATA_TOKEN_TTL_HEADER);
102+
103+
if (!ttl.isPresent()) {
104+
throw SdkClientException.create(EC2_METADATA_TOKEN_TTL_HEADER + " header not found in token response");
105+
}
106+
try {
107+
Duration ttlDuration = Duration.ofSeconds(Long.parseLong(ttl.get()));
108+
return new Token(tokenValue, ttlDuration);
109+
} catch (NumberFormatException nfe) {
110+
throw SdkClientException.create(
111+
"Invalid token format received from IMDS server. Token received: " + tokenValue, nfe);
112+
}
113+
}
80114
}

core/imds/src/main/java/software/amazon/awssdk/imds/internal/BaseEc2MetadataClient.java

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
import software.amazon.awssdk.core.retry.RetryPolicyContext;
2626
import software.amazon.awssdk.http.AbortableInputStream;
2727
import software.amazon.awssdk.http.SdkHttpConfigurationOption;
28-
import software.amazon.awssdk.imds.Ec2MetadataClientBuilder;
2928
import software.amazon.awssdk.imds.Ec2MetadataRetryPolicy;
3029
import software.amazon.awssdk.imds.EndpointMode;
3130
import software.amazon.awssdk.utils.AttributeMap;
@@ -50,16 +49,23 @@ public abstract class BaseEc2MetadataClient {
5049
protected final RequestMarshaller requestMarshaller;
5150
protected final Duration tokenTtl;
5251

53-
protected BaseEc2MetadataClient(Ec2MetadataClientBuilder<?, ?> builder) {
54-
this.retryPolicy = Validate.getOrDefault(builder.getRetryPolicy(), Ec2MetadataRetryPolicy.builder()::build);
55-
this.tokenTtl = Validate.getOrDefault(builder.getTokenTtl(), () -> DEFAULT_TOKEN_TTL);
56-
this.endpoint = getEndpoint(builder);
52+
private BaseEc2MetadataClient(Ec2MetadataRetryPolicy retryPolicy, Duration tokenTtl, URI endpoint,
53+
EndpointMode endpointMode) {
54+
this.retryPolicy = Validate.getOrDefault(retryPolicy, Ec2MetadataRetryPolicy.builder()::build);
55+
this.tokenTtl = Validate.getOrDefault(tokenTtl, () -> DEFAULT_TOKEN_TTL);
56+
this.endpoint = getEndpoint(endpoint, endpointMode);
5757
this.requestMarshaller = new RequestMarshaller(this.endpoint);
5858
}
5959

60-
private URI getEndpoint(Ec2MetadataClientBuilder<?, ?> builder) {
61-
URI builderEndpoint = builder.getEndpoint();
62-
EndpointMode builderEndpointMode = builder.getEndpointMode();
60+
protected BaseEc2MetadataClient(DefaultEc2MetadataClient.Ec2MetadataBuilder builder) {
61+
this(builder.getRetryPolicy(), builder.getTokenTtl(), builder.getEndpoint(), builder.getEndpointMode());
62+
}
63+
64+
protected BaseEc2MetadataClient(DefaultEc2MetadataAsyncClient.Ec2MetadataAsyncBuilder builder) {
65+
this(builder.getRetryPolicy(), builder.getTokenTtl(), builder.getEndpoint(), builder.getEndpointMode());
66+
}
67+
68+
private URI getEndpoint(URI builderEndpoint, EndpointMode builderEndpointMode) {
6369
Validate.mutuallyExclusive("Only one of 'endpoint' or 'endpointMode' must be specified, but not both",
6470
builderEndpoint, builderEndpointMode);
6571
if (builderEndpoint != null) {

0 commit comments

Comments
 (0)