pwrdrvr 0.4.0-alpha.13
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/LICENSE +21 -0
- package/README.md +241 -0
- package/bin/run +5 -0
- package/dist/commands/delete.d.ts +18 -0
- package/dist/commands/delete.d.ts.map +1 -0
- package/dist/commands/delete.js +125 -0
- package/dist/commands/delete.js.map +1 -0
- package/dist/commands/nextjs-version-restore.d.ts +14 -0
- package/dist/commands/nextjs-version-restore.d.ts.map +1 -0
- package/dist/commands/nextjs-version-restore.js +53 -0
- package/dist/commands/nextjs-version-restore.js.map +1 -0
- package/dist/commands/nextjs-version.d.ts +18 -0
- package/dist/commands/nextjs-version.d.ts.map +1 -0
- package/dist/commands/nextjs-version.js +103 -0
- package/dist/commands/nextjs-version.js.map +1 -0
- package/dist/commands/preflight.d.ts +19 -0
- package/dist/commands/preflight.d.ts.map +1 -0
- package/dist/commands/preflight.js +135 -0
- package/dist/commands/preflight.js.map +1 -0
- package/dist/commands/publish-static.d.ts +25 -0
- package/dist/commands/publish-static.d.ts.map +1 -0
- package/dist/commands/publish-static.js +361 -0
- package/dist/commands/publish-static.js.map +1 -0
- package/dist/commands/publish.d.ts +39 -0
- package/dist/commands/publish.d.ts.map +1 -0
- package/dist/commands/publish.js +565 -0
- package/dist/commands/publish.js.map +1 -0
- package/dist/config/Application.d.ts +26 -0
- package/dist/config/Application.d.ts.map +1 -0
- package/dist/config/Application.js +99 -0
- package/dist/config/Application.js.map +1 -0
- package/dist/config/Config.d.ts +18 -0
- package/dist/config/Config.d.ts.map +1 -0
- package/dist/config/Config.js +71 -0
- package/dist/config/Config.js.map +1 -0
- package/dist/config/Deployer.d.ts +10 -0
- package/dist/config/Deployer.d.ts.map +1 -0
- package/dist/config/Deployer.js +17 -0
- package/dist/config/Deployer.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/DeployClient.d.ts +102 -0
- package/dist/lib/DeployClient.d.ts.map +1 -0
- package/dist/lib/DeployClient.js +233 -0
- package/dist/lib/DeployClient.js.map +1 -0
- package/dist/lib/FilesExist.d.ts +5 -0
- package/dist/lib/FilesExist.d.ts.map +1 -0
- package/dist/lib/FilesExist.js +26 -0
- package/dist/lib/FilesExist.js.map +1 -0
- package/dist/lib/S3TransferUtility.d.ts +19 -0
- package/dist/lib/S3TransferUtility.d.ts.map +1 -0
- package/dist/lib/S3TransferUtility.js +94 -0
- package/dist/lib/S3TransferUtility.js.map +1 -0
- package/dist/lib/S3Uploader.d.ts +27 -0
- package/dist/lib/S3Uploader.d.ts.map +1 -0
- package/dist/lib/S3Uploader.js +77 -0
- package/dist/lib/S3Uploader.js.map +1 -0
- package/dist/lib/Versions.d.ts +33 -0
- package/dist/lib/Versions.d.ts.map +1 -0
- package/dist/lib/Versions.js +76 -0
- package/dist/lib/Versions.js.map +1 -0
- package/package.json +83 -0
- package/src/commands/delete.ts +135 -0
- package/src/commands/nextjs-version-restore.ts +70 -0
- package/src/commands/nextjs-version.ts +123 -0
- package/src/commands/preflight.ts +148 -0
- package/src/commands/publish-static.ts +416 -0
- package/src/commands/publish.ts +662 -0
- package/src/commands-deprecated/nextjs-docker-auto.skip +590 -0
- package/src/config/Application.ts +98 -0
- package/src/config/Config.ts +81 -0
- package/src/config/Deployer.ts +17 -0
- package/src/index.ts +1 -0
- package/src/lib/DeployClient.ts +334 -0
- package/src/lib/FilesExist.ts +25 -0
- package/src/lib/S3TransferUtility.spec.ts +15 -0
- package/src/lib/S3TransferUtility.ts +113 -0
- package/src/lib/S3Uploader.ts +94 -0
- package/src/lib/Versions.ts +101 -0
- package/src/lib/__snapshots__/S3TransferUtility.spec.ts.snap +12 -0
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
import { exec } from 'child_process';
|
|
3
|
+
import * as util from 'util';
|
|
4
|
+
import * as lambda from '@aws-sdk/client-lambda';
|
|
5
|
+
import * as s3 from '@aws-sdk/client-s3';
|
|
6
|
+
import * as sts from '@aws-sdk/client-sts';
|
|
7
|
+
import { Command, flags as flagsParser } from '@oclif/command';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import { promises as fs, pathExists, createReadStream } from 'fs-extra';
|
|
10
|
+
import { Listr, ListrTask } from 'listr2';
|
|
11
|
+
import { Config, IConfig } from '../config/Config';
|
|
12
|
+
import DeployClient, { IDeployVersionPreflightResult } from '../lib/DeployClient';
|
|
13
|
+
import S3Uploader from '../lib/S3Uploader';
|
|
14
|
+
import S3TransferUtility from '../lib/S3TransferUtility';
|
|
15
|
+
import { Upload } from '@aws-sdk/lib-storage';
|
|
16
|
+
import { createVersions, IVersions } from '../lib/Versions';
|
|
17
|
+
import { contentType } from 'mime-types';
|
|
18
|
+
import { TaskWrapper } from 'listr2/dist/lib/task-wrapper';
|
|
19
|
+
import { DefaultRenderer } from 'listr2/dist/renderer/default.renderer';
|
|
20
|
+
import { IFileToModify, restoreFiles, writeNewVersions } from '../lib/Versions';
|
|
21
|
+
const asyncSetTimeout = util.promisify(setTimeout);
|
|
22
|
+
const asyncExec = util.promisify(exec);
|
|
23
|
+
|
|
24
|
+
const lambdaClient = new lambda.LambdaClient({
|
|
25
|
+
maxAttempts: 8,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
interface IContext {
|
|
29
|
+
preflightResult: IDeployVersionPreflightResult;
|
|
30
|
+
files: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class DockerAutoCommand extends Command {
|
|
34
|
+
static description =
|
|
35
|
+
'Fully automatic publishing of Docker-based Lambda function using Next.js and serverless-nextjs-router';
|
|
36
|
+
|
|
37
|
+
static examples = [
|
|
38
|
+
`$ pwrdrvr nextjs-docker-auto -d microapps-deployer-dev -r microapps-app-release-dev-repo -n 0.0.14
|
|
39
|
+
✔ Logging into ECR [2s]
|
|
40
|
+
✔ Modifying Config Files [0.0s]
|
|
41
|
+
✔ Preflight Version Check [1s]
|
|
42
|
+
✔ Serverless Next.js Build [1m16s]
|
|
43
|
+
✔ Publish to ECR [32s]
|
|
44
|
+
✔ Deploy to Lambda [11s]
|
|
45
|
+
✔ Confirm Static Assets Folder Exists [0.0s]
|
|
46
|
+
✔ Copy Static Files to Local Upload Dir [0.0s]
|
|
47
|
+
✔ Enumerate Files to Upload to S3 [0.0s]
|
|
48
|
+
✔ Upload Static Files to S3 [1s]
|
|
49
|
+
✔ Creating MicroApp Application: release [0.2s]
|
|
50
|
+
✔ Creating MicroApp Version: 0.0.14 [1s]
|
|
51
|
+
`,
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
static flags = {
|
|
55
|
+
version: flagsParser.version({
|
|
56
|
+
char: 'v',
|
|
57
|
+
}),
|
|
58
|
+
help: flagsParser.help(),
|
|
59
|
+
deployerLambdaName: flagsParser.string({
|
|
60
|
+
char: 'd',
|
|
61
|
+
multiple: false,
|
|
62
|
+
required: true,
|
|
63
|
+
description: 'Name of the deployer lambda function',
|
|
64
|
+
}),
|
|
65
|
+
newVersion: flagsParser.string({
|
|
66
|
+
char: 'n',
|
|
67
|
+
multiple: false,
|
|
68
|
+
required: true,
|
|
69
|
+
description: 'New semantic version to apply',
|
|
70
|
+
}),
|
|
71
|
+
repoName: flagsParser.string({
|
|
72
|
+
char: 'r',
|
|
73
|
+
multiple: false,
|
|
74
|
+
required: true,
|
|
75
|
+
description: 'Name (not URI) of the Docker repo for the app',
|
|
76
|
+
}),
|
|
77
|
+
leaveCopy: flagsParser.boolean({
|
|
78
|
+
char: 'f',
|
|
79
|
+
default: false,
|
|
80
|
+
required: false,
|
|
81
|
+
description: 'Leave a copy of the modifed files as .modified',
|
|
82
|
+
}),
|
|
83
|
+
appLambdaName: flagsParser.string({
|
|
84
|
+
char: 'l',
|
|
85
|
+
multiple: false,
|
|
86
|
+
required: false,
|
|
87
|
+
description: 'Name of the application lambda function',
|
|
88
|
+
}),
|
|
89
|
+
appName: flagsParser.string({
|
|
90
|
+
char: 'a',
|
|
91
|
+
multiple: false,
|
|
92
|
+
required: false,
|
|
93
|
+
description: 'MicroApps app name',
|
|
94
|
+
}),
|
|
95
|
+
staticAssetsPath: flagsParser.string({
|
|
96
|
+
char: 's',
|
|
97
|
+
multiple: false,
|
|
98
|
+
required: false,
|
|
99
|
+
description:
|
|
100
|
+
'Path to files to be uploaded to S3 static bucket at app/version/ path. Do include app/version/ in path if files are already "rooted" under that path locally.',
|
|
101
|
+
}),
|
|
102
|
+
defaultFile: flagsParser.string({
|
|
103
|
+
char: 'i',
|
|
104
|
+
multiple: false,
|
|
105
|
+
required: false,
|
|
106
|
+
description:
|
|
107
|
+
'Default file to return when the app is loaded via the router without a version (e.g. when app/ is requested).',
|
|
108
|
+
}),
|
|
109
|
+
overwrite: flagsParser.boolean({
|
|
110
|
+
char: 'o',
|
|
111
|
+
required: false,
|
|
112
|
+
default: false,
|
|
113
|
+
description:
|
|
114
|
+
'Allow overwrite - Warn but do not fail if version exists. Discouraged outside of test envs if cacheable static files have changed.',
|
|
115
|
+
}),
|
|
116
|
+
noCache: flagsParser.boolean({
|
|
117
|
+
required: false,
|
|
118
|
+
default: false,
|
|
119
|
+
description: 'Force revalidation of CloudFront and browser caching of static assets',
|
|
120
|
+
}),
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
private VersionAndAlias: IVersions;
|
|
124
|
+
private IMAGE_TAG = '';
|
|
125
|
+
private IMAGE_URI = '';
|
|
126
|
+
private FILES_TO_MODIFY: IFileToModify[];
|
|
127
|
+
private _restoreFilesStarted = false;
|
|
128
|
+
|
|
129
|
+
async run(): Promise<void> {
|
|
130
|
+
const config = Config.instance;
|
|
131
|
+
|
|
132
|
+
// const RUNNING_TEXT = ' RUNS ';
|
|
133
|
+
// const RUNNING = chalk.reset.inverse.yellow.bold(RUNNING_TEXT) + ' ';
|
|
134
|
+
const RUNNING = ''; //chalk.reset.inverse.yellow.bold(RUNNING_TEXT) + ' ';
|
|
135
|
+
|
|
136
|
+
const { flags: parsedFlags } = this.parse(DockerAutoCommand);
|
|
137
|
+
const appLambdaName = parsedFlags.appLambdaName ?? config.app.lambdaName;
|
|
138
|
+
const appName = parsedFlags.appName ?? config.app.name;
|
|
139
|
+
const leaveFiles = parsedFlags.leaveCopy;
|
|
140
|
+
const deployerLambdaName = parsedFlags.deployerLambdaName ?? config.deployer.lambdaName;
|
|
141
|
+
const semVer = parsedFlags.newVersion ?? config.app.semVer;
|
|
142
|
+
const ecrRepo = parsedFlags.repoName ?? config.app.ecrRepoName;
|
|
143
|
+
const staticAssetsPath = parsedFlags.staticAssetsPath ?? config.app.staticAssetsPath;
|
|
144
|
+
const defaultFile = parsedFlags.defaultFile ?? config.app.defaultFile;
|
|
145
|
+
const overwrite = parsedFlags.overwrite;
|
|
146
|
+
const noCache = parsedFlags.noCache;
|
|
147
|
+
|
|
148
|
+
// Override the config value
|
|
149
|
+
config.deployer.lambdaName = deployerLambdaName;
|
|
150
|
+
config.app.lambdaName = appLambdaName;
|
|
151
|
+
config.app.name = appName;
|
|
152
|
+
config.app.semVer = semVer;
|
|
153
|
+
config.app.staticAssetsPath = staticAssetsPath;
|
|
154
|
+
config.app.defaultFile = defaultFile;
|
|
155
|
+
|
|
156
|
+
// Get the account ID and region from STS
|
|
157
|
+
// TODO: Move this to the right place
|
|
158
|
+
if (config.app.awsAccountID === '' || config.app.awsRegion === '') {
|
|
159
|
+
const stsClient = new sts.STSClient({
|
|
160
|
+
maxAttempts: 8,
|
|
161
|
+
});
|
|
162
|
+
const stsResponse = await stsClient.send(new sts.GetCallerIdentityCommand({}));
|
|
163
|
+
if (config.app.awsAccountID === '') {
|
|
164
|
+
config.app.awsAccountID = stsResponse.Account;
|
|
165
|
+
}
|
|
166
|
+
if (config.app.awsRegion === '') {
|
|
167
|
+
config.app.awsRegion = stsClient.config.region as string;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (config.app.ecrHost === '') {
|
|
171
|
+
config.app.ecrHost = `${config.app.awsAccountID}.dkr.ecr.${config.app.awsRegion}.amazonaws.com`;
|
|
172
|
+
}
|
|
173
|
+
if (ecrRepo) {
|
|
174
|
+
config.app.ecrRepoName = ecrRepo;
|
|
175
|
+
} else if (config.app.ecrRepoName === '') {
|
|
176
|
+
config.app.ecrRepoName = `microapps-app-${config.app.name}${Config.envLevel}-repo`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
this.VersionAndAlias = createVersions(semVer);
|
|
180
|
+
const versionOnly = { version: this.VersionAndAlias.version };
|
|
181
|
+
|
|
182
|
+
this.FILES_TO_MODIFY = [
|
|
183
|
+
{ path: 'package.json', versions: versionOnly },
|
|
184
|
+
{ path: 'next.config.js', versions: versionOnly },
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
// Install handler to ensure that we restore files
|
|
188
|
+
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
189
|
+
process.on('SIGINT', async () => {
|
|
190
|
+
if (this._restoreFilesStarted) {
|
|
191
|
+
return;
|
|
192
|
+
} else {
|
|
193
|
+
this._restoreFilesStarted = true;
|
|
194
|
+
}
|
|
195
|
+
this.log('Caught Ctrl-C, restoring files');
|
|
196
|
+
await S3Uploader.removeTempDirIfExists();
|
|
197
|
+
await restoreFiles(this.FILES_TO_MODIFY);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
if (config.app.staticAssetsPath === undefined) {
|
|
201
|
+
this.error('staticAssetsPath must be specified');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
//
|
|
205
|
+
// Setup Tasks
|
|
206
|
+
//
|
|
207
|
+
|
|
208
|
+
const tasks = new Listr<IContext>(
|
|
209
|
+
[
|
|
210
|
+
{
|
|
211
|
+
title: 'Logging into ECR',
|
|
212
|
+
task: async (ctx, task) => {
|
|
213
|
+
const origTitle = task.title;
|
|
214
|
+
task.title = RUNNING + origTitle;
|
|
215
|
+
|
|
216
|
+
await this.loginToECR(config);
|
|
217
|
+
|
|
218
|
+
task.title = origTitle;
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
title: 'Modifying Config Files',
|
|
223
|
+
task: async (ctx, task) => {
|
|
224
|
+
const origTitle = task.title;
|
|
225
|
+
task.title = RUNNING + origTitle;
|
|
226
|
+
|
|
227
|
+
// Modify the existing files with the new version
|
|
228
|
+
for (const fileToModify of this.FILES_TO_MODIFY) {
|
|
229
|
+
task.output = `Patching version (${this.VersionAndAlias.version}) into ${fileToModify.path}`;
|
|
230
|
+
if (!(await writeNewVersions(fileToModify.path, fileToModify.versions, leaveFiles))) {
|
|
231
|
+
task.output = `Failed modifying file: ${fileToModify.path}`;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
task.title = origTitle;
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
title: 'Preflight Version Check',
|
|
240
|
+
task: async (ctx, task) => {
|
|
241
|
+
const origTitle = task.title;
|
|
242
|
+
task.title = RUNNING + origTitle;
|
|
243
|
+
|
|
244
|
+
// Confirm the Version Does Not Exist in Published State
|
|
245
|
+
task.output = `Checking if deployed app/version already exists for ${config.app.name}/${semVer}`;
|
|
246
|
+
ctx.preflightResult = await DeployClient.DeployVersionPreflight({
|
|
247
|
+
config,
|
|
248
|
+
overwrite,
|
|
249
|
+
output: (message: string) => (task.output = message),
|
|
250
|
+
});
|
|
251
|
+
if (ctx.preflightResult.exists) {
|
|
252
|
+
if (!overwrite) {
|
|
253
|
+
throw new Error(
|
|
254
|
+
`App/Version already exists: ${config.app.name}/${config.app.semVer}`,
|
|
255
|
+
);
|
|
256
|
+
} else {
|
|
257
|
+
task.title = `Warning: App/Version already exists: ${config.app.name}/${config.app.semVer}`;
|
|
258
|
+
}
|
|
259
|
+
} else {
|
|
260
|
+
task.title = `App/Version does not exist: ${config.app.name}/${config.app.semVer}`;
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
title: 'Serverless Next.js Build',
|
|
266
|
+
task: async (ctx, task) => {
|
|
267
|
+
const origTitle = task.title;
|
|
268
|
+
task.title = RUNNING + origTitle;
|
|
269
|
+
|
|
270
|
+
task.output = `Invoking serverless next.js build for ${config.app.name}/${semVer}`;
|
|
271
|
+
|
|
272
|
+
// Run the serverless next.js build
|
|
273
|
+
await asyncExec('serverless');
|
|
274
|
+
|
|
275
|
+
if (config.app.serverlessNextRouterPath !== undefined) {
|
|
276
|
+
task.output = 'Copying Serverless Next.js router to build output directory';
|
|
277
|
+
await fs.copyFile(
|
|
278
|
+
config.app.serverlessNextRouterPath,
|
|
279
|
+
'./.serverless_nextjs/index.js',
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
task.title = origTitle;
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
title: 'Confirm Static Assets Folder Exists',
|
|
288
|
+
task: async (ctx, task) => {
|
|
289
|
+
const origTitle = task.title;
|
|
290
|
+
task.title = RUNNING + origTitle;
|
|
291
|
+
|
|
292
|
+
// Check that Static Assets Folder exists
|
|
293
|
+
if (!(await pathExists(config.app.staticAssetsPath))) {
|
|
294
|
+
this.error(`Static asset path does not exist: ${config.app.staticAssetsPath}`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
task.title = origTitle;
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
title: 'Publish to ECR',
|
|
302
|
+
task: async (ctx: IContext, task: TaskWrapper<IContext, typeof DefaultRenderer>) => {
|
|
303
|
+
const origTitle = task.title;
|
|
304
|
+
task.title = RUNNING + origTitle;
|
|
305
|
+
|
|
306
|
+
// Docker, build, tag, push to ECR
|
|
307
|
+
// Note: Need to already have AWS env vars set
|
|
308
|
+
await this.publishToECR(config, task);
|
|
309
|
+
|
|
310
|
+
task.title = origTitle;
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
title: 'Deploy to Lambda',
|
|
315
|
+
task: async (ctx, task) => {
|
|
316
|
+
// Allow overwriting a non-overwritable app if the prior
|
|
317
|
+
// publish was not completely successful - in that case
|
|
318
|
+
// the lambda alias may exist and need updating
|
|
319
|
+
const allowOverwrite = overwrite || !ctx.preflightResult.exists;
|
|
320
|
+
const origTitle = task.title;
|
|
321
|
+
task.title = RUNNING + origTitle;
|
|
322
|
+
|
|
323
|
+
// Update the Lambda function
|
|
324
|
+
await this.deployToLambda({
|
|
325
|
+
config,
|
|
326
|
+
versions: this.VersionAndAlias,
|
|
327
|
+
overwrite: allowOverwrite,
|
|
328
|
+
task,
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
task.title = origTitle;
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
title: 'Copy Static Files to Local Upload Dir',
|
|
336
|
+
task: async (ctx, task) => {
|
|
337
|
+
const origTitle = task.title;
|
|
338
|
+
task.title = RUNNING + origTitle;
|
|
339
|
+
|
|
340
|
+
// Copy files to local dir to be uploaded
|
|
341
|
+
await S3Uploader.CopyToUploadDir(config, ctx.preflightResult.response.s3UploadUrl);
|
|
342
|
+
|
|
343
|
+
task.title = origTitle;
|
|
344
|
+
},
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
title: 'Enumerate Files to Upload to S3',
|
|
348
|
+
task: async (ctx, task) => {
|
|
349
|
+
const origTitle = task.title;
|
|
350
|
+
task.title = RUNNING + origTitle;
|
|
351
|
+
|
|
352
|
+
ctx.files = (await S3TransferUtility.GetFiles(S3Uploader.TempDir)) as string[];
|
|
353
|
+
|
|
354
|
+
task.title = origTitle;
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
title: 'Upload Static Files to S3',
|
|
359
|
+
task: (ctx, task) => {
|
|
360
|
+
const origTitle = task.title;
|
|
361
|
+
task.title = RUNNING + origTitle;
|
|
362
|
+
|
|
363
|
+
const { bucketName, destinationPrefix } = S3Uploader.ParseUploadPath(
|
|
364
|
+
ctx.preflightResult.response.s3UploadUrl,
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
// Use temp credentials for S3
|
|
368
|
+
const s3Client = new s3.S3Client({
|
|
369
|
+
maxAttempts: 16,
|
|
370
|
+
credentials: {
|
|
371
|
+
accessKeyId: ctx.preflightResult.response.awsCredentials.accessKeyId,
|
|
372
|
+
secretAccessKey: ctx.preflightResult.response.awsCredentials.secretAccessKey,
|
|
373
|
+
sessionToken: ctx.preflightResult.response.awsCredentials.sessionToken,
|
|
374
|
+
},
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// Setup caching on static assets
|
|
378
|
+
// NoCache - Only used for test deploys, requires browser and CloudFront to refetch every time
|
|
379
|
+
// Overwrite - Reduces default cache time period from 24 hours to 15 minutes
|
|
380
|
+
// Default - 24 hours
|
|
381
|
+
const CacheControl = noCache
|
|
382
|
+
? 'max-age=0, must-revalidate, public'
|
|
383
|
+
: overwrite
|
|
384
|
+
? `max-age=${15 * 60}, public`
|
|
385
|
+
: `max-age=${24 * 60 * 60}, public`;
|
|
386
|
+
|
|
387
|
+
const pathWithoutAppAndVer = path.join(S3Uploader.TempDir, destinationPrefix);
|
|
388
|
+
|
|
389
|
+
const tasks: ListrTask<IContext>[] = ctx.files.map((filePath) => ({
|
|
390
|
+
task: async (ctx: IContext, subtask) => {
|
|
391
|
+
const relFilePath = path.relative(pathWithoutAppAndVer, filePath);
|
|
392
|
+
|
|
393
|
+
const origTitle = relFilePath;
|
|
394
|
+
subtask.title = RUNNING + origTitle;
|
|
395
|
+
|
|
396
|
+
const upload = new Upload({
|
|
397
|
+
client: s3Client,
|
|
398
|
+
leavePartsOnError: false,
|
|
399
|
+
params: {
|
|
400
|
+
Bucket: bucketName,
|
|
401
|
+
Key: path.relative(S3Uploader.TempDir, filePath),
|
|
402
|
+
Body: createReadStream(filePath),
|
|
403
|
+
ContentType: contentType(path.basename(filePath)) || 'application/octet-stream',
|
|
404
|
+
CacheControl,
|
|
405
|
+
},
|
|
406
|
+
});
|
|
407
|
+
await upload.done();
|
|
408
|
+
|
|
409
|
+
subtask.title = origTitle;
|
|
410
|
+
},
|
|
411
|
+
}));
|
|
412
|
+
|
|
413
|
+
task.title = origTitle;
|
|
414
|
+
|
|
415
|
+
return task.newListr(tasks, {
|
|
416
|
+
concurrent: 8,
|
|
417
|
+
rendererOptions: {
|
|
418
|
+
clearOutput: false,
|
|
419
|
+
showErrorMessage: true,
|
|
420
|
+
showTimer: true,
|
|
421
|
+
},
|
|
422
|
+
});
|
|
423
|
+
},
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
title: `Creating MicroApp Application: ${config.app.name}`,
|
|
427
|
+
task: async (ctx, task) => {
|
|
428
|
+
const origTitle = task.title;
|
|
429
|
+
task.title = RUNNING + origTitle;
|
|
430
|
+
|
|
431
|
+
// Call Deployer to Create App if Not Exists
|
|
432
|
+
await DeployClient.CreateApp({ config });
|
|
433
|
+
|
|
434
|
+
task.title = origTitle;
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
title: `Creating MicroApp Version: ${config.app.semVer}`,
|
|
439
|
+
task: async (ctx, task) => {
|
|
440
|
+
const origTitle = task.title;
|
|
441
|
+
task.title = RUNNING + origTitle;
|
|
442
|
+
|
|
443
|
+
// Call Deployer to Deploy AppName/Version
|
|
444
|
+
await DeployClient.DeployVersion({
|
|
445
|
+
appName: config.app.name,
|
|
446
|
+
semVer: config.app.semVer,
|
|
447
|
+
defaultFile: config.app.defaultFile,
|
|
448
|
+
deployerLambdaName: config.deployer.lambdaName,
|
|
449
|
+
appType: 'lambda',
|
|
450
|
+
overwrite,
|
|
451
|
+
output: (message: string) => (task.output = message),
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
task.title = origTitle;
|
|
455
|
+
},
|
|
456
|
+
},
|
|
457
|
+
],
|
|
458
|
+
{
|
|
459
|
+
rendererOptions: {
|
|
460
|
+
showTimer: true,
|
|
461
|
+
},
|
|
462
|
+
},
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
try {
|
|
466
|
+
await tasks.run();
|
|
467
|
+
} finally {
|
|
468
|
+
await S3Uploader.removeTempDirIfExists();
|
|
469
|
+
await restoreFiles(this.FILES_TO_MODIFY);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Login to ECR for Lambda Docker functions
|
|
475
|
+
* @param config
|
|
476
|
+
* @returns
|
|
477
|
+
*/
|
|
478
|
+
private async loginToECR(config: IConfig): Promise<boolean> {
|
|
479
|
+
this.IMAGE_TAG = `${config.app.ecrRepoName}:${this.VersionAndAlias.version}`;
|
|
480
|
+
this.IMAGE_URI = `${config.app.ecrHost}/${this.IMAGE_TAG}`;
|
|
481
|
+
|
|
482
|
+
try {
|
|
483
|
+
await asyncExec(
|
|
484
|
+
`aws ecr get-login-password --region ${config.app.awsRegion} | docker login --username AWS --password-stdin ${config.app.ecrHost}`,
|
|
485
|
+
);
|
|
486
|
+
} catch (error) {
|
|
487
|
+
throw new Error(`ECR Login Failed: ${error.message}`);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return true;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Publish to ECR for Lambda Docker function
|
|
495
|
+
* @param config
|
|
496
|
+
*/
|
|
497
|
+
private async publishToECR(
|
|
498
|
+
config: IConfig,
|
|
499
|
+
task: TaskWrapper<IContext, typeof DefaultRenderer>,
|
|
500
|
+
): Promise<void> {
|
|
501
|
+
task.output = 'Starting Docker build';
|
|
502
|
+
await asyncExec(`docker build -f Dockerfile -t ${this.IMAGE_TAG} .`);
|
|
503
|
+
await asyncExec(`docker tag ${this.IMAGE_TAG} ${config.app.ecrHost}/${this.IMAGE_TAG}`);
|
|
504
|
+
task.output = 'Starting Docker push to ECR';
|
|
505
|
+
await asyncExec(`docker push ${config.app.ecrHost}/${this.IMAGE_TAG}`);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Publish an app version to Lambda
|
|
510
|
+
* @param config
|
|
511
|
+
* @param versions
|
|
512
|
+
*/
|
|
513
|
+
private async deployToLambda(opts: {
|
|
514
|
+
config: IConfig;
|
|
515
|
+
versions: IVersions;
|
|
516
|
+
overwrite: boolean;
|
|
517
|
+
task: TaskWrapper<IContext, typeof DefaultRenderer>;
|
|
518
|
+
}): Promise<void> {
|
|
519
|
+
const { config, versions, overwrite, task } = opts;
|
|
520
|
+
|
|
521
|
+
// Create Lambda version
|
|
522
|
+
task.output = 'Updating Lambda code to point to new Docker image';
|
|
523
|
+
const resultUpdate = await lambdaClient.send(
|
|
524
|
+
new lambda.UpdateFunctionCodeCommand({
|
|
525
|
+
FunctionName: config.app.lambdaName,
|
|
526
|
+
ImageUri: this.IMAGE_URI,
|
|
527
|
+
Publish: true,
|
|
528
|
+
}),
|
|
529
|
+
);
|
|
530
|
+
// await lambdaClient.send(
|
|
531
|
+
// new lambda.PublishVersionCommand({
|
|
532
|
+
// FunctionName: config.app.lambdaName,
|
|
533
|
+
// }),
|
|
534
|
+
// );
|
|
535
|
+
const lambdaVersion = resultUpdate.Version;
|
|
536
|
+
task.output = `Lambda version created: ${resultUpdate.Version}`;
|
|
537
|
+
|
|
538
|
+
let lastUpdateStatus = resultUpdate.LastUpdateStatus;
|
|
539
|
+
for (let i = 0; i < 5; i++) {
|
|
540
|
+
// When the function is created the status will be "Pending"
|
|
541
|
+
// and we have to wait until it's done creating
|
|
542
|
+
// before we can point an alias to it
|
|
543
|
+
if (lastUpdateStatus === 'Successful') {
|
|
544
|
+
task.output = `Lambda function updated, version: ${lambdaVersion}`;
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// If it didn't work, wait and try again
|
|
549
|
+
await asyncSetTimeout(1000 * i);
|
|
550
|
+
|
|
551
|
+
const resultGet = await lambdaClient.send(
|
|
552
|
+
new lambda.GetFunctionCommand({
|
|
553
|
+
FunctionName: config.app.lambdaName,
|
|
554
|
+
Qualifier: lambdaVersion,
|
|
555
|
+
}),
|
|
556
|
+
);
|
|
557
|
+
|
|
558
|
+
// Save the last update status so we can check on re-loop
|
|
559
|
+
lastUpdateStatus = resultGet?.Configuration?.LastUpdateStatus;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Create Lambda alias point
|
|
563
|
+
task.output = `Creating the lambda alias for the new version: ${lambdaVersion}`;
|
|
564
|
+
try {
|
|
565
|
+
const resultLambdaAlias = await lambdaClient.send(
|
|
566
|
+
new lambda.CreateAliasCommand({
|
|
567
|
+
FunctionName: config.app.lambdaName,
|
|
568
|
+
Name: versions.alias,
|
|
569
|
+
FunctionVersion: lambdaVersion,
|
|
570
|
+
}),
|
|
571
|
+
);
|
|
572
|
+
task.output = `Lambda alias created, name: ${resultLambdaAlias.Name}`;
|
|
573
|
+
} catch (error) {
|
|
574
|
+
if (overwrite && error.name === 'ResourceConflictException') {
|
|
575
|
+
task.output = `Alias exists, updating the lambda alias for version: ${lambdaVersion}`;
|
|
576
|
+
|
|
577
|
+
const resultLambdaAlias = await lambdaClient.send(
|
|
578
|
+
new lambda.UpdateAliasCommand({
|
|
579
|
+
FunctionName: config.app.lambdaName,
|
|
580
|
+
Name: versions.alias,
|
|
581
|
+
FunctionVersion: lambdaVersion,
|
|
582
|
+
}),
|
|
583
|
+
);
|
|
584
|
+
task.output = `Lambda alias updated, name: ${resultLambdaAlias.Name}`;
|
|
585
|
+
} else {
|
|
586
|
+
throw error;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { url, ipaddress } from 'convict-format-with-validator';
|
|
2
|
+
import * as yaml from 'js-yaml';
|
|
3
|
+
import * as convict from 'ts-convict';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Represents an Application Config
|
|
7
|
+
*/
|
|
8
|
+
export interface IApplicationConfig {
|
|
9
|
+
name: string;
|
|
10
|
+
semVer: string;
|
|
11
|
+
defaultFile: string;
|
|
12
|
+
staticAssetsPath: string;
|
|
13
|
+
lambdaName: string;
|
|
14
|
+
awsAccountID: string;
|
|
15
|
+
awsRegion: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@convict.Config({
|
|
19
|
+
// optional default file to load, no errors if it doesn't exist
|
|
20
|
+
file: './microapps.yml', // relative to NODE_PATH or cwd()
|
|
21
|
+
|
|
22
|
+
// optional parameter. Defaults to 'strict', can also be 'warn'
|
|
23
|
+
validationMethod: 'strict',
|
|
24
|
+
|
|
25
|
+
// optionally add parsers like yaml or toml
|
|
26
|
+
parser: {
|
|
27
|
+
extension: ['yml', 'yaml'],
|
|
28
|
+
parse: yaml.load,
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
//optional extra formats to use in validation
|
|
32
|
+
formats: {
|
|
33
|
+
url,
|
|
34
|
+
ipaddress,
|
|
35
|
+
},
|
|
36
|
+
})
|
|
37
|
+
export class ApplicationConfig implements IApplicationConfig {
|
|
38
|
+
private _name: string;
|
|
39
|
+
public get name(): string {
|
|
40
|
+
return this._name;
|
|
41
|
+
}
|
|
42
|
+
@convict.Property({
|
|
43
|
+
doc: 'Name of microapps app',
|
|
44
|
+
default: '',
|
|
45
|
+
env: 'APP_NAME',
|
|
46
|
+
})
|
|
47
|
+
public set name(value: string) {
|
|
48
|
+
this._name = value.toLowerCase();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@convict.Property({
|
|
52
|
+
doc: 'SemVer this version is to be published as',
|
|
53
|
+
default: '0.0.1',
|
|
54
|
+
env: 'APP_SEMVER',
|
|
55
|
+
})
|
|
56
|
+
public semVer: string;
|
|
57
|
+
|
|
58
|
+
@convict.Property({
|
|
59
|
+
doc: 'Default file to reference when loading the app with no version',
|
|
60
|
+
default: '',
|
|
61
|
+
env: 'APP_DEFAULT_FILE',
|
|
62
|
+
})
|
|
63
|
+
public defaultFile: string;
|
|
64
|
+
|
|
65
|
+
private _staticAssetsPath: string;
|
|
66
|
+
@convict.Property({
|
|
67
|
+
doc: 'Local path to static assets path to upload to S3',
|
|
68
|
+
default: '',
|
|
69
|
+
env: 'APP_STATIC_ASSETS_PATH',
|
|
70
|
+
})
|
|
71
|
+
public set staticAssetsPath(value: string) {
|
|
72
|
+
this._staticAssetsPath = value;
|
|
73
|
+
}
|
|
74
|
+
public get staticAssetsPath(): string {
|
|
75
|
+
return this._staticAssetsPath.replace(/\$SEMVER/, this.semVer);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@convict.Property({
|
|
79
|
+
doc: 'Name of the lambda to deploy to (not full ARN)',
|
|
80
|
+
default: '',
|
|
81
|
+
env: 'APP_LAMBDA_NAME',
|
|
82
|
+
})
|
|
83
|
+
public lambdaName: string;
|
|
84
|
+
|
|
85
|
+
@convict.Property({
|
|
86
|
+
doc: 'AWS Account ID to deploy to',
|
|
87
|
+
default: '',
|
|
88
|
+
env: 'AWS_ACCOUNT_ID',
|
|
89
|
+
})
|
|
90
|
+
public awsAccountID: string;
|
|
91
|
+
|
|
92
|
+
@convict.Property({
|
|
93
|
+
doc: 'AWS Region to deploy to',
|
|
94
|
+
default: 'us-east-1',
|
|
95
|
+
env: 'AWS_REGION',
|
|
96
|
+
})
|
|
97
|
+
public awsRegion: string;
|
|
98
|
+
}
|