@bitblit/ratchet-aws 6.0.146-alpha → 6.0.148-alpha

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.
Files changed (103) hide show
  1. package/package.json +5 -4
  2. package/src/batch/aws-batch-background-processor.spec.ts +22 -0
  3. package/src/batch/aws-batch-background-processor.ts +71 -0
  4. package/src/batch/aws-batch-ratchet.spec.ts +42 -0
  5. package/src/batch/aws-batch-ratchet.ts +70 -0
  6. package/src/build/ratchet-aws-info.ts +19 -0
  7. package/src/cache/memory-storage-provider.ts +39 -0
  8. package/src/cache/simple-cache-object-wrapper.ts +11 -0
  9. package/src/cache/simple-cache-read-options.ts +9 -0
  10. package/src/cache/simple-cache-storage-provider.ts +15 -0
  11. package/src/cache/simple-cache.spec.ts +42 -0
  12. package/src/cache/simple-cache.ts +81 -0
  13. package/src/cloudwatch/cloud-watch-log-group-ratchet.spec.ts +26 -0
  14. package/src/cloudwatch/cloud-watch-log-group-ratchet.ts +105 -0
  15. package/src/cloudwatch/cloud-watch-logs-ratchet.spec.ts +123 -0
  16. package/src/cloudwatch/cloud-watch-logs-ratchet.ts +232 -0
  17. package/src/cloudwatch/cloud-watch-metrics-ratchet.spec.ts +30 -0
  18. package/src/cloudwatch/cloud-watch-metrics-ratchet.ts +98 -0
  19. package/src/dao/example-prototype-dao-item.ts +8 -0
  20. package/src/dao/memory-prototype-dao-provider.ts +16 -0
  21. package/src/dao/prototype-dao-config.ts +8 -0
  22. package/src/dao/prototype-dao-db.ts +4 -0
  23. package/src/dao/prototype-dao-provider.ts +6 -0
  24. package/src/dao/prototype-dao.spec.ts +33 -0
  25. package/src/dao/prototype-dao.ts +110 -0
  26. package/src/dao/s3-simple-dao.ts +96 -0
  27. package/src/dao/simple-dao-item.ts +13 -0
  28. package/src/dynamodb/dynamo-ratchet-like.ts +61 -0
  29. package/src/dynamodb/dynamo-ratchet.spec.ts +206 -0
  30. package/src/dynamodb/dynamo-ratchet.ts +850 -0
  31. package/src/dynamodb/dynamo-table-ratchet.spec.ts +23 -0
  32. package/src/dynamodb/dynamo-table-ratchet.ts +189 -0
  33. package/src/dynamodb/hash-spreader.spec.ts +22 -0
  34. package/src/dynamodb/hash-spreader.ts +89 -0
  35. package/src/dynamodb/impl/dynamo-db-storage-provider.spec.ts +60 -0
  36. package/src/dynamodb/impl/dynamo-db-storage-provider.ts +140 -0
  37. package/src/dynamodb/impl/dynamo-db-sync-lock.spec.ts +41 -0
  38. package/src/dynamodb/impl/dynamo-db-sync-lock.ts +78 -0
  39. package/src/dynamodb/impl/dynamo-expiring-code-provider.ts +31 -0
  40. package/src/dynamodb/impl/dynamo-runtime-parameter-provider.spec.ts +65 -0
  41. package/src/dynamodb/impl/dynamo-runtime-parameter-provider.ts +44 -0
  42. package/src/ec2/ec2-ratchet.spec.ts +45 -0
  43. package/src/ec2/ec2-ratchet.ts +169 -0
  44. package/src/ecr/ecr-unused-image-cleaner-options.ts +9 -0
  45. package/src/ecr/ecr-unused-image-cleaner-output.ts +8 -0
  46. package/src/ecr/ecr-unused-image-cleaner-repository-output.ts +10 -0
  47. package/src/ecr/ecr-unused-image-cleaner.spec.ts +40 -0
  48. package/src/ecr/ecr-unused-image-cleaner.ts +183 -0
  49. package/src/ecr/retained-image-descriptor.ts +7 -0
  50. package/src/ecr/retained-image-reason.ts +4 -0
  51. package/src/ecr/used-image-finder.ts +6 -0
  52. package/src/ecr/used-image-finders/aws-batch-used-image-finder.ts +40 -0
  53. package/src/ecr/used-image-finders/lambda-used-image-finder.ts +51 -0
  54. package/src/environment/cascade-environment-service-provider.ts +28 -0
  55. package/src/environment/env-var-environment-service-provider.ts +36 -0
  56. package/src/environment/environment-service-config.ts +7 -0
  57. package/src/environment/environment-service-provider.ts +7 -0
  58. package/src/environment/environment-service.spec.ts +41 -0
  59. package/src/environment/environment-service.ts +89 -0
  60. package/src/environment/fixed-environment-service-provider.ts +26 -0
  61. package/src/environment/ssm-environment-service-provider.spec.ts +18 -0
  62. package/src/environment/ssm-environment-service-provider.ts +71 -0
  63. package/src/expiring-code/expiring-code-params.ts +7 -0
  64. package/src/expiring-code/expiring-code-provider.ts +6 -0
  65. package/src/expiring-code/expiring-code-ratchet.spec.ts +10 -0
  66. package/src/expiring-code/expiring-code-ratchet.ts +44 -0
  67. package/src/expiring-code/expiring-code.ts +6 -0
  68. package/src/iam/aws-credentials-ratchet.ts +25 -0
  69. package/src/lambda/lambda-event-detector.ts +55 -0
  70. package/src/lambda/lambda-event-type-guards.ts +38 -0
  71. package/src/model/cloud-watch-metrics-minute-level-dynamo-count-request.ts +18 -0
  72. package/src/model/dynamo-count-result.ts +8 -0
  73. package/src/route53/route-53-ratchet.ts +77 -0
  74. package/src/runtime-parameter/cached-stored-runtime-parameter.ts +5 -0
  75. package/src/runtime-parameter/global-variable-override-runtime-parameter-provider.spec.ts +41 -0
  76. package/src/runtime-parameter/global-variable-override-runtime-parameter-provider.ts +82 -0
  77. package/src/runtime-parameter/memory-runtime-parameter-provider.ts +42 -0
  78. package/src/runtime-parameter/runtime-parameter-provider.ts +12 -0
  79. package/src/runtime-parameter/runtime-parameter-ratchet.spec.ts +53 -0
  80. package/src/runtime-parameter/runtime-parameter-ratchet.ts +84 -0
  81. package/src/runtime-parameter/stored-runtime-parameter.ts +6 -0
  82. package/src/s3/expanded-file-children.ts +5 -0
  83. package/src/s3/impl/s3-environment-service-provider.ts +41 -0
  84. package/src/s3/impl/s3-expiring-code-provider.spec.ts +63 -0
  85. package/src/s3/impl/s3-expiring-code-provider.ts +71 -0
  86. package/src/s3/impl/s3-prototype-dao-provider.spec.ts +45 -0
  87. package/src/s3/impl/s3-prototype-dao-provider.ts +37 -0
  88. package/src/s3/impl/s3-remote-file-tracking-provider-options.ts +6 -0
  89. package/src/s3/impl/s3-remote-file-tracking-provider.spec.ts +67 -0
  90. package/src/s3/impl/s3-remote-file-tracking-provider.ts +157 -0
  91. package/src/s3/impl/s3-storage-provider.spec.ts +32 -0
  92. package/src/s3/impl/s3-storage-provider.ts +60 -0
  93. package/src/s3/s3-cache-ratchet-like.ts +64 -0
  94. package/src/s3/s3-cache-ratchet.spec.ts +150 -0
  95. package/src/s3/s3-cache-ratchet.ts +476 -0
  96. package/src/s3/s3-location-sync-ratchet.ts +207 -0
  97. package/src/s3/s3-ratchet.spec.ts +26 -0
  98. package/src/s3/s3-ratchet.ts +26 -0
  99. package/src/ses/ses-mail-sending-provider.ts +85 -0
  100. package/src/sns/sns-ratchet.spec.ts +24 -0
  101. package/src/sns/sns-ratchet.ts +52 -0
  102. package/src/sync-lock/memory-sync-lock.ts +48 -0
  103. package/src/sync-lock/sync-lock-provider.ts +5 -0
@@ -0,0 +1,65 @@
1
+ import { DynamoRuntimeParameterProvider } from './dynamo-runtime-parameter-provider.js';
2
+ import { DynamoRatchet } from '../dynamo-ratchet.js';
3
+ import { StoredRuntimeParameter } from '../../runtime-parameter/stored-runtime-parameter.js';
4
+
5
+ import { RuntimeParameterRatchet } from '../../runtime-parameter/runtime-parameter-ratchet.js';
6
+ import { PutCommandOutput } from '@aws-sdk/lib-dynamodb';
7
+ import { Logger } from '@bitblit/ratchet-common/logger/logger';
8
+ import { PromiseRatchet } from '@bitblit/ratchet-common/lang/promise-ratchet';
9
+ import { LoggerLevelName } from '@bitblit/ratchet-common/logger/logger-level-name';
10
+ import { beforeEach, describe, expect, test } from 'vitest';
11
+ import { mock, MockProxy } from 'vitest-mock-extended';
12
+
13
+ let mockDynamoRatchet: MockProxy<DynamoRatchet>;
14
+ const testEntry: StoredRuntimeParameter = { groupId: 'test', paramKey: 'test', paramValue: '15', ttlSeconds: 0.5 };
15
+ const testEntry2: StoredRuntimeParameter = { groupId: 'test', paramKey: 'test1', paramValue: '20', ttlSeconds: 0.5 };
16
+
17
+ describe('#runtimeParameterRatchet', function () {
18
+ beforeEach(() => {
19
+ mockDynamoRatchet = mock<DynamoRatchet>();
20
+ });
21
+
22
+ test('fetch and cache a runtime parameter', async () => {
23
+ Logger.setLevel(LoggerLevelName.silly);
24
+ const tableName: string = 'default-table';
25
+ mockDynamoRatchet.simpleGet.mockResolvedValue(testEntry);
26
+ mockDynamoRatchet.simplePut.mockResolvedValue({} as PutCommandOutput);
27
+ const drpp: DynamoRuntimeParameterProvider = new DynamoRuntimeParameterProvider(mockDynamoRatchet, tableName);
28
+ const rpr: RuntimeParameterRatchet = new RuntimeParameterRatchet(drpp);
29
+
30
+ const stored: StoredRuntimeParameter = await rpr.storeParameter('test', 'test1', 15, 0.5);
31
+ Logger.info('Stored : %j', stored);
32
+
33
+ const cache1: number = await rpr.fetchParameter<number>('test', 'test1');
34
+ const cache1a: number = await rpr.fetchParameter<number>('test', 'test1');
35
+ const cache1b: number = await rpr.fetchParameter<number>('test', 'test1');
36
+ expect(cache1).toEqual(15);
37
+ expect(cache1a).toEqual(15);
38
+ expect(cache1b).toEqual(15);
39
+
40
+ await PromiseRatchet.wait(1000);
41
+
42
+ const cache2: number = await rpr.fetchParameter<number>('test', 'test1');
43
+ expect(cache2).toEqual(15);
44
+
45
+ mockDynamoRatchet.simpleGet.mockResolvedValue(null);
46
+ const cacheMiss: number = await rpr.fetchParameter<number>('test', 'test-miss');
47
+ expect(cacheMiss).toBeNull();
48
+
49
+ const cacheDefault: number = await rpr.fetchParameter<number>('test', 'test-miss', 27);
50
+ expect(cacheDefault).toEqual(27);
51
+ }, 30_000);
52
+
53
+ test('reads underlying entries', async () => {
54
+ Logger.setLevel(LoggerLevelName.silly);
55
+ const tableName: string = 'default-table';
56
+ mockDynamoRatchet.fullyExecuteQuery.mockResolvedValue([testEntry, testEntry2]);
57
+ const drpp: DynamoRuntimeParameterProvider = new DynamoRuntimeParameterProvider(mockDynamoRatchet, tableName);
58
+ const rpr: RuntimeParameterRatchet = new RuntimeParameterRatchet(drpp);
59
+
60
+ const vals: StoredRuntimeParameter[] = await rpr.readUnderlyingEntries('test');
61
+
62
+ expect(vals).not.toBeFalsy();
63
+ expect(vals.length).toEqual(2);
64
+ }, 30_000);
65
+ });
@@ -0,0 +1,44 @@
1
+ import { PutCommandOutput, QueryCommandInput } from '@aws-sdk/lib-dynamodb';
2
+ import { RuntimeParameterProvider } from '../../runtime-parameter/runtime-parameter-provider.js';
3
+ import { StoredRuntimeParameter } from '../../runtime-parameter/stored-runtime-parameter.js';
4
+ import { DynamoRatchet } from '../dynamo-ratchet.js';
5
+ import { RequireRatchet } from '@bitblit/ratchet-common/lang/require-ratchet';
6
+ import { Logger } from '@bitblit/ratchet-common/logger/logger';
7
+
8
+ export class DynamoRuntimeParameterProvider implements RuntimeParameterProvider {
9
+ constructor(
10
+ private dynamo: DynamoRatchet,
11
+ private tableName: string,
12
+ ) {
13
+ RequireRatchet.notNullOrUndefined(this.dynamo);
14
+ RequireRatchet.notNullOrUndefined(this.tableName);
15
+ }
16
+
17
+ public async readParameter(groupId: string, paramKey: string): Promise<StoredRuntimeParameter> {
18
+ Logger.silly('Reading %s / %s from underlying db', groupId, paramKey);
19
+ const req: any = {
20
+ groupId: groupId,
21
+ paramKey: paramKey,
22
+ };
23
+ const rval: StoredRuntimeParameter = await this.dynamo.simpleGet<StoredRuntimeParameter>(this.tableName, req);
24
+ return rval;
25
+ }
26
+
27
+ public async readAllParametersForGroup(groupId: string): Promise<StoredRuntimeParameter[]> {
28
+ const qry: QueryCommandInput = {
29
+ TableName: this.tableName,
30
+ KeyConditionExpression: 'groupId = :groupId',
31
+ ExpressionAttributeValues: {
32
+ ':groupId': groupId,
33
+ },
34
+ };
35
+
36
+ const all: StoredRuntimeParameter[] = await this.dynamo.fullyExecuteQuery<StoredRuntimeParameter>(qry);
37
+ return all;
38
+ }
39
+
40
+ public async writeParameter(toStore: StoredRuntimeParameter): Promise<boolean> {
41
+ const rval: PutCommandOutput = await this.dynamo.simplePut(this.tableName, toStore);
42
+ return !!rval;
43
+ }
44
+ }
@@ -0,0 +1,45 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import { Logger } from '@bitblit/ratchet-common/logger/logger';
5
+ import { Ec2Ratchet } from './ec2-ratchet.js';
6
+ import { SendSSHPublicKeyResponse } from '@aws-sdk/client-ec2-instance-connect';
7
+ import { Instance } from '@aws-sdk/client-ec2';
8
+ import { describe, expect, test } from 'vitest';
9
+
10
+ describe('#EC2Ratchet', function () {
11
+ test.skip('should send a public key', async () => {
12
+ const ratchet: Ec2Ratchet = new Ec2Ratchet();
13
+ const instId: string = 'i-replace_me';
14
+ const pubKey: string = fs.readFileSync(path.join(os.homedir(), '.ssh/id_rsa.pub')).toString();
15
+
16
+ const res: SendSSHPublicKeyResponse = await ratchet.sendPublicKeyToEc2Instance(instId, pubKey);
17
+
18
+ Logger.info('Got : %j', res);
19
+ expect(res).toBeTruthy();
20
+ });
21
+
22
+ test.skip('should list instances', async () => {
23
+ const ratchet: Ec2Ratchet = new Ec2Ratchet();
24
+
25
+ const res: Instance[] = await ratchet.listAllInstances();
26
+
27
+ Logger.info('Got : %j', res);
28
+ expect(res).toBeTruthy();
29
+ expect(res.length).toBeGreaterThan(1);
30
+ });
31
+
32
+ test.skip('should start and stop an instance', async () => {
33
+ const ratchet: Ec2Ratchet = new Ec2Ratchet();
34
+
35
+ const instId: string = 'i-replace_me';
36
+
37
+ Logger.info('First start');
38
+ await ratchet.launchInstance(instId, 1000 * 60);
39
+
40
+ Logger.info('Next stop');
41
+ await ratchet.stopInstance(instId, 1000 * 60);
42
+
43
+ Logger.info('Complete');
44
+ });
45
+ });
@@ -0,0 +1,169 @@
1
+ import {
2
+ DescribeInstancesCommand,
3
+ DescribeInstancesCommandInput,
4
+ DescribeInstancesCommandOutput,
5
+ EC2Client,
6
+ Instance,
7
+ StartInstancesCommand,
8
+ StartInstancesCommandInput,
9
+ StopInstancesCommand,
10
+ StopInstancesCommandInput,
11
+ } from '@aws-sdk/client-ec2';
12
+ import {
13
+ EC2InstanceConnectClient,
14
+ SendSSHPublicKeyCommand,
15
+ SendSSHPublicKeyCommandInput,
16
+ SendSSHPublicKeyCommandOutput,
17
+ } from '@aws-sdk/client-ec2-instance-connect';
18
+ import { Logger } from '@bitblit/ratchet-common/logger/logger';
19
+ import { PromiseRatchet } from '@bitblit/ratchet-common/lang/promise-ratchet';
20
+ import { DurationRatchet } from '@bitblit/ratchet-common/lang/duration-ratchet';
21
+
22
+ /**
23
+ * Service to simplify interacting with EC2 instances
24
+ *
25
+ * NOTE! If you are going to describe instances, you MUST use resource: '*' in your
26
+ * IAM priv - any other value will fail. See
27
+ * https://forums.aws.amazon.com/thread.jspa?threadID=142312 and
28
+ * https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-policies-for-amazon-ec2.html
29
+ *
30
+ *
31
+ * Really should combine start and stop below
32
+ */
33
+ export class Ec2Ratchet {
34
+ private ec2: EC2Client;
35
+ private ec2InstanceConnect: EC2InstanceConnectClient;
36
+
37
+ constructor(
38
+ private region: string = 'us-east-1',
39
+ private availabilityZone: string = 'us-east-1a',
40
+ ) {
41
+ this.ec2 = new EC2Client({ region: region });
42
+ this.ec2InstanceConnect = new EC2InstanceConnectClient({ region: region });
43
+ }
44
+ public get eC2Client(): EC2Client {
45
+ return this.ec2;
46
+ }
47
+
48
+ public get eC2InstanceConnectClient(): EC2InstanceConnectClient {
49
+ return this.ec2InstanceConnect;
50
+ }
51
+
52
+ public async stopInstance(instanceId: string, maxWaitForShutdownMS: number = 0): Promise<boolean> {
53
+ let rval: boolean = true;
54
+
55
+ try {
56
+ const stopParams: StopInstancesCommandInput = {
57
+ InstanceIds: [instanceId],
58
+ DryRun: false,
59
+ };
60
+
61
+ Logger.info('About to stop instances : %j', stopParams);
62
+ await this.ec2.send(new StopInstancesCommand(stopParams));
63
+ Logger.info('Stop instance command sent, waiting on shutdown');
64
+ let status: Instance = await this.describeInstance(instanceId);
65
+
66
+ if (maxWaitForShutdownMS > 0) {
67
+ const start: number = new Date().getTime();
68
+ while (!!status && status.State.Code !== 16 && new Date().getTime() - start < maxWaitForShutdownMS) {
69
+ Logger.debug(
70
+ 'Instance status is %j - waiting for 5 seconds (up to %s)',
71
+ status.State,
72
+ DurationRatchet.formatMsDuration(maxWaitForShutdownMS),
73
+ );
74
+ await PromiseRatchet.wait(5000);
75
+ status = await this.describeInstance(instanceId);
76
+ }
77
+ }
78
+ } catch (err) {
79
+ Logger.error('Failed to stop instance %s : %s', instanceId, err, err);
80
+ rval = false;
81
+ }
82
+ return rval;
83
+ }
84
+
85
+ public async launchInstance(instanceId: string, maxWaitForStartupMS: number = 0): Promise<boolean> {
86
+ let rval: boolean = true;
87
+
88
+ try {
89
+ const startParams: StartInstancesCommandInput = {
90
+ InstanceIds: [instanceId],
91
+ DryRun: false,
92
+ };
93
+
94
+ // const result: StartInstancesResult =
95
+ Logger.info('About to start instance : %j', startParams);
96
+ await this.ec2.send(new StartInstancesCommand(startParams));
97
+ Logger.info('Start instance command sent, waiting on startup');
98
+ let status: Instance = await this.describeInstance(instanceId);
99
+
100
+ if (maxWaitForStartupMS > 0) {
101
+ const start: number = new Date().getTime();
102
+ while (!!status && status.State.Code !== 16 && new Date().getTime() - start < maxWaitForStartupMS) {
103
+ Logger.debug(
104
+ 'Instance status is %j - waiting for 5 seconds (up to %s)',
105
+ status.State,
106
+ DurationRatchet.formatMsDuration(maxWaitForStartupMS),
107
+ );
108
+ await PromiseRatchet.wait(5000);
109
+ status = await this.describeInstance(instanceId);
110
+ }
111
+ }
112
+
113
+ if (!!status && !!status.PublicIpAddress) {
114
+ Logger.info('Instance address is %s', status.PublicIpAddress);
115
+ Logger.info('SSH command : ssh -i path_to_pem_file ec2-user@%s', status.PublicIpAddress);
116
+ }
117
+ } catch (err) {
118
+ Logger.error('Failed to start instance %s : %s', instanceId, err, err);
119
+ rval = false;
120
+ }
121
+ return rval;
122
+ }
123
+
124
+ public async describeInstance(instanceId: string): Promise<Instance> {
125
+ const res: Instance[] = await this.listAllInstances([instanceId]);
126
+ return res.length === 1 ? res[0] : null;
127
+ }
128
+
129
+ public async listAllInstances(instanceIds: string[] = []): Promise<Instance[]> {
130
+ let rval: Instance[] = [];
131
+
132
+ const req: DescribeInstancesCommandInput = {
133
+ NextToken: null,
134
+ };
135
+
136
+ if (instanceIds && instanceIds.length > 0) {
137
+ req.InstanceIds = instanceIds;
138
+ }
139
+
140
+ do {
141
+ Logger.debug('Pulling instances... (%j)', req);
142
+ const res: DescribeInstancesCommandOutput = await this.ec2.send(new DescribeInstancesCommand(req));
143
+ res.Reservations.forEach((r) => {
144
+ rval = rval.concat(r.Instances);
145
+ });
146
+ req.NextToken = res.NextToken;
147
+ } while (req.NextToken);
148
+
149
+ Logger.debug('Finished pulling instances (found %d)', rval.length);
150
+ return rval;
151
+ }
152
+
153
+ public async sendPublicKeyToEc2Instance(
154
+ instanceId: string,
155
+ publicKeyString: string,
156
+ instanceOsUser?: string,
157
+ ): Promise<SendSSHPublicKeyCommandOutput> {
158
+ const userName: string = instanceOsUser || 'ec2-user';
159
+ const req: SendSSHPublicKeyCommandInput = {
160
+ InstanceId: instanceId,
161
+ AvailabilityZone: this.availabilityZone,
162
+ InstanceOSUser: userName,
163
+ SSHPublicKey: publicKeyString,
164
+ };
165
+
166
+ const rval: SendSSHPublicKeyCommandOutput = await this.ec2InstanceConnect.send(new SendSSHPublicKeyCommand(req));
167
+ return rval;
168
+ }
169
+ }
@@ -0,0 +1,9 @@
1
+ import { UsedImageFinder } from './used-image-finder.js';
2
+
3
+ export interface EcrUnusedImageCleanerOptions {
4
+ usedImageFinders: UsedImageFinder[]; // List of objects that will find in-use images that should not be removed
5
+
6
+ dryRun?: boolean;
7
+ repositoriesToPurge?: string[]; // If set, only process these repos
8
+ minimumAgeInDays?: number; // If set, don't remove anything younger than this
9
+ }
@@ -0,0 +1,8 @@
1
+ import { EcrUnusedImageCleanerRepositoryOutput } from './ecr-unused-image-cleaner-repository-output.js';
2
+ import { EcrUnusedImageCleanerOptions } from './ecr-unused-image-cleaner-options.js';
3
+
4
+ export interface EcrUnusedImageCleanerOutput {
5
+ registryId: string;
6
+ repositories: EcrUnusedImageCleanerRepositoryOutput[];
7
+ options: EcrUnusedImageCleanerOptions;
8
+ }
@@ -0,0 +1,10 @@
1
+ import { ImageDetail, Repository } from '@aws-sdk/client-ecr';
2
+ import { RetainedImageDescriptor } from './retained-image-descriptor.js';
3
+
4
+ export interface EcrUnusedImageCleanerRepositoryOutput {
5
+ repository: Repository;
6
+ purged: ImageDetail[];
7
+ retained: RetainedImageDescriptor[];
8
+
9
+ totalBytesRecovered: number;
10
+ }
@@ -0,0 +1,40 @@
1
+ import { Logger } from '@bitblit/ratchet-common/logger/logger';
2
+ import { EcrUnusedImageCleaner } from './ecr-unused-image-cleaner.js';
3
+ import { ECRClient } from '@aws-sdk/client-ecr';
4
+ import { AwsCredentialsRatchet } from '../iam/aws-credentials-ratchet.js';
5
+ import { LambdaUsedImageFinder } from './used-image-finders/lambda-used-image-finder.js';
6
+ import { LambdaClient } from '@aws-sdk/client-lambda';
7
+ import { AwsBatchUsedImageFinder } from './used-image-finders/aws-batch-used-image-finder.js';
8
+ import { BatchClient } from '@aws-sdk/client-batch';
9
+ import { EcrUnusedImageCleanerOutput } from './ecr-unused-image-cleaner-output.js';
10
+ import { describe, expect, test } from 'vitest';
11
+
12
+ //import { mockClient } from 'aws-sdk-client-mock';
13
+ //import { ECRClient } from "@aws-sdk/client-ecr";
14
+
15
+ //let mockEcr;
16
+
17
+ describe('#ecrUnusedImageCleaner', function () {
18
+ /*
19
+ mockEcr = mockClient(ECRClient);
20
+
21
+ beforeEach(() => {
22
+ mockEcr.reset();
23
+ });
24
+
25
+ */
26
+
27
+ test.skip('should run the cleaner', async () => {
28
+ Logger.info('Testing cleaner');
29
+ AwsCredentialsRatchet.applySetProfileEnvironmentalVariable('your-profile-here');
30
+ const cleaner: EcrUnusedImageCleaner = new EcrUnusedImageCleaner(new ECRClient({ region: 'us-east-1' }));
31
+ const output: EcrUnusedImageCleanerOutput = await cleaner.performCleaning({
32
+ dryRun: true,
33
+ usedImageFinders: [
34
+ new LambdaUsedImageFinder(new LambdaClient({ region: 'us-east-1' })),
35
+ new AwsBatchUsedImageFinder(new BatchClient({ region: 'us-east-1' })),
36
+ ],
37
+ });
38
+ expect(output).not.toBeNull();
39
+ }, 300_000_000);
40
+ });
@@ -0,0 +1,183 @@
1
+ import {
2
+ BatchDeleteImageCommand,
3
+ BatchDeleteImageCommandInput,
4
+ BatchDeleteImageCommandOutput,
5
+ DescribeImagesCommand,
6
+ DescribeImagesCommandInput,
7
+ DescribeImagesResponse,
8
+ DescribeRegistryCommand,
9
+ DescribeRegistryResponse,
10
+ DescribeRepositoriesCommand,
11
+ DescribeRepositoriesCommandInput,
12
+ DescribeRepositoriesResponse,
13
+ ECRClient,
14
+ ImageDetail,
15
+ Repository,
16
+ } from '@aws-sdk/client-ecr';
17
+ import { RequireRatchet } from '@bitblit/ratchet-common/lang/require-ratchet';
18
+ import { Logger } from '@bitblit/ratchet-common/logger/logger';
19
+ import { StringRatchet } from '@bitblit/ratchet-common/lang/string-ratchet';
20
+ import { EcrUnusedImageCleanerOptions } from './ecr-unused-image-cleaner-options.js';
21
+ import { UsedImageFinder } from './used-image-finder.js';
22
+ import { EcrUnusedImageCleanerRepositoryOutput } from './ecr-unused-image-cleaner-repository-output.js';
23
+ import { RetainedImageDescriptor } from './retained-image-descriptor.js';
24
+ import { RetainedImageReason } from './retained-image-reason.js';
25
+ import { EcrUnusedImageCleanerOutput } from './ecr-unused-image-cleaner-output.js';
26
+
27
+ export class EcrUnusedImageCleaner {
28
+ // Do not delete images that are not at least this many days old.
29
+ private static readonly ECR_IMAGE_MINIMUM_AGE_DAYS: number = 60;
30
+
31
+ // Do not delete images if it would bring the total image count below this number.
32
+ private static readonly ECR_REPOSITORY_MINIMUM_IMAGE_COUNT: number = 600;
33
+
34
+ constructor(private ecr: ECRClient) {
35
+ RequireRatchet.notNullOrUndefined(ecr, 'ecr');
36
+ }
37
+
38
+ public async findAllUsedImages(finders: UsedImageFinder[]): Promise<string[]> {
39
+ const rval: Set<string> = new Set<string>();
40
+ for (const fnd of finders) {
41
+ //for (let i = 0; i < finders.length; i++) {
42
+ const next: string[] = await fnd.findUsedImageUris();
43
+ next.forEach((s) => rval.add(s));
44
+ }
45
+ return Array.from(rval);
46
+ }
47
+
48
+ public async performCleaning(opts: EcrUnusedImageCleanerOptions): Promise<EcrUnusedImageCleanerOutput> {
49
+ Logger.info('Starting cleaning with options : %j', opts);
50
+
51
+ Logger.info('Finding in-use images');
52
+ const usedImagesUris: string[] = await this.findAllUsedImages(opts.usedImageFinders || []);
53
+ const usedImageTags: string[] = usedImagesUris.map((s) => s.substring(s.lastIndexOf(':') + 1));
54
+ Logger.info('Found %d images in use: %j', usedImageTags.length, usedImageTags);
55
+
56
+ const regId: string = await this.fetchRegistryId();
57
+ Logger.info('Processing registry %s', regId);
58
+ const repos: Repository[] = await this.fetchAllRepositoryDescriptors(regId);
59
+ Logger.info('Found repos : %j', repos);
60
+
61
+ const cleaned: EcrUnusedImageCleanerRepositoryOutput[] = [];
62
+ for (let i = 0; i < repos.length; i++) {
63
+ Logger.info('Processing repo %d of %d', i, repos.length);
64
+ try {
65
+ const next = await this.cleanRepository(repos[i], usedImageTags, opts);
66
+ cleaned.push(next);
67
+ } catch (err) {
68
+ Logger.error('Failed to process repo : %j : %s', repos[i], err, err);
69
+ }
70
+ }
71
+
72
+ const rval: EcrUnusedImageCleanerOutput = {
73
+ registryId: regId,
74
+ repositories: cleaned,
75
+ options: opts,
76
+ };
77
+ return rval;
78
+ }
79
+
80
+ public async cleanRepository(
81
+ repo: Repository,
82
+ usedImageTags: string[],
83
+ opts: EcrUnusedImageCleanerOptions,
84
+ ): Promise<EcrUnusedImageCleanerRepositoryOutput> {
85
+ Logger.info('Cleaning repository: %j', repo);
86
+
87
+ const images: ImageDetail[] = await this.fetchAllImageDescriptors(repo);
88
+ Logger.info('Found images: %d : %j', images.length, images);
89
+
90
+ const toPurge: ImageDetail[] = [];
91
+ const toKeep: RetainedImageDescriptor[] = [];
92
+ images.forEach((i) => {
93
+ const matches: boolean[] = usedImageTags.map((tag) => i.imageTags.includes(tag));
94
+ const anyMatch: boolean = matches.find((s) => s);
95
+ if (anyMatch) {
96
+ toKeep.push({ image: i, reason: RetainedImageReason.InUse });
97
+ } else {
98
+ toPurge.push(i);
99
+ }
100
+ });
101
+
102
+ Logger.info('Found %d to purge and %d to keep', toPurge.length, toKeep.length);
103
+
104
+ const totalBytes: number = toPurge.map((p) => p.imageSizeInBytes).reduce((a, i) => a + i, 0);
105
+ Logger.info('Found %s total bytes to purge : %d', StringRatchet.formatBytes(totalBytes), totalBytes);
106
+
107
+ const purgeCmd: BatchDeleteImageCommandInput = {
108
+ registryId: repo.registryId,
109
+ repositoryName: repo.repositoryName,
110
+ imageIds: toPurge.map((p) => {
111
+ return { imageDigest: p.imageDigest, imageTag: p.imageTags[0] };
112
+ }),
113
+ };
114
+
115
+ Logger.info('Purge command : %j', purgeCmd);
116
+
117
+ if (opts.dryRun) {
118
+ Logger.info('Dry run specd, stopping');
119
+ } else {
120
+ if (purgeCmd.imageIds.length > 0) {
121
+ Logger.info('Purging unused images');
122
+ const output: BatchDeleteImageCommandOutput = await this.ecr.send(new BatchDeleteImageCommand(purgeCmd));
123
+ Logger.info('Response was : %j', output);
124
+ } else {
125
+ Logger.info('Skipping - nothing to purge in this repo');
126
+ }
127
+ }
128
+
129
+ const rval: EcrUnusedImageCleanerRepositoryOutput = {
130
+ repository: repo,
131
+ purged: toPurge,
132
+ retained: toKeep,
133
+
134
+ totalBytesRecovered: totalBytes,
135
+ };
136
+ return rval;
137
+ }
138
+
139
+ public async fetchAllImageDescriptors(repo: Repository): Promise<ImageDetail[]> {
140
+ RequireRatchet.notNullOrUndefined(repo, 'repo');
141
+ let rval: ImageDetail[] = [];
142
+ const cmd: DescribeImagesCommandInput = {
143
+ registryId: repo.registryId,
144
+ repositoryName: repo.repositoryName,
145
+ };
146
+ let resp: DescribeImagesResponse = null;
147
+
148
+ do {
149
+ resp = await this.ecr.send(new DescribeImagesCommand(cmd));
150
+ rval = rval.concat(resp.imageDetails);
151
+ cmd.nextToken = resp.nextToken;
152
+ } while (StringRatchet.trimToNull(cmd.nextToken));
153
+
154
+ return rval;
155
+ }
156
+
157
+ public async fetchAllRepositoryDescriptors(registryId: string): Promise<Repository[]> {
158
+ let rval: Repository[] = [];
159
+ const cmd: DescribeRepositoriesCommandInput = {
160
+ registryId: registryId,
161
+ };
162
+ let resp: DescribeRepositoriesResponse = null;
163
+
164
+ do {
165
+ resp = await this.ecr.send(new DescribeRepositoriesCommand(cmd));
166
+ rval = rval.concat(resp.repositories);
167
+ cmd.nextToken = resp.nextToken;
168
+ } while (StringRatchet.trimToNull(cmd.nextToken));
169
+
170
+ return rval;
171
+ }
172
+
173
+ public async fetchAllRepositoryNames(registryId: string): Promise<string[]> {
174
+ const resps: Repository[] = await this.fetchAllRepositoryDescriptors(registryId);
175
+ const rval: string[] = resps.map((r) => r.repositoryName);
176
+ return rval;
177
+ }
178
+
179
+ private async fetchRegistryId(): Promise<string> {
180
+ const response: DescribeRegistryResponse = await this.ecr.send(new DescribeRegistryCommand({}));
181
+ return response.registryId;
182
+ }
183
+ }
@@ -0,0 +1,7 @@
1
+ import { ImageDetail } from '@aws-sdk/client-ecr';
2
+ import { RetainedImageReason } from './retained-image-reason.js';
3
+
4
+ export interface RetainedImageDescriptor {
5
+ image: ImageDetail;
6
+ reason: RetainedImageReason;
7
+ }
@@ -0,0 +1,4 @@
1
+ export enum RetainedImageReason {
2
+ InUse = 'InUse',
3
+ MinimumAge = 'MinimumAge',
4
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Classes implementing this interface are able to find image ids that are currently in use
3
+ */
4
+ export interface UsedImageFinder {
5
+ findUsedImageUris(): Promise<string[]>;
6
+ }
@@ -0,0 +1,40 @@
1
+ import { UsedImageFinder } from '../used-image-finder.js';
2
+
3
+ import { RequireRatchet } from '@bitblit/ratchet-common/lang/require-ratchet';
4
+ import { Logger } from '@bitblit/ratchet-common/logger/logger';
5
+ import { StringRatchet } from '@bitblit/ratchet-common/lang/string-ratchet';
6
+ import {
7
+ BatchClient,
8
+ DescribeJobDefinitionsCommand,
9
+ DescribeJobDefinitionsCommandInput,
10
+ DescribeJobDefinitionsCommandOutput,
11
+ JobDefinition,
12
+ } from '@aws-sdk/client-batch';
13
+
14
+ export class AwsBatchUsedImageFinder implements UsedImageFinder {
15
+ constructor(private batch: BatchClient) {
16
+ RequireRatchet.notNullOrUndefined(batch, 'batch');
17
+ }
18
+ public async findUsedImageUris(): Promise<string[]> {
19
+ const jobs: JobDefinition[] = await this.listAllJobDefinitions(false);
20
+ Logger.info('Found %d jobs', jobs.length);
21
+ const tmp: string[] = jobs.map((j) => j.containerProperties.image).filter((s) => StringRatchet.trimToNull(s));
22
+ const rval: string[] = Array.from(new Set<string>(tmp)); // remove dups
23
+ return rval;
24
+ }
25
+
26
+ public async listAllJobDefinitions(includeInactive?: boolean): Promise<JobDefinition[]> {
27
+ let rval: JobDefinition[] = [];
28
+ const request: DescribeJobDefinitionsCommandInput = {
29
+ nextToken: null,
30
+ status: includeInactive ? undefined : 'ACTIVE',
31
+ };
32
+ do {
33
+ const tmp: DescribeJobDefinitionsCommandOutput = await this.batch.send(new DescribeJobDefinitionsCommand(request));
34
+ rval = rval.concat(tmp.jobDefinitions);
35
+ request.nextToken = tmp.nextToken;
36
+ } while (request.nextToken);
37
+
38
+ return rval;
39
+ }
40
+ }