Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add autoDomain and autoDomainWaitFor option to config #356

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Expand Up @@ -71,6 +71,7 @@ custom:
endpointType: 'regional'
securityPolicy: tls_1_2
apiType: rest
autoDomain: false
```

Multiple API types mapped to different domains can also be supported with the following structure. The key is the API Gateway API type.
Expand Down Expand Up @@ -119,6 +120,8 @@ custom:
| 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. |
| autoDomain | `false` | Toggles whether or not the plugin will run `create_domain/delete_domain` as part of `sls deploy/remove` so that multiple commands are not required. |
| autoDomainWaitFor | `120` | How long to wait for create_domain to finish before starting deployment if domain does not exist immediately. |


## Running
Expand Down
35 changes: 31 additions & 4 deletions src/index.ts
Expand Up @@ -5,7 +5,7 @@ import DomainConfig = require("./DomainConfig");
import DomainInfo = require("./DomainInfo");
import Globals from "./Globals";
import { ServerlessInstance, ServerlessOptions } from "./types";
import {getAWSPagedResults, throttledCall} from "./utils";
import {getAWSPagedResults, sleep, throttledCall} from "./utils";

const certStatuses = ["PENDING_VALIDATION", "ISSUED", "INACTIVE"];

Expand Down Expand Up @@ -54,7 +54,7 @@ class ServerlessCustomDomain {
this.hooks = {
"after:deploy:deploy": this.hookWrapper.bind(this, this.setupBasePathMappings),
"after:info:info": this.hookWrapper.bind(this, this.domainSummaries),
"before:deploy:deploy": this.hookWrapper.bind(this, this.updateCloudFormationOutputs),
"before:deploy:deploy": this.hookWrapper.bind(this, this.createOrGetDomainForCfOutputs),
"before:remove:remove": this.hookWrapper.bind(this, this.removeBasePathMappings),
"create_domain:create": this.hookWrapper.bind(this, this.createDomains),
"delete_domain:delete": this.hookWrapper.bind(this, this.deleteDomains),
Expand Down Expand Up @@ -130,12 +130,33 @@ class ServerlessCustomDomain {
}

/**
* Lifecycle function to add domain info to the CloudFormation stack's Outputs
* Lifecycle function to createDomain before deploy and add domain info to the CloudFormation stack's Outputs
*/
public async updateCloudFormationOutputs(): Promise<void> {
public async createOrGetDomainForCfOutputs(): Promise<void> {
const autoDomain = this.serverless.service.custom.customDomain.autoDomain;
if (autoDomain === true) {
this.serverless.cli.log("Creating domain name before deploy.");
await this.createDomains();
}

await this.getDomainInfo();

if (autoDomain === true) {
const atLeastOneDoesNotExist = () => this.domains.some((domain) => !domain.domainInfo);
const maxWaitFor = parseInt(this.serverless.service.custom.customDomain.autoDomainWaitFor, 10) || 120;
const pollInterval = 3;
for (let i = 0; i * pollInterval < maxWaitFor && atLeastOneDoesNotExist() === true; i++) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do we exit this for loop?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if all domains exist , then atLeastOneDoesNotExist() will be false and exit (await this.getDomainInfo(); in the for loop will repopulate data so it can go from true/false)

otherwise, if it never populates, the i * pollInterval will eventually be greater than maxWaitFor and it'll exit and the deployment will eventually fail in the same way as if you never ran create_domain

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jconstance-amplify let me know what you think :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jconstance-amplify no vacations allowed!!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fortunately for you I came back on Monday! :D

this.serverless.cli.log(`
Poll #${i + 1}: polling every ${pollInterval} seconds
for domain to exist or until ${maxWaitFor} seconds
have elapsed before starting deployment
`);

await sleep(pollInterval);
await this.getDomainInfo();
}
}

await Promise.all(this.domains.map(async (domain) => {
this.addOutputs(domain);
}));
Expand Down Expand Up @@ -200,6 +221,12 @@ class ServerlessCustomDomain {
}
}
}));

const autoDomain = this.serverless.service.custom.customDomain.autoDomain;
if (autoDomain === true) {
this.serverless.cli.log("Deleting domain name after removing base path mapping.");
await this.deleteDomains();
}
}

/**
Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Expand Up @@ -25,6 +25,8 @@ export interface ServerlessInstance { // tslint:disable-line
hostedZonePrivate: boolean | undefined,
enabled: boolean | string | undefined,
securityPolicy: string | undefined,
autoDomain: boolean | undefined,
autoDomainWaitFor: string | undefined,
},
},
};
Expand Down
1 change: 1 addition & 0 deletions src/utils.ts
Expand Up @@ -76,6 +76,7 @@ async function sleep(seconds) {
}

export {
sleep,
getAWSPagedResults,
throttledCall,
};
16 changes: 16 additions & 0 deletions test/integration-tests/auto-domain/handler.js
@@ -0,0 +1,16 @@
"use strict";

module.exports.helloWorld = (event, context, callback) => {
const response = {
statusCode: 200,
headers: {
"Access-Control-Allow-Origin": "*", // Required for CORS support to work
},
body: JSON.stringify({
message: "Go Serverless v1.0! Your function executed successfully!",
input: event,
}),
};

callback(null, response);
};
27 changes: 27 additions & 0 deletions test/integration-tests/auto-domain/serverless.yml
@@ -0,0 +1,27 @@
# create_domain should be run as part of deployment
service: auto-domain-${opt:RANDOM_STRING}
provider:
name: aws
runtime: nodejs12.x
region: us-west-2
stage: test
functions:
helloWorld:
handler: handler.helloWorld
events:
- http:
path: hello-world
method: get
cors: true
plugins:
- serverless-domain-manager
custom:
customDomain:
domainName: auto-domain-${opt:RANDOM_STRING}.${env:TEST_DOMAIN}
basePath: ''
autoDomain: true
autoDomainWaitFor: 120

package:
exclude:
- node_modules/**
8 changes: 8 additions & 0 deletions test/integration-tests/integration.test.ts
Expand Up @@ -21,6 +21,14 @@ const RANDOM_STRING = randomstring.generate({
const TEMP_DIR = `~/tmp/domain-manager-test-${RANDOM_STRING}`;

const testCases = [
{
testBasePath: "(none)",
testDescription: "Creates domain as part of deploy",
testDomain: `auto-domain-${RANDOM_STRING}.${TEST_DOMAIN}`,
testEndpoint: "EDGE",
testFolder: "auto-domain",
testStage: "dev",
},
{
testBasePath: "(none)",
testDescription: "Enabled with default values",
Expand Down
169 changes: 169 additions & 0 deletions test/unit-tests/index.test.ts
Expand Up @@ -64,6 +64,8 @@ const constructPlugin = (customDomainOptions) => {
custom: {
customDomain: {
apiType: customDomainOptions.apiType,
autoDomain: customDomainOptions.autoDomain,
autoDomainWaitFor: customDomainOptions.autoDomainWaitFor,
basePath: customDomainOptions.basePath,
certificateArn: customDomainOptions.certificateArn,
certificateName: customDomainOptions.certificateName,
Expand Down Expand Up @@ -1576,4 +1578,171 @@ describe("Custom Domain Plugin", () => {
AWS.restore();
});
});

describe("autoDomain deploy", () => {
it("Should be disabled by default", () => {
const plugin = constructPlugin({ domainName: "test_domain" });
plugin.initializeVariables();
expect(plugin.serverless.service.custom.customDomain.autoDomain).to.equal(undefined);
});

it("createOrGetDomainForCfOutputs should call createDomain when autoDomain is true", async () => {
AWS.mock("ApiGatewayV2", "getDomainName", (params, callback) => {
callback(null, params);
});
const plugin = constructPlugin({
autoDomain: true,
basePath: "test_basepath",
createRoute53Record: false,
domainName: "test_domain",
restApiId: "test_rest_api_id",
});
plugin.initializeVariables();

plugin.apigateway = new aws.APIGateway();
plugin.apigatewayV2 = new aws.ApiGatewayV2();
plugin.cloudformation = new aws.CloudFormation();

plugin.domains[0].apiMapping = {ApiMappingId: "test_mapping_id"};

const spy = chai.spy.on(plugin.apigatewayV2, "getDomainName");

await plugin.createOrGetDomainForCfOutputs();

expect(plugin.serverless.service.custom.customDomain.autoDomain).to.equal(true);
expect(spy).to.have.been.called();
});

it("createOrGetDomainForCfOutputs should not call createDomain when autoDomain is not true", async () => {
AWS.mock("ApiGatewayV2", "getDomainName", (params, callback) => {
callback(null, params);
});

const plugin = constructPlugin({
autoDomain: false,
basePath: "test_basepath",
createRoute53Record: false,
domainName: "test_domain",
restApiId: "test_rest_api_id",
});
plugin.initializeVariables();

plugin.apigateway = new aws.APIGateway();
plugin.apigatewayV2 = new aws.ApiGatewayV2();
plugin.cloudformation = new aws.CloudFormation();

plugin.domains[0].apiMapping = {ApiMappingId: "test_mapping_id"};

const spy1 = chai.spy.on(plugin.apigateway, "createDomainName");
const spy2 = chai.spy.on(plugin.apigatewayV2, "createDomainName");

await plugin.createOrGetDomainForCfOutputs();

expect(plugin.serverless.service.custom.customDomain.autoDomain).to.equal(false);
expect(spy1).to.have.not.been.called();
expect(spy2).to.have.not.been.called();
});

it("removeBasePathMapping should call deleteDomain when autoDomain is true", async () => {
AWS.mock("CloudFormation", "describeStackResource", (params, callback) => {
callback(null, {
StackResourceDetail:
{
LogicalResourceId: "ApiGatewayRestApi",
PhysicalResourceId: "test_rest_api_id",
},
});
});
AWS.mock("ApiGatewayV2", "getApiMappings", (params, callback) => {
callback(null, {
Items: [
{ ApiId: "test_rest_api_id", MappingKey: "test", ApiMappingId: "test_mapping_id", Stage: "test" },
],
});
});
AWS.mock("ApiGatewayV2", "deleteApiMapping", (params, callback) => {
callback(null, params);
});
AWS.mock("ApiGatewayV2", "deleteDomainName", (params, callback) => {
callback(null, params);
});
AWS.mock("ApiGatewayV2", "getDomainName", (params, callback) => {
callback(null, params);
});

const plugin = constructPlugin({
autoDomain: true,
basePath: "test_basepath",
createRoute53Record: false,
domainName: "test_domain",
restApiId: "test_rest_api_id",
});
plugin.initializeVariables();

plugin.apigatewayV2 = new aws.ApiGatewayV2();
plugin.cloudformation = new aws.CloudFormation();

plugin.domains[0].apiMapping = {ApiMappingId: "test_mapping_id"};

const spy = chai.spy.on(plugin.apigatewayV2, "deleteDomainName");

await plugin.removeBasePathMappings();

expect(plugin.serverless.service.custom.customDomain.autoDomain).to.equal(true);
expect(spy).to.have.been.called.with({DomainName: "test_domain"});
});

it("removeBasePathMapping should not call deleteDomain when autoDomain is not true", async () => {
AWS.mock("CloudFormation", "describeStackResource", (params, callback) => {
callback(null, {
StackResourceDetail:
{
LogicalResourceId: "ApiGatewayRestApi",
PhysicalResourceId: "test_rest_api_id",
},
});
});
AWS.mock("ApiGatewayV2", "getApiMappings", (params, callback) => {
callback(null, {
Items: [
{ ApiId: "test_rest_api_id", MappingKey: "test", ApiMappingId: "test_mapping_id", Stage: "test" },
],
});
});
AWS.mock("ApiGatewayV2", "deleteApiMapping", (params, callback) => {
callback(null, params);
});
AWS.mock("ApiGatewayV2", "deleteDomainName", (params, callback) => {
callback(null, params);
});
AWS.mock("ApiGatewayV2", "getDomainName", (params, callback) => {
callback(null, params);
});

const plugin = constructPlugin({
autoDomain: false,
basePath: "test_basepath",
createRoute53Record: false,
domainName: "test_domain",
restApiId: "test_rest_api_id",
});
plugin.initializeVariables();

plugin.apigatewayV2 = new aws.ApiGatewayV2();
plugin.cloudformation = new aws.CloudFormation();

plugin.domains[0].apiMapping = {ApiMappingId: "test_mapping_id"};

const spy = chai.spy.on(plugin.apigatewayV2, "deleteDomainName");

await plugin.removeBasePathMappings();

expect(plugin.serverless.service.custom.customDomain.autoDomain).to.equal(false);
expect(spy).to.have.not.been.called();
});

afterEach(() => {
consoleOutput = [];
});
});
});