diff --git a/.changes/next-release/feature-AWSSDKforJavaV2-6e40ae3.json b/.changes/next-release/feature-AWSSDKforJavaV2-6e40ae3.json new file mode 100644 index 000000000000..e1dcea8a01ea --- /dev/null +++ b/.changes/next-release/feature-AWSSDKforJavaV2-6e40ae3.json @@ -0,0 +1,6 @@ +{ + "type": "feature", + "category": "AWS SDK for Java V2", + "contributor": "", + "description": "Add support for tracking business metrics from resolved endpoints." +} diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointResolverInterceptorSpec.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointResolverInterceptorSpec.java index f03f615b57bc..7bac753e88e5 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointResolverInterceptorSpec.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointResolverInterceptorSpec.java @@ -160,6 +160,8 @@ public TypeSpec poetSpec() { } endpointParamsKnowledgeIndex.addAccountIdMethodsIfPresent(b); + + b.addMethod(setMetricValuesMethod()); return b.build(); } @@ -255,6 +257,7 @@ private MethodSpec modifyRequestMethod(String endpointAuthSchemeStrategyFieldNam } b.addStatement("executionAttributes.putAttribute(SdkInternalExecutionAttribute.RESOLVED_ENDPOINT, endpoint)"); + b.addStatement("setMetricValues(endpoint, executionAttributes)"); b.addStatement("return result"); b.endControlFlow(); b.beginControlFlow("catch ($T e)", CompletionException.class); @@ -905,4 +908,19 @@ private MethodSpec constructorMethodSpec(String endpointAuthSchemeFieldName) { b.addStatement("this.$N = $N.endpointAuthSchemeStrategy()", endpointAuthSchemeFieldName, factoryLocalVarName); return b.build(); } + + private MethodSpec setMetricValuesMethod() { + MethodSpec.Builder b = MethodSpec.methodBuilder("setMetricValues") + .addModifiers(Modifier.PRIVATE) + .addParameter(Endpoint.class, "endpoint") + .addParameter(ExecutionAttributes.class, "executionAttributes") + .returns(void.class); + + b.beginControlFlow("if (endpoint.attribute($T.METRIC_VALUES) != null)", AwsEndpointAttribute.class); + b.addStatement("executionAttributes.getOptionalAttribute($T.BUSINESS_METRICS).ifPresent(" + + "metrics -> endpoint.attribute($T.METRIC_VALUES).forEach(v -> metrics.addMetric(v)))", + SdkInternalExecutionAttribute.class, AwsEndpointAttribute.class); + b.endControlFlow(); + return b.build(); + } } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/TestGeneratorUtils.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/TestGeneratorUtils.java index 375ada730d63..f2a3ee963733 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/TestGeneratorUtils.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/TestGeneratorUtils.java @@ -30,6 +30,8 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import software.amazon.awssdk.awscore.endpoints.AwsEndpointAttribute; import software.amazon.awssdk.awscore.endpoints.authscheme.SigV4AuthScheme; import software.amazon.awssdk.awscore.endpoints.authscheme.SigV4aAuthScheme; @@ -45,6 +47,8 @@ import software.amazon.awssdk.utils.StringUtils; public final class TestGeneratorUtils { + private static final Logger log = LoggerFactory.getLogger(TestGeneratorUtils.class); + private TestGeneratorUtils() { } @@ -111,16 +115,40 @@ private static void addEndpointAttributeBlock(CodeBlock.Builder builder, String Map knownEndpointAttributes) { if ("authSchemes".equals(attrName)) { addAuthSchemesBlock(builder, attrValue); + } else if ("metricValues".equals(attrName)) { + addMetricValuesBlock(builder, attrValue); } else if (knownEndpointAttributes.containsKey(attrName)) { addAttributeBlock(builder, attrValue, knownEndpointAttributes.get(attrName)); } else { - throw new RuntimeException( - String.format("Encountered unknown expected endpoint attribute: %s. Known attributes: %s.", + log.warn("Ignoring unknown expected endpoint attribute: {}. Known attributes: {}.", attrName, - knownEndpointAttributes)); + knownEndpointAttributes); } } + private static void addMetricValuesBlock(CodeBlock.Builder builder, TreeNode attrValue) { + CodeBlock keyExpr = CodeBlock.builder() + .add("$T.METRIC_VALUES", AwsEndpointAttribute.class) + .build(); + + CodeBlock.Builder schemesListExpr = CodeBlock.builder() + .add("$T.asList(", Arrays.class); + + JrsArray schemesArray = (JrsArray) attrValue; + + Iterator elementsIter = schemesArray.elements(); + while (elementsIter.hasNext()) { + schemesListExpr.add("$S", elementsIter.next().asText()); + + if (elementsIter.hasNext()) { + schemesListExpr.add(","); + } + } + schemesListExpr.add(")"); + + builder.add(".putAttribute($L, $L)", keyExpr, schemesListExpr.build()); + } + private static void addAttributeBlock(CodeBlock.Builder builder, TreeNode attrValue, KeyTypePair keyType) { CodeBlock keyExpr = CodeBlock.builder() .add("$L", keyType.getKey()) diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/CodeGeneratorVisitor.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/CodeGeneratorVisitor.java index 72f8b28a1fa2..daf0d5ff7f39 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/CodeGeneratorVisitor.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/CodeGeneratorVisitor.java @@ -353,6 +353,8 @@ public Void visitPropertiesExpression(PropertiesExpression e) { properties.forEach((k, v) -> { if ("authSchemes".equals(k)) { addAuthSchemesBlock(v); + } else if ("metricValues".equals(k)) { + addMetricValuesBlock(v); } else if (knownEndpointAttributes.containsKey(k)) { addAttributeBlock(k, v); } else { @@ -436,6 +438,12 @@ private ClassName authSchemeClass(String name) { } } + private void addMetricValuesBlock(RuleExpression v) { + builder.add(".putAttribute($T.METRIC_VALUES, ", AwsEndpointAttribute.class); + v.accept(this); + builder.add(")"); + } + private void addAttributeBlock(String k, RuleExpression v) { KeyTypePair keyType = knownEndpointAttributes.get(k); ClassConstant classConstant = parseClassConstant(keyType.getKey()); diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/ClientTestModels.java b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/ClientTestModels.java index 8a46d82291cc..f46dc96b4f93 100644 --- a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/ClientTestModels.java +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/ClientTestModels.java @@ -192,7 +192,27 @@ public static IntermediateModel queryServiceModelsWithUnknownEndpointProperties( File endpointRuleSetModel = new File(ClientTestModels.class.getResource("client/c2j/query/endpoint-rule-set-unknown-properties.json").getFile()); File endpointTestsModel = - new File(ClientTestModels.class.getResource("client/c2j/query/endpoint-tests.json").getFile()); + new File(ClientTestModels.class.getResource("client/c2j/query/endpoint-tests-unknown-properties.json").getFile()); + + C2jModels models = C2jModels + .builder() + .serviceModel(getServiceModel(serviceModel)) + .waitersModel(getWaiters(waitersModel)) + .customizationConfig(CustomizationConfig.create()) + .endpointRuleSetModel(getEndpointRuleSet(endpointRuleSetModel)) + .endpointTestSuiteModel(getEndpointTestSuite(endpointTestsModel)) + .build(); + + return new IntermediateModelBuilder(models).build(); + } + + public static IntermediateModel queryServiceModelsWithUnknownEndpointMetricValues() { + File serviceModel = new File(ClientTestModels.class.getResource("client/c2j/query/service-2.json").getFile()); + File waitersModel = new File(ClientTestModels.class.getResource("client/c2j/query/waiters-2.json").getFile()); + File endpointRuleSetModel = + new File(ClientTestModels.class.getResource("client/c2j/query/endpoint-rule-set-metric-values.json").getFile()); + File endpointTestsModel = + new File(ClientTestModels.class.getResource("client/c2j/query/endpoint-tests-metric-values.json").getFile()); C2jModels models = C2jModels .builder() diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules/EndpointProviderCompiledRulesClassSpecTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules/EndpointProviderCompiledRulesClassSpecTest.java index 5bc9a1f74d75..abf2ed3e3e22 100644 --- a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules/EndpointProviderCompiledRulesClassSpecTest.java +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules/EndpointProviderCompiledRulesClassSpecTest.java @@ -51,4 +51,11 @@ void endpointProviderClassWithUriCache() { new EndpointProviderSpec2(ClientTestModels.queryServiceModelsWithUriCache()); assertThat(endpointProviderSpec, generatesTo("endpoint-provider-uri-cache-class.java")); } + + @Test + void endpointProviderClassWithMetricValues() { + ClassSpec endpointProviderSpec = + new EndpointProviderSpec2(ClientTestModels.queryServiceModelsWithUnknownEndpointMetricValues()); + assertThat(endpointProviderSpec, generatesTo("endpoint-provider-metric-values-class.java")); + } } diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules/EndpointRulesClientTestSpecTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules/EndpointRulesClientTestSpecTest.java index 82eeb7bdc586..8623366020f2 100644 --- a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules/EndpointRulesClientTestSpecTest.java +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules/EndpointRulesClientTestSpecTest.java @@ -34,4 +34,16 @@ public void endpointProviderTestClassWithStringArray() { ClassSpec endpointProviderSpec = new EndpointProviderTestSpec(ClientTestModels.stringArrayServiceModels()); assertThat(endpointProviderSpec, generatesTo("endpoint-rules-stringarray-test-class.java")); } + + @Test + public void endpointProviderTestClassWithUnknownProperties() { + ClassSpec endpointProviderSpec = new EndpointProviderTestSpec(ClientTestModels.queryServiceModelsWithUnknownEndpointProperties()); + assertThat(endpointProviderSpec, generatesTo("endpoint-rules-unknown-property-test-class.java")); + } + + @Test + public void endpointProviderTestClassWithMetricValues() { + ClassSpec endpointProviderSpec = new EndpointProviderTestSpec(ClientTestModels.queryServiceModelsWithUnknownEndpointMetricValues()); + assertThat(endpointProviderSpec, generatesTo("endpoint-rules-metric-values-test-class.java")); + } } diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/query/endpoint-rule-set-metric-values.json b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/query/endpoint-rule-set-metric-values.json new file mode 100644 index 000000000000..cd81ea911199 --- /dev/null +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/query/endpoint-rule-set-metric-values.json @@ -0,0 +1,366 @@ +{ + "version": "1.2", + "serviceId": "query", + "parameters": { + "region": { + "type": "string", + "builtIn": "AWS::Region", + "required": true, + "documentation": "The region to send requests to" + }, + "useDualStackEndpoint": { + "type": "boolean", + "builtIn": "AWS::UseDualStack" + }, + "useFIPSEndpoint": { + "type": "boolean", + "builtIn": "AWS::UseFIPS" + }, + "AccountId": { + "type": "String", + "builtIn": "AWS::Auth::AccountId" + }, + "AccountIdEndpointMode": { + "type": "String", + "builtIn": "AWS::Auth::AccountIdEndpointMode" + }, + "listOfStrings": { + "type": "StringArray", + "required": false + }, + "defaultListOfStrings": { + "type": "stringarray", + "default": [ + "item1", + "item2", + "item3" + ] + }, + "endpointId": { + "type": "string" + }, + "defaultTrueParam": { + "type": "boolean", + "default": true, + "documentation": "A param that defauls to true" + }, + "defaultStringParam": { + "type": "string", + "default": "hello endpoints" + }, + "deprecatedParam": { + "type": "string", + "deprecated": { + "message": "Don't use!", + "since": "2021-01-01" + } + }, + "booleanContextParam": { + "type": "boolean" + }, + "stringContextParam": { + "type": "string" + }, + "operationContextParam": { + "type": "string" + } + }, + "rules": [ + { + "conditions": [ + { + "fn": "aws.partition", + "argv": [ + { + "ref": "region" + } + ], + "assign": "partitionResult" + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "isSet", + "argv": [ + { + "ref": "endpointId" + } + ] + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "isSet", + "argv": [ + { + "ref": "useFIPSEndpoint" + } + ] + }, + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "useFIPSEndpoint" + }, + true + ] + } + ], + "error": "FIPS endpoints not supported with multi-region endpoints", + "type": "error" + }, + { + "conditions": [ + { + "fn": "not", + "argv": [ + { + "fn": "isSet", + "argv": [ + { + "ref": "useFIPSEndpoint" + } + ] + } + ] + }, + { + "fn": "isSet", + "argv": [ + { + "ref": "useDualStackEndpoint" + } + ] + }, + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "useDualStackEndpoint" + }, + true + ] + } + ], + "endpoint": { + "url": "https://{endpointId}.query.{partitionResult#dualStackDnsSuffix}", + "properties": { + "authSchemes": [ + { + "name": "sigv4a", + "signingName": "query", + "signingRegionSet": ["*"] + } + ] + } + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{endpointId}.query.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "name": "sigv4a", + "signingName": "query", + "signingRegionSet": ["*"] + } + ], + "metricValues": ["1", "2"] + } + }, + "type": "endpoint" + } + ], + "type": "tree" + }, + { + "conditions": [ + { + "fn": "isValidHostLabel", + "argv": [ + { + "ref": "region" + }, + false + ] + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "isSet", + "argv": [ + { + "ref": "useFIPSEndpoint" + } + ] + }, + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "useFIPSEndpoint" + }, + true + ] + }, + { + "fn": "not", + "argv": [ + { + "fn": "isSet", + "argv": [ + { + "ref": "useDualStackEndpoint" + } + ] + } + ] + } + ], + "endpoint": { + "url": "https://query-fips.{region}.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "name": "sigv4a", + "signingName": "query", + "signingRegionSet": ["*"] + } + ] + } + }, + "type": "endpoint" + }, + { + "conditions": [ + { + "fn": "isSet", + "argv": [ + { + "ref": "useDualStackEndpoint" + } + ] + }, + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "useDualStackEndpoint" + }, + true + ] + }, + { + "fn": "not", + "argv": [ + { + "fn": "isSet", + "argv": [ + { + "ref": "useFIPSEndpoint" + } + ] + } + ] + } + ], + "endpoint": { + "url": "https://query.{region}.{partitionResult#dualStackDnsSuffix}", + "properties": { + "authSchemes": [ + { + "name": "sigv4a", + "signingName": "query", + "signingRegionSet": ["*"] + }, + { + "name": "sigv4", + "signingName": "query", + "signingRegion": "{region}" + } + ] + } + }, + "type": "endpoint" + }, + { + "conditions": [ + { + "fn": "isSet", + "argv": [ + { + "ref": "useDualStackEndpoint" + } + ] + }, + { + "fn": "isSet", + "argv": [ + { + "ref": "useFIPSEndpoint" + } + ] + }, + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "useDualStackEndpoint" + }, + true + ] + }, + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "useFIPSEndpoint" + }, + true + ] + } + ], + "endpoint": { + "url": "https://query-fips.{region}.{partitionResult#dualStackDnsSuffix}", + "properties": { + "authSchemes": [ + { + "name": "sigv4a", + "signingName": "query", + "signingRegionSet": ["*"] + } + ] + } + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://query.{region}.{partitionResult#dnsSuffix}" + }, + "type": "endpoint" + } + ], + "type": "tree" + }, + { + "conditions": [], + "error": "{region} is not a valid HTTP host-label", + "type": "error" + } + ], + "type": "tree" + } + ] +} diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/query/endpoint-tests-metric-values.json b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/query/endpoint-tests-metric-values.json new file mode 100644 index 000000000000..da4e041a1861 --- /dev/null +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/query/endpoint-tests-metric-values.json @@ -0,0 +1,200 @@ +{ + "testCases": [ + { + "documentation": "test case 1", + "params": { + "region": "us-east-1" + }, + "expect": { + "endpoint": { + "url": "https://myservice.aws", + "properties": { + "metricValues": ["1", "2"] + } + } + } + }, + { + "documentation": "test case 2", + "params": { + "region": "us-east-1", + "booleanContextParam": true, + "stringContextParam": "this is a test" + }, + "expect": { + "endpoint": { + "url": "https://myservice.aws" + } + } + }, + { + "documentation": "test case 3", + "params": { + "region": "us-east-1" + }, + "operationInputs": [ + { + "operationName": "OperationWithContextParam", + "operationParams": { + "StringMember": "this is a test" + } + } + ], + "expect": { + "endpoint": { + "url": "https://myservice.aws" + } + } + }, + { + "documentation": "test case 4", + "params": { + "region": "us-east-6" + }, + "operationInputs": [ + { + "operationName": "OperationWithContextParam", + "operationParams": { + "StringMember": "this is a test" + } + } + ], + "expect": { + "endpoint": { + "url": "https://myservice.aws" + } + } + }, + { + "documentation": "test case 5", + "operationInputs": [ + { + "operationName": "OperationWithContextParam", + "operationParams": { + "StringMember": "this is a test with AccountId and AccountIdEndpointMode" + }, + "builtInParams": { + "AWS::Region": "us-east-5", + "AWS::Auth::AccountId": "012345678901", + "AWS::Auth::AccountIdEndpointMode": "required" + } + } + ], + "params": { + "Region": "us-east-5", + "AccountId": "012345678901", + "AccountIdEndpointMode": "required" + }, + "expect": { + "endpoint": { + "url": "https://012345678901.myservice.aws" + } + } + }, + { + "documentation": "test case 6", + "operationInputs": [ + { + "operationName": "OperationWithMapOperationContextParam", + "operationParams": { + "OperationWithMapOperationContextParam": { + "key": { + "S" : "value" + } + } + }, + "builtInParams": { + "AWS::Region": "us-east-5", + "AWS::Auth::AccountId": "012345678901", + "AWS::Auth::AccountIdEndpointMode": "required" + } + } + ], + "params": { + "Region": "us-east-5", + "AccountId": "012345678901", + "AccountIdEndpointMode": "required", + "ArnList": [ + "arn:aws:dynamodb:us-east-6:012345678901:table/table_name" + ] + }, + "expect": { + "endpoint": { + "url": "https://012345678901.myservice.aws" + } + } + }, + { + "documentation": "test case 7", + "operationInputs": [ + { + "operationName": "OperationWithMapOperationContextParam", + "operationParams": { + "OperationWithMapOperationContextParam": { + "key": { + "S" : "value" + }, + "key2": { + "S" : "value2" + } + } + }, + "builtInParams": { + "AWS::Region": "us-east-5", + "AWS::Auth::AccountId": "012345678901", + "AWS::Auth::AccountIdEndpointMode": "required" + } + } + ], + "params": { + "Region": "us-east-5", + "AccountId": "012345678901", + "AccountIdEndpointMode": "required", + "ArnList": [ + "arn:aws:dynamodb:us-east-6:012345678901:table/table_name" + ] + }, + "expect": { + "endpoint": { + "url": "https://012345678901.myservice.aws" + } + } + }, + { + "documentation": "For region us-iso-west-1 with FIPS enabled and DualStack enabled", + "expect": { + "error": "Should have been skipped!" + }, + "params": { + } + }, + { + "documentation": "Has complex operation input", + "expect": { + "error": "Missing info" + }, + "params": { + }, + "operationInputs": [ + { + "operationName": "OperationWithContextParam", + "operationParams": { + "NestedMember": { + "ChecksumMode": "foo" + } + } + } + ] + }, + { + "documentation": "Has has undeclared input parameter", + "expect": { + "error": "Missing info" + }, + "params": { + "NotAParam": "ABC" + } + } + ], + "version": "1.0" +} \ No newline at end of file diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/query/endpoint-tests-unknown-properties.json b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/query/endpoint-tests-unknown-properties.json new file mode 100644 index 000000000000..1db72731a2d1 --- /dev/null +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/query/endpoint-tests-unknown-properties.json @@ -0,0 +1,200 @@ +{ + "testCases": [ + { + "documentation": "test case 1", + "params": { + "region": "us-east-1" + }, + "expect": { + "endpoint": { + "url": "https://myservice.aws", + "properties": { + "unknownProperty": "value" + } + } + } + }, + { + "documentation": "test case 2", + "params": { + "region": "us-east-1", + "booleanContextParam": true, + "stringContextParam": "this is a test" + }, + "expect": { + "endpoint": { + "url": "https://myservice.aws" + } + } + }, + { + "documentation": "test case 3", + "params": { + "region": "us-east-1" + }, + "operationInputs": [ + { + "operationName": "OperationWithContextParam", + "operationParams": { + "StringMember": "this is a test" + } + } + ], + "expect": { + "endpoint": { + "url": "https://myservice.aws" + } + } + }, + { + "documentation": "test case 4", + "params": { + "region": "us-east-6" + }, + "operationInputs": [ + { + "operationName": "OperationWithContextParam", + "operationParams": { + "StringMember": "this is a test" + } + } + ], + "expect": { + "endpoint": { + "url": "https://myservice.aws" + } + } + }, + { + "documentation": "test case 5", + "operationInputs": [ + { + "operationName": "OperationWithContextParam", + "operationParams": { + "StringMember": "this is a test with AccountId and AccountIdEndpointMode" + }, + "builtInParams": { + "AWS::Region": "us-east-5", + "AWS::Auth::AccountId": "012345678901", + "AWS::Auth::AccountIdEndpointMode": "required" + } + } + ], + "params": { + "Region": "us-east-5", + "AccountId": "012345678901", + "AccountIdEndpointMode": "required" + }, + "expect": { + "endpoint": { + "url": "https://012345678901.myservice.aws" + } + } + }, + { + "documentation": "test case 6", + "operationInputs": [ + { + "operationName": "OperationWithMapOperationContextParam", + "operationParams": { + "OperationWithMapOperationContextParam": { + "key": { + "S" : "value" + } + } + }, + "builtInParams": { + "AWS::Region": "us-east-5", + "AWS::Auth::AccountId": "012345678901", + "AWS::Auth::AccountIdEndpointMode": "required" + } + } + ], + "params": { + "Region": "us-east-5", + "AccountId": "012345678901", + "AccountIdEndpointMode": "required", + "ArnList": [ + "arn:aws:dynamodb:us-east-6:012345678901:table/table_name" + ] + }, + "expect": { + "endpoint": { + "url": "https://012345678901.myservice.aws" + } + } + }, + { + "documentation": "test case 7", + "operationInputs": [ + { + "operationName": "OperationWithMapOperationContextParam", + "operationParams": { + "OperationWithMapOperationContextParam": { + "key": { + "S" : "value" + }, + "key2": { + "S" : "value2" + } + } + }, + "builtInParams": { + "AWS::Region": "us-east-5", + "AWS::Auth::AccountId": "012345678901", + "AWS::Auth::AccountIdEndpointMode": "required" + } + } + ], + "params": { + "Region": "us-east-5", + "AccountId": "012345678901", + "AccountIdEndpointMode": "required", + "ArnList": [ + "arn:aws:dynamodb:us-east-6:012345678901:table/table_name" + ] + }, + "expect": { + "endpoint": { + "url": "https://012345678901.myservice.aws" + } + } + }, + { + "documentation": "For region us-iso-west-1 with FIPS enabled and DualStack enabled", + "expect": { + "error": "Should have been skipped!" + }, + "params": { + } + }, + { + "documentation": "Has complex operation input", + "expect": { + "error": "Missing info" + }, + "params": { + }, + "operationInputs": [ + { + "operationName": "OperationWithContextParam", + "operationParams": { + "NestedMember": { + "ChecksumMode": "foo" + } + } + } + ] + }, + { + "documentation": "Has has undeclared input parameter", + "expect": { + "error": "Missing info" + }, + "params": { + "NotAParam": "ABC" + } + } + ], + "version": "1.0" +} \ No newline at end of file diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolve-interceptor-preSra.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolve-interceptor-preSra.java index 5ff33962d6ae..7f71d8ec6856 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolve-interceptor-preSra.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolve-interceptor-preSra.java @@ -95,6 +95,7 @@ public SdkRequest modifyRequest(Context.ModifyRequest context, ExecutionAttribut result = SignerOverrideUtils.overrideSignerIfNotOverridden(result, executionAttributes, signerProvider); } executionAttributes.putAttribute(SdkInternalExecutionAttribute.RESOLVED_ENDPOINT, endpoint); + setMetricValues(endpoint, executionAttributes); return result; } catch (CompletionException e) { Throwable cause = e.getCause(); @@ -285,4 +286,10 @@ private static String recordAccountIdEndpointMode(ExecutionAttributes executionA m -> executionAttributes.getAttribute(SdkInternalExecutionAttribute.BUSINESS_METRICS).addMetric(m)); return mode.name().toLowerCase(); } + + private void setMetricValues(Endpoint endpoint, ExecutionAttributes executionAttributes) { + if (endpoint.attribute(AwsEndpointAttribute.METRIC_VALUES) != null) { + executionAttributes.getOptionalAttribute(SdkInternalExecutionAttribute.BUSINESS_METRICS).ifPresent(metrics -> endpoint.attribute(AwsEndpointAttribute.METRIC_VALUES).forEach(v -> metrics.addMetric(v))); + } + } } diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolve-interceptor-with-endpointsbasedauth.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolve-interceptor-with-endpointsbasedauth.java index f658a707b0ef..80bc25041da3 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolve-interceptor-with-endpointsbasedauth.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolve-interceptor-with-endpointsbasedauth.java @@ -87,6 +87,7 @@ public SdkRequest modifyRequest(Context.ModifyRequest context, ExecutionAttribut executionAttributes.putAttribute(SdkInternalExecutionAttribute.SELECTED_AUTH_SCHEME, selectedAuthScheme); } executionAttributes.putAttribute(SdkInternalExecutionAttribute.RESOLVED_ENDPOINT, endpoint); + setMetricValues(endpoint, executionAttributes); return result; } catch (CompletionException e) { Throwable cause = e.getCause(); @@ -261,4 +262,10 @@ private static String recordAccountIdEndpointMode(ExecutionAttributes executionA m -> executionAttributes.getAttribute(SdkInternalExecutionAttribute.BUSINESS_METRICS).addMetric(m)); return mode.name().toLowerCase(); } + + private void setMetricValues(Endpoint endpoint, ExecutionAttributes executionAttributes) { + if (endpoint.attribute(AwsEndpointAttribute.METRIC_VALUES) != null) { + executionAttributes.getOptionalAttribute(SdkInternalExecutionAttribute.BUSINESS_METRICS).ifPresent(metrics -> endpoint.attribute(AwsEndpointAttribute.METRIC_VALUES).forEach(v -> metrics.addMetric(v))); + } + } } diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolve-interceptor-with-multiauthsigv4a.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolve-interceptor-with-multiauthsigv4a.java index 1333f274df20..858d39fc7a69 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolve-interceptor-with-multiauthsigv4a.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolve-interceptor-with-multiauthsigv4a.java @@ -76,6 +76,7 @@ public SdkRequest modifyRequest(Context.ModifyRequest context, ExecutionAttribut executionAttributes.putAttribute(SdkInternalExecutionAttribute.SELECTED_AUTH_SCHEME, selectedAuthScheme); } executionAttributes.putAttribute(SdkInternalExecutionAttribute.RESOLVED_ENDPOINT, endpoint); + setMetricValues(endpoint, executionAttributes); return result; } catch (CompletionException e) { Throwable cause = e.getCause(); @@ -165,4 +166,9 @@ private static Optional hostPrefix(String operationName, SdkRequest requ return Optional.empty(); } + private void setMetricValues(Endpoint endpoint, ExecutionAttributes executionAttributes) { + if (endpoint.attribute(AwsEndpointAttribute.METRIC_VALUES) != null) { + executionAttributes.getOptionalAttribute(SdkInternalExecutionAttribute.BUSINESS_METRICS).ifPresent(metrics -> endpoint.attribute(AwsEndpointAttribute.METRIC_VALUES).forEach(v -> metrics.addMetric(v))); + } + } } diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolve-interceptor.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolve-interceptor.java index 1255cc65c777..2b194a0d3548 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolve-interceptor.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolve-interceptor.java @@ -78,6 +78,7 @@ public SdkRequest modifyRequest(Context.ModifyRequest context, ExecutionAttribut executionAttributes.putAttribute(SdkInternalExecutionAttribute.SELECTED_AUTH_SCHEME, selectedAuthScheme); } executionAttributes.putAttribute(SdkInternalExecutionAttribute.RESOLVED_ENDPOINT, endpoint); + setMetricValues(endpoint, executionAttributes); return result; } catch (CompletionException e) { Throwable cause = e.getCause(); @@ -259,4 +260,10 @@ private static String recordAccountIdEndpointMode(ExecutionAttributes executionA m -> executionAttributes.getAttribute(SdkInternalExecutionAttribute.BUSINESS_METRICS).addMetric(m)); return mode.name().toLowerCase(); } + + private void setMetricValues(Endpoint endpoint, ExecutionAttributes executionAttributes) { + if (endpoint.attribute(AwsEndpointAttribute.METRIC_VALUES) != null) { + executionAttributes.getOptionalAttribute(SdkInternalExecutionAttribute.BUSINESS_METRICS).ifPresent(metrics -> endpoint.attribute(AwsEndpointAttribute.METRIC_VALUES).forEach(v -> metrics.addMetric(v))); + } + } } diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-rules-metric-values-test-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-rules-metric-values-test-class.java new file mode 100644 index 000000000000..67576fdf1a95 --- /dev/null +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-rules-metric-values-test-class.java @@ -0,0 +1,87 @@ +package software.amazon.awssdk.services.query.endpoints; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import software.amazon.awssdk.annotations.Generated; +import software.amazon.awssdk.awscore.endpoints.AwsEndpointAttribute; +import software.amazon.awssdk.core.rules.testing.BaseEndpointProviderTest; +import software.amazon.awssdk.core.rules.testing.EndpointProviderTestCase; +import software.amazon.awssdk.core.rules.testing.model.Expect; +import software.amazon.awssdk.endpoints.Endpoint; +import software.amazon.awssdk.regions.Region; + +@Generated("software.amazon.awssdk:codegen") +public class QueryEndpointProviderTests extends BaseEndpointProviderTest { + private static final QueryEndpointProvider PROVIDER = QueryEndpointProvider.defaultProvider(); + + @MethodSource("testCases") + @ParameterizedTest + public void resolvesCorrectEndpoint(EndpointProviderTestCase tc) { + verify(tc); + } + + private static List testCases() { + List testCases = new ArrayList<>(); + testCases.add(new EndpointProviderTestCase(() -> { + QueryEndpointParams.Builder builder = QueryEndpointParams.builder(); + builder.region(Region.of("us-east-1")); + return PROVIDER.resolveEndpoint(builder.build()).join(); + }, Expect + .builder() + .endpoint( + Endpoint.builder().url(URI.create("https://myservice.aws")) + .putAttribute(AwsEndpointAttribute.METRIC_VALUES, Arrays.asList("1", "2")).build()).build())); + testCases.add(new EndpointProviderTestCase(() -> { + QueryEndpointParams.Builder builder = QueryEndpointParams.builder(); + builder.region(Region.of("us-east-1")); + builder.booleanContextParam(true); + builder.stringContextParam("this is a test"); + return PROVIDER.resolveEndpoint(builder.build()).join(); + }, Expect.builder().endpoint(Endpoint.builder().url(URI.create("https://myservice.aws")).build()).build())); + testCases.add(new EndpointProviderTestCase(() -> { + QueryEndpointParams.Builder builder = QueryEndpointParams.builder(); + builder.region(Region.of("us-east-1")); + return PROVIDER.resolveEndpoint(builder.build()).join(); + }, Expect.builder().endpoint(Endpoint.builder().url(URI.create("https://myservice.aws")).build()).build())); + testCases.add(new EndpointProviderTestCase(() -> { + QueryEndpointParams.Builder builder = QueryEndpointParams.builder(); + builder.region(Region.of("us-east-6")); + return PROVIDER.resolveEndpoint(builder.build()).join(); + }, Expect.builder().endpoint(Endpoint.builder().url(URI.create("https://myservice.aws")).build()).build())); + testCases.add(new EndpointProviderTestCase(() -> { + QueryEndpointParams.Builder builder = QueryEndpointParams.builder(); + builder.accountId("012345678901"); + builder.accountIdEndpointMode("required"); + return PROVIDER.resolveEndpoint(builder.build()).join(); + }, Expect.builder().endpoint(Endpoint.builder().url(URI.create("https://012345678901.myservice.aws")).build()).build())); + testCases.add(new EndpointProviderTestCase(() -> { + QueryEndpointParams.Builder builder = QueryEndpointParams.builder(); + builder.accountId("012345678901"); + builder.accountIdEndpointMode("required"); + return PROVIDER.resolveEndpoint(builder.build()).join(); + }, Expect.builder().endpoint(Endpoint.builder().url(URI.create("https://012345678901.myservice.aws")).build()).build())); + testCases.add(new EndpointProviderTestCase(() -> { + QueryEndpointParams.Builder builder = QueryEndpointParams.builder(); + builder.accountId("012345678901"); + builder.accountIdEndpointMode("required"); + return PROVIDER.resolveEndpoint(builder.build()).join(); + }, Expect.builder().endpoint(Endpoint.builder().url(URI.create("https://012345678901.myservice.aws")).build()).build())); + testCases.add(new EndpointProviderTestCase(() -> { + QueryEndpointParams.Builder builder = QueryEndpointParams.builder(); + return PROVIDER.resolveEndpoint(builder.build()).join(); + }, Expect.builder().error("Should have been skipped!").build())); + testCases.add(new EndpointProviderTestCase(() -> { + QueryEndpointParams.Builder builder = QueryEndpointParams.builder(); + return PROVIDER.resolveEndpoint(builder.build()).join(); + }, Expect.builder().error("Missing info").build())); + testCases.add(new EndpointProviderTestCase(() -> { + QueryEndpointParams.Builder builder = QueryEndpointParams.builder(); + return PROVIDER.resolveEndpoint(builder.build()).join(); + }, Expect.builder().error("Missing info").build())); + return testCases; + } +} diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-rules-unknown-property-test-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-rules-unknown-property-test-class.java new file mode 100644 index 000000000000..b471a05fad5c --- /dev/null +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-rules-unknown-property-test-class.java @@ -0,0 +1,83 @@ +/* + * 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.query.endpoints; + +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import software.amazon.awssdk.annotations.Generated; +import software.amazon.awssdk.core.rules.testing.BaseEndpointProviderTest; +import software.amazon.awssdk.core.rules.testing.EndpointProviderTestCase; +import software.amazon.awssdk.core.rules.testing.model.Expect; +import software.amazon.awssdk.endpoints.Endpoint; + +@Generated("software.amazon.awssdk:codegen") +public class QueryEndpointProviderTests extends BaseEndpointProviderTest { + private static final QueryEndpointProvider PROVIDER = QueryEndpointProvider.defaultProvider(); + + @MethodSource("testCases") + @ParameterizedTest + public void resolvesCorrectEndpoint(EndpointProviderTestCase tc) { + verify(tc); + } + + private static List testCases() { + List testCases = new ArrayList<>(); + testCases.add(new EndpointProviderTestCase(() -> { + QueryEndpointParams.Builder builder = QueryEndpointParams.builder(); + return PROVIDER.resolveEndpoint(builder.build()).join(); + }, Expect.builder().endpoint(Endpoint.builder().url(URI.create("https://myservice.aws")).build()).build())); + testCases.add(new EndpointProviderTestCase(() -> { + QueryEndpointParams.Builder builder = QueryEndpointParams.builder(); + return PROVIDER.resolveEndpoint(builder.build()).join(); + }, Expect.builder().endpoint(Endpoint.builder().url(URI.create("https://myservice.aws")).build()).build())); + testCases.add(new EndpointProviderTestCase(() -> { + QueryEndpointParams.Builder builder = QueryEndpointParams.builder(); + return PROVIDER.resolveEndpoint(builder.build()).join(); + }, Expect.builder().endpoint(Endpoint.builder().url(URI.create("https://myservice.aws")).build()).build())); + testCases.add(new EndpointProviderTestCase(() -> { + QueryEndpointParams.Builder builder = QueryEndpointParams.builder(); + return PROVIDER.resolveEndpoint(builder.build()).join(); + }, Expect.builder().endpoint(Endpoint.builder().url(URI.create("https://myservice.aws")).build()).build())); + testCases.add(new EndpointProviderTestCase(() -> { + QueryEndpointParams.Builder builder = QueryEndpointParams.builder(); + return PROVIDER.resolveEndpoint(builder.build()).join(); + }, Expect.builder().endpoint(Endpoint.builder().url(URI.create("https://012345678901.myservice.aws")).build()).build())); + testCases.add(new EndpointProviderTestCase(() -> { + QueryEndpointParams.Builder builder = QueryEndpointParams.builder(); + return PROVIDER.resolveEndpoint(builder.build()).join(); + }, Expect.builder().endpoint(Endpoint.builder().url(URI.create("https://012345678901.myservice.aws")).build()).build())); + testCases.add(new EndpointProviderTestCase(() -> { + QueryEndpointParams.Builder builder = QueryEndpointParams.builder(); + return PROVIDER.resolveEndpoint(builder.build()).join(); + }, Expect.builder().endpoint(Endpoint.builder().url(URI.create("https://012345678901.myservice.aws")).build()).build())); + testCases.add(new EndpointProviderTestCase(() -> { + QueryEndpointParams.Builder builder = QueryEndpointParams.builder(); + return PROVIDER.resolveEndpoint(builder.build()).join(); + }, Expect.builder().error("Should have been skipped!").build())); + testCases.add(new EndpointProviderTestCase(() -> { + QueryEndpointParams.Builder builder = QueryEndpointParams.builder(); + return PROVIDER.resolveEndpoint(builder.build()).join(); + }, Expect.builder().error("Missing info").build())); + testCases.add(new EndpointProviderTestCase(() -> { + QueryEndpointParams.Builder builder = QueryEndpointParams.builder(); + return PROVIDER.resolveEndpoint(builder.build()).join(); + }, Expect.builder().error("Missing info").build())); + return testCases; + } +} diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/endpoint-provider-metric-values-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/endpoint-provider-metric-values-class.java new file mode 100644 index 000000000000..6c854826e953 --- /dev/null +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/endpoint-provider-metric-values-class.java @@ -0,0 +1,136 @@ +package software.amazon.awssdk.services.query.endpoints.internal; + +import java.net.URI; +import java.util.Arrays; +import java.util.concurrent.CompletableFuture; +import software.amazon.awssdk.annotations.Generated; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.awscore.endpoints.AwsEndpointAttribute; +import software.amazon.awssdk.awscore.endpoints.authscheme.SigV4AuthScheme; +import software.amazon.awssdk.awscore.endpoints.authscheme.SigV4aAuthScheme; +import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.endpoints.Endpoint; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.query.endpoints.QueryEndpointParams; +import software.amazon.awssdk.services.query.endpoints.QueryEndpointProvider; +import software.amazon.awssdk.utils.CompletableFutureUtils; +import software.amazon.awssdk.utils.Validate; + +@Generated("software.amazon.awssdk:codegen") +@SdkInternalApi +public final class DefaultQueryEndpointProvider implements QueryEndpointProvider { + @Override + public CompletableFuture resolveEndpoint(QueryEndpointParams params) { + Validate.notNull(params.region(), "Parameter 'region' must not be null"); + try { + Region region = params.region(); + String regionId = region == null ? null : region.id(); + RuleResult result = endpointRule0(params, regionId); + if (result.canContinue()) { + throw SdkClientException.create("Rule engine did not reach an error or endpoint result"); + } + if (result.isError()) { + String errorMsg = result.error(); + if (errorMsg.contains("Invalid ARN") && errorMsg.contains(":s3:::")) { + errorMsg += ". Use the bucket name instead of simple bucket ARNs in GetBucketLocationRequest."; + } + throw SdkClientException.create(errorMsg); + } + return CompletableFuture.completedFuture(result.endpoint()); + } catch (Exception error) { + return CompletableFutureUtils.failedFuture(error); + } + } + + private static RuleResult endpointRule0(QueryEndpointParams params, String region) { + return endpointRule1(params, region); + } + + private static RuleResult endpointRule1(QueryEndpointParams params, String region) { + RulePartition partitionResult = RulesFunctions.awsPartition(region); + if (partitionResult != null) { + RuleResult result = endpointRule2(params, partitionResult); + if (result.isResolved()) { + return result; + } + result = endpointRule6(params, region, partitionResult); + if (result.isResolved()) { + return result; + } + return RuleResult.error(region + " is not a valid HTTP host-label"); + } + return RuleResult.carryOn(); + } + + private static RuleResult endpointRule2(QueryEndpointParams params, RulePartition partitionResult) { + if (params.endpointId() != null) { + if (params.useFipsEndpoint() != null && params.useFipsEndpoint()) { + return RuleResult.error("FIPS endpoints not supported with multi-region endpoints"); + } + if (params.useFipsEndpoint() == null && params.useDualStackEndpoint() != null && params.useDualStackEndpoint()) { + return RuleResult.endpoint(Endpoint + .builder() + .url(URI.create("https://" + params.endpointId() + ".query." + partitionResult.dualStackDnsSuffix())) + .putAttribute( + AwsEndpointAttribute.AUTH_SCHEMES, + Arrays.asList(SigV4aAuthScheme.builder().signingName("query") + .signingRegionSet(Arrays.asList("*")).build())).build()); + } + return RuleResult.endpoint(Endpoint + .builder() + .url(URI.create("https://" + params.endpointId() + ".query." + partitionResult.dnsSuffix())) + .putAttribute( + AwsEndpointAttribute.AUTH_SCHEMES, + Arrays.asList(SigV4aAuthScheme.builder().signingName("query").signingRegionSet(Arrays.asList("*")) + .build())).putAttribute(AwsEndpointAttribute.METRIC_VALUES, Arrays.asList("1", "2")).build()); + } + return RuleResult.carryOn(); + } + + private static RuleResult endpointRule6(QueryEndpointParams params, String region, RulePartition partitionResult) { + if (RulesFunctions.isValidHostLabel(region, false)) { + if (params.useFipsEndpoint() != null && params.useFipsEndpoint() && params.useDualStackEndpoint() == null) { + return RuleResult.endpoint(Endpoint + .builder() + .url(URI.create("https://query-fips." + region + "." + partitionResult.dnsSuffix())) + .putAttribute( + AwsEndpointAttribute.AUTH_SCHEMES, + Arrays.asList(SigV4aAuthScheme.builder().signingName("query") + .signingRegionSet(Arrays.asList("*")).build())).build()); + } + if (params.useDualStackEndpoint() != null && params.useDualStackEndpoint() && params.useFipsEndpoint() == null) { + return RuleResult.endpoint(Endpoint + .builder() + .url(URI.create("https://query." + region + "." + partitionResult.dualStackDnsSuffix())) + .putAttribute( + AwsEndpointAttribute.AUTH_SCHEMES, + Arrays.asList(SigV4aAuthScheme.builder().signingName("query") + .signingRegionSet(Arrays.asList("*")).build(), + SigV4AuthScheme.builder().signingName("query").signingRegion(region).build())).build()); + } + if (params.useDualStackEndpoint() != null && params.useFipsEndpoint() != null && params.useDualStackEndpoint() + && params.useFipsEndpoint()) { + return RuleResult.endpoint(Endpoint + .builder() + .url(URI.create("https://query-fips." + region + "." + partitionResult.dualStackDnsSuffix())) + .putAttribute( + AwsEndpointAttribute.AUTH_SCHEMES, + Arrays.asList(SigV4aAuthScheme.builder().signingName("query") + .signingRegionSet(Arrays.asList("*")).build())).build()); + } + return RuleResult.endpoint(Endpoint.builder() + .url(URI.create("https://query." + region + "." + partitionResult.dnsSuffix())).build()); + } + return RuleResult.carryOn(); + } + + @Override + public boolean equals(Object rhs) { + return rhs != null && getClass().equals(rhs.getClass()); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } +} diff --git a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/endpoints/AwsEndpointAttribute.java b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/endpoints/AwsEndpointAttribute.java index d55024c2e3ff..1c8ff2fc06ed 100644 --- a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/endpoints/AwsEndpointAttribute.java +++ b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/endpoints/AwsEndpointAttribute.java @@ -33,6 +33,12 @@ public final class AwsEndpointAttribute { public static final EndpointAttributeKey> AUTH_SCHEMES = EndpointAttributeKey.forList("AuthSchemes"); + /** + * Business Metric values to be added to the user-agent for the endpoint. + */ + public static final EndpointAttributeKey> METRIC_VALUES = + EndpointAttributeKey.forList("MetricValues"); + private AwsEndpointAttribute() { } diff --git a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/EndpointMetricValuesTest.java b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/EndpointMetricValuesTest.java new file mode 100644 index 000000000000..1315665f0ef4 --- /dev/null +++ b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/EndpointMetricValuesTest.java @@ -0,0 +1,142 @@ +/* + * 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; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.net.URI; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.awscore.endpoints.AwsEndpointAttribute; +import software.amazon.awssdk.core.interceptor.Context; +import software.amazon.awssdk.core.interceptor.ExecutionAttributes; +import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; +import software.amazon.awssdk.endpoints.Endpoint; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.restjsonendpointproviders.RestJsonEndpointProvidersClient; +import software.amazon.awssdk.services.restjsonendpointproviders.endpoints.RestJsonEndpointProvidersEndpointParams; +import software.amazon.awssdk.services.restjsonendpointproviders.endpoints.RestJsonEndpointProvidersEndpointProvider; + +public class EndpointMetricValuesTest { + private static final String USER_AGENT_HEADER_NAME = "User-Agent"; + private static final StaticCredentialsProvider CREDENTIALS_PROVIDER = + StaticCredentialsProvider.create(AwsBasicCredentials.create("akid", "skid")); + + + private RestJsonEndpointProvidersEndpointProvider mockEndpointProvider; + private CapturingInterceptor capturingInterceptor; + + @BeforeEach + void setup() { + capturingInterceptor = new CapturingInterceptor(); + mockEndpointProvider = mock(RestJsonEndpointProvidersEndpointProvider.class); + } + + @Test + void endpointMetricValuesAreAddedToUserAgent() { + List metricValues = Arrays.asList("O", "K"); + when(mockEndpointProvider.resolveEndpoint(any(RestJsonEndpointProvidersEndpointParams.class))) + .thenReturn(CompletableFuture.completedFuture( + Endpoint.builder() + .url(URI.create("https://my-service.com")) + .putAttribute(AwsEndpointAttribute.METRIC_VALUES, metricValues) + .build())); + + RestJsonEndpointProvidersClient client = + RestJsonEndpointProvidersClient.builder() + .endpointProvider(mockEndpointProvider) + .region(Region.US_WEST_2) + .credentialsProvider(CREDENTIALS_PROVIDER) + .overrideConfiguration(c -> c.addExecutionInterceptor(capturingInterceptor)) + .build(); + + assertThatThrownBy(() -> client.operationWithNoInputOrOutput(r -> { + })).hasMessageContaining("short-circuit"); + + String userAgent = assertAndGetUserAgentString(); + Matcher businessMetricMatcher = Pattern.compile("m/([^\\s]+)").matcher(userAgent); + assertTrue(businessMetricMatcher.find()); + assertNotNull(businessMetricMatcher.group(1)); + Set metrics = new HashSet<>(Arrays.asList((businessMetricMatcher.group(1).split(",")))); + assertTrue(metrics.containsAll(metricValues)); + } + + @Test + void endpointMetricValuesDoesNotFailOnEmptyList() { + List metricValues = Collections.emptyList(); + when(mockEndpointProvider.resolveEndpoint(any(RestJsonEndpointProvidersEndpointParams.class))) + .thenReturn(CompletableFuture.completedFuture( + Endpoint.builder() + .url(URI.create("https://my-service.com")) + .putAttribute(AwsEndpointAttribute.METRIC_VALUES, metricValues) + .build())); + + RestJsonEndpointProvidersClient client = + RestJsonEndpointProvidersClient.builder() + .endpointProvider(mockEndpointProvider) + .region(Region.US_WEST_2) + .credentialsProvider(CREDENTIALS_PROVIDER) + .overrideConfiguration(c -> c.addExecutionInterceptor(capturingInterceptor)) + .build(); + + assertThatThrownBy(() -> client.operationWithNoInputOrOutput(r -> { + })).hasMessageContaining("short-circuit"); + + String userAgent = assertAndGetUserAgentString(); + Matcher businessMetricMatcher = Pattern.compile("m/([^\\s]+)").matcher(userAgent); + assertTrue(businessMetricMatcher.find()); + assertNotNull(businessMetricMatcher.group(1)); + } + + private String assertAndGetUserAgentString() { + Map> headers = capturingInterceptor.context.httpRequest().headers(); + assertThat(headers).containsKey(USER_AGENT_HEADER_NAME); + return headers.get(USER_AGENT_HEADER_NAME).get(0); + } + + private static class CapturingInterceptor implements ExecutionInterceptor { + private Context.BeforeTransmission context; + private ExecutionAttributes executionAttributes; + + @Override + public void beforeTransmission(Context.BeforeTransmission context, ExecutionAttributes executionAttributes) { + this.context = context; + this.executionAttributes = executionAttributes; + throw new RuntimeException("short-circuit"); + } + + public ExecutionAttributes executionAttributes() { + return executionAttributes; + } + } +}