+ * If not specified, defaults to {@link Executors#newScheduledThreadPool} with a default value of 3 thread in the
+ * pool.
+ *
+ * @param scheduledExecutorService the ScheduledExecutorService to use for retry attempt.
+ * @return a reference to this builder
+ */
+ Builder scheduledExecutorService(ScheduledExecutorService scheduledExecutorService);
+
+ /**
+ * Define the http client used by the Ec2 Metadata client. If provided, the Ec2MetadataClient will NOT manage the
+ * lifetime if the httpClient and must therefore be closed explicitly by calling the {@link SdkAsyncHttpClient#close()}
+ * method on it.
+ *
+ * If not specified, the IMDS client will look for a SdkAsyncHttpClient class included in the classpath of the
+ * application and creates a new instance of that class, managed by the IMDS Client, that will be closed when the IMDS
+ * Client is closed. If no such class can be found, will throw a {@link SdkClientException}.
+ *
+ * @param httpClient the http client
+ * @return a reference to this builder
+ */
+ Builder httpClient(SdkAsyncHttpClient httpClient);
+
+ /**
+ * An http client builder used to retrieve an instance of an {@link SdkAsyncHttpClient}. If specified, the Ec2
+ * Metadata Client will use the instance returned by the builder and manage its lifetime by closing the http client
+ * once the Ec2 Client itself is closed.
+ *
+ * @param builder the builder to used to retrieve an instance.
+ * @return a reference to this builder
+ */
+ Builder httpClient(SdkAsyncHttpClient.Builder> builder);
+ }
+}
diff --git a/core/imds/src/main/java/software/amazon/awssdk/imds/Ec2MetadataClient.java b/core/imds/src/main/java/software/amazon/awssdk/imds/Ec2MetadataClient.java
new file mode 100644
index 000000000000..f5f5fcb9a154
--- /dev/null
+++ b/core/imds/src/main/java/software/amazon/awssdk/imds/Ec2MetadataClient.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.awssdk.imds;
+
+import software.amazon.awssdk.annotations.SdkPublicApi;
+import software.amazon.awssdk.core.exception.SdkClientException;
+import software.amazon.awssdk.http.SdkHttpClient;
+import software.amazon.awssdk.imds.internal.DefaultEc2MetadataClient;
+import software.amazon.awssdk.utils.SdkAutoCloseable;
+
+
+/**
+ * Interface to represent the Ec2Metadata Client Class. Used to access instance metadata from a running EC2 instance.
+ *
{
+
+ /**
+ * Define the http client used by the Ec2 Metadata client. If provided, the Ec2MetadataClient will NOT manage the
+ * lifetime if the httpClient and must therefore be closed explicitly by calling the {@link SdkHttpClient#close()}
+ * method on it.
+ *
+ * If not specified, the IMDS client will look for a SdkHttpClient class included in the classpath of the
+ * application and create a new instance of that class, managed by the IMDS Client, that will be closed when the IMDS
+ * Client is closed. If no such class can be found, will throw a {@link SdkClientException}.
+ *
+ * @param httpClient the http client
+ * @return a reference to this builder
+ */
+ Builder httpClient(SdkHttpClient httpClient);
+
+ /**
+ * A http client builder used to retrieve an instance of an {@link SdkHttpClient}. If specified, the Ec2 Metadata Client
+ * will use the instance returned by the builder and manage its lifetime by closing the http client once the Ec2 Client
+ * itself is closed.
+ *
+ * @param builder the builder to used to retrieve an instance.
+ * @return a reference to this builder
+ */
+ Builder httpClient(SdkHttpClient.Builder> builder);
+ }
+
+}
diff --git a/core/imds/src/main/java/software/amazon/awssdk/imds/Ec2MetadataClientBuilder.java b/core/imds/src/main/java/software/amazon/awssdk/imds/Ec2MetadataClientBuilder.java
new file mode 100644
index 000000000000..5cb056f12c51
--- /dev/null
+++ b/core/imds/src/main/java/software/amazon/awssdk/imds/Ec2MetadataClientBuilder.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.awssdk.imds;
+
+import java.net.URI;
+import java.time.Duration;
+import java.util.function.Consumer;
+import software.amazon.awssdk.annotations.SdkPublicApi;
+import software.amazon.awssdk.core.retry.RetryMode;
+import software.amazon.awssdk.core.retry.backoff.BackoffStrategy;
+import software.amazon.awssdk.imds.internal.Ec2MetadataEndpointProvider;
+import software.amazon.awssdk.utils.builder.SdkBuilder;
+
+/**
+ * Base shared builder interface for Ec2MetadataClients, sync and async.
+ * @param the Builder Type
+ * @param the Ec2MetadataClient Type
+ */
+@SdkPublicApi
+public interface Ec2MetadataClientBuilder extends SdkBuilder, T> {
+ /**
+ * Define the retry policy which includes the number of retry attempts for any failed request.
+ *
+ * If not specified, defaults to 3 retry attempts and a {@link BackoffStrategy#defaultStrategy()} backoff strategy} that
+ * uses {@link RetryMode#STANDARD}. Can be also specified by using the
+ * {@link Ec2MetadataClientBuilder#retryPolicy(Consumer)} method. if any of the retryPolicy methods are called multiple times,
+ * only the last invocation will be considered.
+ *
+ * @param retryPolicy The retry policy which includes the number of retry attempts for any failed request.
+ * @return a reference to this builder
+ */
+ B retryPolicy(Ec2MetadataRetryPolicy retryPolicy);
+
+ /**
+ * Define the retry policy which includes the number of retry attempts for any failed request. Can be used instead of
+ * {@link Ec2MetadataClientBuilder#retryPolicy(Ec2MetadataRetryPolicy)} to use a "fluent consumer" syntax. User
+ * should not manually build the builder in the consumer.
+ *
+ * If not specified, defaults to 3 retry attempts and a {@link BackoffStrategy#defaultStrategy()} backoff strategy} that
+ * uses {@link RetryMode#STANDARD}. Can be also specified by using the
+ * {@link Ec2MetadataClientBuilder#retryPolicy(Ec2MetadataRetryPolicy)} method. if any of the retryPolicy methods are
+ * called multiple times, only the last invocation will be considered.
+ *
+ * @param builderConsumer the consumer
+ * @return a reference to this builder
+ */
+ B retryPolicy(Consumer builderConsumer);
+
+ /**
+ * Define the endpoint of IMDS.
+ *
+ * If not specified, the IMDS client will attempt to automatically resolve the endpoint value
+ * based on the logic of {@link Ec2MetadataEndpointProvider}.
+ *
+ * @param endpoint The endpoint of IMDS.
+ * @return a reference to this builder
+ */
+ B endpoint(URI endpoint);
+
+ /**
+ * Define the Time to live (TTL) of the token. The token represents a logical session. The TTL specifies the length of time
+ * that the token is valid and, therefore, the duration of the session. Defaults to 21,600 seconds (6 hours) if not specified.
+ *
+ * @param tokenTtl The Time to live (TTL) of the token.
+ * @return a reference to this builder
+ */
+ B tokenTtl(Duration tokenTtl);
+
+ /**
+ * Define the endpoint mode of IMDS. Supported values include IPv4 and IPv6. Used to determine the endpoint of the IMDS
+ * Client only if {@link Ec2MetadataClientBuilder#endpoint(URI)} is not specified. Only one of both endpoint or endpoint mode
+ * but not both. If both are specified in the Builder, a {@link IllegalArgumentException} will be thrown.
+ *
+ * If not specified, the IMDS client will attempt to automatically resolve the endpoint mode value
+ * based on the logic of {@link Ec2MetadataEndpointProvider}.
+ *
+ * @param endpointMode The endpoint mode of IMDS. Supported values include IPv4 and IPv6.
+ * @return a reference to this builder
+ */
+ B endpointMode(EndpointMode endpointMode);
+
+}
diff --git a/core/imds/src/main/java/software/amazon/awssdk/imds/Ec2MetadataResponse.java b/core/imds/src/main/java/software/amazon/awssdk/imds/Ec2MetadataResponse.java
new file mode 100644
index 000000000000..c7f787e967c0
--- /dev/null
+++ b/core/imds/src/main/java/software/amazon/awssdk/imds/Ec2MetadataResponse.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.awssdk.imds;
+
+import java.io.UncheckedIOException;
+import java.util.Arrays;
+import java.util.List;
+import software.amazon.awssdk.annotations.SdkPublicApi;
+import software.amazon.awssdk.core.document.Document;
+import software.amazon.awssdk.imds.internal.unmarshall.document.DocumentUnmarshaller;
+import software.amazon.awssdk.protocols.jsoncore.JsonNode;
+import software.amazon.awssdk.protocols.jsoncore.JsonNodeParser;
+import software.amazon.awssdk.thirdparty.jackson.core.JsonParseException;
+import software.amazon.awssdk.utils.ToString;
+import software.amazon.awssdk.utils.Validate;
+
+/**
+ * This class is used for response handling and parsing the metadata fetched by the get call in the {@link Ec2MetadataClient}
+ * interface. It provides convenience methods to the users to parse the metadata as a String and List. Also provides
+ * ways to parse the metadata as Document type if it is in the json format.
+ */
+@SdkPublicApi
+public final class Ec2MetadataResponse {
+
+ private static final JsonNodeParser JSON_NODE_PARSER = JsonNode.parserBuilder().removeErrorLocations(true).build();
+
+ private final String body;
+
+ private Ec2MetadataResponse(String body) {
+ this.body = Validate.notNull(body, "Metadata is null");
+ }
+
+ /**
+ * Create a {@link Ec2MetadataResponse} with the given body as its content.
+ * @param body the content of the response
+ * @return a {@link Ec2MetadataResponse} with the given body as its content.
+ */
+ public static Ec2MetadataResponse create(String body) {
+ return new Ec2MetadataResponse(body);
+ }
+
+ /**
+ * @return String Representation of the Metadata Response Body.
+ */
+ public String asString() {
+ return body;
+ }
+
+ /**
+ * Splits the Metadata response body on new line character and returns it as a list.
+ * @return The Metadata response split on each line.
+ */
+ public List asList() {
+ return Arrays.asList(body.split("\n"));
+ }
+
+ /**
+ * Parses the response String into a {@link Document} type. This method can be used for
+ * parsing the metadata in a String Json Format.
+ * @return Document Representation, as json, of the Metadata Response Body.
+ * @throws UncheckedIOException (wrapping a {@link JsonParseException} if the Response body is not of JSON format.
+ */
+ public Document asDocument() {
+ JsonNode node = JSON_NODE_PARSER.parse(body);
+ return node.visit(new DocumentUnmarshaller());
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ Ec2MetadataResponse that = (Ec2MetadataResponse) o;
+ return body.equals(that.body);
+ }
+
+ @Override
+ public int hashCode() {
+ return body.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return ToString.builder("MetadataResponse")
+ .add("body", body)
+ .build();
+ }
+}
diff --git a/core/imds/src/main/java/software/amazon/awssdk/imds/Ec2MetadataRetryPolicy.java b/core/imds/src/main/java/software/amazon/awssdk/imds/Ec2MetadataRetryPolicy.java
new file mode 100644
index 000000000000..010f1a36ba75
--- /dev/null
+++ b/core/imds/src/main/java/software/amazon/awssdk/imds/Ec2MetadataRetryPolicy.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.awssdk.imds;
+
+import java.util.Objects;
+import software.amazon.awssdk.annotations.SdkPublicApi;
+import software.amazon.awssdk.core.retry.RetryMode;
+import software.amazon.awssdk.core.retry.backoff.BackoffStrategy;
+import software.amazon.awssdk.utils.Validate;
+import software.amazon.awssdk.utils.builder.CopyableBuilder;
+import software.amazon.awssdk.utils.builder.ToCopyableBuilder;
+
+/**
+ * Interface for specifying a retry policy to use when evaluating whether or not a request should be retried, and the gap
+ * between each retry. The {@link #builder()} can be used to construct a retry policy with numRetries and backoffStrategy.
+ *
+ * When using the {@link #builder()} the SDK will use default values for fields that are not provided.A custom BackoffStrategy
+ * can be used to construct a policy or a default {@link BackoffStrategy} is used.
+ *
+ * @see BackoffStrategy for a list of SDK provided backoff strategies
+ */
+@SdkPublicApi
+public final class Ec2MetadataRetryPolicy implements ToCopyableBuilder {
+
+ private static final int DEFAULT_RETRY_ATTEMPTS = 3;
+
+ private final BackoffStrategy backoffStrategy;
+ private final Integer numRetries;
+
+ private Ec2MetadataRetryPolicy(BuilderImpl builder) {
+
+ this.numRetries = Validate.getOrDefault(builder.numRetries, () -> DEFAULT_RETRY_ATTEMPTS);
+
+ this.backoffStrategy = Validate.getOrDefault(builder.backoffStrategy,
+ () -> BackoffStrategy.defaultStrategy(RetryMode.STANDARD));
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ Ec2MetadataRetryPolicy ec2MetadataRetryPolicy = (Ec2MetadataRetryPolicy) obj;
+
+ if (!Objects.equals(numRetries, ec2MetadataRetryPolicy.numRetries)) {
+ return false;
+ }
+ return Objects.equals(backoffStrategy, ec2MetadataRetryPolicy.backoffStrategy);
+ }
+
+ @Override
+ public int hashCode() {
+
+ int result = Math.max(numRetries, 0);
+ result = 31 * result + (backoffStrategy != null ? backoffStrategy.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "Ec2MetadataRetryPolicy{" +
+ "backoffStrategy=" + backoffStrategy.toString() +
+ ", numRetries=" + numRetries +
+ '}';
+ }
+
+ /**
+ * Method to return the number of retries allowed.
+ * @return The number of retries allowed.
+ */
+ public int numRetries() {
+ return numRetries;
+ }
+
+ /**
+ * Method to return the BackoffStrategy used.
+ * @return The backoff Strategy used.
+ */
+ public BackoffStrategy backoffStrategy() {
+ return backoffStrategy;
+ }
+
+ public static Builder builder() {
+ return new BuilderImpl();
+ }
+
+ @Override
+ public Builder toBuilder() {
+ return builder().numRetries(numRetries)
+ .backoffStrategy(backoffStrategy);
+ }
+
+ public static Ec2MetadataRetryPolicy none() {
+ return builder().numRetries(0).build();
+ }
+
+ public interface Builder extends CopyableBuilder {
+
+ /**
+ * Configure the backoff strategy that should be used for waiting in between retry attempts.
+ */
+ Builder backoffStrategy(BackoffStrategy backoffStrategy);
+
+ /**
+ * Configure the maximum number of times that a single request should be retried, assuming it fails for a retryable error.
+ */
+ Builder numRetries(Integer numRetries);
+
+ }
+
+ private static final class BuilderImpl implements Builder {
+
+ private Integer numRetries;
+ private BackoffStrategy backoffStrategy;
+
+ private BuilderImpl() {
+ }
+
+ @Override
+ public Builder numRetries(Integer numRetries) {
+ this.numRetries = numRetries;
+ return this;
+ }
+
+ @Override
+ public Builder backoffStrategy(BackoffStrategy backoffStrategy) {
+ this.backoffStrategy = backoffStrategy;
+ return this;
+ }
+
+ @Override
+ public Ec2MetadataRetryPolicy build() {
+ return new Ec2MetadataRetryPolicy(this);
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/imds/src/main/java/software/amazon/awssdk/imds/EndpointMode.java b/core/imds/src/main/java/software/amazon/awssdk/imds/EndpointMode.java
new file mode 100644
index 000000000000..0c926eecab8c
--- /dev/null
+++ b/core/imds/src/main/java/software/amazon/awssdk/imds/EndpointMode.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.awssdk.imds;
+
+import software.amazon.awssdk.annotations.SdkPublicApi;
+
+/**
+ * Enum Class for the Endpoint Mode.
+ */
+@SdkPublicApi
+public enum EndpointMode {
+
+ IPV4,
+ IPV6;
+
+ /**
+ * Returns the appropriate EndpointMode Value after parsing the parameter.
+ * @param s EndpointMode in String Format.
+ * @return EndpointMode enumValue (IPV4 or IPV6).
+ * @throws IllegalArgumentException Unrecognized value for endpoint mode.
+ */
+ public static EndpointMode fromValue(String s) {
+ if (s == null) {
+ return null;
+ }
+
+ for (EndpointMode value : values()) {
+ if (value.name().equalsIgnoreCase(s)) {
+ return value;
+ }
+ }
+
+ throw new IllegalArgumentException("Unrecognized value for endpoint mode: " + s);
+ }
+}
diff --git a/core/imds/src/main/java/software/amazon/awssdk/imds/internal/AsyncHttpRequestHelper.java b/core/imds/src/main/java/software/amazon/awssdk/imds/internal/AsyncHttpRequestHelper.java
new file mode 100644
index 000000000000..73a70ab4ea02
--- /dev/null
+++ b/core/imds/src/main/java/software/amazon/awssdk/imds/internal/AsyncHttpRequestHelper.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.awssdk.imds.internal;
+
+import static software.amazon.awssdk.imds.internal.BaseEc2MetadataClient.uncheckedInputStreamToUtf8;
+import static software.amazon.awssdk.imds.internal.RequestMarshaller.EC2_METADATA_TOKEN_TTL_HEADER;
+
+import java.time.Duration;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Function;
+import software.amazon.awssdk.annotations.SdkInternalApi;
+import software.amazon.awssdk.core.exception.RetryableException;
+import software.amazon.awssdk.core.exception.SdkClientException;
+import software.amazon.awssdk.core.http.HttpResponseHandler;
+import software.amazon.awssdk.core.interceptor.ExecutionAttributes;
+import software.amazon.awssdk.core.internal.http.TransformingAsyncResponseHandler;
+import software.amazon.awssdk.core.internal.http.async.AsyncResponseHandler;
+import software.amazon.awssdk.core.internal.http.async.SimpleHttpContentPublisher;
+import software.amazon.awssdk.http.AbortableInputStream;
+import software.amazon.awssdk.http.HttpStatusFamily;
+import software.amazon.awssdk.http.SdkHttpFullRequest;
+import software.amazon.awssdk.http.SdkHttpFullResponse;
+import software.amazon.awssdk.http.async.AsyncExecuteRequest;
+import software.amazon.awssdk.http.async.SdkAsyncHttpClient;
+import software.amazon.awssdk.http.async.SdkHttpContentPublisher;
+import software.amazon.awssdk.utils.CompletableFutureUtils;
+
+@SdkInternalApi
+final class AsyncHttpRequestHelper {
+
+ private AsyncHttpRequestHelper() {
+ // static utility class
+ }
+
+ public static CompletableFuture sendAsyncMetadataRequest(SdkAsyncHttpClient httpClient,
+ SdkHttpFullRequest baseRequest,
+ CompletableFuture> parentFuture) {
+ return sendAsync(httpClient, baseRequest, AsyncHttpRequestHelper::handleResponse, parentFuture);
+ }
+
+ public static CompletableFuture sendAsyncTokenRequest(SdkAsyncHttpClient httpClient,
+ SdkHttpFullRequest baseRequest) {
+ return sendAsync(httpClient, baseRequest, AsyncHttpRequestHelper::handleTokenResponse, null);
+ }
+
+ private static CompletableFuture sendAsync(SdkAsyncHttpClient client,
+ SdkHttpFullRequest request,
+ HttpResponseHandler handler,
+ CompletableFuture> parentFuture) {
+ SdkHttpContentPublisher requestContentPublisher = new SimpleHttpContentPublisher(request);
+ TransformingAsyncResponseHandler responseHandler =
+ new AsyncResponseHandler<>(handler, Function.identity(), new ExecutionAttributes());
+ CompletableFuture responseHandlerFuture = responseHandler.prepare();
+ AsyncExecuteRequest metadataRequest = AsyncExecuteRequest.builder()
+ .request(request)
+ .requestContentPublisher(requestContentPublisher)
+ .responseHandler(responseHandler)
+ .build();
+ CompletableFuture executeFuture = client.execute(metadataRequest);
+ if (parentFuture != null) {
+ CompletableFutureUtils.forwardExceptionTo(parentFuture, executeFuture);
+ CompletableFutureUtils.forwardExceptionTo(parentFuture, responseHandlerFuture);
+ }
+ return responseHandlerFuture;
+ }
+
+ private static String handleResponse(SdkHttpFullResponse response, ExecutionAttributes executionAttributes) {
+ HttpStatusFamily statusCode = HttpStatusFamily.of(response.statusCode());
+ AbortableInputStream inputStream =
+ response.content().orElseThrow(() -> SdkClientException.create("Unexpected error: empty response content"));
+ String responseContent = uncheckedInputStreamToUtf8(inputStream);
+
+ // non-retryable error
+ if (statusCode.isOneOf(HttpStatusFamily.CLIENT_ERROR)) {
+ throw SdkClientException.builder().message(responseContent).build();
+ }
+
+ // retryable error
+ if (statusCode.isOneOf(HttpStatusFamily.SERVER_ERROR)) {
+ throw RetryableException.create(responseContent);
+ }
+ return responseContent;
+ }
+
+ private static Token handleTokenResponse(SdkHttpFullResponse response, ExecutionAttributes executionAttributes) {
+ String tokenValue = handleResponse(response, executionAttributes);
+ Optional ttl = response.firstMatchingHeader(EC2_METADATA_TOKEN_TTL_HEADER);
+
+ if (!ttl.isPresent()) {
+ throw SdkClientException.create(EC2_METADATA_TOKEN_TTL_HEADER + " header not found in token response");
+ }
+ try {
+ Duration ttlDuration = Duration.ofSeconds(Long.parseLong(ttl.get()));
+ return new Token(tokenValue, ttlDuration);
+ } catch (NumberFormatException nfe) {
+ throw SdkClientException.create(
+ "Invalid token format received from IMDS server. Token received: " + tokenValue, nfe);
+ }
+ }
+}
diff --git a/core/imds/src/main/java/software/amazon/awssdk/imds/internal/AsyncTokenCache.java b/core/imds/src/main/java/software/amazon/awssdk/imds/internal/AsyncTokenCache.java
new file mode 100644
index 000000000000..85c98e0f786e
--- /dev/null
+++ b/core/imds/src/main/java/software/amazon/awssdk/imds/internal/AsyncTokenCache.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.awssdk.imds.internal;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Supplier;
+import software.amazon.awssdk.annotations.SdkInternalApi;
+import software.amazon.awssdk.utils.Logger;
+
+/**
+ * A cache for the IMDS {@link Token}, which can be refreshed through an asynchronous operation.
+ * A call to the {@link AsyncTokenCache#get()} method returns an already completed future if the cached token is still fresh.
+ * If the cached token was expired, the returned future will be completed once the refresh process has been
+ * completed.
+ * In the case where multiple call to get
are made while the token is expired, all CompletableFuture returned
+ * will be completed once the single refresh process completes.
+ *
+ */
+@SdkInternalApi
+final class AsyncTokenCache implements Supplier> {
+
+ private static final Logger log = Logger.loggerFor(AsyncTokenCache.class);
+
+ /**
+ * The currently cached value.
+ * Only modified through synchronized block, under the refreshLock.
+ */
+ private volatile Token cachedToken;
+
+ /**
+ * The asynchronous operation that is used to refresh the token.
+ * The Supplier must not block the current thread and is responsible to start the process that will complete the future.
+ * A call the {@link AsyncTokenCache#get} method does not join or wait for the supplied future to finish, it only refreshes
+ * the token once it finishes.
+ */
+ private final Supplier> supplier;
+
+ /**
+ * The collection of future that are waiting for the refresh call to complete. If a call to {@link AsyncTokenCache#get()}
+ * is made while the token request is happening, a future will be added to this collection. All future in this collection
+ * are completed once the token request is done.
+ * Should only be modified while holding the lock on {@link AsyncTokenCache#refreshLock}.
+ */
+ private Collection> waitingFutures = new ArrayList<>();
+
+ /**
+ * Indicates if the token refresh request is currently running or not.
+ */
+ private final AtomicBoolean refreshRunning = new AtomicBoolean(false);
+
+ private final Object refreshLock = new Object();
+
+ AsyncTokenCache(Supplier> supplier) {
+ this.supplier = supplier;
+ }
+
+ @Override
+ public CompletableFuture get() {
+ Token currentValue = cachedToken;
+ if (!needsRefresh(currentValue)) {
+ log.debug(() -> "IMDS Token is not expired");
+ return CompletableFuture.completedFuture(currentValue);
+ }
+ synchronized (refreshLock) {
+ // Make sure the value wasn't refreshed while we were waiting for the lock.
+ currentValue = cachedToken;
+ if (!needsRefresh(currentValue)) {
+ return CompletableFuture.completedFuture(currentValue);
+ }
+ CompletableFuture result = new CompletableFuture<>();
+ waitingFutures.add(result);
+ if (!refreshRunning.get()) {
+ startRefresh();
+ }
+ return result;
+ }
+ }
+
+ private void startRefresh() {
+ log.debug(() -> "IMDS token expired or null, starting asynchronous refresh.");
+ CompletableFuture tokenRequest = supplier.get();
+ refreshRunning.set(true); // After supplier.get(), in case that throws an exception
+ tokenRequest.whenComplete((token, throwable) -> {
+ Collection> toComplete;
+ synchronized (refreshLock) {
+ // Instead of completing the waiting future while holding the lock, we copy the list reference and
+ // release the lock before completing them. This is just in case someone (naughty) is doing something
+ // blocking on the complete calls. It's not good that they're doing that, but at least
+ // it won't block other threads trying to acquire the lock.
+ toComplete = waitingFutures;
+ waitingFutures = new ArrayList<>();
+ refreshRunning.set(false);
+ if (token != null) {
+ log.debug(() -> "IMDS token refresh completed. Token value: " + token.value());
+ cachedToken = token;
+ } else {
+ log.error(() -> "IMDS token refresh completed with error.", throwable);
+ }
+ }
+
+ toComplete.forEach(future -> {
+ if (throwable == null) {
+ future.complete(token);
+ } else {
+ future.completeExceptionally(throwable);
+ }
+ });
+ });
+ }
+
+ private boolean needsRefresh(Token token) {
+ return token == null || token.isExpired();
+ }
+
+}
diff --git a/core/imds/src/main/java/software/amazon/awssdk/imds/internal/BaseEc2MetadataClient.java b/core/imds/src/main/java/software/amazon/awssdk/imds/internal/BaseEc2MetadataClient.java
new file mode 100644
index 000000000000..915753c00a33
--- /dev/null
+++ b/core/imds/src/main/java/software/amazon/awssdk/imds/internal/BaseEc2MetadataClient.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.awssdk.imds.internal;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.net.URI;
+import java.time.Duration;
+import java.time.temporal.ChronoUnit;
+import software.amazon.awssdk.annotations.SdkInternalApi;
+import software.amazon.awssdk.core.exception.RetryableException;
+import software.amazon.awssdk.core.retry.RetryPolicyContext;
+import software.amazon.awssdk.http.AbortableInputStream;
+import software.amazon.awssdk.http.SdkHttpConfigurationOption;
+import software.amazon.awssdk.imds.Ec2MetadataRetryPolicy;
+import software.amazon.awssdk.imds.EndpointMode;
+import software.amazon.awssdk.utils.AttributeMap;
+import software.amazon.awssdk.utils.IoUtils;
+import software.amazon.awssdk.utils.Logger;
+import software.amazon.awssdk.utils.Validate;
+
+@SdkInternalApi
+public abstract class BaseEc2MetadataClient {
+
+ protected static final Duration DEFAULT_TOKEN_TTL = Duration.of(21_600, ChronoUnit.SECONDS);
+ protected static final AttributeMap IMDS_HTTP_DEFAULTS =
+ AttributeMap.builder()
+ .put(SdkHttpConfigurationOption.CONNECTION_TIMEOUT, Duration.ofSeconds(1))
+ .put(SdkHttpConfigurationOption.READ_TIMEOUT, Duration.ofSeconds(1))
+ .build();
+
+ private static final Logger log = Logger.loggerFor(BaseEc2MetadataClient.class);
+
+ protected final Ec2MetadataRetryPolicy retryPolicy;
+ protected final URI endpoint;
+ protected final RequestMarshaller requestMarshaller;
+ protected final Duration tokenTtl;
+
+ private BaseEc2MetadataClient(Ec2MetadataRetryPolicy retryPolicy, Duration tokenTtl, URI endpoint,
+ EndpointMode endpointMode) {
+ this.retryPolicy = Validate.getOrDefault(retryPolicy, Ec2MetadataRetryPolicy.builder()::build);
+ this.tokenTtl = Validate.getOrDefault(tokenTtl, () -> DEFAULT_TOKEN_TTL);
+ this.endpoint = getEndpoint(endpoint, endpointMode);
+ this.requestMarshaller = new RequestMarshaller(this.endpoint);
+ }
+
+ protected BaseEc2MetadataClient(DefaultEc2MetadataClient.Ec2MetadataBuilder builder) {
+ this(builder.getRetryPolicy(), builder.getTokenTtl(), builder.getEndpoint(), builder.getEndpointMode());
+ }
+
+ protected BaseEc2MetadataClient(DefaultEc2MetadataAsyncClient.Ec2MetadataAsyncBuilder builder) {
+ this(builder.getRetryPolicy(), builder.getTokenTtl(), builder.getEndpoint(), builder.getEndpointMode());
+ }
+
+ private URI getEndpoint(URI builderEndpoint, EndpointMode builderEndpointMode) {
+ Validate.mutuallyExclusive("Only one of 'endpoint' or 'endpointMode' must be specified, but not both",
+ builderEndpoint, builderEndpointMode);
+ if (builderEndpoint != null) {
+ return builderEndpoint;
+ }
+ if (builderEndpointMode != null) {
+ return URI.create(Ec2MetadataEndpointProvider.instance().resolveEndpoint(builderEndpointMode));
+ }
+ EndpointMode resolvedEndpointMode = Ec2MetadataEndpointProvider.instance().resolveEndpointMode();
+ return URI.create(Ec2MetadataEndpointProvider.instance().resolveEndpoint(resolvedEndpointMode));
+ }
+
+ protected static String uncheckedInputStreamToUtf8(AbortableInputStream inputStream) {
+ try {
+ return IoUtils.toUtf8String(inputStream);
+ } catch (IOException ioe) {
+ throw new UncheckedIOException(ioe);
+ } finally {
+ IoUtils.closeQuietly(inputStream, log.logger());
+ }
+ }
+
+ protected boolean shouldRetry(RetryPolicyContext retryPolicyContext, Throwable error) {
+ boolean maxAttemptReached = retryPolicyContext.retriesAttempted() >= retryPolicy.numRetries();
+ if (maxAttemptReached) {
+ return false;
+ }
+ return error instanceof RetryableException || error.getCause() instanceof RetryableException;
+ }
+
+}
diff --git a/core/imds/src/main/java/software/amazon/awssdk/imds/internal/DefaultEc2MetadataAsyncClient.java b/core/imds/src/main/java/software/amazon/awssdk/imds/internal/DefaultEc2MetadataAsyncClient.java
new file mode 100644
index 000000000000..cb8ca87a32bb
--- /dev/null
+++ b/core/imds/src/main/java/software/amazon/awssdk/imds/internal/DefaultEc2MetadataAsyncClient.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.awssdk.imds.internal;
+
+import static software.amazon.awssdk.imds.internal.AsyncHttpRequestHelper.sendAsyncTokenRequest;
+
+import java.net.URI;
+import java.time.Duration;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import software.amazon.awssdk.annotations.Immutable;
+import software.amazon.awssdk.annotations.SdkInternalApi;
+import software.amazon.awssdk.annotations.ThreadSafe;
+import software.amazon.awssdk.core.exception.SdkClientException;
+import software.amazon.awssdk.core.internal.http.loader.DefaultSdkAsyncHttpClientBuilder;
+import software.amazon.awssdk.core.retry.RetryPolicyContext;
+import software.amazon.awssdk.http.SdkHttpFullRequest;
+import software.amazon.awssdk.http.async.SdkAsyncHttpClient;
+import software.amazon.awssdk.imds.Ec2MetadataAsyncClient;
+import software.amazon.awssdk.imds.Ec2MetadataResponse;
+import software.amazon.awssdk.imds.Ec2MetadataRetryPolicy;
+import software.amazon.awssdk.imds.EndpointMode;
+import software.amazon.awssdk.utils.CompletableFutureUtils;
+import software.amazon.awssdk.utils.Either;
+import software.amazon.awssdk.utils.Logger;
+import software.amazon.awssdk.utils.ThreadFactoryBuilder;
+import software.amazon.awssdk.utils.Validate;
+
+@SdkInternalApi
+@Immutable
+@ThreadSafe
+public final class DefaultEc2MetadataAsyncClient extends BaseEc2MetadataClient implements Ec2MetadataAsyncClient {
+
+ private static final Logger log = Logger.loggerFor(DefaultEc2MetadataClient.class);
+ private static final int DEFAULT_RETRY_THREAD_POOL_SIZE = 3;
+
+ private final SdkAsyncHttpClient httpClient;
+ private final ScheduledExecutorService asyncRetryScheduler;
+ private final boolean httpClientIsInternal;
+ private final boolean retryExecutorIsInternal;
+ private final AsyncTokenCache tokenCache;
+
+ private DefaultEc2MetadataAsyncClient(Ec2MetadataAsyncBuilder builder) {
+ super(builder);
+
+ Validate.isTrue(builder.httpClient == null || builder.httpClientBuilder == null,
+ "The httpClient and the httpClientBuilder can't both be configured.");
+ this.httpClient = Either
+ .fromNullable(builder.httpClient, builder.httpClientBuilder)
+ .map(e -> e.map(Function.identity(), SdkAsyncHttpClient.Builder::build))
+ .orElseGet(() -> new DefaultSdkAsyncHttpClientBuilder().buildWithDefaults(IMDS_HTTP_DEFAULTS));
+ this.httpClientIsInternal = builder.httpClient == null;
+
+ this.asyncRetryScheduler = Validate.getOrDefault(
+ builder.scheduledExecutorService,
+ () -> {
+ ThreadFactory threadFactory =
+ new ThreadFactoryBuilder().threadNamePrefix("IMDS-ScheduledExecutor").build();
+ return Executors.newScheduledThreadPool(DEFAULT_RETRY_THREAD_POOL_SIZE, threadFactory);
+ });
+ this.retryExecutorIsInternal = builder.scheduledExecutorService == null;
+ Supplier> tokenSupplier = () -> {
+ SdkHttpFullRequest baseTokenRequest = requestMarshaller.createTokenRequest(tokenTtl);
+ return sendAsyncTokenRequest(httpClient, baseTokenRequest);
+ };
+
+ this.tokenCache = new AsyncTokenCache(tokenSupplier);
+ }
+
+ public static Ec2MetadataAsyncClient.Builder builder() {
+ return new DefaultEc2MetadataAsyncClient.Ec2MetadataAsyncBuilder();
+ }
+
+ @Override
+ public CompletableFuture get(String path) {
+ CompletableFuture returnFuture = new CompletableFuture<>();
+ get(path, RetryPolicyContext.builder().retriesAttempted(0).build(), returnFuture);
+ return returnFuture;
+ }
+
+ private void get(String path, RetryPolicyContext retryPolicyContext, CompletableFuture returnFuture) {
+ CompletableFuture tokenFuture = tokenCache.get();
+
+ CompletableFuture result = tokenFuture.thenCompose(token -> {
+ SdkHttpFullRequest baseMetadataRequest = requestMarshaller.createDataRequest(path, token.value(), tokenTtl);
+ return AsyncHttpRequestHelper.sendAsyncMetadataRequest(httpClient, baseMetadataRequest, returnFuture);
+ }).thenApply(Ec2MetadataResponse::create);
+
+ CompletableFutureUtils.forwardExceptionTo(returnFuture, result);
+
+ result.whenComplete((response, error) -> {
+ if (response != null) {
+ returnFuture.complete(response);
+ return;
+ }
+ if (!shouldRetry(retryPolicyContext, error)) {
+ returnFuture.completeExceptionally(error);
+ return;
+ }
+ int newAttempt = retryPolicyContext.retriesAttempted() + 1;
+ log.debug(() -> "Retrying request: Attempt " + newAttempt);
+ RetryPolicyContext newContext =
+ RetryPolicyContext.builder()
+ .retriesAttempted(newAttempt)
+ .exception(SdkClientException.create(error.getMessage(), error))
+ .build();
+ scheduledRetryAttempt(() -> get(path, newContext, returnFuture), newContext);
+ });
+ }
+
+ private void scheduledRetryAttempt(Runnable runnable, RetryPolicyContext retryPolicyContext) {
+ Duration retryDelay = retryPolicy.backoffStrategy().computeDelayBeforeNextRetry(retryPolicyContext);
+ Executor retryExecutor = retryAttempt ->
+ asyncRetryScheduler.schedule(retryAttempt, retryDelay.toMillis(), TimeUnit.MILLISECONDS);
+ CompletableFuture.runAsync(runnable, retryExecutor);
+ }
+
+ @Override
+ public void close() {
+ if (httpClientIsInternal) {
+ httpClient.close();
+ }
+ if (retryExecutorIsInternal) {
+ asyncRetryScheduler.shutdown();
+ }
+ }
+
+ protected static final class Ec2MetadataAsyncBuilder implements Ec2MetadataAsyncClient.Builder {
+
+ private Ec2MetadataRetryPolicy retryPolicy;
+
+ private URI endpoint;
+
+ private Duration tokenTtl;
+
+ private EndpointMode endpointMode;
+
+ private SdkAsyncHttpClient httpClient;
+
+ private SdkAsyncHttpClient.Builder> httpClientBuilder;
+
+ private ScheduledExecutorService scheduledExecutorService;
+
+ private Ec2MetadataAsyncBuilder() {
+ }
+
+ @Override
+ public Ec2MetadataAsyncBuilder retryPolicy(Ec2MetadataRetryPolicy retryPolicy) {
+ this.retryPolicy = retryPolicy;
+ return this;
+ }
+
+ @Override
+ public Ec2MetadataAsyncBuilder retryPolicy(Consumer builderConsumer) {
+ Validate.notNull(builderConsumer, "builderConsumer must not be null");
+ Ec2MetadataRetryPolicy.Builder builder = Ec2MetadataRetryPolicy.builder();
+ builderConsumer.accept(builder);
+ this.retryPolicy = builder.build();
+ return this;
+ }
+
+ @Override
+ public Ec2MetadataAsyncBuilder endpoint(URI endpoint) {
+ this.endpoint = endpoint;
+ return this;
+ }
+
+ @Override
+ public Ec2MetadataAsyncBuilder tokenTtl(Duration tokenTtl) {
+ this.tokenTtl = tokenTtl;
+ return this;
+ }
+
+ @Override
+ public Ec2MetadataAsyncBuilder endpointMode(EndpointMode endpointMode) {
+ this.endpointMode = endpointMode;
+ return this;
+ }
+
+ @Override
+ public Ec2MetadataAsyncBuilder httpClient(SdkAsyncHttpClient httpClient) {
+ this.httpClient = httpClient;
+ return this;
+ }
+
+ @Override
+ public Builder httpClient(SdkAsyncHttpClient.Builder> builder) {
+ this.httpClientBuilder = builder;
+ return this;
+ }
+
+ @Override
+ public Ec2MetadataAsyncBuilder scheduledExecutorService(ScheduledExecutorService scheduledExecutorService) {
+ this.scheduledExecutorService = scheduledExecutorService;
+ return this;
+ }
+
+ public Ec2MetadataRetryPolicy getRetryPolicy() {
+ return this.retryPolicy;
+ }
+
+ public URI getEndpoint() {
+ return this.endpoint;
+ }
+
+ public Duration getTokenTtl() {
+ return this.tokenTtl;
+ }
+
+ public EndpointMode getEndpointMode() {
+ return this.endpointMode;
+ }
+
+ @Override
+ public Ec2MetadataAsyncClient build() {
+ return new DefaultEc2MetadataAsyncClient(this);
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/imds/src/main/java/software/amazon/awssdk/imds/internal/DefaultEc2MetadataClient.java b/core/imds/src/main/java/software/amazon/awssdk/imds/internal/DefaultEc2MetadataClient.java
new file mode 100644
index 000000000000..6129dd7b82f0
--- /dev/null
+++ b/core/imds/src/main/java/software/amazon/awssdk/imds/internal/DefaultEc2MetadataClient.java
@@ -0,0 +1,324 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.awssdk.imds.internal;
+
+import static software.amazon.awssdk.imds.internal.RequestMarshaller.EC2_METADATA_TOKEN_TTL_HEADER;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.net.URI;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import software.amazon.awssdk.annotations.Immutable;
+import software.amazon.awssdk.annotations.SdkInternalApi;
+import software.amazon.awssdk.annotations.ThreadSafe;
+import software.amazon.awssdk.core.exception.RetryableException;
+import software.amazon.awssdk.core.exception.SdkClientException;
+import software.amazon.awssdk.core.internal.http.loader.DefaultSdkHttpClientBuilder;
+import software.amazon.awssdk.core.retry.RetryPolicyContext;
+import software.amazon.awssdk.http.AbortableInputStream;
+import software.amazon.awssdk.http.HttpExecuteRequest;
+import software.amazon.awssdk.http.HttpExecuteResponse;
+import software.amazon.awssdk.http.HttpStatusFamily;
+import software.amazon.awssdk.http.SdkHttpClient;
+import software.amazon.awssdk.imds.Ec2MetadataClient;
+import software.amazon.awssdk.imds.Ec2MetadataResponse;
+import software.amazon.awssdk.imds.Ec2MetadataRetryPolicy;
+import software.amazon.awssdk.imds.EndpointMode;
+import software.amazon.awssdk.utils.Either;
+import software.amazon.awssdk.utils.Logger;
+import software.amazon.awssdk.utils.Validate;
+import software.amazon.awssdk.utils.cache.CachedSupplier;
+import software.amazon.awssdk.utils.cache.RefreshResult;
+
+/**
+ * An Implementation of the Ec2Metadata Interface.
+ */
+@SdkInternalApi
+@Immutable
+@ThreadSafe
+public final class DefaultEc2MetadataClient extends BaseEc2MetadataClient implements Ec2MetadataClient {
+
+ private static final Logger log = Logger.loggerFor(DefaultEc2MetadataClient.class);
+
+ private final SdkHttpClient httpClient;
+ private final Supplier tokenCache;
+ private final boolean httpClientIsInternal;
+
+ private DefaultEc2MetadataClient(Ec2MetadataBuilder builder) {
+ super(builder);
+
+ Validate.isTrue(builder.httpClient == null || builder.httpClientBuilder == null,
+ "The httpClient and the httpClientBuilder can't both be configured.");
+ this.httpClient = Either
+ .fromNullable(builder.httpClient, builder.httpClientBuilder)
+ .map(e -> e.map(Function.identity(), SdkHttpClient.Builder::build))
+ .orElseGet(() -> new DefaultSdkHttpClientBuilder().buildWithDefaults(IMDS_HTTP_DEFAULTS));
+ this.httpClientIsInternal = builder.httpClient == null;
+
+ this.tokenCache = CachedSupplier.builder(() -> RefreshResult.builder(this.getToken())
+ .staleTime(Instant.now().plus(tokenTtl))
+ .build())
+ .build();
+ }
+
+ @Override
+ public void close() {
+ if (httpClientIsInternal) {
+ httpClient.close();
+ }
+ }
+
+ public static Ec2MetadataBuilder builder() {
+ return new DefaultEc2MetadataClient.Ec2MetadataBuilder();
+ }
+
+ /**
+ * Gets the specified instance metadata value by the given path. Will retry base on the
+ * {@link Ec2MetadataRetryPolicy retry policy} provided, in the case of an IOException during request. Will not retry on
+ * SdkClientException, like 4XX HTTP error.
+ *
+ * @param path Input path of the resource to get.
+ * @return Instance metadata value as part of MetadataResponse Object
+ * @throws SdkClientException if the request for a token or the request for the Metadata does not have a 2XX SUCCESS response,
+ * if the maximum number of retries is reached, or if another IOException is thrown during the
+ * request.
+ */
+ @Override
+ public Ec2MetadataResponse get(String path) {
+ Throwable lastCause = null;
+ // 3 retries means 4 total attempts
+ Token token = null;
+ for (int attempt = 0; attempt < retryPolicy.numRetries() + 1; attempt++) {
+ RetryPolicyContext retryPolicyContext = RetryPolicyContext.builder().retriesAttempted(attempt).build();
+ try {
+ if (token == null || token.isExpired()) {
+ token = tokenCache.get();
+ }
+ return sendRequest(path, token.value());
+ } catch (UncheckedIOException | RetryableException e) {
+ lastCause = e;
+ int currentTry = attempt;
+ log.debug(() -> "Error while executing EC2Metadata request, attempting retry. Current attempt: " + currentTry);
+ } catch (SdkClientException sdkClientException) {
+ int totalTries = attempt + 1;
+ log.debug(() -> String.format("Error while executing EC2Metadata request. Total attempts: %d. %s",
+ totalTries,
+ sdkClientException.getMessage()));
+ throw sdkClientException;
+ } catch (IOException ioe) {
+ lastCause = new UncheckedIOException(ioe);
+ int currentTry = attempt;
+ log.debug(() -> "Error while executing EC2Metadata request, attempting retry. Current attempt: " + currentTry);
+ }
+ pauseBeforeRetryIfNeeded(retryPolicyContext);
+ }
+
+ SdkClientException.Builder sdkClientExceptionBuilder = SdkClientException
+ .builder()
+ .message("Exceeded maximum number of retries. Total retry attempts: " + retryPolicy.numRetries());
+ if (lastCause != null) {
+ String msg = sdkClientExceptionBuilder.message();
+ sdkClientExceptionBuilder.cause(lastCause).message(msg);
+ }
+ throw sdkClientExceptionBuilder.build();
+ }
+
+ private Ec2MetadataResponse sendRequest(String path, String token) throws IOException {
+
+ HttpExecuteRequest httpExecuteRequest =
+ HttpExecuteRequest.builder()
+ .request(requestMarshaller.createDataRequest(path, token, tokenTtl))
+ .build();
+ HttpExecuteResponse response = httpClient.prepareRequest(httpExecuteRequest).call();
+
+ int statusCode = response.httpResponse().statusCode();
+ Optional responseBody = response.responseBody();
+
+ if (HttpStatusFamily.of(statusCode).isOneOf(HttpStatusFamily.SERVER_ERROR)) {
+ responseBody.map(BaseEc2MetadataClient::uncheckedInputStreamToUtf8)
+ .ifPresent(str -> log.debug(() -> "Metadata request response body: " + str));
+ throw RetryableException
+ .builder()
+ .message(String.format("The requested metadata at path '%s' returned Http code %s", path, statusCode))
+ .build();
+ }
+
+ if (!HttpStatusFamily.of(statusCode).isOneOf(HttpStatusFamily.SUCCESSFUL)) {
+ responseBody.map(BaseEc2MetadataClient::uncheckedInputStreamToUtf8)
+ .ifPresent(str -> log.debug(() -> "Metadata request response body: " + str));
+ throw SdkClientException
+ .builder()
+ .message(String.format("The requested metadata at path '%s' returned Http code %s", path, statusCode))
+ .build();
+ }
+
+ AbortableInputStream abortableInputStream = responseBody.orElseThrow(
+ SdkClientException.builder().message("Response body empty with Status Code " + statusCode)::build);
+ String data = uncheckedInputStreamToUtf8(abortableInputStream);
+ return Ec2MetadataResponse.create(data);
+ }
+
+ private void pauseBeforeRetryIfNeeded(RetryPolicyContext retryPolicyContext) {
+ long backoffTimeMillis = retryPolicy.backoffStrategy()
+ .computeDelayBeforeNextRetry(retryPolicyContext)
+ .toMillis();
+ if (backoffTimeMillis > 0) {
+ try {
+ TimeUnit.MILLISECONDS.sleep(backoffTimeMillis);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw SdkClientException.builder().message("Thread interrupted while trying to sleep").cause(e).build();
+ }
+ }
+ }
+
+ private Token getToken() {
+ HttpExecuteRequest httpExecuteRequest = HttpExecuteRequest.builder()
+ .request(requestMarshaller.createTokenRequest(tokenTtl))
+ .build();
+ HttpExecuteResponse response = null;
+ try {
+ response = httpClient.prepareRequest(httpExecuteRequest).call();
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+
+ int statusCode = response.httpResponse().statusCode();
+
+ if (HttpStatusFamily.of(statusCode).isOneOf(HttpStatusFamily.SERVER_ERROR)) {
+ response.responseBody().map(BaseEc2MetadataClient::uncheckedInputStreamToUtf8)
+ .ifPresent(str -> log.debug(() -> "Metadata request response body: " + str));
+ throw RetryableException.builder()
+ .message("Could not retrieve token, " + statusCode + " error occurred").build();
+ }
+
+ if (!HttpStatusFamily.of(statusCode).isOneOf(HttpStatusFamily.SUCCESSFUL)) {
+ response.responseBody().map(BaseEc2MetadataClient::uncheckedInputStreamToUtf8)
+ .ifPresent(body -> log.debug(() -> "Token request response body: " + body));
+ throw SdkClientException.builder()
+ .message("Could not retrieve token, " + statusCode + " error occurred.")
+ .build();
+ }
+
+ String ttl = response.httpResponse()
+ .firstMatchingHeader(EC2_METADATA_TOKEN_TTL_HEADER)
+ .orElseThrow(() -> SdkClientException
+ .builder()
+ .message(EC2_METADATA_TOKEN_TTL_HEADER + " header not found in token response")
+ .build());
+ Duration ttlDuration;
+ try {
+ ttlDuration = Duration.ofSeconds(Long.parseLong(ttl));
+ } catch (NumberFormatException nfe) {
+ throw SdkClientException.create("Invalid token format received from IMDS server", nfe);
+ }
+
+ AbortableInputStream abortableInputStream = response.responseBody().orElseThrow(
+ SdkClientException.builder().message("Empty response body")::build);
+
+ String value = uncheckedInputStreamToUtf8(abortableInputStream);
+ return new Token(value, ttlDuration);
+ }
+
+ protected static final class Ec2MetadataBuilder implements Ec2MetadataClient.Builder {
+
+ private Ec2MetadataRetryPolicy retryPolicy;
+
+ private URI endpoint;
+
+ private Duration tokenTtl;
+
+ private EndpointMode endpointMode;
+
+ private SdkHttpClient httpClient;
+
+ private SdkHttpClient.Builder> httpClientBuilder;
+
+ private Ec2MetadataBuilder() {
+ }
+
+ @Override
+ public Ec2MetadataBuilder retryPolicy(Ec2MetadataRetryPolicy retryPolicy) {
+ this.retryPolicy = retryPolicy;
+ return this;
+ }
+
+ @Override
+ public Builder retryPolicy(Consumer builderConsumer) {
+ Validate.notNull(builderConsumer, "builderConsumer must not be null");
+ Ec2MetadataRetryPolicy.Builder builder = Ec2MetadataRetryPolicy.builder();
+ builderConsumer.accept(builder);
+ return retryPolicy(builder.build());
+ }
+
+ @Override
+ public Ec2MetadataBuilder endpoint(URI endpoint) {
+ this.endpoint = endpoint;
+ return this;
+ }
+
+ @Override
+ public Ec2MetadataBuilder tokenTtl(Duration tokenTtl) {
+ this.tokenTtl = tokenTtl;
+ return this;
+ }
+
+ @Override
+ public Ec2MetadataBuilder endpointMode(EndpointMode endpointMode) {
+ this.endpointMode = endpointMode;
+ return this;
+ }
+
+ @Override
+ public Ec2MetadataBuilder httpClient(SdkHttpClient httpClient) {
+ this.httpClient = httpClient;
+ return this;
+ }
+
+ @Override
+ public Builder httpClient(SdkHttpClient.Builder> builder) {
+ this.httpClientBuilder = builder;
+ return this;
+ }
+
+ public Ec2MetadataRetryPolicy getRetryPolicy() {
+ return this.retryPolicy;
+ }
+
+ public URI getEndpoint() {
+ return this.endpoint;
+ }
+
+ public Duration getTokenTtl() {
+ return this.tokenTtl;
+ }
+
+ public EndpointMode getEndpointMode() {
+ return this.endpointMode;
+ }
+
+ @Override
+ public Ec2MetadataClient build() {
+ return new DefaultEc2MetadataClient(this);
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/imds/src/main/java/software/amazon/awssdk/imds/internal/Ec2MetadataEndpointProvider.java b/core/imds/src/main/java/software/amazon/awssdk/imds/internal/Ec2MetadataEndpointProvider.java
new file mode 100644
index 000000000000..95b91204a014
--- /dev/null
+++ b/core/imds/src/main/java/software/amazon/awssdk/imds/internal/Ec2MetadataEndpointProvider.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.awssdk.imds.internal;
+
+import static software.amazon.awssdk.imds.EndpointMode.IPV4;
+import static software.amazon.awssdk.imds.EndpointMode.IPV6;
+
+import java.util.EnumMap;
+import java.util.Optional;
+import java.util.function.Supplier;
+import software.amazon.awssdk.annotations.SdkInternalApi;
+import software.amazon.awssdk.core.SdkSystemSetting;
+import software.amazon.awssdk.imds.EndpointMode;
+import software.amazon.awssdk.profiles.Profile;
+import software.amazon.awssdk.profiles.ProfileFile;
+import software.amazon.awssdk.profiles.ProfileFileSystemSetting;
+import software.amazon.awssdk.profiles.ProfileProperty;
+import software.amazon.awssdk.utils.Validate;
+
+/**
+ * Endpoint Provider Class which contains methods for endpoint resolution.
+ */
+@SdkInternalApi
+public final class Ec2MetadataEndpointProvider {
+
+ private static final Ec2MetadataEndpointProvider DEFAULT_ENDPOINT_PROVIDER = builder().build();
+
+ private static final EnumMap DEFAULT_ENDPOINT_MODE;
+
+ static {
+ DEFAULT_ENDPOINT_MODE = new EnumMap<>(EndpointMode.class);
+ DEFAULT_ENDPOINT_MODE.put(IPV4, "http://169.254.169.254");
+ DEFAULT_ENDPOINT_MODE.put(IPV6, "http://[fd00:ec2::254]");
+ }
+
+ private final Supplier profileFile;
+ private final String profileName;
+
+ private Ec2MetadataEndpointProvider(Builder builder) {
+ this.profileFile = builder.profileFile;
+ this.profileName = builder.profileName;
+ }
+
+ public static Ec2MetadataEndpointProvider instance() {
+ return DEFAULT_ENDPOINT_PROVIDER;
+ }
+
+ /**
+ * Resolve the endpoint to be used for the {@link DefaultEc2MetadataClient} client. Users may manually provide an endpoint
+ * through the {@code AWS_EC2_METADATA_SERVICE_ENDPOINT} environment variable or the {@code ec2_metadata_service_endpoint}
+ * key in their aws config file.
+ * If an endpoint is specified is this manner, use it. If no values are provided, the defaults to:
+ *
+ * - If endpoint mode is set to IPv4: {@code "http://169.254.169.254"}
+ * - If endpoint mode is set to IPv6: {@code "http://[fd00:ec2::254]"}
+ *
+ * (the default endpoint mode is IPV4).
+ * @param endpointMode Used only if an endpoint value is not specified. If so, this method will use the endpointMode to
+ * choose the default value to return.
+ * @return the String representing the endpoint to be used,
+ */
+ public String resolveEndpoint(EndpointMode endpointMode) {
+ Optional endpointFromSystem = SdkSystemSetting.AWS_EC2_METADATA_SERVICE_ENDPOINT.getNonDefaultStringValue();
+ if (endpointFromSystem.isPresent()) {
+ return stripEndingSlash(endpointFromSystem.get());
+ }
+
+ Optional endpointFromConfigProfile = resolveProfile()
+ .flatMap(profile -> profile.property(ProfileProperty.EC2_METADATA_SERVICE_ENDPOINT));
+ if (endpointFromConfigProfile.isPresent()) {
+ return stripEndingSlash(endpointFromConfigProfile.get());
+ }
+
+ Validate.notNull(endpointMode, "endpointMode must not be null.");
+ return endpointFromConfigProfile.orElseGet(() -> DEFAULT_ENDPOINT_MODE.get(endpointMode));
+ }
+
+ private static String stripEndingSlash(String uri) {
+ return uri.endsWith("/")
+ ? uri.substring(0, uri.length() - 1)
+ : uri;
+ }
+
+ public EndpointMode resolveEndpointMode() {
+ Optional systemEndpointMode = SdkSystemSetting.AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE.getNonDefaultStringValue();
+ if (systemEndpointMode.isPresent()) {
+ return EndpointMode.fromValue(systemEndpointMode.get());
+ }
+
+ Optional configFileEndPointMode = resolveProfile()
+ .flatMap(p -> p.property(ProfileProperty.EC2_METADATA_SERVICE_ENDPOINT_MODE))
+ .map(EndpointMode::fromValue);
+ if (configFileEndPointMode.isPresent()) {
+ return configFileEndPointMode.get();
+ }
+
+ String defaultSystemEndpointMode = SdkSystemSetting.AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE.defaultValue();
+ return EndpointMode.fromValue(defaultSystemEndpointMode);
+ }
+
+ public Optional resolveProfile() {
+ String profileNameToUse = profileName == null
+ ? ProfileFileSystemSetting.AWS_PROFILE.getStringValueOrThrow()
+ : profileName;
+ ProfileFile profileFileToUse = profileFile.get();
+ return profileFileToUse.profile(profileNameToUse);
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static final class Builder {
+ private Supplier profileFile;
+ private String profileName;
+
+ private Builder() {
+ this.profileFile = ProfileFile::defaultProfileFile;
+ }
+
+ public Builder profileFile(Supplier profileFile) {
+ Validate.notNull(profileFile, "profileFile Supplier must not be null");
+ this.profileFile = profileFile;
+ return this;
+ }
+
+ public Builder profileName(String profileName) {
+ this.profileName = profileName;
+ return this;
+ }
+
+ public Ec2MetadataEndpointProvider build() {
+ return new Ec2MetadataEndpointProvider(this);
+ }
+ }
+
+}
diff --git a/core/imds/src/main/java/software/amazon/awssdk/imds/internal/RequestMarshaller.java b/core/imds/src/main/java/software/amazon/awssdk/imds/internal/RequestMarshaller.java
new file mode 100644
index 000000000000..04358583ef3e
--- /dev/null
+++ b/core/imds/src/main/java/software/amazon/awssdk/imds/internal/RequestMarshaller.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.awssdk.imds.internal;
+
+import java.net.URI;
+import java.time.Duration;
+import software.amazon.awssdk.annotations.SdkInternalApi;
+import software.amazon.awssdk.core.util.SdkUserAgent;
+import software.amazon.awssdk.http.SdkHttpFullRequest;
+import software.amazon.awssdk.http.SdkHttpMethod;
+
+/**
+ * Class to parse the parameters to a SdkHttpRequest, make the call to the endpoint and send the HttpExecuteResponse
+ * to the DefaultEc2Metadata class for further processing.
+ */
+@SdkInternalApi
+public class RequestMarshaller {
+
+ public static final String TOKEN_RESOURCE_PATH = "/latest/api/token";
+
+ public static final String TOKEN_HEADER = "x-aws-ec2-metadata-token";
+
+ public static final String EC2_METADATA_TOKEN_TTL_HEADER = "x-aws-ec2-metadata-token-ttl-seconds";
+
+ public static final String USER_AGENT = "user_agent";
+
+ public static final String ACCEPT = "Accept";
+
+ public static final String CONNECTION = "connection";
+
+ private final URI basePath;
+ private final URI tokenPath;
+
+ public RequestMarshaller(URI basePath) {
+ this.basePath = basePath;
+ this.tokenPath = URI.create(basePath + TOKEN_RESOURCE_PATH);
+ }
+
+ public SdkHttpFullRequest createTokenRequest(Duration tokenTtl) {
+ return defaulttHttpBuilder()
+ .method(SdkHttpMethod.PUT)
+ .uri(tokenPath)
+ .putHeader(EC2_METADATA_TOKEN_TTL_HEADER, String.valueOf(tokenTtl.getSeconds()))
+ .build();
+ }
+
+ public SdkHttpFullRequest createDataRequest(String path, String token, Duration tokenTtl) {
+ URI resourcePath = URI.create(basePath + path);
+ return defaulttHttpBuilder()
+ .method(SdkHttpMethod.GET)
+ .uri(resourcePath)
+ .putHeader(EC2_METADATA_TOKEN_TTL_HEADER, String.valueOf(tokenTtl.getSeconds()))
+ .putHeader(TOKEN_HEADER, token)
+ .build();
+ }
+
+ private SdkHttpFullRequest.Builder defaulttHttpBuilder() {
+ return SdkHttpFullRequest.builder()
+ .putHeader(USER_AGENT, SdkUserAgent.create().userAgent())
+ .putHeader(ACCEPT, "*/*")
+ .putHeader(CONNECTION, "keep-alive");
+ }
+}
diff --git a/core/imds/src/main/java/software/amazon/awssdk/imds/internal/Token.java b/core/imds/src/main/java/software/amazon/awssdk/imds/internal/Token.java
new file mode 100644
index 000000000000..1a54fedbc840
--- /dev/null
+++ b/core/imds/src/main/java/software/amazon/awssdk/imds/internal/Token.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.awssdk.imds.internal;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Objects;
+import software.amazon.awssdk.annotations.Immutable;
+import software.amazon.awssdk.annotations.SdkInternalApi;
+import software.amazon.awssdk.utils.ToString;
+
+@SdkInternalApi
+@Immutable
+public final class Token {
+ private final String value;
+ private final Duration ttl;
+ private final Instant createdTime;
+
+ public Token(String value, Duration ttl) {
+ this.value = value;
+ this.ttl = ttl;
+ this.createdTime = Instant.now();
+ }
+
+ public String value() {
+ return value;
+ }
+
+ public Duration ttl() {
+ return ttl;
+ }
+
+ public Instant createdTime() {
+ return createdTime;
+ }
+
+ public boolean isExpired() {
+ return Instant.now().isAfter(createdTime.plus(ttl));
+ }
+
+ @Override
+ public String toString() {
+ return ToString.builder("Token")
+ .add("value", value)
+ .add("ttl", ttl)
+ .add("createdTime", createdTime)
+ .build();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ Token token = (Token) o;
+
+ if (!Objects.equals(value, token.value)) {
+ return false;
+ }
+ if (!Objects.equals(ttl, token.ttl)) {
+ return false;
+ }
+ return Objects.equals(createdTime, token.createdTime);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = value != null ? value.hashCode() : 0;
+ result = 31 * result + (ttl != null ? ttl.hashCode() : 0);
+ result = 31 * result + (createdTime != null ? createdTime.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/core/imds/src/main/java/software/amazon/awssdk/imds/internal/unmarshall/document/DocumentUnmarshaller.java b/core/imds/src/main/java/software/amazon/awssdk/imds/internal/unmarshall/document/DocumentUnmarshaller.java
new file mode 100644
index 000000000000..16db2588a327
--- /dev/null
+++ b/core/imds/src/main/java/software/amazon/awssdk/imds/internal/unmarshall/document/DocumentUnmarshaller.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.awssdk.imds.internal.unmarshall.document;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import software.amazon.awssdk.annotations.SdkInternalApi;
+import software.amazon.awssdk.core.document.Document;
+import software.amazon.awssdk.protocols.jsoncore.JsonNode;
+import software.amazon.awssdk.protocols.jsoncore.JsonNodeVisitor;
+
+@SdkInternalApi
+public final class DocumentUnmarshaller implements JsonNodeVisitor {
+ @Override
+ public Document visitNull() {
+ return Document.fromNull();
+ }
+
+ @Override
+ public Document visitBoolean(boolean bool) {
+ return Document.fromBoolean(bool);
+ }
+
+ @Override
+ public Document visitNumber(String number) {
+ return Document.fromNumber(number);
+ }
+
+ @Override
+ public Document visitString(String string) {
+ return Document.fromString(string);
+ }
+
+ @Override
+ public Document visitArray(List array) {
+ return Document.fromList(array.stream()
+ .map(node -> node.visit(this))
+ .collect(Collectors.toList()));
+ }
+
+ @Override
+ public Document visitObject(Map object) {
+ return Document.fromMap(object.entrySet()
+ .stream().collect(Collectors.toMap(Map.Entry::getKey,
+ entry -> entry.getValue().visit(this),
+ (left, right) -> left,
+ LinkedHashMap::new)));
+ }
+
+ @Override
+ public Document visitEmbeddedObject(Object embeddedObject) {
+ throw new UnsupportedOperationException("Embedded objects are not supported within Document types.");
+ }
+}
diff --git a/core/imds/src/test/java/software/amazon/awssdk/imds/Ec2MetadataResponseTest.java b/core/imds/src/test/java/software/amazon/awssdk/imds/Ec2MetadataResponseTest.java
new file mode 100644
index 000000000000..ed5d35a6c81f
--- /dev/null
+++ b/core/imds/src/test/java/software/amazon/awssdk/imds/Ec2MetadataResponseTest.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.awssdk.imds;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+import software.amazon.awssdk.core.document.Document;
+import software.amazon.awssdk.thirdparty.jackson.core.JsonParseException;
+
+/**
+ * The class tests the utility methods provided by MetadataResponse Class .
+ */
+class Ec2MetadataResponseTest {
+
+ @Test
+ void check_asString_success() {
+
+ String response = "foobar";
+
+ Ec2MetadataResponse metadataResponse = Ec2MetadataResponse.create(response);
+ String result = metadataResponse.asString();
+ assertThat(result).isEqualTo(response);
+
+ }
+
+ @Test
+ void check_asString_failure() {
+ assertThatThrownBy(() -> Ec2MetadataResponse.create(null)).isInstanceOf(NullPointerException.class);
+ }
+
+ @Test
+ void check_asList_success_with_delimiter() {
+
+ String response = "sai\ntest";
+
+ Ec2MetadataResponse metadataResponse = Ec2MetadataResponse.create(response);
+ List result = metadataResponse.asList();
+ assertThat(result).hasSize(2);
+ }
+
+ @Test
+ void check_asList_success_without_delimiter() {
+
+ String response = "test1-test2";
+
+ Ec2MetadataResponse metadataResponse = Ec2MetadataResponse.create(response);
+ List result = metadataResponse.asList();
+ assertThat(result).hasSize(1);
+ }
+
+ @Test
+ void check_asDocument_success() {
+ String jsonResponse = "{"
+ + "\"instanceType\":\"m1.small\","
+ + "\"devpayProductCodes\":[\"bar\",\"foo\"]"
+ + "}";
+
+ Ec2MetadataResponse metadataResponse = Ec2MetadataResponse.create(jsonResponse);
+ Document document = metadataResponse.asDocument();
+ Map expectedMap = new LinkedHashMap<>();
+
+ List documentList = new ArrayList<>();
+ documentList.add(Document.fromString("bar"));
+ documentList.add(Document.fromString("foo"));
+
+ expectedMap.put("instanceType", Document.fromString("m1.small"));
+ expectedMap.put("devpayProductCodes", Document.fromList(documentList));
+ Document expectedDocumentMap = Document.fromMap(expectedMap);
+ assertThat(document).isEqualTo(expectedDocumentMap);
+ }
+
+ @Test
+ void toDocument_nonJsonFormat_ExpectIllegalArgument() {
+ String malformed = "this is not json";
+ Ec2MetadataResponse metadataResponse = Ec2MetadataResponse.create(malformed);
+ assertThatThrownBy(metadataResponse::asDocument).getCause().isInstanceOf(JsonParseException.class);
+ }
+
+ @Test
+ void equals_hasCode() {
+ Ec2MetadataResponse metadataResponse = Ec2MetadataResponse.create("Line 1");
+ assertThat(metadataResponse).isEqualTo(Ec2MetadataResponse.create("Line 1"))
+ .hasSameHashCodeAs("Line 1");
+ assertThat(metadataResponse.equals(null)).isFalse();
+ }
+
+}
diff --git a/core/imds/src/test/java/software/amazon/awssdk/imds/Ec2MetadataRetryPolicyTest.java b/core/imds/src/test/java/software/amazon/awssdk/imds/Ec2MetadataRetryPolicyTest.java
new file mode 100644
index 000000000000..7131127b935d
--- /dev/null
+++ b/core/imds/src/test/java/software/amazon/awssdk/imds/Ec2MetadataRetryPolicyTest.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.awssdk.imds;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.jupiter.api.Test;
+import software.amazon.awssdk.core.retry.backoff.BackoffStrategy;
+
+class Ec2MetadataRetryPolicyTest {
+
+ @Test
+ void equals_hashCode() {
+ BackoffStrategy backoffStrategy = BackoffStrategy.defaultStrategy();
+ Ec2MetadataRetryPolicy policy = Ec2MetadataRetryPolicy.builder()
+ .numRetries(3)
+ .backoffStrategy(backoffStrategy)
+ .build();
+ assertThat(policy).isEqualTo(Ec2MetadataRetryPolicy.builder()
+ .numRetries(3)
+ .backoffStrategy(backoffStrategy)
+ .build());
+ }
+
+ @Test
+ void builder_setNumRetriesCorrectly() {
+ Ec2MetadataRetryPolicy policy = Ec2MetadataRetryPolicy.builder()
+ .numRetries(3)
+ .build();
+ assertThat(policy.numRetries()).isEqualTo(3);
+ }
+}
diff --git a/core/imds/src/test/java/software/amazon/awssdk/imds/EndpointModeTest.java b/core/imds/src/test/java/software/amazon/awssdk/imds/EndpointModeTest.java
new file mode 100644
index 000000000000..c880c3dd2d72
--- /dev/null
+++ b/core/imds/src/test/java/software/amazon/awssdk/imds/EndpointModeTest.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.awssdk.imds;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit Tests to test the EndpointMode enum functionality.
+ */
+class EndpointModeTest {
+
+ @Test
+ void verifyFromValue_when_nullParameterIsPassed(){
+ assertThat(EndpointMode.fromValue(null)).isEqualTo(null);
+ }
+
+ @Test
+ void verifyFromValue_when_normalParameterIsPassed(){
+ assertThat(EndpointMode.fromValue("ipv4")).isEqualTo(EndpointMode.IPV4);
+ }
+
+ @Test
+ void verifyFromValue_when_wrongParameterIsPassed(){
+ assertThatThrownBy(() -> {
+ EndpointMode.fromValue("ipv8");
+ }).hasMessageContaining("Unrecognized value for endpoint mode")
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+
+}
diff --git a/core/imds/src/test/java/software/amazon/awssdk/imds/TestConstants.java b/core/imds/src/test/java/software/amazon/awssdk/imds/TestConstants.java
new file mode 100644
index 000000000000..dd342025d678
--- /dev/null
+++ b/core/imds/src/test/java/software/amazon/awssdk/imds/TestConstants.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.awssdk.imds;
+
+public class TestConstants {
+ public static final String TOKEN_RESOURCE_PATH = "/latest/api/token";
+ public static final String TOKEN_HEADER = "x-aws-ec2-metadata-token";
+ public static final String EC2_METADATA_TOKEN_TTL_HEADER = "x-aws-ec2-metadata-token-ttl-seconds";
+ public static final String EC2_METADATA_ROOT = "/latest/meta-data";
+ public static final String AMI_ID_RESOURCE = EC2_METADATA_ROOT + "/ami-id";
+}
diff --git a/core/imds/src/test/java/software/amazon/awssdk/imds/internal/BaseEc2MetadataClientTest.java b/core/imds/src/test/java/software/amazon/awssdk/imds/internal/BaseEc2MetadataClientTest.java
new file mode 100644
index 000000000000..96f95d16ec8f
--- /dev/null
+++ b/core/imds/src/test/java/software/amazon/awssdk/imds/internal/BaseEc2MetadataClientTest.java
@@ -0,0 +1,316 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.awssdk.imds.internal;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.exactly;
+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.stubFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.verify;
+import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.params.provider.Arguments.arguments;
+import static software.amazon.awssdk.imds.EndpointMode.IPV4;
+import static software.amazon.awssdk.imds.EndpointMode.IPV6;
+import static software.amazon.awssdk.imds.TestConstants.AMI_ID_RESOURCE;
+import static software.amazon.awssdk.imds.TestConstants.EC2_METADATA_TOKEN_TTL_HEADER;
+import static software.amazon.awssdk.imds.TestConstants.TOKEN_HEADER;
+import static software.amazon.awssdk.imds.TestConstants.TOKEN_RESOURCE_PATH;
+
+import com.github.tomakehurst.wiremock.junit5.WireMockTest;
+import java.net.URI;
+import java.time.Duration;
+import java.util.function.Consumer;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import software.amazon.awssdk.core.exception.SdkClientException;
+import software.amazon.awssdk.core.retry.backoff.FixedDelayBackoffStrategy;
+import software.amazon.awssdk.imds.Ec2MetadataClientBuilder;
+import software.amazon.awssdk.imds.Ec2MetadataResponse;
+import software.amazon.awssdk.imds.Ec2MetadataRetryPolicy;
+import software.amazon.awssdk.imds.EndpointMode;
+
+@WireMockTest
+abstract class BaseEc2MetadataClientTest> {
+
+ protected static final int DEFAULT_TOTAL_ATTEMPTS = 4;
+
+ protected abstract BaseEc2MetadataClient overrideClient(Consumer builderConsumer);
+
+ protected abstract void successAssertions(String path, Consumer assertions);
+
+ protected abstract void failureAssertions(String path, Class exceptionType,
+ Consumer assertions);
+
+ protected abstract int getPort();
+
+ @Test
+ void get_successOnFirstTry_shouldNotRetryAndSucceed() {
+ stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(
+ aResponse().withBody("some-token").withHeader(EC2_METADATA_TOKEN_TTL_HEADER, "21600")));
+ stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withBody("{}")));
+ successAssertions(AMI_ID_RESOURCE, response -> {
+ assertThat(response.asString()).isEqualTo("{}");
+ verify(exactly(1), putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH))
+ .withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600")));
+ verify(exactly(1), getRequestedFor(urlPathEqualTo(AMI_ID_RESOURCE))
+ .withHeader(TOKEN_HEADER, equalTo("some-token")));
+ });
+ }
+
+ @Test
+ void get_failsEverytime_shouldRetryAndFails() {
+ stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(
+ aResponse().withBody("some-token").withHeader(EC2_METADATA_TOKEN_TTL_HEADER, "21600")));
+ stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withStatus(500).withBody("Error 500")));
+ failureAssertions(AMI_ID_RESOURCE, SdkClientException.class, ex -> {
+ verify(exactly(1), putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH))
+ .withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600")));
+ verify(exactly(DEFAULT_TOTAL_ATTEMPTS), getRequestedFor(urlPathEqualTo(AMI_ID_RESOURCE))
+ .withHeader(TOKEN_HEADER, equalTo("some-token")));
+ });
+ }
+
+ @Test
+ void get_returnsStatus4XX_shouldFailsAndNotRetry() {
+ stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(
+ aResponse().withBody("some-token").withHeader(EC2_METADATA_TOKEN_TTL_HEADER, "21600")));
+ stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withStatus(400).withBody("error")));
+ failureAssertions(AMI_ID_RESOURCE, SdkClientException.class, ex -> {
+ verify(exactly(1), putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH))
+ .withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600")));
+ verify(exactly(1), getRequestedFor(urlPathEqualTo(AMI_ID_RESOURCE))
+ .withHeader(TOKEN_HEADER, equalTo("some-token")));
+ });
+ }
+
+ @Test
+ void get_failsOnceThenSucceed_withCustomClient_shouldSucceed() {
+ stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(
+ aResponse().withBody("some-token").withHeader(EC2_METADATA_TOKEN_TTL_HEADER, "21600")));
+ stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE))
+ .inScenario("Retry Scenario")
+ .whenScenarioStateIs(STARTED)
+ .willReturn(aResponse().withStatus(500).withBody("Error 500"))
+ .willSetStateTo("Cause Success"));
+ stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE))
+ .inScenario("Retry Scenario")
+ .whenScenarioStateIs("Cause Success")
+ .willReturn(aResponse().withBody("{}")));
+
+ overrideClient(builder -> builder
+ .retryPolicy(Ec2MetadataRetryPolicy.builder()
+ .numRetries(5)
+ .backoffStrategy(FixedDelayBackoffStrategy.create(Duration.ofMillis(300)))
+ .build())
+ .endpoint(URI.create("http://localhost:" + getPort()))
+ .tokenTtl(Duration.ofSeconds(1024)));
+
+ successAssertions(AMI_ID_RESOURCE, response -> {
+ assertThat(response.asString()).isEqualTo("{}");
+ verify(exactly(1), putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH))
+ .withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("1024")));
+ verify(exactly(2), getRequestedFor(urlPathEqualTo(AMI_ID_RESOURCE))
+ .withHeader(TOKEN_HEADER, equalTo("some-token")));
+ });
+ }
+
+ @Test
+ void getToken_failsEverytime_shouldRetryAndFailsAndNotCallService() {
+ stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withStatus(500).withBody("Error 500")));
+ failureAssertions(AMI_ID_RESOURCE, SdkClientException.class, ex -> {
+ verify(exactly(DEFAULT_TOTAL_ATTEMPTS), putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH))
+ .withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600")));
+ verify(exactly(0), getRequestedFor(urlPathEqualTo(AMI_ID_RESOURCE))
+ .withHeader(TOKEN_HEADER, equalTo("some-token")));
+ });
+ }
+
+ @Test
+ void getToken_returnsStatus4XX_shouldFailsAndNotRetry() {
+ stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withStatus(400).withBody("ERROR 400")));
+ failureAssertions(AMI_ID_RESOURCE, SdkClientException.class, ex -> {
+ verify(exactly(1), putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH))
+ .withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600")));
+ verify(exactly(0), getRequestedFor(urlPathEqualTo(AMI_ID_RESOURCE))
+ .withHeader(TOKEN_HEADER, equalTo("some-token")));
+ });
+ }
+
+ @Test
+ void getToken_failsOnceThenSucceed_withCustomClient_shouldSucceed() {
+ stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).inScenario("Retry Scenario")
+ .whenScenarioStateIs(STARTED)
+ .willReturn(aResponse().withStatus(500).withBody("Error 500"))
+ .willSetStateTo("Cause Success"));
+ stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).inScenario("Retry Scenario")
+ .whenScenarioStateIs("Cause Success")
+ .willReturn(
+ aResponse()
+ .withBody("some-token")
+ .withHeader(EC2_METADATA_TOKEN_TTL_HEADER, "21600")));
+ stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).inScenario("Retry Scenario")
+ .whenScenarioStateIs("Cause Success")
+ .willReturn(aResponse().withBody("Success")));
+
+ overrideClient(builder -> builder
+ .retryPolicy(Ec2MetadataRetryPolicy.builder()
+ .numRetries(5)
+ .backoffStrategy(FixedDelayBackoffStrategy.create(Duration.ofMillis(300)))
+ .build())
+ .endpoint(URI.create("http://localhost:" + getPort()))
+ .build());
+
+ successAssertions(AMI_ID_RESOURCE, response -> {
+ assertThat(response.asString()).isEqualTo("Success");
+ verify(exactly(2), putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH))
+ .withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600")));
+ verify(exactly(1), getRequestedFor(urlPathEqualTo(AMI_ID_RESOURCE))
+ .withHeader(TOKEN_HEADER, equalTo("some-token")));
+ });
+ }
+
+ @Test
+ void get_noRetries_shouldNotRetry() {
+ stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody("some-token")));
+ stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withStatus(500).withBody("Error 500")));
+
+ overrideClient(builder -> builder
+ .endpoint(URI.create("http://localhost:" + getPort()))
+ .retryPolicy(Ec2MetadataRetryPolicy.none()).build());
+
+ failureAssertions(AMI_ID_RESOURCE, SdkClientException.class, ex -> {
+ verify(1, putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH))
+ .withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600")));
+ verify(1, putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH))
+ .withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600")));
+ });
+ }
+
+ @Test
+ void builder_endpointAndEndpointModeSpecified_shouldThrowIllegalArgException() {
+ assertThatThrownBy(() -> overrideClient(builder -> builder
+ .endpoint(URI.create("http://localhost:" + getPort()))
+ .endpointMode(IPV6)))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ void builder_defaultValue_clientShouldUseIPV4Endpoint() {
+ BaseEc2MetadataClient client = overrideClient(builder -> {
+ });
+ assertThat(client.endpoint).hasToString("http://169.254.169.254");
+ }
+
+ @Test
+ void builder_setEndpoint_shouldUseEndpoint() {
+ BaseEc2MetadataClient client = overrideClient(builder -> builder.endpoint(URI.create("http://localhost:" + 12312)));
+ assertThat(client.endpoint).hasToString("http://localhost:" + 12312);
+ }
+
+ @ParameterizedTest
+ @MethodSource("endpointArgumentSource")
+ void builder_setEndPointMode_shouldUseEndpointModeValue(EndpointMode endpointMode, String value) {
+ BaseEc2MetadataClient client = overrideClient(builder -> builder.endpointMode(endpointMode));
+ assertThat(client.endpoint).hasToString(value);
+ }
+
+ private static Stream endpointArgumentSource() {
+ return Stream.of(
+ arguments(IPV4, "http://169.254.169.254"),
+ arguments(IPV6, "http://[fd00:ec2::254]"));
+ }
+
+ @Test
+ void get_tokenExpiresWhileRetrying_shouldSucceedWithNewToken() {
+ stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(
+ aResponse().withBody("some-token").withHeader(EC2_METADATA_TOKEN_TTL_HEADER, "2")));
+
+ stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).inScenario("Retry Scenario")
+ .whenScenarioStateIs(STARTED)
+ .willReturn(aResponse().withStatus(500)
+ .withBody("Error 500")
+ .withFixedDelay(700))
+ .willSetStateTo("Retry-1"));
+ stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).inScenario("Retry Scenario")
+ .whenScenarioStateIs("Retry-1")
+ .willReturn(aResponse().withStatus(500)
+ .withBody("Error 500")
+ .withFixedDelay(700))
+ .willSetStateTo("Retry-2"));
+ stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).inScenario("Retry Scenario")
+ .whenScenarioStateIs("Retry-2")
+ .willReturn(aResponse().withStatus(500)
+ .withBody("Error 500")
+ .withFixedDelay(700))
+ .willSetStateTo("Retry-3"));
+ stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).inScenario("Retry Scenario")
+ .whenScenarioStateIs("Retry-3")
+ .willReturn(aResponse().withStatus(200)
+ .withBody("Success")
+ .withFixedDelay(700)));
+
+ overrideClient(builder -> builder
+ .tokenTtl(Duration.ofSeconds(2))
+ .endpoint(URI.create("http://localhost:" + getPort()))
+ .build());
+
+ successAssertions(AMI_ID_RESOURCE, response -> {
+ assertThat(response.asString()).isEqualTo("Success");
+ verify(exactly(2), putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH))
+ .withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("2")));
+ verify(exactly(4), getRequestedFor(urlPathEqualTo(AMI_ID_RESOURCE))
+ .withHeader(TOKEN_HEADER, equalTo("some-token")));
+ });
+ }
+
+ @Test
+ void getToken_responseWithoutTtlHeaders_shouldFailAndNotRetry() {
+ stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(
+ aResponse().withBody("some-token"))); // no EC2_METADATA_TOKEN_TTL_HEADER header
+ stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withStatus(200).withBody("some-value")));
+ failureAssertions(AMI_ID_RESOURCE, SdkClientException.class, e -> {
+ verify(exactly(1), putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH))
+ .withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600")));
+ verify(exactly(0), getRequestedFor(urlPathEqualTo(AMI_ID_RESOURCE))
+ .withHeader(TOKEN_HEADER, equalTo("some-token")));
+ });
+ }
+
+ @Test
+ void getToken_responseTtlHeadersNotANumber_shouldFailAndNotRetry() {
+ stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody("some-token")));
+ stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withStatus(200).withBody("some-value")));
+ stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(
+ aResponse().withBody("some-token").withHeader(EC2_METADATA_TOKEN_TTL_HEADER, "not-a-number")));
+ stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withStatus(200).withBody("some-value")));
+ failureAssertions(AMI_ID_RESOURCE, SdkClientException.class, e -> {
+ verify(exactly(1), putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH))
+ .withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600")));
+ verify(exactly(0), getRequestedFor(urlPathEqualTo(AMI_ID_RESOURCE))
+ .withHeader(TOKEN_HEADER, equalTo("some-token")));
+ });
+ }
+}
diff --git a/core/imds/src/test/java/software/amazon/awssdk/imds/internal/CachedTokenClientTest.java b/core/imds/src/test/java/software/amazon/awssdk/imds/internal/CachedTokenClientTest.java
new file mode 100644
index 000000000000..04ad39dc23e9
--- /dev/null
+++ b/core/imds/src/test/java/software/amazon/awssdk/imds/internal/CachedTokenClientTest.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.awssdk.imds.internal;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.exactly;
+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.stubFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.verify;
+import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static software.amazon.awssdk.imds.TestConstants.EC2_METADATA_TOKEN_TTL_HEADER;
+
+import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
+import com.github.tomakehurst.wiremock.junit5.WireMockTest;
+import java.net.URI;
+import java.time.Duration;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import software.amazon.awssdk.core.exception.SdkClientException;
+import software.amazon.awssdk.imds.Ec2MetadataClient;
+import software.amazon.awssdk.imds.Ec2MetadataResponse;
+
+@WireMockTest
+class CachedTokenClientTest {
+
+ private Ec2MetadataClient.Builder clientBuilder;
+
+ @BeforeEach
+ void init(WireMockRuntimeInfo wmRuntimeInfo) {
+ this.clientBuilder = Ec2MetadataClient.builder()
+ .endpoint(URI.create("http://localhost:" + wmRuntimeInfo.getHttpPort()));
+ }
+
+ @Test
+ void get_tokenFailsError4xx_shouldNotRetry() {
+ stubFor(put(urlPathEqualTo("/latest/api/token")).willReturn(aResponse().withStatus(400).withBody("ERROR 400")));
+ stubFor(get(urlPathEqualTo("/latest/meta-data/ami-id")).willReturn(aResponse().withBody("{}")));
+
+ assertThatThrownBy(() -> clientBuilder.build().get("/latest/meta-data/ami-id")).isInstanceOf(SdkClientException.class);
+ verify(exactly(1), putRequestedFor(urlPathEqualTo("/latest/api/token"))
+ .withHeader("x-aws-ec2-metadata-token-ttl-seconds", equalTo("21600")));
+ }
+
+ @Test
+ void getToken_failsError5xx_shouldRetryUntilMaxRetriesIsReached() {
+ stubFor(put(urlPathEqualTo("/latest/api/token")).willReturn(aResponse().withStatus(500).withBody("ERROR 500")));
+ stubFor(get(urlPathEqualTo("/latest/meta-data/ami-id")).willReturn(aResponse().withBody("{}")));
+
+ assertThatThrownBy(() -> clientBuilder.build().get("/latest/meta-data/ami-id")).isInstanceOf(SdkClientException.class);
+ verify(exactly(4), putRequestedFor(urlPathEqualTo("/latest/api/token"))
+ .withHeader("x-aws-ec2-metadata-token-ttl-seconds", equalTo("21600")));
+ }
+
+ @Test
+ void getToken_failsThenSucceeds_doesCacheTokenThatSucceeds() {
+ stubFor(put(urlPathEqualTo("/latest/api/token")).inScenario("Retry Scenario")
+ .whenScenarioStateIs(STARTED)
+ .willReturn(aResponse().withStatus(500).withBody("Error 500"))
+ .willSetStateTo("Cause Success"));
+ stubFor(put(urlPathEqualTo("/latest/api/token")).inScenario("Retry Scenario")
+ .whenScenarioStateIs("Cause Success")
+ .willReturn(aResponse()
+ .withBody("token-ok")
+ .withHeader(EC2_METADATA_TOKEN_TTL_HEADER, "21600")));
+ stubFor(get(urlPathEqualTo("/latest/meta-data/ami-id")).inScenario("Retry Scenario")
+ .whenScenarioStateIs("Cause Success")
+ .willReturn(aResponse().withBody("Success")));
+
+ // 3 requests
+ Ec2MetadataClient client = clientBuilder.build();
+ Ec2MetadataResponse response = client.get("/latest/meta-data/ami-id");
+ assertThat(response.asString()).isEqualTo("Success");
+ response = client.get("/latest/meta-data/ami-id");
+ assertThat(response.asString()).isEqualTo("Success");
+ response = client.get("/latest/meta-data/ami-id");
+ assertThat(response.asString()).isEqualTo("Success");
+
+ verify(exactly(2), putRequestedFor(urlPathEqualTo("/latest/api/token"))
+ .withHeader("x-aws-ec2-metadata-token-ttl-seconds", equalTo("21600")));
+ verify(exactly(3), getRequestedFor(urlPathEqualTo("/latest/meta-data/ami-id"))
+ .withHeader("x-aws-ec2-metadata-token", equalTo("token-ok")));
+ }
+
+ @Test
+ void get_multipleCallsSuccess_shouldReuseToken() throws Exception {
+ stubFor(put(urlPathEqualTo("/latest/api/token")).willReturn(
+ aResponse().withBody("some-token").withHeader(EC2_METADATA_TOKEN_TTL_HEADER, "21600")));
+ stubFor(get(urlPathEqualTo("/latest/meta-data/ami-id"))
+ .willReturn(aResponse().withBody("{}").withFixedDelay(800)));
+
+ int tokenTTlSeconds = 4;
+ Ec2MetadataClient client = clientBuilder.tokenTtl(Duration.ofSeconds(tokenTTlSeconds)).build();
+
+ int totalRequests = 10;
+ for (int i = 0; i < totalRequests; i++) {
+ Ec2MetadataResponse response = client.get("/latest/meta-data/ami-id");
+ assertThat(response.asString()).isEqualTo("{}");
+ }
+ verify(exactly(2), putRequestedFor(urlPathEqualTo("/latest/api/token"))
+ .withHeader("x-aws-ec2-metadata-token-ttl-seconds", equalTo(String.valueOf(tokenTTlSeconds))));
+ verify(exactly(totalRequests), getRequestedFor(urlPathEqualTo("/latest/meta-data/ami-id"))
+ .withHeader("x-aws-ec2-metadata-token", equalTo("some-token")));
+ }
+
+}
diff --git a/core/imds/src/test/java/software/amazon/awssdk/imds/internal/Ec2MetadataAsyncClientTest.java b/core/imds/src/test/java/software/amazon/awssdk/imds/internal/Ec2MetadataAsyncClientTest.java
new file mode 100644
index 000000000000..f6bfea862ada
--- /dev/null
+++ b/core/imds/src/test/java/software/amazon/awssdk/imds/internal/Ec2MetadataAsyncClientTest.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.awssdk.imds.internal;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.put;
+import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.verify;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.AssertionsForClassTypes.catchThrowable;
+import static org.junit.jupiter.api.Assertions.fail;
+import static software.amazon.awssdk.imds.TestConstants.AMI_ID_RESOURCE;
+import static software.amazon.awssdk.imds.TestConstants.EC2_METADATA_TOKEN_TTL_HEADER;
+import static software.amazon.awssdk.imds.TestConstants.TOKEN_RESOURCE_PATH;
+
+import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
+import com.github.tomakehurst.wiremock.junit5.WireMockTest;
+import java.net.URI;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Consumer;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import software.amazon.awssdk.core.internal.http.loader.DefaultSdkAsyncHttpClientBuilder;
+import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient;
+import software.amazon.awssdk.imds.Ec2MetadataAsyncClient;
+import software.amazon.awssdk.imds.Ec2MetadataResponse;
+
+@WireMockTest
+class Ec2MetadataAsyncClientTest extends BaseEc2MetadataClientTest {
+
+ private Ec2MetadataAsyncClient client;
+
+ private int port;
+
+ @BeforeEach
+ public void init(WireMockRuntimeInfo wiremock) {
+ this.port = wiremock.getHttpPort();
+ this.client = Ec2MetadataAsyncClient.builder()
+ .endpoint(URI.create("http://localhost:" + wiremock.getHttpPort()))
+ .build();
+ }
+
+ @Override
+ protected int getPort() {
+ return port;
+ }
+
+ @Override
+ protected BaseEc2MetadataClient overrideClient(Consumer builderConsumer) {
+ Ec2MetadataAsyncClient.Builder builder = Ec2MetadataAsyncClient.builder();
+ builderConsumer.accept(builder);
+ this.client = builder.build();
+ return (BaseEc2MetadataClient) this.client;
+ }
+
+ @Override
+ protected void successAssertions(String path, Consumer assertions) {
+ CompletableFuture response = client.get(path);
+ try {
+ assertions.accept(response.join());
+ } catch (Exception e) {
+ fail("unexpected error while exeucting tests", e);
+ }
+ }
+
+ @Override
+ @SuppressWarnings("unchecked") // safe because of assertion: assertThat(ex).getCause().isInstanceOf(exceptionType);
+ protected void failureAssertions(String path, Class exceptionType, Consumer assertions) {
+ CompletableFuture future = client.get(path);
+ Throwable ex = catchThrowable(future::join);
+ assertThat(future).isCompletedExceptionally();
+ assertThat(ex).getCause().isInstanceOf(exceptionType);
+ assertions.accept((T) ex.getCause());
+ }
+
+ @Test
+ void get_cancelResponseFuture_shouldCancelHttpRequest() {
+ stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(
+ aResponse().withBody("some-token").withFixedDelay(1000)));
+ stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(
+ aResponse().withBody("some-content").withFixedDelay(1000)));
+
+ CompletableFuture responseFuture = client.get(AMI_ID_RESOURCE);
+ try {
+ responseFuture.cancel(true);
+ responseFuture.join();
+ } catch (CancellationException e) {
+ // ignore java.util.concurrent.CancellationException
+ }
+ verify(0, getRequestedFor(urlPathEqualTo(AMI_ID_RESOURCE)));
+ }
+
+ @Test
+ void builder_httpClientWithDefaultBuilder_shouldBuildProperly() {
+ Ec2MetadataAsyncClient buildClient = Ec2MetadataAsyncClient.builder()
+ .httpClient(new DefaultSdkAsyncHttpClientBuilder())
+ .endpoint(URI.create("http://localhost:" + port))
+ .build();
+ stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(
+ aResponse().withBody("some-token").withHeader(EC2_METADATA_TOKEN_TTL_HEADER, "21600")));
+ stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withBody("some-value")));
+ CompletableFuture responseFuture = buildClient.get(AMI_ID_RESOURCE);
+ Ec2MetadataResponse response = responseFuture.join();
+ assertThat(response.asString()).isEqualTo("some-value");
+ }
+
+ @Test
+ void builder_httpClientAndHttpBuilder_shouldThrowException() {
+ assertThatThrownBy(() -> Ec2MetadataAsyncClient.builder()
+ .httpClient(new DefaultSdkAsyncHttpClientBuilder())
+ .httpClient(NettyNioAsyncHttpClient.create())
+ .build())
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+
+}
diff --git a/core/imds/src/test/java/software/amazon/awssdk/imds/internal/Ec2MetadataClientTest.java b/core/imds/src/test/java/software/amazon/awssdk/imds/internal/Ec2MetadataClientTest.java
new file mode 100644
index 000000000000..4cf8b1b203e7
--- /dev/null
+++ b/core/imds/src/test/java/software/amazon/awssdk/imds/internal/Ec2MetadataClientTest.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.awssdk.imds.internal;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.put;
+import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.AssertionsForClassTypes.catchThrowable;
+import static software.amazon.awssdk.imds.TestConstants.AMI_ID_RESOURCE;
+import static software.amazon.awssdk.imds.TestConstants.EC2_METADATA_TOKEN_TTL_HEADER;
+import static software.amazon.awssdk.imds.TestConstants.TOKEN_RESOURCE_PATH;
+
+import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
+import com.github.tomakehurst.wiremock.junit5.WireMockTest;
+import java.net.URI;
+import java.util.function.Consumer;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import software.amazon.awssdk.core.internal.http.loader.DefaultSdkHttpClientBuilder;
+import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient;
+import software.amazon.awssdk.imds.Ec2MetadataClient;
+import software.amazon.awssdk.imds.Ec2MetadataResponse;
+
+@WireMockTest
+class Ec2MetadataClientTest extends BaseEc2MetadataClientTest {
+
+ private Ec2MetadataClient client;
+
+ private int port;
+
+ @BeforeEach
+ public void init(WireMockRuntimeInfo wiremock) {
+ this.port = wiremock.getHttpPort();
+ this.client = Ec2MetadataClient.builder()
+ .endpoint(URI.create("http://localhost:" + wiremock.getHttpPort()))
+ .build();
+ }
+
+ @Override
+ protected int getPort() {
+ return port;
+ }
+
+ @Override
+ protected BaseEc2MetadataClient overrideClient(Consumer builderConsumer) {
+ Ec2MetadataClient.Builder builder = Ec2MetadataClient.builder();
+ builderConsumer.accept(builder);
+ this.client = builder.build();
+ return (BaseEc2MetadataClient) this.client;
+ }
+
+ @Override
+ protected void successAssertions(String path, Consumer assertions) {
+ Ec2MetadataResponse response = client.get(path);
+ assertions.accept(response);
+ }
+
+ @Override
+ @SuppressWarnings("unchecked") // safe because of assertion: assertThat(ex).isInstanceOf(exceptionType);
+ protected void failureAssertions(String path, Class exceptionType, Consumer assertions) {
+ Throwable ex = catchThrowable(() -> client.get(path));
+ assertThat(ex).isInstanceOf(exceptionType);
+ assertions.accept((T) ex);
+ }
+
+ @Test
+ void builder_httpClientWithDefaultBuilder_shouldBuildProperly() {
+ Ec2MetadataClient buildClient = Ec2MetadataClient.builder()
+ .httpClient(new DefaultSdkHttpClientBuilder())
+ .endpoint(URI.create("http://localhost:" + port))
+ .build();
+ stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(
+ aResponse().withBody("some-token").withHeader(EC2_METADATA_TOKEN_TTL_HEADER, "21600")));
+ stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withBody("{}")));
+ Ec2MetadataResponse response = buildClient.get(AMI_ID_RESOURCE);
+ assertThat(response.asString()).isEqualTo("{}");
+ }
+
+ @Test
+ void builder_httpClientAndHttpBuilder_shouldThrowException() {
+ assertThatThrownBy(() -> Ec2MetadataClient.builder()
+ .httpClient(new DefaultSdkHttpClientBuilder())
+ .httpClient(UrlConnectionHttpClient.create())
+ .build())
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+
+}
diff --git a/core/imds/src/test/java/software/amazon/awssdk/imds/internal/EndpointProviderTest.java b/core/imds/src/test/java/software/amazon/awssdk/imds/internal/EndpointProviderTest.java
new file mode 100644
index 000000000000..b619d35e9803
--- /dev/null
+++ b/core/imds/src/test/java/software/amazon/awssdk/imds/internal/EndpointProviderTest.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.awssdk.imds.internal;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.params.provider.Arguments.arguments;
+import static software.amazon.awssdk.imds.EndpointMode.IPV4;
+import static software.amazon.awssdk.imds.EndpointMode.IPV6;
+
+import java.net.URISyntaxException;
+import java.nio.file.Paths;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import software.amazon.awssdk.core.SdkSystemSetting;
+import software.amazon.awssdk.imds.EndpointMode;
+import software.amazon.awssdk.profiles.ProfileFile;
+import software.amazon.awssdk.profiles.ProfileFileSystemSetting;
+import software.amazon.awssdk.testutils.EnvironmentVariableHelper;
+import software.amazon.awssdk.utils.internal.SystemSettingUtilsTestBackdoor;
+
+/**
+ * Test Class to test the endpoint resolution functionality.
+ */
+class EndpointProviderTest {
+
+ @AfterEach
+ void reset() {
+ System.clearProperty(SdkSystemSetting.AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE.property());
+ System.clearProperty(SdkSystemSetting.AWS_EC2_METADATA_SERVICE_ENDPOINT.property());
+ SystemSettingUtilsTestBackdoor.clearEnvironmentVariableOverrides();
+ }
+
+ private static Stream provideEndpointAndEndpointModes() {
+ String testIpv4Url = "http://90:90:90:90";
+ String testIpv6Url = "http://[9876:ec2::123]";
+ return Stream.of(
+ arguments(null, true, testIpv4Url, true, "testIPv6", testIpv4Url),
+ arguments(null, true, testIpv6Url, true, "testIPv4", testIpv6Url),
+
+ arguments(null, true, testIpv4Url, false, "testIPv6", testIpv4Url),
+ arguments(null, true, testIpv6Url, false, "testIPv4", testIpv6Url),
+
+ arguments(null, false, "unused", true, "testIPv6", "[1234:ec2::456]"),
+ arguments(null, false, "unused", true, "testIPv4", "http://42.42.42.42"),
+
+ arguments(IPV4, false, "unused", false, "unused", "http://169.254.169.254"),
+ arguments(IPV6, false, "unused", false, "unused", "http://[fd00:ec2::254]")
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("provideEndpointAndEndpointModes")
+ void validateResolveEndpoint(EndpointMode endpointMode,
+ boolean setEnvVariable,
+ String envEndpoint,
+ boolean setConfigFile,
+ String profile,
+ String expectedValue)
+ throws URISyntaxException {
+
+ if (setEnvVariable) {
+ System.setProperty(SdkSystemSetting.AWS_EC2_METADATA_SERVICE_ENDPOINT.property(), envEndpoint);
+ }
+
+ if (setConfigFile) {
+ String testFile = "/profile-config/test-profiles.tst";
+ SystemSettingUtilsTestBackdoor.addEnvironmentVariableOverride(
+ ProfileFileSystemSetting.AWS_PROFILE.environmentVariable(),
+ profile);
+ SystemSettingUtilsTestBackdoor.addEnvironmentVariableOverride(
+ ProfileFileSystemSetting.AWS_CONFIG_FILE.environmentVariable(),
+ Paths.get(getClass().getResource(testFile).toURI()).toString());
+ }
+
+ Ec2MetadataEndpointProvider endpointProvider = Ec2MetadataEndpointProvider.builder().build();
+ String endpoint = endpointProvider.resolveEndpoint(endpointMode);
+ assertThat(endpoint).isEqualTo(expectedValue);
+ }
+
+ private static Stream provideEndpointModes() {
+ return Stream.of(
+ arguments(false, "unused", false, "unused", IPV4),
+ arguments(true, "IPv4", true, "IPv4", IPV4),
+ arguments(true, "IPv6", true, "IPv6", IPV6),
+ arguments(true, "IPv6", true, "IPv4", IPV6),
+ arguments(true, "IPv4", true, "IPv6", IPV4),
+ arguments(false, "unused", true, "IPv6", IPV6),
+ arguments(false, "unused", true, "IPv4", IPV4),
+ arguments(true, "IPv6", false, "unused", IPV6),
+ arguments(true, "IPv4", false, "unused", IPV4)
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("provideEndpointModes")
+ void endpointModeCheck(boolean useEnvVariable, String envVarValue, boolean useConfigFile, String configFileValue,
+ EndpointMode expectedValue)
+ throws URISyntaxException {
+
+ if (useEnvVariable) {
+ System.setProperty(SdkSystemSetting.AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE.property(), envVarValue);
+ }
+
+ if (useConfigFile) {
+ String testFile = "/profile-config/test-profiles.tst";
+ SystemSettingUtilsTestBackdoor.addEnvironmentVariableOverride(
+ ProfileFileSystemSetting.AWS_PROFILE.environmentVariable(),
+ "test" + configFileValue);
+
+ SystemSettingUtilsTestBackdoor.addEnvironmentVariableOverride(
+ ProfileFileSystemSetting.AWS_CONFIG_FILE.environmentVariable(),
+ Paths.get(getClass().getResource(testFile).toURI()).toString());
+ }
+
+ Ec2MetadataEndpointProvider endpointProvider = Ec2MetadataEndpointProvider.builder().build();
+ EndpointMode endpointMode = endpointProvider.resolveEndpointMode();
+
+ assertThat(endpointMode).isEqualTo(expectedValue);
+ }
+
+ @Test
+ void endpointFromBuilder_withIpv4_shouldBesetCorrectly() {
+ ProfileFile.Builder content = ProfileFile.builder()
+ .type(ProfileFile.Type.CONFIGURATION)
+ .content(Paths.get("src/test/resources/profile-config/test-profiles.tst"));
+ Ec2MetadataEndpointProvider provider = Ec2MetadataEndpointProvider.builder()
+ .profileFile(content::build)
+ .profileName("testIPv4")
+ .build();
+ assertThat(provider.resolveEndpointMode()).isEqualTo(IPV4);
+ assertThat(provider.resolveEndpoint(IPV4)).isEqualTo("http://42.42.42.42");
+ }
+
+ @Test
+ void endpointFromBuilder_withIpv6_shouldBesetCorrectly() {
+ ProfileFile.Builder content = ProfileFile.builder()
+ .type(ProfileFile.Type.CONFIGURATION)
+ .content(Paths.get("src/test/resources/profile-config/test-profiles.tst"));
+ Ec2MetadataEndpointProvider provider = Ec2MetadataEndpointProvider.builder()
+ .profileFile(content::build)
+ .profileName("testIPv6")
+ .build();
+ assertThat(provider.resolveEndpointMode()).isEqualTo(IPV6);
+ assertThat(provider.resolveEndpoint(IPV6)).isEqualTo("[1234:ec2::456]");
+ }
+
+
+}
diff --git a/core/imds/src/test/java/software/amazon/awssdk/imds/internal/LargeAsyncRequestTest.java b/core/imds/src/test/java/software/amazon/awssdk/imds/internal/LargeAsyncRequestTest.java
new file mode 100644
index 000000000000..9f347361ab1a
--- /dev/null
+++ b/core/imds/src/test/java/software/amazon/awssdk/imds/internal/LargeAsyncRequestTest.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.awssdk.imds.internal;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.exactly;
+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.stubFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.verify;
+import static org.assertj.core.api.Assertions.assertThat;
+import static software.amazon.awssdk.imds.TestConstants.AMI_ID_RESOURCE;
+import static software.amazon.awssdk.imds.TestConstants.EC2_METADATA_TOKEN_TTL_HEADER;
+import static software.amazon.awssdk.imds.TestConstants.TOKEN_HEADER;
+import static software.amazon.awssdk.imds.TestConstants.TOKEN_RESOURCE_PATH;
+
+import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
+import com.github.tomakehurst.wiremock.junit5.WireMockTest;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.concurrent.CompletableFuture;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient;
+import software.amazon.awssdk.imds.Ec2MetadataAsyncClient;
+import software.amazon.awssdk.imds.Ec2MetadataResponse;
+
+@WireMockTest
+class LargeAsyncRequestTest {
+ private int port;
+
+ @BeforeEach
+ public void init(WireMockRuntimeInfo wiremock) {
+ this.port = wiremock.getHttpPort();
+ }
+
+ @Test
+ void largeRequestTest() throws Exception {
+
+ int size = 10 * 1024 * 1024; // 10MB
+ byte[] bytes = new byte[size];
+ for (int i = 0; i < size; i++) {
+ bytes[i] = (byte) (i % 128);
+ }
+ String ec2MetadataContent = new String(bytes, StandardCharsets.US_ASCII);
+ stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(
+ aResponse().withBody("some-token").withHeader(EC2_METADATA_TOKEN_TTL_HEADER, "21600")));
+ stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withBody(ec2MetadataContent)));
+
+ try (Ec2MetadataAsyncClient client =
+ Ec2MetadataAsyncClient.builder()
+ .endpoint(URI.create("http://localhost:" + port))
+ .httpClient(NettyNioAsyncHttpClient.builder().readTimeout(Duration.ofSeconds(30)).build())
+ .build()) {
+ CompletableFuture res = client.get(AMI_ID_RESOURCE);
+ Ec2MetadataResponse response = res.get();
+ assertThat(response.asString()).hasSize(size);
+ assertThat(response.asString()).isEqualTo(ec2MetadataContent);
+ verify(exactly(1), putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH))
+ .withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600")));
+ verify(exactly(1), getRequestedFor(urlPathEqualTo(AMI_ID_RESOURCE))
+ .withHeader(TOKEN_HEADER, equalTo("some-token")));
+ }
+
+ }
+}
diff --git a/core/imds/src/test/java/software/amazon/awssdk/imds/internal/MultipleAsyncRequestsTest.java b/core/imds/src/test/java/software/amazon/awssdk/imds/internal/MultipleAsyncRequestsTest.java
new file mode 100644
index 000000000000..d49f0b3e4d70
--- /dev/null
+++ b/core/imds/src/test/java/software/amazon/awssdk/imds/internal/MultipleAsyncRequestsTest.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.awssdk.imds.internal;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.exactly;
+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.stubFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching;
+import static com.github.tomakehurst.wiremock.client.WireMock.verify;
+import static org.assertj.core.api.Assertions.assertThat;
+import static software.amazon.awssdk.imds.TestConstants.AMI_ID_RESOURCE;
+import static software.amazon.awssdk.imds.TestConstants.EC2_METADATA_TOKEN_TTL_HEADER;
+import static software.amazon.awssdk.imds.TestConstants.TOKEN_HEADER;
+import static software.amazon.awssdk.imds.TestConstants.TOKEN_RESOURCE_PATH;
+
+import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder;
+import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
+import com.github.tomakehurst.wiremock.junit5.WireMockTest;
+import java.net.URI;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import software.amazon.awssdk.imds.Ec2MetadataAsyncClient;
+import software.amazon.awssdk.imds.Ec2MetadataResponse;
+
+@WireMockTest
+class MultipleAsyncRequestsTest {
+
+ private int port;
+
+ @BeforeEach
+ public void init(WireMockRuntimeInfo wiremock) {
+ this.port = wiremock.getHttpPort();
+ }
+
+ @Test
+ void multipleRequests() {
+ int totalRequests = 128;
+ String tokenValue = "some-token";
+
+ stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(
+ aResponse().withBody(tokenValue).withHeader(EC2_METADATA_TOKEN_TTL_HEADER, "21600")));
+ for (int i = 0; i < totalRequests; i++) {
+ ResponseDefinitionBuilder responseStub = aResponse().withStatus(200).withBody("response::" + i);
+ stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE + "/" + i)).willReturn(responseStub));
+ }
+ Ec2MetadataAsyncClient client = Ec2MetadataAsyncClient.builder()
+ .endpoint(URI.create("http://localhost:" + this.port))
+ .build();
+ List> requests = Stream.iterate(0, x -> x + 1)
+ .map(i -> client.get(AMI_ID_RESOURCE + "/" + i))
+ .limit(totalRequests)
+ .collect(Collectors.toList());
+ CompletableFuture> responses = CompletableFuture
+ .allOf(requests.toArray(new CompletableFuture[0]))
+ .thenApply(unusedVoid -> requests.stream()
+ .map(CompletableFuture::join)
+ .collect(Collectors.toList()));
+
+ List resolvedResponses = responses.join();
+ for (int i = 0; i < totalRequests; i++) {
+ Ec2MetadataResponse response = resolvedResponses.get(i);
+ assertThat(response.asString()).isEqualTo("response::" + i);
+ }
+ verify(exactly(1), putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH))
+ .withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600")));
+ verify(exactly(totalRequests), getRequestedFor(urlPathMatching(AMI_ID_RESOURCE + "/" + "\\d+"))
+ .withHeader(TOKEN_HEADER, equalTo(tokenValue)));
+
+ }
+}
diff --git a/core/imds/src/test/java/software/amazon/awssdk/imds/internal/unmarshall/document/DocumentUnmarshallerTest.java b/core/imds/src/test/java/software/amazon/awssdk/imds/internal/unmarshall/document/DocumentUnmarshallerTest.java
new file mode 100644
index 000000000000..bf0c5818f606
--- /dev/null
+++ b/core/imds/src/test/java/software/amazon/awssdk/imds/internal/unmarshall/document/DocumentUnmarshallerTest.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.awssdk.imds.internal.unmarshall.document;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+import software.amazon.awssdk.core.SdkNumber;
+import software.amazon.awssdk.core.document.Document;
+import software.amazon.awssdk.protocols.jsoncore.JsonNode;
+import software.amazon.awssdk.protocols.jsoncore.internal.EmbeddedObjectJsonNode;
+
+
+class DocumentUnmarshallerTest {
+ @Test
+ void testDocumentFromNumberNode() {
+ JsonNode node = JsonNode.parser().parse("100");
+ assertThat(Document.fromNumber(SdkNumber.fromInteger(100)).asNumber().intValue())
+ .isEqualTo(node.visit(new DocumentUnmarshaller()).asNumber().intValue());
+ }
+
+ @Test
+ void testDocumentFromBoolean() {
+ JsonNode node = JsonNode.parser().parse("true");
+ assertThat(Document.fromBoolean(true)).isEqualTo(node.visit(new DocumentUnmarshaller()));
+ }
+
+ @Test
+ void testDocumentFromString() {
+ JsonNode node = JsonNode.parser().parse("\"100.00\"");
+ assertThat(Document.fromString("100.00")).isEqualTo(node.visit(new DocumentUnmarshaller()));
+ }
+
+ @Test
+ void testDocumentFromNull() {
+ JsonNode node = JsonNode.parser().parse("null");
+ assertThat(Document.fromNull()).isEqualTo(node.visit(new DocumentUnmarshaller()));
+ }
+
+ @Test
+ void testExceptionIsThrownFromEmbededObjectType() {
+ assertThatExceptionOfType(UnsupportedOperationException.class)
+ .isThrownBy(() -> new EmbeddedObjectJsonNode(new Object()).visit(new DocumentUnmarshaller()));
+ }
+
+ @Test
+ void testDocumentFromObjectNode(){
+ JsonNode node = JsonNode.parser().parse("{\"firstKey\": \"firstValue\", \"secondKey\": \"secondValue\"}");
+
+ Document documentMap = node.visit(new DocumentUnmarshaller());
+ Map expectedMap = new LinkedHashMap<>();
+ expectedMap.put("firstKey", Document.fromString("firstValue"));
+ expectedMap.put("secondKey", Document.fromString("secondValue"));
+ Document expectedDocumentMap = Document.fromMap(expectedMap);
+ assertThat(documentMap).isEqualTo(expectedDocumentMap);
+ }
+
+ @Test
+ void testDocumentFromArrayNode(){
+ JsonNode node = JsonNode.parser().parse("[\"One\", 10, true, null]");
+ List documentList = new ArrayList<>();
+ documentList.add(Document.fromString("One"));
+ documentList.add(Document.fromNumber(SdkNumber.fromBigDecimal(BigDecimal.TEN)));
+ documentList.add(Document.fromBoolean(true));
+ documentList.add(Document.fromNull());
+ Document document = Document.fromList(documentList);
+ Document actualDocument = node.visit(new DocumentUnmarshaller());
+ assertThat(actualDocument).isEqualTo(document);
+ }
+}
diff --git a/core/imds/src/test/resources/profile-config/test-profiles.tst b/core/imds/src/test/resources/profile-config/test-profiles.tst
new file mode 100644
index 000000000000..9787e34511ff
--- /dev/null
+++ b/core/imds/src/test/resources/profile-config/test-profiles.tst
@@ -0,0 +1,13 @@
+[default]
+aws_secret_access_key = defaultAccessKey
+aws_access_key_id = defaultSecretAccessKey
+region = us-west-2
+
+
+[profile testIPv4]
+ec2_metadata_service_endpoint_mode = IPv4
+ec2_metadata_service_endpoint = http://42.42.42.42
+
+[profile testIPv6]
+ec2_metadata_service_endpoint_mode = IPv6
+ec2_metadata_service_endpoint = [1234:ec2::456]
diff --git a/core/pom.xml b/core/pom.xml
index d782380b9b43..e973199c629c 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -45,6 +45,7 @@
metrics-spi
json-utils
endpoints-spi
+ imds
diff --git a/pom.xml b/pom.xml
index 1492399456d3..192ce68990b1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -609,6 +609,7 @@
url-connection-client
cloudwatch-metric-publisher
utils
+ imds
dynamodb-enhanced
diff --git a/test/tests-coverage-reporting/pom.xml b/test/tests-coverage-reporting/pom.xml
index 7ac33efe956e..7a56ff860b44 100644
--- a/test/tests-coverage-reporting/pom.xml
+++ b/test/tests-coverage-reporting/pom.xml
@@ -236,6 +236,11 @@
software.amazon.awssdk
${awsjavasdk.version}
+
+ software.amazon.awssdk
+ imds
+ ${awsjavasdk.version}
+