Skip to content

Commit 00e4962

Browse files
committed
Implement auto mode discovery
1 parent 136752b commit 00e4962

File tree

6 files changed

+410
-1
lines changed

6 files changed

+410
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
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} the mode by inspecting the environment. It falls
31+
* back to the standard defaults mode if the target mode cannot be determined.
32+
*/
33+
@SdkInternalApi
34+
public final class AutoDefaultsModeDiscovery {
35+
private static final DefaultsMode FALLBACK_DEFAULTS_MODE = DefaultsMode.STANDARD;
36+
private static final String ANDROID_JAVA_VENDOR = "The Android Project";
37+
private static final String AWS_DEFAULT_REGION_ENV_VAR = "AWS_DEFAULT_REGION";
38+
39+
/**
40+
* Discovers the defaultMode using the following workflow:
41+
*
42+
* 1. Check if it's on mobile
43+
* 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.
44+
* 3. If we couldn't figure out the region from environment variables. Check IMDSv2
45+
* 4. Finally, use fallback mode
46+
*/
47+
public DefaultsMode discover(Region regionResolvedFromSdkClient) {
48+
49+
if (isMobile()) {
50+
return DefaultsMode.MOBILE;
51+
}
52+
53+
if (isAwsExecutionEnvironment()) {
54+
Optional<String> regionStr = regionFromAwsExecutionEnvironment();
55+
56+
if (regionStr.isPresent()) {
57+
return compareRegion(regionStr.get(), regionResolvedFromSdkClient.id());
58+
}
59+
}
60+
61+
Optional<String> regionFromEc2 = queryImdsV2();
62+
if (regionFromEc2.isPresent()) {
63+
return compareRegion(regionFromEc2.get(), regionResolvedFromSdkClient.id());
64+
}
65+
66+
return FALLBACK_DEFAULTS_MODE;
67+
}
68+
69+
private static DefaultsMode compareRegion(String region, String anotherRegion) {
70+
if (region.equalsIgnoreCase(anotherRegion)) {
71+
return DefaultsMode.IN_REGION;
72+
}
73+
74+
return DefaultsMode.CROSS_REGION;
75+
}
76+
77+
private static Optional<String> queryImdsV2() {
78+
try {
79+
String ec2InstanceRegion = EC2MetadataUtils.getEC2InstanceRegion();
80+
// ec2InstanceRegion could be null
81+
return Optional.ofNullable(ec2InstanceRegion);
82+
} catch (Exception exception) {
83+
return Optional.empty();
84+
}
85+
}
86+
87+
/**
88+
* Check to see if the application is running on a mobile device by verifying the Java
89+
* vendor system property. Currently only checks for Android. While it's technically possible to
90+
* use Java with iOS, it's not a common use-case.
91+
* <p>
92+
* https://developer.android.com/reference/java/lang/System#getProperties()
93+
*/
94+
private static boolean isMobile() {
95+
return JavaSystemSetting.JAVA_VENDOR.getStringValue()
96+
.filter(o -> o.equals(ANDROID_JAVA_VENDOR))
97+
.isPresent();
98+
}
99+
100+
private static boolean isAwsExecutionEnvironment() {
101+
return SdkSystemSetting.AWS_EXECUTION_ENV.getStringValue().isPresent();
102+
}
103+
104+
private static Optional<String> regionFromAwsExecutionEnvironment() {
105+
Optional<String> regionFromRegionEnvVar = SdkSystemSetting.AWS_REGION.getStringValue();
106+
return OptionalUtils.firstPresent(regionFromRegionEnvVar,
107+
() -> SystemSettingUtils.resolveEnvironmentVariable(new DefaultRegionEnvVar()));
108+
}
109+
110+
private static final class DefaultRegionEnvVar implements SystemSetting {
111+
@Override
112+
public String property() {
113+
return null;
114+
}
115+
116+
@Override
117+
public String environmentVariable() {
118+
return AWS_DEFAULT_REGION_ENV_VAR;
119+
}
120+
121+
@Override
122+
public String defaultValue() {
123+
return null;
124+
}
125+
}
126+
}
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
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.testutils.EnvironmentVariableHelper;
39+
import software.amazon.awssdk.utils.JavaSystemSetting;
40+
41+
@RunWith(Parameterized.class)
42+
public class AutoDefaultsModeDiscoveryTest {
43+
private static final EnvironmentVariableHelper ENVIRONMENT_VARIABLE_HELPER = new EnvironmentVariableHelper();
44+
@Parameterized.Parameter
45+
public TestData testData;
46+
47+
@Parameterized.Parameters
48+
public static Collection<Object> data() {
49+
return Arrays.asList(new Object[] {
50+
new TestData().clientRegion(Region.US_EAST_1)
51+
.javaVendorProperty("The Android Project")
52+
.awsExecutionEnvVar("AWS_Lambda_java8")
53+
.awsRegionEnvVar("us-east-1")
54+
.expectedResolvedMode(DefaultsMode.MOBILE),
55+
56+
new TestData().clientRegion(Region.US_EAST_1)
57+
.awsExecutionEnvVar("AWS_Lambda_java8")
58+
.awsRegionEnvVar("us-east-1")
59+
.expectedResolvedMode(DefaultsMode.IN_REGION),
60+
61+
new TestData().clientRegion(Region.US_EAST_1)
62+
.awsExecutionEnvVar("AWS_Lambda_java8")
63+
.awsDefaultRegionEnvVar("us-west-2")
64+
.expectedResolvedMode(DefaultsMode.CROSS_REGION),
65+
66+
new TestData().clientRegion(Region.US_EAST_1)
67+
.awsDefaultRegionEnvVar("us-west-2")
68+
.ec2MetadataConfig(new Ec2MetadataConfig().region("us-east-1")
69+
.imdsAvailable(true))
70+
.expectedResolvedMode(DefaultsMode.IN_REGION),
71+
72+
new TestData().clientRegion(Region.US_EAST_1)
73+
.awsDefaultRegionEnvVar("us-west-2")
74+
.ec2MetadataConfig(new Ec2MetadataConfig().region("us-west-2")
75+
.imdsAvailable(true)
76+
.ec2MetadataDisabledEnvVar("false"))
77+
.expectedResolvedMode(DefaultsMode.CROSS_REGION),
78+
79+
new TestData().clientRegion(Region.US_EAST_1)
80+
.awsDefaultRegionEnvVar("us-west-2")
81+
.ec2MetadataConfig(new Ec2MetadataConfig().imdsAvailable(false))
82+
.expectedResolvedMode(DefaultsMode.STANDARD),
83+
});
84+
}
85+
86+
@Rule
87+
public WireMockRule wireMock = new WireMockRule(0);
88+
89+
@Before
90+
public void methodSetup() {
91+
System.setProperty(SdkSystemSetting.AWS_EC2_METADATA_SERVICE_ENDPOINT.property(),
92+
"http://localhost:" + wireMock.port());
93+
}
94+
95+
@After
96+
public void cleanUp() {
97+
wireMock.resetAll();
98+
ENVIRONMENT_VARIABLE_HELPER.reset();
99+
System.clearProperty(JavaSystemSetting.JAVA_VENDOR.property());
100+
}
101+
102+
@Test
103+
public void differentCombinationOfConfigs_shouldResolveCorrectly() throws Exception {
104+
if (testData.javaVendorProperty != null) {
105+
System.setProperty(JavaSystemSetting.JAVA_VENDOR.property(), testData.javaVendorProperty);
106+
}
107+
108+
if (testData.awsExecutionEnvVar != null) {
109+
ENVIRONMENT_VARIABLE_HELPER.set(SdkSystemSetting.AWS_EXECUTION_ENV.environmentVariable(),
110+
testData.awsExecutionEnvVar);
111+
}
112+
113+
if (testData.awsRegionEnvVar != null) {
114+
ENVIRONMENT_VARIABLE_HELPER.set(SdkSystemSetting.AWS_REGION.environmentVariable(), testData.awsRegionEnvVar);
115+
}
116+
117+
if (testData.awsDefaultRegionEnvVar != null) {
118+
ENVIRONMENT_VARIABLE_HELPER.set("AWS_DEFAULT_REGION", testData.awsDefaultRegionEnvVar);
119+
}
120+
121+
if (testData.ec2MetadataConfig != null) {
122+
if (testData.ec2MetadataConfig.ec2MetadataDisabledEnvVar != null) {
123+
ENVIRONMENT_VARIABLE_HELPER.set(SdkSystemSetting.AWS_EC2_METADATA_DISABLED.environmentVariable(),
124+
testData.ec2MetadataConfig.ec2MetadataDisabledEnvVar);
125+
}
126+
127+
if (testData.ec2MetadataConfig.imdsAvailable) {
128+
stubSuccessfulResponse(testData.ec2MetadataConfig.region);
129+
}
130+
}
131+
132+
Callable<DefaultsMode> result = () -> new AutoDefaultsModeDiscovery().discover(testData.clientRegion);
133+
assertThat(result.call()).isEqualTo(testData.expectedResolvedMode);
134+
}
135+
136+
public void stubSuccessfulResponse(String region) {
137+
stubFor(put("/latest/api/token")
138+
.willReturn(aResponse().withStatus(200).withBody("token")));
139+
140+
141+
stubFor(get("/latest/dynamic/instance-identity/document")
142+
.willReturn(aResponse().withStatus(200).withBody(constructInstanceInfo(region))));
143+
}
144+
145+
private String constructInstanceInfo(String region) {
146+
return String.format("{"
147+
+ "\"pendingTime\":\"2014-08-07T22:07:46Z\","
148+
+ "\"instanceType\":\"m1.small\","
149+
+ "\"imageId\":\"ami-123456\","
150+
+ "\"instanceId\":\"i-123456\","
151+
+ "\"region\":\"%s\","
152+
+ "\"version\":\"2010-08-31\""
153+
+ "}", region);
154+
}
155+
156+
157+
private static final class TestData {
158+
private Region clientRegion;
159+
private String javaVendorProperty;
160+
private String awsExecutionEnvVar;
161+
private String awsRegionEnvVar;
162+
private String awsDefaultRegionEnvVar;
163+
private Ec2MetadataConfig ec2MetadataConfig;
164+
private DefaultsMode expectedResolvedMode;
165+
166+
public TestData clientRegion(Region clientRegion) {
167+
this.clientRegion = clientRegion;
168+
return this;
169+
}
170+
171+
public TestData javaVendorProperty(String javaVendorProperty) {
172+
this.javaVendorProperty = javaVendorProperty;
173+
return this;
174+
}
175+
176+
public TestData awsExecutionEnvVar(String awsExecutionEnvVar) {
177+
this.awsExecutionEnvVar = awsExecutionEnvVar;
178+
return this;
179+
}
180+
181+
public TestData awsRegionEnvVar(String awsRegionEnvVar) {
182+
this.awsRegionEnvVar = awsRegionEnvVar;
183+
return this;
184+
}
185+
186+
public TestData awsDefaultRegionEnvVar(String awsDefaultRegionEnvVar) {
187+
this.awsDefaultRegionEnvVar = awsDefaultRegionEnvVar;
188+
return this;
189+
}
190+
191+
public TestData ec2MetadataConfig(Ec2MetadataConfig ec2MetadataConfig) {
192+
this.ec2MetadataConfig = ec2MetadataConfig;
193+
return this;
194+
}
195+
196+
public TestData expectedResolvedMode(DefaultsMode expectedResolvedMode) {
197+
this.expectedResolvedMode = expectedResolvedMode;
198+
return this;
199+
}
200+
}
201+
202+
private static final class Ec2MetadataConfig {
203+
private boolean imdsAvailable;
204+
private String region;
205+
private String ec2MetadataDisabledEnvVar;
206+
207+
public Ec2MetadataConfig imdsAvailable(boolean imdsAvailable) {
208+
this.imdsAvailable = imdsAvailable;
209+
return this;
210+
}
211+
212+
public Ec2MetadataConfig region(String region) {
213+
this.region = region;
214+
return this;
215+
}
216+
217+
public Ec2MetadataConfig ec2MetadataDisabledEnvVar(String ec2MetadataDisabledEnvVar) {
218+
this.ec2MetadataDisabledEnvVar = ec2MetadataDisabledEnvVar;
219+
return this;
220+
}
221+
}
222+
}
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)