@webda/aws 4.0.0-beta.3 → 4.0.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/deployers/awsdeployer.d.ts +340 -0
- package/lib/deployers/awsdeployer.d.ts.map +1 -0
- package/lib/deployers/awsdeployer.js +673 -0
- package/lib/deployers/awsdeployer.js.map +1 -0
- package/lib/deployers/cloudformation.service.d.ts +438 -0
- package/lib/deployers/cloudformation.service.d.ts.map +1 -0
- package/lib/deployers/cloudformation.service.js +889 -0
- package/lib/deployers/cloudformation.service.js.map +1 -0
- package/lib/deployers/index.d.ts +5 -0
- package/lib/deployers/index.d.ts.map +1 -0
- package/lib/deployers/index.js +5 -0
- package/lib/deployers/index.js.map +1 -0
- package/lib/deployers/lambda-entrypoint.d.ts +34 -0
- package/lib/deployers/lambda-entrypoint.d.ts.map +1 -0
- package/lib/deployers/lambda-entrypoint.js +47 -0
- package/lib/deployers/lambda-entrypoint.js.map +1 -0
- package/lib/deployers/lambdapackager.service.d.ts +123 -0
- package/lib/deployers/lambdapackager.service.d.ts.map +1 -0
- package/lib/deployers/lambdapackager.service.js +149 -0
- package/lib/deployers/lambdapackager.service.js.map +1 -0
- package/lib/deployers/zip.d.ts +42 -0
- package/lib/deployers/zip.d.ts.map +1 -0
- package/lib/deployers/zip.js +129 -0
- package/lib/deployers/zip.js.map +1 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/services/contributors.d.ts +1 -2
- package/lib/services/contributors.d.ts.map +1 -1
- package/lib/services/dynamodb.service.d.ts +2 -1
- package/lib/services/dynamodb.service.d.ts.map +1 -1
- package/lib/services/dynamodb.service.js +5 -4
- package/lib/services/dynamodb.service.js.map +1 -1
- package/lib/services/s3binary.service.d.ts.map +1 -1
- package/lib/services/s3binary.service.js +21 -20
- package/lib/services/s3binary.service.js.map +1 -1
- package/package.json +12 -14
- package/webda.module.json +698 -4
|
@@ -0,0 +1,889 @@
|
|
|
1
|
+
var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
|
|
2
|
+
var useValue = arguments.length > 2;
|
|
3
|
+
for (var i = 0; i < initializers.length; i++) {
|
|
4
|
+
value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
5
|
+
}
|
|
6
|
+
return useValue ? value : void 0;
|
|
7
|
+
};
|
|
8
|
+
var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
|
|
9
|
+
function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
|
|
10
|
+
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
11
|
+
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
12
|
+
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
13
|
+
var _, done = false;
|
|
14
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
15
|
+
var context = {};
|
|
16
|
+
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
17
|
+
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
18
|
+
context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
|
|
19
|
+
var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
|
|
20
|
+
if (kind === "accessor") {
|
|
21
|
+
if (result === void 0) continue;
|
|
22
|
+
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
23
|
+
if (_ = accept(result.get)) descriptor.get = _;
|
|
24
|
+
if (_ = accept(result.set)) descriptor.set = _;
|
|
25
|
+
if (_ = accept(result.init)) initializers.unshift(_);
|
|
26
|
+
}
|
|
27
|
+
else if (_ = accept(result)) {
|
|
28
|
+
if (kind === "field") initializers.unshift(_);
|
|
29
|
+
else descriptor[key] = _;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
33
|
+
done = true;
|
|
34
|
+
};
|
|
35
|
+
import { APIGateway } from "@aws-sdk/client-api-gateway";
|
|
36
|
+
import { CloudFormation } from "@aws-sdk/client-cloudformation";
|
|
37
|
+
import { Command, useApplication, useRouter, WebdaError } from "@webda/core";
|
|
38
|
+
import * as fs from "node:fs";
|
|
39
|
+
import * as path from "node:path";
|
|
40
|
+
import * as YAML from "yaml";
|
|
41
|
+
import { AWSDeployer, AWSDeployerParameters } from "./awsdeployer.js";
|
|
42
|
+
import { createLambdaPackage, LAMBDA_DEFAULT_HANDLER } from "./lambdapackager.service.js";
|
|
43
|
+
/**
|
|
44
|
+
* Default runtime of the Lambda function
|
|
45
|
+
*/
|
|
46
|
+
export const LAMBDA_LATEST_VERSION = "nodejs22.x";
|
|
47
|
+
/**
|
|
48
|
+
* Sections of the template, in the order they are generated
|
|
49
|
+
*
|
|
50
|
+
* Lambda and Fargate add statements to the Role assume policy so they come before the Role
|
|
51
|
+
*/
|
|
52
|
+
export const CLOUDFORMATION_SECTIONS = [
|
|
53
|
+
"Resources",
|
|
54
|
+
"Lambda",
|
|
55
|
+
"Fargate",
|
|
56
|
+
"Role",
|
|
57
|
+
"Policy",
|
|
58
|
+
"APIGateway",
|
|
59
|
+
"APIGatewayDomain"
|
|
60
|
+
];
|
|
61
|
+
/**
|
|
62
|
+
* CloudFormation deployer parameters
|
|
63
|
+
*
|
|
64
|
+
* Define how to generate CloudFormation resources
|
|
65
|
+
*/
|
|
66
|
+
export class CloudFormationDeployerParameters extends AWSDeployerParameters {
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Deploy the application and its resources using AWS CloudFormation
|
|
70
|
+
*
|
|
71
|
+
* Declared as a unit of a deployment:
|
|
72
|
+
* ```json
|
|
73
|
+
* {
|
|
74
|
+
* "units": [{
|
|
75
|
+
* "name": "MyStack",
|
|
76
|
+
* "type": "Webda/CloudFormationDeployer",
|
|
77
|
+
* "AssetsBucket": "my-assets",
|
|
78
|
+
* "Lambda": {},
|
|
79
|
+
* "APIGateway": {}
|
|
80
|
+
* }]
|
|
81
|
+
* }
|
|
82
|
+
* ```
|
|
83
|
+
* then `webda -d <deployment> deploy`
|
|
84
|
+
*
|
|
85
|
+
* @WebdaModda
|
|
86
|
+
*/
|
|
87
|
+
let CloudFormationDeployer = (() => {
|
|
88
|
+
var _a;
|
|
89
|
+
let _classSuper = AWSDeployer;
|
|
90
|
+
let _instanceExtraInitializers = [];
|
|
91
|
+
let _deploy_decorators;
|
|
92
|
+
return _a = class CloudFormationDeployer extends _classSuper {
|
|
93
|
+
constructor() {
|
|
94
|
+
super(...arguments);
|
|
95
|
+
/**
|
|
96
|
+
* The template being generated
|
|
97
|
+
*/
|
|
98
|
+
this.template = (__runInitializers(this, _instanceExtraInitializers), {});
|
|
99
|
+
/**
|
|
100
|
+
* Result of the deployment
|
|
101
|
+
*/
|
|
102
|
+
this.result = {};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* @override
|
|
106
|
+
*/
|
|
107
|
+
async defaultResources() {
|
|
108
|
+
var _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t;
|
|
109
|
+
await super.defaultResources();
|
|
110
|
+
const resources = this.resources;
|
|
111
|
+
const app = useApplication();
|
|
112
|
+
const packageDesc = app.getPackageDescription() ?? {};
|
|
113
|
+
const awsDefaults = packageDesc.webda?.aws ?? {};
|
|
114
|
+
if (resources.ResourcesToImport) {
|
|
115
|
+
resources.ChangeSetType ?? (resources.ChangeSetType = "IMPORT");
|
|
116
|
+
if (resources.ChangeSetType !== "IMPORT") {
|
|
117
|
+
throw new WebdaError.CodeError("CHANGESET_TYPE", "ChangeSetType cannot be anything else than IMPORT if you have ResourcesToImport set");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
resources.ChangeSetType ?? (resources.ChangeSetType = "CREATE");
|
|
121
|
+
resources.AssetsBucket ?? (resources.AssetsBucket = awsDefaults.AssetsBucket);
|
|
122
|
+
if (!resources.AssetsBucket) {
|
|
123
|
+
throw new WebdaError.CodeError("ASSETS_BUCKET_REQUIRED", "AssetsBucket must be defined");
|
|
124
|
+
}
|
|
125
|
+
resources.AssetsPrefix ?? (resources.AssetsPrefix = "${deployment}/${deployer.name}/");
|
|
126
|
+
resources.Description ?? (resources.Description = "Deployed by @webda/aws/cloudformation");
|
|
127
|
+
resources.FileName ?? (resources.FileName = `cloudformation-${resources.name}`);
|
|
128
|
+
resources.StackName ?? (resources.StackName = resources.name);
|
|
129
|
+
resources.Format ?? (resources.Format = "JSON");
|
|
130
|
+
resources.OpenAPIFileName ?? (resources.OpenAPIFileName = "${resources.name}-openapi-${package.version}");
|
|
131
|
+
resources.OpenAPITitle ?? (resources.OpenAPITitle = resources.name);
|
|
132
|
+
resources.CustomResources ?? (resources.CustomResources = {});
|
|
133
|
+
// Default Lambda value
|
|
134
|
+
if (resources.Lambda) {
|
|
135
|
+
resources.LambdaPackager ?? (resources.LambdaPackager = {});
|
|
136
|
+
(_b = resources.LambdaPackager).zipPath ?? (_b.zipPath = "dist/lambda-${package.version}.zip");
|
|
137
|
+
(_c = resources.Lambda).Runtime ?? (_c.Runtime = LAMBDA_LATEST_VERSION);
|
|
138
|
+
(_d = resources.Lambda).MemorySize ?? (_d.MemorySize = 2048);
|
|
139
|
+
(_e = resources.Lambda).Timeout ?? (_e.Timeout = 30);
|
|
140
|
+
(_f = resources.Lambda).Handler ?? (_f.Handler = LAMBDA_DEFAULT_HANDLER);
|
|
141
|
+
(_g = resources.Lambda).FunctionName ?? (_g.FunctionName = resources.name);
|
|
142
|
+
if (!resources.Lambda.Role) {
|
|
143
|
+
resources.Lambda.Role = { "Fn::GetAtt": ["Role", "Arn"] };
|
|
144
|
+
// If no role is specified auto enable Role and Policy creation
|
|
145
|
+
resources.Role ?? (resources.Role = {});
|
|
146
|
+
resources.Policy ?? (resources.Policy = {});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Default Role
|
|
150
|
+
if (resources.Role) {
|
|
151
|
+
(_h = resources.Role).RoleName ?? (_h.RoleName = `${resources.name}Role`);
|
|
152
|
+
if (!resources.Role.Policies || resources.Role.Policies.length === 0) {
|
|
153
|
+
resources.Policy ?? (resources.Policy = {});
|
|
154
|
+
}
|
|
155
|
+
(_j = resources.Role).AssumeRolePolicyDocument ?? (_j.AssumeRolePolicyDocument = { Statement: [] });
|
|
156
|
+
(_k = resources.Role.AssumeRolePolicyDocument).Statement ?? (_k.Statement = []);
|
|
157
|
+
(_l = resources.Role.AssumeRolePolicyDocument).Version ?? (_l.Version = "2012-10-17");
|
|
158
|
+
}
|
|
159
|
+
// Default Policy
|
|
160
|
+
if (resources.Policy) {
|
|
161
|
+
(_m = resources.Policy).PolicyName ?? (_m.PolicyName = `${resources.name}Policy`);
|
|
162
|
+
(_o = resources.Policy).Roles ?? (_o.Roles = []);
|
|
163
|
+
if (resources.Role) {
|
|
164
|
+
resources.Policy.Roles.push({ Ref: "Role" });
|
|
165
|
+
}
|
|
166
|
+
(_p = resources.Policy).PolicyDocument ?? (_p.PolicyDocument = { Statement: [] });
|
|
167
|
+
}
|
|
168
|
+
if (resources.APIGatewayStage) {
|
|
169
|
+
(_q = resources.APIGatewayStage).StageName ?? (_q.StageName = app.getCurrentDeployment() || "default");
|
|
170
|
+
}
|
|
171
|
+
// Activate Domain
|
|
172
|
+
if (resources.APIGatewayDomain) {
|
|
173
|
+
(_r = resources.APIGatewayDomain).SecurityPolicy ?? (_r.SecurityPolicy = "TLS_1_2");
|
|
174
|
+
// Enable BasePathMapping if does not exist
|
|
175
|
+
resources.APIGatewayBasePathMapping ?? (resources.APIGatewayBasePathMapping = {});
|
|
176
|
+
}
|
|
177
|
+
// Default BasePathMapping
|
|
178
|
+
if (resources.APIGatewayBasePathMapping) {
|
|
179
|
+
(_s = resources.APIGatewayBasePathMapping).BasePath ?? (_s.BasePath = "");
|
|
180
|
+
(_t = resources.APIGatewayBasePathMapping).DomainName ?? (_t.DomainName = resources.APIGatewayDomain?.DomainName);
|
|
181
|
+
if (resources.APIGatewayBasePathMapping.DomainName?.endsWith(".")) {
|
|
182
|
+
resources.APIGatewayBasePathMapping.DomainName = resources.APIGatewayBasePathMapping.DomainName.substring(0, resources.APIGatewayBasePathMapping.DomainName.length - 1);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
resources.Statics ?? (resources.Statics = []);
|
|
186
|
+
resources.Statics.forEach(conf => {
|
|
187
|
+
if (!conf.AssetsPath) {
|
|
188
|
+
conf.AssetsPath = conf.Source;
|
|
189
|
+
if (conf.AssetsPath.startsWith("/")) {
|
|
190
|
+
conf.AssetsPath = conf.AssetsPath.substring(1);
|
|
191
|
+
}
|
|
192
|
+
if (!conf.AssetsPath.endsWith("/")) {
|
|
193
|
+
conf.AssetsPath += "/";
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (conf.CloudFront) {
|
|
197
|
+
const DistributionConfig = conf.CloudFront.DistributionConfig || {};
|
|
198
|
+
DistributionConfig.Aliases ?? (DistributionConfig.Aliases = []);
|
|
199
|
+
if (DistributionConfig.Aliases.indexOf(conf.DomainName) < 0) {
|
|
200
|
+
DistributionConfig.Aliases.push(conf.DomainName);
|
|
201
|
+
}
|
|
202
|
+
DistributionConfig.PriceClass ?? (DistributionConfig.PriceClass = "PriceClass_100");
|
|
203
|
+
DistributionConfig.Comment ?? (DistributionConfig.Comment = "Deployed with @webda/aws/cloudformation");
|
|
204
|
+
DistributionConfig.Enabled ?? (DistributionConfig.Enabled = true);
|
|
205
|
+
DistributionConfig.DefaultCacheBehavior ?? (DistributionConfig.DefaultCacheBehavior = {
|
|
206
|
+
AllowedMethods: ["GET", "HEAD"],
|
|
207
|
+
ViewerProtocolPolicy: "redirect-to-https",
|
|
208
|
+
TargetOriginId: conf.DomainName,
|
|
209
|
+
ForwardedValues: {
|
|
210
|
+
QueryString: false
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
DistributionConfig.Origins ?? (DistributionConfig.Origins = [
|
|
214
|
+
{
|
|
215
|
+
DomainName: `${resources.AssetsBucket}.s3.amazonaws.com`,
|
|
216
|
+
Id: conf.DomainName,
|
|
217
|
+
OriginPath: `/${conf.AssetsPath.substring(0, conf.AssetsPath.length - 1)}`,
|
|
218
|
+
S3OriginConfig: {}
|
|
219
|
+
}
|
|
220
|
+
]);
|
|
221
|
+
conf.CloudFront.DistributionConfig = DistributionConfig;
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Deploy the application with CloudFormation
|
|
227
|
+
*
|
|
228
|
+
* It packages the Lambda, uploads the assets, the OpenAPI definition and the template
|
|
229
|
+
* to the AssetsBucket, then creates or updates the stack.
|
|
230
|
+
*
|
|
231
|
+
* @returns the deployment result: the template location and content
|
|
232
|
+
*/
|
|
233
|
+
async deploy() {
|
|
234
|
+
await this.prepare();
|
|
235
|
+
await this.generateTemplate();
|
|
236
|
+
this.log("INFO", "Deploy with CloudFormation");
|
|
237
|
+
// Upload new version it
|
|
238
|
+
await this.sendCloudFormationTemplate();
|
|
239
|
+
// Load the stack
|
|
240
|
+
await this.createCloudFormation();
|
|
241
|
+
return this.result;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Upload the assets and generate the CloudFormation template
|
|
245
|
+
*
|
|
246
|
+
* @returns the template
|
|
247
|
+
*/
|
|
248
|
+
async generateTemplate() {
|
|
249
|
+
const { Description, AssetsBucket } = this.resources;
|
|
250
|
+
this.template = {
|
|
251
|
+
Description,
|
|
252
|
+
Resources: { ...this.resources.CustomResources }
|
|
253
|
+
};
|
|
254
|
+
this.result = {};
|
|
255
|
+
// Ensure S3 bucket exist
|
|
256
|
+
this.log("DEBUG", "Check assets bucket", AssetsBucket);
|
|
257
|
+
await this.createBucket(AssetsBucket);
|
|
258
|
+
// Export the OpenAPI definition
|
|
259
|
+
const openapi = await this.completeOpenAPI(useRouter().exportOpenAPI(false));
|
|
260
|
+
this.openapiS3Object = this.getStringified(openapi, this.resources.OpenAPIFileName);
|
|
261
|
+
await this.putFilesOnBucket(AssetsBucket, [this.openapiS3Object]);
|
|
262
|
+
// If APIGatewayImportOpenApi update REST API
|
|
263
|
+
if (this.resources.APIGatewayImportOpenApi) {
|
|
264
|
+
this.log("INFO", "Importing open api");
|
|
265
|
+
await this.importOpenApi(openapi);
|
|
266
|
+
}
|
|
267
|
+
this.log("INFO", "Uploading statics");
|
|
268
|
+
await this.uploadStatics();
|
|
269
|
+
for (const section of CLOUDFORMATION_SECTIONS) {
|
|
270
|
+
if (this.resources[section] !== undefined) {
|
|
271
|
+
this.log("TRACE", "Add CloudFormation Resource", section);
|
|
272
|
+
await this[section]();
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// Add any static
|
|
276
|
+
for (const info of this.resources.Statics) {
|
|
277
|
+
this.log("TRACE", "Add Static Resource", info.DomainName);
|
|
278
|
+
await this.createStatic(info);
|
|
279
|
+
}
|
|
280
|
+
return this.template;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Upload any asset to bucket
|
|
284
|
+
*/
|
|
285
|
+
async uploadStatics() {
|
|
286
|
+
for (const { Source, AssetsPath } of this.resources.Statics) {
|
|
287
|
+
await this.putFolderOnBucket(this.resources.AssetsBucket, useApplication().getPath(Source), AssetsPath);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Add the resources of a static website
|
|
292
|
+
*
|
|
293
|
+
* @param info - the static website
|
|
294
|
+
*/
|
|
295
|
+
async createStatic(info) {
|
|
296
|
+
const { DomainName, CloudFront, Bucket } = info;
|
|
297
|
+
const resPrefix = `Static${DomainName.replace(/\./g, "")}`;
|
|
298
|
+
if (Bucket) {
|
|
299
|
+
this.template.Resources[`${resPrefix}Bucket`] = {
|
|
300
|
+
Type: "AWS::S3::Bucket",
|
|
301
|
+
Properties: {
|
|
302
|
+
...Bucket,
|
|
303
|
+
BucketName: DomainName,
|
|
304
|
+
Tags: this.getDefaultTags(Bucket.Tags)
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
if (CloudFront) {
|
|
309
|
+
if (!CloudFront.DistributionConfig.ViewerCertificate) {
|
|
310
|
+
CloudFront.DistributionConfig.ViewerCertificate = {
|
|
311
|
+
AcmCertificateArn: (await this.getCertificate(DomainName, "us-east-1")).CertificateArn,
|
|
312
|
+
SslSupportMethod: "sni-only"
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
this.template.Resources[`${resPrefix}CloudFront`] = {
|
|
316
|
+
Type: "AWS::CloudFront::Distribution",
|
|
317
|
+
Properties: {
|
|
318
|
+
DistributionConfig: CloudFront.DistributionConfig,
|
|
319
|
+
Tags: this.getDefaultTags(CloudFront.Tags)
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
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
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Copy the CloudFormation template to the Assets bucket
|
|
328
|
+
*/
|
|
329
|
+
async sendCloudFormationTemplate() {
|
|
330
|
+
const res = this.getStringified(this.template, this.resources.FileName);
|
|
331
|
+
this.result.CloudFormation = {
|
|
332
|
+
Bucket: this.resources.AssetsBucket,
|
|
333
|
+
Key: res.key
|
|
334
|
+
};
|
|
335
|
+
this.result.CloudFormationContent = res.src.toString();
|
|
336
|
+
await this.putFilesOnBucket(this.resources.AssetsBucket, [res]);
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* CloudFormation client
|
|
340
|
+
* @returns the client
|
|
341
|
+
*/
|
|
342
|
+
getCloudFormation() {
|
|
343
|
+
return new CloudFormation(this.getClientConfig("CloudFormation"));
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* API Gateway client
|
|
347
|
+
* @returns the client
|
|
348
|
+
*/
|
|
349
|
+
getAPIGateway() {
|
|
350
|
+
return new APIGateway(this.getClientConfig("APIGateway"));
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Delete the CloudFormation stack
|
|
354
|
+
* @returns when the stack is deleted
|
|
355
|
+
*/
|
|
356
|
+
async deleteCloudFormation() {
|
|
357
|
+
const cloudformation = this.getCloudFormation();
|
|
358
|
+
await cloudformation.deleteStack({ StackName: this.resources.StackName });
|
|
359
|
+
return this.waitFor(async (resolve) => {
|
|
360
|
+
try {
|
|
361
|
+
await cloudformation.describeStacks({
|
|
362
|
+
StackName: this.resources.StackName
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
resolve();
|
|
367
|
+
return true;
|
|
368
|
+
}
|
|
369
|
+
return false;
|
|
370
|
+
}, 50, "Waiting on stack to be deleted", 5000);
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Add a Route53 alias record to the template
|
|
374
|
+
*
|
|
375
|
+
* @param ref - the alias target
|
|
376
|
+
* @param domain - the domain
|
|
377
|
+
* @param HostedZoneId - the zone of the alias target
|
|
378
|
+
*/
|
|
379
|
+
async createCloudFormationDNSEntry(ref, domain, HostedZoneId) {
|
|
380
|
+
const zone = await this.getZoneForDomainName(domain);
|
|
381
|
+
if (!zone) {
|
|
382
|
+
this.log("WARN", "Cannot find Route53 zone for", domain, ", you will have to create manually the CNAME");
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
// Possible conflict if my.name.domain.io and myn.ame.domain.io exists
|
|
386
|
+
this.template.Resources[`DNSEntry${domain.replace(/\./g, "")}`] = {
|
|
387
|
+
Type: "AWS::Route53::RecordSet",
|
|
388
|
+
Properties: {
|
|
389
|
+
AliasTarget: { DNSName: ref, HostedZoneId },
|
|
390
|
+
Comment: "Created by @webda/aws/cloudformation",
|
|
391
|
+
Type: "A",
|
|
392
|
+
Name: domain,
|
|
393
|
+
HostedZoneId: zone.Id
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Import the OpenAPI definition into an existing REST API
|
|
399
|
+
*
|
|
400
|
+
* @param openapi - the definition
|
|
401
|
+
*/
|
|
402
|
+
async importOpenApi(openapi) {
|
|
403
|
+
await this.getAPIGateway().putRestApi({
|
|
404
|
+
body: Buffer.from(JSON.stringify(openapi)),
|
|
405
|
+
failOnWarnings: false,
|
|
406
|
+
restApiId: this.resources.APIGatewayImportOpenApi,
|
|
407
|
+
mode: "overwrite"
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Create the stack changeset
|
|
412
|
+
* @param cloudformation - the client
|
|
413
|
+
* @returns the changeset
|
|
414
|
+
*/
|
|
415
|
+
async createCloudFormationChangeSet(cloudformation) {
|
|
416
|
+
const changeSetParams = {
|
|
417
|
+
...this.resources.StackOptions,
|
|
418
|
+
StackName: this.resources.StackName,
|
|
419
|
+
ChangeSetName: "WebdaCloudFormationDeployer",
|
|
420
|
+
Capabilities: ["CAPABILITY_IAM", "CAPABILITY_NAMED_IAM"],
|
|
421
|
+
Tags: this.getDefaultTags("StackOptions"),
|
|
422
|
+
TemplateURL: `https://${this.result.CloudFormation.Bucket}.s3.amazonaws.com/${this.result.CloudFormation.Key}`,
|
|
423
|
+
ResourcesToImport: this.resources.ResourcesToImport
|
|
424
|
+
};
|
|
425
|
+
const update = this.resources.ChangeSetType === "IMPORT" ? "IMPORT" : "UPDATE";
|
|
426
|
+
let changeSet;
|
|
427
|
+
try {
|
|
428
|
+
changeSet = await cloudformation.createChangeSet({
|
|
429
|
+
...changeSetParams,
|
|
430
|
+
ChangeSetType: update
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
catch (err) {
|
|
434
|
+
if (err.message.endsWith(" is in ROLLBACK_COMPLETE state and can not be updated.")) {
|
|
435
|
+
this.log("WARN", "Deleting buguous stack");
|
|
436
|
+
await this.deleteCloudFormation();
|
|
437
|
+
changeSet = await cloudformation.createChangeSet({
|
|
438
|
+
...changeSetParams,
|
|
439
|
+
ChangeSetType: this.resources.ChangeSetType
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
else if (err.message === `Stack [${this.resources.StackName}] does not exist`) {
|
|
443
|
+
changeSet = await cloudformation.createChangeSet({
|
|
444
|
+
...changeSetParams,
|
|
445
|
+
ChangeSetType: this.resources.ChangeSetType
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
else if (err.message.endsWith(" state and can not be updated.")) {
|
|
449
|
+
await this.waitFor(async (resolve) => {
|
|
450
|
+
const res = await cloudformation.describeStacks({
|
|
451
|
+
StackName: this.resources.StackName
|
|
452
|
+
});
|
|
453
|
+
if (res.Stacks.length === 0) {
|
|
454
|
+
// If it disapeared, recreate
|
|
455
|
+
changeSet = await cloudformation.createChangeSet({
|
|
456
|
+
...changeSetParams,
|
|
457
|
+
ChangeSetType: this.resources.ChangeSetType
|
|
458
|
+
});
|
|
459
|
+
resolve();
|
|
460
|
+
return true;
|
|
461
|
+
}
|
|
462
|
+
if (res.Stacks[0].StackStatus.endsWith("COMPLETE")) {
|
|
463
|
+
changeSet = await cloudformation.createChangeSet({
|
|
464
|
+
...changeSetParams,
|
|
465
|
+
ChangeSetType: update
|
|
466
|
+
});
|
|
467
|
+
resolve();
|
|
468
|
+
return true;
|
|
469
|
+
}
|
|
470
|
+
return false;
|
|
471
|
+
}, 50, "Waiting for COMPLETE state", 5000);
|
|
472
|
+
}
|
|
473
|
+
else if (err.name === "AlreadyExistsException" || err.code === "AlreadyExistsException") {
|
|
474
|
+
this.log("WARN", "ChangeSet exists and need to be clean");
|
|
475
|
+
await cloudformation.deleteChangeSet({
|
|
476
|
+
StackName: this.resources.StackName,
|
|
477
|
+
ChangeSetName: "WebdaCloudFormationDeployer"
|
|
478
|
+
});
|
|
479
|
+
changeSet = await cloudformation.createChangeSet({
|
|
480
|
+
...changeSetParams,
|
|
481
|
+
ChangeSetType: update
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
else {
|
|
485
|
+
throw err;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return changeSet;
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Upload and create the cloudformation stack or update it
|
|
492
|
+
*/
|
|
493
|
+
async createCloudFormation() {
|
|
494
|
+
const cloudformation = this.getCloudFormation();
|
|
495
|
+
const changeSet = await this.createCloudFormationChangeSet(cloudformation);
|
|
496
|
+
// Wait for change set
|
|
497
|
+
this.log("TRACE", "ChangeSet", changeSet);
|
|
498
|
+
// It will trigger an exception if timeout
|
|
499
|
+
const changes = await this.waitFor(async (resolve) => {
|
|
500
|
+
const localChanges = await cloudformation.describeChangeSet({
|
|
501
|
+
ChangeSetName: "WebdaCloudFormationDeployer",
|
|
502
|
+
StackName: this.resources.StackName
|
|
503
|
+
});
|
|
504
|
+
if (localChanges.Status === "FAILED" || localChanges.Status === "CREATE_COMPLETE") {
|
|
505
|
+
resolve(localChanges);
|
|
506
|
+
return true;
|
|
507
|
+
}
|
|
508
|
+
return false;
|
|
509
|
+
}, 50, "Waiting for ChangeSet to be ready...", 5000);
|
|
510
|
+
if (changes.Status === "FAILED") {
|
|
511
|
+
if (changes.StatusReason ===
|
|
512
|
+
"The submitted information didn't contain changes. Submit different information to create a change set.") {
|
|
513
|
+
this.log("INFO", "No changes to be made");
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
this.log("ERROR", "Cannot execute ChangeSet:", changes.StatusReason);
|
|
517
|
+
}
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
changes.Changes.filter(j => j.Type === "Resource").forEach(({ ResourceChange: info }) => this.log("INFO", `${info.Action.toUpperCase().padEnd(38)} ${info.ResourceType.padEnd(30)} ${info.LogicalResourceId}`));
|
|
521
|
+
this.log("INFO", "Executing Change Set");
|
|
522
|
+
let lastEvent = (await cloudformation.describeStackEvents({
|
|
523
|
+
StackName: this.resources.StackName
|
|
524
|
+
})).StackEvents.shift();
|
|
525
|
+
await cloudformation.executeChangeSet({
|
|
526
|
+
ChangeSetName: "WebdaCloudFormationDeployer",
|
|
527
|
+
StackName: this.resources.StackName
|
|
528
|
+
});
|
|
529
|
+
// Wait for the completion of change and display events in the meantime
|
|
530
|
+
this.log("INFO", "Waiting for update completion");
|
|
531
|
+
let i = 0;
|
|
532
|
+
let Timeout = true;
|
|
533
|
+
do {
|
|
534
|
+
const events = (await cloudformation.describeStackEvents({
|
|
535
|
+
StackName: this.resources.StackName
|
|
536
|
+
})).StackEvents;
|
|
537
|
+
let display = !lastEvent;
|
|
538
|
+
let event;
|
|
539
|
+
while ((event = events.pop())) {
|
|
540
|
+
if (display) {
|
|
541
|
+
this.log("INFO", `${event.ResourceStatus.padEnd(38)} ${event.ResourceType.padEnd(30)} ${event.LogicalResourceId}`);
|
|
542
|
+
lastEvent = event;
|
|
543
|
+
if (lastEvent.LogicalResourceId === this.resources.StackName &&
|
|
544
|
+
(lastEvent.ResourceStatus === "UPDATE_COMPLETE" ||
|
|
545
|
+
lastEvent.ResourceStatus === "CREATE_COMPLETE" ||
|
|
546
|
+
lastEvent.ResourceStatus === "ROLLBACK_COMPLETE" ||
|
|
547
|
+
lastEvent.ResourceStatus === "UPDATE_ROLLBACK_COMPLETE")) {
|
|
548
|
+
Timeout = false;
|
|
549
|
+
break;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
else if (event.EventId === lastEvent.EventId) {
|
|
553
|
+
display = true;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
// If we are done do not pause
|
|
557
|
+
if (Timeout) {
|
|
558
|
+
await this.sleep(10);
|
|
559
|
+
i++;
|
|
560
|
+
}
|
|
561
|
+
} while (i < 60 && Timeout);
|
|
562
|
+
if (Timeout) {
|
|
563
|
+
this.log("WARN", "Timeout while waiting for stack to update");
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
/*
|
|
567
|
+
CloudFormation does not update Stage when resources are modified
|
|
568
|
+
|
|
569
|
+
https://stackoverflow.com/questions/41423439/cloudformation-doesnt-deploy-to-api-gateway-stages-on-update
|
|
570
|
+
*/
|
|
571
|
+
if (this.resources.APIGateway) {
|
|
572
|
+
let NextToken = undefined;
|
|
573
|
+
let stageName;
|
|
574
|
+
let restApiId;
|
|
575
|
+
// Retrieve resources
|
|
576
|
+
do {
|
|
577
|
+
const res = await cloudformation.listStackResources({
|
|
578
|
+
StackName: this.resources.StackName,
|
|
579
|
+
NextToken
|
|
580
|
+
});
|
|
581
|
+
NextToken = res.NextToken;
|
|
582
|
+
res.StackResourceSummaries.forEach(resource => {
|
|
583
|
+
if (resource.ResourceType === "AWS::ApiGateway::Stage") {
|
|
584
|
+
stageName = resource.PhysicalResourceId;
|
|
585
|
+
}
|
|
586
|
+
else if (resource.ResourceType === "AWS::ApiGateway::RestApi") {
|
|
587
|
+
restApiId = resource.PhysicalResourceId;
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
} while (NextToken);
|
|
591
|
+
if (restApiId && stageName) {
|
|
592
|
+
await this.getAPIGateway().createDeployment({
|
|
593
|
+
restApiId,
|
|
594
|
+
stageName
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Pause for x seconds
|
|
601
|
+
* @param sec - seconds to wait
|
|
602
|
+
*/
|
|
603
|
+
async sleep(sec) {
|
|
604
|
+
await new Promise(resolve => setTimeout(resolve, sec * 1000));
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Add the CloudFormation contributions of the application services
|
|
608
|
+
*/
|
|
609
|
+
async Resources() {
|
|
610
|
+
for (const service of this.getApplicationServices()) {
|
|
611
|
+
if (typeof service.getCloudFormation === "function") {
|
|
612
|
+
const res = service.getCloudFormation(this);
|
|
613
|
+
for (const j in res) {
|
|
614
|
+
// Logical ids are alphanumeric
|
|
615
|
+
this.template.Resources[`Service${service.getName()}${j}`.replace(/[^A-Za-z0-9]/g, "")] = res[j];
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Add the API Gateway integration to the OpenAPI definition
|
|
622
|
+
*
|
|
623
|
+
* @param openapi - the definition
|
|
624
|
+
* @returns the definition
|
|
625
|
+
*/
|
|
626
|
+
async completeOpenAPI(openapi) {
|
|
627
|
+
var _b;
|
|
628
|
+
openapi.info.title = this.resources.OpenAPITitle;
|
|
629
|
+
if (!this.resources.Lambda) {
|
|
630
|
+
return openapi;
|
|
631
|
+
}
|
|
632
|
+
const info = await this.getAWSIdentity();
|
|
633
|
+
const arn = `arn:aws:lambda:${this.getRegion()}:${info.Account}:function:${this.resources.Lambda.FunctionName}`;
|
|
634
|
+
for (const p in openapi.paths) {
|
|
635
|
+
// Not using mockCors as the {@link RequestFilter.checkRequest} can be dynamic
|
|
636
|
+
(_b = openapi.paths[p])["options"] ?? (_b["options"] = {});
|
|
637
|
+
for (const m in openapi.paths[p]) {
|
|
638
|
+
openapi.paths[p][m]["x-amazon-apigateway-integration"] = {
|
|
639
|
+
httpMethod: "POST",
|
|
640
|
+
uri: `arn:aws:apigateway:${this.getRegion()}:lambda:path/2015-03-31/functions/${arn}/invocations`,
|
|
641
|
+
type: "aws_proxy"
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return openapi;
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Serialize an object for the AssetsBucket
|
|
649
|
+
*
|
|
650
|
+
* @param object - to serialize
|
|
651
|
+
* @param filename - name of the file, its extension forces the format
|
|
652
|
+
* @param addPrefix - add the AssetsPrefix
|
|
653
|
+
* @returns the key and content
|
|
654
|
+
*/
|
|
655
|
+
getStringified(object, filename, addPrefix = true) {
|
|
656
|
+
let key = addPrefix ? `${this.resources.AssetsPrefix}${filename}` : filename;
|
|
657
|
+
if (key.startsWith("/")) {
|
|
658
|
+
key = key.substring(1);
|
|
659
|
+
}
|
|
660
|
+
let src;
|
|
661
|
+
// Default to Format parameter
|
|
662
|
+
let format = this.resources.Format;
|
|
663
|
+
// Auto guess format
|
|
664
|
+
if (filename.endsWith(".yml") || filename.endsWith(".yaml")) {
|
|
665
|
+
format = "YAML";
|
|
666
|
+
}
|
|
667
|
+
else if (filename.endsWith(".json")) {
|
|
668
|
+
format = "JSON";
|
|
669
|
+
}
|
|
670
|
+
if (format === "YAML") {
|
|
671
|
+
src = Buffer.from(YAML.stringify(object));
|
|
672
|
+
if (!key.endsWith(".yml") && !key.endsWith(".yaml")) {
|
|
673
|
+
key += ".yml";
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
else {
|
|
677
|
+
src = Buffer.from(JSON.stringify(object, undefined, 2));
|
|
678
|
+
if (!key.endsWith(".json")) {
|
|
679
|
+
key += ".json";
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
return {
|
|
683
|
+
key,
|
|
684
|
+
src
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Add the API Gateway in front of the Lambda function
|
|
689
|
+
*/
|
|
690
|
+
async APIGateway() {
|
|
691
|
+
if (!this.resources.Lambda) {
|
|
692
|
+
throw new WebdaError.CodeError("LAMBDA_REQUIRED", "APIGateway requires a Lambda");
|
|
693
|
+
}
|
|
694
|
+
this.template.Resources.APIGateway = {
|
|
695
|
+
Type: "AWS::ApiGateway::RestApi",
|
|
696
|
+
Properties: {
|
|
697
|
+
...this.resources.APIGateway,
|
|
698
|
+
Tags: this.getDefaultTags("APIGateway"),
|
|
699
|
+
BodyS3Location: {
|
|
700
|
+
Bucket: this.resources.AssetsBucket,
|
|
701
|
+
Key: this.openapiS3Object.key
|
|
702
|
+
}
|
|
703
|
+
},
|
|
704
|
+
DependsOn: "LambdaFunction"
|
|
705
|
+
};
|
|
706
|
+
this.template.Resources.LambdaApiGatewayPermission = {
|
|
707
|
+
Type: "AWS::Lambda::Permission",
|
|
708
|
+
Properties: {
|
|
709
|
+
Action: "lambda:InvokeFunction",
|
|
710
|
+
FunctionName: this.resources.Lambda.FunctionName,
|
|
711
|
+
Principal: "apigateway.amazonaws.com",
|
|
712
|
+
SourceArn: {
|
|
713
|
+
"Fn::Join": [
|
|
714
|
+
":",
|
|
715
|
+
[
|
|
716
|
+
"arn:aws:execute-api",
|
|
717
|
+
{ Ref: "AWS::Region" },
|
|
718
|
+
{ Ref: "AWS::AccountId" },
|
|
719
|
+
{ "Fn::Join": ["", [{ Ref: "APIGateway" }, "/*"]] }
|
|
720
|
+
]
|
|
721
|
+
]
|
|
722
|
+
}
|
|
723
|
+
},
|
|
724
|
+
DependsOn: "LambdaFunction"
|
|
725
|
+
};
|
|
726
|
+
this.template.Resources.APIGatewayDeployment = {
|
|
727
|
+
Type: "AWS::ApiGateway::Deployment",
|
|
728
|
+
Properties: {
|
|
729
|
+
...this.resources.APIGatewayDeployment,
|
|
730
|
+
RestApiId: { Ref: "APIGateway" }
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
this.template.Resources.APIGatewayStage = {
|
|
734
|
+
Type: "AWS::ApiGateway::Stage",
|
|
735
|
+
Properties: {
|
|
736
|
+
...this.resources.APIGatewayStage,
|
|
737
|
+
Tags: this.getDefaultTags("APIGatewayStage"),
|
|
738
|
+
DeploymentId: { Ref: "APIGatewayDeployment" },
|
|
739
|
+
RestApiId: { Ref: "APIGateway" }
|
|
740
|
+
}
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Add the API Gateway custom domain
|
|
745
|
+
*/
|
|
746
|
+
async APIGatewayDomain() {
|
|
747
|
+
let region;
|
|
748
|
+
if (this.resources.APIGatewayDomain.EndpointConfiguration) {
|
|
749
|
+
if (this.resources.APIGatewayDomain.EndpointConfiguration.Types.indexOf("EDGE") >= 0) {
|
|
750
|
+
region = "us-east-1";
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
else {
|
|
754
|
+
region = "us-east-1";
|
|
755
|
+
}
|
|
756
|
+
if (!this.resources.APIGatewayDomain.CertificateArn) {
|
|
757
|
+
this.resources.APIGatewayDomain.CertificateArn = (await this.getCertificate(this.resources.APIGatewayDomain.DomainName, region)).CertificateArn;
|
|
758
|
+
}
|
|
759
|
+
this.template.Resources.APIGatewayDomain = {
|
|
760
|
+
Type: "AWS::ApiGateway::DomainName",
|
|
761
|
+
Properties: {
|
|
762
|
+
...this.resources.APIGatewayDomain,
|
|
763
|
+
Tags: this.getDefaultTags("APIGatewayDomain")
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
this.template.Resources.APIGatewayBasePathMapping = {
|
|
767
|
+
Type: "AWS::ApiGateway::BasePathMapping",
|
|
768
|
+
Properties: {
|
|
769
|
+
...this.resources.APIGatewayBasePathMapping,
|
|
770
|
+
DomainName: { Ref: "APIGatewayDomain" },
|
|
771
|
+
RestApiId: { Ref: "APIGateway" },
|
|
772
|
+
Stage: { Ref: "APIGatewayStage" }
|
|
773
|
+
}
|
|
774
|
+
};
|
|
775
|
+
// Should do conditional with RegionalDomainName/RegionalHostedZoneId
|
|
776
|
+
await this.createCloudFormationDNSEntry({ "Fn::GetAtt": ["APIGatewayDomain", "DistributionDomainName"] }, this.resources.APIGatewayDomain.DomainName, { "Fn::GetAtt": ["APIGatewayDomain", "DistributionHostedZoneId"] });
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Add the IAM policy of the application
|
|
780
|
+
*/
|
|
781
|
+
async Policy() {
|
|
782
|
+
const PolicyDocument = await this.getPolicyDocument(this.resources.Policy.PolicyDocument.Statement);
|
|
783
|
+
this.template.Resources.Policy = {
|
|
784
|
+
Type: "AWS::IAM::Policy",
|
|
785
|
+
Properties: {
|
|
786
|
+
...this.resources.Policy,
|
|
787
|
+
PolicyDocument
|
|
788
|
+
}
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Add statements to the generated Role assume policy
|
|
793
|
+
*
|
|
794
|
+
* @param statements - to add
|
|
795
|
+
*/
|
|
796
|
+
addAssumeRolePolicyStatement(...statements) {
|
|
797
|
+
// If we have a Role generation only
|
|
798
|
+
if (this.resources.Role) {
|
|
799
|
+
this.resources.Role.AssumeRolePolicyDocument.Statement.push(...statements);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Add the IAM role of the application
|
|
804
|
+
*/
|
|
805
|
+
async Role() {
|
|
806
|
+
this.template.Resources.Role = {
|
|
807
|
+
Type: "AWS::IAM::Role",
|
|
808
|
+
Properties: {
|
|
809
|
+
...this.resources.Role,
|
|
810
|
+
Tags: this.getDefaultTags("Role")
|
|
811
|
+
}
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Package the application and add the Lambda function
|
|
816
|
+
*/
|
|
817
|
+
async Lambda() {
|
|
818
|
+
const Code = await this.generateLambdaPackage();
|
|
819
|
+
this.addAssumeRolePolicyStatement({
|
|
820
|
+
Effect: "Allow",
|
|
821
|
+
Principal: { Service: "lambda.amazonaws.com" },
|
|
822
|
+
Action: "sts:AssumeRole"
|
|
823
|
+
});
|
|
824
|
+
this.template.Resources.LambdaFunction = {
|
|
825
|
+
Type: "AWS::Lambda::Function",
|
|
826
|
+
Properties: {
|
|
827
|
+
...this.resources.Lambda,
|
|
828
|
+
Code,
|
|
829
|
+
Tags: this.getDefaultTags("Lambda")
|
|
830
|
+
}
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* Allow the ECS tasks to use the generated Role
|
|
835
|
+
*/
|
|
836
|
+
async Fargate() {
|
|
837
|
+
this.addAssumeRolePolicyStatement({
|
|
838
|
+
Effect: "Allow",
|
|
839
|
+
Principal: { Service: "ecs-tasks.amazonaws.com" },
|
|
840
|
+
Action: "sts:AssumeRole"
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* Create the Lambda zip of the application
|
|
845
|
+
*
|
|
846
|
+
* @param options - the package options
|
|
847
|
+
* @returns the zip information
|
|
848
|
+
*/
|
|
849
|
+
async buildLambdaPackage(options) {
|
|
850
|
+
return createLambdaPackage(useApplication(), options);
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Create the Lambda package and upload it to the AssetsBucket
|
|
854
|
+
*
|
|
855
|
+
* @returns the S3 location of the package
|
|
856
|
+
*/
|
|
857
|
+
async generateLambdaPackage() {
|
|
858
|
+
const { AssetsBucket: S3Bucket, LambdaPackager, AssetsPrefix = "", KeepPackage = false } = this.resources;
|
|
859
|
+
const { zipPath } = await this.buildLambdaPackage(LambdaPackager);
|
|
860
|
+
const result = {
|
|
861
|
+
S3Bucket,
|
|
862
|
+
S3Key: AssetsPrefix + path.basename(zipPath)
|
|
863
|
+
};
|
|
864
|
+
// Copy package to S3
|
|
865
|
+
await this.putFilesOnBucket(result.S3Bucket, [
|
|
866
|
+
{
|
|
867
|
+
key: result.S3Key,
|
|
868
|
+
src: zipPath
|
|
869
|
+
}
|
|
870
|
+
]);
|
|
871
|
+
if (!KeepPackage) {
|
|
872
|
+
fs.unlinkSync(zipPath);
|
|
873
|
+
}
|
|
874
|
+
return result;
|
|
875
|
+
}
|
|
876
|
+
},
|
|
877
|
+
(() => {
|
|
878
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
879
|
+
_deploy_decorators = [Command("deploy", {
|
|
880
|
+
description: "Deploy the application with AWS CloudFormation",
|
|
881
|
+
requires: ["router", "rest-domain"]
|
|
882
|
+
})];
|
|
883
|
+
__esDecorate(_a, null, _deploy_decorators, { kind: "method", name: "deploy", static: false, private: false, access: { has: obj => "deploy" in obj, get: obj => obj.deploy }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
884
|
+
if (_metadata) Object.defineProperty(_a, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
885
|
+
})(),
|
|
886
|
+
_a;
|
|
887
|
+
})();
|
|
888
|
+
export { CloudFormationDeployer };
|
|
889
|
+
//# sourceMappingURL=cloudformation.service.js.map
|