aws-amicleaner 1.0.6 → 1.1.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/README.md CHANGED
@@ -68,7 +68,7 @@ npx aws-amicleaner --include-name 'amiprefix-*' --exclude-newest 0 --exclude-day
68
68
  --exclude-days EXCLUDEDAYS
69
69
  Exclude AMIs from deletion that are younger than N days
70
70
  --exclude-in-use, --no-exclude-in-use
71
- Exclude AMIs from deletion that are in use by EC2 instances, ASGs, Launch Configurations, and Launch Templates (default: true)
71
+ Exclude AMIs from deletion that are in use by EC2 instances, Launch Configurations, and Launch Templates (default: true)
72
72
  -f, --force-delete, --no-force-delete
73
73
  Skip confirmation before deletion (default: false)
74
74
  --verbose, --no-verbose
@@ -0,0 +1,25 @@
1
+ import {defineConfig} from 'eslint/config';
2
+ import globals from 'globals';
3
+ import js from '@eslint/js';
4
+
5
+ export default defineConfig([{
6
+ files: ['**/*.js'],
7
+ plugins: {
8
+ js,
9
+ },
10
+ extends: ['js/recommended'],
11
+ languageOptions: {
12
+ globals: {
13
+ ...globals.commonjs,
14
+ ...globals.node,
15
+ ...globals.mocha
16
+ },
17
+ ecmaVersion: 2022,
18
+ },
19
+ rules: {
20
+ indent: ['error', 2],
21
+ 'linebreak-style': ['error', 'unix'],
22
+ quotes: ['error', 'single'],
23
+ semi: ['error', 'always'],
24
+ },
25
+ }]);
package/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { createInterface } = require('node:readline');
4
- const { ArgumentParser, BooleanOptionalAction } = require('argparse');
5
- const { fetchAMIs, deleteAMI, fetchRegions} = require('./lib.js');
6
- const pLimit = require('p-limit');
7
-
8
- const AWS = require('aws-sdk');
9
- const PrettyTable = require('./prettytable.js');
3
+ import {createInterface} from 'node:readline';
4
+ import {ArgumentParser, BooleanOptionalAction} from 'argparse';
5
+ import {fetchAMIs, deleteAMI, fetchRegions} from './lib.js';
6
+ import pLimit from 'p-limit';
7
+ import {AutoScalingClient} from '@aws-sdk/client-auto-scaling';
8
+ import {EC2Client} from '@aws-sdk/client-ec2';
9
+ import PrettyTable from './prettytable.js';
10
10
 
11
11
  const rl = createInterface({
12
12
  input: process.stdin,
@@ -36,13 +36,13 @@ async function run({
36
36
  const autoscaling = {};
37
37
  const ec2Client = (region) => {
38
38
  if (!(region in ec2)) {
39
- ec2[region] = new AWS.EC2({apiVersion: '2016-11-15', region});
39
+ ec2[region] = new EC2Client({apiVersion: '2016-11-15', region});
40
40
  }
41
41
  return ec2[region];
42
42
  };
43
43
  const autoscalingClient = (region) => {
44
44
  if (!(region in autoscaling)) {
45
- autoscaling[region] = new AWS.AutoScaling({apiVersion: '2011-01-01', region});
45
+ autoscaling[region] = new AutoScalingClient({apiVersion: '2011-01-01', region});
46
46
  }
47
47
  return autoscaling[region];
48
48
  };
@@ -119,7 +119,7 @@ aws-amicleaner --include-name 'amiprefix-*' --exclude-newest 0 --exclude-days 0
119
119
  parser.add_argument('--include-tag-value', {dest: 'includeTagValue', type: 'string', help: 'The tag value (for the tag key) that must be present, wildcard * supported'});
120
120
  parser.add_argument('--exclude-newest', {dest: 'excludeNewest', type: 'int', default: 5, help: 'Exclude the newest N AMIs'});
121
121
  parser.add_argument('--exclude-days', {dest: 'excludeDays', type: 'int', default: 7, help: 'Exclude AMIs from deletion that are younger than N days'});
122
- 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'});
122
+ parser.add_argument('--exclude-in-use', {dest: 'excludeInUse', default: true, action: BooleanOptionalAction, help: 'Exclude AMIs from deletion that are in use by EC2 instances, Launch Configurations, and Launch Templates'});
123
123
  parser.add_argument('-f', '--force-delete', {dest: 'forceDelete', default: false, action: BooleanOptionalAction, help: 'Skip confirmation before deletion'});
124
124
  parser.add_argument('--verbose', {dest: 'verbose', default: true, action: BooleanOptionalAction, help: 'Display additional information'});
125
125
  const args = parser.parse_args();
package/lib.js CHANGED
@@ -1,17 +1,17 @@
1
- const wildcard = require('wildcard');
2
- const pLimit = require('p-limit');
3
-
4
- const MAX_ITEMS_PER_LAUNCH_CONFIGURATION_PAGE = 50;
1
+ import wildcard from 'wildcard';
2
+ import pLimit from 'p-limit';
3
+ import {paginateDescribeLaunchConfigurations} from '@aws-sdk/client-auto-scaling';
4
+ import {DescribeRegionsCommand, paginateDescribeInstances, DescribeLaunchTemplateVersionsCommand, paginateDescribeImages, DeregisterImageCommand, DeleteSnapshotCommand, paginateDescribeLaunchTemplates} from '@aws-sdk/client-ec2';
5
5
 
6
6
  function mapAMI(raw) {
7
7
  return {
8
8
  id: raw.ImageId,
9
9
  name: raw.Name,
10
10
  creationDate: Date.parse(raw.CreationDate),
11
- tags: raw.Tags.reduce((acc, {Key: key, Value: value}) => {
11
+ tags: Array.isArray(raw.Tags) ? raw.Tags.reduce((acc, {Key: key, Value: value}) => {
12
12
  acc[key] = value;
13
13
  return acc;
14
- }, {}),
14
+ }, {}) : {},
15
15
  blockDeviceMappings: raw.BlockDeviceMappings.filter(raw => raw.Ebs).map(raw => ({snapshotId: raw.Ebs.SnapshotId})),
16
16
  excluded: false,
17
17
  excludeReasons: [],
@@ -20,7 +20,7 @@ function mapAMI(raw) {
20
20
  };
21
21
  }
22
22
 
23
- async function fetchRegions(ec2, rawRegions) {
23
+ export async function fetchRegions(ec2, rawRegions) {
24
24
  const regions = new Set();
25
25
 
26
26
  if (rawRegions.length === 0) {
@@ -31,7 +31,7 @@ async function fetchRegions(ec2, rawRegions) {
31
31
 
32
32
  const rawRegionsWithWildcard = rawRegions.filter(region => region.includes('*'));
33
33
  if (rawRegionsWithWildcard.length !== 0) {
34
- const {Regions} = await ec2.describeRegions({}).promise();
34
+ const {Regions} = await ec2.send(new DescribeRegionsCommand({}));
35
35
  rawRegionsWithWildcard.forEach(rawRegionWithWildcard => {
36
36
  wildcard(rawRegionWithWildcard, Regions.map(r => r.RegionName)).forEach(region => regions.add(region));
37
37
  });
@@ -39,100 +39,78 @@ async function fetchRegions(ec2, rawRegions) {
39
39
 
40
40
  return regions;
41
41
  }
42
- exports.fetchRegions = fetchRegions;
43
42
 
44
- async function fetchInUseAMIIDs(ec2, autoscaling) {
43
+ export async function fetchInUseAMIIDs(ec2, autoscaling) {
45
44
  const inUseAMIIDs = new Set();
46
45
 
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;
46
+ const instancePaginator = paginateDescribeInstances({
47
+ client: ec2
48
+ }, {
49
+ Filters: [{
50
+ Name: 'instance-state-name',
51
+ Values: [
52
+ 'pending',
53
+ 'running',
54
+ 'shutting-down',
55
+ 'stopping',
56
+ 'stopped'
57
+ ]
58
+ }]
59
+ });
60
+ for await (const page of instancePaginator) {
61
+ for (const reservation of page.Reservations) {
62
+ reservation.Instances.forEach(instance => inUseAMIIDs.add(instance.ImageId));
65
63
  }
66
- })()) {
67
- reservation.Instances.forEach(instance => inUseAMIIDs.add(instance.ImageId));
68
64
  }
69
65
 
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);
66
+ const lcPaginator = paginateDescribeLaunchConfigurations({client: autoscaling}, {});
67
+ for await (const page of lcPaginator) {
68
+ page.LaunchConfigurations.forEach(lc => inUseAMIIDs.add(lc.ImageId));
82
69
  }
83
70
 
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
- }
71
+ const ltPaginator = paginateDescribeLaunchTemplates({client: ec2}, {});
72
+ const ltLimit = pLimit(5);
73
+ for await (const page of ltPaginator) {
74
+ await Promise.all(
75
+ page.LaunchTemplates.map(({LaunchTemplateId: id, DefaultVersionNumber: version}) => ltLimit(async () => {
76
+ const data = await ec2.send(new DescribeLaunchTemplateVersionsCommand({
77
+ LaunchTemplateId: id,
78
+ Versions: [version]
79
+ }));
80
+ inUseAMIIDs.add(data.LaunchTemplateVersions[0].LaunchTemplateData.ImageId);
81
+ }))
82
+ );
83
+ // CloudFormation does not update the default version of a launch template, therefore we are also considering the latest version as being used
84
+ await Promise.all(
85
+ page.LaunchTemplates.map(({LaunchTemplateId: id, LatestVersionNumber: version}) => ltLimit(async () => {
86
+ const data = await ec2.send(new DescribeLaunchTemplateVersionsCommand({
87
+ LaunchTemplateId: id,
88
+ Versions: [version]
89
+ }));
90
+ inUseAMIIDs.add(data.LaunchTemplateVersions[0].LaunchTemplateData.ImageId);
91
+ }))
92
+ );
93
93
  }
94
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
95
  return inUseAMIIDs;
110
96
  }
111
- exports.fetchInUseAMIIDs = fetchInUseAMIIDs;
112
97
 
113
- async function fetchAMIs(now, ec2, autoscaling, includeName, includeTagKey, includeTagValue, excludeNewest, excludeInUse, excludeDays) {
98
+ export async function fetchAMIs(now, ec2, autoscaling, includeName, includeTagKey, includeTagValue, excludeNewest, excludeInUse, excludeDays) {
114
99
  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));
100
+ const input = {
101
+ Owners: ['self']
102
+ };
103
+ if (includeTagKey !== undefined) {
104
+ input.Filters = [{
105
+ Name: 'tag-key',
106
+ Values: [includeTagKey]
107
+ }];
108
+ }
109
+ const paginator = paginateDescribeImages({
110
+ client: ec2
111
+ }, input);
112
+ for await (const page of paginator) {
113
+ page.Images.forEach(rawAMI => amis.push(mapAMI(rawAMI)));
136
114
  }
137
115
 
138
116
  if (includeName !== undefined) {
@@ -186,18 +164,16 @@ async function fetchAMIs(now, ec2, autoscaling, includeName, includeTagKey, incl
186
164
 
187
165
  return amis;
188
166
  }
189
- exports.fetchAMIs = fetchAMIs;
190
167
 
191
- async function deleteAMI(ec2, ami) {
192
- await ec2.deregisterImage({
168
+ export async function deleteAMI(ec2, ami) {
169
+ await ec2.send(new DeregisterImageCommand({
193
170
  ImageId: ami.id
194
- }).promise();
171
+ }));
195
172
  console.log(`AMI ${ami.id} deregistered`);
196
173
  for (const blockDevice of ami.blockDeviceMappings) {
197
- await ec2.deleteSnapshot({
174
+ await ec2.send(new DeleteSnapshotCommand({
198
175
  SnapshotId: blockDevice.snapshotId
199
- }).promise();
176
+ }));
200
177
  console.log(`snapshot ${blockDevice.snapshotId} of AMI ${ami.id} deleted`);
201
178
  }
202
179
  }
203
- exports.deleteAMI = deleteAMI;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aws-amicleaner",
3
- "version": "1.0.6",
3
+ "version": "1.1.0",
4
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
5
  "main": "index.js",
6
6
  "bin": "index.js",
@@ -18,15 +18,19 @@
18
18
  },
19
19
  "homepage": "https://github.com/widdix/aws-amicleaner#readme",
20
20
  "dependencies": {
21
+ "@aws-sdk/client-auto-scaling": "3.777.0",
22
+ "@aws-sdk/client-ec2": "3.777.0",
21
23
  "argparse": "2.0.1",
22
- "aws-sdk": "2.1552.0",
23
- "p-limit": "3.1.0",
24
+ "p-limit": "6.2.0",
24
25
  "wildcard": "2.0.1"
25
26
  },
26
27
  "devDependencies": {
27
- "c8": "9.1.0",
28
- "eslint": "8.56.0",
29
- "mocha": "10.2.0"
28
+ "@eslint/js": "9.23.0",
29
+ "aws-sdk-client-mock": "4.1.0",
30
+ "c8": "10.1.3",
31
+ "eslint": "9.23.0",
32
+ "globals": "16.0.0",
33
+ "mocha": "11.1.0"
30
34
  },
31
35
  "mocha": {
32
36
  "timeout": 10000
@@ -36,5 +40,6 @@
36
40
  "text",
37
41
  "html"
38
42
  ]
39
- }
43
+ },
44
+ "type": "module"
40
45
  }
package/prettytable.js CHANGED
@@ -37,7 +37,7 @@
37
37
  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
38
38
  // SOFTWARE.
39
39
 
40
- var PrettyTable = function () {
40
+ export default function PrettyTable() {
41
41
  // Skeleton structure of table with list of column names, row and max width of each column element
42
42
  this.table = {
43
43
  'columnNames': [],
@@ -237,5 +237,3 @@ PrettyTable.prototype.deleteTable = function () {
237
237
  'maxWidth': []
238
238
  };
239
239
  };
240
-
241
- module.exports = PrettyTable;
package/renovate.json ADDED
@@ -0,0 +1,3 @@
1
+ {
2
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json"
3
+ }