carlin 0.19.0
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/CHANGELOG.md +378 -0
- package/bin/carlin +2 -0
- package/dist/cli.js +250 -0
- package/dist/config.js +10 -0
- package/dist/deploy/addDefaults.cloudFormation.js +138 -0
- package/dist/deploy/baseStack/command.js +9 -0
- package/dist/deploy/baseStack/config.js +30 -0
- package/dist/deploy/baseStack/deployBaseStack.js +61 -0
- package/dist/deploy/baseStack/getBaseStackResource.js +26 -0
- package/dist/deploy/baseStack/getBucket.template.js +44 -0
- package/dist/deploy/baseStack/getLambdaImageBuilder.template.js +188 -0
- package/dist/deploy/baseStack/getLambdaLayerBuilder.template.js +140 -0
- package/dist/deploy/baseStack/getVpc.template.js +169 -0
- package/dist/deploy/cicd/cicd.template.js +872 -0
- package/dist/deploy/cicd/command.js +84 -0
- package/dist/deploy/cicd/config.js +7 -0
- package/dist/deploy/cicd/deployCicd.js +114 -0
- package/dist/deploy/cicd/ecsTaskReportCommand.js +53 -0
- package/dist/deploy/cicd/getCicdStackName.js +11 -0
- package/dist/deploy/cicd/getTriggerPipelineObjectKey.js +12 -0
- package/dist/deploy/cicd/lambdas/cicdApiV1.handler.js +124 -0
- package/dist/deploy/cicd/lambdas/ecsTaskReport.handler.js +79 -0
- package/dist/deploy/cicd/lambdas/executeTasks.js +91 -0
- package/dist/deploy/cicd/lambdas/getProcessEnvVariable.js +10 -0
- package/dist/deploy/cicd/lambdas/githubWebhooksApiV1.handler.js +143 -0
- package/dist/deploy/cicd/lambdas/index.js +11 -0
- package/dist/deploy/cicd/lambdas/pipelines.handler.js +154 -0
- package/dist/deploy/cicd/lambdas/putApprovalResultManualTask.js +52 -0
- package/dist/deploy/cicd/pipelines.js +52 -0
- package/dist/deploy/cloudFormation.core.js +286 -0
- package/dist/deploy/cloudFormation.js +201 -0
- package/dist/deploy/command.js +211 -0
- package/dist/deploy/lambda.js +177 -0
- package/dist/deploy/lambdaLayer/command.js +51 -0
- package/dist/deploy/lambdaLayer/deployLambdaLayer.js +142 -0
- package/dist/deploy/s3.js +167 -0
- package/dist/deploy/stackName.js +78 -0
- package/dist/deploy/staticApp/command.js +101 -0
- package/dist/deploy/staticApp/getOriginShieldRegion.js +30 -0
- package/dist/deploy/staticApp/staticApp.js +206 -0
- package/dist/deploy/staticApp/staticApp.template.js +874 -0
- package/dist/deploy/utils.js +31 -0
- package/dist/index.js +7 -0
- package/dist/utils/addGroupToOptions.js +11 -0
- package/dist/utils/buildLambdaFiles.js +99 -0
- package/dist/utils/cloudFormationTemplate.js +134 -0
- package/dist/utils/codeBuild.js +52 -0
- package/dist/utils/environmentVariables.js +11 -0
- package/dist/utils/exec.js +24 -0
- package/dist/utils/formatCode.js +30 -0
- package/dist/utils/getAwsAccountId.js +10 -0
- package/dist/utils/getCurrentBranch.js +35 -0
- package/dist/utils/getEnvironment.js +6 -0
- package/dist/utils/getIamPath.js +6 -0
- package/dist/utils/getProjectName.js +28 -0
- package/dist/utils/index.js +26 -0
- package/dist/utils/packageJson.js +46 -0
- package/dist/utils/readCloudFormationTemplate.js +35 -0
- package/dist/utils/readObjectFile.js +50 -0
- package/package.json +83 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getTagCommands = exports.getMainCommands = exports.getClosedPrCommands = exports.getPrCommands = exports.pipelines = void 0;
|
|
4
|
+
exports.pipelines = ['pr', 'closed-pr', 'main', 'tag'];
|
|
5
|
+
const executeCommandFile = (pipeline) => `chmod +x ./cicd/commands/${pipeline} && ./cicd/commands/${pipeline}`;
|
|
6
|
+
const getPrCommands = ({ branch }) => [
|
|
7
|
+
'git status',
|
|
8
|
+
'git fetch',
|
|
9
|
+
/**
|
|
10
|
+
* Update to the most recent main branch to Lerna performs the diff properly.
|
|
11
|
+
*/
|
|
12
|
+
'git pull origin main',
|
|
13
|
+
`git checkout ${branch}`,
|
|
14
|
+
`git pull origin ${branch}`,
|
|
15
|
+
'git rev-parse HEAD',
|
|
16
|
+
'git status',
|
|
17
|
+
'yarn',
|
|
18
|
+
executeCommandFile('pr'),
|
|
19
|
+
];
|
|
20
|
+
exports.getPrCommands = getPrCommands;
|
|
21
|
+
const getClosedPrCommands = ({ branch }) => [
|
|
22
|
+
'git status',
|
|
23
|
+
'git fetch',
|
|
24
|
+
/**
|
|
25
|
+
* Get the most recent main because the PR was approved.
|
|
26
|
+
*/
|
|
27
|
+
'git pull origin main',
|
|
28
|
+
'git rev-parse HEAD',
|
|
29
|
+
`export CARLIN_BRANCH=${branch}`,
|
|
30
|
+
executeCommandFile('closed-pr'),
|
|
31
|
+
];
|
|
32
|
+
exports.getClosedPrCommands = getClosedPrCommands;
|
|
33
|
+
const getMainCommands = () => [
|
|
34
|
+
`export CARLIN_ENVIRONMENT=Staging`,
|
|
35
|
+
'git status',
|
|
36
|
+
'git fetch',
|
|
37
|
+
'git pull origin main',
|
|
38
|
+
'git rev-parse HEAD',
|
|
39
|
+
'yarn',
|
|
40
|
+
executeCommandFile('main'),
|
|
41
|
+
];
|
|
42
|
+
exports.getMainCommands = getMainCommands;
|
|
43
|
+
const getTagCommands = ({ tag }) => [
|
|
44
|
+
`export CARLIN_ENVIRONMENT=Production`,
|
|
45
|
+
'git status',
|
|
46
|
+
'git fetch --tags',
|
|
47
|
+
`git checkout tags/${tag} -b ${tag}-branch`,
|
|
48
|
+
'git rev-parse HEAD',
|
|
49
|
+
'yarn',
|
|
50
|
+
executeCommandFile('tag'),
|
|
51
|
+
];
|
|
52
|
+
exports.getTagCommands = getTagCommands;
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.destroy = exports.canDestroyStack = exports.deploy = exports.defaultTemplatePaths = exports.enableTerminationProtection = exports.updateStack = exports.createStack = exports.deleteStack = exports.printStackOutputsAfterDeploy = exports.getStackOutput = exports.describeStack = exports.describeStackEvents = exports.doesStackExist = exports.describeStackResource = exports.describeStacks = exports.cloudFormationV2 = exports.cloudFormation = void 0;
|
|
7
|
+
const client_cloudformation_1 = require("@aws-sdk/client-cloudformation");
|
|
8
|
+
const aws_sdk_1 = __importDefault(require("aws-sdk"));
|
|
9
|
+
const npmlog_1 = __importDefault(require("npmlog"));
|
|
10
|
+
const utils_1 = require("../utils");
|
|
11
|
+
const addDefaults_cloudFormation_1 = require("./addDefaults.cloudFormation");
|
|
12
|
+
const s3_1 = require("./s3");
|
|
13
|
+
const logPrefix = 'cloudformation';
|
|
14
|
+
npmlog_1.default.addLevel('event', 10000, { fg: 'yellow' });
|
|
15
|
+
npmlog_1.default.addLevel('output', 10000, { fg: 'blue' });
|
|
16
|
+
/**
|
|
17
|
+
* https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cloudformation-limits.html
|
|
18
|
+
*/
|
|
19
|
+
const TEMPLATE_BODY_MAX_SIZE = 51200;
|
|
20
|
+
const isTemplateBodyGreaterThanMaxSize = (template) => Buffer.byteLength(JSON.stringify(template), 'utf8') >= TEMPLATE_BODY_MAX_SIZE;
|
|
21
|
+
/**
|
|
22
|
+
* Update CloudFormation template to base stack bucket.
|
|
23
|
+
* @param input.stackName: CloudFormation stack name.
|
|
24
|
+
* @param input.template: CloudFormation template.
|
|
25
|
+
*/
|
|
26
|
+
const uploadTemplateToBaseStackBucket = async ({ stackName, template, }) => {
|
|
27
|
+
console.info({ stackName, template });
|
|
28
|
+
console.log('uploadTemplateToBaseStackBucket needs to be implemented.');
|
|
29
|
+
throw new Error();
|
|
30
|
+
// const key = getPepeBucketTemplateKey({ stackName });
|
|
31
|
+
// return uploadFileToS3({
|
|
32
|
+
// bucket: await getPepeBucketName(),
|
|
33
|
+
// contentType: 'application/json',
|
|
34
|
+
// file: Buffer.from(JSON.stringify(template, null, 2)),
|
|
35
|
+
// key,
|
|
36
|
+
// });
|
|
37
|
+
};
|
|
38
|
+
const cloudFormation = () => {
|
|
39
|
+
return new client_cloudformation_1.CloudFormationClient({
|
|
40
|
+
apiVersion: '2010-05-15',
|
|
41
|
+
region: utils_1.getEnvVar('REGION'),
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
exports.cloudFormation = cloudFormation;
|
|
45
|
+
const cloudFormationV2 = () => {
|
|
46
|
+
return new aws_sdk_1.default.CloudFormation({ apiVersion: '2010-05-15' });
|
|
47
|
+
};
|
|
48
|
+
exports.cloudFormationV2 = cloudFormationV2;
|
|
49
|
+
const describeStacks = async ({ stackName, } = {}) => {
|
|
50
|
+
const { Stacks } = await exports.cloudFormation().send(new client_cloudformation_1.DescribeStacksCommand({ StackName: stackName }));
|
|
51
|
+
return Stacks;
|
|
52
|
+
};
|
|
53
|
+
exports.describeStacks = describeStacks;
|
|
54
|
+
const describeStackResource = async (input) => {
|
|
55
|
+
return exports.cloudFormation().send(new client_cloudformation_1.DescribeStackResourceCommand(input));
|
|
56
|
+
};
|
|
57
|
+
exports.describeStackResource = describeStackResource;
|
|
58
|
+
const doesStackExist = async ({ stackName }) => {
|
|
59
|
+
npmlog_1.default.info(logPrefix, `Checking if stack ${stackName} already exists...`);
|
|
60
|
+
try {
|
|
61
|
+
await exports.describeStacks({ stackName });
|
|
62
|
+
npmlog_1.default.info(logPrefix, `Stack ${stackName} already exists.`);
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
if (err.Code === 'ValidationError') {
|
|
67
|
+
npmlog_1.default.info(logPrefix, `Stack ${stackName} does not exist.`);
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
exports.doesStackExist = doesStackExist;
|
|
74
|
+
const describeStackEvents = async ({ stackName, }) => {
|
|
75
|
+
npmlog_1.default.error(logPrefix, 'Stack events:');
|
|
76
|
+
const { StackEvents } = await exports.cloudFormation().send(new client_cloudformation_1.DescribeStackEventsCommand({ StackName: stackName }));
|
|
77
|
+
const events = (StackEvents || [])
|
|
78
|
+
.filter(({ Timestamp }) => Date.now() - Number(Timestamp) < 10 * 60 * 1000)
|
|
79
|
+
.filter(({ ResourceStatusReason }) => ResourceStatusReason)
|
|
80
|
+
/**
|
|
81
|
+
* Show newer events last.
|
|
82
|
+
*/
|
|
83
|
+
.reverse();
|
|
84
|
+
events.forEach(({ LogicalResourceId, ResourceStatusReason }) => npmlog_1.default.event(LogicalResourceId, ResourceStatusReason));
|
|
85
|
+
return events;
|
|
86
|
+
};
|
|
87
|
+
exports.describeStackEvents = describeStackEvents;
|
|
88
|
+
const describeStack = async ({ stackName }) => {
|
|
89
|
+
const stacks = await exports.describeStacks({ stackName });
|
|
90
|
+
if (!stacks) {
|
|
91
|
+
throw new Error(`Stack ${stackName} not found and cannot be described.`);
|
|
92
|
+
}
|
|
93
|
+
return stacks[0];
|
|
94
|
+
};
|
|
95
|
+
exports.describeStack = describeStack;
|
|
96
|
+
const getStackOutput = async ({ stackName, outputKey, }) => {
|
|
97
|
+
const { Outputs = [] } = await exports.describeStack({ stackName });
|
|
98
|
+
const output = Outputs === null || Outputs === void 0 ? void 0 : Outputs.find(({ OutputKey }) => OutputKey === outputKey);
|
|
99
|
+
if (!output) {
|
|
100
|
+
throw new Error(`Output ${outputKey} doesn't exist on ${stackName} stack`);
|
|
101
|
+
}
|
|
102
|
+
return output;
|
|
103
|
+
};
|
|
104
|
+
exports.getStackOutput = getStackOutput;
|
|
105
|
+
const printStackOutputsAfterDeploy = async ({ stackName, }) => {
|
|
106
|
+
const { EnableTerminationProtection, StackName, Outputs, } = await exports.describeStack({ stackName });
|
|
107
|
+
npmlog_1.default.output('Describe Stack');
|
|
108
|
+
npmlog_1.default.output('StackName', StackName);
|
|
109
|
+
npmlog_1.default.output('EnableTerminationProtection', EnableTerminationProtection);
|
|
110
|
+
(Outputs || []).forEach(({ OutputKey, OutputValue, Description, ExportName }) => {
|
|
111
|
+
npmlog_1.default.output(`${OutputKey}`, [
|
|
112
|
+
'',
|
|
113
|
+
`OutputKey: ${OutputKey}`,
|
|
114
|
+
`OutputValue: ${OutputValue}`,
|
|
115
|
+
`Description: ${Description}`,
|
|
116
|
+
`ExportName: ${ExportName}`,
|
|
117
|
+
'',
|
|
118
|
+
].join('\n'));
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
exports.printStackOutputsAfterDeploy = printStackOutputsAfterDeploy;
|
|
122
|
+
const deleteStack = async ({ stackName }) => {
|
|
123
|
+
npmlog_1.default.info(logPrefix, `Deleting stack ${stackName}...`);
|
|
124
|
+
await exports.cloudFormation().send(new client_cloudformation_1.DeleteStackCommand({ StackName: stackName }));
|
|
125
|
+
try {
|
|
126
|
+
await exports.cloudFormationV2()
|
|
127
|
+
.waitFor('stackDeleteComplete', { StackName: stackName })
|
|
128
|
+
.promise();
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
npmlog_1.default.error(logPrefix, `An error occurred when deleting stack ${stackName}.`);
|
|
132
|
+
await exports.describeStackEvents({ stackName });
|
|
133
|
+
throw err;
|
|
134
|
+
}
|
|
135
|
+
npmlog_1.default.info(logPrefix, `Stack ${stackName} deleted.`);
|
|
136
|
+
};
|
|
137
|
+
exports.deleteStack = deleteStack;
|
|
138
|
+
const createStack = async ({ params, }) => {
|
|
139
|
+
const { StackName: stackName } = params;
|
|
140
|
+
npmlog_1.default.info(logPrefix, `Creating stack ${stackName}...`);
|
|
141
|
+
await exports.cloudFormation().send(new client_cloudformation_1.CreateStackCommand(params));
|
|
142
|
+
try {
|
|
143
|
+
await exports.cloudFormationV2()
|
|
144
|
+
.waitFor('stackCreateComplete', { StackName: stackName })
|
|
145
|
+
.promise();
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
npmlog_1.default.error(logPrefix, `An error occurred when creating stack ${stackName}.`);
|
|
149
|
+
await exports.describeStackEvents({ stackName });
|
|
150
|
+
await exports.deleteStack({ stackName });
|
|
151
|
+
throw err;
|
|
152
|
+
}
|
|
153
|
+
npmlog_1.default.info(logPrefix, `Stack ${stackName} was created.`);
|
|
154
|
+
};
|
|
155
|
+
exports.createStack = createStack;
|
|
156
|
+
const updateStack = async ({ params, }) => {
|
|
157
|
+
const { StackName: stackName } = params;
|
|
158
|
+
npmlog_1.default.info(logPrefix, `Updating stack ${stackName}...`);
|
|
159
|
+
try {
|
|
160
|
+
await exports.cloudFormation().send(new client_cloudformation_1.UpdateStackCommand(params));
|
|
161
|
+
await exports.cloudFormationV2()
|
|
162
|
+
.waitFor('stackUpdateComplete', { StackName: stackName })
|
|
163
|
+
.promise();
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
if (err.message === 'No updates are to be performed.') {
|
|
167
|
+
npmlog_1.default.info(logPrefix, err.message);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
npmlog_1.default.error(logPrefix, 'An error occurred when updating stack.');
|
|
171
|
+
await exports.describeStackEvents({ stackName });
|
|
172
|
+
throw err;
|
|
173
|
+
}
|
|
174
|
+
npmlog_1.default.info(logPrefix, `Stack ${stackName} was updated.`);
|
|
175
|
+
};
|
|
176
|
+
exports.updateStack = updateStack;
|
|
177
|
+
const enableTerminationProtection = async ({ stackName, }) => {
|
|
178
|
+
npmlog_1.default.info(logPrefix, `Enabling termination protection...`);
|
|
179
|
+
try {
|
|
180
|
+
await exports.cloudFormation().send(new client_cloudformation_1.UpdateTerminationProtectionCommand({
|
|
181
|
+
EnableTerminationProtection: true,
|
|
182
|
+
StackName: stackName,
|
|
183
|
+
}));
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
npmlog_1.default.error(logPrefix, 'An error occurred when enabling termination protection');
|
|
187
|
+
throw err;
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
exports.enableTerminationProtection = enableTerminationProtection;
|
|
191
|
+
exports.defaultTemplatePaths = ['ts', 'js', 'yaml', 'yml', 'json'].map((extension) => `src/cloudformation.${extension}`);
|
|
192
|
+
/**
|
|
193
|
+
* 1. Add defaults to CloudFormation template and parameters.
|
|
194
|
+
* 1. Check is CloudFormation template body is greater than max size limit.
|
|
195
|
+
* 1. If is greater, upload to S3 base stack.
|
|
196
|
+
* 1. If stack exists, update the stack, else create a new stack.
|
|
197
|
+
* 1. If `terminationProtection` option is true or `environment` is defined,
|
|
198
|
+
* then stack termination protection will be enabled.
|
|
199
|
+
*/
|
|
200
|
+
const deploy = async ({ terminationProtection = false, ...paramsAndTemplate }) => {
|
|
201
|
+
const { params, template } = await addDefaults_cloudFormation_1.addDefaults(paramsAndTemplate);
|
|
202
|
+
const stackName = params.StackName;
|
|
203
|
+
delete params.TemplateBody;
|
|
204
|
+
delete params.TemplateURL;
|
|
205
|
+
if (isTemplateBodyGreaterThanMaxSize(template)) {
|
|
206
|
+
const { url } = await uploadTemplateToBaseStackBucket({
|
|
207
|
+
stackName,
|
|
208
|
+
template,
|
|
209
|
+
});
|
|
210
|
+
params.TemplateURL = url;
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
params.TemplateBody = JSON.stringify(template);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* CAPABILITY_AUTO_EXPAND allows serverless transform.
|
|
217
|
+
*/
|
|
218
|
+
params.Capabilities = [
|
|
219
|
+
'CAPABILITY_AUTO_EXPAND',
|
|
220
|
+
'CAPABILITY_IAM',
|
|
221
|
+
'CAPABILITY_NAMED_IAM',
|
|
222
|
+
];
|
|
223
|
+
if (await exports.doesStackExist({ stackName })) {
|
|
224
|
+
await exports.updateStack({ params });
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
await exports.createStack({ params });
|
|
228
|
+
}
|
|
229
|
+
if (terminationProtection || !!utils_1.getEnvironment()) {
|
|
230
|
+
await exports.enableTerminationProtection({ stackName });
|
|
231
|
+
}
|
|
232
|
+
await exports.printStackOutputsAfterDeploy({ stackName });
|
|
233
|
+
return exports.describeStack({ stackName });
|
|
234
|
+
};
|
|
235
|
+
exports.deploy = deploy;
|
|
236
|
+
const canDestroyStack = async ({ stackName }) => {
|
|
237
|
+
const { EnableTerminationProtection } = await exports.describeStack({ stackName });
|
|
238
|
+
if (EnableTerminationProtection) {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
return true;
|
|
242
|
+
};
|
|
243
|
+
exports.canDestroyStack = canDestroyStack;
|
|
244
|
+
const emptyStackBuckets = async ({ stackName }) => {
|
|
245
|
+
const buckets = [];
|
|
246
|
+
await (async function getBuckets({ nextToken }) {
|
|
247
|
+
const { NextToken, StackResourceSummaries } = await exports.cloudFormation().send(new client_cloudformation_1.ListStackResourcesCommand({
|
|
248
|
+
StackName: stackName,
|
|
249
|
+
NextToken: nextToken,
|
|
250
|
+
}));
|
|
251
|
+
if (NextToken) {
|
|
252
|
+
await getBuckets({ nextToken: NextToken });
|
|
253
|
+
}
|
|
254
|
+
(StackResourceSummaries || []).forEach(({ ResourceType, PhysicalResourceId }) => {
|
|
255
|
+
if (ResourceType === 'AWS::S3::Bucket' && PhysicalResourceId) {
|
|
256
|
+
buckets.push(PhysicalResourceId);
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
})({});
|
|
260
|
+
return Promise.all(buckets.map((bucket) => s3_1.emptyS3Directory({ bucket })));
|
|
261
|
+
};
|
|
262
|
+
/**
|
|
263
|
+
* 1. Check if `environment` is defined. If defined, return. It doesn't destroy
|
|
264
|
+
* stacks with defined `environment`.
|
|
265
|
+
* 1. Check if termination protection is disabled.
|
|
266
|
+
* 1. If the stack deployed buckets, empty all buckets.
|
|
267
|
+
* 1. Delete the stack.
|
|
268
|
+
*/
|
|
269
|
+
const destroy = async ({ stackName }) => {
|
|
270
|
+
const environment = utils_1.getEnvironment();
|
|
271
|
+
if (environment) {
|
|
272
|
+
npmlog_1.default.info(logPrefix, `Cannot destroy stack when environment (${environment}) is defined.`);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (!(await exports.doesStackExist({ stackName }))) {
|
|
276
|
+
npmlog_1.default.info(logPrefix, `Stack ${stackName} doesn't exist.`);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (!(await exports.canDestroyStack({ stackName }))) {
|
|
280
|
+
const message = `Stack ${stackName} cannot be destroyed while TerminationProtection is enabled.`;
|
|
281
|
+
throw new Error(message);
|
|
282
|
+
}
|
|
283
|
+
await emptyStackBuckets({ stackName });
|
|
284
|
+
await exports.deleteStack({ stackName });
|
|
285
|
+
};
|
|
286
|
+
exports.destroy = destroy;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.destroyCloudFormation = exports.deployCloudFormation = exports.defaultTemplatePaths = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const npmlog_1 = __importDefault(require("npmlog"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const utils_1 = require("../utils");
|
|
11
|
+
const cloudFormation_core_1 = require("./cloudFormation.core");
|
|
12
|
+
const lambda_1 = require("./lambda");
|
|
13
|
+
const s3_1 = require("./s3");
|
|
14
|
+
const stackName_1 = require("./stackName");
|
|
15
|
+
const utils_2 = require("./utils");
|
|
16
|
+
const logPrefix = 'cloudformation';
|
|
17
|
+
npmlog_1.default.addLevel('event', 10000, { fg: 'yellow' });
|
|
18
|
+
npmlog_1.default.addLevel('output', 10000, { fg: 'blue' });
|
|
19
|
+
exports.defaultTemplatePaths = ['ts', 'js', 'yaml', 'yml', 'json'].map((extension) => `./cloudformation.${extension}`);
|
|
20
|
+
const findAndReadCloudFormationTemplate = ({ templatePath: defaultTemplatePath, }) => {
|
|
21
|
+
const templatePath = defaultTemplatePath ||
|
|
22
|
+
exports.defaultTemplatePaths
|
|
23
|
+
/**
|
|
24
|
+
* Iterate over extensions. If the template of the current extension is
|
|
25
|
+
* found, we save it on the accumulator and return it every time until
|
|
26
|
+
* the loop ends.
|
|
27
|
+
*/
|
|
28
|
+
.reduce((acc, cur) => {
|
|
29
|
+
if (acc) {
|
|
30
|
+
return acc;
|
|
31
|
+
}
|
|
32
|
+
return fs_1.default.existsSync(path_1.default.resolve(process.cwd(), cur)) ? cur : acc;
|
|
33
|
+
}, '');
|
|
34
|
+
if (!templatePath) {
|
|
35
|
+
throw new Error('Cannot find a CloudFormation template.');
|
|
36
|
+
}
|
|
37
|
+
const extension = templatePath === null || templatePath === void 0 ? void 0 : templatePath.split('.').pop();
|
|
38
|
+
const fullPath = path_1.default.resolve(process.cwd(), templatePath);
|
|
39
|
+
/**
|
|
40
|
+
* We need to read Yaml first because CloudFormation specific tags aren't
|
|
41
|
+
* recognized when parsing a simple Yaml file. I.e., a possible error:
|
|
42
|
+
* "Error message: "unknown tag !<!Ref> at line 21, column 34:\n"
|
|
43
|
+
*/
|
|
44
|
+
if (['yaml', 'yml'].includes(extension)) {
|
|
45
|
+
return utils_1.readCloudFormationYamlTemplate({ templatePath });
|
|
46
|
+
}
|
|
47
|
+
return utils_1.readObjectFile({ path: fullPath });
|
|
48
|
+
};
|
|
49
|
+
const deployCloudFormation = async ({ lambdaDockerfile, lambdaInput, lambdaImage, lambdaExternals = [], parameters, template, templatePath, }) => {
|
|
50
|
+
try {
|
|
51
|
+
const { stackName } = await utils_2.handleDeployInitialization({ logPrefix });
|
|
52
|
+
const cloudFormationTemplate = (() => {
|
|
53
|
+
if (template) {
|
|
54
|
+
return { ...template };
|
|
55
|
+
}
|
|
56
|
+
return findAndReadCloudFormationTemplate({ templatePath });
|
|
57
|
+
})();
|
|
58
|
+
await cloudFormation_core_1.cloudFormationV2()
|
|
59
|
+
.validateTemplate({
|
|
60
|
+
TemplateBody: JSON.stringify(cloudFormationTemplate, null, 2),
|
|
61
|
+
})
|
|
62
|
+
.promise();
|
|
63
|
+
const params = {
|
|
64
|
+
StackName: stackName,
|
|
65
|
+
Parameters: parameters || [],
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* 1. If Lambda code exists, build and upload the code to base stack bucket.
|
|
69
|
+
*
|
|
70
|
+
* 1. If `lambdaImage` is `false`, retrieve the `bucket`, `key`, and
|
|
71
|
+
* `version` of the uploaded code and pass to CloudFormation template
|
|
72
|
+
* as [parameters](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html).
|
|
73
|
+
*
|
|
74
|
+
* 1. If `lambdaImage` is `true`, a Lambda image will be created and the
|
|
75
|
+
* image URI will be passed to CloudFormation as parameter.
|
|
76
|
+
*/
|
|
77
|
+
const deployCloudFormationDeployLambdaCode = async () => {
|
|
78
|
+
const response = await lambda_1.deployLambdaCode({
|
|
79
|
+
lambdaDockerfile,
|
|
80
|
+
lambdaExternals,
|
|
81
|
+
lambdaInput,
|
|
82
|
+
lambdaImage,
|
|
83
|
+
stackName,
|
|
84
|
+
});
|
|
85
|
+
if (response) {
|
|
86
|
+
const { bucket, key, versionId, imageUri } = response;
|
|
87
|
+
if (imageUri) {
|
|
88
|
+
cloudFormationTemplate.Parameters = {
|
|
89
|
+
LambdaImageUri: { Type: 'String' },
|
|
90
|
+
...cloudFormationTemplate.Parameters,
|
|
91
|
+
};
|
|
92
|
+
params.Parameters.push({
|
|
93
|
+
ParameterKey: 'LambdaImageUri',
|
|
94
|
+
ParameterValue: imageUri,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
else if (bucket && key && versionId) {
|
|
98
|
+
/**
|
|
99
|
+
* Add Parameters to CloudFormation template.
|
|
100
|
+
*/
|
|
101
|
+
cloudFormationTemplate.Parameters = {
|
|
102
|
+
LambdaS3Bucket: { Type: 'String' },
|
|
103
|
+
LambdaS3Key: { Type: 'String' },
|
|
104
|
+
LambdaS3Version: { Type: 'String' },
|
|
105
|
+
LambdaS3ObjectVersion: { Type: 'String' },
|
|
106
|
+
...cloudFormationTemplate.Parameters,
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* Add S3Bucket and S3Key to params.
|
|
110
|
+
*/
|
|
111
|
+
params.Parameters.push({
|
|
112
|
+
ParameterKey: 'LambdaS3Bucket',
|
|
113
|
+
ParameterValue: bucket,
|
|
114
|
+
}, {
|
|
115
|
+
ParameterKey: 'LambdaS3Key',
|
|
116
|
+
ParameterValue: key,
|
|
117
|
+
},
|
|
118
|
+
/**
|
|
119
|
+
* Used by CloudFormation AWS::Lambda::Function
|
|
120
|
+
* @see {@link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-code.html}
|
|
121
|
+
*/
|
|
122
|
+
{
|
|
123
|
+
ParameterKey: 'LambdaS3ObjectVersion',
|
|
124
|
+
ParameterValue: versionId,
|
|
125
|
+
},
|
|
126
|
+
/**
|
|
127
|
+
* Used by CloudFormation AWS::Serverless::Function
|
|
128
|
+
* @see {@link https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-property-function-functioncode.html}
|
|
129
|
+
*/
|
|
130
|
+
{
|
|
131
|
+
ParameterKey: 'LambdaS3Version',
|
|
132
|
+
ParameterValue: versionId,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
await deployCloudFormationDeployLambdaCode();
|
|
138
|
+
const output = await cloudFormation_core_1.deploy({
|
|
139
|
+
params,
|
|
140
|
+
template: cloudFormationTemplate,
|
|
141
|
+
});
|
|
142
|
+
return output;
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
return utils_2.handleDeployError({ error, logPrefix });
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
exports.deployCloudFormation = deployCloudFormation;
|
|
149
|
+
const emptyStackBuckets = async ({ stackName }) => {
|
|
150
|
+
const buckets = [];
|
|
151
|
+
await (async function getBuckets({ nextToken }) {
|
|
152
|
+
const { NextToken, StackResourceSummaries, } = await cloudFormation_core_1.cloudFormationV2()
|
|
153
|
+
.listStackResources({ StackName: stackName, NextToken: nextToken })
|
|
154
|
+
.promise();
|
|
155
|
+
if (NextToken) {
|
|
156
|
+
await getBuckets({ nextToken: NextToken });
|
|
157
|
+
}
|
|
158
|
+
(StackResourceSummaries || []).forEach(({ ResourceType, PhysicalResourceId }) => {
|
|
159
|
+
if (ResourceType === 'AWS::S3::Bucket' && PhysicalResourceId) {
|
|
160
|
+
buckets.push(PhysicalResourceId);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
})({});
|
|
164
|
+
return Promise.all(buckets.map((bucket) => s3_1.emptyS3Directory({ bucket })));
|
|
165
|
+
};
|
|
166
|
+
/**
|
|
167
|
+
* 1. Check if `environment` is defined. If defined, return. It doesn't destroy
|
|
168
|
+
* stacks with defined `environment`.
|
|
169
|
+
* 1. Check if termination protection is disabled.
|
|
170
|
+
* 1. If the stack deployed buckets, empty all buckets.
|
|
171
|
+
* 1. Delete the stack.
|
|
172
|
+
*/
|
|
173
|
+
const destroy = async ({ stackName }) => {
|
|
174
|
+
const environment = utils_1.getEnvironment();
|
|
175
|
+
if (environment) {
|
|
176
|
+
npmlog_1.default.info(logPrefix, `Cannot destroy stack when environment (${environment}) is defined.`);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (!(await cloudFormation_core_1.doesStackExist({ stackName }))) {
|
|
180
|
+
npmlog_1.default.info(logPrefix, `Stack ${stackName} doesn't exist.`);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (!(await cloudFormation_core_1.canDestroyStack({ stackName }))) {
|
|
184
|
+
const message = `Stack ${stackName} cannot be destroyed while TerminationProtection is enabled.`;
|
|
185
|
+
throw new Error(message);
|
|
186
|
+
}
|
|
187
|
+
await emptyStackBuckets({ stackName });
|
|
188
|
+
await cloudFormation_core_1.deleteStack({ stackName });
|
|
189
|
+
};
|
|
190
|
+
const destroyCloudFormation = async ({ stackName: defaultStackName, } = {}) => {
|
|
191
|
+
try {
|
|
192
|
+
npmlog_1.default.info(logPrefix, 'CAUTION! Starting CloudFormation destroy...');
|
|
193
|
+
const stackName = defaultStackName || (await stackName_1.getStackName());
|
|
194
|
+
npmlog_1.default.info(logPrefix, `stackName: ${stackName}`);
|
|
195
|
+
await destroy({ stackName });
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
utils_2.handleDeployError({ error, logPrefix });
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
exports.destroyCloudFormation = destroyCloudFormation;
|