Skip to content

Commit

Permalink
Merge pull request #319 from TehNrd/http-ws-support
Browse files Browse the repository at this point in the history
Add support for HTTP and WebSocket API Types
  • Loading branch information
aoskotsky-amplify committed May 6, 2020
2 parents 1a05bac + 3eca83b commit bf0fc34
Show file tree
Hide file tree
Showing 8 changed files with 1,158 additions and 489 deletions.
100 changes: 100 additions & 0 deletions DomainConfig.ts
@@ -0,0 +1,100 @@
/**
* Wrapper class for Custom Domain information
*/

import Globals from "./Globals";
import DomainInfo = require("./DomainInfo");
import * as AWS from "aws-sdk"; // imported for Types

class DomainConfig {

public givenDomainName: string;
public basePath: string | undefined;
public stage: string | undefined;
public certificateName: string | undefined;
public certificateArn: string | undefined;
public createRoute53Record: boolean | undefined;
public endpointType: string | undefined;
public apiType: string | undefined;
public hostedZoneId: string | undefined;
public hostedZonePrivate: boolean | undefined;
public enabled: boolean | string | undefined;
public securityPolicy: string | undefined;

public domainInfo: DomainInfo | undefined;
public apiId: string | undefined;
public apiMapping: AWS.ApiGatewayV2.GetApiMappingResponse;
public allowPathMatching: boolean | false;

constructor(config: any) {

this.enabled = this.evaluateEnabled(config.enabled);
this.givenDomainName = config.domainName;
this.hostedZonePrivate = config.hostedZonePrivate;
this.certificateArn = config.certificateArn;
this.certificateName = config.certificateName;
this.createRoute53Record = config.createRoute53Record;
this.hostedZoneId = config.hostedZoneId;
this.hostedZonePrivate = config.hostedZonePrivate;
this.allowPathMatching = config.allowPathMatching;

let basePath = config.basePath;
if (basePath == null || basePath.trim() === "") {
basePath = "(none)";
}
this.basePath = basePath;

let stage = config.stage;
if (typeof stage === "undefined") {
stage = Globals.options.stage || Globals.serverless.service.provider.stage;
}
this.stage = stage;

const endpointTypeWithDefault = config.endpointType || Globals.endpointTypes.edge;
const endpointTypeToUse = Globals.endpointTypes[endpointTypeWithDefault.toLowerCase()];
if (!endpointTypeToUse) {
throw new Error(`${endpointTypeWithDefault} is not supported endpointType, use edge or regional.`);
}
this.endpointType = endpointTypeToUse;

const apiTypeWithDefault = config.apiType || Globals.apiTypes.rest;
const apiTypeToUse = Globals.apiTypes[apiTypeWithDefault.toLowerCase()];
if (!apiTypeToUse) {
throw new Error(`${apiTypeWithDefault} is not supported api type, use REST, HTTP or WEBSOCKET.`);
}
this.apiType = apiTypeToUse;

const securityPolicyDefault = config.securityPolicy || Globals.tlsVersions.tls_1_2;
const tlsVersionToUse = Globals.tlsVersions[securityPolicyDefault.toLowerCase()];
if (!tlsVersionToUse) {
throw new Error(`${securityPolicyDefault} is not a supported securityPolicy, use tls_1_0 or tls_1_2.`);
}
this.securityPolicy = tlsVersionToUse;
}

/**
* Determines whether this plug-in is enabled.
*
* This method reads the customDomain property "enabled" to see if this plug-in should be enabled.
* If the property's value is undefined, a default value of true is assumed (for backwards
* compatibility).
* If the property's value is provided, this should be boolean, otherwise an exception is thrown.
* If no customDomain object exists, an exception is thrown.
*/
private evaluateEnabled(enabled: any): boolean {
// const enabled = this.serverless.service.custom.customDomain.enabled;
if (enabled === undefined) {
return true;
}
if (typeof enabled === "boolean") {
return enabled;
} else if (typeof enabled === "string" && enabled === "true") {
return true;
} else if (typeof enabled === "string" && enabled === "false") {
return false;
}
throw new Error(`serverless-domain-manager: Ambiguous enablement boolean: "${enabled}"`);
}
}

export = DomainConfig;
18 changes: 13 additions & 5 deletions DomainInfo.ts
Expand Up @@ -18,11 +18,19 @@ class DomainInfo {
private defaultSecurityPolicy: string = "TLS_1_2";

constructor(data: any) {
this.domainName = data.distributionDomainName || data.regionalDomainName;
this.hostedZoneId = data.distributionHostedZoneId ||
data.regionalHostedZoneId ||
this.defaultHostedZoneId;
this.securityPolicy = data.securityPolicy || this.defaultSecurityPolicy;
this.domainName = data.distributionDomainName
|| data.regionalDomainName
|| data.DomainNameConfigurations && data.DomainNameConfigurations[0].ApiGatewayDomainName
|| data.DomainName;

this.hostedZoneId = data.distributionHostedZoneId
|| data.regionalHostedZoneId
|| data.DomainNameConfigurations && data.DomainNameConfigurations[0].HostedZoneId
|| this.defaultHostedZoneId;

this.securityPolicy = data.securityPolicy
|| data.DomainNameConfigurations && data.DomainNameConfigurations[0].SecurityPolicy
|| this.defaultSecurityPolicy;
}
}

Expand Down
23 changes: 23 additions & 0 deletions Globals.ts
@@ -0,0 +1,23 @@
import { ServerlessInstance, ServerlessOptions } from "./types";

export default class Globals {

public static serverless: ServerlessInstance;
public static options: ServerlessOptions;

public static endpointTypes = {
edge: "EDGE",
regional: "REGIONAL",
};

public static apiTypes = {
http: "HTTP",
rest: "REST",
websocket: "WEBSOCKET",
};

public static tlsVersions = {
tls_1_0: "TLS_1_0",
tls_1_2: "TLS_1_2",
};
}
56 changes: 55 additions & 1 deletion README.md
Expand Up @@ -58,7 +58,7 @@ plugins:
- serverless-domain-manager
```

Add the plugin configuration (example for `serverless.foo.com/api`).
Add the plugin configuration (example for `serverless.foo.com/api`). For a single domain and API type the following structure can be used.

```yaml
custom:
Expand All @@ -70,6 +70,38 @@ custom:
createRoute53Record: true
endpointType: 'regional'
securityPolicy: tls_1_2
apiType: rest
```

Multiple API types mapped to different domains can also be supported with the follow structure. The key is the API Gateway API type.

```yaml
custom:
customDomain:
rest:
domainName: rest.serverless.foo.com
stage: ci
basePath: api
certificateName: '*.foo.com'
createRoute53Record: true
endpointType: 'regional'
securityPolicy: tls_1_2
http:
domainName: http.serverless.foo.com
stage: ci
basePath: api
certificateName: '*.foo.com'
createRoute53Record: true
endpointType: 'regional'
securityPolicy: tls_1_2
websocket:
domainName: ws.serverless.foo.com
stage: ci
basePath: api
certificateName: '*.foo.com'
createRoute53Record: true
endpointType: 'regional'
securityPolicy: tls_1_2
```

| Parameter Name | Default Value | Description |
Expand All @@ -81,10 +113,13 @@ custom:
| certificateArn | `(none)` | The arn of a specific certificate from Certificate Manager to use with this API. |
| createRoute53Record | `true` | Toggles whether or not the plugin will create an A Alias and AAAA Alias records in Route53 mapping the `domainName` to the generated distribution domain name. If false, does not create a record. |
| endpointType | edge | Defines the endpoint type, accepts `regional` or `edge`. |
| apiType | rest | Defines the api type, accepts `rest`, `http` or `websocket`. |
| hostedZoneId | | If hostedZoneId is set the route53 record set will be created in the matching zone, otherwise the hosted zone will be figured out from the domainName (hosted zone with matching domain). |
| hostedZonePrivate | | If hostedZonePrivate is set to `true` then only private hosted zones will be used for route 53 records. If it is set to `false` then only public hosted zones will be used for route53 records. Setting this parameter is specially useful if you have multiple hosted zones with the same domain name (e.g. a public and a private one) |
| enabled | true | Sometimes there are stages for which is not desired to have custom domain names. This flag allows the developer to disable the plugin for such cases. Accepts either `boolean` or `string` values and defaults to `true` for backwards compatibility. |
securityPolicy | tls_1_2 | The security policy to apply to the custom domain name. Accepts `tls_1_0` or `tls_1_2`|
allowPathMatching | false | When updating an existing api mapping this will match on the basePath instead of the API ID to find existing mappings for an upsate. This should only be used when changing API types. For example, migrating a REST API to an HTTP API. See Changing API Types for more information. |


## Running

Expand Down Expand Up @@ -139,6 +174,25 @@ npm install
## Writing Integration Tests
Unit tests are found in `test/unit-tests`. Integration tests are found in `test/integration-tests`. Each folder in `tests/integration-tests` contains the serverless-domain-manager configuration being tested. To create a new integration test, create a new folder for the `handler.js` and `serverless.yml` with the same naming convention and update `integration.test.js`.

## Changing API Types
AWS API Gateway has three different API types: REST, HTTP, and WebSocket. Special steps need to be taken when migrating from one api type to another. A common migration will be from a REST API to an HTTP API given the potential cost savings. Below are the steps required to change from REST to HTTP. A similar process can be applied for other API type migrations.

**REST to HTTP**
1) Confirm the Domain name is a Regional domain name. Edge domains are not supported by AWS for HTTP APIs. See this [guide for migrating an edge-optimized custom domain name to regional](
https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-regional-api-custom-domain-migrate.html).
2) Wait for all DNS changes to take effect/propagate and ensure all traffic is being routed to the regional domain name before proceeding.
3) Make sure you have setup new or modified existing routes to use [httpApi event](https://serverless.com/framework/docs/providers/aws/events/http-api) in your serverless.yml file.
4) Make the following changes to the `customDomain` properties in the serverless.yml confg:
```yaml
endpointType: regional
apiType: http
allowPathMatching: true # Only for one deploy
```
5) Run `sls deploy`
6) Remove the `allowPathMatching` option, it should only be used once when migrating a base path from one API type to another.

NOTE: Always test this process in a lower level staging or development environment before performing it in production.


# Known Issues
* (5/23/2017) CloudFormation does not support changing the base path from empty to something or vice a versa. You must run `sls remove` to remove the base path mapping.
Expand Down

0 comments on commit bf0fc34

Please sign in to comment.