@fishawack/lab-env 5.3.0 → 5.4.0-beta.1
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 +24 -0
- package/_Test/provision.js +2 -0
- package/_Test/prune.js +291 -0
- package/cli.js +1 -0
- package/commands/create/cmds/key.js +0 -1
- package/commands/create/cmds/prune.js +723 -0
- package/commands/create/libs/prompts.js +1 -1
- package/commands/create/services/aws/cloudfront.js +53 -0
- package/commands/create/services/aws/elasticbeanstalk.js +70 -0
- package/commands/create/services/aws/iam.js +11 -0
- package/eslint.config.js +1 -0
- package/globals.js +9 -4
- package/package.json +1 -1
- package/python/0/CHANGELOG.md +10 -0
- package/python/0/Dockerfile +9 -0
- package/python/0/docker-compose.yml +9 -2
- package/python/0/entrypoint.sh +24 -0
- package/python/0/package.json +1 -1
|
@@ -0,0 +1,723 @@
|
|
|
1
|
+
const _ = require("../../../globals.js");
|
|
2
|
+
const inquirer = require("inquirer");
|
|
3
|
+
const aws = require("../services/aws/index.js");
|
|
4
|
+
const { setAWSClientDefaults } = require("../services/aws/misc.js");
|
|
5
|
+
const { colorize, Spinner } = require("../libs/utilities");
|
|
6
|
+
const { client, region } = require("../libs/prompts.js");
|
|
7
|
+
|
|
8
|
+
const VERSIONS_TO_KEEP = 10;
|
|
9
|
+
const STALE_MONTHS = 3;
|
|
10
|
+
const REGIONS = region.choices;
|
|
11
|
+
|
|
12
|
+
function identifyPrunableVersions(versions) {
|
|
13
|
+
const sorted = [...versions].sort(
|
|
14
|
+
(a, b) => new Date(b.DateCreated) - new Date(a.DateCreated),
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
keep: sorted.slice(0, VERSIONS_TO_KEEP),
|
|
19
|
+
prunable: sorted.slice(VERSIONS_TO_KEEP),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function printVersionReport(results) {
|
|
24
|
+
let totalVersions = 0;
|
|
25
|
+
let totalPrunable = 0;
|
|
26
|
+
|
|
27
|
+
for (const { appName, keep, prunable } of results) {
|
|
28
|
+
totalVersions += keep.length + prunable.length;
|
|
29
|
+
totalPrunable += prunable.length;
|
|
30
|
+
|
|
31
|
+
console.log(`\n ${colorize(appName, "title")}`);
|
|
32
|
+
console.log(
|
|
33
|
+
` Total: ${keep.length + prunable.length} | Keeping: ${keep.length} | Prunable: ${colorize(prunable.length, prunable.length ? "error" : "success")}`,
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
if (keep.length) {
|
|
37
|
+
console.log(`\n ${colorize("Keeping", "success")}`);
|
|
38
|
+
keep.forEach((v) => {
|
|
39
|
+
const date = new Date(v.DateCreated)
|
|
40
|
+
.toISOString()
|
|
41
|
+
.split("T")[0];
|
|
42
|
+
console.log(
|
|
43
|
+
` ${colorize("✓", "success")} ${v.VersionLabel} ${colorize(`(${date})`, "info")}`,
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (prunable.length) {
|
|
49
|
+
console.log(`\n ${colorize("Prunable", "error")}`);
|
|
50
|
+
prunable.forEach((v) => {
|
|
51
|
+
const date = new Date(v.DateCreated)
|
|
52
|
+
.toISOString()
|
|
53
|
+
.split("T")[0];
|
|
54
|
+
console.log(
|
|
55
|
+
` ${colorize("✕", "error")} ${v.VersionLabel} ${colorize(`(${date})`, "warning")}`,
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
} else {
|
|
59
|
+
console.log(` ${colorize("(nothing to prune)", "success")}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { totalVersions, totalPrunable };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function deleteVersions(results) {
|
|
67
|
+
let deleted = 0;
|
|
68
|
+
let failed = 0;
|
|
69
|
+
|
|
70
|
+
for (const { client, region, appName, prunable } of results) {
|
|
71
|
+
setAWSClientDefaults(client, region);
|
|
72
|
+
|
|
73
|
+
for (const v of prunable) {
|
|
74
|
+
let attempts = 0;
|
|
75
|
+
|
|
76
|
+
while (attempts < 3) {
|
|
77
|
+
try {
|
|
78
|
+
await aws.elasticbeanstalk.deleteApplicationVersion(
|
|
79
|
+
appName,
|
|
80
|
+
v.VersionLabel,
|
|
81
|
+
);
|
|
82
|
+
deleted++;
|
|
83
|
+
break;
|
|
84
|
+
} catch (e) {
|
|
85
|
+
attempts++;
|
|
86
|
+
|
|
87
|
+
if (
|
|
88
|
+
e.name === "TooManyRequestsException" ||
|
|
89
|
+
e.name === "Throttling" ||
|
|
90
|
+
e.$metadata?.httpStatusCode === 429
|
|
91
|
+
) {
|
|
92
|
+
const delay = Math.pow(2, attempts) * 1000;
|
|
93
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
94
|
+
} else {
|
|
95
|
+
failed++;
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { deleted, failed };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function isProductionDistribution(tags) {
|
|
107
|
+
const env = tags.find((t) => t.Key === "environment");
|
|
108
|
+
|
|
109
|
+
if (!env) return false;
|
|
110
|
+
|
|
111
|
+
return /prod/i.test(env.Value);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isAutomatedDistribution(tags) {
|
|
115
|
+
return tags.some((t) => t.Key === "automated");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function isStaleDistribution(lastModified) {
|
|
119
|
+
const cutoff = new Date();
|
|
120
|
+
cutoff.setMonth(cutoff.getMonth() - STALE_MONTHS);
|
|
121
|
+
|
|
122
|
+
return new Date(lastModified) < cutoff;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function getTagValue(tags, key) {
|
|
126
|
+
const tag = tags.find((t) => t.Key === key);
|
|
127
|
+
return tag ? tag.Value : null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function printDistributionReport(results) {
|
|
131
|
+
let totalDistributions = 0;
|
|
132
|
+
let totalPrunable = 0;
|
|
133
|
+
|
|
134
|
+
for (const { keep, prunable, ignored } of results) {
|
|
135
|
+
totalDistributions +=
|
|
136
|
+
keep.length + prunable.length + (ignored?.length || 0);
|
|
137
|
+
totalPrunable += prunable.length;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
for (const { keep } of results) {
|
|
141
|
+
if (!keep.length) continue;
|
|
142
|
+
|
|
143
|
+
console.log(`\n ${colorize("Keeping", "success")}`);
|
|
144
|
+
keep.forEach((d) => {
|
|
145
|
+
const date = new Date(d.lastActivity || d.LastModifiedTime)
|
|
146
|
+
.toISOString()
|
|
147
|
+
.split("T")[0];
|
|
148
|
+
const env = d.environment || "unknown";
|
|
149
|
+
console.log(
|
|
150
|
+
` ${colorize("✓", "success")} ${d.Id} ${colorize(d.name, "info")} ${colorize(date, "warning")} ${colorize(env, "success")} ${colorize(`(${d.reason})`, "info")}`,
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
for (const { ignored } of results) {
|
|
156
|
+
if (!ignored?.length) continue;
|
|
157
|
+
|
|
158
|
+
console.log(`\n ${colorize("Ignored (not automated)", "info")}`);
|
|
159
|
+
ignored.forEach((d) => {
|
|
160
|
+
const date = new Date(d.LastModifiedTime)
|
|
161
|
+
.toISOString()
|
|
162
|
+
.split("T")[0];
|
|
163
|
+
const env = d.environment || "unknown";
|
|
164
|
+
console.log(
|
|
165
|
+
` ${colorize("-", "info")} ${d.Id} ${colorize(d.name, "info")} ${colorize(date, "warning")} ${colorize(env, "success")}`,
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
for (const { prunable } of results) {
|
|
171
|
+
if (!prunable.length) continue;
|
|
172
|
+
|
|
173
|
+
console.log(`\n ${colorize("Prunable", "error")}`);
|
|
174
|
+
prunable.forEach((d) => {
|
|
175
|
+
const date = new Date(d.lastActivity || d.LastModifiedTime)
|
|
176
|
+
.toISOString()
|
|
177
|
+
.split("T")[0];
|
|
178
|
+
const env = d.environment || "unknown";
|
|
179
|
+
console.log(
|
|
180
|
+
` ${colorize("✕", "error")} ${d.Id} ${colorize(d.name, "info")} ${colorize(date, "warning")} ${colorize(env, "success")}`,
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return { totalDistributions, totalPrunable };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function deleteDistributions(results) {
|
|
189
|
+
let deleted = 0;
|
|
190
|
+
let failed = 0;
|
|
191
|
+
|
|
192
|
+
for (const { client, region, prunable } of results) {
|
|
193
|
+
setAWSClientDefaults(client, region);
|
|
194
|
+
|
|
195
|
+
for (const d of prunable) {
|
|
196
|
+
try {
|
|
197
|
+
await aws.staticTerminate(
|
|
198
|
+
d.name,
|
|
199
|
+
d.repository,
|
|
200
|
+
d.environment,
|
|
201
|
+
d.Id,
|
|
202
|
+
);
|
|
203
|
+
deleted++;
|
|
204
|
+
} catch {
|
|
205
|
+
failed++;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return { deleted, failed };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function resolveClients(argv) {
|
|
214
|
+
if (argv.all) return client.choices;
|
|
215
|
+
if (argv.client) return [argv.client];
|
|
216
|
+
|
|
217
|
+
const { check } = await inquirer.prompt([
|
|
218
|
+
{
|
|
219
|
+
type: "confirm",
|
|
220
|
+
name: "check",
|
|
221
|
+
message: "Prune all clients?",
|
|
222
|
+
default: true,
|
|
223
|
+
},
|
|
224
|
+
]);
|
|
225
|
+
|
|
226
|
+
if (check) return client.choices;
|
|
227
|
+
|
|
228
|
+
const answers = await inquirer.prompt([client]);
|
|
229
|
+
return [answers.client];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function resolveRegions(argv) {
|
|
233
|
+
if (argv.all) return REGIONS;
|
|
234
|
+
if (argv.region) return [argv.region];
|
|
235
|
+
|
|
236
|
+
const { check } = await inquirer.prompt([
|
|
237
|
+
{
|
|
238
|
+
type: "confirm",
|
|
239
|
+
name: "check",
|
|
240
|
+
message: "Prune for all regions?",
|
|
241
|
+
default: true,
|
|
242
|
+
},
|
|
243
|
+
]);
|
|
244
|
+
|
|
245
|
+
if (check) return REGIONS;
|
|
246
|
+
|
|
247
|
+
const answers = await inquirer.prompt([region]);
|
|
248
|
+
return [answers.region];
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function resolveApplications(argv, applications) {
|
|
252
|
+
if (argv.all) return applications;
|
|
253
|
+
|
|
254
|
+
if (argv.application) {
|
|
255
|
+
const filtered = applications.filter(
|
|
256
|
+
(app) => app.ApplicationName === argv.application,
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
if (!filtered.length) {
|
|
260
|
+
console.log(
|
|
261
|
+
`\n ${colorize(`Application "${argv.application}" not found.`, "error")}\n`,
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return filtered;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const { check } = await inquirer.prompt([
|
|
269
|
+
{
|
|
270
|
+
type: "confirm",
|
|
271
|
+
name: "check",
|
|
272
|
+
message: "Prune all applications?",
|
|
273
|
+
default: true,
|
|
274
|
+
},
|
|
275
|
+
]);
|
|
276
|
+
|
|
277
|
+
if (check) return applications;
|
|
278
|
+
|
|
279
|
+
const { selected } = await inquirer.prompt([
|
|
280
|
+
{
|
|
281
|
+
type: "checkbox",
|
|
282
|
+
name: "selected",
|
|
283
|
+
message: "Which applications would you like to prune?",
|
|
284
|
+
choices: applications.map((app) => ({
|
|
285
|
+
name: app.ApplicationName,
|
|
286
|
+
value: app.ApplicationName,
|
|
287
|
+
})),
|
|
288
|
+
validate: (input) =>
|
|
289
|
+
input.length > 0 || "Select at least one application",
|
|
290
|
+
},
|
|
291
|
+
]);
|
|
292
|
+
|
|
293
|
+
return applications.filter((app) => selected.includes(app.ApplicationName));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
module.exports = [
|
|
297
|
+
"prune",
|
|
298
|
+
_.config.preset === "devops" ? "Prune unused resources from AWS" : false,
|
|
299
|
+
(yargs) => {
|
|
300
|
+
yargs.option("all", {
|
|
301
|
+
alias: "a",
|
|
302
|
+
describe: "Prune all clients, regions and applications",
|
|
303
|
+
type: "boolean",
|
|
304
|
+
default: false,
|
|
305
|
+
});
|
|
306
|
+
yargs.option("client", {
|
|
307
|
+
alias: "c",
|
|
308
|
+
describe: "AWS account to use",
|
|
309
|
+
type: "string",
|
|
310
|
+
});
|
|
311
|
+
yargs.option("region", {
|
|
312
|
+
alias: "r",
|
|
313
|
+
describe: "AWS region to use",
|
|
314
|
+
type: "string",
|
|
315
|
+
});
|
|
316
|
+
yargs.option("application", {
|
|
317
|
+
describe: "Application name to prune",
|
|
318
|
+
type: "string",
|
|
319
|
+
});
|
|
320
|
+
yargs.option("include-ignored", {
|
|
321
|
+
alias: "i",
|
|
322
|
+
describe: "Include non-automated distributions in prune targets",
|
|
323
|
+
type: "boolean",
|
|
324
|
+
default: false,
|
|
325
|
+
});
|
|
326
|
+
},
|
|
327
|
+
async (argv) => {
|
|
328
|
+
const { resource } = await inquirer.prompt([
|
|
329
|
+
{
|
|
330
|
+
type: "list",
|
|
331
|
+
name: "resource",
|
|
332
|
+
message: "What would you like to prune?",
|
|
333
|
+
choices: [
|
|
334
|
+
"Elastic Beanstalk application versions",
|
|
335
|
+
"CloudFront static distributions",
|
|
336
|
+
],
|
|
337
|
+
},
|
|
338
|
+
]);
|
|
339
|
+
|
|
340
|
+
const clients = await resolveClients(argv);
|
|
341
|
+
const regions =
|
|
342
|
+
resource === "CloudFront static distributions"
|
|
343
|
+
? ["us-east-1"]
|
|
344
|
+
: await resolveRegions(argv);
|
|
345
|
+
|
|
346
|
+
if (resource === "Elastic Beanstalk application versions") {
|
|
347
|
+
const allResults = [];
|
|
348
|
+
|
|
349
|
+
for (const c of clients) {
|
|
350
|
+
for (const r of regions) {
|
|
351
|
+
setAWSClientDefaults(c, r);
|
|
352
|
+
|
|
353
|
+
let applications;
|
|
354
|
+
|
|
355
|
+
try {
|
|
356
|
+
applications =
|
|
357
|
+
await aws.elasticbeanstalk.listApplications();
|
|
358
|
+
} catch {
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (!applications.length) continue;
|
|
363
|
+
|
|
364
|
+
const selected = await resolveApplications(
|
|
365
|
+
argv,
|
|
366
|
+
applications,
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
if (!selected.length) continue;
|
|
370
|
+
|
|
371
|
+
console.log(`\n ${colorize(`${c} / ${r}`, "divider")}`);
|
|
372
|
+
|
|
373
|
+
for (const app of selected) {
|
|
374
|
+
const versions =
|
|
375
|
+
await aws.elasticbeanstalk.listApplicationVersions(
|
|
376
|
+
app.ApplicationName,
|
|
377
|
+
);
|
|
378
|
+
const { keep, prunable } =
|
|
379
|
+
identifyPrunableVersions(versions);
|
|
380
|
+
allResults.push({
|
|
381
|
+
client: c,
|
|
382
|
+
region: r,
|
|
383
|
+
appName: app.ApplicationName,
|
|
384
|
+
keep,
|
|
385
|
+
prunable,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
printVersionReport(allResults.slice(-selected.length));
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const totalPrunable = allResults.reduce(
|
|
394
|
+
(sum, r) => sum + r.prunable.length,
|
|
395
|
+
0,
|
|
396
|
+
);
|
|
397
|
+
const totalVersions = allResults.reduce(
|
|
398
|
+
(sum, r) => sum + r.keep.length + r.prunable.length,
|
|
399
|
+
0,
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
console.log(
|
|
403
|
+
`\n ${colorize("Grand total", "title")}: ${totalVersions} versions, keeping ${VERSIONS_TO_KEEP} per app, ${colorize(`${totalPrunable} prunable`, totalPrunable ? "error" : "success")}\n`,
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
if (!totalPrunable) return;
|
|
407
|
+
|
|
408
|
+
const { confirm } = await inquirer.prompt([
|
|
409
|
+
{
|
|
410
|
+
type: "confirm",
|
|
411
|
+
name: "confirm",
|
|
412
|
+
message: `Delete ${colorize(totalPrunable, "error")} application version(s)? This cannot be undone.`,
|
|
413
|
+
default: false,
|
|
414
|
+
},
|
|
415
|
+
]);
|
|
416
|
+
|
|
417
|
+
if (!confirm) {
|
|
418
|
+
console.log(`\n ${colorize("Aborted.", "warning")}\n`);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const { deleted, failed } = await Spinner.prototype.simple(
|
|
423
|
+
`Deleting ${totalPrunable} application version(s)`,
|
|
424
|
+
() => deleteVersions(allResults),
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
console.log(
|
|
428
|
+
`\n ${colorize("✓", "success")} Deleted ${colorize(deleted, "success")} application version(s)${failed ? ` (${colorize(`${failed} failed`, "error")})` : ""}\n`,
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (resource === "CloudFront static distributions") {
|
|
433
|
+
const allResults = [];
|
|
434
|
+
|
|
435
|
+
for (const c of clients) {
|
|
436
|
+
setAWSClientDefaults(c, "us-east-1");
|
|
437
|
+
|
|
438
|
+
const spinner = new Spinner(`Scanning ${c}`, true);
|
|
439
|
+
|
|
440
|
+
let distributions;
|
|
441
|
+
|
|
442
|
+
try {
|
|
443
|
+
distributions = await aws.cloudfront.listDistributions();
|
|
444
|
+
} catch {
|
|
445
|
+
spinner.ora.stop();
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (!distributions.length) {
|
|
450
|
+
spinner.ora.stop();
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const keep = [];
|
|
455
|
+
const prunable = [];
|
|
456
|
+
const ignored = [];
|
|
457
|
+
|
|
458
|
+
for (const dist of distributions) {
|
|
459
|
+
let tags;
|
|
460
|
+
let attempts = 0;
|
|
461
|
+
|
|
462
|
+
while (attempts < 5) {
|
|
463
|
+
try {
|
|
464
|
+
// prettier-ignore
|
|
465
|
+
tags = await aws.cloudfront.getDistributionTags(dist.ARN);
|
|
466
|
+
break;
|
|
467
|
+
} catch (e) {
|
|
468
|
+
attempts++;
|
|
469
|
+
|
|
470
|
+
if (
|
|
471
|
+
e.name === "Throttling" ||
|
|
472
|
+
e.$metadata?.httpStatusCode === 429
|
|
473
|
+
) {
|
|
474
|
+
const delay = Math.pow(2, attempts) * 1000;
|
|
475
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
476
|
+
} else {
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (!tags) continue;
|
|
483
|
+
|
|
484
|
+
// Throttle to stay under ListTagsForResource rate limit
|
|
485
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
486
|
+
|
|
487
|
+
if (!isAutomatedDistribution(tags)) {
|
|
488
|
+
const origin =
|
|
489
|
+
dist.Origins?.Items?.[0]?.DomainName || "";
|
|
490
|
+
const name = origin.split(".s3.")[0];
|
|
491
|
+
const environment = getTagValue(tags, "environment");
|
|
492
|
+
const repository = getTagValue(tags, "repository");
|
|
493
|
+
ignored.push({
|
|
494
|
+
client: c,
|
|
495
|
+
region: "us-east-1",
|
|
496
|
+
Id: dist.Id,
|
|
497
|
+
name,
|
|
498
|
+
LastModifiedTime: dist.LastModifiedTime,
|
|
499
|
+
environment,
|
|
500
|
+
repository,
|
|
501
|
+
});
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const repository = getTagValue(tags, "repository");
|
|
506
|
+
const environment = getTagValue(tags, "environment");
|
|
507
|
+
const origin = dist.Origins?.Items?.[0]?.DomainName || "";
|
|
508
|
+
const name = origin.split(".s3.")[0];
|
|
509
|
+
|
|
510
|
+
const entry = {
|
|
511
|
+
client: c,
|
|
512
|
+
region: "us-east-1",
|
|
513
|
+
Id: dist.Id,
|
|
514
|
+
name,
|
|
515
|
+
LastModifiedTime: dist.LastModifiedTime,
|
|
516
|
+
lastActivity: dist.LastModifiedTime,
|
|
517
|
+
repository,
|
|
518
|
+
environment,
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
// Use latest invalidation date as activity indicator
|
|
522
|
+
try {
|
|
523
|
+
// prettier-ignore
|
|
524
|
+
const invalidationDate = await aws.cloudfront.getLatestInvalidationDate(dist.Id);
|
|
525
|
+
if (invalidationDate) {
|
|
526
|
+
const invDate = new Date(invalidationDate);
|
|
527
|
+
const modDate = new Date(dist.LastModifiedTime);
|
|
528
|
+
if (invDate > modDate) {
|
|
529
|
+
entry.lastActivity = invalidationDate;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
} catch {
|
|
533
|
+
// Fall back to LastModifiedTime
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Throttle API calls
|
|
537
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
538
|
+
|
|
539
|
+
if (isProductionDistribution(tags)) {
|
|
540
|
+
keep.push({
|
|
541
|
+
...entry,
|
|
542
|
+
reason: "production",
|
|
543
|
+
});
|
|
544
|
+
} else if (!isStaleDistribution(entry.lastActivity)) {
|
|
545
|
+
keep.push({
|
|
546
|
+
...entry,
|
|
547
|
+
reason: "recent",
|
|
548
|
+
});
|
|
549
|
+
} else {
|
|
550
|
+
prunable.push(entry);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
spinner.ora.stop();
|
|
555
|
+
|
|
556
|
+
if (!keep.length && !prunable.length && !ignored.length)
|
|
557
|
+
continue;
|
|
558
|
+
|
|
559
|
+
const total = keep.length + prunable.length + ignored.length;
|
|
560
|
+
|
|
561
|
+
console.log(`\n ${colorize(c, "divider")}`);
|
|
562
|
+
console.log(
|
|
563
|
+
` Total: ${total} | Keeping: ${keep.length} | Ignored: ${ignored.length} | Prunable: ${colorize(prunable.length, prunable.length ? "error" : "success")}`,
|
|
564
|
+
);
|
|
565
|
+
|
|
566
|
+
allResults.push({
|
|
567
|
+
client: c,
|
|
568
|
+
region: "us-east-1",
|
|
569
|
+
keep,
|
|
570
|
+
prunable,
|
|
571
|
+
ignored,
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
printDistributionReport(allResults.slice(-1));
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const totalPrunable = allResults.reduce(
|
|
578
|
+
(sum, r) => sum + r.prunable.length,
|
|
579
|
+
0,
|
|
580
|
+
);
|
|
581
|
+
const totalIgnored = allResults.reduce(
|
|
582
|
+
(sum, r) => sum + (r.ignored?.length || 0),
|
|
583
|
+
0,
|
|
584
|
+
);
|
|
585
|
+
const totalDistributions = allResults.reduce(
|
|
586
|
+
(sum, r) =>
|
|
587
|
+
sum +
|
|
588
|
+
r.keep.length +
|
|
589
|
+
r.prunable.length +
|
|
590
|
+
(r.ignored?.length || 0),
|
|
591
|
+
0,
|
|
592
|
+
);
|
|
593
|
+
|
|
594
|
+
console.log(
|
|
595
|
+
`\n ${colorize("Grand total", "title")}: ${totalDistributions} distributions, ${totalIgnored} ignored, ${colorize(`${totalPrunable} prunable`, totalPrunable ? "error" : "success")}${argv.includeIgnored ? ` ${colorize(`(+${totalIgnored} ignored included)`, "warning")}` : ""}\n`,
|
|
596
|
+
);
|
|
597
|
+
|
|
598
|
+
const totalTargets = argv.includeIgnored
|
|
599
|
+
? totalPrunable + totalIgnored
|
|
600
|
+
: totalPrunable;
|
|
601
|
+
|
|
602
|
+
if (!totalTargets) return;
|
|
603
|
+
|
|
604
|
+
const targetLabel = argv.includeIgnored
|
|
605
|
+
? `${totalPrunable} prunable + ${totalIgnored} ignored`
|
|
606
|
+
: `${totalPrunable}`;
|
|
607
|
+
|
|
608
|
+
const { confirm } = await inquirer.prompt([
|
|
609
|
+
{
|
|
610
|
+
type: "confirm",
|
|
611
|
+
name: "confirm",
|
|
612
|
+
message: `Delete ${colorize(targetLabel, "error")} distribution(s)? This will remove S3 buckets, CloudFront distributions and functions. This cannot be undone.`,
|
|
613
|
+
default: false,
|
|
614
|
+
},
|
|
615
|
+
]);
|
|
616
|
+
|
|
617
|
+
if (!confirm) {
|
|
618
|
+
console.log(`\n ${colorize("Aborted.", "warning")}\n`);
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const { pruneAll } = await inquirer.prompt([
|
|
623
|
+
{
|
|
624
|
+
type: "confirm",
|
|
625
|
+
name: "pruneAll",
|
|
626
|
+
message: "Prune all?",
|
|
627
|
+
default: false,
|
|
628
|
+
},
|
|
629
|
+
]);
|
|
630
|
+
|
|
631
|
+
let toPrune = allResults;
|
|
632
|
+
|
|
633
|
+
if (!pruneAll) {
|
|
634
|
+
const allPrunable = allResults.flatMap((r) =>
|
|
635
|
+
r.prunable.map((d) => ({
|
|
636
|
+
...d,
|
|
637
|
+
_source: "prunable",
|
|
638
|
+
})),
|
|
639
|
+
);
|
|
640
|
+
|
|
641
|
+
const allIgnored = argv.includeIgnored
|
|
642
|
+
? allResults.flatMap((r) =>
|
|
643
|
+
(r.ignored || []).map((d) => ({
|
|
644
|
+
...d,
|
|
645
|
+
_source: "ignored",
|
|
646
|
+
})),
|
|
647
|
+
)
|
|
648
|
+
: [];
|
|
649
|
+
|
|
650
|
+
const formatChoice = (d, label) => {
|
|
651
|
+
const date = new Date(d.lastActivity || d.LastModifiedTime)
|
|
652
|
+
.toISOString()
|
|
653
|
+
.split("T")[0];
|
|
654
|
+
const env = d.environment || "unknown";
|
|
655
|
+
const suffix = label ? ` [${label}]` : "";
|
|
656
|
+
return {
|
|
657
|
+
name: `${d.Id} ${d.name} ${date} ${env}${suffix}`,
|
|
658
|
+
value: d.Id,
|
|
659
|
+
};
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
const choices = [];
|
|
663
|
+
|
|
664
|
+
if (allPrunable.length) {
|
|
665
|
+
choices.push(new inquirer.Separator("── Prunable ──"));
|
|
666
|
+
choices.push(...allPrunable.map((d) => formatChoice(d)));
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (allIgnored.length) {
|
|
670
|
+
// prettier-ignore
|
|
671
|
+
choices.push(new inquirer.Separator("── Ignored (not automated) ──"));
|
|
672
|
+
choices.push(
|
|
673
|
+
...allIgnored.map((d) => formatChoice(d, "ignored")),
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const { selected } = await inquirer.prompt([
|
|
678
|
+
{
|
|
679
|
+
type: "checkbox",
|
|
680
|
+
name: "selected",
|
|
681
|
+
message: "Which distributions would you like to prune?",
|
|
682
|
+
choices,
|
|
683
|
+
validate: (input) =>
|
|
684
|
+
input.length > 0 ||
|
|
685
|
+
"Select at least one distribution",
|
|
686
|
+
},
|
|
687
|
+
]);
|
|
688
|
+
|
|
689
|
+
const selectedSet = new Set(selected);
|
|
690
|
+
|
|
691
|
+
toPrune = allResults
|
|
692
|
+
.map((r) => ({
|
|
693
|
+
...r,
|
|
694
|
+
prunable: [
|
|
695
|
+
...r.prunable,
|
|
696
|
+
...(argv.includeIgnored ? r.ignored || [] : []),
|
|
697
|
+
].filter((d) => selectedSet.has(d.Id)),
|
|
698
|
+
}))
|
|
699
|
+
.filter((r) => r.prunable.length > 0);
|
|
700
|
+
} else if (argv.includeIgnored) {
|
|
701
|
+
// Prune all: merge ignored into prunable
|
|
702
|
+
toPrune = allResults.map((r) => ({
|
|
703
|
+
...r,
|
|
704
|
+
prunable: [...r.prunable, ...(r.ignored || [])],
|
|
705
|
+
}));
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const deleteCount = toPrune.reduce(
|
|
709
|
+
(sum, r) => sum + r.prunable.length,
|
|
710
|
+
0,
|
|
711
|
+
);
|
|
712
|
+
|
|
713
|
+
const { deleted, failed } = await Spinner.prototype.simple(
|
|
714
|
+
`Deleting ${deleteCount} distribution(s)`,
|
|
715
|
+
() => deleteDistributions(toPrune),
|
|
716
|
+
);
|
|
717
|
+
|
|
718
|
+
console.log(
|
|
719
|
+
`\n ${colorize("✓", "success")} Deleted ${colorize(deleted, "success")} distribution(s)${failed ? ` (${colorize(`${failed} failed`, "error")})` : ""}\n`,
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
},
|
|
723
|
+
];
|