diff --git a/.changes/next-release/feature-AWSSDKforJavav2-4a07ee0.json b/.changes/next-release/feature-AWSSDKforJavav2-4a07ee0.json new file mode 100644 index 000000000000..4aabfebecb7b --- /dev/null +++ b/.changes/next-release/feature-AWSSDKforJavav2-4a07ee0.json @@ -0,0 +1,6 @@ +{ + "type": "feature", + "category": "AWS SDK for Java v2", + "contributor": "L-Applin", + "description": "EC2Metadata Asynchronous Client" +} diff --git a/core/imds/pom.xml b/core/imds/pom.xml index db567c65dfa7..354ece021363 100644 --- a/core/imds/pom.xml +++ b/core/imds/pom.xml @@ -58,7 +58,7 @@ software.amazon.awssdk url-connection-client ${awsjavasdk.version} - compile + test software.amazon.awssdk @@ -114,7 +114,7 @@ software.amazon.awssdk - apache-client + netty-nio-client ${awsjavasdk.version} test diff --git a/core/imds/src/main/java/software/amazon/awssdk/imds/Ec2Metadata.java b/core/imds/src/main/java/software/amazon/awssdk/imds/Ec2Metadata.java deleted file mode 100644 index b489b5065c90..000000000000 --- a/core/imds/src/main/java/software/amazon/awssdk/imds/Ec2Metadata.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.awssdk.imds; - -import java.net.URI; -import java.time.Duration; -import software.amazon.awssdk.annotations.SdkPublicApi; -import software.amazon.awssdk.http.SdkHttpClient; -import software.amazon.awssdk.imds.internal.DefaultEc2Metadata; - - -/** - * Interface to represent the Ec2Metadata Client Class. Used to access instance metadata from a running instance. - */ -@SdkPublicApi -public interface Ec2Metadata { - - /** - * Gets the specified instance metadata value by the given path. - * @param path Input path - * @return Instance metadata value as part of MetadataResponse Object - */ - MetadataResponse get(String path); - - /** - * @return The Builder Object consisting all the fields. - */ - Ec2Metadata.Builder toBuilder(); - - /** - * Create an {@code Ec2Metadata} using the default values. - */ - static Ec2Metadata create() { - return builder().build(); - } - - /** - * Creates a default builder for {@link Ec2Metadata}. - */ - static Ec2Metadata.Builder builder() { - return DefaultEc2Metadata.builder(); - } - - /** - * The builder definition for a {@link Ec2Metadata}. - */ - interface Builder { - - /** - * Define the retry policy which includes the number of retry attempts for any failed request. - * - * @param retryPolicy The retry policy which includes the number of retry attempts for any failed request. - * @return Returns a reference to this builder - */ - Builder retryPolicy(Ec2MetadataRetryPolicy retryPolicy); - - /** - * Define the endpoint of IMDS. - * - * @param endpoint The endpoint of IMDS. - * @return Returns a reference to this builder - */ - Builder endpoint(URI endpoint); - - /** - * Define the Time to live (TTL) of the token. - * - * @param tokenTtl The Time to live (TTL) of the token. - * @return Returns a reference to this builder - */ - Builder tokenTtl(Duration tokenTtl); - - /** - * Define the endpoint mode of IMDS. Supported values include IPv4 and IPv6. - * - * @param endpointMode The endpoint mode of IMDS.Supported values include IPv4 and IPv6. - * @return Returns a reference to this builder - */ - Builder endpointMode(EndpointMode endpointMode); - - /** - * Define the SdkHttpClient instance to make the http requests. - * - * @param httpClient The SdkHttpClient instance to make the http requests. - * @return Returns a reference to this builder - */ - Builder httpClient(SdkHttpClient httpClient); - - Ec2Metadata build(); - - } - -} diff --git a/core/imds/src/main/java/software/amazon/awssdk/imds/Ec2MetadataAsyncClient.java b/core/imds/src/main/java/software/amazon/awssdk/imds/Ec2MetadataAsyncClient.java new file mode 100644 index 000000000000..e53173e201d8 --- /dev/null +++ b/core/imds/src/main/java/software/amazon/awssdk/imds/Ec2MetadataAsyncClient.java @@ -0,0 +1,88 @@ +/* + * 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.concurrent.CompletableFuture; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.imds.internal.DefaultEc2MetadataAsyncClient; +import software.amazon.awssdk.utils.SdkAutoCloseable; + +/** + * Interface to represent the Ec2Metadata Client Class. Used to access instance metadata from a running instance. + */ +@SdkPublicApi +public interface Ec2MetadataAsyncClient extends SdkAutoCloseable { + + /** + * Gets the specified instance metadata value by the given path. + * + * @param path Input path + * @return A CompletableFuture that completes when the MetadataResponse is made available. + */ + CompletableFuture get(String path); + + /** + * Create an {@link Ec2MetadataAsyncClient} instance using the default values. + * + * @return + */ + static Ec2MetadataAsyncClient create() { + return builder().build(); + } + + static Ec2MetadataAsyncClient.Builder builder() { + return DefaultEc2MetadataAsyncClient.builder(); + } + + /** + * The builder definition for a {@link Ec2MetadataClient}. All parameters are optional and have default values if not + * specified. Therefore, an instance can be simply created with {@code Ec2MetadataAsyncClient.builder().build()} or + * {@code Ec2MetadataAsyncClient.create()}, both having the same result. + */ + interface Builder extends Ec2MetadataClientBuilder { + + /** + * Define the {@link ScheduledExecutorService} used to schedule asynchronous retry attempts. 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, 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); + } +} 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..23eb1a95bc23 --- /dev/null +++ b/core/imds/src/main/java/software/amazon/awssdk/imds/Ec2MetadataClient.java @@ -0,0 +1,73 @@ +/* + * 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.http.async.SdkAsyncHttpClient; +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 instance. + */ +@SdkPublicApi +public interface Ec2MetadataClient extends SdkAutoCloseable { + + /** + * Gets the specified instance metadata value by the given path. + * @param path Input path + * @return Instance metadata value as part of MetadataResponse Object + */ + MetadataResponse get(String path); + + /** + * Create an {@link Ec2MetadataClient} instance using the default values. + */ + static Ec2MetadataClient create() { + return builder().build(); + } + + /** + * Creates a default builder for {@link Ec2MetadataClient}. + */ + static Builder builder() { + return DefaultEc2MetadataClient.builder(); + } + + /** + * The builder definition for a {@link Ec2MetadataClient}. + */ + interface Builder extends Ec2MetadataClientBuilder { + + /** + * 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 SdkHttpClient 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(SdkHttpClient httpClient); + } + +} 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..c619ec3ba4e8 --- /dev/null +++ b/core/imds/src/main/java/software/amazon/awssdk/imds/Ec2MetadataClientBuilder.java @@ -0,0 +1,86 @@ +/* + * 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 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 Ec2MetadataClient + * @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} + *

+ * @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); + + Ec2MetadataRetryPolicy getRetryPolicy(); + + /** + * 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); + + URI getEndpoint(); + + /** + * 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); + + Duration getTokenTtl(); + + /** + * 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); + + EndpointMode getEndpointMode(); + +} 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 index 1870bf107712..7ac1ea3afb22 100644 --- a/core/imds/src/main/java/software/amazon/awssdk/imds/Ec2MetadataRetryPolicy.java +++ b/core/imds/src/main/java/software/amazon/awssdk/imds/Ec2MetadataRetryPolicy.java @@ -34,12 +34,14 @@ @SdkPublicApi public class Ec2MetadataRetryPolicy implements ToCopyableBuilder { + private static final int DEFAULT_RETRY_ATTEMPTS = 3; + private final BackoffStrategy backoffStrategy; private final int numRetries; private Ec2MetadataRetryPolicy(BuilderImpl builder) { - this.numRetries = builder.numRetries != null ? builder.numRetries : 3; + this.numRetries = builder.numRetries != null ? builder.numRetries : DEFAULT_RETRY_ATTEMPTS; this.backoffStrategy = builder.backoffStrategy != null ? builder.backoffStrategy : BackoffStrategy.defaultStrategy(RetryMode.STANDARD); diff --git a/core/imds/src/main/java/software/amazon/awssdk/imds/MetadataResponse.java b/core/imds/src/main/java/software/amazon/awssdk/imds/MetadataResponse.java index 412e3821f346..c24900da7271 100644 --- a/core/imds/src/main/java/software/amazon/awssdk/imds/MetadataResponse.java +++ b/core/imds/src/main/java/software/amazon/awssdk/imds/MetadataResponse.java @@ -28,7 +28,8 @@ import software.amazon.awssdk.utils.Validate; /** - * The class is used for Response Handling and Parsing the metadata fetched by the get call in the {@link Ec2Metadata} interface. + * The class is used for response handling and parsing the metadata fetched by the get call in the {@link Ec2MetadataClient} + * interface. * The class provides convenience methods to the users to parse the metadata as a String, List and Document. */ @SdkPublicApi 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..e7b0177fdd88 --- /dev/null +++ b/core/imds/src/main/java/software/amazon/awssdk/imds/internal/BaseEc2MetadataClient.java @@ -0,0 +1,77 @@ +/* + * 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.Ec2MetadataEndpointProvider.DEFAULT_ENDPOINT_PROVIDER; + +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.http.AbortableInputStream; +import software.amazon.awssdk.imds.Ec2MetadataClientBuilder; +import software.amazon.awssdk.imds.Ec2MetadataRetryPolicy; +import software.amazon.awssdk.imds.EndpointMode; +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); + 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; + + protected BaseEc2MetadataClient(Ec2MetadataClientBuilder builder) { + this.retryPolicy = Validate.getOrDefault(builder.getRetryPolicy(), Ec2MetadataRetryPolicy.builder()::build); + this.tokenTtl = Validate.getOrDefault(builder.getTokenTtl(), () -> DEFAULT_TOKEN_TTL); + this.endpoint = getEndpoint(builder); + this.requestMarshaller = new RequestMarshaller(this.endpoint); + } + + private URI getEndpoint(Ec2MetadataClientBuilder builder) { + URI builderEndpoint = builder.getEndpoint(); + EndpointMode builderEndpointMode = builder.getEndpointMode(); + 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(DEFAULT_ENDPOINT_PROVIDER.resolveEndpoint(builderEndpointMode)); + } + EndpointMode resolvedEndpointMode = DEFAULT_ENDPOINT_PROVIDER.resolveEndpointMode(); + return URI.create(DEFAULT_ENDPOINT_PROVIDER.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()); + } + } + +} 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..1ae015124bfb --- /dev/null +++ b/core/imds/src/main/java/software/amazon/awssdk/imds/internal/DefaultEc2MetadataAsyncClient.java @@ -0,0 +1,279 @@ +/* + * 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 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.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.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.core.internal.http.loader.DefaultSdkAsyncHttpClientBuilder; +import software.amazon.awssdk.core.retry.RetryPolicyContext; +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.imds.Ec2MetadataAsyncClient; +import software.amazon.awssdk.imds.Ec2MetadataRetryPolicy; +import software.amazon.awssdk.imds.EndpointMode; +import software.amazon.awssdk.imds.MetadataResponse; +import software.amazon.awssdk.utils.AttributeMap; +import software.amazon.awssdk.utils.CompletableFutureUtils; +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 DefaultEc2MetadataAsyncClient(Ec2MetadataAsyncBuilder builder) { + super(builder); + this.httpClient = Validate.getOrDefault(builder.httpClient, + () -> new DefaultSdkAsyncHttpClientBuilder().buildWithDefaults(AttributeMap.empty())); + this.asyncRetryScheduler = Validate.getOrDefault(builder.scheduledExecutorService, + () -> { + ThreadFactory threadFactory = new ThreadFactoryBuilder().threadNamePrefix("IMDS-ScheduledExecutor").build(); + return Executors.newScheduledThreadPool(DEFAULT_RETRY_THREAD_POOL_SIZE, threadFactory); + }); + this.httpClientIsInternal = builder.httpClient == null; + this.retryExecutorIsInternal = builder.scheduledExecutorService == null; + } + + 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) { + SdkHttpFullRequest baseTokenRequest = requestMarshaller.createTokenRequest(tokenTtl); + CompletableFuture tokenFuture = sendAsyncRequest(baseTokenRequest); + CompletableFutureUtils.forwardExceptionTo(returnFuture, tokenFuture); + + CompletableFuture result = tokenFuture.thenCompose(token -> { + SdkHttpFullRequest baseMetadataRequest = requestMarshaller.createDataRequest(path, token, tokenTtl); + return sendAsyncRequest(baseMetadataRequest); + }).thenApply(MetadataResponse::create); + + CompletableFutureUtils.forwardExceptionTo(tokenFuture, 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() + .request(baseTokenRequest) + .retriesAttempted(newAttempt) + .exception(SdkClientException.create(error.getMessage(), error)) + .build(); + scheduledRetryAttempt(() -> get(path, newContext, returnFuture), newContext); + }); + } + + private CompletableFuture sendAsyncRequest(SdkHttpFullRequest baseRequest) { + SdkHttpContentPublisher requestContentPublisher = new SimpleHttpContentPublisher(baseRequest); + StringResponseHandler stringResponseHandler = new StringResponseHandler(); + TransformingAsyncResponseHandler responseHandler = new AsyncResponseHandler<>(stringResponseHandler, + Function.identity(), + new ExecutionAttributes()); + CompletableFuture responseHandlerFuture = responseHandler.prepare(); + stringResponseHandler.setFuture(responseHandlerFuture); + AsyncExecuteRequest metadataRequest = AsyncExecuteRequest.builder() + .request(baseRequest) + .requestContentPublisher(requestContentPublisher) + .responseHandler(responseHandler) + .build(); + CompletableFuture executeFuture = httpClient.execute(metadataRequest); + CompletableFutureUtils.forwardExceptionTo(responseHandlerFuture, executeFuture); + return responseHandlerFuture; + } + + 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); + } + + private boolean shouldRetry(RetryPolicyContext retryPolicyContext, Throwable error) { + boolean maxAttemptReached = retryPolicyContext.retriesAttempted() >= retryPolicy.numRetries(); + if (maxAttemptReached) { + return false; + } + return error instanceof RetryableException || error.getCause() instanceof RetryableException; + } + + @Override + public void close() { + if (httpClientIsInternal) { + httpClient.close(); + } + if (retryExecutorIsInternal) { + asyncRetryScheduler.shutdown(); + } + } + + private static final class StringResponseHandler implements HttpResponseHandler { + private CompletableFuture future; + + public void setFuture(CompletableFuture future) { + this.future = future; + } + + @Override + public String handle(SdkHttpFullResponse response, ExecutionAttributes executionAttributes) throws Exception { + HttpStatusFamily statusCode = HttpStatusFamily.of(response.statusCode()); + if (statusCode.isOneOf(HttpStatusFamily.CLIENT_ERROR)) { + // non-retryable error + Supplier msg = () -> String.format("Error while executing EC2Metadata request: received http" + + " status %d", + response.statusCode()); + log.debug(msg); + future.completeExceptionally(SdkClientException.create(msg.get())); + } else if (statusCode.isOneOf(HttpStatusFamily.SERVER_ERROR)) { + // retryable error + Supplier msg = () -> String.format("Error while executing EC2Metadata request: received http" + + " status %d", + response.statusCode()); + log.debug(msg); + future.completeExceptionally(RetryableException.create(msg.get())); + } + AbortableInputStream inputStream = response + .content().orElseThrow(() -> SdkClientException.create("Unexpected error: empty response content")); + return uncheckedInputStreamToUtf8(inputStream); + } + } + + private static final class Ec2MetadataAsyncBuilder implements Ec2MetadataAsyncClient.Builder { + + private Ec2MetadataRetryPolicy retryPolicy; + + private URI endpoint; + + private Duration tokenTtl; + + private EndpointMode endpointMode; + + private SdkAsyncHttpClient httpClient; + + private ScheduledExecutorService scheduledExecutorService; + + private Ec2MetadataAsyncBuilder() { + } + + @Override + public Ec2MetadataAsyncBuilder retryPolicy(Ec2MetadataRetryPolicy retryPolicy) { + this.retryPolicy = retryPolicy; + 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 Ec2MetadataAsyncBuilder scheduledExecutorService(ScheduledExecutorService scheduledExecutorService) { + this.scheduledExecutorService = scheduledExecutorService; + return this; + } + + @Override + public Ec2MetadataRetryPolicy getRetryPolicy() { + return this.retryPolicy; + } + + @Override + public URI getEndpoint() { + return this.endpoint; + } + + @Override + public Duration getTokenTtl() { + return this.tokenTtl; + } + + @Override + public EndpointMode getEndpointMode() { + return this.endpointMode; + } + + @Override + public Ec2MetadataAsyncClient build() { + return new DefaultEc2MetadataAsyncClient(this); + } + } +} diff --git a/core/imds/src/main/java/software/amazon/awssdk/imds/internal/DefaultEc2Metadata.java b/core/imds/src/main/java/software/amazon/awssdk/imds/internal/DefaultEc2MetadataClient.java similarity index 70% rename from core/imds/src/main/java/software/amazon/awssdk/imds/internal/DefaultEc2Metadata.java rename to core/imds/src/main/java/software/amazon/awssdk/imds/internal/DefaultEc2MetadataClient.java index 4ee315075937..36f11cc3a2dd 100644 --- a/core/imds/src/main/java/software/amazon/awssdk/imds/internal/DefaultEc2Metadata.java +++ b/core/imds/src/main/java/software/amazon/awssdk/imds/internal/DefaultEc2MetadataClient.java @@ -26,19 +26,21 @@ 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.http.urlconnection.UrlConnectionHttpClient; -import software.amazon.awssdk.imds.Ec2Metadata; +import software.amazon.awssdk.imds.Ec2MetadataClient; import software.amazon.awssdk.imds.Ec2MetadataRetryPolicy; import software.amazon.awssdk.imds.EndpointMode; import software.amazon.awssdk.imds.MetadataResponse; +import software.amazon.awssdk.utils.AttributeMap; import software.amazon.awssdk.utils.IoUtils; import software.amazon.awssdk.utils.Logger; +import software.amazon.awssdk.utils.Validate; /** * An Implementation of the Ec2Metadata Interface. @@ -46,50 +48,29 @@ @SdkInternalApi @Immutable @ThreadSafe -public final class DefaultEc2Metadata implements Ec2Metadata { +public final class DefaultEc2MetadataClient extends BaseEc2MetadataClient implements Ec2MetadataClient { - private static final Logger log = Logger.loggerFor(DefaultEc2Metadata.class); - - private static final Ec2MetadataEndpointProvider ENDPOINT_PROVIDER = - Ec2MetadataEndpointProvider.builder().build(); - - private final Ec2MetadataRetryPolicy retryPolicy; - - private final URI endpoint; - - private final Duration tokenTtl; - - private final EndpointMode endpointMode; + private static final Logger log = Logger.loggerFor(DefaultEc2MetadataClient.class); private final SdkHttpClient httpClient; + private final boolean httpClientIsInternal; - private final RequestMarshaller requestMarshaller; - - private DefaultEc2Metadata(DefaultEc2Metadata.Ec2MetadataBuilder builder) { - this.retryPolicy = builder.retryPolicy != null ? builder.retryPolicy - : Ec2MetadataRetryPolicy.builder().build(); - this.endpointMode = builder.endpointMode != null ? builder.endpointMode - : ENDPOINT_PROVIDER.resolveEndpointMode(); - this.endpoint = builder.endpoint != null ? builder.endpoint - : URI.create(ENDPOINT_PROVIDER.resolveEndpoint(this.endpointMode)); - this.tokenTtl = builder.tokenTtl != null ? builder.tokenTtl - : Duration.ofSeconds(21600); - this.httpClient = builder.httpClient != null ? builder.httpClient - : UrlConnectionHttpClient.create(); - this.requestMarshaller = new RequestMarshaller(this.endpoint); + private DefaultEc2MetadataClient(DefaultEc2MetadataClient.Ec2MetadataBuilder builder) { + super(builder); + this.httpClient = Validate.getOrDefault(builder.httpClient, + () -> new DefaultSdkHttpClientBuilder().buildWithDefaults(AttributeMap.empty())); + this.httpClientIsInternal = builder.httpClient == null; } - public static Ec2Metadata.Builder builder() { - return new DefaultEc2Metadata.Ec2MetadataBuilder(); + @Override + public void close() { + if (httpClientIsInternal) { + httpClient.close(); + } } - @Override - public Ec2Metadata.Builder toBuilder() { - return builder().retryPolicy(retryPolicy) - .endpoint(endpoint) - .tokenTtl(tokenTtl) - .endpointMode(endpointMode) - .httpClient(httpClient); + public static Ec2MetadataBuilder builder() { + return new DefaultEc2MetadataClient.Ec2MetadataBuilder(); } /** @@ -141,14 +122,17 @@ public MetadataResponse get(String path) { private MetadataResponse sendRequest(String path, String token) throws IOException { - HttpExecuteRequest httpExecuteRequest = requestMarshaller.createDataRequest(path, token, tokenTtl); + 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(this::uncheckedInputStreamToUtf8) + responseBody.map(BaseEc2MetadataClient::uncheckedInputStreamToUtf8) .ifPresent(str -> log.debug(() -> "Metadata request response body: " + str)); throw RetryableException.builder() .message("The requested metadata at path ( " + path + " ) returned Http code " + statusCode) @@ -156,7 +140,7 @@ private MetadataResponse sendRequest(String path, String token) throws IOExcepti } if (!HttpStatusFamily.of(statusCode).isOneOf(HttpStatusFamily.SUCCESSFUL)) { - responseBody.map(this::uncheckedInputStreamToUtf8) + responseBody.map(BaseEc2MetadataClient::uncheckedInputStreamToUtf8) .ifPresent(str -> log.debug(() -> "Metadata request response body: " + str)); throw SdkClientException.builder() .message("The requested metadata at path ( " + path + " ) returned Http code " + statusCode).build(); @@ -183,20 +167,22 @@ private void pauseBeforeRetryIfNeeded(RetryPolicyContext retryPolicyContext) { } private String getToken() throws IOException { - HttpExecuteRequest httpExecuteRequest = requestMarshaller.createTokenRequest(tokenTtl); + HttpExecuteRequest httpExecuteRequest = HttpExecuteRequest.builder() + .request(requestMarshaller.createTokenRequest(tokenTtl)) + .build(); HttpExecuteResponse response = httpClient.prepareRequest(httpExecuteRequest).call(); int statusCode = response.httpResponse().statusCode(); if (HttpStatusFamily.of(statusCode).isOneOf(HttpStatusFamily.SERVER_ERROR)) { - response.responseBody().map(this::uncheckedInputStreamToUtf8) + 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(this::uncheckedInputStreamToUtf8) + 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.") @@ -209,17 +195,7 @@ private String getToken() throws IOException { return IoUtils.toUtf8String(abortableInputStream); } - private String uncheckedInputStreamToUtf8(AbortableInputStream inputStream) { - try { - return IoUtils.toUtf8String(inputStream); - } catch (IOException ioe) { - throw new UncheckedIOException(ioe); - } finally { - IoUtils.closeQuietly(inputStream, log.logger()); - } - } - - private static final class Ec2MetadataBuilder implements Ec2Metadata.Builder { + private static final class Ec2MetadataBuilder implements Ec2MetadataClient.Builder { private Ec2MetadataRetryPolicy retryPolicy; @@ -234,60 +210,59 @@ private static final class Ec2MetadataBuilder implements Ec2Metadata.Builder { private Ec2MetadataBuilder() { } - public void setRetryPolicy(Ec2MetadataRetryPolicy retryPolicy) { + @Override + public Ec2MetadataBuilder retryPolicy(Ec2MetadataRetryPolicy retryPolicy) { this.retryPolicy = retryPolicy; + return this; } - public void setEndpoint(URI endpoint) { + @Override + public Ec2MetadataBuilder endpoint(URI endpoint) { this.endpoint = endpoint; + return this; } - public void setTokenTtl(Duration tokenTtl) { + @Override + public Ec2MetadataBuilder tokenTtl(Duration tokenTtl) { this.tokenTtl = tokenTtl; + return this; } - public void setEndpointMode(EndpointMode endpointMode) { + @Override + public Ec2MetadataBuilder endpointMode(EndpointMode endpointMode) { this.endpointMode = endpointMode; - } - - public void setHttpClient(SdkHttpClient httpClient) { - this.httpClient = httpClient; + return this; } @Override - public Builder retryPolicy(Ec2MetadataRetryPolicy retryPolicy) { - this.retryPolicy = retryPolicy; + public Ec2MetadataBuilder httpClient(SdkHttpClient httpClient) { + this.httpClient = httpClient; return this; } @Override - public Builder endpoint(URI endpoint) { - this.endpoint = endpoint; - return this; + public Ec2MetadataRetryPolicy getRetryPolicy() { + return this.retryPolicy; } @Override - public Builder tokenTtl(Duration tokenTtl) { - this.tokenTtl = tokenTtl; - return this; + public URI getEndpoint() { + return this.endpoint; } @Override - public Builder endpointMode(EndpointMode endpointMode) { - this.endpointMode = endpointMode; - return this; + public Duration getTokenTtl() { + return this.tokenTtl; } @Override - public Builder httpClient(SdkHttpClient httpClient) { - - this.httpClient = httpClient; - return this; + public EndpointMode getEndpointMode() { + return this.endpointMode; } @Override - public Ec2Metadata build() { - return new DefaultEc2Metadata(this); + 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 index 1e2b570f0df6..1141bf25cb14 100644 --- 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 @@ -36,6 +36,8 @@ @SdkInternalApi public final class Ec2MetadataEndpointProvider { + public static final Ec2MetadataEndpointProvider DEFAULT_ENDPOINT_PROVIDER = builder().build(); + private static final EnumMap DEFAULT_ENDPOINT_MODE; static { @@ -52,9 +54,11 @@ private Ec2MetadataEndpointProvider(Builder builder) { this.profileName = builder.profileName; } + /** - * Resolve the endpoint to be used for the {@link DefaultEc2Metadata} client. Users may manually provide an endpoint through - * the {@code AWS_EC2_METADATA_SERVICE_ENDPOINT} environment variable or th {@code ec2_metadata_service_endpoint} key in + * 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 value are provide, the defaults to: *
    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 index b61a3e1c6fff..dde1dc2e4941 100644 --- 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 @@ -19,9 +19,8 @@ import java.time.Duration; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.core.util.SdkUserAgent; -import software.amazon.awssdk.http.HttpExecuteRequest; +import software.amazon.awssdk.http.SdkHttpFullRequest; import software.amazon.awssdk.http.SdkHttpMethod; -import software.amazon.awssdk.http.SdkHttpRequest; /** * Class to parse the parameters to a SdkHttpRequest , make the call to the endpoint and send the HttpExecuteResponse @@ -30,17 +29,17 @@ @SdkInternalApi public class RequestMarshaller { - private static final String TOKEN_RESOURCE_PATH = "/latest/api/token"; + public static final String TOKEN_RESOURCE_PATH = "/latest/api/token"; - private static final String TOKEN_HEADER = "x-aws-ec2-metadata-token"; + public static final String TOKEN_HEADER = "x-aws-ec2-metadata-token"; - private static final String EC2_METADATA_TOKEN_TTL_HEADER = "x-aws-ec2-metadata-token-ttl-seconds"; + public static final String EC2_METADATA_TOKEN_TTL_HEADER = "x-aws-ec2-metadata-token-ttl-seconds"; - private static final String USER_AGENT = "user_agent"; + public static final String USER_AGENT = "user_agent"; - private static final String ACCEPT = "Accept"; + public static final String ACCEPT = "Accept"; - private static final String CONNECTION = "connection"; + public static final String CONNECTION = "connection"; private final URI basePath; private final URI tokenPath; @@ -50,28 +49,26 @@ public RequestMarshaller(URI basePath) { this.tokenPath = URI.create(basePath + TOKEN_RESOURCE_PATH); } - public HttpExecuteRequest createTokenRequest(Duration tokenTtl) { - SdkHttpRequest sdkHttpRequest = defaulttHttpBuilder() + public SdkHttpFullRequest createTokenRequest(Duration tokenTtl) { + return defaulttHttpBuilder() .method(SdkHttpMethod.PUT) .uri(tokenPath) .putHeader(EC2_METADATA_TOKEN_TTL_HEADER, String.valueOf(tokenTtl.getSeconds())) .build(); - return HttpExecuteRequest.builder().request(sdkHttpRequest).build(); } - public HttpExecuteRequest createDataRequest(String path, String token, Duration tokenTtl) { + public SdkHttpFullRequest createDataRequest(String path, String token, Duration tokenTtl) { URI resourcePath = URI.create(basePath + path); - SdkHttpRequest sdkHttpRequest = defaulttHttpBuilder() + return defaulttHttpBuilder() .method(SdkHttpMethod.GET) .uri(resourcePath) .putHeader(EC2_METADATA_TOKEN_TTL_HEADER, String.valueOf(tokenTtl.getSeconds())) .putHeader(TOKEN_HEADER, token) .build(); - return HttpExecuteRequest.builder().request(sdkHttpRequest).build(); } - private SdkHttpRequest.Builder defaulttHttpBuilder() { - return SdkHttpRequest.builder() + 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/test/java/software/amazon/awssdk/imds/Ec2MetadataWithApacheClientTest.java b/core/imds/src/test/java/software/amazon/awssdk/imds/Ec2MetadataWithApacheClientTest.java deleted file mode 100644 index c74997c46f42..000000000000 --- a/core/imds/src/test/java/software/amazon/awssdk/imds/Ec2MetadataWithApacheClientTest.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.awssdk.imds; - -import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; -import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; -import static com.github.tomakehurst.wiremock.client.WireMock.get; -import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; -import static com.github.tomakehurst.wiremock.client.WireMock.put; -import static com.github.tomakehurst.wiremock.client.WireMock.putRequestedFor; -import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; -import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; -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 com.github.tomakehurst.wiremock.client.WireMock; -import com.github.tomakehurst.wiremock.http.Fault; -import com.github.tomakehurst.wiremock.junit.WireMockRule; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; -import software.amazon.awssdk.core.SdkSystemSetting; -import software.amazon.awssdk.core.exception.SdkClientException; -import software.amazon.awssdk.http.apache.ApacheHttpClient; - -/** - * Unit Tests to test the Ec2Metadata Client functionality with Apache HttpClient. - */ -@RunWith(MockitoJUnitRunner.class) -public class Ec2MetadataWithApacheClientTest { - - private static final String TOKEN_RESOURCE_PATH = "/latest/api/token"; - - private static final String TOKEN_HEADER = "x-aws-ec2-metadata-token"; - - private static final String EC2_METADATA_TOKEN_TTL_HEADER = "x-aws-ec2-metadata-token-ttl-seconds"; - - private static final String EC2_METADATA_ROOT = "/latest/meta-data"; - - private static final String AMI_ID_RESOURCE = EC2_METADATA_ROOT + "/ami-id"; - - @Rule - public WireMockRule mockMetadataEndpoint = new WireMockRule(); - - private Ec2Metadata ec2Metadata; - - @Before - public void methodSetup() { - System.setProperty(SdkSystemSetting.AWS_EC2_METADATA_SERVICE_ENDPOINT.property(), "http://localhost:" + mockMetadataEndpoint.port()); - this.ec2Metadata = Ec2Metadata.builder().httpClient(ApacheHttpClient.create()).build(); - } - - @Test - public void get_failsThrice_shouldThrowException() { - - stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody("some-token"))); - stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withFault(Fault.EMPTY_RESPONSE))); - - assertThatThrownBy(() -> ec2Metadata.get("/latest/meta-data/ami-id")) - .hasMessageContaining("Exceeded maximum number of retries.") - .isInstanceOf(SdkClientException.class); - WireMock.verify(4, putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)) - .withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600"))); - WireMock.verify(4, getRequestedFor(urlPathEqualTo(AMI_ID_RESOURCE)) - .withHeader(TOKEN_HEADER, equalTo("some-token"))); - } - - @Test - public void get_failsOnceWith401_shouldSucceedOnSecondAttempt() { - - stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody("some-token"))); - - stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).inScenario("Retry Scenario") - .whenScenarioStateIs(STARTED) - .willReturn(aResponse().withFault(Fault.EMPTY_RESPONSE)) - .willSetStateTo("Cause Success")); - - stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).inScenario("Retry Scenario") - .whenScenarioStateIs("Cause Success") - .willReturn(aResponse().withBody("{}"))); - - - MetadataResponse metadataResponse = ec2Metadata.get("/latest/meta-data/ami-id"); - assertThat(metadataResponse.asString()).isEqualTo("{}"); - - WireMock.verify(2, putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)) - .withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600"))); - WireMock.verify(2, getRequestedFor(urlPathEqualTo(AMI_ID_RESOURCE)) - .withHeader(TOKEN_HEADER, equalTo("some-token"))); - } - -} 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..a2b4e7fbd43e --- /dev/null +++ b/core/imds/src/test/java/software/amazon/awssdk/imds/internal/BaseEc2MetadataClientTest.java @@ -0,0 +1,245 @@ +/* + * 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 com.github.tomakehurst.wiremock.junit.WireMockRule; +import java.net.URI; +import java.time.Duration; +import java.util.function.Consumer; +import java.util.stream.Stream; +import org.junit.After; +import org.junit.Rule; +import org.junit.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.Ec2MetadataRetryPolicy; +import software.amazon.awssdk.imds.EndpointMode; +import software.amazon.awssdk.imds.MetadataResponse; + +public abstract class BaseEc2MetadataClientTest> { + + protected static final String TOKEN_RESOURCE_PATH = "/latest/api/token"; + protected static final String TOKEN_HEADER = "x-aws-ec2-metadata-token"; + protected static final String EC2_METADATA_TOKEN_TTL_HEADER = "x-aws-ec2-metadata-token-ttl-seconds"; + protected static final String EC2_METADATA_ROOT = "/latest/meta-data"; + protected static final String AMI_ID_RESOURCE = EC2_METADATA_ROOT + "/ami-id"; + protected static final int DEFAULT_TOTAL_ATTEMPTS = 4; + + @Rule + public WireMockRule mockMetadataEndpoint = new WireMockRule(); + + protected abstract BaseEc2MetadataClient overrideClient(Consumer builderConsumer); + + protected abstract void successAssertions(String path, Consumer assertions); + + protected abstract void failureAssertions(String path, Class exceptionType, + Consumer assertions); + + @After + public void reset() { + mockMetadataEndpoint.resetAll(); + } + + @Test + public void get_successOnFirstTry_shouldNotRetryAndSucceed() { + stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody("some-token"))); + 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 + public void get_failsEverytime_shouldRetryAndFails() { + stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody("some-token"))); + stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).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(DEFAULT_TOTAL_ATTEMPTS), getRequestedFor(urlPathEqualTo(AMI_ID_RESOURCE)) + .withHeader(TOKEN_HEADER, equalTo("some-token"))); + }); + } + + @Test + public void get_returnsStatus4XX_shouldFailsAndNotRetry() { + stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody("some-token"))); + 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 + public void get_failsOnceThenSucceed_withCustomClient_shouldSucceed() { + stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody("some-token"))); + 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:" + mockMetadataEndpoint.port())) + .tokenTtl(Duration.ofSeconds(1024))); + + successAssertions(AMI_ID_RESOURCE, response -> { + assertThat(response.asString()).isEqualTo("{}"); + verify(exactly(2), 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 + public 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 + public 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 + public 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"))); + 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:" + mockMetadataEndpoint.port())) + .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 + public 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:" + mockMetadataEndpoint.port())) + .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 + public void builder_endpointAndEndpointModeSpecified_shouldThrowIllegalArgException() { + assertThatThrownBy(() -> overrideClient(builder -> builder + .endpoint(URI.create("http://localhost:" + mockMetadataEndpoint.port())) + .endpointMode(IPV6))) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void builder_defaultValue_clientShouldUseIPV4Endpoint() { + BaseEc2MetadataClient client = overrideClient(builder -> {}); + assertThat(client.endpoint).hasToString("http://169.254.169.254"); + } + + @Test + public 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") + public 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]")); + } +} 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..01fb00e8a961 --- /dev/null +++ b/core/imds/src/test/java/software/amazon/awssdk/imds/internal/Ec2MetadataAsyncClientTest.java @@ -0,0 +1,159 @@ +/* + * 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 org.assertj.core.api.AssertionsForClassTypes.catchThrowable; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import software.amazon.awssdk.http.async.AsyncExecuteRequest; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.imds.Ec2MetadataAsyncClient; +import software.amazon.awssdk.imds.MetadataResponse; + +public class Ec2MetadataAsyncClientTest extends BaseEc2MetadataClientTest { + + private Ec2MetadataAsyncClient client; + + @Before + public void init() { + this.client = Ec2MetadataAsyncClient.builder() + .endpoint(URI.create("http://localhost:" + mockMetadataEndpoint.port())) + .build(); + } + + @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 + public void get_multipleAsyncRequest_shouldCompleteSuccessfully() { + int totalRequests = 128; + stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)) + .willReturn(aResponse().withFixedDelay(200).withBody("some-token"))); + for (int i = 0; i < totalRequests; i++) { + ResponseDefinitionBuilder responseStub = aResponse() + .withFixedDelay(300).withStatus(200).withBody("response::" + i); + stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE + "/" + i)).willReturn(responseStub)); + } + 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++) { + MetadataResponse response = resolvedResponses.get(i); + assertThat(response.asString()).isEqualTo("response::" + i); + } + verify(exactly(totalRequests), + 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("some-token"))); + } + + @Test + public void get_largeResponse_shouldSucceed() 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"))); + stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withBody(ec2MetadataContent))); + + try (Ec2MetadataAsyncClient client = + Ec2MetadataAsyncClient.builder().endpoint(URI.create("http://localhost:" + mockMetadataEndpoint.port())).build()) { + CompletableFuture res = client.get(AMI_ID_RESOURCE); + MetadataResponse 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"))); + } + } + + @Test + public void get_cancelResponseFuture_shouldPropagate() { + SdkAsyncHttpClient mockClient = Mockito.mock(SdkAsyncHttpClient.class); + CompletableFuture responseFuture = new CompletableFuture<>(); + when(mockClient.execute(any(AsyncExecuteRequest.class))).thenReturn(responseFuture); + overrideClient(builder -> builder.httpClient(mockClient)); + + Ec2MetadataAsyncClient mockedClient = Ec2MetadataAsyncClient.builder().httpClient(mockClient).build(); + CompletableFuture future = mockedClient.get(AMI_ID_RESOURCE); + future.cancel(true); + + assertThat(responseFuture).isCancelled(); + } +} 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..790c5457e1ea --- /dev/null +++ b/core/imds/src/test/java/software/amazon/awssdk/imds/internal/Ec2MetadataClientTest.java @@ -0,0 +1,59 @@ +/* + * 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.assertj.core.api.AssertionsForClassTypes.catchThrowable; + +import java.net.URI; +import java.util.function.Consumer; +import org.junit.Before; +import software.amazon.awssdk.imds.Ec2MetadataClient; +import software.amazon.awssdk.imds.MetadataResponse; + +public class Ec2MetadataClientTest extends BaseEc2MetadataClientTest { + + private Ec2MetadataClient client; + + @Before + public void init() { + this.client = Ec2MetadataClient.builder() + .endpoint(URI.create("http://localhost:" + mockMetadataEndpoint.port())) + .build(); + } + + @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) { + MetadataResponse 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); + } +} diff --git a/core/imds/src/test/java/software/amazon/awssdk/imds/internal/Ec2MetadataEndpointProviderTest.java b/core/imds/src/test/java/software/amazon/awssdk/imds/internal/Ec2MetadataEndpointProviderTest.java deleted file mode 100644 index 3a4dbd733cfb..000000000000 --- a/core/imds/src/test/java/software/amazon/awssdk/imds/internal/Ec2MetadataEndpointProviderTest.java +++ /dev/null @@ -1,394 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.awssdk.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.http.Fault.CONNECTION_RESET_BY_PEER; -import static com.github.tomakehurst.wiremock.http.Fault.EMPTY_RESPONSE; -import static com.github.tomakehurst.wiremock.http.Fault.MALFORMED_RESPONSE_CHUNK; -import static com.github.tomakehurst.wiremock.http.Fault.RANDOM_DATA_THEN_CLOSE; -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 com.github.tomakehurst.wiremock.client.WireMock; -import com.github.tomakehurst.wiremock.junit.WireMockRule; -import java.net.URI; -import java.time.Duration; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; -import software.amazon.awssdk.core.SdkSystemSetting; -import software.amazon.awssdk.core.exception.SdkClientException; -import software.amazon.awssdk.core.retry.backoff.BackoffStrategy; -import software.amazon.awssdk.core.retry.backoff.FixedDelayBackoffStrategy; -import software.amazon.awssdk.http.SdkHttpClient; -import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient; -import software.amazon.awssdk.imds.Ec2Metadata; -import software.amazon.awssdk.imds.Ec2MetadataRetryPolicy; -import software.amazon.awssdk.imds.MetadataResponse; - -/** - * Unit Tests to test the Ec2Metadata Client functionality - */ -@RunWith(MockitoJUnitRunner.class) -public class Ec2MetadataEndpointProviderTest { - - private static final String TOKEN_RESOURCE_PATH = "/latest/api/token"; - - private static final String TOKEN_HEADER = "x-aws-ec2-metadata-token"; - - private static final String EC2_METADATA_TOKEN_TTL_HEADER = "x-aws-ec2-metadata-token-ttl-seconds"; - - private static final String EC2_METADATA_ROOT = "/latest/meta-data"; - - private static final String AMI_ID_RESOURCE = EC2_METADATA_ROOT + "/ami-id"; - - @Rule - public WireMockRule mockMetadataEndpoint = new WireMockRule(); - - public Ec2Metadata ec2Metadata; - - @Before - public void methodSetup() { - System.setProperty(SdkSystemSetting.AWS_EC2_METADATA_SERVICE_ENDPOINT.property(), "http://localhost:" + mockMetadataEndpoint.port()); - this.ec2Metadata = Ec2Metadata.builder() - .endpoint(URI.create("http://localhost:" + mockMetadataEndpoint.port())) - .build(); - } - - @Test - public void get_succeedOnFirstAttempt() { - stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody("some-token"))); - stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withBody("{}"))); - - MetadataResponse metadataResponse = ec2Metadata.get("/latest/meta-data/ami-id"); - assertThat(metadataResponse.asString()).isEqualTo("{}"); - - WireMock.verify(1, putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)) - .withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600"))); - WireMock.verify(1, getRequestedFor(urlPathEqualTo(AMI_ID_RESOURCE)) - .withHeader(TOKEN_HEADER, equalTo("some-token"))); - } - - @Test - public void get_doesNotRetryOn404() { - - stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody("some-token"))); - stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withBody("{}").withStatus(404))); - - assertThatThrownBy(() -> ec2Metadata.get("/latest/meta-data/ami-id")) - .hasMessageContaining("metadata") - .isInstanceOf(SdkClientException.class); - WireMock.verify(1, putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)) - .withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600"))); - WireMock.verify(1, getRequestedFor(urlPathEqualTo(AMI_ID_RESOURCE)) - .withHeader(TOKEN_HEADER, equalTo("some-token"))); - } - - @Test - public void get_doesNotRetryOn401() { - - stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody("some-token"))); - stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withBody("{}").withStatus(401))); - - assertThatThrownBy(() -> ec2Metadata.get("/latest/meta-data/ami-id")) - .hasMessageContaining("401") - .isInstanceOf(SdkClientException.class); - - WireMock.verify(1, putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)) - .withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600"))); - WireMock.verify(1, getRequestedFor(urlPathEqualTo(AMI_ID_RESOURCE)) - .withHeader(TOKEN_HEADER, equalTo("some-token"))); - } - - @Test - public void getToken_doesNotRetryOn403() { - - stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withStatus(403))); - stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withBody("{}"))); - - assertThatThrownBy(() -> ec2Metadata.get("/latest/meta-data/ami-id")) - .hasMessageContaining("token") - .isInstanceOf(SdkClientException.class); - WireMock.verify(exactly(1), putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)) - .withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600"))); - WireMock.verify(0, getRequestedFor(urlPathEqualTo(AMI_ID_RESOURCE)) - .withHeader(TOKEN_HEADER, equalTo("some-token"))); - } - - @Test - public void getToken_doesNotRetryOn401() { - - stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withStatus(401))); - stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withBody("{}"))); - - assertThatThrownBy(() -> ec2Metadata.get("/latest/meta-data/ami-id")) - .hasMessageContaining("token") - .isInstanceOf(SdkClientException.class); - WireMock.verify(exactly(1), putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)) - .withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600"))); - WireMock.verify(0, getRequestedFor(urlPathEqualTo(AMI_ID_RESOURCE)) - .withHeader(TOKEN_HEADER, equalTo("some-token"))); - } - - @Test - public void get_failsThriceWithFixedDelay() { - - stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody("some-token"))); - stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withFixedDelay(2_000))); - - SdkHttpClient client = UrlConnectionHttpClient.builder().socketTimeout(Duration.ofMillis(500)).build(); - Ec2Metadata ec2MetadataRequest = Ec2Metadata.builder() - .httpClient(client) - .endpoint(URI.create("http://localhost:" + mockMetadataEndpoint.port())) - .build(); - assertThatThrownBy(() -> ec2MetadataRequest.get("/latest/meta-data/ami-id")) - .hasMessageContaining("Exceeded maximum number of retries.") - .isInstanceOf(SdkClientException.class); - - WireMock.verify(4, putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)) - .withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600"))); - WireMock.verify(4, getRequestedFor(urlPathEqualTo(AMI_ID_RESOURCE)) - .withHeader(TOKEN_HEADER, equalTo("some-token"))); - } - - - @Test - public void getToken_failsThriceWithFixedDelay() { - - stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)) - .willReturn(aResponse().withBody("some-token").withFixedDelay(2_000))); - - SdkHttpClient client = UrlConnectionHttpClient.builder().socketTimeout(Duration.ofMillis(500)).build(); - Ec2Metadata ec2MetadataRequest = Ec2Metadata.builder() - .endpoint(URI.create("http://localhost:" + mockMetadataEndpoint.port())) - .httpClient(client) - .build(); - assertThatThrownBy(() -> ec2MetadataRequest.get("/latest/meta-data/ami-id")) - .hasMessageContaining("Exceeded maximum number of retries.") - .isInstanceOf(SdkClientException.class); - - WireMock.verify(4, putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)) - .withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600"))); - WireMock.verify(0, getRequestedFor(urlPathEqualTo(AMI_ID_RESOURCE)) - .withHeader(TOKEN_HEADER, equalTo("some-token"))); - } - - @Test - public void getToken_failsOnceThenSucceedOnSecondAttempt() { - - stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).inScenario("Retry Scenario") - .whenScenarioStateIs(STARTED) - .willReturn(aResponse().withFault(CONNECTION_RESET_BY_PEER)) - .willSetStateTo("Cause Success")); - - stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).inScenario("Retry Scenario") - .whenScenarioStateIs("Cause Success") - .willReturn(aResponse().withStatus(200).withBody("some-token"))); - - stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withBody("{}"))); - - MetadataResponse metadataResponse = ec2Metadata.get("/latest/meta-data/ami-id"); - assertThat(metadataResponse.asString()).isEqualTo("{}"); - - WireMock.verify(2, putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)) - .withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600"))); - WireMock.verify(1, getRequestedFor(urlPathEqualTo(AMI_ID_RESOURCE)) - .withHeader(TOKEN_HEADER, equalTo("some-token"))); - } - - @Test - public void get_failsOnceThenSucceedOnSecondAttempt() { - - stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody("some-token"))); - - stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).inScenario("Retry Scenario") - .whenScenarioStateIs(STARTED) - .willReturn(aResponse().withFault(RANDOM_DATA_THEN_CLOSE)) - .willSetStateTo("Cause Success")); - - stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).inScenario("Retry Scenario") - .whenScenarioStateIs("Cause Success") - .willReturn(aResponse().withBody("{}"))); - - - MetadataResponse metadataResponse = ec2Metadata.get("/latest/meta-data/ami-id"); - assertThat(metadataResponse.asString()).isEqualTo("{}"); - - WireMock.verify(2, putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)) - .withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600"))); - WireMock.verify(2, getRequestedFor(urlPathEqualTo(AMI_ID_RESOURCE)) - .withHeader(TOKEN_HEADER, equalTo("some-token"))); - - } - - @Test - public void get_whenGetTokenFailsOnceThenOk_shouldSucceedOnSecondAttempt() { - - stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).inScenario("Retry Scenario") - .whenScenarioStateIs(STARTED) - .willReturn(aResponse().withFault(EMPTY_RESPONSE)) - .willSetStateTo("Cause Success")); - - stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).inScenario("Retry Scenario") - .whenScenarioStateIs("Cause Success") - .willReturn(aResponse().withBody("some-token"))); - - stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).inScenario("Retry Scenario") - .whenScenarioStateIs("Cause Success") - .willReturn(aResponse().withBody("{}"))); - - - MetadataResponse metadataResponse = ec2Metadata.get("/latest/meta-data/ami-id"); - assertThat(metadataResponse.asString()).isEqualTo("{}"); - - WireMock.verify(2, putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)) - .withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600"))); - WireMock.verify(exactly(1), getRequestedFor(urlPathEqualTo(AMI_ID_RESOURCE)) - .withHeader(TOKEN_HEADER, equalTo("some-token"))); - } - - @Test - public void getToken_failedTwice_shouldSucceedOnThirdAttempt() { - - stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).inScenario("Retry Scenario") - .whenScenarioStateIs(STARTED) - .willReturn(aResponse().withFault(MALFORMED_RESPONSE_CHUNK)) - .willSetStateTo("Try-2")); - - stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).inScenario("Retry Scenario") - .whenScenarioStateIs("Try-2") - .willReturn(aResponse().withFault(CONNECTION_RESET_BY_PEER)) - .willSetStateTo("Try-3")); - - stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).inScenario("Retry Scenario") - .whenScenarioStateIs("Try-3") - .willReturn(aResponse().withBody("some-token"))); - - stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)) - .withHeader(TOKEN_HEADER, equalTo("some-token")) - .willReturn(aResponse().withBody("{}"))); - - MetadataResponse metadataResponse = ec2Metadata.get("/latest/meta-data/ami-id"); - assertThat(metadataResponse.asString()).isEqualTo("{}"); - - WireMock.verify(3, putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)) - .withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600"))); - WireMock.verify(exactly(1), getRequestedFor(urlPathEqualTo(AMI_ID_RESOURCE)) - .withHeader(TOKEN_HEADER, equalTo("some-token"))); - } - - @Test - public void getToken_failsTwiceWithIOExceptionThenOK_shouldSucceedOnThirdAttempt() { - - stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).inScenario("Retry Scenario") - .whenScenarioStateIs(STARTED) - .willReturn(aResponse().withFault(MALFORMED_RESPONSE_CHUNK)) - .willSetStateTo("Try-2")); - - stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).inScenario("Retry Scenario") - .whenScenarioStateIs("Try-2") - .willReturn(aResponse().withFault(RANDOM_DATA_THEN_CLOSE)) - .willSetStateTo("Try-3")); - - stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).inScenario("Retry Scenario") - .whenScenarioStateIs("Try-3") - .willReturn(aResponse().withBody("valid-token"))); - - stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)) - .withHeader(TOKEN_HEADER, equalTo("valid-token")) - .willReturn(aResponse().withBody("{}"))); - - MetadataResponse metadataResponse = ec2Metadata.get("/latest/meta-data/ami-id"); - assertThat(metadataResponse.asString()).isEqualTo("{}"); - - WireMock.verify(3, putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)) - .withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600"))); - WireMock.verify(1, getRequestedFor(urlPathEqualTo(AMI_ID_RESOURCE)) - .withHeader(TOKEN_HEADER, equalTo("valid-token"))); - } - - @Test - public void get_failsEveryAttempt_shouldThrowOnFourthAttempt() { - - stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody("some-token"))); - stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withFault(MALFORMED_RESPONSE_CHUNK))); - - assertThatThrownBy(() -> ec2Metadata.get("/latest/meta-data/ami-id")) - .hasMessageContaining("Exceeded maximum number of retries.") - .isInstanceOf(SdkClientException.class); - - WireMock.verify(4, putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)) - .withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600"))); - WireMock.verify(4, getRequestedFor(urlPathEqualTo(AMI_ID_RESOURCE)) - .withHeader(TOKEN_HEADER, equalTo("some-token"))); - } - - @Test - public void get_customRetryAmount() { - stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody("some-token"))); - stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withFault(RANDOM_DATA_THEN_CLOSE))); - - int numRetries = 7; - BackoffStrategy noWait = FixedDelayBackoffStrategy.create(Duration.ofMillis(10)); - Ec2Metadata ec2MetadataRequest = Ec2Metadata.builder() - .endpoint(URI.create("http://localhost:" + mockMetadataEndpoint.port())) - .retryPolicy(Ec2MetadataRetryPolicy.builder() - .backoffStrategy(noWait) - .numRetries(numRetries) - .build()) - .build(); - assertThatThrownBy(() -> ec2MetadataRequest.get("/latest/meta-data/ami-id")) - .hasMessageContaining("Exceeded maximum number of retries.") - .isInstanceOf(SdkClientException.class); - - WireMock.verify(8, putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)) - .withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600"))); - WireMock.verify(8, getRequestedFor(urlPathEqualTo(AMI_ID_RESOURCE)) - .withHeader(TOKEN_HEADER, equalTo("some-token"))); - } - - @Test - public void get_withOneMaxRetries_shouldNotRetry() { - stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody("some-token"))); - stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withFault(RANDOM_DATA_THEN_CLOSE))); - - Ec2Metadata ec2MetadataRequest = Ec2Metadata.builder() - .endpoint(URI.create("http://localhost:" + mockMetadataEndpoint.port())) - .retryPolicy(Ec2MetadataRetryPolicy.none()) - .build(); - assertThatThrownBy(() -> ec2MetadataRequest.get("/latest/meta-data/ami-id")) - .hasMessageContaining("Exceeded maximum number of retries.") - .isInstanceOf(SdkClientException.class); - - WireMock.verify(1, putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)) - .withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600"))); - WireMock.verify(1, putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)) - .withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600"))); - - } -} 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 index f414174dfaee..114049cdb1dc 100644 --- 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 @@ -25,11 +25,13 @@ 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; @@ -133,4 +135,31 @@ void endpointModeCheck(boolean useEnvVariable, String envVarValue, boolean useCo 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/test/tests-coverage-reporting/pom.xml b/test/tests-coverage-reporting/pom.xml index d230e3055fdd..53ef2634c3fc 100644 --- a/test/tests-coverage-reporting/pom.xml +++ b/test/tests-coverage-reporting/pom.xml @@ -221,6 +221,11 @@ software.amazon.awssdk ${awsjavasdk.version}-PREVIEW + + software.amazon.awssdk + imds + ${awsjavasdk.version} +