-
Notifications
You must be signed in to change notification settings - Fork 42
/
Copy pathcli.ts
373 lines (344 loc) · 11.8 KB
/
cli.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
#!/usr/bin/env node
require("source-map-support").install();
import { paginateDescribeLogGroups } from "@aws-sdk/client-cloudwatch-logs";
import { paginateListRoles } from "@aws-sdk/client-iam";
import { paginateListFunctions, paginateListLayers } from "@aws-sdk/client-lambda";
import { paginateListSubscriptions, paginateListTopics } from "@aws-sdk/client-sns";
import { paginateListQueues } from "@aws-sdk/client-sqs";
import { paginateListDirectoryBuckets } from "@aws-sdk/client-s3";
import { Paginator } from "@aws-sdk/types";
import { program } from "commander";
import { readdir, remove } from "fs-extra";
import { tmpdir } from "os";
import path from "path";
import * as readline from "readline";
import * as awsFaast from "./aws/aws-faast";
import { PersistentCache, caches } from "./cache";
import { keysOf, uuidv4Pattern } from "./shared";
import { throttle } from "./throttle";
const warn = console.warn;
const log = console.log;
interface CleanupOptions {
region?: string; // AWS only.
execute: boolean;
}
async function deleteResources(
name: string,
matchingResources: string[],
doRemove: (arg: string) => Promise<any>,
{ concurrency = 10, rate = 5, burst = 5 } = {}
) {
const ora = (await import("ora")).default;
if (matchingResources.length > 0) {
const timeEstimate = (nResources: number) =>
nResources <= 5 ? "" : `(est: ${(nResources / 5).toFixed(0)}s)`;
const updateSpinnerText = (nResources: number = 0) =>
`Deleting ${matchingResources.length} ${name} ${timeEstimate(nResources)}`;
const spinner = ora(updateSpinnerText(matchingResources.length)).start();
let done = 0;
const scheduleRemove = throttle(
{
concurrency,
rate,
burst,
retry: 5
},
async arg => {
await doRemove(arg);
done++;
}
);
const timer = setInterval(
() => (spinner.text = updateSpinnerText(matchingResources.length - done)),
1000
);
try {
await Promise.all(
matchingResources.map(resource =>
scheduleRemove(resource).catch(err =>
console.warn(`Could not remove resource ${resource}: ${err}`)
)
)
);
} finally {
clearInterval(timer);
spinner.text = updateSpinnerText();
}
spinner.stopAndPersist({ symbol: "✔" });
}
}
async function cleanupAWS({ region, execute }: CleanupOptions) {
let nResources = 0;
const output = (msg: string) => !execute && log(msg);
const { cloudwatch, iam, lambda, sns, sqs, s3 } = await awsFaast.createAwsApis(
region! as awsFaast.AwsRegion
);
async function listAWSResource<T, U>(
pattern: RegExp,
getList: () => Paginator<T>,
extractList: (arg: T) => U[] | undefined,
extractElement: (arg: U) => string | undefined
) {
const allResources: string[] = [];
for await (const page of getList()) {
const elems = (page && extractList(page)) || [];
allResources.push(...elems.map(elem => extractElement(elem) || ""));
}
const matchingResources = allResources.filter(t => t.match(pattern));
matchingResources.forEach(resource => output(` ${resource}`));
return matchingResources;
}
async function deleteAWSResource<T, U>(
name: string,
pattern: RegExp,
getList: () => Paginator<T>,
extractList: (arg: T) => U[] | undefined,
extractElement: (arg: U) => string | undefined,
doRemove: (arg: string) => Promise<any>
) {
const allResources = await listAWSResource(
pattern,
getList,
extractList,
extractElement
);
nResources += allResources.length;
if (execute) {
await deleteResources(name, allResources, doRemove, {
concurrency: 10,
rate: 5,
burst: 5
});
}
}
output(`SNS subscriptions`);
await deleteAWSResource(
"SNS subscription(s)",
new RegExp(`:faast-${uuidv4Pattern}`),
() => paginateListSubscriptions({ client: sns }, {}),
page => page.Subscriptions,
subscription => subscription.SubscriptionArn,
SubscriptionArn => sns.unsubscribe({ SubscriptionArn })
);
output(`SNS topics`);
await deleteAWSResource(
"SNS topic(s)",
new RegExp(`:faast-${uuidv4Pattern}`),
() => paginateListTopics({ client: sns }, {}),
page => page.Topics,
topic => topic.TopicArn,
TopicArn => sns.deleteTopic({ TopicArn })
);
output(`SQS queues`);
await deleteAWSResource(
"SQS queue(s)",
new RegExp(`/faast-${uuidv4Pattern}`),
() => paginateListQueues({ client: sqs }, {}),
page => page.QueueUrls,
queueUrl => queueUrl,
QueueUrl => sqs.deleteQueue({ QueueUrl })
);
output(`S3 buckets`);
await deleteAWSResource(
"S3 bucket(s)",
new RegExp(`^faast-${uuidv4Pattern}`),
() => paginateListDirectoryBuckets({ client: s3 }, {}),
page => page.Buckets,
Bucket => Bucket.Name,
async Bucket => {
const objects = await s3.listObjectsV2({ Bucket, Prefix: "faast-" });
const keys = (objects.Contents || []).map(entry => ({ Key: entry.Key! }));
if (keys.length > 0) {
await s3.deleteObjects({ Bucket, Delete: { Objects: keys } });
}
await s3.deleteBucket({ Bucket });
}
);
output(`Lambda functions`);
await deleteAWSResource(
"Lambda function(s)",
new RegExp(`^faast-${uuidv4Pattern}`),
() => paginateListFunctions({ client: lambda }, {}),
page => page.Functions,
func => func.FunctionName,
FunctionName => lambda.deleteFunction({ FunctionName })
);
output(`IAM roles`);
await deleteAWSResource(
"IAM role(s)",
/^faast-cached-lambda-role$/,
() => paginateListRoles({ client: iam }, {}),
page => page.Roles,
role => role.RoleName,
RoleName => awsFaast.deleteRole(RoleName, iam)
);
output(`IAM test roles`);
await deleteAWSResource(
"IAM test role(s)",
new RegExp(`^faast-test-.*${uuidv4Pattern}$`),
() => paginateListRoles({ client: iam }, {}),
page => page.Roles,
role => role.RoleName,
RoleName => awsFaast.deleteRole(RoleName, iam)
);
output(`Lambda layers`);
await deleteAWSResource(
"Lambda layer(s)",
new RegExp(`^faast-(${uuidv4Pattern})|([a-f0-9]{64})`),
() => paginateListLayers({ client: lambda }, { CompatibleRuntime: "nodejs" }),
page => page.Layers,
layer => layer.LayerName,
async LayerName => {
const versions = await lambda.listLayerVersions({ LayerName });
for (const layerVersion of versions.LayerVersions || []) {
await lambda.deleteLayerVersion({
LayerName,
VersionNumber: layerVersion.Version!
});
}
}
);
async function cleanupCacheDir(cache: PersistentCache) {
output(`Persistent cache: ${cache.dir}`);
const entries = await cache.entries();
if (!execute) {
output(` cache entries: ${entries.length}`);
}
nResources += entries.length;
if (execute) {
cache.clear({ leaveEmptyDir: false });
}
}
for (const cache of keysOf(caches)) {
await cleanupCacheDir(await caches[cache]);
}
output(`Cloudwatch log groups`);
await deleteAWSResource(
"Cloudwatch log group(s)",
new RegExp(`/faast-${uuidv4Pattern}$`),
() => paginateDescribeLogGroups({ client: cloudwatch }, {}),
page => page.logGroups,
logGroup => logGroup.logGroupName,
logGroupName => cloudwatch.deleteLogGroup({ logGroupName })
);
return nResources;
}
async function cleanupLocal({ execute }: CleanupOptions) {
const output = (msg: string) => !execute && log(msg);
const tmpDir = tmpdir();
const dir = await readdir(tmpDir);
let nResources = 0;
output(`Temporary directories:`);
const entryRegexp = new RegExp(`^faast-${uuidv4Pattern}$`);
for (const entry of dir) {
if (entry.match(entryRegexp)) {
nResources++;
const faastDir = path.join(tmpDir, entry);
output(`${faastDir}`);
if (execute) {
await remove(faastDir);
}
}
}
return nResources;
}
async function prompt() {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
await new Promise<void>(resolve => {
rl.question(
"WARNING: this operation will delete resources. Confirm? [y/N] ",
answer => {
if (answer !== "y") {
log(`Execution aborted.`);
process.exit(0);
}
rl.close();
resolve();
}
);
});
}
async function runCleanup(cloud: string, options: CleanupOptions) {
let nResources = 0;
if (cloud === "aws") {
nResources = await cleanupAWS(options);
} else if (cloud === "local") {
nResources = await cleanupLocal(options);
} else {
warn(`Unknown cloud name "${cloud}". Must specify "aws" or "local".`);
process.exit(-1);
}
if (options.execute) {
log(`Done.`);
} else {
if (nResources === 0) {
log(`No resources to clean up.`);
}
}
return nResources;
}
async function main() {
let cloud!: string;
let command: string | undefined;
program
.version("0.1.0")
.option("-v, --verbose", "Verbose mode")
.option(
"-r, --region <region>",
"Cloud region to operate on. Defaults to us-west-2 for AWS."
)
.option(
"-x, --execute",
"Execute the cleanup process. If this option is not specified, the output will be a dry run."
)
.option("-f, --force", "When used with -x, skips the prompt")
.command("cleanup <cloud>")
.description(
`Cleanup faast.js resources that may have leaked. The <cloud> argument must be "aws" or "local".
By default the output is a dry run and will only print the actions that would be performed if '-x' is specified.`
)
.action((arg: string) => {
command = "cleanup";
cloud = arg;
});
const opts = program.parse(process.argv).opts();
if (opts.verbose) {
process.env.DEBUG = "faast:*";
}
const execute = opts.execute || false;
let region = opts.region;
if (!region) {
switch (cloud) {
case "aws":
region = awsFaast.defaults.region;
break;
}
}
const force = opts.force || false;
region && log(`Region: ${region}`);
const options = { region, execute };
let nResources = 0;
if (command === "cleanup") {
if (execute && !force) {
nResources = await runCleanup(cloud, { ...options, execute: false });
if (nResources > 0) {
await prompt();
} else {
process.exit(0);
}
}
nResources = await runCleanup(cloud, options);
if (!execute && nResources > 0) {
log(
`(dryrun mode, no resources will be deleted, specify -x to execute cleanup)`
);
}
} else {
log(`No command specified.`);
program.help();
}
}
main();