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
index 1d89b27c6bb1..cd54ba1c75f4 100644
--- a/core/imds/src/main/java/software/amazon/awssdk/imds/Ec2Metadata.java
+++ b/core/imds/src/main/java/software/amazon/awssdk/imds/Ec2Metadata.java
@@ -18,7 +18,6 @@
import java.net.URI;
import java.time.Duration;
import software.amazon.awssdk.annotations.SdkPublicApi;
-import software.amazon.awssdk.core.retry.RetryPolicy;
import software.amazon.awssdk.http.SdkHttpClient;
import software.amazon.awssdk.imds.internal.DefaultEc2Metadata;
import software.amazon.awssdk.imds.internal.EndpointMode;
@@ -67,7 +66,7 @@ interface Builder {
* @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(RetryPolicy retryPolicy);
+ Builder retryPolicy(Ec2MetadataRetryPolicy retryPolicy);
/**
* Define the endpoint of IMDS.
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..0b7638e34e4b
--- /dev/null
+++ b/core/imds/src/main/java/software/amazon/awssdk/imds/Ec2MetadataRetryPolicy.java
@@ -0,0 +1,155 @@
+/*
+ * 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.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 class Ec2MetadataRetryPolicy implements ToCopyableBuilder {
+
+ private final BackoffStrategy backoffStrategy;
+ private final int numRetries;
+
+ private Ec2MetadataRetryPolicy(BuilderImpl builder) {
+
+ this.numRetries = builder.numRetries != null ? builder.numRetries : 3;
+
+ this.backoffStrategy = builder.backoffStrategy != null ? builder.backoffStrategy :
+ BackoffStrategy.defaultStrategy(RetryMode.STANDARD);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ Ec2MetadataRetryPolicy ec2MetadataRetryPolicy = (Ec2MetadataRetryPolicy) o;
+
+ if (!Objects.equals(numRetries, ec2MetadataRetryPolicy.numRetries)) {
+ return false;
+ }
+ return Objects.equals(backoffStrategy, ec2MetadataRetryPolicy.backoffStrategy);
+ }
+
+ @Override
+ public int hashCode() {
+
+ int result = numRetries >= 0 ? 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 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);
+
+ @Override
+ Ec2MetadataRetryPolicy build();
+ }
+
+ 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;
+ }
+
+ public void setNumRetries(Integer numRetries) {
+ numRetries(numRetries);
+ }
+
+ @Override
+ public Builder backoffStrategy(BackoffStrategy backoffStrategy) {
+ this.backoffStrategy = backoffStrategy;
+ return this;
+ }
+
+ public void setBackoffStrategy(BackoffStrategy backoffStrategy) {
+ backoffStrategy(backoffStrategy);
+ }
+
+ @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/internal/DefaultEc2Metadata.java b/core/imds/src/main/java/software/amazon/awssdk/imds/internal/DefaultEc2Metadata.java
index 5d1d415d9b2e..e8bcdb55cf98 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/DefaultEc2Metadata.java
@@ -16,19 +16,15 @@
package software.amazon.awssdk.imds.internal;
import java.io.IOException;
-import java.net.HttpURLConnection;
import java.net.URI;
import java.time.Duration;
import java.util.Objects;
import java.util.Optional;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
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.exception.SdkServiceException;
-import software.amazon.awssdk.core.retry.RetryPolicy;
+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;
@@ -36,8 +32,10 @@
import software.amazon.awssdk.http.SdkHttpMethod;
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;
import software.amazon.awssdk.utils.IoUtils;
+import software.amazon.awssdk.utils.Logger;
/**
* An Implementation of the Ec2Metadata Interface.
@@ -49,12 +47,13 @@ public final class DefaultEc2Metadata implements Ec2Metadata {
private static final String TOKEN_RESOURCE_PATH = "/latest/api/token";
- private static final Logger log = LoggerFactory.getLogger(DefaultEc2Metadata.class);
+ private static final Logger log = Logger.loggerFor(DefaultEc2Metadata.class);
private static final RequestMarshaller REQUEST_MARSHALLER = new RequestMarshaller();
private static final EndpointProvider ENDPOINT_PROVIDER = EndpointProvider.builder().build();
- private final RetryPolicy retryPolicy;
+
+ private final Ec2MetadataRetryPolicy retryPolicy;
private final URI endpoint;
@@ -68,7 +67,7 @@ public final class DefaultEc2Metadata implements Ec2Metadata {
private DefaultEc2Metadata(DefaultEc2Metadata.Ec2MetadataBuilder builder) {
- this.retryPolicy = builder.retryPolicy != null ? builder.retryPolicy : RetryPolicy.builder().build();
+ this.retryPolicy = builder.retryPolicy != null ? builder.retryPolicy : Ec2MetadataRetryPolicy.builder().build();
this.endpoint = URI.create(ENDPOINT_PROVIDER.resolveEndpoint(builder.endpoint, builder.endpointMode));
this.tokenTtl = builder.tokenTtl != null ? builder.tokenTtl : Duration.ofSeconds(21600);
this.endpointMode = ENDPOINT_PROVIDER.resolveEndpointMode(builder.endpointMode);
@@ -152,78 +151,130 @@ public String toString() {
public MetadataResponse get(String path) {
MetadataResponse metadataResponse = null;
- String data = null;
AbortableInputStream abortableInputStream = null;
- try {
- String token = getToken();
- URI uri = URI.create(endpoint + path);
- HttpExecuteRequest httpExecuteRequest = REQUEST_MARSHALLER.createDataRequest(uri, SdkHttpMethod.GET, token,
- tokenTtl);
- HttpExecuteResponse response = httpClient.prepareRequest(httpExecuteRequest).call();
- int statusCode = response.httpResponse().statusCode();
- Optional responseBody = response.responseBody();
- if (statusCode == HttpURLConnection.HTTP_OK && responseBody.isPresent()) {
- abortableInputStream = responseBody.get();
- data = IoUtils.toUtf8String(abortableInputStream);
- metadataResponse = new MetadataResponse(data);
- } else if (statusCode == HttpURLConnection.HTTP_NOT_FOUND) {
- throw SdkServiceException.builder()
- .message("The requested metadata at path ( " + path + " ) is not found ").build();
- } else if (statusCode == HttpURLConnection.HTTP_OK) {
- throw SdkClientException.builder()
- .message("Response body empty with Status Code " + statusCode).build();
- } else {
- throw SdkClientException.builder()
- .message("Instance metadata service returned unexpected status code " + statusCode)
- .build();
+ for (int tries = 1 ; tries <= retryPolicy.numRetries() + 1; tries ++) {
+
+ try {
+ Optional token = getToken();
+ if (token.isPresent()) {
+ HttpExecuteResponse response = getDataHttpResponse(path, token.get());
+
+ int statusCode = response.httpResponse().statusCode();
+ Optional responseBody = response.responseBody();
+
+ if (statusCode == 200) {
+ if (!responseBody.isPresent()) {
+ throw SdkClientException.builder()
+ .message("Response body empty with Status Code " + statusCode).build();
+ }
+ abortableInputStream = responseBody.get();
+ String data = IoUtils.toUtf8String(abortableInputStream);
+ metadataResponse = new MetadataResponse(data);
+ return metadataResponse;
+ }
+ handleException(statusCode, path);
+ }
+ //TODO Create IMDS Custom Exception
+ } catch (IOException io) {
+ log.warn(() -> "Received an IOException ", io);
+ } finally {
+ IoUtils.closeQuietly(abortableInputStream, log.logger());
}
- } catch (SdkServiceException sd) {
- throw SdkServiceException.builder().message(sd.getMessage()).cause(sd).build();
- } catch (IOException | SdkClientException io) {
- // TODO Retry Logic will be added
- log.warn("Received an IOException {0} " , io);
- } finally {
- IoUtils.closeQuietly(abortableInputStream, log);
+ pauseBeforeRetryIfNeeded(tries);
}
-
return metadataResponse;
}
- private String getToken() throws IOException {
+ private void handleException(int statusCode, String path) {
+ if (statusCode == 404) {
+ throw SdkClientException.builder()
+ .message("The requested metadata at path ( " + path + " ) is not found ").build();
+ }
+ }
+
+ private void pauseBeforeRetryIfNeeded(int tries) {
+
+ if (tries == retryPolicy.numRetries() + 1) {
+ throw SdkClientException.builder().message("Exceeded maximum number of retries.").build();
+ }
+
+ try {
+ long backoffTimeMillis = getBackoffDuration(tries);
+ Thread.sleep(backoffTimeMillis);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw SdkClientException.builder().message("Thread interrupted while trying to sleep").cause(e).build();
+ }
+ }
+
+ private HttpExecuteResponse getDataHttpResponse(String path, String token) throws IOException {
+
+ URI uri = URI.create(endpoint + path);
+ HttpExecuteRequest httpExecuteRequest = REQUEST_MARSHALLER.createDataRequest(uri, SdkHttpMethod.GET, token,
+ tokenTtl);
+ return httpClient.prepareRequest(httpExecuteRequest).call();
+ }
+
+ private long getBackoffDuration(int tries) {
+
+ long backoffTime = 0L;
+ if (tries >= 1) {
+ backoffTime = retryPolicy.backoffStrategy()
+ .computeDelayBeforeNextRetry(RetryPolicyContext.builder()
+ .retriesAttempted(tries - 1)
+ .build()).toMillis();
+ }
+
+ return backoffTime;
+ }
+
+ private Optional getToken() throws IOException {
AbortableInputStream abortableInputStream = null;
try {
- URI uri = URI.create(endpoint + TOKEN_RESOURCE_PATH);
- HttpExecuteRequest httpExecuteRequest = REQUEST_MARSHALLER.createTokenRequest(uri, SdkHttpMethod.PUT, tokenTtl);
- HttpExecuteResponse response = httpClient.prepareRequest(httpExecuteRequest).call();
+ HttpExecuteResponse response = getTokenHttpResponse();
int statusCode = response.httpResponse().statusCode();
Optional responseBody = response.responseBody();
- if (statusCode == HttpURLConnection.HTTP_OK && responseBody.isPresent()) {
+ if (statusCode == 200) {
+ if (!responseBody.isPresent()) {
+ throw SdkClientException.builder()
+ .message("Response body empty with Status Code " + statusCode).build();
+ }
abortableInputStream = responseBody.get();
- return IoUtils.toUtf8String(abortableInputStream);
- } else if (statusCode == HttpURLConnection.HTTP_FORBIDDEN || statusCode == HttpURLConnection.HTTP_BAD_REQUEST) {
- throw SdkServiceException.builder()
- .message("Could not retrieve token as " + statusCode + " error occurred.").build();
- } else if (statusCode == HttpURLConnection.HTTP_OK) {
- throw SdkClientException.builder()
- .message("Response body empty with Status Code " + statusCode).build();
- } else {
- throw SdkClientException.builder()
- .message("Instance metadata service returned unexpected status code " + statusCode)
- .build();
+ return Optional.of(IoUtils.toUtf8String(abortableInputStream));
}
+ handleErrorResponse(statusCode);
} catch (IOException e) {
+ log.warn(() -> "Received an IOException ", e);
throw e;
} finally {
- IoUtils.closeQuietly(abortableInputStream, log);
+ IoUtils.closeQuietly(abortableInputStream, log.logger());
}
+ return Optional.empty();
+ }
+
+ private void handleErrorResponse(int statusCode) {
+
+ if (statusCode == 403 || statusCode == 400) {
+ throw SdkClientException.builder()
+ .message("Could not retrieve token as " + statusCode + " error occurred.").build();
+ }
+ }
+
+
+ private HttpExecuteResponse getTokenHttpResponse() throws IOException {
+
+ URI uri = URI.create(endpoint + TOKEN_RESOURCE_PATH);
+ HttpExecuteRequest httpExecuteRequest = REQUEST_MARSHALLER.createTokenRequest(uri, SdkHttpMethod.PUT, tokenTtl);
+ return httpClient.prepareRequest(httpExecuteRequest).call();
+
}
private static final class Ec2MetadataBuilder implements Ec2Metadata.Builder {
- private RetryPolicy retryPolicy;
+ private Ec2MetadataRetryPolicy retryPolicy;
private URI endpoint;
@@ -238,7 +289,7 @@ private static final class Ec2MetadataBuilder implements Ec2Metadata.Builder {
private Ec2MetadataBuilder() {
}
- public void setRetryPolicy(RetryPolicy retryPolicy) {
+ public void setRetryPolicy(Ec2MetadataRetryPolicy retryPolicy) {
this.retryPolicy = retryPolicy;
}
@@ -263,7 +314,7 @@ public void setHttpClient(SdkHttpClient httpClient) {
}
@Override
- public Builder retryPolicy(RetryPolicy retryPolicy) {
+ public Builder retryPolicy(Ec2MetadataRetryPolicy retryPolicy) {
this.retryPolicy = retryPolicy;
return this;
}
diff --git a/core/imds/src/test/java/software/amazon/awssdk/imds/Ec2MetadataTest.java b/core/imds/src/test/java/software/amazon/awssdk/imds/Ec2MetadataTest.java
index 70bf073ed144..61f6a03264a6 100644
--- a/core/imds/src/test/java/software/amazon/awssdk/imds/Ec2MetadataTest.java
+++ b/core/imds/src/test/java/software/amazon/awssdk/imds/Ec2MetadataTest.java
@@ -23,10 +23,13 @@
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 static org.mockito.Mockito.when;
import com.github.tomakehurst.wiremock.client.WireMock;
+import com.github.tomakehurst.wiremock.http.Fault;
import com.github.tomakehurst.wiremock.junit.WireMockRule;
import java.io.IOException;
import java.net.URI;
@@ -34,14 +37,11 @@
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
-import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import software.amazon.awssdk.core.SdkSystemSetting;
-import software.amazon.awssdk.core.exception.SdkServiceException;
-import software.amazon.awssdk.protocols.jsoncore.JsonNode;
-import software.amazon.awssdk.protocols.jsoncore.JsonNodeParser;
+import software.amazon.awssdk.core.exception.SdkClientException;
/**
* Unit Tests to test the Ec2Metadata Client functionality
@@ -50,7 +50,9 @@
public class Ec2MetadataTest {
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";
@@ -60,14 +62,9 @@ public class Ec2MetadataTest {
@Mock
private Ec2Metadata ec2Metadata;
- @Rule
- public ExpectedException thrown = ExpectedException.none();
-
@Rule
public WireMockRule mockMetadataEndpoint = new WireMockRule();
- private static final JsonNodeParser jsonParser = JsonNode.parser();
-
@Before
public void methodSetup() {
System.setProperty(SdkSystemSetting.AWS_EC2_METADATA_SERVICE_ENDPOINT.property(), "http://localhost:" + mockMetadataEndpoint.port());
@@ -75,7 +72,7 @@ public void methodSetup() {
}
@Test
- public void when_dummy_string_is_returned(){
+ public void get_whenDummyResponseIsReturned(){
MetadataResponse metadataResponse = new MetadataResponse("IMDS");
when(ec2Metadata.get("/ami-id")).thenReturn(metadataResponse);
@@ -84,7 +81,7 @@ public void when_dummy_string_is_returned(){
}
@Test
- public void verify_equals_hashcode(){
+ public void verifyEc2Metadata_equalsAndHashcode(){
EqualsVerifier.forClass(Ec2Metadata.class)
.usingGetClass()
@@ -92,7 +89,7 @@ public void verify_equals_hashcode(){
}
@Test
- public void get_AmiId_onMetadataResource_200_Success() throws IOException {
+ public void get_shouldSucceedOnFirstAttempt() throws IOException {
stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody("some-token")));
stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withBody("{}")));
@@ -106,94 +103,282 @@ public void get_AmiId_onMetadataResource_200_Success() throws IOException {
}
@Test
- public void get_AmiId_onMetadataResource_404Error_throws() throws IOException {
-
- thrown.expect(SdkServiceException.class);
- thrown.expectMessage("metadata");
+ public void get_failedThriceWith404() throws IOException {
stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody("some-token")));
stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withBody("{}").withStatus(404)));
- Ec2Metadata ec2Metadata = Ec2Metadata.builder().endpoint(URI.create("http://localhost:8080")).build();
- MetadataResponse metadataResponse = ec2Metadata.get("/latest/meta-data/ami-id");
+ assertThatThrownBy(() -> {
+ Ec2Metadata ec2Metadata = Ec2Metadata.builder().endpoint(URI.create("http://localhost:8080")).build();
+ MetadataResponse metadataResponse = ec2Metadata.get("/latest/meta-data/ami-id");
+ }).hasMessageContaining("metadata")
+ .isInstanceOf(SdkClientException.class);
}
@Test
- public void get_AmiId_onMetadataResource_401Error_throws() throws IOException {
+ public void get_failedThriceWith401() throws IOException {
stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody("some-token")));
stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withBody("{}").withStatus(401)));
- Ec2Metadata ec2Metadata = Ec2Metadata.builder().endpoint(URI.create("http://localhost:8080")).build();
- MetadataResponse metadataResponse = ec2Metadata.get("/latest/meta-data/ami-id");
- assertThat(metadataResponse).isNull();
+ assertThatThrownBy(() -> {
+ Ec2Metadata ec2Metadata = Ec2Metadata.builder().endpoint(URI.create("http://localhost:8080")).build();
+ MetadataResponse metadataResponse = ec2Metadata.get("/latest/meta-data/ami-id");
+ }).hasMessageContaining("Exceeded maximum number of retries.")
+ .isInstanceOf(SdkClientException.class);
}
@Test
- public void get_AmiId_onMetadataResource_IOException_throws() {
+ public void get_failedThriceWithFixedDelay() {
stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody("some-token")));
stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withFixedDelay(Integer.MAX_VALUE)));
- Ec2Metadata ec2Metadata = Ec2Metadata.builder().endpoint(URI.create("http://localhost:8080")).build();
-
- MetadataResponse metadataResponse = ec2Metadata.get("/latest/meta-data/ami-id");
- assertThat(metadataResponse).isNull();
+ assertThatThrownBy(() -> {
+ Ec2Metadata ec2Metadata = Ec2Metadata.builder().endpoint(URI.create("http://localhost:8080")).build();
+ MetadataResponse metadataResponse = ec2Metadata.get("/latest/meta-data/ami-id");
+ }).hasMessageContaining("Exceeded maximum number of retries.")
+ .isInstanceOf(SdkClientException.class);
WireMock.verify(putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)).withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600")));
WireMock.verify(getRequestedFor(urlPathEqualTo(AMI_ID_RESOURCE)).withHeader(TOKEN_HEADER, equalTo("some-token")));
}
@Test
- public void get_AmiId_onTokenResource_403Error_throws() throws IOException {
-
- thrown.expect(SdkServiceException.class);
- thrown.expectMessage("token");
+ public void getToken_failedThriceWith403() throws IOException {
stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withStatus(403)));
stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withBody("{}")));
- Ec2Metadata ec2Metadata = Ec2Metadata.builder().endpoint(URI.create("http://localhost:8080")).build();
- MetadataResponse metadataResponse = ec2Metadata.get("/latest/meta-data/ami-id");
+ assertThatThrownBy(() -> {
+ Ec2Metadata ec2Metadata = Ec2Metadata.builder().endpoint(URI.create("http://localhost:8080")).build();
+ MetadataResponse metadataResponse = ec2Metadata.get("/latest/meta-data/ami-id");
+ }).hasMessageContaining("token")
+ .isInstanceOf(SdkClientException.class);
}
@Test
- public void get_AmiId_onTokenResource_401Error_throws() throws IOException {
+ public void getToken_failedThriceWith401() throws IOException {
stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withStatus(401)));
stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withBody("{}")));
- Ec2Metadata ec2Metadata = Ec2Metadata.builder().endpoint(URI.create("http://localhost:8080")).build();
-
- MetadataResponse metadataResponse = ec2Metadata.get("/latest/meta-data/ami-id");
- assertThat(metadataResponse).isNull();
+ assertThatThrownBy(() -> {
+ Ec2Metadata ec2Metadata = Ec2Metadata.builder().endpoint(URI.create("http://localhost:8080")).build();
+ MetadataResponse metadataResponse = ec2Metadata.get("/latest/meta-data/ami-id");
+ }).hasMessageContaining("Exceeded maximum number of retries.")
+ .isInstanceOf(SdkClientException.class);
WireMock.verify(putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)).withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600")));
}
@Test
- public void getAmiId_onTokenResource_IOError_throws() throws IOException {
+ public void getToken_failedThriceWithFixedDelay() throws IOException {
+
stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withFixedDelay(Integer.MAX_VALUE)));
stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withBody("{}")));
+ assertThatThrownBy(() -> {
+ Ec2Metadata ec2Metadata = Ec2Metadata.builder().endpoint(URI.create("http://localhost:8080")).build();
+ MetadataResponse metadataResponse = ec2Metadata.get("/latest/meta-data/ami-id");
+ }).hasMessageContaining("Exceeded maximum number of retries.")
+ .isInstanceOf(SdkClientException.class);
+
+ WireMock.verify(putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)).withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600")));
+ }
+
+ @Test
+ public void getToken_failedOnceWith401_shouldSucceedOnSecondAttempt() throws IOException {
+
+ stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).inScenario("Retry Scenario")
+ .whenScenarioStateIs(STARTED)
+ .willReturn(aResponse().withStatus(401))
+ .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)).willReturn(aResponse().withBody("{}")));
+
Ec2Metadata ec2Metadata = Ec2Metadata.builder().endpoint(URI.create("http://localhost:8080")).build();
MetadataResponse metadataResponse = ec2Metadata.get("/latest/meta-data/ami-id");
- assertThat(metadataResponse).isNull();
+ assertThat(metadataResponse.asString()).isEqualTo("{}");
WireMock.verify(putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)).withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600")));
+
}
+
@Test
- public void getAmiId_onTokenResource_200() throws IOException {
+ public void get_failedOnceWith401_shouldSucceedOnSecondAttempt() throws IOException {
+
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(401))
+ .willSetStateTo("Cause Success"));
+
+ stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).inScenario("Retry Scenario")
+ .whenScenarioStateIs("Cause Success")
+ .willReturn(aResponse().withBody("{}")));
+
+
+ Ec2Metadata ec2Metadata = Ec2Metadata.builder().endpoint(URI.create("http://localhost:8080")).build();
+ MetadataResponse metadataResponse = ec2Metadata.get("/latest/meta-data/ami-id");
+ assertThat(metadataResponse.asString()).isEqualTo("{}");
+
+ WireMock.verify(putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)).withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600")));
+
+ }
+
+ @Test
+ public void getAndGetToken_failedOnceWith401_shouldSucceedOnSecondAttempt() throws IOException {
+
+ stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).inScenario("Retry Scenario")
+ .whenScenarioStateIs(STARTED)
+ .willReturn(aResponse().withStatus(401))
+ .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(STARTED)
+ .willReturn(aResponse().withStatus(401))
+ .willSetStateTo("Cause Success"));
+
+ stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).inScenario("Retry Scenario")
+ .whenScenarioStateIs("Cause Success")
+ .willReturn(aResponse().withBody("{}")));
+
+
+ Ec2Metadata ec2Metadata = Ec2Metadata.builder().endpoint(URI.create("http://localhost:8080")).build();
+ MetadataResponse metadataResponse = ec2Metadata.get("/latest/meta-data/ami-id");
+ assertThat(metadataResponse.asString()).isEqualTo("{}");
+
+ WireMock.verify(putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)).withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600")));
+
+ }
+
+ @Test
+ public void getAndGetToken_failedOnceWith403_shouldSucceedOnSecondAttempt() throws IOException {
+
+ stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).inScenario("Retry Scenario")
+ .whenScenarioStateIs(STARTED)
+ .willReturn(aResponse().withStatus(403))
+ .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(STARTED)
+ .willReturn(aResponse().withStatus(403))
+ .willSetStateTo("Cause Success"));
+
+ stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).inScenario("Retry Scenario")
+ .whenScenarioStateIs("Cause Success")
+ .willReturn(aResponse().withBody("{}")));
+
+ assertThatThrownBy(() -> {
+ Ec2Metadata ec2Metadata = Ec2Metadata.builder().endpoint(URI.create("http://localhost:8080")).build();
+ MetadataResponse metadataResponse = ec2Metadata.get("/latest/meta-data/ami-id");
+ }).hasMessageContaining("Could not retrieve token ");
+
+ WireMock.verify(putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)).withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600")));
+
+ }
+
+ @Test
+ public void getToken_failedTwiceWith200_shouldSucceedOnThirdAttempt() throws IOException {
+
+ stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).inScenario("Retry Scenario")
+ .whenScenarioStateIs(STARTED)
+ .willReturn(aResponse().withStatus(200))
+ .willSetStateTo("Try-2"));
+
+ stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).inScenario("Retry Scenario")
+ .whenScenarioStateIs("Try-2")
+ .willReturn(aResponse().withStatus(200))
+ .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)).willReturn(aResponse().withBody("{}")));
Ec2Metadata ec2Metadata = Ec2Metadata.builder().endpoint(URI.create("http://localhost:8080")).build();
+ MetadataResponse metadataResponse = ec2Metadata.get("/latest/meta-data/ami-id");
+ assertThat(metadataResponse.asString()).isEqualTo("{}");
+
+ WireMock.verify(putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)).withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600")));
+
+ }
+
+ @Test
+ public void getToken_failedTwiceWithIOExceptionAnd200_shouldSucceedOnThirdAttempt() throws IOException {
+
+ stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).inScenario("Retry Scenario")
+ .whenScenarioStateIs(STARTED)
+ .willReturn(aResponse().withFault(Fault.MALFORMED_RESPONSE_CHUNK))
+ .willSetStateTo("Try-2"));
+
+ stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).inScenario("Retry Scenario")
+ .whenScenarioStateIs("Try-2")
+ .willReturn(aResponse().withStatus(200))
+ .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)).willReturn(aResponse().withBody("{}")));
+ Ec2Metadata ec2Metadata = Ec2Metadata.builder().endpoint(URI.create("http://localhost:8080")).build();
MetadataResponse metadataResponse = ec2Metadata.get("/latest/meta-data/ami-id");
assertThat(metadataResponse.asString()).isEqualTo("{}");
WireMock.verify(putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)).withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600")));
- WireMock.verify(getRequestedFor(urlPathEqualTo(AMI_ID_RESOURCE)).withHeader(TOKEN_HEADER, equalTo("some-token")));
+
+ }
+ @Test
+ public void get_failedTwiceWith401_shouldFailOnThirdAttempt() throws IOException {
+
+ 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(401))
+ .willSetStateTo("Try-2"));
+
+ stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).inScenario("Retry Scenario")
+ .whenScenarioStateIs("Try-2")
+ .willReturn(aResponse().withStatus(401))
+ .willSetStateTo("Try-3"));
+
+ stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).inScenario("Retry Scenario")
+ .whenScenarioStateIs("Try-3")
+ .willReturn(aResponse().withStatus(401)));
+
+
+ assertThatThrownBy(() -> {
+ Ec2Metadata ec2Metadata = Ec2Metadata.builder().endpoint(URI.create("http://localhost:8080")).build();
+ MetadataResponse metadataResponse = ec2Metadata.get("/latest/meta-data/ami-id");
+ }).hasMessageContaining("Exceeded maximum number of retries.")
+ .isInstanceOf(SdkClientException.class);
+
+ WireMock.verify(putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)).withHeader(EC2_METADATA_TOKEN_TTL_HEADER, equalTo("21600")));
+
+ }
+
+ @Test
+ public void verifyEc2MetadataRetryPolicy_equalsAndHashcode(){
+
+ EqualsVerifier.forClass(Ec2MetadataRetryPolicy.class).usingGetClass().verify();
}
}