@webda/aws
Version:
Webda AWS Services implementation
837 lines (836 loc) • 36.9 kB
JavaScript
import { APIGateway } from "@aws-sdk/client-api-gateway";
import { CloudFormation } from "@aws-sdk/client-cloudformation";
import { STS } from "@aws-sdk/client-sts";
import { JSONUtils, WebdaError } from "@webda/core";
import * as fs from "fs";
import * as path from "path";
import * as YAML from "yaml";
import { AWSDeployer } from "./index.js";
const LAMBDA_LATEST_VERSION = "nodejs14.x";
/**
* Deploy the application and its resources using AWS CloudFormation
*
* @WebdaDeployer WebdaAWSDeployer/CloudFormation
*/
export default class CloudFormationDeployer extends AWSDeployer {
constructor(manager, resources) {
super(manager, resources);
this.template = {};
this.result = {};
}
async defaultResources() {
var _a, _b, _c;
await super.defaultResources();
let packageDesc = this.getApplication().getPackageDescription();
packageDesc.webda = packageDesc.webda || {};
packageDesc.webda.aws = packageDesc.webda.aws || {};
if (this.resources.ResourcesToImport) {
if (!this.resources.ChangeSetType) {
this.resources.ChangeSetType = "IMPORT";
}
if (this.resources.ChangeSetType !== "IMPORT") {
throw new Error("ChangeSetType cannot be anything else than IMPORT if you have ResourcesToImport set");
}
}
this.resources.ChangeSetType = this.resources.ChangeSetType || "CREATE";
this.resources.AssetsBucket = this.resources.AssetsBucket || packageDesc.webda.aws.AssetsBucket;
if (!this.resources.AssetsBucket) {
throw new WebdaError.CodeError("ASSETS_BUCKET_REQUIRED", "AssetsBucket must be defined");
}
this.resources.AssetsPrefix = this.resources.AssetsPrefix || "${deployment}/${deployer.name}/";
this.resources.Description = this.resources.Description || "Deployed by @webda/aws/cloudformation";
this.resources.FileName = this.resources.FileName || `cloudformation-${this.resources.name}`;
this.resources.StackName = this.resources.StackName || this.resources.name;
this.resources.Format = this.resources.Format || "JSON";
this.resources.OpenAPIFileName = this.resources.OpenAPIFileName || "${resources.name}-openapi-${package.version}";
this.resources.OpenAPITitle = this.resources.OpenAPITitle || this.resources.name;
// Default Lambda value
if (this.resources.Lambda) {
let zipPath = "lambda-${package.version}.zip";
if (this.resources.LambdaPackager) {
zipPath = this.resources.LambdaPackager.zipPath;
}
(_a = this.resources).LambdaPackager ?? (_a.LambdaPackager = { zipPath });
this.resources.LambdaPackager.zipPath = zipPath;
this.resources.Lambda.Runtime = this.resources.Lambda.Runtime || LAMBDA_LATEST_VERSION;
this.resources.Lambda.MemorySize = this.resources.Lambda.MemorySize || 2048;
this.resources.Lambda.Timeout = this.resources.Lambda.Timeout || 30;
this.resources.Lambda.Handler =
this.resources.Lambda.Handler || "node_modules/@webda/aws/lib/deployers/lambda-entrypoint.handler";
this.resources.Lambda.FunctionName = this.resources.Lambda.FunctionName || this.resources.name;
if (!this.resources.Lambda.Role) {
this.resources.Lambda.Role = { "Fn::GetAtt": ["Role", "Arn"] };
// If no role is specified auto enable Role and Policy creation
this.resources.Role = this.resources.Role || {};
this.resources.Policy = this.resources.Policy || {};
}
}
// Default Role
if (this.resources.Role) {
this.resources.Role.RoleName = this.resources.Role.RoleName || `${this.resources.name}Role`;
if (!this.resources.Role.Policies || this.resources.Role.Policies.length === 0) {
this.resources.Policy = this.resources.Policy || {};
}
this.resources.Role.AssumeRolePolicyDocument = this.resources.Role.AssumeRolePolicyDocument || {
Statement: []
};
this.resources.Role.AssumeRolePolicyDocument.Statement =
this.resources.Role.AssumeRolePolicyDocument.Statement || [];
this.resources.Role.AssumeRolePolicyDocument.Version =
this.resources.Role.AssumeRolePolicyDocument.Version || "2012-10-17";
}
// Default Policy
if (this.resources.Policy) {
this.resources.Policy.PolicyName = this.resources.Policy.PolicyName || `${this.resources.name}Policy`;
this.resources.Policy.Roles = this.resources.Policy.Roles || [];
if (this.resources.Role) {
this.resources.Policy.Roles.push({ Ref: "Role" });
}
this.resources.Policy.PolicyDocument = this.resources.Policy.PolicyDocument || { Statement: [] };
}
this.resources.Tags = this.transformMapTagsToArray(this.resources.Tags || []);
if (this.resources.APIGatewayStage) {
this.resources.APIGatewayStage.StageName =
this.resources.APIGatewayStage.StageName ||
// @ts-ignore
this.getApplication().currentDeployment;
}
// Activate Domain
if (this.resources.APIGatewayDomain) {
(_b = this.resources.APIGatewayDomain).SecurityPolicy ?? (_b.SecurityPolicy = "TLS_1_2");
}
// Default BasePathMapping
if (this.resources.APIGatewayBasePathMapping) {
this.resources.APIGatewayBasePathMapping.BasePath = this.resources.APIGatewayBasePathMapping.BasePath || "";
(_c = this.resources.APIGatewayBasePathMapping).DomainName ?? (_c.DomainName = this.resources.APIGatewayDomain.DomainName);
if (this.resources.APIGatewayBasePathMapping.DomainName.endsWith(".")) {
this.resources.APIGatewayBasePathMapping.DomainName =
this.resources.APIGatewayBasePathMapping.DomainName.substring(0, this.resources.APIGatewayBasePathMapping.DomainName.length - 1);
}
}
// Activate Domain
if (this.resources.APIGatewayV2Domain) {
// Enable BasePathMapping if does not exist
this.resources.APIGatewayV2ApiMapping = this.resources.APIGatewayV2ApiMapping || {};
this.resources.APIGatewayV2Domain.DomainNameConfigurations =
this.resources.APIGatewayV2Domain.DomainNameConfigurations || [];
}
// Default BasePathMapping
if (this.resources.APIGatewayV2ApiMapping) {
this.resources.APIGatewayV2ApiMapping.BasePath = this.resources.APIGatewayV2ApiMapping.BasePath || "";
this.resources.APIGatewayV2ApiMapping.DomainName =
this.resources.APIGatewayV2ApiMapping.DomainName || this.resources.APIGatewayDomain.DomainName;
if (this.resources.APIGatewayV2ApiMapping.DomainName.endsWith(".")) {
this.resources.APIGatewayV2ApiMapping.DomainName = this.resources.APIGatewayV2ApiMapping.DomainName.substring(0, this.resources.APIGatewayV2ApiMapping.DomainName.length - 1);
}
}
this.resources.Statics = (this.resources.Statics || []);
this.resources.Statics.forEach(conf => {
// Should move to init
if (!conf.AssetsPath) {
conf.AssetsPath = conf.Source;
if (conf.AssetsPath.startsWith("/")) {
conf.AssetsPath = conf.AssetsPath.substring(1);
}
if (!conf.AssetsPath.endsWith("/")) {
conf.AssetsPath += "/";
}
}
if (conf.CloudFront) {
let DistributionConfig = conf.CloudFront.DistributionConfig || {};
DistributionConfig.Aliases = DistributionConfig.Aliases || [];
if (DistributionConfig.Aliases.indexOf(conf.DomainName) < 0) {
DistributionConfig.Aliases.push(conf.DomainName);
}
DistributionConfig.PriceClass = DistributionConfig.PriceClass || "PriceClass_100";
DistributionConfig.Comment = DistributionConfig.Comment || "Deployed with @webda/aws/cloudformation";
if (DistributionConfig.Enabled === undefined) {
DistributionConfig.Enabled = true;
}
if (!DistributionConfig.DefaultCacheBehavior) {
DistributionConfig.DefaultCacheBehavior = {
AllowedMethods: ["GET", "HEAD"],
ViewerProtocolPolicy: "redirect-to-https",
TargetOriginId: conf.DomainName,
ForwardedValues: {
QueryString: false
}
};
}
if (!DistributionConfig.Origins) {
DistributionConfig.Origins = [
{
DomainName: `${this.resources.AssetsBucket}.s3.amazonaws.com`,
Id: conf.DomainName,
OriginPath: `/${conf.AssetsPath.substring(0, conf.AssetsPath.length - 1)}`,
S3OriginConfig: {}
}
];
}
conf.CloudFront.DistributionConfig = DistributionConfig;
}
});
// Manage Docker build
if (this.resources.Docker) {
this.resources.Docker.push = this.resources.Docker.push === undefined ? true : this.resources.Docker.push;
this.resources.Docker.includeRepository =
this.resources.Docker.includeRepository === undefined ? true : this.resources.Docker.includeRepository;
if (this.resources.Docker.includeRepository && this.resources.Docker.tag) {
let accountId = (await this.getAWSIdentity()).Account;
this.resources.Docker.tag =
accountId + ".dkr.ecr.eu-west-1.amazonaws.com/${package.webda.aws.Repository}:" + this.resources.Docker.tag;
}
}
}
async deploy() {
const { Description } = this.resources;
this.template = {
Description,
Resources: { ...this.resources.CustomResources }
};
this.result = {};
// Ensure S3 bucket exist
this.logger.log("DEBUG", "Check assets bucket", this.resources.AssetsBucket);
await this.createBucket(this.resources.AssetsBucket);
// Check if we need OpenAPI export
let openapi = await this.completeOpenAPI(this.manager.getWebda().exportOpenAPI(false));
this.openapiS3Object = this.getStringified(openapi, this.resources.OpenAPIFileName);
await this.putFilesOnBucket(this.resources.AssetsBucket, [this.openapiS3Object]);
// If APIGatewayImportOpenApi update REST API
if (this.resources.APIGatewayImportOpenApi) {
this.logger.log("INFO", "Importing open api");
await this.importOpenApi(openapi);
}
// Build Docker if needed
if (this.resources.Docker && this.resources.Docker.tag) {
this.logger.log("INFO", "Building Docker image", this.resources.Docker.tag);
await this.buildDocker();
}
this.logger.log("INFO", "Uploading statics");
// Upload any Statics
await this.uploadStatics();
// Dynamicly call each methods
for (let i in this.resources) {
if (this[i] && typeof this[i] === "function") {
this.logger.log("TRACE", "Add CloudFormation Resource", i);
await this[i]();
}
}
// Add any static
for (let i in this.resources.Statics) {
this.logger.log("TRACE", "Add Static Resource", i);
await this.createStatic(this.resources.Statics[i]);
}
this.logger.log("INFO", "Deploy with CloudFormation");
// Upload new version it
await this.sendCloudFormationTemplate();
// Load the stack
await this.createCloudFormation();
return this.result;
}
/**
* Upload any asset to bucket
*/
async uploadStatics() {
for (let i in this.resources.Statics) {
const { Source, AssetsPath } = this.resources.Statics[i];
await this.putFolderOnBucket(this.resources.AssetsBucket, this.manager.getApplication().getAppPath(Source), AssetsPath);
}
}
async createStatic(info) {
const { DomainName, CloudFront, Bucket } = info;
info.Bucket = info.Bucket || {};
// Create bucket
let resPrefix = `Static${DomainName.replace(/\./g, "")}`;
if (Bucket) {
this.template.Resources[`${resPrefix}Bucket`] = {
Type: "AWS::S3::Bucket",
Properties: {
...info.Bucket,
BucketName: DomainName,
Tags: this.getDefaultTags(info.Bucket.Tags)
}
};
}
if (CloudFront) {
if (!CloudFront.DistributionConfig.ViewerCertificate) {
CloudFront.DistributionConfig.ViewerCertificate = {
AcmCertificateArn: (await this.getCertificate(DomainName)).CertificateArn,
SslSupportMethod: "sni-only"
};
}
this.template.Resources[`${resPrefix}CloudFront`] = {
Type: "AWS::CloudFront::Distribution",
Properties: {
DistributionConfig: CloudFront.DistributionConfig,
Tags: this.getDefaultTags(info.CloudFront.Tags)
}
};
await this.createCloudFormationDNSEntry({ "Fn::GetAtt": [`${resPrefix}CloudFront`, "DomainName"] }, DomainName, "Z2FDTNDATAQYW2" // Constant as seen here: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-aliastarget-1.html#cfn-route53-aliastarget-hostedzoneid
);
}
}
/**
* Copy the CloudFormation template to the Assets bucket
*/
async sendCloudFormationTemplate() {
let res = this.getStringified(this.template, this.resources.FileName);
this.result.CloudFormation = {
Bucket: this.resources.AssetsBucket,
Key: res.key
};
this.result.CloudFormationContent = res.src.toString();
await this.putFilesOnBucket(this.resources.AssetsBucket, [res]);
}
/**
* Delete the CloudFormation stack
* @returns
*/
async deleteCloudFormation() {
let cloudformation = new CloudFormation({});
await cloudformation.deleteStack({ StackName: this.resources.StackName });
return this.waitFor(async (resolve) => {
try {
await cloudformation.describeStacks({
StackName: this.resources.StackName
});
}
catch (err) {
resolve();
return true;
}
}, 50, "Waiting on stack to be deleted", 5000);
}
/**
*
* @param ref
* @param domain
* @param HostedZoneId
* @returns
*/
async createCloudFormationDNSEntry(ref, domain, HostedZoneId) {
let zone = await this.getZoneForDomainName(domain);
if (!zone) {
this.logger.log("WARN", "Cannot find Route53 zone for", domain, ", you will have to create manually the CNAME");
return;
}
// Possible conflict if my.name.domain.io and myn.ame.domain.io exists
this.template.Resources[`DNSEntry${domain.replace(/\./g, "")}`] = {
Type: "AWS::Route53::RecordSet",
Properties: {
AliasTarget: { DNSName: ref, HostedZoneId },
Comment: "Created by @webda/aws/cloudformation",
Type: "A",
Name: domain,
HostedZoneId: zone.Id
}
};
}
async importOpenApi(openapi) {
let apigateway = new APIGateway({});
console.log("Creating API Gateway");
await apigateway.putRestApi({
body: Buffer.from(JSON.stringify(openapi)),
failOnWarnings: false,
restApiId: this.resources.APIGatewayImportOpenApi,
mode: "overwrite"
});
}
/**
* Create the stack changeset
* @param cloudformation
* @returns
*/
async createCloudFormationChangeSet(cloudformation) {
let changeSetParams = {
...this.resources.StackOptions,
StackName: this.resources.StackName,
ChangeSetName: "WebdaCloudFormationDeployer",
Capabilities: ["CAPABILITY_IAM", "CAPABILITY_NAMED_IAM"],
Tags: this.getDefaultTags("StackOptions"),
TemplateURL: `https://${this.result.CloudFormation.Bucket}.s3.amazonaws.com/${this.result.CloudFormation.Key}`,
ResourcesToImport: this.resources.ResourcesToImport
};
let changeSet;
try {
changeSet = await cloudformation.createChangeSet({
...changeSetParams,
ChangeSetType: this.resources.ChangeSetType === "IMPORT" ? "IMPORT" : "UPDATE"
});
}
catch (err) {
if (err.message.endsWith(" is in ROLLBACK_COMPLETE state and can not be updated.")) {
this.logger.log("WARN", "Deleting buguous stack");
await this.deleteCloudFormation();
changeSet = await cloudformation.createChangeSet({
...changeSetParams,
ChangeSetType: this.resources.ChangeSetType
});
}
else if (err.message === `Stack [${this.resources.StackName}] does not exist`) {
changeSet = await cloudformation.createChangeSet({
...changeSetParams,
ChangeSetType: this.resources.ChangeSetType
});
}
else if (err.message.endsWith(" state and can not be updated.")) {
await this.waitFor(async (resolve) => {
let res = await cloudformation.describeStacks({
StackName: this.resources.StackName
});
if (res.Stacks.length === 0) {
// If it disapeared, recreate
changeSet = await cloudformation.createChangeSet({
...changeSetParams,
ChangeSetType: this.resources.ChangeSetType
});
resolve();
return true;
}
if (res.Stacks[0].StackStatus.endsWith("COMPLETE")) {
changeSet = await cloudformation.createChangeSet({
...changeSetParams,
ChangeSetType: this.resources.ChangeSetType === "IMPORT" ? "IMPORT" : "UPDATE"
});
resolve();
return true;
}
return false;
}, 50, "Waiting for COMPLETE state", 5000);
}
else if (err.code === "AlreadyExistsException") {
this.logger.log("WARN", "ChangeSet exists and need to be clean");
await cloudformation.deleteChangeSet({
StackName: this.resources.StackName,
ChangeSetName: "WebdaCloudFormationDeployer"
});
changeSet = await cloudformation.createChangeSet({
...changeSetParams,
ChangeSetType: this.resources.ChangeSetType === "IMPORT" ? "IMPORT" : "UPDATE"
});
}
else {
throw err;
}
}
return changeSet;
}
/**
* Upload and create the cloudformation stack or update it
* @returns
*/
async createCloudFormation() {
let cloudformation = new CloudFormation({});
let changeSet = await this.createCloudFormationChangeSet(cloudformation);
// Wait for change set
this.logger.log("TRACE", `ChangeSet: ${changeSet}`);
// It will trigger an exception if timeout
let changes = await this.waitFor(async (resolve) => {
let localChanges = await cloudformation.describeChangeSet({
ChangeSetName: "WebdaCloudFormationDeployer",
StackName: this.resources.StackName
});
if (localChanges.Status === "FAILED" || localChanges.Status === "CREATE_COMPLETE") {
resolve(localChanges);
return true;
}
}, 50, "Waiting for ChangeSet to be ready...", 5000);
if (changes.Status === "FAILED") {
if (changes.StatusReason ===
"The submitted information didn't contain changes. Submit different information to create a change set.") {
this.logger.log("INFO", "No changes to be made");
}
else {
this.logger.log("ERROR", "Cannot execute ChangeSet:", changes.StatusReason);
}
return;
}
changes.Changes.filter(j => j.Type === "Resource").forEach(({ ResourceChange: info }) => this.logger.log("INFO", `${info.Action.toUpperCase().padEnd(38)} ${info.ResourceType.padEnd(30)} ${info.LogicalResourceId}`));
this.logger.log("INFO", "Executing Change Set");
let lastEvent = (await cloudformation.describeStackEvents({
StackName: this.resources.StackName
})).StackEvents.shift();
await cloudformation.executeChangeSet({
ChangeSetName: "WebdaCloudFormationDeployer",
StackName: this.resources.StackName
});
// Wait for the completion of change and display events in the meantime
this.logger.log("INFO", "Waiting for update completion");
let i = 0;
let Timeout = true;
do {
let events = (await cloudformation.describeStackEvents({
StackName: this.resources.StackName
})).StackEvents;
let display = lastEvent ? false : true;
let event;
while ((event = events.pop())) {
if (display) {
this.logger.log("INFO", `${event.ResourceStatus.padEnd(38)} ${event.ResourceType.padEnd(30)} ${event.LogicalResourceId}`);
lastEvent = event;
if (lastEvent.LogicalResourceId === this.resources.StackName &&
(lastEvent.ResourceStatus === "UPDATE_COMPLETE" ||
lastEvent.ResourceStatus === "CREATE_COMPLETE" ||
lastEvent.ResourceStatus === "ROLLBACK_COMPLETE" ||
lastEvent.ResourceStatus === "UPDATE_ROLLBACK_COMPLETE")) {
Timeout = false;
break;
}
}
else if (event.EventId === lastEvent.EventId) {
display = true;
}
}
// If we are done do not pause
if (Timeout) {
await this.sleep(10);
i++;
}
} while (i < 60 && Timeout);
if (Timeout) {
this.logger.log("WARN", "Timeout while waiting for stack to update");
return;
}
/*
CloudFormation does not update Stage when resources are modified
https://stackoverflow.com/questions/41423439/cloudformation-doesnt-deploy-to-api-gateway-stages-on-update
*/
if (this.APIGateway) {
let NextToken = undefined;
let stageName;
let restApiId;
// Retrieve resources
do {
let res = await cloudformation.listStackResources({
StackName: this.resources.StackName,
NextToken
});
NextToken = res.NextToken;
res.StackResourceSummaries.forEach(resource => {
if (resource.ResourceType === "AWS::ApiGateway::Stage") {
stageName = resource.PhysicalResourceId;
}
else if (resource.ResourceType === "AWS::ApiGateway::RestApi") {
restApiId = resource.PhysicalResourceId;
}
});
} while (NextToken);
// Get RestAPIId
if (restApiId && stageName) {
const gw = new APIGateway({});
await gw.createDeployment({
restApiId,
stageName
});
}
}
}
/**
* Pause for x seconds
* @param seconds
*/
async sleep(sec) {
await new Promise(resolve => setTimeout(resolve, sec * 1000));
}
async Resources() {
let services = this.manager.getWebda().getServices();
// Build policy
for (let i in services) {
if ("getCloudFormation" in services[i]) {
// Update to match recuring policy - might need to split if policy too big
let res = services[i].getCloudFormation(this);
for (let j in res) {
this.template.Resources[`Service${i}_${j}`] = res[j];
}
}
}
}
getRegion() {
return "us-east-1";
}
async completeOpenAPI(openapi) {
openapi.info.title = this.resources.OpenAPITitle;
let info = await this.getAWSIdentity();
let arn = `arn:aws:lambda:${this.getRegion()}:${info.Account}:function:${this.resources.Lambda.FunctionName}`;
for (let p in openapi.paths) {
// Not using mockCors as the {@link RequestFilter.checkRequest} can be dynamic
// Invalid mapping expression parameter specified: method.response.header.Access-Control-Allow-Credentials
if (!openapi.paths[p]["options"]) {
openapi.paths[p]["options"] = {};
}
for (let m in openapi.paths[p]) {
openapi.paths[p][m]["x-amazon-apigateway-integration"] = {
httpMethod: "POST",
uri: `arn:aws:apigateway:${this.getRegion()}:lambda:path/2015-03-31/functions/${arn}/invocations`,
type: "aws_proxy"
};
}
}
return openapi;
}
getStringified(object, filename, addPrefix = true) {
let key = addPrefix ? `${this.resources.AssetsPrefix}${filename}` : filename;
if (key.startsWith("/")) {
key = key.substring(1);
}
let src;
// Default to Format parameter
let format = this.resources.Format;
// Auto guess format
if (filename.endsWith(".yml") || filename.endsWith(".yaml")) {
format = "YAML";
}
else if (filename.endsWith(".json")) {
format = "JSON";
}
if (format === "YAML") {
src = Buffer.from(YAML.stringify(object));
if (!key.endsWith(".yml") && !key.endsWith(".yaml")) {
key += ".yml";
}
}
else {
src = Buffer.from(JSON.stringify(object, undefined, 2));
if (!key.endsWith(".json")) {
key += ".json";
}
}
return {
key,
src
};
}
async APIGateway() {
this.template.Resources.APIGateway = {
Type: "AWS::ApiGateway::RestApi",
Properties: {
...this.resources.APIGateway,
Tags: this.getDefaultTags("APIGateway"),
BodyS3Location: {
Bucket: this.resources.AssetsBucket,
Key: this.openapiS3Object.key
}
},
DependsOn: "LambdaFunction"
};
this.template.Resources.LambdaApiGatewayPermission = {
Type: "AWS::Lambda::Permission",
Properties: {
Action: "lambda:InvokeFunction",
FunctionName: this.resources.Lambda.FunctionName,
Principal: "apigateway.amazonaws.com",
SourceArn: {
"Fn::Join": [
":",
[
"arn:aws:execute-api",
{ Ref: "AWS::Region" },
{ Ref: "AWS::AccountId" },
{ "Fn::Join": ["", [{ Ref: "APIGateway" }, "/*"]] }
]
]
}
},
DependsOn: "LambdaFunction"
};
this.template.Resources.APIGatewayDeployment = {
Type: "AWS::ApiGateway::Deployment",
Properties: {
...this.resources.APIGatewayDeployment,
RestApiId: { Ref: "APIGateway" }
}
};
// SourceArn: "arn:aws:execute-api:" + AWS.config.region + ":" + awsId + ":" + this.restApiId + "/*"
this.template.Resources.APIGatewayStage = {
Type: "AWS::ApiGateway::Stage",
Properties: {
...this.resources.APIGatewayStage,
Tags: this.getDefaultTags("APIGatewayStage"),
DeploymentId: { Ref: "APIGatewayDeployment" },
RestApiId: { Ref: "APIGateway" }
}
};
}
async APIGatewayDomain() {
let region;
if (this.resources.APIGatewayDomain.EndpointConfiguration) {
if (this.resources.APIGatewayDomain.EndpointConfiguration.Types.indexOf("EDGE") >= 0) {
region = "us-east-1";
}
}
else {
region = "us-east-1";
}
if (!this.resources.APIGatewayDomain.CertificateArn) {
this.resources.APIGatewayDomain.CertificateArn = (await this.getCertificate(this.resources.APIGatewayDomain.DomainName, region)).CertificateArn;
}
this.template.Resources.APIGatewayDomain = {
Type: "AWS::ApiGateway::DomainName",
Properties: {
...this.resources.APIGatewayDomain,
Tags: this.getDefaultTags("APIGatewayDomain")
}
};
this.template.Resources.APIGatewayBasePathMapping = {
Type: "AWS::ApiGateway::BasePathMapping",
Properties: {
...this.resources.APIGatewayBasePathMapping,
DomainName: { Ref: "APIGatewayDomain" },
RestApiId: { Ref: "APIGateway" },
Stage: { Ref: "APIGatewayStage" }
}
};
// Should do conditional with RegionalDomainName/RegionalHostedZoneId
await this.createCloudFormationDNSEntry({ "Fn::GetAtt": ["APIGatewayDomain", "DistributionDomainName"] }, this.resources.APIGatewayDomain.DomainName, { "Fn::GetAtt": ["APIGatewayDomain", "DistributionHostedZoneId"] });
}
async Policy() {
let PolicyDocument = JSON.stringify(await this.getPolicyDocument(this.resources.Policy.PolicyDocument.Statement), undefined, 2);
this.template.Resources.Policy = {
Type: "AWS::IAM::Policy",
Properties: {
...this.resources.Policy,
PolicyDocument
}
};
}
addAssumeRolePolicyStatement(...statements) {
// If we have a Role generation only
if (this.resources.Role) {
this.resources.Role.AssumeRolePolicyDocument.Statement.push(...statements);
}
}
async Role() {
// add auto generation
let AssumeRolePolicyDocument = JSON.stringify(this.resources.Role.AssumeRolePolicyDocument, undefined, 2);
this.template.Resources.Role = {
Type: "AWS::IAM::Role",
Properties: {
...this.resources.Role,
AssumeRolePolicyDocument,
Tags: this.getDefaultTags("Role")
}
};
}
async Lambda() {
const Code = await this.generateLambdaPackage();
this.addAssumeRolePolicyStatement({
Effect: "Allow",
Principal: { Service: "lambda.amazonaws.com" },
Action: "sts:AssumeRole"
});
this.template.Resources.LambdaFunction = {
Type: "AWS::Lambda::Function",
Properties: {
...this.resources.Lambda,
Code,
Tags: this.getDefaultTags("Lambda")
}
};
}
async buildDocker() {
// Launch the Docker deployment
await this.manager.run("WebdaDeployer/Docker", {
...this.resources.Docker
});
}
async Fargate() {
this.addAssumeRolePolicyStatement({
Effect: "Allow",
Principal: { Service: "ecs-tasks.amazonaws.com" },
Action: "sts:AssumeRole"
});
}
async generateLambdaPackage() {
const { AssetsBucket: S3Bucket, LambdaPackager, AssetsPrefix = "", KeepPackage = false } = this.resources;
const { zipPath: ZipPath } = LambdaPackager;
let result = {
S3Bucket,
S3Key: AssetsPrefix + path.basename(ZipPath)
};
// Create the package
await this.manager.run("WebdaAWSDeployer/LambdaPackager", LambdaPackager);
// Copy package to S3
await this.putFilesOnBucket(result.S3Bucket, [
{
key: result.S3Key,
src: ZipPath
}
]);
/* c8 ignore next 3 */
if (!KeepPackage) {
fs.unlinkSync(ZipPath);
}
return result;
}
/**
* Init the project on AWS
*
* It creates a CloudFormation stack with one ECR and one Bucket
* and save their name in package.json(webda.aws)
*
* If you want to use your own ECR and S3 for assets just add
* them inside your package.json with `webda.aws.AssetsBucket` and
* `webda.aws.Repository`
*
* @param console
* @returns
*/
static async init(console) {
let packageDescr = console.app.getAppPath("package.json");
if (!fs.existsSync(packageDescr)) {
console.log("ERROR", "package.json not found");
return -1;
}
let pkg = JSONUtils.loadFile(packageDescr);
pkg.webda = pkg.webda || {};
let sts = new STS({});
let identity;
try {
identity = await sts.getCallerIdentity({});
}
catch (ex) {
console.log("ERROR", "Cannot retrieve your AWS credentials, make sure to have a correct AWS setup");
return -1;
}
let pkgName = (pkg.name || "").replace(/@/g, "").replace(/\//g, "-");
let StackName = `webda-${pkgName}-global`;
if (!pkg.webda.aws) {
console.log("INFO", `This will create a small CloudFormation on your AWS account (${identity.Account}) and region (${sts.config.region || process.env.AWS_DEFAULT_REGION || "us-east-1"})`);
console.log("INFO", `The stack will be called ${StackName}`);
pkg.webda.aws = {
AssetsBucket: `webda-${pkgName}-assets`,
Repository: `webda-${pkgName}`
};
let cloudformation = new CloudFormation({});
await cloudformation.createStack({
StackName,
TemplateBody: JSON.stringify({
Resources: {
WebdaAssetsBucket: {
Type: "AWS::S3::Bucket",
Properties: {
BucketName: pkg.webda.aws.AssetsBucket
}
},
WebdaECR: {
Type: "AWS::ECR::Repository",
Properties: {
RepositoryName: pkg.webda.aws.Repository
}
}
}
})
});
console.log("INFO", "Updating package description with default information");
JSONUtils.saveFile(pkg, packageDescr);
return 0;
}
else {
// Check if stack already exists
console.log("WARN", "Default information are already in your package.json");
return -1;
}
}
}
export { CloudFormationDeployer, LAMBDA_LATEST_VERSION };
//# sourceMappingURL=cloudformation.js.map