Skip to content

Commit

Permalink
fix(apigateway): allow multi-level base path mapping (#23362)
Browse files Browse the repository at this point in the history
API Gateway allows multi-level base path mapping but CDK doesn't support the same yet. ([AWS Announcement Post](https://aws.amazon.com/about-aws/whats-new/2021/03/amazon-api-gateway-custom-domain-names-support-multi-level-base-path-mappings/))
Also, added new base path mapping validations such as shouldn't start or end with `/` or shouldn't contain consecutive `/`. 

fixes #23347 
----

### All Submissions:

* [x] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md)

### Adding new Construct Runtime Dependencies:

* [ ] This PR adds new construct runtime dependencies following the process described [here](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md/#adding-construct-runtime-dependencies)

### New Features

* [ ] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/main/INTEGRATION_TESTS.md)?
	* [ ] Did you use `yarn integ` to deploy the infrastructure and generate the snapshot (i.e. `yarn integ` without `--dry-run`)?

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
VarunWachaspati committed Dec 17, 2022
1 parent 810d736 commit 86b6c6f
Show file tree
Hide file tree
Showing 6 changed files with 162 additions and 9 deletions.
10 changes: 8 additions & 2 deletions packages/@aws-cdk/aws-apigateway/lib/base-path-mapping.ts
Expand Up @@ -56,8 +56,14 @@ export class BasePathMapping extends Resource {
super(scope, id);

if (props.basePath && !Token.isUnresolved(props.basePath)) {
if (!props.basePath.match(/^[a-zA-Z0-9$_.+!*'()-]+$/)) {
throw new Error(`A base path may only contain letters, numbers, and one of "$-_.+!*'()", received: ${props.basePath}`);
if (props.basePath.startsWith('/') || props.basePath.endsWith('/')) {
throw new Error(`A base path cannot start or end with /", received: ${props.basePath}`);
}
if (props.basePath.match(/\/{2,}/)) {
throw new Error(`A base path cannot have more than one consecutive /", received: ${props.basePath}`);
}
if (!props.basePath.match(/^[a-zA-Z0-9$_.+!*'()-/]+$/)) {
throw new Error(`A base path may only contain letters, numbers, and one of "$-_.+!*'()/", received: ${props.basePath}`);
}
}

Expand Down
110 changes: 103 additions & 7 deletions packages/@aws-cdk/aws-apigateway/test/base-path-mapping.test.ts
Expand Up @@ -8,7 +8,7 @@ describe('BasePathMapping', () => {
// GIVEN
const stack = new cdk.Stack();
const api = new apigw.RestApi(stack, 'MyApi');
api.root.addMethod('GET'); // api must have atleast one method.
api.root.addMethod('GET'); // api must have at least one method.
const domain = new apigw.DomainName(stack, 'MyDomain', {
domainName: 'example.com',
certificate: acm.Certificate.fromCertificateArn(stack, 'cert', 'arn:aws:acm:us-east-1:1111111:certificate/11-3336f1-44483d-adc7-9cd375c5169d'),
Expand All @@ -33,7 +33,7 @@ describe('BasePathMapping', () => {
// GIVEN
const stack = new cdk.Stack();
const api = new apigw.RestApi(stack, 'MyApi');
api.root.addMethod('GET'); // api must have atleast one method.
api.root.addMethod('GET'); // api must have at least one method.
const domain = new apigw.DomainName(stack, 'MyDomain', {
domainName: 'example.com',
certificate: acm.Certificate.fromCertificateArn(stack, 'cert', 'arn:aws:acm:us-east-1:1111111:certificate/11-3336f1-44483d-adc7-9cd375c5169d'),
Expand All @@ -53,19 +53,43 @@ describe('BasePathMapping', () => {
});
});

test('throw error for invalid basePath property', () => {
test('specify multi-level basePath property', () => {
// GIVEN
const stack = new cdk.Stack();
const api = new apigw.RestApi(stack, 'MyApi');
api.root.addMethod('GET'); // api must have atleast one method.
api.root.addMethod('GET'); // api must have at least one method.
const domain = new apigw.DomainName(stack, 'MyDomain', {
domainName: 'example.com',
certificate: acm.Certificate.fromCertificateArn(stack, 'cert', 'arn:aws:acm:us-east-1:1111111:certificate/11-3336f1-44483d-adc7-9cd375c5169d'),
endpointType: apigw.EndpointType.REGIONAL,
});

// WHEN
const invalidBasePath = '/invalid-/base-path';
new apigw.BasePathMapping(stack, 'MyBasePath', {
restApi: api,
domainName: domain,
basePath: 'api/v1/example',
});

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::BasePathMapping', {
BasePath: 'api/v1/example',
});
});

test('throws when basePath contains an invalid character', () => {
// GIVEN
const stack = new cdk.Stack();
const api = new apigw.RestApi(stack, 'MyApi');
api.root.addMethod('GET'); // api must have at least one method.
const domain = new apigw.DomainName(stack, 'MyDomain', {
domainName: 'example.com',
certificate: acm.Certificate.fromCertificateArn(stack, 'cert', 'arn:aws:acm:us-east-1:1111111:certificate/11-3336f1-44483d-adc7-9cd375c5169d'),
endpointType: apigw.EndpointType.REGIONAL,
});

// WHEN
const invalidBasePath = 'invalid-/base-path?';

// THEN
expect(() => {
Expand All @@ -77,11 +101,83 @@ describe('BasePathMapping', () => {
}).toThrowError(/base path may only contain/);
});

test('throw error for basePath starting with /', () => {
// GIVEN
const stack = new cdk.Stack();
const api = new apigw.RestApi(stack, 'MyApi');
api.root.addMethod('GET'); // api must have at least one method.
const domain = new apigw.DomainName(stack, 'MyDomain', {
domainName: 'example.com',
certificate: acm.Certificate.fromCertificateArn(stack, 'cert', 'arn:aws:acm:us-east-1:1111111:certificate/11-3336f1-44483d-adc7-9cd375c5169d'),
endpointType: apigw.EndpointType.REGIONAL,
});

// WHEN
const invalidBasePath = '/invalid-base-path';

// THEN
expect(() => {
new apigw.BasePathMapping(stack, 'MyBasePath', {
restApi: api,
domainName: domain,
basePath: invalidBasePath,
});
}).toThrowError(/A base path cannot start or end with/);
});

test('throw error for basePath ending with /', () => {
// GIVEN
const stack = new cdk.Stack();
const api = new apigw.RestApi(stack, 'MyApi');
api.root.addMethod('GET'); // api must have at least one method.
const domain = new apigw.DomainName(stack, 'MyDomain', {
domainName: 'example.com',
certificate: acm.Certificate.fromCertificateArn(stack, 'cert', 'arn:aws:acm:us-east-1:1111111:certificate/11-3336f1-44483d-adc7-9cd375c5169d'),
endpointType: apigw.EndpointType.REGIONAL,
});

// WHEN
const invalidBasePath = 'invalid-base-path/';

// THEN
expect(() => {
new apigw.BasePathMapping(stack, 'MyBasePath', {
restApi: api,
domainName: domain,
basePath: invalidBasePath,
});
}).toThrowError(/A base path cannot start or end with/);
});

test('throw error for basePath containing more than one consecutive /', () => {
// GIVEN
const stack = new cdk.Stack();
const api = new apigw.RestApi(stack, 'MyApi');
api.root.addMethod('GET'); // api must have at least one method.
const domain = new apigw.DomainName(stack, 'MyDomain', {
domainName: 'example.com',
certificate: acm.Certificate.fromCertificateArn(stack, 'cert', 'arn:aws:acm:us-east-1:1111111:certificate/11-3336f1-44483d-adc7-9cd375c5169d'),
endpointType: apigw.EndpointType.REGIONAL,
});

// WHEN
const invalidBasePath = 'in//valid-base-path';

// THEN
expect(() => {
new apigw.BasePathMapping(stack, 'MyBasePath', {
restApi: api,
domainName: domain,
basePath: invalidBasePath,
});
}).toThrowError(/A base path cannot have more than one consecutive \//);
});

test('specify stage property', () => {
// GIVEN
const stack = new cdk.Stack();
const api = new apigw.RestApi(stack, 'MyApi');
api.root.addMethod('GET'); // api must have atleast one method.
api.root.addMethod('GET'); // api must have at least one method.
const domain = new apigw.DomainName(stack, 'MyDomain', {
domainName: 'example.com',
certificate: acm.Certificate.fromCertificateArn(stack, 'cert', 'arn:aws:acm:us-east-1:1111111:certificate/11-3336f1-44483d-adc7-9cd375c5169d'),
Expand Down Expand Up @@ -111,7 +207,7 @@ describe('BasePathMapping', () => {
// GIVEN
const stack = new cdk.Stack();
const api = new apigw.RestApi(stack, 'MyApi');
api.root.addMethod('GET'); // api must have atleast one method.
api.root.addMethod('GET'); // api must have at least one method.
const domain = new apigw.DomainName(stack, 'MyDomain', {
domainName: 'example.com',
certificate: acm.Certificate.fromCertificateArn(stack, 'cert', 'arn:aws:acm:us-east-1:1111111:certificate/11-3336f1-44483d-adc7-9cd375c5169d'),
Expand Down
Expand Up @@ -81,6 +81,12 @@
"data": "MappingTwo551C79ED"
}
],
"/test-stack/MappingThree/Resource": [
{
"type": "aws:cdk:logicalId",
"data": "MappingThree36BBA1B6"
}
],
"/test-stack/BootstrapVersion": [
{
"type": "aws:cdk:logicalId",
Expand Down
Expand Up @@ -70,6 +70,16 @@
"Ref": "ApiF70053CD"
}
}
},
"MappingThree36BBA1B6": {
"Type": "AWS::ApiGateway::BasePathMapping",
"Properties": {
"DomainName": "domainName",
"BasePath": "api/v1/multi-level-path",
"RestApiId": {
"Ref": "ApiF70053CD"
}
}
}
},
"Outputs": {
Expand Down
Expand Up @@ -217,6 +217,34 @@
"fqn": "@aws-cdk/aws-apigateway.BasePathMapping",
"version": "0.0.0"
}
},
"MappingThree": {
"id": "MappingThree",
"path": "test-stack/MappingThree",
"children": {
"Resource": {
"id": "Resource",
"path": "test-stack/MappingThree/Resource",
"attributes": {
"aws:cdk:cloudformation:type": "AWS::ApiGateway::BasePathMapping",
"aws:cdk:cloudformation:props": {
"domainName": "domainName",
"basePath": "path",
"restApiId": {
"Ref": "ApiF70053CD"
}
}
},
"constructInfo": {
"fqn": "@aws-cdk/aws-apigateway.CfnBasePathMapping",
"version": "0.0.0"
}
}
},
"constructInfo": {
"fqn": "@aws-cdk/aws-apigateway.BasePathMapping",
"version": "0.0.0"
}
}
},
"constructInfo": {
Expand Down
Expand Up @@ -27,6 +27,13 @@ export class TestStack extends cdk.Stack {
basePath: 'path',
attachToStage: false,
});

new apigateway.BasePathMapping(this, 'MappingThree', {
domainName,
restApi,
basePath: 'api/v1/multi-level-path',
attachToStage: false,
});
}
}

Expand Down

0 comments on commit 86b6c6f

Please sign in to comment.