Skip to content

[Default Configuration Part 2]:Implement auto mode discovery #2786

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Oct 26, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,12 @@
import software.amazon.awssdk.utils.internal.SystemSettingUtils;

/**
* This class attempts to discover the appropriate {@link DefaultsMode} the mode by inspecting the environment. It falls
* back to the standard defaults mode if the target mode cannot be determined.
* This class attempts to discover the appropriate {@link DefaultsMode} by inspecting the environment. It falls
* back to the {@link DefaultsMode#STANDARD} mode if the target mode cannot be determined.
*/
@SdkInternalApi
public final class AutoDefaultsModeDiscovery {
private static final String EC2_METADATA_REGION_PATH = "/latest/meta-data/placement/region";
private static final DefaultsMode FALLBACK_DEFAULTS_MODE = DefaultsMode.STANDARD;
private static final String ANDROID_JAVA_VENDOR = "The Android Project";
private static final String AWS_DEFAULT_REGION_ENV_VAR = "AWS_DEFAULT_REGION";
Expand All @@ -54,20 +55,20 @@ public DefaultsMode discover(Region regionResolvedFromSdkClient) {
Optional<String> regionStr = regionFromAwsExecutionEnvironment();

if (regionStr.isPresent()) {
return compareRegion(regionStr.get(), regionResolvedFromSdkClient.id());
return compareRegion(regionStr.get(), regionResolvedFromSdkClient);
}
}

Optional<String> regionFromEc2 = queryImdsV2();
if (regionFromEc2.isPresent()) {
return compareRegion(regionFromEc2.get(), regionResolvedFromSdkClient.id());
return compareRegion(regionFromEc2.get(), regionResolvedFromSdkClient);
}

return FALLBACK_DEFAULTS_MODE;
}

private static DefaultsMode compareRegion(String region, String anotherRegion) {
if (region.equalsIgnoreCase(anotherRegion)) {
private static DefaultsMode compareRegion(String region, Region clientRegion) {
if (region.equalsIgnoreCase(clientRegion.id())) {
return DefaultsMode.IN_REGION;
}

Expand All @@ -76,7 +77,7 @@ private static DefaultsMode compareRegion(String region, String anotherRegion) {

private static Optional<String> queryImdsV2() {
try {
String ec2InstanceRegion = EC2MetadataUtils.getEC2InstanceRegion();
String ec2InstanceRegion = EC2MetadataUtils.fetchData(EC2_METADATA_REGION_PATH, false, 1);
// ec2InstanceRegion could be null
return Optional.ofNullable(ec2InstanceRegion);
} catch (Exception exception) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import software.amazon.awssdk.core.SdkSystemSetting;
import software.amazon.awssdk.defaultsmode.DefaultsMode;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.regions.internal.util.EC2MetadataUtils;
import software.amazon.awssdk.testutils.EnvironmentVariableHelper;
import software.amazon.awssdk.utils.JavaSystemSetting;

Expand All @@ -47,35 +48,50 @@ public class AutoDefaultsModeDiscoveryTest {
@Parameterized.Parameters
public static Collection<Object> data() {
return Arrays.asList(new Object[] {

// Mobile
new TestData().clientRegion(Region.US_EAST_1)
.javaVendorProperty("The Android Project")
.awsExecutionEnvVar("AWS_Lambda_java8")
.awsRegionEnvVar("us-east-1")
.expectedResolvedMode(DefaultsMode.MOBILE),

// Region available from AWS execution environment
new TestData().clientRegion(Region.US_EAST_1)
.awsExecutionEnvVar("AWS_Lambda_java8")
.awsRegionEnvVar("us-east-1")
.expectedResolvedMode(DefaultsMode.IN_REGION),

// Region available from AWS execution environment
new TestData().clientRegion(Region.US_EAST_1)
.awsExecutionEnvVar("AWS_Lambda_java8")
.awsDefaultRegionEnvVar("us-west-2")
.expectedResolvedMode(DefaultsMode.CROSS_REGION),

// ImdsV2 available, in-region
new TestData().clientRegion(Region.US_EAST_1)
.awsDefaultRegionEnvVar("us-west-2")
.ec2MetadataConfig(new Ec2MetadataConfig().region("us-east-1")
.imdsAvailable(true))
.expectedResolvedMode(DefaultsMode.IN_REGION),

// ImdsV2 available, cross-region
new TestData().clientRegion(Region.US_EAST_1)
.awsDefaultRegionEnvVar("us-west-2")
.ec2MetadataConfig(new Ec2MetadataConfig().region("us-west-2")
.imdsAvailable(true)
.ec2MetadataDisabledEnvVar("false"))
.expectedResolvedMode(DefaultsMode.CROSS_REGION),

// Imdsv2 disabled, should not query ImdsV2 and use fallback mode
new TestData().clientRegion(Region.US_EAST_1)
.awsDefaultRegionEnvVar("us-west-2")
.ec2MetadataConfig(new Ec2MetadataConfig().region("us-west-2")
.imdsAvailable(true)
.ec2MetadataDisabledEnvVar("true"))
.expectedResolvedMode(DefaultsMode.STANDARD),

// Imdsv2 not available, should use fallback mode.
new TestData().clientRegion(Region.US_EAST_1)
.awsDefaultRegionEnvVar("us-west-2")
.ec2MetadataConfig(new Ec2MetadataConfig().imdsAvailable(false))
Expand All @@ -94,6 +110,7 @@ public void methodSetup() {

@After
public void cleanUp() {
EC2MetadataUtils.clearCache();
wireMock.resetAll();
ENVIRONMENT_VARIABLE_HELPER.reset();
System.clearProperty(JavaSystemSetting.JAVA_VENDOR.property());
Expand Down Expand Up @@ -143,23 +160,10 @@ public void stubSuccessfulResponse(String region) {
stubFor(put("/latest/api/token")
.willReturn(aResponse().withStatus(200).withBody("token")));


stubFor(get("/latest/dynamic/instance-identity/document")
.willReturn(aResponse().withStatus(200).withBody(constructInstanceInfo(region))));
stubFor(get("/latest/meta-data/placement/region")
.willReturn(aResponse().withStatus(200).withBody(region)));
}

private String constructInstanceInfo(String region) {
return String.format("{"
+ "\"pendingTime\":\"2014-08-07T22:07:46Z\","
+ "\"instanceType\":\"m1.small\","
+ "\"imageId\":\"ami-123456\","
+ "\"instanceId\":\"i-123456\","
+ "\"region\":\"%s\","
+ "\"version\":\"2010-08-31\""
+ "}", region);
}


private static final class TestData {
private Region clientRegion;
private String javaVendorProperty;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,15 @@
import software.amazon.awssdk.regions.util.ResourcesEndpointProvider;

/**
* Utility class for retrieving Amazon EC2 instance metadata.<br>

*
* Utility class for retrieving Amazon EC2 instance metadata.
*
* <p>
* <b>Note</b>: this is an internal API subject to change. Users of the SDK
* should not depend on this.
*
* <p>
* You can use the data to build more generic AMIs that can be modified by
* configuration files supplied at launch time. For example, if you run web
* servers for various small businesses, they can all use the same AMI and
Expand Down Expand Up @@ -361,7 +369,7 @@ public static List<String> getItems(String path, int tries) {
}

@SdkTestInternalApi
static void clearCache() {
public static void clearCache() {
CACHE.clear();
}

Expand Down Expand Up @@ -391,6 +399,11 @@ private static List<String> getItems(String path, int tries, boolean slurp) {
log.warn("Unable to retrieve the requested metadata.");
return null;
} catch (IOException | URISyntaxException | RuntimeException e) {
// If there is no retry available, just throw exception instead of pausing.
if (tries - 1 == 0) {
throw SdkClientException.builder().message("Unable to contact EC2 metadata service.").cause(e).build();
}

// Retry on any other exceptions
Copy link
Contributor

@Bennett-Lynch Bennett-Lynch Oct 25, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this comment got slightly lost from what it was referencing, on line 401.

Edit: Upon second read, maybe it's fine.

int pause = (int) (Math.pow(2, DEFAULT_QUERY_RETRIES - tries) * MINIMUM_RETRY_WAIT_TIME_MILLISECONDS);
try {
Expand Down Expand Up @@ -427,19 +440,30 @@ public static String getToken() {
}
}


private static String fetchData(String path) {
return fetchData(path, false);
}

private static String fetchData(String path, boolean force) {
return fetchData(path, force, DEFAULT_QUERY_RETRIES);
}

/**
* Fetch data using the given path
*
* @param path the path
* @param force whether to force to override the value in the cache
* @param attempts the number of attempts that should be executed.
* @return the value retrieved from the path
*/
public static String fetchData(String path, boolean force, int attempts) {
if (SdkSystemSetting.AWS_EC2_METADATA_DISABLED.getBooleanValueOrThrow()) {
throw SdkClientException.builder().message("EC2 metadata usage is disabled.").build();
}

try {
if (force || !CACHE.containsKey(path)) {
CACHE.put(path, getData(path));
CACHE.put(path, getData(path, attempts));
}
return CACHE.get(path);
} catch (SdkClientException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
import static org.assertj.core.api.Assertions.assertThat;

import 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;
Expand Down Expand Up @@ -131,4 +133,17 @@ public void getAmiId_queriesTokenResource_400Error_throws() {

EC2MetadataUtils.getAmiId();
}

@Test
public void fetchDataWithAttemptNumber_ioError_shouldHonor() {
int attempts = 1;
thrown.expect(SdkClientException.class);
thrown.expectMessage("Unable to contact EC2 metadata service");

stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody("some-token")));;
stubFor(get(urlPathEqualTo(AMI_ID_RESOURCE)).willReturn(aResponse().withFault(Fault.CONNECTION_RESET_BY_PEER)));

EC2MetadataUtils.fetchData(AMI_ID_RESOURCE, false, attempts);
WireMock.verify(attempts, getRequestedFor(urlPathEqualTo(AMI_ID_RESOURCE)));
}
}