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

Added stack search in Nested Stacks #235

Merged
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
12 changes: 7 additions & 5 deletions DomainInfo.ts
Expand Up @@ -18,11 +18,13 @@ 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.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
2 changes: 2 additions & 0 deletions README.md
Expand Up @@ -70,6 +70,7 @@ custom:
createRoute53Record: true
endpointType: 'regional'
securityPolicy: tls_1_2
apiType: http
```

| Parameter Name | Default Value | Description |
Expand All @@ -81,6 +82,7 @@ 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. |
Expand Down
256 changes: 188 additions & 68 deletions index.ts
Expand Up @@ -9,6 +9,12 @@ const endpointTypes = {
regional: "REGIONAL",
};

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

const tlsVersions = {
tls_1_0: "TLS_1_0",
tls_1_2: "TLS_1_2",
Expand All @@ -20,6 +26,7 @@ class ServerlessCustomDomain {

// AWS SDK resources
public apigateway: any;
public apigatewayV2: any;
public route53: any;
public acm: any;
public acmRegion: string;
Expand All @@ -39,6 +46,7 @@ class ServerlessCustomDomain {
private endpointType: string;
private stage: string;
private securityPolicy: string;
private apiType: string;

constructor(serverless: ServerlessInstance, options: ServerlessOptions) {
this.serverless = serverless;
Expand Down Expand Up @@ -135,11 +143,12 @@ class ServerlessCustomDomain {
*/
public async setupBasePathMapping(): Promise<void> {
// check if basepathmapping exists
const restApiId = await this.getRestApiId();
const currentBasePath = await this.getBasePathMapping(restApiId);
// if basepath that matches restApiId exists, update; else, create
const apiId = await this.getApiId();
const currentBasePath = await this.getBasePathMapping(apiId);

// if basepath that matches apiId exists, update; else, create
if (!currentBasePath) {
await this.createBasePathMapping(restApiId);
await this.createBasePathMapping(apiId);
} else {
await this.updateBasePathMapping(currentBasePath);
}
Expand Down Expand Up @@ -180,6 +189,7 @@ class ServerlessCustomDomain {

this.serverless.providers.aws.sdk.config.update({maxRetries: 20});
this.apigateway = new this.serverless.providers.aws.sdk.APIGateway(credentials);
this.apigatewayV2 = new this.serverless.providers.aws.sdk.ApiGatewayV2(credentials);
this.route53 = new this.serverless.providers.aws.sdk.Route53(credentials);
this.cloudformation = new this.serverless.providers.aws.sdk.CloudFormation(credentials);

Expand All @@ -204,6 +214,14 @@ class ServerlessCustomDomain {
}
this.endpointType = endpointTypeToUse;

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

const securityPolicyDefault = this.serverless.service.custom.customDomain.securityPolicy ||
tlsVersions.tls_1_2;
const tlsVersionToUse = tlsVersions[securityPolicyDefault.toLowerCase()];
Expand Down Expand Up @@ -324,30 +342,56 @@ class ServerlessCustomDomain {
* @param certificateArn: Certificate ARN to use for custom domain
*/
public async createCustomDomain(certificateArn: string): Promise<DomainInfo> {
// Set up parameters
const params = {
certificateArn,
domainName: this.givenDomainName,
endpointConfiguration: {
types: [this.endpointType],
},
regionalCertificateArn: certificateArn,
securityPolicy: this.securityPolicy,
};
if (this.endpointType === endpointTypes.edge) {
params.regionalCertificateArn = undefined;
} else if (this.endpointType === endpointTypes.regional) {
params.certificateArn = undefined;
}

// Make API call
let createdDomain = {};
try {
createdDomain = await this.apigateway.createDomainName(params).promise();
} catch (err) {
this.logIfDebug(err);
throw new Error(`Error: Failed to create custom domain ${this.givenDomainName}\n`);

// Gateway API is completely different for v1 and v2 so seperating into two blocks
if (this.apiType === "REST") {
// Set up parameters
const params = {
certificateArn,
domainName: this.givenDomainName,
endpointConfiguration: {
types: [this.endpointType],
},
regionalCertificateArn: certificateArn,
securityPolicy: this.securityPolicy,
};
if (this.endpointType === endpointTypes.edge) {
params.regionalCertificateArn = undefined;
} else if (this.endpointType === endpointTypes.regional) {
params.certificateArn = undefined;
}

// Make API call to create domain
try {
// If creating REST api use v1 of api gateway, else use v2 for HTTP and Websocket
createdDomain = await this.apigateway.createDomainName(params).promise();
} catch (err) {
this.logIfDebug(err);
throw new Error(`Error: Failed to create custom domain ${this.givenDomainName}\n`);
}

} else if (this.apiType === "HTTP" || this.apiType === "WEBSOCKET") {
const params = {
DomainName: this.givenDomainName,
DomainNameConfigurations: [{
CertificateArn: certificateArn,
EndpointType: this.endpointType,
SecurityPolicy: this.securityPolicy,
}],
};

// Make API call to create domain
try {
// If creating REST api use v1 of api gateway, else use v2 for HTTP and Websocket
createdDomain = await this.apigatewayV2.createDomainName(params).promise();
} catch (err) {
this.logIfDebug(err);
throw new Error(`Error: Failed to create custom domain ${this.givenDomainName}\n`);
}
}

return new DomainInfo(createdDomain);
}

Expand Down Expand Up @@ -477,45 +521,86 @@ class ServerlessCustomDomain {
}

public async getBasePathMapping(restApiId: string): Promise<string> {
const params = {
domainName: this.givenDomainName,
};
let basepathInfo;
let currentBasePath;
try {
basepathInfo = await this.apigateway.getBasePathMappings(params).promise();
} catch (err) {
this.logIfDebug(err);
throw new Error(`Error: Unable to get BasePathMappings for ${this.givenDomainName}`);
}
if (basepathInfo.items !== undefined && basepathInfo.items instanceof Array) {
for (const basepathObj of basepathInfo.items) {
if (basepathObj.restApiId === restApiId) {
currentBasePath = basepathObj.basePath;
break;

if (this.apiType === "REST") {
const params = {
domainName: this.givenDomainName,
};
try {
basepathInfo = await this.apigateway.getBasePathMappings(params).promise();
} catch (err) {
this.logIfDebug(err);
throw new Error(`Error: Unable to get BasePathMappings for ${this.givenDomainName}`);
}
if (basepathInfo.items !== undefined && basepathInfo.items instanceof Array) {
for (const basepathObj of basepathInfo.items) {
if (basepathObj.restApiId === restApiId) {
currentBasePath = basepathObj.basePath;
break;
}
}
}
return currentBasePath;

} else if (this.apiType === "HTTP" || this.apiType === "WEBSOCKET") { // V2 HTTP and WEBSOCKET
const params = {
DomainName: this.givenDomainName,
};
try {
basepathInfo = await this.apigatewayV2.getApiMappings(params).promise();
} catch (err) {
this.logIfDebug(err);
throw new Error(`Error: Unable to get BasePathMappings for ${this.givenDomainName}`);
}
if (basepathInfo.Items !== undefined && basepathInfo.Items instanceof Array) {
for (const basepathObj of basepathInfo.Items) {
if (basepathObj.ApiId === restApiId) {
currentBasePath = basepathObj.ApiMappingKey;
break;
}
}
}
return currentBasePath;
}
return currentBasePath;
}

/**
* Creates basepath mapping
*/
public async createBasePathMapping(restApiId: string): Promise<void> {
const params = {
basePath: this.basePath,
domainName: this.givenDomainName,
restApiId,
stage: this.stage,
};
// Make API call
try {
await this.apigateway.createBasePathMapping(params).promise();
this.serverless.cli.log("Created basepath mapping.");
} catch (err) {
this.logIfDebug(err);
throw new Error(`Error: Unable to create basepath mapping.\n`);
if (this.apiType === "REST") {
const params = {
basePath: this.basePath,
domainName: this.givenDomainName,
restApiId,
stage: this.stage,
};
// Make API call
try {
const resp = await this.apigateway.createBasePathMapping(params).promise();
this.serverless.cli.log("Created basepath mapping.");
} catch (err) {
this.logIfDebug(err);
throw new Error(`Error: Unable to create basepath mapping.\n API Gateway: ${err.message}`);
}

} else if (this.apiType === "HTTP" || this.apiType === "WEBSOCKET") { // V2 HTTP and WEBSOCKET
const params = {
ApiId: restApiId,
ApiMappingKey: this.basePath,
DomainName: this.givenDomainName,
Stage: this.stage,
};
// Make API call
try {
await this.apigatewayV2.createApiMapping(params).promise();
this.serverless.cli.log("Created basepath mapping.");
} catch (err) {
this.logIfDebug(err);
throw new Error(`Error: Unable to create basepath mapping.\n`);
}
}
}

Expand Down Expand Up @@ -547,31 +632,66 @@ class ServerlessCustomDomain {
/**
* Gets rest API id from CloudFormation stack
*/
public async getRestApiId(): Promise<string> {
public async getApiId(): Promise<string> {
if (this.serverless.service.provider.apiGateway && this.serverless.service.provider.apiGateway.restApiId) {
this.serverless.cli.log(`Mapping custom domain to existing API
${this.serverless.service.provider.apiGateway.restApiId}.`);
return this.serverless.service.provider.apiGateway.restApiId;
}
const stackName = this.serverless.service.provider.stackName ||

const stackFamilyName = this.serverless.service.provider.stackName ||
`${this.serverless.service.service}-${this.stage}`;
const params = {
LogicalResourceId: "ApiGatewayRestApi",
StackName: stackName,
};

let LogicalResourceId = "ApiGatewayRestApi";
if (this.apiType === "HTTP") {
LogicalResourceId = "HttpApi";
} else if (this.apiType === "WEBSOCKET") {
LogicalResourceId = "WebsocketsApi";
}

const allStackDescriptions = [];
let nextToken = true;
this.serverless.cli.log("Searching for apiId in stacks");
while (nextToken) {
try {
const params = { NextToken: undefined };
if (typeof nextToken === "string") {
params.NextToken = nextToken;
}
const describeStacksResponse = await this.cloudformation.describeStacks(params).promise();
for (const stackDescription of describeStacksResponse.Stacks) {
allStackDescriptions.push(stackDescription);
}
nextToken = describeStacksResponse.NextToken;
} catch (err) {
this.logIfDebug(err);
}
}
const familyStackNames = allStackDescriptions
.map((stack) => stack.StackName)
.filter((stackName) => stackName.includes(stackFamilyName));
let response;
try {
response = await this.cloudformation.describeStackResource(params).promise();
} catch (err) {
this.logIfDebug(err);
throw new Error(`Error: Failed to find CloudFormation resources for ${this.givenDomainName}\n`);
for (const familyStackName of familyStackNames) {
try {
response = await this.cloudformation.describeStackResource({
LogicalResourceId,
StackName: familyStackName,
}).promise();
break;
} catch (err) {
this.logIfDebug(err);
}
}
const restApiId = response.StackResourceDetail.PhysicalResourceId;
if (!restApiId) {
throw new Error(`Error: No RestApiId associated with CloudFormation stack ${stackName}`);
if (!response) {
throw new Error(`Error: Failed to find a stack ${stackFamilyName}\n`);
}

const apiId = response.StackResourceDetail.PhysicalResourceId;
this.serverless.cli.log(`Found apiId: ${apiId}`);
if (!apiId) {
throw new Error(`Error: No ApiId associated with CloudFormation stack ${stackFamilyName}`);
}
return restApiId;
return apiId;
}

/**
Expand Down