Skip to content

Commit 56f5207

Browse files
authored
[Default Configuration Part 2]:Implement auto mode discovery (#2786)
* Implement auto mode discovery * Fix tests on CodeBuild * Address feedback * Add comment and rename misnamed constant
1 parent 136752b commit 56f5207

File tree

8 files changed

+469
-9
lines changed

8 files changed

+469
-9
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.awscore.internal.defaultsmode;
17+
18+
import java.util.Optional;
19+
import software.amazon.awssdk.annotations.SdkInternalApi;
20+
import software.amazon.awssdk.core.SdkSystemSetting;
21+
import software.amazon.awssdk.defaultsmode.DefaultsMode;
22+
import software.amazon.awssdk.regions.Region;
23+
import software.amazon.awssdk.regions.internal.util.EC2MetadataUtils;
24+
import software.amazon.awssdk.utils.JavaSystemSetting;
25+
import software.amazon.awssdk.utils.OptionalUtils;
26+
import software.amazon.awssdk.utils.SystemSetting;
27+
import software.amazon.awssdk.utils.internal.SystemSettingUtils;
28+
29+
/**
30+
* This class attempts to discover the appropriate {@link DefaultsMode} by inspecting the environment. It falls
31+
* back to the {@link DefaultsMode#STANDARD} mode if the target mode cannot be determined.
32+
*/
33+
@SdkInternalApi
34+
public final class AutoDefaultsModeDiscovery {
35+
private static final String EC2_METADATA_REGION_PATH = "/latest/meta-data/placement/region";
36+
private static final DefaultsMode FALLBACK_DEFAULTS_MODE = DefaultsMode.STANDARD;
37+
private static final String ANDROID_JAVA_VENDOR = "The Android Project";
38+
private static final String AWS_DEFAULT_REGION_ENV_VAR = "AWS_DEFAULT_REGION";
39+
40+
/**
41+
* Discovers the defaultMode using the following workflow:
42+
*
43+
* 1. Check if it's on mobile
44+
* 2. If it's not on mobile (best we can tell), see if we can determine whether we're an in-region or cross-region client.
45+
* 3. If we couldn't figure out the region from environment variables. Check IMDSv2. This step might take up to 1 second
46+
* (default connect timeout)
47+
* 4. Finally, use fallback mode
48+
*/
49+
public DefaultsMode discover(Region regionResolvedFromSdkClient) {
50+
51+
if (isMobile()) {
52+
return DefaultsMode.MOBILE;
53+
}
54+
55+
if (isAwsExecutionEnvironment()) {
56+
Optional<String> regionStr = regionFromAwsExecutionEnvironment();
57+
58+
if (regionStr.isPresent()) {
59+
return compareRegion(regionStr.get(), regionResolvedFromSdkClient);
60+
}
61+
}
62+
63+
Optional<String> regionFromEc2 = queryImdsV2();
64+
if (regionFromEc2.isPresent()) {
65+
return compareRegion(regionFromEc2.get(), regionResolvedFromSdkClient);
66+
}
67+
68+
return FALLBACK_DEFAULTS_MODE;
69+
}
70+
71+
private static DefaultsMode compareRegion(String region, Region clientRegion) {
72+
if (region.equalsIgnoreCase(clientRegion.id())) {
73+
return DefaultsMode.IN_REGION;
74+
}
75+
76+
return DefaultsMode.CROSS_REGION;
77+
}
78+
79+
private static Optional<String> queryImdsV2() {
80+
try {
81+
String ec2InstanceRegion = EC2MetadataUtils.fetchData(EC2_METADATA_REGION_PATH, false, 1);
82+
// ec2InstanceRegion could be null
83+
return Optional.ofNullable(ec2InstanceRegion);
84+
} catch (Exception exception) {
85+
return Optional.empty();
86+
}
87+
}
88+
89+
/**
90+
* Check to see if the application is running on a mobile device by verifying the Java
91+
* vendor system property. Currently only checks for Android. While it's technically possible to
92+
* use Java with iOS, it's not a common use-case.
93+
* <p>
94+
* https://developer.android.com/reference/java/lang/System#getProperties()
95+
*/
96+
private static boolean isMobile() {
97+
return JavaSystemSetting.JAVA_VENDOR.getStringValue()
98+
.filter(o -> o.equals(ANDROID_JAVA_VENDOR))
99+
.isPresent();
100+
}
101+
102+
private static boolean isAwsExecutionEnvironment() {
103+
return SdkSystemSetting.AWS_EXECUTION_ENV.getStringValue().isPresent();
104+
}
105+
106+
private static Optional<String> regionFromAwsExecutionEnvironment() {
107+
Optional<String> regionFromRegionEnvVar = SdkSystemSetting.AWS_REGION.getStringValue();
108+
return OptionalUtils.firstPresent(regionFromRegionEnvVar,
109+
() -> SystemSettingUtils.resolveEnvironmentVariable(new DefaultRegionEnvVar()));
110+
}
111+
112+
private static final class DefaultRegionEnvVar implements SystemSetting {
113+
@Override
114+
public String property() {
115+
return null;
116+
}
117+
118+
@Override
119+
public String environmentVariable() {
120+
return AWS_DEFAULT_REGION_ENV_VAR;
121+
}
122+
123+
@Override
124+
public String defaultValue() {
125+
return null;
126+
}
127+
}
128+
}
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.awscore.internal;
17+
18+
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
19+
import static com.github.tomakehurst.wiremock.client.WireMock.get;
20+
import static com.github.tomakehurst.wiremock.client.WireMock.put;
21+
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
22+
import static org.assertj.core.api.Assertions.assertThat;
23+
24+
import com.github.tomakehurst.wiremock.junit.WireMockRule;
25+
import java.util.Arrays;
26+
import java.util.Collection;
27+
import java.util.concurrent.Callable;
28+
import org.junit.After;
29+
import org.junit.Before;
30+
import org.junit.Rule;
31+
import org.junit.Test;
32+
import org.junit.runner.RunWith;
33+
import org.junit.runners.Parameterized;
34+
import software.amazon.awssdk.awscore.internal.defaultsmode.AutoDefaultsModeDiscovery;
35+
import software.amazon.awssdk.core.SdkSystemSetting;
36+
import software.amazon.awssdk.defaultsmode.DefaultsMode;
37+
import software.amazon.awssdk.regions.Region;
38+
import software.amazon.awssdk.regions.internal.util.EC2MetadataUtils;
39+
import software.amazon.awssdk.testutils.EnvironmentVariableHelper;
40+
import software.amazon.awssdk.utils.JavaSystemSetting;
41+
42+
@RunWith(Parameterized.class)
43+
public class AutoDefaultsModeDiscoveryTest {
44+
private static final EnvironmentVariableHelper ENVIRONMENT_VARIABLE_HELPER = new EnvironmentVariableHelper();
45+
@Parameterized.Parameter
46+
public TestData testData;
47+
48+
@Parameterized.Parameters
49+
public static Collection<Object> data() {
50+
return Arrays.asList(new Object[] {
51+
52+
// Mobile
53+
new TestData().clientRegion(Region.US_EAST_1)
54+
.javaVendorProperty("The Android Project")
55+
.awsExecutionEnvVar("AWS_Lambda_java8")
56+
.awsRegionEnvVar("us-east-1")
57+
.expectedResolvedMode(DefaultsMode.MOBILE),
58+
59+
// Region available from AWS execution environment
60+
new TestData().clientRegion(Region.US_EAST_1)
61+
.awsExecutionEnvVar("AWS_Lambda_java8")
62+
.awsRegionEnvVar("us-east-1")
63+
.expectedResolvedMode(DefaultsMode.IN_REGION),
64+
65+
// Region available from AWS execution environment
66+
new TestData().clientRegion(Region.US_EAST_1)
67+
.awsExecutionEnvVar("AWS_Lambda_java8")
68+
.awsDefaultRegionEnvVar("us-west-2")
69+
.expectedResolvedMode(DefaultsMode.CROSS_REGION),
70+
71+
// ImdsV2 available, in-region
72+
new TestData().clientRegion(Region.US_EAST_1)
73+
.awsDefaultRegionEnvVar("us-west-2")
74+
.ec2MetadataConfig(new Ec2MetadataConfig().region("us-east-1")
75+
.imdsAvailable(true))
76+
.expectedResolvedMode(DefaultsMode.IN_REGION),
77+
78+
// ImdsV2 available, cross-region
79+
new TestData().clientRegion(Region.US_EAST_1)
80+
.awsDefaultRegionEnvVar("us-west-2")
81+
.ec2MetadataConfig(new Ec2MetadataConfig().region("us-west-2")
82+
.imdsAvailable(true)
83+
.ec2MetadataDisabledEnvVar("false"))
84+
.expectedResolvedMode(DefaultsMode.CROSS_REGION),
85+
86+
// Imdsv2 disabled, should not query ImdsV2 and use fallback mode
87+
new TestData().clientRegion(Region.US_EAST_1)
88+
.awsDefaultRegionEnvVar("us-west-2")
89+
.ec2MetadataConfig(new Ec2MetadataConfig().region("us-west-2")
90+
.imdsAvailable(true)
91+
.ec2MetadataDisabledEnvVar("true"))
92+
.expectedResolvedMode(DefaultsMode.STANDARD),
93+
94+
// Imdsv2 not available, should use fallback mode.
95+
new TestData().clientRegion(Region.US_EAST_1)
96+
.awsDefaultRegionEnvVar("us-west-2")
97+
.ec2MetadataConfig(new Ec2MetadataConfig().imdsAvailable(false))
98+
.expectedResolvedMode(DefaultsMode.STANDARD),
99+
});
100+
}
101+
102+
@Rule
103+
public WireMockRule wireMock = new WireMockRule(0);
104+
105+
@Before
106+
public void methodSetup() {
107+
System.setProperty(SdkSystemSetting.AWS_EC2_METADATA_SERVICE_ENDPOINT.property(),
108+
"http://localhost:" + wireMock.port());
109+
}
110+
111+
@After
112+
public void cleanUp() {
113+
EC2MetadataUtils.clearCache();
114+
wireMock.resetAll();
115+
ENVIRONMENT_VARIABLE_HELPER.reset();
116+
System.clearProperty(JavaSystemSetting.JAVA_VENDOR.property());
117+
}
118+
119+
@Test
120+
public void differentCombinationOfConfigs_shouldResolveCorrectly() throws Exception {
121+
if (testData.javaVendorProperty != null) {
122+
System.setProperty(JavaSystemSetting.JAVA_VENDOR.property(), testData.javaVendorProperty);
123+
}
124+
125+
if (testData.awsExecutionEnvVar != null) {
126+
ENVIRONMENT_VARIABLE_HELPER.set(SdkSystemSetting.AWS_EXECUTION_ENV.environmentVariable(),
127+
testData.awsExecutionEnvVar);
128+
} else {
129+
ENVIRONMENT_VARIABLE_HELPER.remove(SdkSystemSetting.AWS_EXECUTION_ENV.environmentVariable());
130+
}
131+
132+
if (testData.awsRegionEnvVar != null) {
133+
ENVIRONMENT_VARIABLE_HELPER.set(SdkSystemSetting.AWS_REGION.environmentVariable(), testData.awsRegionEnvVar);
134+
} else {
135+
ENVIRONMENT_VARIABLE_HELPER.remove(SdkSystemSetting.AWS_REGION.environmentVariable());
136+
}
137+
138+
if (testData.awsDefaultRegionEnvVar != null) {
139+
ENVIRONMENT_VARIABLE_HELPER.set("AWS_DEFAULT_REGION", testData.awsDefaultRegionEnvVar);
140+
} else {
141+
ENVIRONMENT_VARIABLE_HELPER.remove("AWS_DEFAULT_REGION");
142+
}
143+
144+
if (testData.ec2MetadataConfig != null) {
145+
if (testData.ec2MetadataConfig.ec2MetadataDisabledEnvVar != null) {
146+
ENVIRONMENT_VARIABLE_HELPER.set(SdkSystemSetting.AWS_EC2_METADATA_DISABLED.environmentVariable(),
147+
testData.ec2MetadataConfig.ec2MetadataDisabledEnvVar);
148+
}
149+
150+
if (testData.ec2MetadataConfig.imdsAvailable) {
151+
stubSuccessfulResponse(testData.ec2MetadataConfig.region);
152+
}
153+
}
154+
155+
Callable<DefaultsMode> result = () -> new AutoDefaultsModeDiscovery().discover(testData.clientRegion);
156+
assertThat(result.call()).isEqualTo(testData.expectedResolvedMode);
157+
}
158+
159+
public void stubSuccessfulResponse(String region) {
160+
stubFor(put("/latest/api/token")
161+
.willReturn(aResponse().withStatus(200).withBody("token")));
162+
163+
stubFor(get("/latest/meta-data/placement/region")
164+
.willReturn(aResponse().withStatus(200).withBody(region)));
165+
}
166+
167+
private static final class TestData {
168+
private Region clientRegion;
169+
private String javaVendorProperty;
170+
private String awsExecutionEnvVar;
171+
private String awsRegionEnvVar;
172+
private String awsDefaultRegionEnvVar;
173+
private Ec2MetadataConfig ec2MetadataConfig;
174+
private DefaultsMode expectedResolvedMode;
175+
176+
public TestData clientRegion(Region clientRegion) {
177+
this.clientRegion = clientRegion;
178+
return this;
179+
}
180+
181+
public TestData javaVendorProperty(String javaVendorProperty) {
182+
this.javaVendorProperty = javaVendorProperty;
183+
return this;
184+
}
185+
186+
public TestData awsExecutionEnvVar(String awsExecutionEnvVar) {
187+
this.awsExecutionEnvVar = awsExecutionEnvVar;
188+
return this;
189+
}
190+
191+
public TestData awsRegionEnvVar(String awsRegionEnvVar) {
192+
this.awsRegionEnvVar = awsRegionEnvVar;
193+
return this;
194+
}
195+
196+
public TestData awsDefaultRegionEnvVar(String awsDefaultRegionEnvVar) {
197+
this.awsDefaultRegionEnvVar = awsDefaultRegionEnvVar;
198+
return this;
199+
}
200+
201+
public TestData ec2MetadataConfig(Ec2MetadataConfig ec2MetadataConfig) {
202+
this.ec2MetadataConfig = ec2MetadataConfig;
203+
return this;
204+
}
205+
206+
public TestData expectedResolvedMode(DefaultsMode expectedResolvedMode) {
207+
this.expectedResolvedMode = expectedResolvedMode;
208+
return this;
209+
}
210+
}
211+
212+
private static final class Ec2MetadataConfig {
213+
private boolean imdsAvailable;
214+
private String region;
215+
private String ec2MetadataDisabledEnvVar;
216+
217+
public Ec2MetadataConfig imdsAvailable(boolean imdsAvailable) {
218+
this.imdsAvailable = imdsAvailable;
219+
return this;
220+
}
221+
222+
public Ec2MetadataConfig region(String region) {
223+
this.region = region;
224+
return this;
225+
}
226+
227+
public Ec2MetadataConfig ec2MetadataDisabledEnvVar(String ec2MetadataDisabledEnvVar) {
228+
this.ec2MetadataDisabledEnvVar = ec2MetadataDisabledEnvVar;
229+
return this;
230+
}
231+
}
232+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#
2+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License").
5+
# You may not use this file except in compliance with the License.
6+
# A copy of the License is located at
7+
#
8+
# http://aws.amazon.com/apache2.0
9+
#
10+
# or in the "license" file accompanying this file. This file is distributed
11+
# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
# express or implied. See the License for the specific language governing
13+
# permissions and limitations under the License.
14+
#
15+
16+
# Set up logging implementation
17+
org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog
18+
org.eclipse.jetty.LEVEL=OFF

0 commit comments

Comments
 (0)