aws-amicleaner 1.0.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/LICENSE ADDED
@@ -0,0 +1,9 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) widdix GmbH
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,72 @@
1
+ To clean up your AWS AMIs:
2
+ 1. Include AMIs by name or tag.
3
+ 2. Exclude AMIs in use, younger than N days, or the newest N images.
4
+ 3. Manually confirm the list of AMIs for deletion.
5
+
6
+ ## Examples
7
+
8
+ To delete all AMIs in eu-west-1 where the name starts with amiprefix-, are older than 5 days, and not the newest 3 images, run:
9
+ ```bash
10
+ aws-amicleaner --region eu-west-1 --include-name 'amiprefix-*' --exclude-newest 3 --exclude-days 5 --exclude-in-use --verbose
11
+ ```
12
+
13
+ A typical confirmation screen:
14
+
15
+ ```
16
+ +-----------+-------------+----------------------+--------------------------+---------+-----------------+-------------------------+
17
+ | Region | ID | Name | Creation Date | Delete? | Include reasons | Exclude reasons |
18
+ +-----------+-------------+----------------------+--------------------------+---------+-----------------+-------------------------+
19
+ | eu-west-1 | ami-0a...72 | amiprefix-1685107232 | 2023-05-27T13:32:21.000Z | no | name match | days not passed, newest |
20
+ | eu-west-1 | ami-02...3f | amiprefix-1685103569 | 2023-05-27T12:30:50.000Z | no | name match | days not passed, newest |
21
+ | eu-west-1 | ami-09...d5 | amiprefix-1685095689 | 2023-05-27T10:19:59.000Z | no | name match | days not passed, newest |
22
+ | eu-west-1 | ami-0f...c7 | amiprefix-1685039741 | 2023-05-26T18:47:37.000Z | no | name match | days not passed |
23
+ | eu-west-1 | ami-0f...f0 | amiprefix-1685018189 | 2023-05-26T12:49:02.000Z | no | name match | days not passed |
24
+ | eu-west-1 | ami-06...a8 | amiprefix-1685015512 | 2023-05-26T12:04:39.000Z | no | name match | days not passed |
25
+ | eu-west-1 | ami-04...44 | amiprefix-1684998014 | 2023-05-25T07:14:42.000Z | yes | name match | |
26
+ | eu-west-1 | ami-02...6e | amiprefix-1684954911 | 2023-05-24T19:15:37.000Z | yes | name match | |
27
+ | eu-west-1 | ami-0e...da | amiprefix-1684952424 | 2023-05-24T18:32:06.000Z | yes | name match | |
28
+ | eu-west-1 | ami-0b...54 | amiprefix-1684949922 | 2023-05-24T17:50:26.000Z | yes | name match | |
29
+ | eu-west-1 | ami-0b...2c | amiprefix-1684937102 | 2023-05-24T14:17:53.000Z | yes | name match | |
30
+ | eu-west-1 | ami-0e...aa | amiprefix-1684915092 | 2023-05-24T08:09:42.000Z | yes | name match | |
31
+ +-----------+-------------+----------------------+--------------------------+---------+-----------------+-------------------------+
32
+
33
+ Do you want to continue and remove 6 AMIs [y/N] ? :
34
+ ```
35
+
36
+ To delete all AMIs in eu-west-* (eu-west-1, eu-west-2, eu-west-3) tagged with CostCenter=X342-*-1111, are older than 7 days (default), are not the newest 5 images (default), and are not in use (default), run:
37
+ ```bash
38
+ aws-amicleaner --region 'eu-west-*' --include-tag-key CostCenter --include-tag-value 'X342-*-1111'
39
+ ```
40
+
41
+ Run the command without confirmation (useful in scripts):
42
+ ```bash
43
+ aws-amicleaner --region 'eu-west-*' --include-tag-key CostCenter --include-tag-value 'X342-*-1111' --force-delete
44
+ ```
45
+
46
+ To disable the defaults, run:
47
+ ```bash
48
+ aws-amicleaner --include-name 'amiprefix-*' --exclude-newest 0 --exclude-days 0 --no-exclude-in-use --no-verbose
49
+ ```
50
+
51
+ ## Arguments
52
+
53
+ ```
54
+ -h, --help show this help message and exit
55
+ --region REGION The AWS region, e.g. us-east-1, arg can be used more than once, wildcard * supported
56
+ --include-name INCLUDENAME
57
+ The name that must be present, wildcard * supported
58
+ --include-tag-key INCLUDETAGKEY
59
+ The tag key that must be present
60
+ --include-tag-value INCLUDETAGVALUE
61
+ The tag value (for the tag key) that must be present, wildcard * supported
62
+ --exclude-newest EXCLUDENEWEST
63
+ Exclude the newest N AMIs
64
+ --exclude-days EXCLUDEDAYS
65
+ Exclude AMIs from deletion that are younger than N days
66
+ --exclude-in-use, --no-exclude-in-use
67
+ Exclude AMIs from deletion that are in use by EC2 instances, ASGs, Launch Configurations, and Launch Templates (default: true)
68
+ -f, --force-delete, --no-force-delete
69
+ Skip confirmation before deletion (default: false)
70
+ --verbose, --no-verbose
71
+ Display additional information (default: true)
72
+ ```
package/index.js ADDED
@@ -0,0 +1,144 @@
1
+ const { createInterface } = require('node:readline');
2
+ const { ArgumentParser, BooleanOptionalAction } = require('argparse');
3
+ const { fetchAMIs, deleteAMI, fetchRegions} = require('./lib.js');
4
+ const pLimit = require('p-limit');
5
+
6
+ const AWS = require('aws-sdk');
7
+ const PrettyTable = require('./prettytable.js');
8
+
9
+ const rl = createInterface({
10
+ input: process.stdin,
11
+ output: process.stdout,
12
+ });
13
+
14
+ async function input(query) {
15
+ return new Promise(resolve => {
16
+ rl.question(query, answer => resolve(answer));
17
+ });
18
+ }
19
+
20
+ async function run({
21
+ regions: rawRegions,
22
+ includeName,
23
+ includeTagKey,
24
+ includeTagValue,
25
+ excludeNewest,
26
+ excludeInUse,
27
+ excludeDays,
28
+ forceDelete,
29
+ verbose
30
+ }) {
31
+ const now = Date.now();
32
+
33
+ const ec2 = {};
34
+ const autoscaling = {};
35
+ const ec2Client = (region) => {
36
+ if (!(region in ec2)) {
37
+ ec2[region] = new AWS.EC2({apiVersion: '2016-11-15', region});
38
+ }
39
+ return ec2[region];
40
+ };
41
+ const autoscalingClient = (region) => {
42
+ if (!(region in autoscaling)) {
43
+ autoscaling[region] = new AWS.AutoScaling({apiVersion: '2011-01-01', region});
44
+ }
45
+ return autoscaling[region];
46
+ };
47
+
48
+ let amis = [];
49
+
50
+ const regions = await fetchRegions(ec2Client('us-east-1'), rawRegions);
51
+ for (const region of regions) {
52
+ let regionalAMIs = await fetchAMIs(now, ec2Client(region), autoscalingClient(region), includeName, includeTagKey, includeTagValue, excludeNewest, excludeInUse, excludeDays);
53
+ regionalAMIs = regionalAMIs.map(ami => ({
54
+ region,
55
+ ...ami
56
+ }));
57
+ amis = [...amis, ...regionalAMIs];
58
+ }
59
+
60
+ if (verbose === true) {
61
+ const pt = new PrettyTable();
62
+ pt.sortTable('Name');
63
+ pt.create(['Region', 'ID', 'Name', 'Creation Date', 'Delete?', 'Include reasons', 'Exclude reasons'], amis.map(ami => [
64
+ ami.region,
65
+ ami.id,
66
+ ami.name,
67
+ new Date(ami.creationDate).toISOString(),
68
+ (ami.included === true && ami.excluded === false) ? 'yes' : 'no',
69
+ (ami.included === true) ? ami.includeReasons.join(', ') : '',
70
+ (ami.excluded === true) ? ami.excludeReasons.join(', ') : ''
71
+ ]));
72
+ pt.print();
73
+ }
74
+
75
+ amis = amis.filter(ami => ami.included === true && ami.excluded === false);
76
+
77
+ if (amis.length === 0) {
78
+ return;
79
+ }
80
+
81
+ let del = false;
82
+ if (forceDelete === true) {
83
+ del = true;
84
+ } else {
85
+ const answer = await input(`Do you want to continue and remove ${amis.length} AMIs [y/N] ? : `);
86
+ del = answer === 'y';
87
+ }
88
+
89
+ if (del === true) {
90
+ const limit = pLimit(5);
91
+ await Promise.all(amis.map(ami => limit(() => deleteAMI(ec2Client(ami.region), ami))));
92
+ } else {
93
+ return;
94
+ }
95
+ }
96
+
97
+ function readArgs() {
98
+ const parser = new ArgumentParser({
99
+ prog: 'aws-amicleaner',
100
+ description: 'To clean up your AWS AMIs: First, include AMIs by name or tag. Second, exclude AMIs in use, younger than N days, or the newest N images. Third, manually confirm the list of AMIs for deletion.',
101
+ usage: `To delete all AMIs where the name starts with amiprefix-, are older than 5 days, and not the newest 3 images, run:
102
+ aws-amicleaner --include-name 'amiprefix-*' --exclude-newest 3 --exclude-days 5 --exclude-in-use --verbose
103
+
104
+ To delete all AMIs tagged with CostCenter=X342-*-1111, are older than 7 days (default), are not the newest 5 images (default), and are not in use (default), run:
105
+ aws-amicleaner --include-tag-key CostCenter --include-tag-value 'X342-*-1111'
106
+
107
+ Run the command without confirmation (useful in scripts):
108
+ aws-amicleaner --include-tag-key CostCenter --include-tag-value 'X342-*-1111' --force-delete
109
+
110
+ To disable the defaults, run:
111
+ aws-amicleaner --include-name 'amiprefix-*' --exclude-newest 0 --exclude-days 0 --no-exclude-in-use --no-verbose
112
+ `
113
+ });
114
+ parser.add_argument('--region', {dest: 'regions', type: 'string', action: 'append', default: [], help: 'The AWS region, e.g. us-east-1, arg can be used more than once, wildcard * supported'});
115
+ parser.add_argument('--include-name', {dest: 'includeName', type: 'string', help: 'The name that must be present, wildcard * supported'});
116
+ parser.add_argument('--include-tag-key', {dest: 'includeTagKey', type: 'string', help: 'The tag key that must be present'});
117
+ parser.add_argument('--include-tag-value', {dest: 'includeTagValue', type: 'string', help: 'The tag value (for the tag key) that must be present, wildcard * supported'});
118
+ parser.add_argument('--exclude-newest', {dest: 'excludeNewest', type: 'int', default: 5, help: 'Exclude the newest N AMIs'});
119
+ parser.add_argument('--exclude-days', {dest: 'excludeDays', type: 'int', default: 7, help: 'Exclude AMIs from deletion that are younger than N days'});
120
+ parser.add_argument('--exclude-in-use', {dest: 'excludeInUse', default: true, action: BooleanOptionalAction, help: 'Exclude AMIs from deletion that are in use by EC2 instances, ASGs, Launch Configurations, and Launch Templates'});
121
+ parser.add_argument('-f', '--force-delete', {dest: 'forceDelete', default: false, action: BooleanOptionalAction, help: 'Skip confirmation before deletion'});
122
+ parser.add_argument('--verbose', {dest: 'verbose', default: true, action: BooleanOptionalAction, help: 'Display additional information'});
123
+ const args = parser.parse_args();
124
+
125
+ if (args.includeName === undefined && args.includeTagKey === undefined) {
126
+ throw new Error('--include-name or --include-tag-key missing');
127
+ }
128
+ if (args.includeName !== undefined && args.includeTagKey !== undefined) {
129
+ throw new Error('Use either --include-name or --include-tag-key');
130
+ }
131
+ if (args.includeTagKey !== undefined && args.includeTagValue === undefined) {
132
+ throw new Error('--include-tag-value missing');
133
+ }
134
+
135
+ return args;
136
+ }
137
+
138
+ const args = readArgs();
139
+
140
+ if (args.verbose === true) {
141
+ console.log('args', args);
142
+ }
143
+
144
+ run(args).then(() => process.exit(0));
package/lib.js ADDED
@@ -0,0 +1,203 @@
1
+ const wildcard = require('wildcard');
2
+ const pLimit = require('p-limit');
3
+
4
+ const MAX_ITEMS_PER_LAUNCH_CONFIGURATION_PAGE = 50;
5
+
6
+ function mapAMI(raw) {
7
+ return {
8
+ id: raw.ImageId,
9
+ name: raw.Name,
10
+ creationDate: Date.parse(raw.CreationDate),
11
+ tags: raw.Tags.reduce((acc, {Key: key, Value: value}) => {
12
+ acc[key] = value;
13
+ return acc;
14
+ }, {}),
15
+ blockDeviceMappings: raw.BlockDeviceMappings.map(raw => ({snapshotId: raw.Ebs.SnapshotId})),
16
+ excluded: false,
17
+ excludeReasons: [],
18
+ included: false,
19
+ includeReasons: []
20
+ };
21
+ }
22
+
23
+ async function fetchRegions(ec2, rawRegions) {
24
+ const regions = new Set();
25
+
26
+ if (rawRegions.length === 0) {
27
+ regions.add(undefined);
28
+ }
29
+
30
+ rawRegions.filter(region => !region.includes('*')).forEach(region => regions.add(region));
31
+
32
+ const rawRegionsWithWildcard = rawRegions.filter(region => region.includes('*'));
33
+ if (rawRegionsWithWildcard.length !== 0) {
34
+ const {Regions} = await ec2.describeRegions({}).promise();
35
+ rawRegionsWithWildcard.forEach(rawRegionWithWildcard => {
36
+ wildcard(rawRegionWithWildcard, Regions.map(r => r.RegionName)).forEach(region => regions.add(region));
37
+ });
38
+ }
39
+
40
+ return regions;
41
+ }
42
+ exports.fetchRegions = fetchRegions;
43
+
44
+ async function fetchInUseAMIIDs(ec2, autoscaling) {
45
+ const inUseAMIIDs = new Set();
46
+
47
+ for await (const reservation of (async function*() {
48
+ let nextToken = '';
49
+ while (nextToken !== undefined) {
50
+ const {Reservations: reservations, NextToken} = await ec2.describeInstances({
51
+ NextToken: (nextToken === '') ? undefined : nextToken,
52
+ Filters: [{
53
+ Name: 'instance-state-name',
54
+ Values: [
55
+ 'pending',
56
+ 'running',
57
+ 'shutting-down',
58
+ 'stopping',
59
+ 'stopped'
60
+ ]
61
+ }]
62
+ }).promise();
63
+ yield* reservations;
64
+ nextToken = NextToken;
65
+ }
66
+ })()) {
67
+ reservation.Instances.forEach(instance => inUseAMIIDs.add(instance.ImageId));
68
+ }
69
+
70
+ const asgs = [];
71
+ for await (const asg of (async function*() {
72
+ let nextToken = '';
73
+ while (nextToken !== undefined) {
74
+ const {AutoScalingGroups, NextToken} = await autoscaling.describeAutoScalingGroups({
75
+ NextToken: (nextToken === '') ? undefined : nextToken
76
+ }).promise();
77
+ yield* AutoScalingGroups;
78
+ nextToken = NextToken;
79
+ }
80
+ })()) {
81
+ asgs.push(asg);
82
+ }
83
+
84
+ // in use by ASG -> Launch Configuration
85
+ const inUseLCNames = asgs.filter(asg => 'LaunchConfigurationName' in asg).map(asg => asg.LaunchConfigurationName);
86
+ if (inUseLCNames.length > 0) {
87
+ for (let i = 0; i < Math.ceil(inUseLCNames.length/MAX_ITEMS_PER_LAUNCH_CONFIGURATION_PAGE); i++) {
88
+ const {LaunchConfigurations: lcs} = await autoscaling.describeLaunchConfigurations({
89
+ LaunchConfigurationNames: inUseLCNames.slice(i*MAX_ITEMS_PER_LAUNCH_CONFIGURATION_PAGE, (i+1)*MAX_ITEMS_PER_LAUNCH_CONFIGURATION_PAGE)
90
+ }).promise();
91
+ lcs.forEach(lc => inUseAMIIDs.add(lc.ImageId));
92
+ }
93
+ }
94
+
95
+ const inUseLTs = [
96
+ ...asgs.filter(asg => 'LaunchTemplate' in asg).map(asg => ({id: asg.LaunchTemplate.LaunchTemplateId, version: asg.LaunchTemplate.Version})),
97
+ ...asgs.filter(asg => 'MixedInstancesPolicy' in asg).map(asg => ({id: asg.MixedInstancesPolicy.LaunchTemplate.LaunchTemplateSpecification.LaunchTemplateId, version: asg.MixedInstancesPolicy.LaunchTemplate.LaunchTemplateSpecification.Version}))
98
+ ];
99
+ const limit = pLimit(5);
100
+ await Promise.all(
101
+ inUseLTs.map(({id, version}) => limit(() =>
102
+ ec2.describeLaunchTemplateVersions({
103
+ LaunchTemplateId: id,
104
+ Versions: [version]
105
+ }).promise().then(data => data.LaunchTemplateVersions[0].LaunchTemplateData.ImageId))
106
+ )
107
+ ).then(amiIDs => amiIDs.forEach(amiID => inUseAMIIDs.add(amiID)));
108
+
109
+ return inUseAMIIDs;
110
+ }
111
+ exports.fetchInUseAMIIDs = fetchInUseAMIIDs;
112
+
113
+ async function fetchAMIs(now, ec2, autoscaling, includeName, includeTagKey, includeTagValue, excludeNewest, excludeInUse, excludeDays) {
114
+ let amis = [];
115
+ for await (const rawAMI of (async function*() {
116
+ let nextToken = '';
117
+ while (nextToken !== undefined) {
118
+ const params = {
119
+ Owners: ['self']
120
+ };
121
+ if (includeTagKey !== undefined) {
122
+ params.Filters = [{
123
+ Name: 'tag-key',
124
+ Values: [includeTagKey]
125
+ }];
126
+ }
127
+ if (nextToken !== '') {
128
+ params.NextToken = nextToken;
129
+ }
130
+ const {Images, NextToken} = await ec2.describeImages(params).promise();
131
+ yield* Images;
132
+ nextToken = NextToken;
133
+ }
134
+ })()) {
135
+ amis.push(mapAMI(rawAMI));
136
+ }
137
+
138
+ if (includeName !== undefined) {
139
+ amis = amis.filter(ami => wildcard(includeName, ami.name)).map(ami => {
140
+ ami.included = true;
141
+ ami.includeReasons.push('name match');
142
+ return ami;
143
+ });
144
+ } else if (includeTagKey !== undefined) {
145
+ amis = amis.filter(ami => wildcard(includeTagValue, ami.tags[includeTagKey])).map(ami => {
146
+ ami.included = true;
147
+ ami.includeReasons.push('tag match');
148
+ return ami;
149
+ });
150
+ } else {
151
+ throw new Error('no include defined');
152
+ }
153
+
154
+ if (excludeInUse === true) {
155
+ const inUseAMIIDs = await fetchInUseAMIIDs(ec2, autoscaling);
156
+ amis = amis.map(ami => {
157
+ if (inUseAMIIDs.has(ami.id)) {
158
+ ami.excluded = true;
159
+ ami.excludeReasons.push('in use');
160
+ }
161
+ return ami;
162
+ });
163
+ }
164
+
165
+ if (excludeDays > 0) {
166
+ const ts = now-(excludeDays*24*60*60*1000);
167
+ amis = amis.map(ami => {
168
+ if (ami.creationDate > ts) {
169
+ ami.excluded = true;
170
+ ami.excludeReasons.push('days not passed');
171
+ }
172
+ return ami;
173
+ });
174
+
175
+ }
176
+
177
+ if (excludeNewest > 0) {
178
+ amis = amis.sort((a, b) => b.creationDate-a.creationDate).map((ami, i) => {
179
+ if (i < excludeNewest) {
180
+ ami.excluded = true;
181
+ ami.excludeReasons.push('newest');
182
+ }
183
+ return ami;
184
+ });
185
+ }
186
+
187
+ return amis;
188
+ }
189
+ exports.fetchAMIs = fetchAMIs;
190
+
191
+ async function deleteAMI(ec2, ami) {
192
+ await ec2.deregisterImage({
193
+ ImageId: ami.id
194
+ }).promise();
195
+ console.log(`AMI ${ami.id} deregistered`);
196
+ for (const blockDevice of ami.blockDeviceMappings) {
197
+ await ec2.deleteSnapshot({
198
+ SnapshotId: blockDevice.snapshotId
199
+ }).promise();
200
+ console.log(`snapshot ${blockDevice.snapshotId} of AMI ${ami.id} deleted`);
201
+ }
202
+ }
203
+ exports.deleteAMI = deleteAMI;
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "aws-amicleaner",
3
+ "version": "1.0.0",
4
+ "description": "To clean up your AWS AMIs: First, include AMIs by name or tag. Second, exclude AMIs in use, younger than N days, or the newest N images. Third, manually confirm the list of AMIs for deletion.",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "eslint . && c8 mocha"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/widdix/aws-amicleaner.git"
12
+ },
13
+ "author": "Michael Wittig <michael@widdix.de>",
14
+ "license": "MIT",
15
+ "bugs": {
16
+ "url": "https://github.com/widdix/aws-amicleaner/issues"
17
+ },
18
+ "homepage": "https://github.com/widdix/aws-amicleaner#readme",
19
+ "dependencies": {
20
+ "aws-sdk": "2.1387.0",
21
+ "p-limit": "3.1.0",
22
+ "wildcard": "2.0.1"
23
+ },
24
+ "devDependencies": {
25
+ "c8": "7.13.0",
26
+ "eslint": "8.41.0",
27
+ "mocha": "10.2.0"
28
+ },
29
+ "mocha": {
30
+ "timeout": 10000
31
+ },
32
+ "c8": {
33
+ "reporter": [
34
+ "text",
35
+ "html"
36
+ ]
37
+ }
38
+ }
package/prettytable.js ADDED
@@ -0,0 +1,241 @@
1
+ // Not used via module prettytabl;e becausde of error:
2
+ // Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './lib/sync' is not defined by "exports" in /Users/michael/Projects/widdix/aws-amicleaner/node_modules/csv-parse/package.json
3
+ // at new NodeError (node:internal/errors:387:5)
4
+ // at throwExportsNotFound (node:internal/modules/esm/resolve:365:9)
5
+ // at packageExportsResolve (node:internal/modules/esm/resolve:649:3)
6
+ // at resolveExports (node:internal/modules/cjs/loader:554:36)
7
+ // at Function.Module._findPath (node:internal/modules/cjs/loader:594:31)
8
+ // at Function.Module._resolveFilename (node:internal/modules/cjs/loader:1012:27)
9
+ // at Function.Module._load (node:internal/modules/cjs/loader:871:27)
10
+ // at Module.require (node:internal/modules/cjs/loader:1098:19)
11
+ // at require (node:internal/modules/cjs/helpers:108:18)
12
+ // at Object.<anonymous> (/Users/michael/Projects/widdix/aws-amicleaner/node_modules/prettytable/prettytable.js:1:13) {
13
+ // code: 'ERR_PACKAGE_PATH_NOT_EXPORTED'
14
+ // }
15
+
16
+ // Source https://github.com/jyotiska/prettytable
17
+
18
+ // The MIT License (MIT)
19
+ //
20
+ // Copyright (c) 2016 Jyotiska NK
21
+ //
22
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
23
+ // of this software and associated documentation files (the "Software"), to deal
24
+ // in the Software without restriction, including without limitation the rights
25
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
26
+ // copies of the Software, and to permit persons to whom the Software is
27
+ // furnished to do so, subject to the following conditions:
28
+ //
29
+ // The above copyright notice and this permission notice shall be included in all
30
+ // copies or substantial portions of the Software.
31
+ //
32
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
33
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
34
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
35
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
36
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
37
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
38
+ // SOFTWARE.
39
+
40
+ var PrettyTable = function () {
41
+ // Skeleton structure of table with list of column names, row and max width of each column element
42
+ this.table = {
43
+ 'columnNames': [],
44
+ 'rows': [],
45
+ 'maxWidth': []
46
+ };
47
+ this.version = '0.3.1';
48
+ };
49
+
50
+ // Define list of columns for the table
51
+ PrettyTable.prototype.fieldNames = function (names) {
52
+ this.table.columnNames = names;
53
+ for (var i = 0; i < names.length; i++) {
54
+ this.table.maxWidth.push(names[i].length);
55
+ }
56
+ };
57
+
58
+ // Add a single row to the table
59
+ PrettyTable.prototype.addRow = function (row) {
60
+ this.table.rows.push(row);
61
+ for (var i = 0; i < row.length; i++) {
62
+ if (row[i].toString().length > this.table.maxWidth[i]) {
63
+ this.table.maxWidth[i] = row[i].toString().length;
64
+ }
65
+ }
66
+ };
67
+
68
+ // Single function to create table when headers and array of rows passed
69
+ PrettyTable.prototype.create = function (headers, rows) {
70
+ // Add table headers
71
+ this.fieldNames(headers);
72
+
73
+ // Add rows one by one
74
+ for (var i = 0; i < rows.length; i++) {
75
+ this.addRow(rows[i]);
76
+ }
77
+ };
78
+
79
+ // Convert the table to string
80
+ PrettyTable.prototype.toString = function () {
81
+ var finalTable = '';
82
+ var columnString = '| ';
83
+ var rowString = '';
84
+ var lengthDifference = '';
85
+
86
+ // Draw a line based on the max width of each column and return
87
+ var drawLine = function (table) {
88
+ var finalLine = '+';
89
+ for (var i = 0; i < table.maxWidth.length; i++) {
90
+ finalLine += Array(table.maxWidth[i] + 3).join('-') + '+';
91
+ }
92
+ return finalLine;
93
+ };
94
+
95
+ // If no columns present, return empty string
96
+ if (this.table.columnNames.length === 0) {
97
+ return finalTable;
98
+ }
99
+
100
+ // Create the table header from column list
101
+ for (var i = 0; i < this.table.columnNames.length; i++) {
102
+ columnString += this.table.columnNames[i];
103
+ // Adjust for max width of the column and pad spaces
104
+ if (this.table.columnNames[i].length < this.table.maxWidth[i]) {
105
+ lengthDifference = this.table.maxWidth[i] - this.table.columnNames[i].length;
106
+ columnString += Array(lengthDifference + 1).join(' ');
107
+ }
108
+ columnString += ' | ';
109
+ }
110
+ finalTable += drawLine(this.table) + '\n';
111
+ finalTable += columnString + '\n';
112
+ finalTable += drawLine(this.table) + '\n';
113
+
114
+ // Construct the table body
115
+ for (i = 0; i < this.table.rows.length; i++) {
116
+ var tempRowString = '| ';
117
+ for (var k = 0; k < this.table.rows[i].length; k++) {
118
+ tempRowString += this.table.rows[i][k];
119
+ // Adjust max width of each cell and pad spaces as necessary
120
+ if (this.table.rows[i][k].toString().length < this.table.maxWidth[k]) {
121
+ lengthDifference = this.table.maxWidth[k] - this.table.rows[i][k].toString().length;
122
+ tempRowString += Array(lengthDifference + 1).join(' ');
123
+ }
124
+ tempRowString += ' | ';
125
+ }
126
+ rowString += tempRowString + '\n';
127
+ }
128
+ // Remove newline from the end of the table string
129
+ rowString = rowString.slice(0, -1);
130
+ // Append to the final table string
131
+ finalTable += rowString + '\n';
132
+ // Draw last line and return
133
+ finalTable += drawLine(this.table) + '\n';
134
+ return finalTable;
135
+ };
136
+
137
+ // Write the table string to the console
138
+ PrettyTable.prototype.print = function () {
139
+ console.log(this.toString());
140
+ };
141
+
142
+ // Write the table string to the console as HTML table formats
143
+ PrettyTable.prototype.html = function (attributes) {
144
+ // If attributes provided, add them as inline properties, else create default table tag
145
+ var htmlTable = '';
146
+ if (typeof attributes == 'undefined') {
147
+ htmlTable = '<table>';
148
+ }
149
+ else {
150
+ var attributeList = [];
151
+ for (var key in attributes) {
152
+ attributeList.push(key + '=\'' + attributes[key] + '\'');
153
+ }
154
+ var attributeString = attributeList.join(' ');
155
+ htmlTable = '<table ' + attributeString + '>';
156
+ }
157
+
158
+ // Define the table headers in <thead> from table column list
159
+ var tableHead = '<thead><tr>';
160
+ for (var i = 0; i < this.table.columnNames.length; i++) {
161
+ var headerString = '<th>' + this.table.columnNames[i] + '</th>';
162
+ tableHead += headerString;
163
+ }
164
+ tableHead += '</tr></thead>';
165
+ htmlTable += tableHead;
166
+
167
+ // Construct the table body from the array of rows
168
+ var tableBody = '<tbody>';
169
+ for (i = 0; i < this.table.rows.length; i++) {
170
+ var rowData = '<tr>';
171
+ for (var k = 0; k < this.table.rows[i].length; k++) {
172
+ var cellData = '<td>' + this.table.rows[i][k] + '</td>';
173
+ rowData += cellData;
174
+ }
175
+ rowData += '</tr>';
176
+ tableBody += rowData;
177
+ }
178
+ // Close all tags and return
179
+ tableBody += '</tbody>';
180
+ htmlTable += tableBody;
181
+ htmlTable += '</table>';
182
+
183
+ return htmlTable;
184
+ };
185
+
186
+ // Sort the table given a column in ascending or descending order
187
+ PrettyTable.prototype.sortTable = function (colname, reverse) {
188
+ // Find the index of the column given the name
189
+ var colindex = this.table.columnNames.indexOf(colname);
190
+
191
+ // Comparator method which takes the column index and sort direction
192
+ var Comparator = function ( a, b) {
193
+ if (typeof reverse === 'boolean' && reverse === true) {
194
+ if (a[colindex] < b[colindex]) {
195
+ return 1;
196
+ }
197
+ else if (a[colindex] > b[colindex]) {
198
+ return -1;
199
+ }
200
+ else {
201
+ return 0;
202
+ }
203
+ }
204
+ else {
205
+ if (a[colindex] < b[colindex]) {
206
+ return -1;
207
+ }
208
+ else if (a[colindex] > b[colindex]) {
209
+ return 1;
210
+ }
211
+ else {
212
+ return 0;
213
+ }
214
+ }
215
+ };
216
+ // Sort array of table rows
217
+ this.table.rows = this.table.rows.sort(Comparator);
218
+ };
219
+
220
+ // Delete a single row from the table given row number
221
+ PrettyTable.prototype.deleteRow = function (rownum) {
222
+ if (rownum <= this.table.rows.length && rownum > 0) {
223
+ this.table.rows.splice(rownum - 1, 1);
224
+ }
225
+ };
226
+
227
+ // Clear the contents from the table, but keep columns and structure
228
+ PrettyTable.prototype.clearTable = function () {
229
+ this.table.rows = [];
230
+ };
231
+
232
+ // Delete the entire table
233
+ PrettyTable.prototype.deleteTable = function () {
234
+ this.table = {
235
+ 'columnNames': [],
236
+ 'rows': [],
237
+ 'maxWidth': []
238
+ };
239
+ };
240
+
241
+ module.exports = PrettyTable;