Skip to content

Commit

Permalink
Add docs and validation for endpoint patterns (#2069)
Browse files Browse the repository at this point in the history
* Add docs and validation for endpoint patterns used by standardRegionalEndpoints and standardPartitionalEndpoints

* Fixes from PR

* Add validation for scheme to endpoint patterns

* Update docs

* Add additional url validation on endpoint patterns

* Fixes from PR
  • Loading branch information
alextwoods authored Dec 22, 2023
1 parent 1a858da commit 8fa7d38
Show file tree
Hide file tree
Showing 13 changed files with 252 additions and 30 deletions.
53 changes: 40 additions & 13 deletions docs/source-2.0/aws/aws-endpoints-region.rst
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,10 @@ Summary
that indicates that a service's endpoints should be resolved using the standard AWS regional
patterns:

- Default: ``{service}.{region}.{dnsSuffix}``
- Fips: ``{service}-fips.{region}.{dnsSuffix}``
- Dualstack: ``{service}.{region}.{dualStackDnsSuffix}``
- Fips/Dualstack: ``{service}-fips.{region}.{dualStackDnsSuffix}``
- Default: ``https://{service}.{region}.{dnsSuffix}``
- Fips: ``https://{service}-fips.{region}.{dnsSuffix}``
- Dualstack: ``https://{service}.{region}.{dualStackDnsSuffix}``
- Fips/Dualstack: ``https://{service}-fips.{region}.{dualStackDnsSuffix}``

Trait selector
``service``
Expand Down Expand Up @@ -184,7 +184,7 @@ FIPS endpoints in US GovCloud:
partitionSpecialCases: {
aws-us-gov: [
{
endpoint: "myservice.{region}.{dnsSuffix}",
endpoint: "https://myservice.{region}.{dnsSuffix}",
fips: true
}
]
Expand All @@ -209,7 +209,7 @@ in the given partition. A PartitionSpecialCase object contains the following pro
- Description
* - endpoint
- ``string``
- **Required**. The special-cased endpoint template.
- **Required**. The special-cased :ref:`endpoint pattern <aws.endpoints#endpoint-pattern>`
* - dualStack
- ``boolean``
- When ``true`` the special case will apply to dualstack endpoint variants.
Expand All @@ -232,7 +232,7 @@ A ``RegionSpecialCase`` object contains the following properties:
- Description
* - endpoint
- ``string``
- **Required**. The special-cased endpoint template.
- **Required**. The special-cased :ref:`endpoint pattern <aws.endpoints#endpoint-pattern>`.
* - dualStack
- ``boolean``
- When ``true`` the special case will apply to dualstack endpoint variants.
Expand Down Expand Up @@ -270,8 +270,8 @@ Trait value
* - endpointPatternType
- ``string``
- **Required** The pattern type to use for the partition endpoint. This value can be set to ``service_dnsSuffix`` to
use the ``{service}.{dnsSuffix}`` pattern or ``service_region_dnsSuffix`` to use
``{service}.{region}.{dnsSuffix}``.
use the ``https://{service}.{dnsSuffix}`` pattern or ``service_region_dnsSuffix`` to use
``https://{service}.{region}.{dnsSuffix}``.
* - partitionEndpointSpecialCases
- ``map`` of partition to `PartitionEndpointSpecialCase object`_
- A map of partition to partition endpoint special cases - partitions that do not follow the
Expand All @@ -285,8 +285,8 @@ Partitional services (also known as "global" services) resolve a single endpoint
That single endpoint is located in the partition's ``defaultGlobalRegion``. Partitional
services should follow one of two standard patterns:

- ``service_dnsSuffix``: ``{service}.{dnsSuffix}``
- ``service_region_dnsSuffix``: ``{service}.{region}.{dnsSuffix}``
- ``service_dnsSuffix``: ``https://{service}.{dnsSuffix}``
- ``service_region_dnsSuffix``: ``https://{service}.{region}.{dnsSuffix}``

The following example defines a partitional service that uses ``{service}.{dnsSuffix}``:

Expand All @@ -312,7 +312,7 @@ the ``aws`` partition and uses a non-standard global region in the ``aws-cn`` pa
@standardPartitionalEndpoints(
endpointPatternType: "service_dnsSuffix",
partitionEndpointSpecialCases: {
aws: [{endpoint: "myservice.global.amazonaws.com"}],
aws: [{endpoint: "https://myservice.global.amazonaws.com"}],
aws-cn: [{region: "cn-north-1"}]
}
)
Expand All @@ -334,7 +334,7 @@ A ``PartitionEndpointSpecialCase`` object contains the following properties:
- Description
* - endpoint
- ``string``
- The special-cased endpoint template.
- The special-cased :ref:`endpoint pattern <aws.endpoints#endpoint-pattern>`.
* - region
- ``string``
- Override the ``defaultGlobalRegion`` used in this partition.
Expand Down Expand Up @@ -412,3 +412,30 @@ The following example specifies a service that has standard regional endpoints e
service MyService {
version: "2020-04-02"
}
.. _aws.endpoints#endpoint-pattern:

----------------
Endpoint Pattern
----------------

Endpoint Patterns SHOULD begin with a scheme of either `http` or `https`. When specifying special case endpoints in
:ref:`StandardRegionalEndpoints <aws.endpoints#standardRegionalEndpoints-trait>` or
:ref:`StandardPartitionalEndpoints <aws.endpoints#standardPartitionalEndpoints-trait>` you may use
an endpoint pattern such as ``https://{service}.{region}.{dnsSuffix}`` with the following pattern substitutions:

.. list-table::
:header-rows: 1
:widths: 10 60

* - Pattern
- Description
* - ``{region}``
- The region from the :ref:`AWS::Region built-in <rules-engine-aws-built-ins-region>`.
* - ``{service}``
- The endpoint prefix from the :ref:`service's endpointPrefix <service-endpoint-prefix>`.
* - ``{dnsSuffix}``
- The dns suffix from the :ref:`resolved partition's properties <rules-engine-aws-library-awsPartition-Partition>`.
* - ``{dualStackDnsSuffix}``
- The dualStack dns suffix from the :ref:`resolved partition's properties <rules-engine-aws-library-awsPartition-Partition>`.

Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public enum EndpointPatternType {
SERVICE_DNSSUFFIX("service_dnsSuffix"),

/** An endpoint with pattern `{service}.{region}.{dnsSuffix}`. */
SERVICE_REGION_DNSSUFFI("service_region_dnsSuffix");
SERVICE_REGION_DNSSUFFIX("service_region_dnsSuffix");

private final String name;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package software.amazon.smithy.rulesengine.aws.validators;

import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import software.amazon.smithy.model.FromSourceLocation;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.shapes.ServiceShape;
import software.amazon.smithy.model.validation.AbstractValidator;
import software.amazon.smithy.model.validation.ValidationEvent;
import software.amazon.smithy.rulesengine.aws.traits.PartitionEndpointSpecialCase;
import software.amazon.smithy.rulesengine.aws.traits.PartitionSpecialCase;
import software.amazon.smithy.rulesengine.aws.traits.RegionSpecialCase;
import software.amazon.smithy.rulesengine.aws.traits.StandardPartitionalEndpointsTrait;
import software.amazon.smithy.rulesengine.aws.traits.StandardRegionalEndpointsTrait;
import software.amazon.smithy.utils.SetUtils;

/**
* Validate special case endpoints from endpoint traits that are applied on a service.
*/
public final class AwsSpecialCaseEndpointValidator extends AbstractValidator {

private static final Set<String> SUPPORTED_PATTERNS = SetUtils.of(
"{region}", "{service}", "{dnsSuffix}", "{dualStackDnsSuffix}"
);

private static final Pattern PATTERN = Pattern.compile("\\{[^\\}]*\\}");

@Override
public List<ValidationEvent> validate(Model model) {
List<ValidationEvent> events = new ArrayList<>();
for (ServiceShape serviceShape : model.getServiceShapesWithTrait(StandardRegionalEndpointsTrait.class)) {
events.addAll(validateRegionalEndpointPatterns(serviceShape,
serviceShape.expectTrait(StandardRegionalEndpointsTrait.class)));
}
for (ServiceShape serviceShape : model.getServiceShapesWithTrait(StandardPartitionalEndpointsTrait.class)) {
events.addAll(validatePartitionalEndpointPatterns(serviceShape,
serviceShape.expectTrait(StandardPartitionalEndpointsTrait.class)));
}
return events;
}

private List<ValidationEvent> validateRegionalEndpointPatterns(
ServiceShape serviceShape, StandardRegionalEndpointsTrait regionalEndpoints) {
List<ValidationEvent> events = new ArrayList<>();

for (List<RegionSpecialCase> specialCases : regionalEndpoints.getRegionSpecialCases().values()) {
for (RegionSpecialCase specialCase : specialCases) {
events.addAll(validateEndpointPatterns(
serviceShape, regionalEndpoints, specialCase.getEndpoint()));
}
}

for (List<PartitionSpecialCase> specialCases : regionalEndpoints.getPartitionSpecialCases().values()) {
for (PartitionSpecialCase specialCase : specialCases) {
events.addAll(validateEndpointPatterns(
serviceShape, regionalEndpoints, specialCase.getEndpoint()));
}
}

return events;
}

private List<ValidationEvent> validatePartitionalEndpointPatterns(
ServiceShape serviceShape, StandardPartitionalEndpointsTrait partitionalEndpoints) {

List<ValidationEvent> events = new ArrayList<>();

for (List<PartitionEndpointSpecialCase> specialCases
: partitionalEndpoints.getPartitionEndpointSpecialCases().values()) {
for (PartitionEndpointSpecialCase specialCase : specialCases) {
events.addAll(validateEndpointPatterns(
serviceShape, partitionalEndpoints, specialCase.getEndpoint()));
}
}

return events;
}

private List<ValidationEvent> validateEndpointPatterns(
ServiceShape serviceShape, FromSourceLocation location, String endpoint) {
List<ValidationEvent> events = new ArrayList<>();

Matcher m = PATTERN.matcher(endpoint);
List<String> unsupportedPatterns = new ArrayList<>();
while (m.find()) {
if (!SUPPORTED_PATTERNS.contains(m.group())) {
unsupportedPatterns.add(m.group());
}
}

if (!unsupportedPatterns.isEmpty()) {
events.add(danger(
serviceShape, location,
String.format("Endpoint `%s` contains unsupported patterns: %s",
endpoint, String.join(", ", unsupportedPatterns)),
"UnsupportedEndpointPattern"));
}

if (!(endpoint.startsWith("http://") || endpoint.startsWith("https://"))) {
events.add(danger(
serviceShape, location,
String.format("Endpoint `%s` should start with scheme `http://` or `https://`",
endpoint),
"InvalidEndpointPatternScheme"));
}

if (!isValidURL(endpoint)) {
events.add(error(
serviceShape, location,
String.format("Endpoint `%s` should be a valid URL.",
endpoint),
"InvalidEndpointPatternUrl"));
}

return events;
}

private boolean isValidURL(String endpointPattern) {
String url = endpointPattern
.replace("{", "")
.replace("}", "");
try {
new URL(url).toURI();
return true;
} catch (MalformedURLException e) {
return false;
} catch (URISyntaxException e) {
return false;
}
}
}
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
software.amazon.smithy.rulesengine.aws.validators.RuleSetAwsBuiltInValidator
software.amazon.smithy.rulesengine.aws.validators.AwsSpecialCaseEndpointValidator
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,19 @@ public void loadsFromModel() {

trait = getTraitFromService(model, "ns.foo#Service2");

assertEquals(trait.getEndpointPatternType(), EndpointPatternType.SERVICE_REGION_DNSSUFFI);
assertEquals(trait.getEndpointPatternType(), EndpointPatternType.SERVICE_REGION_DNSSUFFIX);
assertEquals(trait.getPartitionEndpointSpecialCases().size(), 1);

List<PartitionEndpointSpecialCase> cases = trait.getPartitionEndpointSpecialCases().get("aws-us-gov");

PartitionEndpointSpecialCase case1 = cases.get(0);
assertEquals(case1.getEndpoint(), "myservice.{region}.{dnsSuffix}");
assertEquals(case1.getEndpoint(), "https://myservice.{region}.{dnsSuffix}");
assertEquals(case1.getFips(), true);
assertEquals(case1.getRegion(), "us-east-1");
assertNull(case1.getDualStack());

PartitionEndpointSpecialCase case2 = cases.get(1);
assertEquals(case2.getEndpoint(), "myservice.global.amazonaws.com");
assertEquals(case2.getEndpoint(), "https://myservice.global.amazonaws.com");
assertEquals(case2.getDualStack(), true);
assertEquals(case2.getRegion(), "us-west-2");
assertNull(case2.getFips());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,12 @@ public void loadsFromModel() {
List<PartitionSpecialCase> partitionSpecialCases = trait.getPartitionSpecialCases().get("aws-us-gov");

PartitionSpecialCase partitionSpecialCase1 = partitionSpecialCases.get(0);
assertEquals(partitionSpecialCase1.getEndpoint(), "myservice.{region}.{dnsSuffix}");
assertEquals(partitionSpecialCase1.getEndpoint(), "https://myservice.{region}.{dnsSuffix}");
assertEquals(partitionSpecialCase1.getFips(), true);
assertNull(partitionSpecialCase1.getDualStack());

PartitionSpecialCase partitionSpecialCase2 = partitionSpecialCases.get(1);
assertEquals(partitionSpecialCase2.getEndpoint(), "myservice.global.amazonaws.com");
assertEquals(partitionSpecialCase2.getEndpoint(), "https://myservice.global.amazonaws.com");
assertEquals(partitionSpecialCase2.getDualStack(), true);
assertNull(partitionSpecialCase2.getFips());

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[WARNING] example#Service1: This shape applies a trait that is unstable: aws.endpoints#standardRegionalEndpoints | UnstableTrait.aws.endpoints#standardRegionalEndpoints
[DANGER] example#Service1: Endpoint `https://myservice.{invalid}.{dnsSuffix}` contains unsupported patterns: {invalid} | AwsSpecialCaseEndpoint.UnsupportedEndpointPattern
[DANGER] example#Service1: Endpoint `{invalid}-fips.{region}.{badSuffix}` contains unsupported patterns: {invalid}, {badSuffix} | AwsSpecialCaseEndpoint.UnsupportedEndpointPattern
[DANGER] example#Service1: Endpoint `{invalid}-fips.{region}.{badSuffix}` should start with scheme `http://` or `https://` | AwsSpecialCaseEndpoint.InvalidEndpointPatternScheme
[ERROR] example#Service1: Endpoint `{invalid}-fips.{region}.{badSuffix}` should be a valid URL. | AwsSpecialCaseEndpoint.InvalidEndpointPatternUrl
[ERROR] example#Service1: Endpoint `https://{region}. invalidurl {dnsSuffix}` should be a valid URL. | AwsSpecialCaseEndpoint.InvalidEndpointPatternUrl
[WARNING] example#Service2: This shape applies a trait that is unstable: aws.endpoints#standardPartitionalEndpoints | UnstableTrait.aws.endpoints#standardPartitionalEndpoints
[DANGER] example#Service2: Endpoint `myservice.{invalid}.{dnsSuffix}` should start with scheme `http://` or `https://` | AwsSpecialCaseEndpoint.InvalidEndpointPatternScheme
[DANGER] example#Service2: Endpoint `myservice.{invalid}.{dnsSuffix}` contains unsupported patterns: {invalid} | AwsSpecialCaseEndpoint.UnsupportedEndpointPattern
[ERROR] example#Service2: Endpoint `myservice.{invalid}.{dnsSuffix}` should be a valid URL. | AwsSpecialCaseEndpoint.InvalidEndpointPatternUrl
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
$version: "1.0"

namespace example

use aws.endpoints#standardRegionalEndpoints
use aws.endpoints#standardPartitionalEndpoints

@standardRegionalEndpoints(
regionSpecialCases: {
"us-east-1": [
{
endpoint: "https://myservice.{invalid}.{dnsSuffix}",
}
]
},
partitionSpecialCases: {
"aws": [
{
endpoint: "{invalid}-fips.{region}.{badSuffix}",
fips: true
},
{
endpoint: "https://{region}. invalidurl {dnsSuffix}",
dualStack: true
}
]
}
)
service Service1 {}

@standardPartitionalEndpoints(
endpointPatternType: "service_dnsSuffix",
partitionEndpointSpecialCases: {
"aws": [
{
endpoint: "myservice.{invalid}.{dnsSuffix}",
region: "us-east-1"
}
]
}
)
service Service2 {}
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ service Service2 {
partitionSpecialCases: {
"aws-us-gov": [
{
endpoint: "myservice.{region}.{dnsSuffix}",
endpoint: "https://myservice.{region}.{dnsSuffix}",
dualStack: true
},
{
endpoint: "myservice.global.amazonaws.com",
endpoint: "https://myservice.global.amazonaws.com",
dualStack: true
}
]
Expand All @@ -45,12 +45,12 @@ service Service3 {
partitionEndpointSpecialCases: {
"aws-us-gov": [
{
endpoint: "myservice.{region}.{dnsSuffix}",
endpoint: "https://myservice.{region}.{dnsSuffix}",
region: "us-east-1"
fips: true
},
{
endpoint: "myservice.global.amazonaws.com",
endpoint: "https://myservice.global.amazonaws.com",
region: "us-west-2"
dualStack: true
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ use aws.endpoints#standardPartitionalEndpoints
@standardPartitionalEndpoints(
endpointPatternType: "service_dnsSuffix",
partitionEndpointSpecialCases: {
"aws-us": [{endpoint: "myservice.us-west-2.amazonaws.com", region: "us-west-2"}],
"aws-cn": [{endpoint: "myservice.cn-north-1.amazonaws.com", region: "cn-north-1"}]
"aws-us": [{endpoint: "https://myservice.us-west-2.amazonaws.com", region: "us-west-2"}],
"aws-cn": [{endpoint: "https://myservice.cn-north-1.amazonaws.com", region: "cn-north-1"}]
}
)
service MyService {
Expand Down
Loading

0 comments on commit 8fa7d38

Please sign in to comment.