diff --git a/.changes/next-release/feature-AWSSDKforJavav2-12967c8.json b/.changes/next-release/feature-AWSSDKforJavav2-12967c8.json new file mode 100644 index 000000000000..13760a45cd6e --- /dev/null +++ b/.changes/next-release/feature-AWSSDKforJavav2-12967c8.json @@ -0,0 +1,6 @@ +{ + "category": "AWS SDK for Java v2", + "contributor": "", + "type": "feature", + "description": "Introduce a defaults mode configuration that determines how certain default configuration options are resolved in the SDK. See `DefaultsMode` for more information." +} diff --git a/codegen-lite-maven-plugin/src/main/java/software/amazon/awssdk/codegen/lite/maven/plugin/DefaultsModeGenerationMojo.java b/codegen-lite-maven-plugin/src/main/java/software/amazon/awssdk/codegen/lite/maven/plugin/DefaultsModeGenerationMojo.java new file mode 100644 index 000000000000..f75936f95794 --- /dev/null +++ b/codegen-lite-maven-plugin/src/main/java/software/amazon/awssdk/codegen/lite/maven/plugin/DefaultsModeGenerationMojo.java @@ -0,0 +1,74 @@ +/* + * 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.codegen.lite.maven.plugin; + +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.project.MavenProject; +import software.amazon.awssdk.codegen.lite.CodeGenerator; +import software.amazon.awssdk.codegen.lite.defaultsmode.DefaultConfiguration; +import software.amazon.awssdk.codegen.lite.defaultsmode.DefaultsLoader; +import software.amazon.awssdk.codegen.lite.defaultsmode.DefaultsModeConfigurationGenerator; +import software.amazon.awssdk.codegen.lite.defaultsmode.DefaultsModeGenerator; + +/** + * The Maven mojo to generate defaults mode related classes. + */ +@Mojo(name = "generate-defaults-mode") +public class DefaultsModeGenerationMojo extends AbstractMojo { + + private static final String DEFAULTS_MODE_BASE = "software.amazon.awssdk.awscore.defaultsmode"; + private static final String DEFAULTS_MODE_CONFIGURATION_BASE = "software.amazon.awssdk.awscore.internal.defaultsmode"; + + @Parameter(property = "outputDirectory", defaultValue = "${project.build.directory}") + private String outputDirectory; + + @Parameter(defaultValue = "${project}", readonly = true) + private MavenProject project; + + @Parameter(property = "defaultConfigurationFile", defaultValue = + "${basedir}/src/main/resources/software/amazon/awssdk/awscore/internal/defaults/sdk-default-configuration.json") + private File defaultConfigurationFile; + + public void execute() { + Path baseSourcesDirectory = Paths.get(outputDirectory).resolve("generated-sources").resolve("sdk"); + Path testsDirectory = Paths.get(outputDirectory).resolve("generated-test-sources").resolve("sdk-tests"); + + DefaultConfiguration configuration = DefaultsLoader.load(defaultConfigurationFile); + + generateDefaultsModeClass(baseSourcesDirectory, configuration); + generateDefaultsModeConfiguartionClass(baseSourcesDirectory, configuration); + + project.addCompileSourceRoot(baseSourcesDirectory.toFile().getAbsolutePath()); + project.addTestCompileSourceRoot(testsDirectory.toFile().getAbsolutePath()); + } + + public void generateDefaultsModeClass(Path baseSourcesDirectory, DefaultConfiguration configuration) { + Path sourcesDirectory = baseSourcesDirectory.resolve(DEFAULTS_MODE_BASE.replace(".", "/")); + new CodeGenerator(sourcesDirectory.toString(), new DefaultsModeGenerator(DEFAULTS_MODE_BASE, configuration)).generate(); + } + + public void generateDefaultsModeConfiguartionClass(Path baseSourcesDirectory, DefaultConfiguration configuration) { + Path sourcesDirectory = baseSourcesDirectory.resolve(DEFAULTS_MODE_CONFIGURATION_BASE.replace(".", "/")); + new CodeGenerator(sourcesDirectory.toString(), new DefaultsModeConfigurationGenerator(DEFAULTS_MODE_CONFIGURATION_BASE, + DEFAULTS_MODE_BASE, + configuration)).generate(); + } +} diff --git a/codegen-lite/pom.xml b/codegen-lite/pom.xml index afc9ced1ea6f..edc7d74dd337 100644 --- a/codegen-lite/pom.xml +++ b/codegen-lite/pom.xml @@ -57,6 +57,11 @@ utils ${awsjavasdk.version} + + software.amazon.awssdk + json-utils + ${awsjavasdk.version} + com.squareup javapoet diff --git a/codegen-lite/src/main/java/software/amazon/awssdk/codegen/lite/defaultsmode/DefaultConfiguration.java b/codegen-lite/src/main/java/software/amazon/awssdk/codegen/lite/defaultsmode/DefaultConfiguration.java new file mode 100644 index 000000000000..c18a68996c72 --- /dev/null +++ b/codegen-lite/src/main/java/software/amazon/awssdk/codegen/lite/defaultsmode/DefaultConfiguration.java @@ -0,0 +1,65 @@ +/* + * 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.codegen.lite.defaultsmode; + +import java.util.Map; + +/** + * Container for default configuration + */ +public class DefaultConfiguration { + /** + * The transformed configuration values for each mode + */ + private Map> modeDefaults; + + /** + * The documentation for each mode + */ + private Map modesDocumentation; + + /* + * The documentation for each configuration option + */ + private Map configurationDocumentation; + + public Map> modeDefaults() { + return modeDefaults; + } + + public DefaultConfiguration modeDefaults(Map> modeDefaults) { + this.modeDefaults = modeDefaults; + return this; + } + + public Map modesDocumentation() { + return modesDocumentation; + } + + public DefaultConfiguration modesDocumentation(Map documentation) { + this.modesDocumentation = documentation; + return this; + } + + public Map configurationDocumentation() { + return configurationDocumentation; + } + + public DefaultConfiguration configurationDocumentation(Map configurationDocumentation) { + this.configurationDocumentation = configurationDocumentation; + return this; + } +} diff --git a/codegen-lite/src/main/java/software/amazon/awssdk/codegen/lite/defaultsmode/DefaultsLoader.java b/codegen-lite/src/main/java/software/amazon/awssdk/codegen/lite/defaultsmode/DefaultsLoader.java new file mode 100644 index 000000000000..be455e729f60 --- /dev/null +++ b/codegen-lite/src/main/java/software/amazon/awssdk/codegen/lite/defaultsmode/DefaultsLoader.java @@ -0,0 +1,205 @@ +/* + * 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.codegen.lite.defaultsmode; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.protocols.jsoncore.JsonNode; +import software.amazon.awssdk.protocols.jsoncore.JsonNodeParser; +import software.amazon.awssdk.protocols.jsoncore.JsonNodeVisitor; +import software.amazon.awssdk.utils.Logger; + +/** + * Loads sdk-default-configuration.json into memory. It filters out unsupported configuration options from the file + */ +@SdkInternalApi +public final class DefaultsLoader { + private static final Logger log = Logger.loggerFor(DefaultsLoader.class); + + private static final Set UNSUPPORTED_OPTIONS = new HashSet<>(); + + static { + UNSUPPORTED_OPTIONS.add("stsRegionalEndpoints"); + } + + private DefaultsLoader() { + } + + public static DefaultConfiguration load(File path) { + return loadDefaultsFromFile(path); + } + + private static DefaultConfiguration loadDefaultsFromFile(File path) { + DefaultConfiguration defaultsResolution = new DefaultConfiguration(); + Map> resolvedDefaults = new HashMap<>(); + + try (FileInputStream fileInputStream = new FileInputStream(path)) { + JsonNodeParser jsonNodeParser = JsonNodeParser.builder().build(); + + Map sdkDefaultConfiguration = jsonNodeParser.parse(fileInputStream) + .asObject(); + + Map base = sdkDefaultConfiguration.get("base").asObject(); + Map modes = sdkDefaultConfiguration.get("modes").asObject(); + + modes.forEach((mode, modifiers) -> applyModificationToOneMode(resolvedDefaults, base, mode, modifiers)); + + Map documentation = sdkDefaultConfiguration.get("documentation").asObject(); + Map modesDocumentation = documentation.get("modes").asObject(); + Map configDocumentation = documentation.get("configuration").asObject(); + + defaultsResolution.modesDocumentation( + modesDocumentation.entrySet() + .stream() + .collect(HashMap::new, (m, e) -> m.put(e.getKey(), e.getValue().asString()), Map::putAll)); + defaultsResolution.configurationDocumentation( + configDocumentation.entrySet() + .stream() + .filter(e -> !UNSUPPORTED_OPTIONS.contains(e.getKey())) + .collect(HashMap::new, (m, e) -> m.put(e.getKey(), e.getValue().asString()), Map::putAll)); + + } catch (IOException e) { + throw new RuntimeException(e); + } + + defaultsResolution.modeDefaults(resolvedDefaults); + + return defaultsResolution; + } + + private static void applyModificationToOneConfigurationOption(Map resolvedDefaultsForCurrentMode, + String option, + JsonNode modifier) { + String resolvedValue; + String baseValue = resolvedDefaultsForCurrentMode.get(option); + + if (UNSUPPORTED_OPTIONS.contains(option)) { + return; + } + + Map modifierMap = modifier.asObject(); + + if (modifierMap.size() != 1) { + throw new IllegalStateException("More than one modifier exists for option " + option); + } + + String modifierString = modifierMap.keySet().iterator().next(); + + switch (modifierString) { + case "override": + resolvedValue = modifierMap.get("override").visit(new StringJsonNodeVisitor()); + break; + case "multiply": + resolvedValue = processMultiply(baseValue, modifierMap); + break; + case "add": + resolvedValue = processAdd(baseValue, modifierMap); + break; + default: + throw new UnsupportedOperationException("Unsupported modifier: " + modifierString); + } + + resolvedDefaultsForCurrentMode.put(option, resolvedValue); + } + + private static void applyModificationToOneMode(Map> resolvedDefaults, + Map base, + String mode, + JsonNode modifiers) { + + log.info(() -> "Apply modification for mode: " + mode); + Map resolvedDefaultsForCurrentMode = + base.entrySet().stream().filter(e -> !UNSUPPORTED_OPTIONS.contains(e.getKey())) + .collect(HashMap::new, (m, e) -> m.put(e.getKey(), + e.getValue().visit(new StringJsonNodeVisitor())), Map::putAll); + + + // Iterate the configuration options and apply modification. + modifiers.asObject().forEach((option, modifier) -> applyModificationToOneConfigurationOption( + resolvedDefaultsForCurrentMode, option, modifier)); + + resolvedDefaults.put(mode, resolvedDefaultsForCurrentMode); + } + + private static String processAdd(String baseValue, Map modifierMap) { + String resolvedValue; + String add = modifierMap.get("add").asNumber(); + int parsedAdd = Integer.parseInt(add); + int number = Math.addExact(Integer.parseInt(baseValue), parsedAdd); + resolvedValue = String.valueOf(number); + return resolvedValue; + } + + private static String processMultiply(String baseValue, Map modifierMap) { + String resolvedValue; + String multiply = modifierMap.get("multiply").asNumber(); + double parsedValue = Double.parseDouble(multiply); + + double resolvedNumber = Integer.parseInt(baseValue) * parsedValue; + int castValue = (int) resolvedNumber; + + if (castValue != resolvedNumber) { + throw new IllegalStateException("The transformed value must be be a float number: " + castValue); + } + + resolvedValue = String.valueOf(castValue); + return resolvedValue; + } + + private static final class StringJsonNodeVisitor implements JsonNodeVisitor { + @Override + public String visitNull() { + throw new IllegalStateException("Invalid type encountered"); + } + + @Override + public String visitBoolean(boolean b) { + throw new IllegalStateException("Invalid type (boolean) encountered " + b); + } + + @Override + public String visitNumber(String s) { + return s; + } + + @Override + public String visitString(String s) { + return s; + } + + @Override + public String visitArray(List list) { + throw new IllegalStateException("Invalid type (list) encountered: " + list); + } + + @Override + public String visitObject(Map map) { + throw new IllegalStateException("Invalid type (map) encountered: " + map); + } + + @Override + public String visitEmbeddedObject(Object o) { + throw new IllegalStateException("Invalid type (embedded) encountered: " + o); + } + } +} diff --git a/codegen-lite/src/main/java/software/amazon/awssdk/codegen/lite/defaultsmode/DefaultsModeConfigurationGenerator.java b/codegen-lite/src/main/java/software/amazon/awssdk/codegen/lite/defaultsmode/DefaultsModeConfigurationGenerator.java new file mode 100644 index 000000000000..dfff8bc408cb --- /dev/null +++ b/codegen-lite/src/main/java/software/amazon/awssdk/codegen/lite/defaultsmode/DefaultsModeConfigurationGenerator.java @@ -0,0 +1,274 @@ +/* + * 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.codegen.lite.defaultsmode; + +import static javax.lang.model.element.Modifier.FINAL; +import static javax.lang.model.element.Modifier.PRIVATE; +import static javax.lang.model.element.Modifier.PUBLIC; +import static javax.lang.model.element.Modifier.STATIC; + +import com.squareup.javapoet.AnnotationSpec; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.CodeBlock; +import com.squareup.javapoet.FieldSpec; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.ParameterizedTypeName; +import com.squareup.javapoet.TypeSpec; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import javax.lang.model.element.Modifier; +import software.amazon.awssdk.annotations.Generated; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.codegen.lite.PoetClass; +import software.amazon.awssdk.utils.AttributeMap; + +/** + * Generates DefaultsModeConfiguration class that contains default options for each mode + */ +public class DefaultsModeConfigurationGenerator implements PoetClass { + + private static final String DEFAULT_CONFIG_BY_MODE_ENUM_MAP = "DEFAULT_CONFIG_BY_MODE"; + private static final String DEFAULT_HTTP_CONFIG_BY_MODE_ENUM_MAP = "DEFAULT_HTTP_CONFIG_BY_MODE"; + private static final String DEFAULTS_VAR_SUFFIX = "_DEFAULTS"; + private static final String HTTP_DEFAULTS_VAR_SUFFIX = "_HTTP_DEFAULTS"; + private static final Map CONFIGURATION_MAPPING = new HashMap<>(); + private static final Map HTTP_CONFIGURATION_MAPPING = new HashMap<>(); + private static final String CONNECT_TIMEOUT_IN_MILLIS = "connectTimeoutInMillis"; + private static final String TLS_NEGOTIATION_TIMEOUT_IN_MILLIS = "tlsNegotiationTimeoutInMillis"; + private static final String S3_US_EAST_1_REGIONAL_ENDPOINTS = "s3UsEast1RegionalEndpoints"; + + private final String basePackage; + private final String defaultsModeBase; + private final DefaultConfiguration configuration; + + static { + HTTP_CONFIGURATION_MAPPING.put(CONNECT_TIMEOUT_IN_MILLIS, + new OptionMetadata(ClassName.get("java.time", "Duration"), + ClassName.get("software.amazon.awssdk.http", + "SdkHttpConfigurationOption", "CONNECTION_TIMEOUT"))); + HTTP_CONFIGURATION_MAPPING.put(TLS_NEGOTIATION_TIMEOUT_IN_MILLIS, + new OptionMetadata(ClassName.get("java.time", "Duration"), + ClassName.get("software.amazon.awssdk.http", + "SdkHttpConfigurationOption", + "TLS_NEGOTIATION_TIMEOUT"))); + CONFIGURATION_MAPPING.put("retryMode", new OptionMetadata(ClassName.get("software.amazon.awssdk.core.retry", "RetryMode" + ), ClassName.get("software.amazon.awssdk.core.client.config", "SdkClientOption", "DEFAULT_RETRY_MODE"))); + + CONFIGURATION_MAPPING.put(S3_US_EAST_1_REGIONAL_ENDPOINTS, + new OptionMetadata(ClassName.get(String.class), + ClassName.get("software.amazon.awssdk.regions", + "ServiceMetadataAdvancedOption", + "DEFAULT_S3_US_EAST_1_REGIONAL_ENDPOINT"))); + } + + public DefaultsModeConfigurationGenerator(String basePackage, String defaultsModeBase, DefaultConfiguration configuration) { + this.basePackage = basePackage; + this.configuration = configuration; + this.defaultsModeBase = defaultsModeBase; + } + + @Override + public TypeSpec poetClass() { + TypeSpec.Builder builder = TypeSpec.classBuilder(className()) + .addModifiers(PUBLIC, FINAL) + .addJavadoc(documentation()) + .addAnnotation(SdkInternalApi.class) + .addAnnotation(AnnotationSpec.builder(Generated.class) + .addMember("value", + "$S", + "software.amazon.awssdk:codegen") + .build()) + .addMethod(defaultConfigMethod(DEFAULT_CONFIG_BY_MODE_ENUM_MAP, "defaultConfig")) + .addMethod(defaultConfigMethod(DEFAULT_HTTP_CONFIG_BY_MODE_ENUM_MAP, + "defaultHttpConfig")) + .addMethod(createConstructor()); + + + configuration.modeDefaults().entrySet().forEach(entry -> { + builder.addField(addDefaultsFieldForMode(entry)); + builder.addField(addHttpDefaultsFieldForMode(entry)); + }); + + addDefaultsFieldForLegacy(builder, "LEGACY_DEFAULTS"); + addDefaultsFieldForLegacy(builder, "LEGACY_HTTP_DEFAULTS"); + + addEnumMapField(builder, DEFAULT_CONFIG_BY_MODE_ENUM_MAP); + addEnumMapField(builder, DEFAULT_HTTP_CONFIG_BY_MODE_ENUM_MAP); + + addStaticEnumMapBlock(builder); + return builder.build(); + } + + private void addStaticEnumMapBlock(TypeSpec.Builder builder) { + CodeBlock.Builder staticCodeBlock = CodeBlock.builder(); + + putItemsToEnumMap(staticCodeBlock, configuration.modeDefaults().keySet(), DEFAULTS_VAR_SUFFIX, + DEFAULT_CONFIG_BY_MODE_ENUM_MAP); + putItemsToEnumMap(staticCodeBlock, configuration.modeDefaults().keySet(), HTTP_DEFAULTS_VAR_SUFFIX, + DEFAULT_HTTP_CONFIG_BY_MODE_ENUM_MAP); + + builder.addStaticBlock(staticCodeBlock.build()); + } + + private void addEnumMapField(TypeSpec.Builder builder, String name) { + ParameterizedTypeName map = ParameterizedTypeName.get(ClassName.get(Map.class), + defaultsModeClassName(), + ClassName.get(AttributeMap.class)); + FieldSpec field = FieldSpec.builder(map, name, PRIVATE, STATIC, FINAL) + .initializer("new $T<>(DefaultsMode.class)", EnumMap.class).build(); + builder.addField(field); + } + + private void putItemsToEnumMap(CodeBlock.Builder codeBlock, Set modes, String suffix, String mapName) { + modes.forEach(m -> { + String mode = sanitizeMode(m); + codeBlock.addStatement("$N.put(DefaultsMode.$N, $N)", mapName, mode, mode + suffix); + }); + + // Add LEGACY since LEGACY is not in the modes set + codeBlock.addStatement("$N.put(DefaultsMode.LEGACY, LEGACY$N)", mapName, suffix); + } + + @Override + public ClassName className() { + return ClassName.get(basePackage, "DefaultsModeConfiguration"); + } + + private FieldSpec addDefaultsFieldForMode(Map.Entry> modeEntry) { + String mode = modeEntry.getKey(); + String fieldName = sanitizeMode(mode) + DEFAULTS_VAR_SUFFIX; + + CodeBlock.Builder attributeBuilder = CodeBlock.builder() + .add("$T.builder()", AttributeMap.class); + + modeEntry.getValue() + .entrySet() + .stream() + .filter(e -> CONFIGURATION_MAPPING.containsKey(e.getKey())) + .forEach(e -> attributeMapBuilder(e.getKey(), e.getValue(), attributeBuilder)); + + + FieldSpec.Builder fieldSpec = FieldSpec.builder(AttributeMap.class, fieldName, PRIVATE, STATIC, FINAL) + .initializer(attributeBuilder + .add(".build()") + .build()); + + + return fieldSpec.build(); + } + + private void addDefaultsFieldForLegacy(TypeSpec.Builder builder, String name) { + FieldSpec field = FieldSpec.builder(AttributeMap.class, name, PRIVATE, STATIC, FINAL) + .initializer("$T.empty()", AttributeMap.class).build(); + builder.addField(field); + } + + private void attributeMapBuilder(String option, String value, CodeBlock.Builder attributeBuilder) { + OptionMetadata optionMetadata = CONFIGURATION_MAPPING.get(option); + switch (option) { + case "retryMode": + attributeBuilder.add(".put($T, $T.$N)", optionMetadata.attribute, optionMetadata.type, + value.toUpperCase(Locale.US)); + break; + case S3_US_EAST_1_REGIONAL_ENDPOINTS: + attributeBuilder.add(".put($T, $S)", optionMetadata.attribute, value); + break; + default: + throw new IllegalStateException("Unsupported option " + option); + } + } + + private void httpAttributeMapBuilder(String option, String value, CodeBlock.Builder attributeBuilder) { + OptionMetadata optionMetadata = HTTP_CONFIGURATION_MAPPING.get(option); + switch (option) { + case CONNECT_TIMEOUT_IN_MILLIS: + case TLS_NEGOTIATION_TIMEOUT_IN_MILLIS: + attributeBuilder.add(".put($T, $T.ofMillis($N))", optionMetadata.attribute, optionMetadata.type, value); + break; + default: + throw new IllegalStateException("Unsupported option " + option); + } + } + + private FieldSpec addHttpDefaultsFieldForMode(Map.Entry> modeEntry) { + String mode = modeEntry.getKey(); + String fieldName = sanitizeMode(mode) + HTTP_DEFAULTS_VAR_SUFFIX; + + CodeBlock.Builder attributeBuilder = CodeBlock.builder() + .add("$T.builder()", AttributeMap.class); + + modeEntry.getValue() + .entrySet() + .stream() + .filter(e -> HTTP_CONFIGURATION_MAPPING.containsKey(e.getKey())) + .forEach(e -> httpAttributeMapBuilder(e.getKey(), e.getValue(), attributeBuilder)); + + FieldSpec.Builder fieldSpec = FieldSpec.builder(AttributeMap.class, fieldName, PRIVATE, STATIC, FINAL) + .initializer(attributeBuilder + .add(".build()") + .build()); + + return fieldSpec.build(); + } + + private String sanitizeMode(String str) { + return str.replace('-', '_').toUpperCase(Locale.US); + } + + private CodeBlock documentation() { + CodeBlock.Builder builder = CodeBlock.builder() + .add("Contains a collection of default configuration options for each " + + "DefaultsMode"); + + return builder.build(); + } + + private MethodSpec defaultConfigMethod(String enumMap, String methodName) { + MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(methodName) + .returns(AttributeMap.class) + .addModifiers(PUBLIC, STATIC) + .addJavadoc("Return the default config options for a given defaults " + + "mode") + .addParameter(defaultsModeClassName(), "mode") + .addStatement("return $N.getOrDefault(mode, $T.empty())", + enumMap, AttributeMap.class); + + return methodBuilder.build(); + } + + private ClassName defaultsModeClassName() { + return ClassName.get(defaultsModeBase, "DefaultsMode"); + } + + private MethodSpec createConstructor() { + return MethodSpec.constructorBuilder() + .addModifiers(Modifier.PRIVATE) + .build(); + } + + private static final class OptionMetadata { + private final ClassName type; + private final ClassName attribute; + + OptionMetadata(ClassName type, ClassName attribute) { + this.type = type; + this.attribute = attribute; + } + } +} diff --git a/codegen-lite/src/main/java/software/amazon/awssdk/codegen/lite/defaultsmode/DefaultsModeGenerator.java b/codegen-lite/src/main/java/software/amazon/awssdk/codegen/lite/defaultsmode/DefaultsModeGenerator.java new file mode 100644 index 000000000000..3ce48e8d4a72 --- /dev/null +++ b/codegen-lite/src/main/java/software/amazon/awssdk/codegen/lite/defaultsmode/DefaultsModeGenerator.java @@ -0,0 +1,189 @@ +/* + * 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.codegen.lite.defaultsmode; + +import static javax.lang.model.element.Modifier.FINAL; +import static javax.lang.model.element.Modifier.PRIVATE; +import static javax.lang.model.element.Modifier.PUBLIC; +import static javax.lang.model.element.Modifier.STATIC; + +import com.squareup.javapoet.AnnotationSpec; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.CodeBlock; +import com.squareup.javapoet.FieldSpec; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.ParameterizedTypeName; +import com.squareup.javapoet.TypeSpec; +import java.util.Locale; +import java.util.Map; +import javax.lang.model.element.Modifier; +import software.amazon.awssdk.annotations.Generated; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.codegen.lite.PoetClass; +import software.amazon.awssdk.utils.Validate; +import software.amazon.awssdk.utils.internal.EnumUtils; + +/** + * Generates DefaultsMode enum + */ +public class DefaultsModeGenerator implements PoetClass { + + private static final String VALUE = "value"; + private static final String VALUE_MAP = "VALUE_MAP"; + + private final String basePackage; + private final DefaultConfiguration configuration; + + public DefaultsModeGenerator(String basePackage, DefaultConfiguration configuration) { + this.basePackage = basePackage; + this.configuration = configuration; + } + + @Override + public TypeSpec poetClass() { + TypeSpec.Builder builder = TypeSpec.enumBuilder(className()) + .addField(valueMapField()) + .addField(String.class, VALUE, Modifier.PRIVATE, Modifier.FINAL) + .addModifiers(PUBLIC) + .addJavadoc(documentation()) + .addAnnotation(SdkPublicApi.class) + .addAnnotation(AnnotationSpec.builder(Generated.class) + .addMember(VALUE, + "$S", + "software.amazon.awssdk:codegen") + .build()) + .addMethod(fromValueSpec()) + .addMethod(toStringBuilder().addStatement("return $T.valueOf($N)", String.class, + VALUE).build()) + .addMethod(createConstructor()); + + builder.addEnumConstant("LEGACY", enumValueTypeSpec("legacy", javaDocForMode("legacy"))); + + configuration.modeDefaults().keySet().forEach(k -> { + String enumKey = sanitizeEnum(k); + builder.addEnumConstant(enumKey, enumValueTypeSpec(k, javaDocForMode(k))); + }); + + builder.addEnumConstant("AUTO", enumValueTypeSpec("auto", javaDocForMode("auto"))); + + return builder.build(); + } + + @Override + public ClassName className() { + return ClassName.get(basePackage, "DefaultsMode"); + } + + private TypeSpec enumValueTypeSpec(String value, String documentation) { + return TypeSpec.anonymousClassBuilder("$S", value) + .addJavadoc(documentation) + .build(); + } + + private FieldSpec valueMapField() { + ParameterizedTypeName mapType = ParameterizedTypeName.get(ClassName.get(Map.class), + ClassName.get(String.class), + className()); + return FieldSpec.builder(mapType, VALUE_MAP) + .addModifiers(PRIVATE, STATIC, FINAL) + .initializer("$1T.uniqueIndex($2T.class, $2T::toString)", EnumUtils.class, className()) + .build(); + } + + private String sanitizeEnum(String str) { + return str.replace('-', '_').toUpperCase(Locale.US); + } + + private String javaDocForMode(String mode) { + return configuration.modesDocumentation().getOrDefault(mode, ""); + } + + private CodeBlock documentation() { + CodeBlock.Builder builder = CodeBlock.builder() + .add("A defaults mode determines how certain default configuration options are " + + "resolved in " + + "the SDK. " + + "Based on the provided " + + "mode, the SDK will vend sensible default values tailored to the mode for " + + "the following settings:") + .add(System.lineSeparator()); + + builder.add("
    "); + configuration.configurationDocumentation().forEach((k, v) -> { + builder.add("
  • " + k + ": " + v + "
  • "); + }); + builder.add("
").add(System.lineSeparator()); + + builder.add("

All options above can be configured by users, and the overridden value will take precedence.") + .add("

Note: for any mode other than {@link #LEGACY}, the vended default values might change " + + "as best practices may evolve. As a result, it is encouraged to perform testing when upgrading the SDK if" + + " you are using a mode other than {@link #LEGACY}") + .add(System.lineSeparator()); + + return builder.add("

While the {@link #LEGACY} defaults mode is specific to Java, other modes are " + + "standardized across " + + "all of the AWS SDKs

") + .add(System.lineSeparator()) + .add("

The defaults mode can be configured:") + .add(System.lineSeparator()) + .add("

    ") + .add("
  1. Directly on a client via {@code AwsClientBuilder.Builder#defaultsMode" + + "(DefaultsMode)}.
  2. ") + .add(System.lineSeparator()) + .add("
  3. On a configuration profile via the \"defaults_mode\" profile file property.
  4. ") + .add(System.lineSeparator()) + .add("
  5. Globally via the \"aws.defaultsMode\" system property.
  6. ") + .add("
  7. Globally via the \"AWS_DEFAULTS_MODE\" environment variable.
  8. ") + .add("
") + .build(); + } + + + private MethodSpec fromValueSpec() { + return MethodSpec.methodBuilder("fromValue") + .returns(className()) + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addJavadoc("Use this in place of valueOf to convert the raw string returned by the service into the " + + "enum value.\n\n" + + "@param $N real value\n" + + "@return $T corresponding to the value\n", VALUE, className()) + .addParameter(String.class, VALUE) + .addStatement("$T.paramNotNull(value, $S)", Validate.class, VALUE) + .beginControlFlow("if (!VALUE_MAP.containsKey(value))") + .addStatement("throw new IllegalArgumentException($S + value)", "The provided value is not a" + + " valid " + + "defaults mode ") + .endControlFlow() + .addStatement("return $N.get($N)", VALUE_MAP, VALUE) + .build(); + } + + private MethodSpec createConstructor() { + return MethodSpec.constructorBuilder() + .addModifiers(Modifier.PRIVATE) + .addParameter(String.class, VALUE) + .addStatement("this.$1N = $1N", VALUE) + .build(); + } + + private static MethodSpec.Builder toStringBuilder() { + return MethodSpec.methodBuilder("toString") + .returns(String.class) + .addModifiers(Modifier.PUBLIC) + .addAnnotation(Override.class); + } + +} diff --git a/codegen-lite/src/test/java/software/amazon/awssdk/codegen/lite/defaultsmode/DefaultsModeGenerationTest.java b/codegen-lite/src/test/java/software/amazon/awssdk/codegen/lite/defaultsmode/DefaultsModeGenerationTest.java new file mode 100644 index 000000000000..9ceb3c9e40d4 --- /dev/null +++ b/codegen-lite/src/test/java/software/amazon/awssdk/codegen/lite/defaultsmode/DefaultsModeGenerationTest.java @@ -0,0 +1,51 @@ +/* + * 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.codegen.lite.defaultsmode; + +import static org.hamcrest.MatcherAssert.assertThat; +import static software.amazon.awssdk.codegen.lite.PoetMatchers.generatesTo; + +import java.io.File; +import java.nio.file.Paths; +import org.junit.Before; +import org.junit.Test; + +public class DefaultsModeGenerationTest { + + private static final String DEFAULT_CONFIGURATION = "/software/amazon/awssdk/codegen/lite/test-sdk-default-configuration.json"; + private static final String DEFAULTS_MODE_BASE = "software.amazon.awssdk.defaultsmode"; + + private File file; + private DefaultConfiguration defaultConfiguration; + + @Before + public void before() throws Exception { + this.file = Paths.get(getClass().getResource(DEFAULT_CONFIGURATION).toURI()).toFile(); + this.defaultConfiguration = DefaultsLoader.load(file); + } + + @Test + public void defaultsModeEnum() { + DefaultsModeGenerator generator = new DefaultsModeGenerator(DEFAULTS_MODE_BASE, defaultConfiguration); + assertThat(generator, generatesTo("defaults-mode.java")); + } + + @Test + public void defaultsModeConfigurationClass() { + DefaultsModeConfigurationGenerator generator = new DefaultsModeConfigurationGenerator(DEFAULTS_MODE_BASE, DEFAULTS_MODE_BASE, defaultConfiguration); + assertThat(generator, generatesTo("defaults-mode-configuration.java")); + } + +} diff --git a/codegen-lite/src/test/resources/software/amazon/awssdk/codegen/lite/defaultsmode/defaults-mode-configuration.java b/codegen-lite/src/test/resources/software/amazon/awssdk/codegen/lite/defaultsmode/defaults-mode-configuration.java new file mode 100644 index 000000000000..d1e73a79daa7 --- /dev/null +++ b/codegen-lite/src/test/resources/software/amazon/awssdk/codegen/lite/defaultsmode/defaults-mode-configuration.java @@ -0,0 +1,89 @@ +package software.amazon.awssdk.defaultsmode; + +import java.time.Duration; +import java.util.EnumMap; +import java.util.Map; +import software.amazon.awssdk.annotations.Generated; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.core.client.config.SdkClientOption; +import software.amazon.awssdk.core.retry.RetryMode; +import software.amazon.awssdk.http.SdkHttpConfigurationOption; +import software.amazon.awssdk.regions.ServiceMetadataAdvancedOption; +import software.amazon.awssdk.utils.AttributeMap; + +/** + * Contains a collection of default configuration options for each DefaultsMode + */ +@SdkInternalApi +@Generated("software.amazon.awssdk:codegen") +public final class DefaultsModeConfiguration { + private static final AttributeMap STANDARD_DEFAULTS = AttributeMap.builder() + .put(SdkClientOption.DEFAULT_RETRY_MODE, RetryMode.STANDARD) + .put(ServiceMetadataAdvancedOption.DEFAULT_S3_US_EAST_1_REGIONAL_ENDPOINT, "regional").build(); + + private static final AttributeMap STANDARD_HTTP_DEFAULTS = AttributeMap.builder() + .put(SdkHttpConfigurationOption.CONNECTION_TIMEOUT, Duration.ofMillis(2000)) + .put(SdkHttpConfigurationOption.TLS_NEGOTIATION_TIMEOUT, Duration.ofMillis(2000)).build(); + + private static final AttributeMap MOBILE_DEFAULTS = AttributeMap.builder() + .put(SdkClientOption.DEFAULT_RETRY_MODE, RetryMode.ADAPTIVE) + .put(ServiceMetadataAdvancedOption.DEFAULT_S3_US_EAST_1_REGIONAL_ENDPOINT, "regional").build(); + + private static final AttributeMap MOBILE_HTTP_DEFAULTS = AttributeMap.builder() + .put(SdkHttpConfigurationOption.CONNECTION_TIMEOUT, Duration.ofMillis(10000)) + .put(SdkHttpConfigurationOption.TLS_NEGOTIATION_TIMEOUT, Duration.ofMillis(11000)).build(); + + private static final AttributeMap CROSS_REGION_DEFAULTS = AttributeMap.builder() + .put(SdkClientOption.DEFAULT_RETRY_MODE, RetryMode.STANDARD) + .put(ServiceMetadataAdvancedOption.DEFAULT_S3_US_EAST_1_REGIONAL_ENDPOINT, "regional").build(); + + private static final AttributeMap CROSS_REGION_HTTP_DEFAULTS = AttributeMap.builder() + .put(SdkHttpConfigurationOption.CONNECTION_TIMEOUT, Duration.ofMillis(2800)) + .put(SdkHttpConfigurationOption.TLS_NEGOTIATION_TIMEOUT, Duration.ofMillis(2800)).build(); + + private static final AttributeMap IN_REGION_DEFAULTS = AttributeMap.builder() + .put(SdkClientOption.DEFAULT_RETRY_MODE, RetryMode.STANDARD) + .put(ServiceMetadataAdvancedOption.DEFAULT_S3_US_EAST_1_REGIONAL_ENDPOINT, "regional").build(); + + private static final AttributeMap IN_REGION_HTTP_DEFAULTS = AttributeMap.builder() + .put(SdkHttpConfigurationOption.CONNECTION_TIMEOUT, Duration.ofMillis(1000)) + .put(SdkHttpConfigurationOption.TLS_NEGOTIATION_TIMEOUT, Duration.ofMillis(1000)).build(); + + private static final AttributeMap LEGACY_DEFAULTS = AttributeMap.empty(); + + private static final AttributeMap LEGACY_HTTP_DEFAULTS = AttributeMap.empty(); + + private static final Map DEFAULT_CONFIG_BY_MODE = new EnumMap<>(DefaultsMode.class); + + private static final Map DEFAULT_HTTP_CONFIG_BY_MODE = new EnumMap<>(DefaultsMode.class); + + static { + DEFAULT_CONFIG_BY_MODE.put(DefaultsMode.STANDARD, STANDARD_DEFAULTS); + DEFAULT_CONFIG_BY_MODE.put(DefaultsMode.MOBILE, MOBILE_DEFAULTS); + DEFAULT_CONFIG_BY_MODE.put(DefaultsMode.CROSS_REGION, CROSS_REGION_DEFAULTS); + DEFAULT_CONFIG_BY_MODE.put(DefaultsMode.IN_REGION, IN_REGION_DEFAULTS); + DEFAULT_CONFIG_BY_MODE.put(DefaultsMode.LEGACY, LEGACY_DEFAULTS); + DEFAULT_HTTP_CONFIG_BY_MODE.put(DefaultsMode.STANDARD, STANDARD_HTTP_DEFAULTS); + DEFAULT_HTTP_CONFIG_BY_MODE.put(DefaultsMode.MOBILE, MOBILE_HTTP_DEFAULTS); + DEFAULT_HTTP_CONFIG_BY_MODE.put(DefaultsMode.CROSS_REGION, CROSS_REGION_HTTP_DEFAULTS); + DEFAULT_HTTP_CONFIG_BY_MODE.put(DefaultsMode.IN_REGION, IN_REGION_HTTP_DEFAULTS); + DEFAULT_HTTP_CONFIG_BY_MODE.put(DefaultsMode.LEGACY, LEGACY_HTTP_DEFAULTS); + } + + private DefaultsModeConfiguration() { + } + + /** + * Return the default config options for a given defaults mode + */ + public static AttributeMap defaultConfig(DefaultsMode mode) { + return DEFAULT_CONFIG_BY_MODE.getOrDefault(mode, AttributeMap.empty()); + } + + /** + * Return the default config options for a given defaults mode + */ + public static AttributeMap defaultHttpConfig(DefaultsMode mode) { + return DEFAULT_HTTP_CONFIG_BY_MODE.getOrDefault(mode, AttributeMap.empty()); + } +} diff --git a/codegen-lite/src/test/resources/software/amazon/awssdk/codegen/lite/defaultsmode/defaults-mode.java b/codegen-lite/src/test/resources/software/amazon/awssdk/codegen/lite/defaultsmode/defaults-mode.java new file mode 100644 index 000000000000..01c77d2f12d4 --- /dev/null +++ b/codegen-lite/src/test/resources/software/amazon/awssdk/codegen/lite/defaultsmode/defaults-mode.java @@ -0,0 +1,96 @@ +package software.amazon.awssdk.defaultsmode; + +import java.util.Map; +import software.amazon.awssdk.annotations.Generated; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.utils.Validate; +import software.amazon.awssdk.utils.internal.EnumUtils; + +/** + * A defaults mode determines how certain default configuration options are resolved in the SDK. Based on the provided + * mode, the SDK will vend sensible default values tailored to the mode for the following settings: + *
    + *
  • retryMode: PLACEHOLDER
  • + *
  • s3UsEast1RegionalEndpoints: PLACEHOLDER
  • + *
  • connectTimeoutInMillis: PLACEHOLDER
  • + *
  • tlsNegotiationTimeoutInMillis: PLACEHOLDER
  • + *
+ *

+ * All options above can be configured by users, and the overridden value will take precedence. + *

+ * Note: for any mode other than {@link #LEGACY}, the vended default values might change as best practices may + * evolve. As a result, it is encouraged to perform testing when upgrading the SDK if you are using a mode other than + * {@link #LEGACY} + *

+ * While the {@link #LEGACY} defaults mode is specific to Java, other modes are standardized across all of the AWS SDKs + *

+ *

+ * The defaults mode can be configured: + *

    + *
  1. Directly on a client via {@code AwsClientBuilder.Builder#defaultsMode(DefaultsMode)}.
  2. + *
  3. On a configuration profile via the "defaults_mode" profile file property.
  4. + *
  5. Globally via the "aws.defaultsMode" system property.
  6. + *
  7. Globally via the "AWS_DEFAULTS_MODE" environment variable.
  8. + *
+ */ +@SdkPublicApi +@Generated("software.amazon.awssdk:codegen") +public enum DefaultsMode { + /** + * PLACEHOLDER + */ + LEGACY("legacy"), + + /** + * PLACEHOLDER + */ + STANDARD("standard"), + + /** + * PLACEHOLDER + */ + MOBILE("mobile"), + + /** + * PLACEHOLDER + */ + CROSS_REGION("cross-region"), + + /** + * PLACEHOLDER + */ + IN_REGION("in-region"), + + /** + * PLACEHOLDER + */ + AUTO("auto"); + + private static final Map VALUE_MAP = EnumUtils.uniqueIndex(DefaultsMode.class, DefaultsMode::toString); + + private final String value; + + private DefaultsMode(String value) { + this.value = value; + } + + /** + * Use this in place of valueOf to convert the raw string returned by the service into the enum value. + * + * @param value + * real value + * @return DefaultsMode corresponding to the value + */ + public static DefaultsMode fromValue(String value) { + Validate.paramNotNull(value, "value"); + if (!VALUE_MAP.containsKey(value)) { + throw new IllegalArgumentException("The provided value is not a valid defaults mode " + value); + } + return VALUE_MAP.get(value); + } + + @Override + public String toString() { + return String.valueOf(value); + } +} diff --git a/codegen-lite/src/test/resources/software/amazon/awssdk/codegen/lite/test-sdk-default-configuration.json b/codegen-lite/src/test/resources/software/amazon/awssdk/codegen/lite/test-sdk-default-configuration.json new file mode 100644 index 000000000000..5c9f0fab48b7 --- /dev/null +++ b/codegen-lite/src/test/resources/software/amazon/awssdk/codegen/lite/test-sdk-default-configuration.json @@ -0,0 +1,64 @@ +{ + "version": 1, + "base": { + "retryMode": "standard", + "stsRegionalEndpoints": "regional", + "s3UsEast1RegionalEndpoints": "regional", + "connectTimeoutInMillis": 1000, + "tlsNegotiationTimeoutInMillis": 1000 + }, + "modes": { + "standard": { + "connectTimeoutInMillis": { + "multiply":2 + }, + "tlsNegotiationTimeoutInMillis": { + "multiply":2 + } + }, + "in-region": { + "connectTimeoutInMillis": { + "multiply": 1 + }, + "tlsNegotiationTimeoutInMillis": { + "multiply": 1 + } + }, + "cross-region": { + "connectTimeoutInMillis": { + "multiply": 2.8 + }, + "tlsNegotiationTimeoutInMillis": { + "multiply": 2.8 + } + }, + "mobile": { + "connectTimeoutInMillis": { + "override": 10000 + }, + "tlsNegotiationTimeoutInMillis": { + "add": 10000 + }, + "retryMode": { + "override": "adaptive" + } + } + }, + "documentation": { + "modes": { + "standard": "PLACEHOLDER", + "in-region": "PLACEHOLDER", + "cross-region": "PLACEHOLDER", + "mobile": "PLACEHOLDER", + "auto": "PLACEHOLDER", + "legacy": "PLACEHOLDER" + }, + "configuration": { + "retryMode": "PLACEHOLDER", + "stsRegionalEndpoints": "PLACEHOLDER", + "s3UsEast1RegionalEndpoints": "PLACEHOLDER", + "connectTimeoutInMillis": "PLACEHOLDER", + "tlsNegotiationTimeoutInMillis": "PLACEHOLDER" + } + } +} \ No newline at end of file diff --git a/core/aws-core/pom.xml b/core/aws-core/pom.xml index 7561698efd00..f597d3e1cb29 100644 --- a/core/aws-core/pom.xml +++ b/core/aws-core/pom.xml @@ -169,6 +169,19 @@ + + software.amazon.awssdk + codegen-lite-maven-plugin + ${awsjavasdk.version} + + + generate-sources + + generate-defaults-mode + + + + diff --git a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/builder/AwsClientBuilder.java b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/builder/AwsClientBuilder.java index 08c1175b5dd9..27744a58a572 100644 --- a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/builder/AwsClientBuilder.java +++ b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/builder/AwsClientBuilder.java @@ -17,6 +17,7 @@ import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.awscore.defaultsmode.DefaultsMode; import software.amazon.awssdk.core.client.builder.SdkClientBuilder; import software.amazon.awssdk.regions.Region; @@ -66,6 +67,26 @@ public interface AwsClientBuilder + * If this is not specified, the SDK will attempt to identify the defaults mode automatically using the following logic: + *
    + *
  1. Check the "defaults_mode" profile file property.
  2. + *
  3. Check "aws.defaultsMode" system property.
  4. + *
  5. Check the "AWS_DEFAULTS_MODE" environment variable.
  6. + *
+ * + * @param defaultsMode the defaultsMode to use + * @return This object for method chaining. + * @see DefaultsMode + */ + default BuilderT defaultsMode(DefaultsMode defaultsMode) { + throw new UnsupportedOperationException(); + } + /** * Configure whether the SDK should use the AWS dualstack endpoint. * diff --git a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilder.java b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilder.java index 2857aab0c2a9..b522b5f8d678 100644 --- a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilder.java +++ b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilder.java @@ -15,6 +15,8 @@ package software.amazon.awssdk.awscore.client.builder; +import static software.amazon.awssdk.awscore.client.config.AwsClientOption.DEFAULTS_MODE; + import java.net.URI; import java.util.Arrays; import java.util.List; @@ -25,11 +27,15 @@ import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; import software.amazon.awssdk.awscore.client.config.AwsAdvancedClientOption; import software.amazon.awssdk.awscore.client.config.AwsClientOption; +import software.amazon.awssdk.awscore.defaultsmode.DefaultsMode; import software.amazon.awssdk.awscore.endpoint.DefaultServiceEndpointBuilder; import software.amazon.awssdk.awscore.endpoint.DualstackEnabledProvider; import software.amazon.awssdk.awscore.endpoint.FipsEnabledProvider; import software.amazon.awssdk.awscore.eventstream.EventStreamInitialRequestInterceptor; import software.amazon.awssdk.awscore.interceptor.HelpfulUnknownHostExceptionInterceptor; +import software.amazon.awssdk.awscore.internal.defaultsmode.AutoDefaultsModeDiscovery; +import software.amazon.awssdk.awscore.internal.defaultsmode.DefaultsModeConfiguration; +import software.amazon.awssdk.awscore.internal.defaultsmode.DefaultsModeResolver; import software.amazon.awssdk.awscore.retry.AwsRetryPolicy; import software.amazon.awssdk.core.client.builder.SdkDefaultClientBuilder; import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption; @@ -43,9 +49,11 @@ import software.amazon.awssdk.profiles.ProfileFile; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.regions.ServiceMetadata; +import software.amazon.awssdk.regions.ServiceMetadataAdvancedOption; import software.amazon.awssdk.regions.providers.DefaultAwsRegionProviderChain; import software.amazon.awssdk.utils.AttributeMap; import software.amazon.awssdk.utils.CollectionUtils; +import software.amazon.awssdk.utils.Logger; /** * An SDK-internal implementation of the methods in {@link AwsClientBuilder}, {@link AwsAsyncClientBuilder} and @@ -69,16 +77,21 @@ public abstract class AwsDefaultClientBuilder, ClientT> extends SdkDefaultClientBuilder implements AwsClientBuilder { + private static final Logger log = Logger.loggerFor(AwsClientBuilder.class); private static final String DEFAULT_ENDPOINT_PROTOCOL = "https"; + private final AutoDefaultsModeDiscovery autoDefaultsModeDiscovery; protected AwsDefaultClientBuilder() { super(); + autoDefaultsModeDiscovery = new AutoDefaultsModeDiscovery(); } @SdkTestInternalApi AwsDefaultClientBuilder(SdkHttpClient.Builder defaultHttpClientBuilder, - SdkAsyncHttpClient.Builder defaultAsyncHttpClientFactory) { + SdkAsyncHttpClient.Builder defaultAsyncHttpClientFactory, + AutoDefaultsModeDiscovery autoDefaultsModeDiscovery) { super(defaultHttpClientBuilder, defaultAsyncHttpClientFactory); + this.autoDefaultsModeDiscovery = autoDefaultsModeDiscovery; } /** @@ -105,6 +118,19 @@ protected final AttributeMap childHttpConfig() { return serviceHttpConfig(); } + /** + * Return HTTP related defaults with the following chain of priorities. + *
    + *
  1. Service-Specific Defaults
  2. + *
  3. Defaults vended by {@link DefaultsMode}
  4. + *
+ */ + @Override + protected final AttributeMap childHttpConfig(SdkClientConfiguration configuration) { + AttributeMap attributeMap = serviceHttpConfig(); + return mergeSmartHttpDefaults(configuration, attributeMap); + } + /** * Optionally overridden by child classes to define service-specific HTTP configuration defaults. */ @@ -116,10 +142,10 @@ protected AttributeMap serviceHttpConfig() { protected final SdkClientConfiguration mergeChildDefaults(SdkClientConfiguration configuration) { SdkClientConfiguration config = mergeServiceDefaults(configuration); config = config.merge(c -> c.option(AwsAdvancedClientOption.ENABLE_DEFAULT_REGION_DETECTION, true) - .option(SdkAdvancedClientOption.DISABLE_HOST_PREFIX_INJECTION, false) - .option(AwsClientOption.SERVICE_SIGNING_NAME, signingName()) - .option(SdkClientOption.SERVICE_NAME, serviceName()) - .option(AwsClientOption.ENDPOINT_PREFIX, serviceEndpointPrefix())); + .option(SdkAdvancedClientOption.DISABLE_HOST_PREFIX_INJECTION, false) + .option(AwsClientOption.SERVICE_SIGNING_NAME, signingName()) + .option(SdkClientOption.SERVICE_NAME, serviceName()) + .option(AwsClientOption.ENDPOINT_PREFIX, serviceEndpointPrefix())); return mergeInternalDefaults(config); } @@ -137,6 +163,13 @@ protected SdkClientConfiguration mergeInternalDefaults(SdkClientConfiguration co return configuration; } + /** + * Return a client configuration object, populated with the following chain of priorities. + *
    + *
  1. Defaults vended from {@link DefaultsMode}
  2. + *
  3. AWS Global Defaults
  4. + *
+ */ @Override protected final SdkClientConfiguration finalizeChildConfiguration(SdkClientConfiguration configuration) { configuration = finalizeServiceConfiguration(configuration); @@ -148,6 +181,8 @@ protected final SdkClientConfiguration finalizeChildConfiguration(SdkClientConfi .option(AwsClientOption.FIPS_ENDPOINT_ENABLED, resolveFipsEndpointEnabled(configuration)) .build(); + configuration = mergeSmartDefaults(configuration); + return configuration.toBuilder() .option(AwsClientOption.CREDENTIALS_PROVIDER, resolveCredentials(configuration)) .option(SdkClientOption.ENDPOINT, resolveEndpoint(configuration)) @@ -157,6 +192,19 @@ protected final SdkClientConfiguration finalizeChildConfiguration(SdkClientConfi .build(); } + private SdkClientConfiguration mergeSmartDefaults(SdkClientConfiguration configuration) { + DefaultsMode defaultsMode = resolveDefaultsMode(configuration); + AttributeMap defaultConfig = DefaultsModeConfiguration.defaultConfig(defaultsMode); + return configuration.toBuilder() + .option(DEFAULTS_MODE, defaultsMode) + .build() + .merge(c -> c.option(SdkClientOption.DEFAULT_RETRY_MODE, + defaultConfig.get(SdkClientOption.DEFAULT_RETRY_MODE)) + .option(ServiceMetadataAdvancedOption.DEFAULT_S3_US_EAST_1_REGIONAL_ENDPOINT, + defaultConfig.get( + ServiceMetadataAdvancedOption.DEFAULT_S3_US_EAST_1_REGIONAL_ENDPOINT))); + } + /** * Optionally overridden by child classes to derive service-specific configuration from the default-applied configuration. */ @@ -164,6 +212,14 @@ protected SdkClientConfiguration finalizeServiceConfiguration(SdkClientConfigura return configuration; } + /** + * Merged the HTTP defaults specified for each {@link DefaultsMode} + */ + private AttributeMap mergeSmartHttpDefaults(SdkClientConfiguration configuration, AttributeMap attributeMap) { + DefaultsMode defaultsMode = configuration.option(DEFAULTS_MODE); + return attributeMap.merge(DefaultsModeConfiguration.defaultHttpConfig(defaultsMode)); + } + /** * Resolve the signing region from the default-applied configuration. */ @@ -185,6 +241,8 @@ private URI endpointFromConfig(SdkClientConfiguration config) { .withRegion(config.option(AwsClientOption.AWS_REGION)) .withProfileFile(() -> config.option(SdkClientOption.PROFILE_FILE)) .withProfileName(config.option(SdkClientOption.PROFILE_NAME)) + .putAdvancedOption(ServiceMetadataAdvancedOption.DEFAULT_S3_US_EAST_1_REGIONAL_ENDPOINT, + config.option(ServiceMetadataAdvancedOption.DEFAULT_S3_US_EAST_1_REGIONAL_ENDPOINT)) .withDualstackEnabled(config.option(AwsClientOption.DUALSTACK_ENDPOINT_ENABLED)) .withFipsEnabled(config.option(AwsClientOption.FIPS_ENDPOINT_ENABLED)) .getServiceEndpoint(); @@ -217,6 +275,25 @@ private Region regionFromDefaultProvider(SdkClientConfiguration config) { .getRegion(); } + private DefaultsMode resolveDefaultsMode(SdkClientConfiguration config) { + DefaultsMode defaultsMode = + config.option(AwsClientOption.DEFAULTS_MODE) != null ? + config.option(AwsClientOption.DEFAULTS_MODE) : + DefaultsModeResolver.create() + .profileFile(() -> config.option(SdkClientOption.PROFILE_FILE)) + .profileName(config.option(SdkClientOption.PROFILE_NAME)) + .resolve(); + + if (defaultsMode == DefaultsMode.AUTO) { + defaultsMode = autoDefaultsModeDiscovery.discover(config.option(AwsClientOption.AWS_REGION)); + DefaultsMode finalDefaultsMode = defaultsMode; + log.debug(() -> String.format("Resolved %s client's AUTO configuration mode to %s", serviceName(), + finalDefaultsMode)); + } + + return defaultsMode; + } + /** * Resolve whether a dualstack endpoint should be used for this client. */ @@ -344,4 +421,14 @@ private List awsInterceptors() { return Arrays.asList(new HelpfulUnknownHostExceptionInterceptor(), new EventStreamInitialRequestInterceptor()); } + + @Override + public final BuilderT defaultsMode(DefaultsMode defaultsMode) { + clientConfiguration.option(DEFAULTS_MODE, defaultsMode); + return thisBuilder(); + } + + public final void setDefaultsMode(DefaultsMode defaultsMode) { + defaultsMode(defaultsMode); + } } diff --git a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/config/AwsClientOption.java b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/config/AwsClientOption.java index a8c6cdb22b70..2d1b665916c2 100644 --- a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/config/AwsClientOption.java +++ b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/config/AwsClientOption.java @@ -18,6 +18,7 @@ import software.amazon.awssdk.annotations.SdkProtectedApi; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; import software.amazon.awssdk.awscore.client.builder.AwsClientBuilder; +import software.amazon.awssdk.awscore.defaultsmode.DefaultsMode; import software.amazon.awssdk.core.client.config.ClientOption; import software.amazon.awssdk.regions.Region; @@ -65,6 +66,11 @@ public final class AwsClientOption extends ClientOption { */ public static final AwsClientOption ENDPOINT_PREFIX = new AwsClientOption<>(String.class); + /** + * Option to specify the {@link DefaultsMode} + */ + public static final AwsClientOption DEFAULTS_MODE = new AwsClientOption<>(DefaultsMode.class); + private AwsClientOption(Class valueClass) { super(valueClass); } diff --git a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/endpoint/DefaultServiceEndpointBuilder.java b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/endpoint/DefaultServiceEndpointBuilder.java index fc1fc751a4e7..a0113969a810 100644 --- a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/endpoint/DefaultServiceEndpointBuilder.java +++ b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/endpoint/DefaultServiceEndpointBuilder.java @@ -18,7 +18,9 @@ import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.function.Supplier; import software.amazon.awssdk.annotations.NotThreadSafe; import software.amazon.awssdk.annotations.SdkProtectedApi; @@ -29,6 +31,7 @@ import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.regions.ServiceEndpointKey; import software.amazon.awssdk.regions.ServiceMetadata; +import software.amazon.awssdk.regions.ServiceMetadataAdvancedOption; import software.amazon.awssdk.utils.Lazy; import software.amazon.awssdk.utils.Validate; @@ -46,6 +49,7 @@ public final class DefaultServiceEndpointBuilder { private Region region; private Supplier profileFile; private String profileName; + private final Map, Object> advancedOptions = new HashMap<>(); private Boolean dualstackEnabled; private Boolean fipsEnabled; @@ -77,6 +81,11 @@ public DefaultServiceEndpointBuilder withProfileName(String profileName) { return this; } + public DefaultServiceEndpointBuilder putAdvancedOption(ServiceMetadataAdvancedOption option, T value) { + advancedOptions.put(option, value); + return this; + } + public DefaultServiceEndpointBuilder withDualstackEnabled(Boolean dualstackEnabled) { this.dualstackEnabled = dualstackEnabled; return this; @@ -126,7 +135,8 @@ public URI getServiceEndpoint() { ServiceMetadata serviceMetadata = ServiceMetadata.of(serviceName) .reconfigure(c -> c.profileFile(profileFile) - .profileName(profileName)); + .profileName(profileName) + .advancedOptions(advancedOptions)); URI endpoint = addProtocolToServiceEndpoint(serviceMetadata.endpointFor(ServiceEndpointKey.builder() .region(region) .tags(endpointTags) diff --git a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/internal/defaultsmode/AutoDefaultsModeDiscovery.java b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/internal/defaultsmode/AutoDefaultsModeDiscovery.java new file mode 100644 index 000000000000..5adc6ab7a092 --- /dev/null +++ b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/internal/defaultsmode/AutoDefaultsModeDiscovery.java @@ -0,0 +1,128 @@ +/* + * 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.awscore.internal.defaultsmode; + +import java.util.Optional; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.awscore.defaultsmode.DefaultsMode; +import software.amazon.awssdk.core.SdkSystemSetting; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.regions.internal.util.EC2MetadataUtils; +import software.amazon.awssdk.utils.JavaSystemSetting; +import software.amazon.awssdk.utils.OptionalUtils; +import software.amazon.awssdk.utils.SystemSetting; +import software.amazon.awssdk.utils.internal.SystemSettingUtils; + +/** + * 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 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"; + + /** + * Discovers the defaultMode using the following workflow: + * + * 1. Check if it's on mobile + * 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. + * 3. If we couldn't figure out the region from environment variables. Check IMDSv2. This step might take up to 1 second + * (default connect timeout) + * 4. Finally, use fallback mode + */ + public DefaultsMode discover(Region regionResolvedFromSdkClient) { + + if (isMobile()) { + return DefaultsMode.MOBILE; + } + + if (isAwsExecutionEnvironment()) { + Optional regionStr = regionFromAwsExecutionEnvironment(); + + if (regionStr.isPresent()) { + return compareRegion(regionStr.get(), regionResolvedFromSdkClient); + } + } + + Optional regionFromEc2 = queryImdsV2(); + if (regionFromEc2.isPresent()) { + return compareRegion(regionFromEc2.get(), regionResolvedFromSdkClient); + } + + return FALLBACK_DEFAULTS_MODE; + } + + private static DefaultsMode compareRegion(String region, Region clientRegion) { + if (region.equalsIgnoreCase(clientRegion.id())) { + return DefaultsMode.IN_REGION; + } + + return DefaultsMode.CROSS_REGION; + } + + private static Optional queryImdsV2() { + try { + String ec2InstanceRegion = EC2MetadataUtils.fetchData(EC2_METADATA_REGION_PATH, false, 1); + // ec2InstanceRegion could be null + return Optional.ofNullable(ec2InstanceRegion); + } catch (Exception exception) { + return Optional.empty(); + } + } + + /** + * Check to see if the application is running on a mobile device by verifying the Java + * vendor system property. Currently only checks for Android. While it's technically possible to + * use Java with iOS, it's not a common use-case. + *

+ * https://developer.android.com/reference/java/lang/System#getProperties() + */ + private static boolean isMobile() { + return JavaSystemSetting.JAVA_VENDOR.getStringValue() + .filter(o -> o.equals(ANDROID_JAVA_VENDOR)) + .isPresent(); + } + + private static boolean isAwsExecutionEnvironment() { + return SdkSystemSetting.AWS_EXECUTION_ENV.getStringValue().isPresent(); + } + + private static Optional regionFromAwsExecutionEnvironment() { + Optional regionFromRegionEnvVar = SdkSystemSetting.AWS_REGION.getStringValue(); + return OptionalUtils.firstPresent(regionFromRegionEnvVar, + () -> SystemSettingUtils.resolveEnvironmentVariable(new DefaultRegionEnvVar())); + } + + private static final class DefaultRegionEnvVar implements SystemSetting { + @Override + public String property() { + return null; + } + + @Override + public String environmentVariable() { + return AWS_DEFAULT_REGION_ENV_VAR; + } + + @Override + public String defaultValue() { + return null; + } + } +} diff --git a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/internal/defaultsmode/DefaultsModeResolver.java b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/internal/defaultsmode/DefaultsModeResolver.java new file mode 100644 index 000000000000..c20ceebd04f9 --- /dev/null +++ b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/internal/defaultsmode/DefaultsModeResolver.java @@ -0,0 +1,99 @@ +/* + * 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.awscore.internal.defaultsmode; + +import java.util.Locale; +import java.util.Optional; +import java.util.function.Supplier; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.awscore.defaultsmode.DefaultsMode; +import software.amazon.awssdk.core.SdkSystemSetting; +import software.amazon.awssdk.core.retry.RetryMode; +import software.amazon.awssdk.profiles.ProfileFile; +import software.amazon.awssdk.profiles.ProfileFileSystemSetting; +import software.amazon.awssdk.profiles.ProfileProperty; +import software.amazon.awssdk.utils.OptionalUtils; + +/** + * Allows customizing the variables used during determination of a {@link DefaultsMode}. Created via {@link #create()}. + */ +@SdkInternalApi +public final class DefaultsModeResolver { + + private static final DefaultsMode SDK_DEFAULT_DEFAULTS_MODE = DefaultsMode.LEGACY; + private Supplier profileFile; + private String profileName; + private DefaultsMode mode; + + private DefaultsModeResolver() { + } + + public static DefaultsModeResolver create() { + return new DefaultsModeResolver(); + } + + /** + * Configure the profile file that should be used when determining the {@link RetryMode}. The supplier is only consulted + * if a higher-priority determinant (e.g. environment variables) does not find the setting. + */ + public DefaultsModeResolver profileFile(Supplier profileFile) { + this.profileFile = profileFile; + return this; + } + + /** + * Configure the profile file name should be used when determining the {@link RetryMode}. + */ + public DefaultsModeResolver profileName(String profileName) { + this.profileName = profileName; + return this; + } + + /** + * Configure the {@link DefaultsMode} that should be used if the mode is not specified anywhere else. + */ + public DefaultsModeResolver defaultMode(DefaultsMode mode) { + this.mode = mode; + return this; + } + + /** + * Resolve which defaults mode should be used, based on the configured values. + */ + public DefaultsMode resolve() { + return OptionalUtils.firstPresent(DefaultsModeResolver.fromSystemSettings(), () -> fromProfileFile(profileFile, + profileName)) + .orElseGet(this::fromDefaultMode); + } + + private static Optional fromSystemSettings() { + return SdkSystemSetting.AWS_DEFAULTS_MODE.getStringValue() + .map(value -> DefaultsMode.fromValue(value.toLowerCase(Locale.US))); + } + + private static Optional fromProfileFile(Supplier profileFile, String profileName) { + profileFile = profileFile != null ? profileFile : ProfileFile::defaultProfileFile; + profileName = profileName != null ? profileName : ProfileFileSystemSetting.AWS_PROFILE.getStringValueOrThrow(); + return profileFile.get() + .profile(profileName) + .flatMap(p -> p.property(ProfileProperty.DEFAULTS_MODE)) + .map(value -> DefaultsMode.fromValue(value.toLowerCase(Locale.US))); + } + + private DefaultsMode fromDefaultMode() { + return mode != null ? mode : SDK_DEFAULT_DEFAULTS_MODE; + } +} diff --git a/core/aws-core/src/main/resources/software/amazon/awssdk/awscore/internal/defaults/sdk-default-configuration.json b/core/aws-core/src/main/resources/software/amazon/awssdk/awscore/internal/defaults/sdk-default-configuration.json new file mode 100644 index 000000000000..3db13b26cc5e --- /dev/null +++ b/core/aws-core/src/main/resources/software/amazon/awssdk/awscore/internal/defaults/sdk-default-configuration.json @@ -0,0 +1,55 @@ +{ + "version": 1, + "base": { + "retryMode": "standard", + "stsRegionalEndpoints": "regional", + "s3UsEast1RegionalEndpoints": "regional", + "connectTimeoutInMillis": 1100, + "tlsNegotiationTimeoutInMillis": 1100 + }, + "modes": { + "standard": { + "connectTimeoutInMillis": { + "override": 3100 + }, + "tlsNegotiationTimeoutInMillis": { + "override": 3100 + } + }, + "in-region": { + }, + "cross-region": { + "connectTimeoutInMillis": { + "override": 3100 + }, + "tlsNegotiationTimeoutInMillis": { + "override": 3100 + } + }, + "mobile": { + "connectTimeoutInMillis": { + "override": 30000 + }, + "tlsNegotiationTimeoutInMillis": { + "override": 30000 + } + } + }, + "documentation": { + "modes": { + "standard": "

The STANDARD mode provides the latest recommended default values that should be safe to run in most scenarios

Note that the default values vended from this mode might change as best practices may evolve. As a result, it is encouraged to perform tests when upgrading the SDK

", + "in-region": "

The IN_REGION mode builds on the standard mode and includes optimization tailored for applications which call AWS services from within the same AWS region

Note that the default values vended from this mode might change as best practices may evolve. As a result, it is encouraged to perform tests when upgrading the SDK

", + "cross-region": "

The CROSS_REGION mode builds on the standard mode and includes optimization tailored for applications which call AWS services in a different region

Note that the default values vended from this mode might change as best practices may evolve. As a result, it is encouraged to perform tests when upgrading the SDK

", + "mobile": "

The MOBILE mode builds on the standard mode and includes optimization tailored for mobile applications

Note that the default values vended from this mode might change as best practices may evolve. As a result, it is encouraged to perform tests when upgrading the SDK

", + "auto": "

The AUTO mode is an experimental mode that builds on the standard mode. The SDK will attempt to discover the execution environment to determine the appropriate settings automatically.

Note that the auto detection is heuristics-based and does not guarantee 100% accuracy. STANDARD mode will be used if the execution environment cannot be determined. The auto detection might query EC2 Instance Metadata service, which might introduce latency. Therefore we recommend choosing an explicit defaults_mode instead if startup latency is critical to your application

", + "legacy": "

The LEGACY mode provides default settings that vary per SDK and were used prior to establishment of defaults_mode

" + }, + "configuration": { + "retryMode": "

A retry mode specifies how the SDK attempts retries. See Retry Mode

", + "stsRegionalEndpoints": "

Specifies how the SDK determines the AWS service endpoint that it uses to talk to the AWS Security Token Service (AWS STS). See Setting STS Regional endpoints

", + "s3UsEast1RegionalEndpoints": "

Specifies how the SDK determines the AWS service endpoint that it uses to talk to the Amazon S3 for the us-east-1 region

", + "connectTimeoutInMillis": "

The amount of time after making an initial connection attempt on a socket, where if the client does not receive a completion of the connect handshake, the client gives up and fails the operation

", + "tlsNegotiationTimeoutInMillis": "

The maximum amount of time that a TLS handshake is allowed to take from the time the CLIENT HELLO message is sent to ethe time the client and server have fully negotiated ciphers and exchanged keys

" + } + } +} \ No newline at end of file diff --git a/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/builder/DefaultAwsClientBuilderTest.java b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/builder/DefaultAwsClientBuilderTest.java index c32600c5f014..227e406ceac4 100644 --- a/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/builder/DefaultAwsClientBuilderTest.java +++ b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/builder/DefaultAwsClientBuilderTest.java @@ -42,7 +42,8 @@ import org.mockito.runners.MockitoJUnitRunner; import software.amazon.awssdk.auth.credentials.AnonymousCredentialsProvider; import software.amazon.awssdk.auth.signer.Aws4Signer; -import software.amazon.awssdk.awscore.client.config.AwsClientOption; +import software.amazon.awssdk.awscore.defaultsmode.DefaultsMode; +import software.amazon.awssdk.awscore.internal.defaultsmode.AutoDefaultsModeDiscovery; import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; import software.amazon.awssdk.core.client.config.SdkClientConfiguration; import software.amazon.awssdk.core.client.config.SdkClientOption; @@ -76,10 +77,14 @@ public class DefaultAwsClientBuilderTest { @Mock private SdkAsyncHttpClient.Builder defaultAsyncHttpClientFactory; + @Mock + private AutoDefaultsModeDiscovery autoModeDiscovery; + @Before public void setup() { when(defaultHttpClientBuilder.buildWithDefaults(any())).thenReturn(mock(SdkHttpClient.class)); when(defaultAsyncHttpClientFactory.buildWithDefaults(any())).thenReturn(mock(SdkAsyncHttpClient.class)); + when(autoModeDiscovery.discover(any())).thenReturn(DefaultsMode.IN_REGION); } @Test @@ -232,7 +237,7 @@ private class TestClientBuilder extends AwsDefaultClientBuilder { public TestClientBuilder() { - super(defaultHttpClientBuilder, null); + super(defaultHttpClientBuilder, null, autoModeDiscovery); } @Override @@ -273,7 +278,7 @@ private class TestAsyncClientBuilder extends AwsDefaultClientBuilder { public TestAsyncClientBuilder() { - super(defaultHttpClientBuilder, defaultAsyncHttpClientFactory); + super(defaultHttpClientBuilder, defaultAsyncHttpClientFactory, autoModeDiscovery); } @Override diff --git a/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/builder/DefaultsModeTest.java b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/builder/DefaultsModeTest.java new file mode 100644 index 000000000000..9565b93bb14d --- /dev/null +++ b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/builder/DefaultsModeTest.java @@ -0,0 +1,249 @@ +/* + * 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.awscore.client.builder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static software.amazon.awssdk.awscore.client.config.AwsAdvancedClientOption.ENABLE_DEFAULT_REGION_DETECTION; +import static software.amazon.awssdk.awscore.client.config.AwsClientOption.DEFAULTS_MODE; +import static software.amazon.awssdk.core.client.config.SdkClientOption.DEFAULT_RETRY_MODE; +import static software.amazon.awssdk.core.client.config.SdkClientOption.RETRY_POLICY; +import static software.amazon.awssdk.regions.ServiceMetadataAdvancedOption.DEFAULT_S3_US_EAST_1_REGIONAL_ENDPOINT; + +import java.time.Duration; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import software.amazon.awssdk.auth.credentials.AnonymousCredentialsProvider; +import software.amazon.awssdk.awscore.defaultsmode.DefaultsMode; +import software.amazon.awssdk.awscore.internal.defaultsmode.AutoDefaultsModeDiscovery; +import software.amazon.awssdk.awscore.internal.defaultsmode.DefaultsModeConfiguration; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.core.client.config.SdkClientConfiguration; +import software.amazon.awssdk.core.retry.RetryMode; +import software.amazon.awssdk.http.SdkHttpClient; +import software.amazon.awssdk.http.SdkHttpConfigurationOption; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.utils.AttributeMap; + +@RunWith(MockitoJUnitRunner.class) +public class DefaultsModeTest { + + private static final AttributeMap SERVICE_DEFAULTS = AttributeMap + .builder() + .put(SdkHttpConfigurationOption.READ_TIMEOUT, Duration.ofSeconds(10)) + .build(); + + private static final String ENDPOINT_PREFIX = "test"; + private static final String SIGNING_NAME = "test"; + private static final String SERVICE_NAME = "test"; + + @Mock + private SdkHttpClient.Builder defaultHttpClientBuilder; + + @Mock + private SdkAsyncHttpClient.Builder defaultAsyncHttpClientBuilder; + + @Mock + private AutoDefaultsModeDiscovery autoModeDiscovery; + + @Test + public void defaultClient_shouldUseLegacyModeWithExistingDefaults() { + TestClient client = testClientBuilder() + .region(Region.US_WEST_2) + .httpClientBuilder((SdkHttpClient.Builder) serviceDefaults -> { + assertThat(serviceDefaults).isEqualTo(SERVICE_DEFAULTS); + return mock(SdkHttpClient.class); + }) + .build(); + + assertThat(client.clientConfiguration.option(DEFAULTS_MODE)).isEqualTo(DefaultsMode.LEGACY); + assertThat(client.clientConfiguration.option(RETRY_POLICY).retryMode()).isEqualTo(RetryMode.defaultRetryMode()); + assertThat(client.clientConfiguration.option(DEFAULT_S3_US_EAST_1_REGIONAL_ENDPOINT)).isNull(); + } + + @Test + public void nonLegacyDefaultsMode_shouldApplySdkDefaultsAndHttpDefaults() { + DefaultsMode targetMode = DefaultsMode.IN_REGION; + + TestClient client = + testClientBuilder().region(Region.US_WEST_1) + .defaultsMode(targetMode) + .httpClientBuilder((SdkHttpClient.Builder) serviceDefaults -> { + AttributeMap defaultHttpConfig = DefaultsModeConfiguration.defaultHttpConfig(targetMode); + AttributeMap mergedDefaults = SERVICE_DEFAULTS.merge(defaultHttpConfig); + assertThat(serviceDefaults).isEqualTo(mergedDefaults); + return mock(SdkHttpClient.class); + }).build(); + + assertThat(client.clientConfiguration.option(DEFAULTS_MODE)).isEqualTo(targetMode); + + AttributeMap attributes = DefaultsModeConfiguration.defaultConfig(targetMode); + + assertThat(client.clientConfiguration.option(RETRY_POLICY).retryMode()).isEqualTo(attributes.get(DEFAULT_RETRY_MODE)); + assertThat(client.clientConfiguration.option(DEFAULT_S3_US_EAST_1_REGIONAL_ENDPOINT)).isEqualTo("regional"); + } + + @Test + public void nonLegacyDefaultsModeAsyncClient_shouldApplySdkDefaultsAndHttpDefaults() { + DefaultsMode targetMode = DefaultsMode.IN_REGION; + + TestAsyncClient client = + testAsyncClientBuilder().region(Region.US_WEST_1) + .defaultsMode(targetMode) + .httpClientBuilder((SdkHttpClient.Builder) serviceDefaults -> { + AttributeMap defaultHttpConfig = DefaultsModeConfiguration.defaultHttpConfig(targetMode); + AttributeMap mergedDefaults = SERVICE_DEFAULTS.merge(defaultHttpConfig); + assertThat(serviceDefaults).isEqualTo(mergedDefaults); + return mock(SdkHttpClient.class); + }).build(); + + assertThat(client.clientConfiguration.option(DEFAULTS_MODE)).isEqualTo(targetMode); + + AttributeMap attributes = DefaultsModeConfiguration.defaultConfig(targetMode); + + assertThat(client.clientConfiguration.option(RETRY_POLICY).retryMode()).isEqualTo(attributes.get(DEFAULT_RETRY_MODE)); + } + + @Test + public void clientOverrideRetryMode_shouldTakePrecedence() { + TestClient client = + testClientBuilder().region(Region.US_WEST_1) + .defaultsMode(DefaultsMode.IN_REGION) + .overrideConfiguration(o -> o.retryPolicy(RetryMode.LEGACY)) + .build(); + assertThat(client.clientConfiguration.option(DEFAULTS_MODE)).isEqualTo(DefaultsMode.IN_REGION); + assertThat(client.clientConfiguration.option(RETRY_POLICY).retryMode()).isEqualTo(RetryMode.LEGACY); + } + + @Test + public void autoMode_shouldResolveDefaultsMode() { + DefaultsMode expectedMode = DefaultsMode.IN_REGION; + when(autoModeDiscovery.discover(any(Region.class))).thenReturn(expectedMode); + TestClient client = + testClientBuilder().region(Region.US_WEST_1) + .defaultsMode(DefaultsMode.AUTO) + .build(); + + assertThat(client.clientConfiguration.option(DEFAULTS_MODE)).isEqualTo(expectedMode); + } + + private static class TestClient { + private final SdkClientConfiguration clientConfiguration; + + public TestClient(SdkClientConfiguration clientConfiguration) { + this.clientConfiguration = clientConfiguration; + } + } + + private AwsClientBuilder testClientBuilder() { + ClientOverrideConfiguration overrideConfig = + ClientOverrideConfiguration.builder() + .putAdvancedOption(ENABLE_DEFAULT_REGION_DETECTION, false) + .build(); + + return new TestClientBuilder().credentialsProvider(AnonymousCredentialsProvider.create()) + .overrideConfiguration(overrideConfig); + } + + private AwsClientBuilder testAsyncClientBuilder() { + ClientOverrideConfiguration overrideConfig = + ClientOverrideConfiguration.builder() + .putAdvancedOption(ENABLE_DEFAULT_REGION_DETECTION, false) + .build(); + + return new TestAsyncClientBuilder().credentialsProvider(AnonymousCredentialsProvider.create()) + .overrideConfiguration(overrideConfig); + } + + private class TestClientBuilder extends AwsDefaultClientBuilder + implements AwsClientBuilder { + + public TestClientBuilder() { + super(defaultHttpClientBuilder, defaultAsyncHttpClientBuilder, autoModeDiscovery); + } + + @Override + protected TestClient buildClient() { + return new TestClient(super.syncClientConfiguration()); + } + + @Override + protected String serviceEndpointPrefix() { + return ENDPOINT_PREFIX; + } + + @Override + protected String signingName() { + return SIGNING_NAME; + } + + @Override + protected String serviceName() { + return SERVICE_NAME; + } + + @Override + protected AttributeMap serviceHttpConfig() { + return SERVICE_DEFAULTS; + } + } + + private class TestAsyncClientBuilder extends AwsDefaultClientBuilder + implements AwsClientBuilder { + + public TestAsyncClientBuilder() { + super(defaultHttpClientBuilder, defaultAsyncHttpClientBuilder, autoModeDiscovery); + } + + @Override + protected TestAsyncClient buildClient() { + return new TestAsyncClient(super.asyncClientConfiguration()); + } + + @Override + protected String serviceEndpointPrefix() { + return ENDPOINT_PREFIX; + } + + @Override + protected String signingName() { + return SIGNING_NAME; + } + + @Override + protected String serviceName() { + return SERVICE_NAME; + } + + @Override + protected AttributeMap serviceHttpConfig() { + return SERVICE_DEFAULTS; + } + } + + private static class TestAsyncClient { + private final SdkClientConfiguration clientConfiguration; + + private TestAsyncClient(SdkClientConfiguration clientConfiguration) { + this.clientConfiguration = clientConfiguration; + } + } +} diff --git a/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/endpoint/DefaultServiceEndpointBuilderTest.java b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/endpoint/DefaultServiceEndpointBuilderTest.java index 90fa1f40034d..38b65f87a653 100644 --- a/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/endpoint/DefaultServiceEndpointBuilderTest.java +++ b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/endpoint/DefaultServiceEndpointBuilderTest.java @@ -20,6 +20,7 @@ import org.junit.Test; import software.amazon.awssdk.awscore.endpoint.DefaultServiceEndpointBuilder; import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.regions.ServiceMetadataAdvancedOption; public class DefaultServiceEndpointBuilderTest { @@ -43,4 +44,12 @@ public void getServiceEndpoint_S3NonStandardRegion_HttpProtocol() throws Excepti .withRegion(Region.EU_CENTRAL_1); assertEquals("http://s3.eu-central-1.amazonaws.com", endpointBuilder.getServiceEndpoint().toString()); } + + @Test + public void getServiceEndpoint_regionalOption_shouldUseRegionalEndpoint() throws Exception { + DefaultServiceEndpointBuilder endpointBuilder = new DefaultServiceEndpointBuilder("s3", "http") + .withRegion(Region.US_EAST_1).putAdvancedOption(ServiceMetadataAdvancedOption.DEFAULT_S3_US_EAST_1_REGIONAL_ENDPOINT, + "regional"); + assertEquals("http://s3.us-east-1.amazonaws.com", endpointBuilder.getServiceEndpoint().toString()); + } } diff --git a/core/aws-core/src/test/java/software/amazon/awssdk/awscore/internal/defaultsmode/AutoDefaultsModeDiscoveryTest.java b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/internal/defaultsmode/AutoDefaultsModeDiscoveryTest.java new file mode 100644 index 000000000000..0170dfc39da4 --- /dev/null +++ b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/internal/defaultsmode/AutoDefaultsModeDiscoveryTest.java @@ -0,0 +1,231 @@ +/* + * 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.awscore.internal.defaultsmode; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.put; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static org.assertj.core.api.Assertions.assertThat; + +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import java.util.Arrays; +import java.util.Collection; +import java.util.concurrent.Callable; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import software.amazon.awssdk.awscore.defaultsmode.DefaultsMode; +import software.amazon.awssdk.core.SdkSystemSetting; +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; + +@RunWith(Parameterized.class) +public class AutoDefaultsModeDiscoveryTest { + private static final EnvironmentVariableHelper ENVIRONMENT_VARIABLE_HELPER = new EnvironmentVariableHelper(); + @Parameterized.Parameter + public TestData testData; + + @Parameterized.Parameters + public static Collection 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)) + .expectedResolvedMode(DefaultsMode.STANDARD), + }); + } + + @Rule + public WireMockRule wireMock = new WireMockRule(0); + + @Before + public void methodSetup() { + System.setProperty(SdkSystemSetting.AWS_EC2_METADATA_SERVICE_ENDPOINT.property(), + "http://localhost:" + wireMock.port()); + } + + @After + public void cleanUp() { + EC2MetadataUtils.clearCache(); + wireMock.resetAll(); + ENVIRONMENT_VARIABLE_HELPER.reset(); + System.clearProperty(JavaSystemSetting.JAVA_VENDOR.property()); + } + + @Test + public void differentCombinationOfConfigs_shouldResolveCorrectly() throws Exception { + if (testData.javaVendorProperty != null) { + System.setProperty(JavaSystemSetting.JAVA_VENDOR.property(), testData.javaVendorProperty); + } + + if (testData.awsExecutionEnvVar != null) { + ENVIRONMENT_VARIABLE_HELPER.set(SdkSystemSetting.AWS_EXECUTION_ENV.environmentVariable(), + testData.awsExecutionEnvVar); + } else { + ENVIRONMENT_VARIABLE_HELPER.remove(SdkSystemSetting.AWS_EXECUTION_ENV.environmentVariable()); + } + + if (testData.awsRegionEnvVar != null) { + ENVIRONMENT_VARIABLE_HELPER.set(SdkSystemSetting.AWS_REGION.environmentVariable(), testData.awsRegionEnvVar); + } else { + ENVIRONMENT_VARIABLE_HELPER.remove(SdkSystemSetting.AWS_REGION.environmentVariable()); + } + + if (testData.awsDefaultRegionEnvVar != null) { + ENVIRONMENT_VARIABLE_HELPER.set("AWS_DEFAULT_REGION", testData.awsDefaultRegionEnvVar); + } else { + ENVIRONMENT_VARIABLE_HELPER.remove("AWS_DEFAULT_REGION"); + } + + if (testData.ec2MetadataConfig != null) { + if (testData.ec2MetadataConfig.ec2MetadataDisabledEnvVar != null) { + ENVIRONMENT_VARIABLE_HELPER.set(SdkSystemSetting.AWS_EC2_METADATA_DISABLED.environmentVariable(), + testData.ec2MetadataConfig.ec2MetadataDisabledEnvVar); + } + + if (testData.ec2MetadataConfig.imdsAvailable) { + stubSuccessfulResponse(testData.ec2MetadataConfig.region); + } + } + + Callable result = () -> new AutoDefaultsModeDiscovery().discover(testData.clientRegion); + assertThat(result.call()).isEqualTo(testData.expectedResolvedMode); + } + + public void stubSuccessfulResponse(String region) { + stubFor(put("/latest/api/token") + .willReturn(aResponse().withStatus(200).withBody("token"))); + + stubFor(get("/latest/meta-data/placement/region") + .willReturn(aResponse().withStatus(200).withBody(region))); + } + + private static final class TestData { + private Region clientRegion; + private String javaVendorProperty; + private String awsExecutionEnvVar; + private String awsRegionEnvVar; + private String awsDefaultRegionEnvVar; + private Ec2MetadataConfig ec2MetadataConfig; + private DefaultsMode expectedResolvedMode; + + public TestData clientRegion(Region clientRegion) { + this.clientRegion = clientRegion; + return this; + } + + public TestData javaVendorProperty(String javaVendorProperty) { + this.javaVendorProperty = javaVendorProperty; + return this; + } + + public TestData awsExecutionEnvVar(String awsExecutionEnvVar) { + this.awsExecutionEnvVar = awsExecutionEnvVar; + return this; + } + + public TestData awsRegionEnvVar(String awsRegionEnvVar) { + this.awsRegionEnvVar = awsRegionEnvVar; + return this; + } + + public TestData awsDefaultRegionEnvVar(String awsDefaultRegionEnvVar) { + this.awsDefaultRegionEnvVar = awsDefaultRegionEnvVar; + return this; + } + + public TestData ec2MetadataConfig(Ec2MetadataConfig ec2MetadataConfig) { + this.ec2MetadataConfig = ec2MetadataConfig; + return this; + } + + public TestData expectedResolvedMode(DefaultsMode expectedResolvedMode) { + this.expectedResolvedMode = expectedResolvedMode; + return this; + } + } + + private static final class Ec2MetadataConfig { + private boolean imdsAvailable; + private String region; + private String ec2MetadataDisabledEnvVar; + + public Ec2MetadataConfig imdsAvailable(boolean imdsAvailable) { + this.imdsAvailable = imdsAvailable; + return this; + } + + public Ec2MetadataConfig region(String region) { + this.region = region; + return this; + } + + public Ec2MetadataConfig ec2MetadataDisabledEnvVar(String ec2MetadataDisabledEnvVar) { + this.ec2MetadataDisabledEnvVar = ec2MetadataDisabledEnvVar; + return this; + } + } +} \ No newline at end of file diff --git a/core/aws-core/src/test/java/software/amazon/awssdk/awscore/internal/defaultsmode/DefaultsModeConfigurationTest.java b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/internal/defaultsmode/DefaultsModeConfigurationTest.java new file mode 100644 index 000000000000..e7a2ba9f7b9b --- /dev/null +++ b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/internal/defaultsmode/DefaultsModeConfigurationTest.java @@ -0,0 +1,39 @@ +/* + * 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.awscore.internal.defaultsmode; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Arrays; +import org.junit.Test; +import software.amazon.awssdk.awscore.defaultsmode.DefaultsMode; +import software.amazon.awssdk.utils.AttributeMap; + +public class DefaultsModeConfigurationTest { + + @Test + public void defaultConfig_shouldPresentExceptLegacyAndAuto() { + Arrays.stream(DefaultsMode.values()).forEach(m -> { + if (m == DefaultsMode.LEGACY || m == DefaultsMode.AUTO) { + assertThat(DefaultsModeConfiguration.defaultConfig(m)).isEqualTo(AttributeMap.empty()); + assertThat(DefaultsModeConfiguration.defaultHttpConfig(m)).isEqualTo(AttributeMap.empty()); + } else { + assertThat(DefaultsModeConfiguration.defaultConfig(m)).isNotEqualTo(AttributeMap.empty()); + assertThat(DefaultsModeConfiguration.defaultHttpConfig(m)).isNotEqualTo(AttributeMap.empty()); + } + }); + } +} diff --git a/core/aws-core/src/test/java/software/amazon/awssdk/awscore/internal/defaultsmode/DefaultsModeResolverTest.java b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/internal/defaultsmode/DefaultsModeResolverTest.java new file mode 100644 index 000000000000..a0df1ee0c562 --- /dev/null +++ b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/internal/defaultsmode/DefaultsModeResolverTest.java @@ -0,0 +1,135 @@ +/* + * 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.awscore.internal.defaultsmode; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collection; +import java.util.concurrent.Callable; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import software.amazon.awssdk.awscore.defaultsmode.DefaultsMode; +import software.amazon.awssdk.core.SdkSystemSetting; +import software.amazon.awssdk.profiles.ProfileFileSystemSetting; +import software.amazon.awssdk.testutils.EnvironmentVariableHelper; +import software.amazon.awssdk.utils.Validate; + +@RunWith(Parameterized.class) +public class DefaultsModeResolverTest { + private static final EnvironmentVariableHelper ENVIRONMENT_VARIABLE_HELPER = new EnvironmentVariableHelper(); + + @Parameterized.Parameter + public TestData testData; + + @Parameterized.Parameters + public static Collection data() { + return Arrays.asList(new Object[] { + // Test defaults + new TestData(null, null, null, null, DefaultsMode.LEGACY), + new TestData(null, null, "PropertyNotSet", null, DefaultsMode.LEGACY), + + // Test resolution + new TestData("legacy", null, null, null, DefaultsMode.LEGACY), + new TestData("standard", null, null, null, DefaultsMode.STANDARD), + new TestData("auto", null, null, null, DefaultsMode.AUTO), + new TestData("lEgAcY", null, null, null, DefaultsMode.LEGACY), + new TestData("sTanDaRd", null, null, null, DefaultsMode.STANDARD), + new TestData("AUtO", null, null, null, DefaultsMode.AUTO), + + // Test precedence + new TestData("standard", "legacy", "PropertySetToLegacy", DefaultsMode.LEGACY, DefaultsMode.STANDARD), + new TestData("standard", null, null, DefaultsMode.LEGACY, DefaultsMode.STANDARD), + new TestData(null, "standard", "PropertySetToLegacy", DefaultsMode.LEGACY, DefaultsMode.STANDARD), + new TestData(null, "standard", null, DefaultsMode.LEGACY, DefaultsMode.STANDARD), + new TestData(null, null, "PropertySetToStandard", DefaultsMode.LEGACY, DefaultsMode.STANDARD), + new TestData(null, null, "PropertySetToAuto", DefaultsMode.LEGACY, DefaultsMode.AUTO), + new TestData(null, null, null, DefaultsMode.STANDARD, DefaultsMode.STANDARD), + + // Test invalid values + new TestData("wrongValue", null, null, null, IllegalArgumentException.class), + new TestData(null, "wrongValue", null, null, IllegalArgumentException.class), + new TestData(null, null, "PropertySetToUnsupportedValue", null, IllegalArgumentException.class), + + // Test capitalization standardization + new TestData("sTaNdArD", null, null, null, DefaultsMode.STANDARD), + new TestData(null, "sTaNdArD", null, null, DefaultsMode.STANDARD), + new TestData(null, null, "PropertyMixedCase", null, DefaultsMode.STANDARD), + }); + } + + @Before + @After + public void methodSetup() { + ENVIRONMENT_VARIABLE_HELPER.reset(); + System.clearProperty(SdkSystemSetting.AWS_DEFAULTS_MODE.property()); + System.clearProperty(ProfileFileSystemSetting.AWS_PROFILE.property()); + System.clearProperty(ProfileFileSystemSetting.AWS_CONFIG_FILE.property()); + } + + @Test + public void differentCombinationOfConfigs_shouldResolveCorrectly() throws Exception { + if (testData.envVarValue != null) { + ENVIRONMENT_VARIABLE_HELPER.set(SdkSystemSetting.AWS_DEFAULTS_MODE.environmentVariable(), testData.envVarValue); + } + + if (testData.systemProperty != null) { + System.setProperty(SdkSystemSetting.AWS_DEFAULTS_MODE.property(), testData.systemProperty); + } + + if (testData.configFile != null) { + String diskLocationForFile = diskLocationForConfig(testData.configFile); + Validate.isTrue(Files.isReadable(Paths.get(diskLocationForFile)), diskLocationForFile + " is not readable."); + System.setProperty(ProfileFileSystemSetting.AWS_PROFILE.property(), "default"); + System.setProperty(ProfileFileSystemSetting.AWS_CONFIG_FILE.property(), diskLocationForFile); + } + + Callable result = DefaultsModeResolver.create().defaultMode(testData.defaultMode)::resolve; + if (testData.expected instanceof Class) { + Class expectedClassType = (Class) testData.expected; + assertThatThrownBy(result::call).isInstanceOf(expectedClassType); + } else { + assertThat(result.call()).isEqualTo(testData.expected); + } + } + + private String diskLocationForConfig(String configFileName) { + return getClass().getResource(configFileName).getFile(); + } + + + private static class TestData { + private final String envVarValue; + private final String systemProperty; + private final String configFile; + private final DefaultsMode defaultMode; + private final Object expected; + + TestData(String systemProperty, String envVarValue, String configFile, DefaultsMode defaultMode, Object expected) { + this.envVarValue = envVarValue; + this.systemProperty = systemProperty; + this.configFile = configFile; + this.defaultMode = defaultMode; + this.expected = expected; + } + } +} \ No newline at end of file diff --git a/core/aws-core/src/test/resources/jetty-logging.properties b/core/aws-core/src/test/resources/jetty-logging.properties new file mode 100644 index 000000000000..4ee410e7fa92 --- /dev/null +++ b/core/aws-core/src/test/resources/jetty-logging.properties @@ -0,0 +1,18 @@ +# +# 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. +# + +# Set up logging implementation +org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog +org.eclipse.jetty.LEVEL=OFF diff --git a/core/aws-core/src/test/resources/log4j.properties b/core/aws-core/src/test/resources/log4j.properties new file mode 100644 index 000000000000..391579e1fcc1 --- /dev/null +++ b/core/aws-core/src/test/resources/log4j.properties @@ -0,0 +1,35 @@ +# +# 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. +# + +log4j.rootLogger=WARN, A1 +log4j.appender.A1=org.apache.log4j.ConsoleAppender +log4j.appender.A1.layout=org.apache.log4j.PatternLayout + +# Print the date in ISO 8601 format +log4j.appender.A1.layout.ConversionPattern=%d [%t] %-5p %c - %m%n + +# Adjust to see more / less logging +#log4j.logger.com.amazonaws.ec2=DEBUG + +# HttpClient 3 Wire Logging +#log4j.logger.httpclient.wire=DEBUG + +# HttpClient 4 Wire Logging +# log4j.logger.org.apache.http.wire=INFO +# log4j.logger.org.apache.http=DEBUG +# log4j.logger.org.apache.http.wire=DEBUG +# log4j.logger.software.amazonaws.awssdk=DEBUG + + diff --git a/core/aws-core/src/test/resources/software/amazon/awssdk/awscore/internal/defaultsmode/PropertyMixedCase b/core/aws-core/src/test/resources/software/amazon/awssdk/awscore/internal/defaultsmode/PropertyMixedCase new file mode 100644 index 000000000000..b400595208f4 --- /dev/null +++ b/core/aws-core/src/test/resources/software/amazon/awssdk/awscore/internal/defaultsmode/PropertyMixedCase @@ -0,0 +1,2 @@ +[default] +defaults_mode = sTanDard \ No newline at end of file diff --git a/core/aws-core/src/test/resources/software/amazon/awssdk/awscore/internal/defaultsmode/PropertyNotSet b/core/aws-core/src/test/resources/software/amazon/awssdk/awscore/internal/defaultsmode/PropertyNotSet new file mode 100644 index 000000000000..399487f9b5e5 --- /dev/null +++ b/core/aws-core/src/test/resources/software/amazon/awssdk/awscore/internal/defaultsmode/PropertyNotSet @@ -0,0 +1 @@ +[default] \ No newline at end of file diff --git a/core/aws-core/src/test/resources/software/amazon/awssdk/awscore/internal/defaultsmode/PropertySetToAuto b/core/aws-core/src/test/resources/software/amazon/awssdk/awscore/internal/defaultsmode/PropertySetToAuto new file mode 100644 index 000000000000..353d1deb69e6 --- /dev/null +++ b/core/aws-core/src/test/resources/software/amazon/awssdk/awscore/internal/defaultsmode/PropertySetToAuto @@ -0,0 +1,2 @@ +[default] +defaults_mode = auto \ No newline at end of file diff --git a/core/aws-core/src/test/resources/software/amazon/awssdk/awscore/internal/defaultsmode/PropertySetToLegacy b/core/aws-core/src/test/resources/software/amazon/awssdk/awscore/internal/defaultsmode/PropertySetToLegacy new file mode 100644 index 000000000000..db8ae0695416 --- /dev/null +++ b/core/aws-core/src/test/resources/software/amazon/awssdk/awscore/internal/defaultsmode/PropertySetToLegacy @@ -0,0 +1,2 @@ +[default] +defaults_mode = legacy \ No newline at end of file diff --git a/core/aws-core/src/test/resources/software/amazon/awssdk/awscore/internal/defaultsmode/PropertySetToStandard b/core/aws-core/src/test/resources/software/amazon/awssdk/awscore/internal/defaultsmode/PropertySetToStandard new file mode 100644 index 000000000000..fe9ff9fce00e --- /dev/null +++ b/core/aws-core/src/test/resources/software/amazon/awssdk/awscore/internal/defaultsmode/PropertySetToStandard @@ -0,0 +1,2 @@ +[default] +defaults_mode = standard \ No newline at end of file diff --git a/core/aws-core/src/test/resources/software/amazon/awssdk/awscore/internal/defaultsmode/PropertySetToUnsupportedValue b/core/aws-core/src/test/resources/software/amazon/awssdk/awscore/internal/defaultsmode/PropertySetToUnsupportedValue new file mode 100644 index 000000000000..7f88da4108ab --- /dev/null +++ b/core/aws-core/src/test/resources/software/amazon/awssdk/awscore/internal/defaultsmode/PropertySetToUnsupportedValue @@ -0,0 +1,2 @@ +[default] +defaults_mode = unsupported-value \ No newline at end of file diff --git a/core/profiles/src/main/java/software/amazon/awssdk/profiles/ProfileProperty.java b/core/profiles/src/main/java/software/amazon/awssdk/profiles/ProfileProperty.java index 6a2c3293ac69..32804fbd44ea 100644 --- a/core/profiles/src/main/java/software/amazon/awssdk/profiles/ProfileProperty.java +++ b/core/profiles/src/main/java/software/amazon/awssdk/profiles/ProfileProperty.java @@ -104,6 +104,13 @@ public final class ProfileProperty { */ public static final String RETRY_MODE = "retry_mode"; + /** + * The "defaults mode" to be used for clients created using the currently-configured profile. Defaults mode determins how SDK + * default configuration should be resolved. See the {@code DefaultsMode} class JavaDoc for more + * information. + */ + public static final String DEFAULTS_MODE = "defaults_mode"; + /** * Aws region where the SSO directory for the given 'sso_start_url' is hosted. This is independent of the general 'region'. */ diff --git a/core/regions/src/main/java/software/amazon/awssdk/regions/ServiceMetadataAdvancedOption.java b/core/regions/src/main/java/software/amazon/awssdk/regions/ServiceMetadataAdvancedOption.java new file mode 100644 index 000000000000..89cc510a9866 --- /dev/null +++ b/core/regions/src/main/java/software/amazon/awssdk/regions/ServiceMetadataAdvancedOption.java @@ -0,0 +1,45 @@ +/* + * 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.regions; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.core.SdkSystemSetting; +import software.amazon.awssdk.core.client.config.ClientOption; +import software.amazon.awssdk.profiles.ProfileProperty; + + +/** + * A collection of advanced options that can be configured on a {@link ServiceMetadata} via + * {@link ServiceMetadataConfiguration.Builder#putAdvancedOption(ServiceMetadataAdvancedOption, Object)}. + * + * @param The type of value associated with the option. + */ +@SdkPublicApi +public class ServiceMetadataAdvancedOption extends ClientOption { + + /** + * The default S3 regional endpoint setting for the {@code us-east-1} region to use. Setting + * the value to {@code regional} causes the SDK to use the {@code s3.us-east-1.amazonaws.com} endpoint when using the + * {@link Region#US_EAST_1} region instead of the global {@code s3.amazonaws.com} by default if it's not configured otherwise + * via {@link SdkSystemSetting#AWS_S3_US_EAST_1_REGIONAL_ENDPOINT} or {@link ProfileProperty#S3_US_EAST_1_REGIONAL_ENDPOINT} + */ + public static final ServiceMetadataAdvancedOption DEFAULT_S3_US_EAST_1_REGIONAL_ENDPOINT = + new ServiceMetadataAdvancedOption<>(String.class); + + protected ServiceMetadataAdvancedOption(Class valueClass) { + super(valueClass); + } +} diff --git a/core/regions/src/main/java/software/amazon/awssdk/regions/ServiceMetadataConfiguration.java b/core/regions/src/main/java/software/amazon/awssdk/regions/ServiceMetadataConfiguration.java index 7639020c470d..aa26bdab9562 100644 --- a/core/regions/src/main/java/software/amazon/awssdk/regions/ServiceMetadataConfiguration.java +++ b/core/regions/src/main/java/software/amazon/awssdk/regions/ServiceMetadataConfiguration.java @@ -15,10 +15,13 @@ package software.amazon.awssdk.regions; +import java.util.Map; +import java.util.Optional; import java.util.function.Supplier; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.profiles.ProfileFile; import software.amazon.awssdk.profiles.ProfileFileSystemSetting; +import software.amazon.awssdk.utils.AttributeMap; /** * Configuration for a {@link ServiceMetadata}. This allows modifying the values used by default when a metadata instance is @@ -30,10 +33,12 @@ public final class ServiceMetadataConfiguration { private final Supplier profileFile; private final String profileName; + private final AttributeMap advancedOptions; private ServiceMetadataConfiguration(Builder builder) { this.profileFile = builder.profileFile; this.profileName = builder.profileName; + this.advancedOptions = builder.advancedOptions.build(); } /** @@ -57,9 +62,19 @@ public String profileName() { return profileName; } + /** + * Load the optional requested advanced option that was configured on the service metadata builder. + * + * @see ServiceMetadataConfiguration.Builder#putAdvancedOption(ServiceMetadataAdvancedOption, Object) + */ + public Optional advancedOption(ServiceMetadataAdvancedOption option) { + return Optional.ofNullable(advancedOptions.get(option)); + } + public static final class Builder { private Supplier profileFile; private String profileName; + private AttributeMap.Builder advancedOptions = AttributeMap.builder(); private Builder() { } @@ -85,6 +100,24 @@ public Builder profileName(String profileName) { return this; } + /** + * Configure the map of advanced override options. This will override all values currently configured. The values in the + * map must match the key type of the map, or a runtime exception will be raised. + */ + public Builder putAdvancedOption(ServiceMetadataAdvancedOption option, T value) { + this.advancedOptions.put(option, value); + return this; + } + + /** + * Configure an advanced override option. + * @see ServiceMetadataAdvancedOption + */ + public Builder advancedOptions(Map, ?> advancedOptions) { + this.advancedOptions.putAll(advancedOptions); + return this; + } + /** * Build the {@link ServiceMetadata} instance with the updated configuration. */ diff --git a/core/regions/src/main/java/software/amazon/awssdk/regions/internal/util/EC2MetadataUtils.java b/core/regions/src/main/java/software/amazon/awssdk/regions/internal/util/EC2MetadataUtils.java index 13a2dbb245eb..4280003676aa 100644 --- a/core/regions/src/main/java/software/amazon/awssdk/regions/internal/util/EC2MetadataUtils.java +++ b/core/regions/src/main/java/software/amazon/awssdk/regions/internal/util/EC2MetadataUtils.java @@ -39,7 +39,15 @@ import software.amazon.awssdk.regions.util.ResourcesEndpointProvider; /** - * Utility class for retrieving Amazon EC2 instance metadata.
+ + * + * Utility class for retrieving Amazon EC2 instance metadata. + * + *

+ * Note: this is an internal API subject to change. Users of the SDK + * should not depend on this. + * + *

* 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 @@ -73,7 +81,7 @@ public final class EC2MetadataUtils { private static final String EC2_METADATA_TOKEN_HEADER = "x-aws-ec2-metadata-token"; - private static final int DEFAULT_QUERY_RETRIES = 3; + private static final int DEFAULT_QUERY_ATTEMPTS = 3; private static final int MINIMUM_RETRY_WAIT_TIME_MILLISECONDS = 250; private static final Logger log = LoggerFactory.getLogger(EC2MetadataUtils.class); private static final Map CACHE = new ConcurrentHashMap<>(); @@ -341,7 +349,7 @@ private static String[] stringArrayValue(JsonNode jsonNode) { } public static String getData(String path) { - return getData(path, DEFAULT_QUERY_RETRIES); + return getData(path, DEFAULT_QUERY_ATTEMPTS); } public static String getData(String path, int tries) { @@ -353,7 +361,7 @@ public static String getData(String path, int tries) { } public static List getItems(String path) { - return getItems(path, DEFAULT_QUERY_RETRIES, false); + return getItems(path, DEFAULT_QUERY_ATTEMPTS, false); } public static List getItems(String path, int tries) { @@ -361,7 +369,7 @@ public static List getItems(String path, int tries) { } @SdkTestInternalApi - static void clearCache() { + public static void clearCache() { CACHE.clear(); } @@ -391,8 +399,13 @@ private static List 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 - int pause = (int) (Math.pow(2, DEFAULT_QUERY_RETRIES - tries) * MINIMUM_RETRY_WAIT_TIME_MILLISECONDS); + int pause = (int) (Math.pow(2, DEFAULT_QUERY_ATTEMPTS - tries) * MINIMUM_RETRY_WAIT_TIME_MILLISECONDS); try { Thread.sleep(pause < MINIMUM_RETRY_WAIT_TIME_MILLISECONDS ? MINIMUM_RETRY_WAIT_TIME_MILLISECONDS : pause); @@ -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_ATTEMPTS); + } + + /** + * 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) { diff --git a/core/regions/src/main/java/software/amazon/awssdk/regions/servicemetadata/EnhancedS3ServiceMetadata.java b/core/regions/src/main/java/software/amazon/awssdk/regions/servicemetadata/EnhancedS3ServiceMetadata.java index d7a4b069f460..79b109899974 100644 --- a/core/regions/src/main/java/software/amazon/awssdk/regions/servicemetadata/EnhancedS3ServiceMetadata.java +++ b/core/regions/src/main/java/software/amazon/awssdk/regions/servicemetadata/EnhancedS3ServiceMetadata.java @@ -26,6 +26,7 @@ import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.regions.ServiceEndpointKey; import software.amazon.awssdk.regions.ServiceMetadata; +import software.amazon.awssdk.regions.ServiceMetadataAdvancedOption; import software.amazon.awssdk.regions.ServiceMetadataConfiguration; import software.amazon.awssdk.regions.ServicePartitionMetadata; import software.amazon.awssdk.utils.Lazy; @@ -54,7 +55,7 @@ private EnhancedS3ServiceMetadata(ServiceMetadataConfiguration config) { Supplier profileName = config.profileName() != null ? () -> config.profileName() : ProfileFileSystemSetting.AWS_PROFILE::getStringValueOrThrow; - this.useUsEast1RegionalEndpoint = new Lazy<>(() -> useUsEast1RegionalEndpoint(profileFile, profileName)); + this.useUsEast1RegionalEndpoint = new Lazy<>(() -> useUsEast1RegionalEndpoint(profileFile, profileName, config)); this.s3ServiceMetadata = new S3ServiceMetadata().reconfigure(config); } @@ -81,7 +82,8 @@ public List servicePartitions() { return s3ServiceMetadata.servicePartitions(); } - private boolean useUsEast1RegionalEndpoint(Supplier profileFile, Supplier profileName) { + private boolean useUsEast1RegionalEndpoint(Supplier profileFile, Supplier profileName, + ServiceMetadataConfiguration config) { String env = envVarSetting(); if (env != null) { @@ -94,7 +96,8 @@ private boolean useUsEast1RegionalEndpoint(Supplier profileFile, Su return REGIONAL_SETTING.equalsIgnoreCase(profile); } - return false; + return config.advancedOption(ServiceMetadataAdvancedOption.DEFAULT_S3_US_EAST_1_REGIONAL_ENDPOINT) + .filter(REGIONAL_SETTING::equalsIgnoreCase).isPresent(); } private static String envVarSetting() { diff --git a/core/regions/src/test/java/software/amazon/awssdk/regions/internal/util/EC2MetadataUtilsTest.java b/core/regions/src/test/java/software/amazon/awssdk/regions/internal/util/EC2MetadataUtilsTest.java index e24b403e542c..4172db957e78 100644 --- a/core/regions/src/test/java/software/amazon/awssdk/regions/internal/util/EC2MetadataUtilsTest.java +++ b/core/regions/src/test/java/software/amazon/awssdk/regions/internal/util/EC2MetadataUtilsTest.java @@ -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; @@ -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))); + } } diff --git a/core/regions/src/test/java/software/amazon/awssdk/regions/servicemetadata/EnhancedS3ServiceMetadataTest.java b/core/regions/src/test/java/software/amazon/awssdk/regions/servicemetadata/EnhancedS3ServiceMetadataTest.java index 062592471266..3462090224d3 100644 --- a/core/regions/src/test/java/software/amazon/awssdk/regions/servicemetadata/EnhancedS3ServiceMetadataTest.java +++ b/core/regions/src/test/java/software/amazon/awssdk/regions/servicemetadata/EnhancedS3ServiceMetadataTest.java @@ -18,83 +18,119 @@ import static org.assertj.core.api.Assertions.assertThat; import java.net.URI; -import java.net.URISyntaxException; +import java.nio.file.Files; import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collection; import org.junit.After; import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; import software.amazon.awssdk.core.SdkSystemSetting; import software.amazon.awssdk.profiles.ProfileFile; -import software.amazon.awssdk.profiles.ProfileFileSystemSetting; import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.regions.ServiceMetadata; +import software.amazon.awssdk.regions.ServiceMetadataAdvancedOption; import software.amazon.awssdk.testutils.EnvironmentVariableHelper; +import software.amazon.awssdk.utils.Validate; +@RunWith(Parameterized.class) public class EnhancedS3ServiceMetadataTest { private static final EnvironmentVariableHelper ENVIRONMENT_VARIABLE_HELPER = new EnvironmentVariableHelper(); private static final URI S3_GLOBAL_ENDPOINT = URI.create("s3.amazonaws.com"); private static final URI S3_IAD_REGIONAL_ENDPOINT = URI.create("s3.us-east-1.amazonaws.com"); - private EnhancedS3ServiceMetadata enhancedMetadata = new EnhancedS3ServiceMetadata(); + private ServiceMetadata enhancedMetadata = new EnhancedS3ServiceMetadata(); + + @Parameterized.Parameter + public TestData testData; + + @Parameterized.Parameters + public static Collection data() { + return Arrays.asList(new Object[] { + // Test defaults + new TestData(null, null, null, null, S3_GLOBAL_ENDPOINT), + + // Test precedence + new TestData("regional", null, null, null, S3_IAD_REGIONAL_ENDPOINT), + new TestData("test", "regional", "/profileconfig/s3_regional_config_profile.tst", "regional", + S3_GLOBAL_ENDPOINT), + new TestData(null, "regional", "/profileconfig/s3_regional_config_profile.tst", "non-regional", + S3_IAD_REGIONAL_ENDPOINT), + new TestData(null, null, "/profileconfig/s3_regional_config_profile.tst", "non-regional", S3_IAD_REGIONAL_ENDPOINT), + new TestData(null, null, null, "regional", S3_IAD_REGIONAL_ENDPOINT), + + // Test capitalization standardization + new TestData("rEgIONal", null, null, null, S3_IAD_REGIONAL_ENDPOINT), + new TestData(null, "rEgIONal", null, null, S3_IAD_REGIONAL_ENDPOINT), + new TestData(null, null, "/profileconfig/s3_regional_config_profile_mixed_case.tst", null, S3_IAD_REGIONAL_ENDPOINT), + new TestData(null, null, null, "rEgIONal", S3_IAD_REGIONAL_ENDPOINT), + + // Test other value + new TestData("othervalue", null, null, null, S3_GLOBAL_ENDPOINT), + new TestData(null, "dafsad", null, null, S3_GLOBAL_ENDPOINT), + new TestData(null, null, "/profileconfig/s3_regional_config_profile_non_regional.tst", null, S3_GLOBAL_ENDPOINT), + new TestData(null, null, null, "somehtingelse", S3_GLOBAL_ENDPOINT), + }); + } + @After public void methodSetup() { ENVIRONMENT_VARIABLE_HELPER.reset(); - System.clearProperty(ProfileFileSystemSetting.AWS_PROFILE.property()); - System.clearProperty(ProfileFileSystemSetting.AWS_CONFIG_FILE.property()); - - enhancedMetadata = new EnhancedS3ServiceMetadata(); - } - - @Test - public void optionNotSet_returnsGlobalEndpoint() { - assertThat(enhancedMetadata.endpointFor(Region.US_EAST_1)).isEqualTo(S3_GLOBAL_ENDPOINT); + System.clearProperty(SdkSystemSetting.AWS_S3_US_EAST_1_REGIONAL_ENDPOINT.property()); } @Test - public void regionalSet_profile_returnsRegionalEndpoint() throws URISyntaxException { - String testFile = "/profileconfig/s3_regional_config_profile.tst"; - - System.setProperty(ProfileFileSystemSetting.AWS_PROFILE.property(), "regional_s3_endpoint"); - System.setProperty(ProfileFileSystemSetting.AWS_CONFIG_FILE.property(), Paths.get(getClass().getResource(testFile).toURI()).toString()); - - assertThat(enhancedMetadata.endpointFor(Region.US_EAST_1)).isEqualTo(S3_IAD_REGIONAL_ENDPOINT); + public void differentCombinationOfConfigs_shouldResolveCorrectly() { + enhancedMetadata = + new EnhancedS3ServiceMetadata().reconfigure(c -> c.putAdvancedOption(ServiceMetadataAdvancedOption.DEFAULT_S3_US_EAST_1_REGIONAL_ENDPOINT, + testData.advancedOption)); + if (testData.envVarValue != null) { + ENVIRONMENT_VARIABLE_HELPER.set(SdkSystemSetting.AWS_S3_US_EAST_1_REGIONAL_ENDPOINT.environmentVariable(), + testData.envVarValue); + } + + if (testData.systemProperty != null) { + System.setProperty(SdkSystemSetting.AWS_S3_US_EAST_1_REGIONAL_ENDPOINT.property(), testData.systemProperty); + } + + if (testData.configFile != null) { + String diskLocationForFile = diskLocationForConfig(testData.configFile); + Validate.isTrue(Files.isReadable(Paths.get(diskLocationForFile)), diskLocationForFile + " is not readable."); + + ProfileFile file = ProfileFile.builder() + .content(Paths.get(diskLocationForFile)) + .type(ProfileFile.Type.CONFIGURATION) + .build(); + + enhancedMetadata = enhancedMetadata.reconfigure(c -> c.profileFile(() -> file) + .profileName("regional_s3_endpoint") + .putAdvancedOption(ServiceMetadataAdvancedOption.DEFAULT_S3_US_EAST_1_REGIONAL_ENDPOINT, + testData.advancedOption)); + } + + URI result = enhancedMetadata.endpointFor(Region.US_EAST_1); + assertThat(result).isEqualTo(testData.expected); } - @Test - public void reconfiguredProfile_returnsRegionalEndpoint() throws URISyntaxException { - String testFile = "/profileconfig/s3_regional_config_profile.tst"; - - ProfileFile file = ProfileFile.builder() - .content(Paths.get(getClass().getResource(testFile).toURI())) - .type(ProfileFile.Type.CONFIGURATION) - .build(); - - assertThat(enhancedMetadata.reconfigure(c -> c.profileFile(() -> file) - .profileName("regional_s3_endpoint")) - .endpointFor(Region.US_EAST_1)) - .isEqualTo(S3_IAD_REGIONAL_ENDPOINT); + private String diskLocationForConfig(String configFileName) { + return getClass().getResource(configFileName).getFile(); } - @Test - public void regionalSet_env_returnsRegionalEndpoint() { - ENVIRONMENT_VARIABLE_HELPER.set(SdkSystemSetting.AWS_S3_US_EAST_1_REGIONAL_ENDPOINT.environmentVariable(), "regional"); - assertThat(enhancedMetadata.endpointFor(Region.US_EAST_1)).isEqualTo(S3_IAD_REGIONAL_ENDPOINT); - } - - @Test - public void regionalSet_mixedCase_env_returnsRegionalEndpoint() { - ENVIRONMENT_VARIABLE_HELPER.set(SdkSystemSetting.AWS_S3_US_EAST_1_REGIONAL_ENDPOINT.environmentVariable(), "rEgIoNaL"); - assertThat(enhancedMetadata.endpointFor(Region.US_EAST_1)).isEqualTo(S3_IAD_REGIONAL_ENDPOINT); - } - - @Test - public void global_env_returnsGlobalEndpoint() { - ENVIRONMENT_VARIABLE_HELPER.set(SdkSystemSetting.AWS_S3_US_EAST_1_REGIONAL_ENDPOINT.environmentVariable(), "non_regional"); - assertThat(enhancedMetadata.endpointFor(Region.US_EAST_1)).isEqualTo(S3_GLOBAL_ENDPOINT); - } - - @Test - public void valueNotEqualToRegional_env_returnsGlobalEndpoint() { - ENVIRONMENT_VARIABLE_HELPER.set(SdkSystemSetting.AWS_S3_US_EAST_1_REGIONAL_ENDPOINT.environmentVariable(), "some-nonsense-value"); - assertThat(enhancedMetadata.endpointFor(Region.US_EAST_1)).isEqualTo(S3_GLOBAL_ENDPOINT); + private static class TestData { + private final String envVarValue; + private final String systemProperty; + private final String configFile; + private final String advancedOption; + private final URI expected; + + TestData(String systemProperty, String envVarValue, String configFile, String advancedOption, URI expected) { + this.envVarValue = envVarValue; + this.systemProperty = systemProperty; + this.configFile = configFile; + this.advancedOption = advancedOption; + this.expected = expected; + } } } diff --git a/core/regions/src/test/resources/profileconfig/s3_regional_config_profile_mixed_case.tst b/core/regions/src/test/resources/profileconfig/s3_regional_config_profile_mixed_case.tst new file mode 100644 index 000000000000..2fd93950a604 --- /dev/null +++ b/core/regions/src/test/resources/profileconfig/s3_regional_config_profile_mixed_case.tst @@ -0,0 +1,2 @@ +[profile regional_s3_endpoint] +s3_us_east_1_regional_endpoint=REgiONal \ No newline at end of file diff --git a/core/regions/src/test/resources/profileconfig/s3_regional_config_profile_non_regional.tst b/core/regions/src/test/resources/profileconfig/s3_regional_config_profile_non_regional.tst new file mode 100644 index 000000000000..a517714117fe --- /dev/null +++ b/core/regions/src/test/resources/profileconfig/s3_regional_config_profile_non_regional.tst @@ -0,0 +1,2 @@ +[profile regional_s3_endpoint] +s3_us_east_1_regional_endpoint=somethingelse \ No newline at end of file diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/SdkSystemSetting.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/SdkSystemSetting.java index e5e867a97449..1e5c400ca617 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/SdkSystemSetting.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/SdkSystemSetting.java @@ -168,6 +168,11 @@ public enum SdkSystemSetting implements SystemSetting { */ AWS_MAX_ATTEMPTS("aws.maxAttempts", null), + /** + * Which {@code DefaultsMode} to use, case insensitive + */ + AWS_DEFAULTS_MODE("aws.defaultsMode", null), + /** * Defines whether dualstack endpoints should be resolved during default endpoint resolution instead of non-dualstack * endpoints. diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java index e3659382c141..7501c7b96945 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java @@ -275,8 +275,8 @@ private SdkHttpClient resolveSyncHttpClient(SdkClientConfiguration config) { "The httpClient and the httpClientBuilder can't both be configured."); return Either.fromNullable(config.option(SdkClientOption.SYNC_HTTP_CLIENT), httpClientBuilder) - .map(e -> e.map(NonManagedSdkHttpClient::new, b -> b.buildWithDefaults(childHttpConfig()))) - .orElseGet(() -> defaultHttpClientBuilder.buildWithDefaults(childHttpConfig())); + .map(e -> e.map(NonManagedSdkHttpClient::new, b -> b.buildWithDefaults(childHttpConfig(config)))) + .orElseGet(() -> defaultHttpClientBuilder.buildWithDefaults(childHttpConfig(config))); } /** @@ -286,13 +286,22 @@ private SdkAsyncHttpClient resolveAsyncHttpClient(SdkClientConfiguration config) Validate.isTrue(config.option(ASYNC_HTTP_CLIENT) == null || asyncHttpClientBuilder == null, "The asyncHttpClient and the asyncHttpClientBuilder can't both be configured."); return Either.fromNullable(config.option(ASYNC_HTTP_CLIENT), asyncHttpClientBuilder) - .map(e -> e.map(NonManagedSdkAsyncHttpClient::new, b -> b.buildWithDefaults(childHttpConfig()))) - .orElseGet(() -> defaultAsyncHttpClientBuilder.buildWithDefaults(childHttpConfig())); + .map(e -> e.map(NonManagedSdkAsyncHttpClient::new, b -> b.buildWithDefaults(childHttpConfig(config)))) + .orElseGet(() -> defaultAsyncHttpClientBuilder.buildWithDefaults(childHttpConfig(config))); } /** * Optionally overridden by child implementations to provide implementation-specific default HTTP configuration. */ + protected AttributeMap childHttpConfig(SdkClientConfiguration configuration) { + return childHttpConfig(); + } + + /** + * Optionally overridden by child implementations to provide implementation-specific default HTTP configuration. + * @deprecated use {@link #childHttpConfig(SdkClientConfiguration)} instead + */ + @Deprecated protected AttributeMap childHttpConfig() { return AttributeMap.empty(); } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientConfiguration.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientConfiguration.java index c87e7b22f400..aabc7a5ba631 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientConfiguration.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientConfiguration.java @@ -79,6 +79,25 @@ public void close() { attributes.close(); } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + SdkClientConfiguration that = (SdkClientConfiguration) o; + + return attributes.equals(that.attributes); + } + + @Override + public int hashCode() { + return attributes.hashCode(); + } + public static final class Builder implements CopyableBuilder { private final AttributeMap.Builder attributes; diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/client/config/SdkClientConfigurationTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/client/config/SdkClientConfigurationTest.java new file mode 100644 index 000000000000..c943b3eef096 --- /dev/null +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/client/config/SdkClientConfigurationTest.java @@ -0,0 +1,29 @@ +/* + * 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.core.client.config; + +import nl.jqno.equalsverifier.EqualsVerifier; +import org.junit.Test; + +public class SdkClientConfigurationTest { + + @Test + public void equalsHashcode() { + EqualsVerifier.forClass(SdkClientConfiguration.class) + .withNonnullFields("attributes") + .verify(); + } +} diff --git a/http-client-spi/src/main/java/software/amazon/awssdk/http/SdkHttpClient.java b/http-client-spi/src/main/java/software/amazon/awssdk/http/SdkHttpClient.java index cd2ac439cc89..b57130a6eb9b 100644 --- a/http-client-spi/src/main/java/software/amazon/awssdk/http/SdkHttpClient.java +++ b/http-client-spi/src/main/java/software/amazon/awssdk/http/SdkHttpClient.java @@ -70,8 +70,8 @@ default SdkHttpClient build() { } /** - * Create an {@link SdkHttpClient} with service specific defaults applied. Applying service defaults is optional - * and some options may not be supported by a particular implementation. + * Create an {@link SdkHttpClient} with service specific defaults and defaults from {@code DefaultsMode} applied. + * Applying service defaults is optional and some options may not be supported by a particular implementation. * * @param serviceDefaults Service specific defaults. Keys will be one of the constants defined in * {@link SdkHttpConfigurationOption}. diff --git a/http-client-spi/src/main/java/software/amazon/awssdk/http/async/SdkAsyncHttpClient.java b/http-client-spi/src/main/java/software/amazon/awssdk/http/async/SdkAsyncHttpClient.java index 3f7e8dcbd53b..a24f2c7c91a0 100644 --- a/http-client-spi/src/main/java/software/amazon/awssdk/http/async/SdkAsyncHttpClient.java +++ b/http-client-spi/src/main/java/software/amazon/awssdk/http/async/SdkAsyncHttpClient.java @@ -19,6 +19,7 @@ import software.amazon.awssdk.annotations.Immutable; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.annotations.ThreadSafe; +import software.amazon.awssdk.http.SdkHttpConfigurationOption; import software.amazon.awssdk.utils.AttributeMap; import software.amazon.awssdk.utils.SdkAutoCloseable; import software.amazon.awssdk.utils.builder.SdkBuilder; diff --git a/services/s3/src/test/java/software/amazon/awssdk/services/s3/S3EndpointResolutionTest.java b/services/s3/src/test/java/software/amazon/awssdk/services/s3/S3EndpointResolutionTest.java index 8816881a3981..18cae7db4682 100644 --- a/services/s3/src/test/java/software/amazon/awssdk/services/s3/S3EndpointResolutionTest.java +++ b/services/s3/src/test/java/software/amazon/awssdk/services/s3/S3EndpointResolutionTest.java @@ -28,6 +28,7 @@ import org.junit.Test; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.awscore.defaultsmode.DefaultsMode; import software.amazon.awssdk.core.SdkSystemSetting; import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption; @@ -577,6 +578,24 @@ public void regionalSettingUnset_usesGlobalEndpoint() throws UnsupportedEncoding assertThat(mockHttpClient.getLastRequest().getUri().getHost()).isEqualTo("s3.amazonaws.com"); } + @Test + public void standardDefaultsMode_usesRegionalIadEndpoint() throws UnsupportedEncodingException { + mockHttpClient.stubNextResponse(mockListObjectsResponse()); + + S3Client s3Client = S3Client.builder() + .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("akid", + "skid"))) + .httpClient(mockHttpClient) + .defaultsMode(DefaultsMode.STANDARD) + .serviceConfiguration(S3Configuration.builder() + .pathStyleAccessEnabled(true) + .build()) + .region(Region.US_EAST_1) + .build(); + s3Client.listObjects(ListObjectsRequest.builder().bucket(BUCKET).build()); + assertThat(mockHttpClient.getLastRequest().getUri().getHost()).isEqualTo("s3.us-east-1.amazonaws.com"); + } + /** * Assert that the provided request would have gone to the given endpoint. * diff --git a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/defaultsmode/AsyncClientDefaultsModeTest.java b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/defaultsmode/AsyncClientDefaultsModeTest.java new file mode 100644 index 000000000000..4c21e16d7bbe --- /dev/null +++ b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/defaultsmode/AsyncClientDefaultsModeTest.java @@ -0,0 +1,42 @@ +/* + * 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.services.defaultsmode; + +import java.util.concurrent.CompletionException; +import software.amazon.awssdk.services.protocolrestjson.ProtocolRestJsonAsyncClient; +import software.amazon.awssdk.services.protocolrestjson.ProtocolRestJsonAsyncClientBuilder; +import software.amazon.awssdk.services.protocolrestjson.model.AllTypesResponse; + +public class AsyncClientDefaultsModeTest + extends ClientDefaultsModeTestSuite { + @Override + protected ProtocolRestJsonAsyncClientBuilder newClientBuilder() { + return ProtocolRestJsonAsyncClient.builder(); + } + + @Override + protected AllTypesResponse callAllTypes(ProtocolRestJsonAsyncClient client) { + try { + return client.allTypes().join(); + } catch (CompletionException e) { + if (e.getCause() instanceof RuntimeException) { + throw (RuntimeException) e.getCause(); + } + + throw e; + } + } +} diff --git a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/defaultsmode/ClientDefaultsModeTestSuite.java b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/defaultsmode/ClientDefaultsModeTestSuite.java new file mode 100644 index 000000000000..a407de097528 --- /dev/null +++ b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/defaultsmode/ClientDefaultsModeTestSuite.java @@ -0,0 +1,95 @@ +/* + * Copyright 2010-2020 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.services.defaultsmode; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.anyRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl; +import static com.github.tomakehurst.wiremock.client.WireMock.containing; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; + +import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import java.net.URI; +import org.junit.Rule; +import org.junit.Test; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.awscore.client.builder.AwsClientBuilder; +import software.amazon.awssdk.awscore.defaultsmode.DefaultsMode; +import software.amazon.awssdk.core.retry.RetryMode; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.protocolrestjson.model.AllTypesResponse; + +/** + * Tests suites to verify {@link DefaultsMode} behavior. We currently just test SDK default configuration such as + * {@link RetryMode}; there is no easy way to test HTTP timeout option. + * + */ +public abstract class ClientDefaultsModeTestSuite> { + @Rule + public WireMockRule wireMock = new WireMockRule(0); + + @Test + public void legacyDefaultsMode_shouldUseLegacySetting() { + stubResponse(); + ClientT client = clientBuilder().overrideConfiguration(o -> o.retryPolicy(RetryMode.LEGACY)).build(); + callAllTypes(client); + + WireMock.verify(postRequestedFor(anyUrl()).withHeader("User-Agent", containing("cfg/retry-mode/legacy"))); + } + + @Test + public void standardDefaultsMode_shouldApplyStandardDefaults() { + stubResponse(); + ClientT client = clientBuilder().defaultsMode(DefaultsMode.STANDARD).build(); + callAllTypes(client); + + WireMock.verify(postRequestedFor(anyUrl()).withHeader("User-Agent", containing("cfg/retry-mode/standard"))); + } + + @Test + public void retryModeOverridden_shouldTakePrecedence() { + stubResponse(); + ClientT client = + clientBuilder().defaultsMode(DefaultsMode.STANDARD).overrideConfiguration(o -> o.retryPolicy(RetryMode.LEGACY)).build(); + callAllTypes(client); + + WireMock.verify(postRequestedFor(anyUrl()).withHeader("User-Agent", containing("cfg/retry-mode/legacy"))); + } + + private BuilderT clientBuilder() { + return newClientBuilder().credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("akid", "skid"))) + .region(Region.US_EAST_1) + .endpointOverride(URI.create("http://localhost:" + wireMock.port())); + } + + protected abstract BuilderT newClientBuilder(); + + protected abstract AllTypesResponse callAllTypes(ClientT client); + + private void verifyRequestCount(int count) { + verify(count, anyRequestedFor(anyUrl())); + } + + private void stubResponse() { + stubFor(post(anyUrl()) + .willReturn(aResponse())); + } +} diff --git a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/defaultsmode/SyncClientDefaultsModeTest.java b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/defaultsmode/SyncClientDefaultsModeTest.java new file mode 100644 index 000000000000..106da961ce65 --- /dev/null +++ b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/defaultsmode/SyncClientDefaultsModeTest.java @@ -0,0 +1,32 @@ +/* + * 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.services.defaultsmode; + +import software.amazon.awssdk.services.protocolrestjson.ProtocolRestJsonClient; +import software.amazon.awssdk.services.protocolrestjson.ProtocolRestJsonClientBuilder; +import software.amazon.awssdk.services.protocolrestjson.model.AllTypesResponse; + +public class SyncClientDefaultsModeTest extends ClientDefaultsModeTestSuite { + @Override + protected ProtocolRestJsonClientBuilder newClientBuilder() { + return ProtocolRestJsonClient.builder(); + } + + @Override + protected AllTypesResponse callAllTypes(ProtocolRestJsonClient client) { + return client.allTypes(); + } +} diff --git a/utils/src/main/java/software/amazon/awssdk/utils/AttributeMap.java b/utils/src/main/java/software/amazon/awssdk/utils/AttributeMap.java index 32169bd3bc0c..7c182233e4ee 100644 --- a/utils/src/main/java/software/amazon/awssdk/utils/AttributeMap.java +++ b/utils/src/main/java/software/amazon/awssdk/utils/AttributeMap.java @@ -36,6 +36,7 @@ @SdkProtectedApi @Immutable public final class AttributeMap implements ToCopyableBuilder, SdkAutoCloseable { + private static final AttributeMap EMPTY = AttributeMap.builder().build(); private final Map, Object> attributes; private AttributeMap(Map, ?> attributes) { @@ -74,7 +75,7 @@ public AttributeMap merge(AttributeMap lowerPrecedence) { } public static AttributeMap empty() { - return builder().build(); + return EMPTY; } public AttributeMap copy() { diff --git a/utils/src/main/java/software/amazon/awssdk/utils/internal/SystemSettingUtils.java b/utils/src/main/java/software/amazon/awssdk/utils/internal/SystemSettingUtils.java index 1625ca0bff64..39447b081dda 100644 --- a/utils/src/main/java/software/amazon/awssdk/utils/internal/SystemSettingUtils.java +++ b/utils/src/main/java/software/amazon/awssdk/utils/internal/SystemSettingUtils.java @@ -73,7 +73,7 @@ private static Optional resolveProperty(SystemSetting setting) { /** * Attempt to load this setting from the environment variables. */ - private static Optional resolveEnvironmentVariable(SystemSetting setting) { + public static Optional resolveEnvironmentVariable(SystemSetting setting) { try { // CHECKSTYLE:OFF - This is the only place we're allowed to use System.getenv return Optional.ofNullable(setting.environmentVariable()).map(System::getenv);