@squiz/job-runner-lib 2.8.0 → 2.10.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.
Files changed (31) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/lib/domains/job/constants/QueueBacklog.constants.d.ts +11 -0
  3. package/lib/domains/job/constants/RuntimeImage.constants.d.ts +3 -4
  4. package/lib/domains/job/constants/RuntimeImage.constants.spec.d.ts +5 -0
  5. package/lib/domains/job/index.d.ts +1 -0
  6. package/lib/domains/job/repository/JobExecutionRepository/JobExecutionRepository.d.ts +15 -0
  7. package/lib/domains/job/utils/ResolveRuntimeImageTaskDefinitionArn/ResolveRuntimeImageTaskDefinitionArn.d.ts +1 -1
  8. package/lib/domains/job/utils/ResolveRuntimeImageTaskDefinitionArn/ResolveRuntimeImageTaskDefinitionArn.spec.d.ts +5 -0
  9. package/lib/domains/job/utils/ResolveRuntimeImageTaskDefinitionArn/ResolveRuntimeImageTaskDefinitionArn.types.d.ts +1 -1
  10. package/lib/index.js +9 -9
  11. package/lib/index.js.map +4 -4
  12. package/package.json +1 -1
  13. package/src/domains/job/constants/QueueBacklog.constants.ts +12 -0
  14. package/src/domains/job/constants/RuntimeImage.constants.spec.ts +26 -0
  15. package/src/domains/job/constants/RuntimeImage.constants.ts +6 -9
  16. package/src/domains/job/index.ts +1 -0
  17. package/src/domains/job/manifest/mocks/JobManifest.mock.ts +2 -2
  18. package/src/domains/job/manifest/mocks/JobManifestDto.mock.ts +2 -2
  19. package/src/domains/job/manifest/mocks/JobManifestRecord.mock.ts +2 -2
  20. package/src/domains/job/manifest/mocks/JobManifestRequest.mock.ts +2 -2
  21. package/src/domains/job/repository/JobExecutionRepository/JobExecutionRepository.spec.ts +65 -0
  22. package/src/domains/job/repository/JobExecutionRepository/JobExecutionRepository.ts +53 -0
  23. package/src/domains/job/repository/JobManifestRepository/JobManifestRepository.spec.ts +24 -3
  24. package/src/domains/job/repository/JobRepository/JobRepository.spec.ts +24 -3
  25. package/src/domains/job/services/JobExecutionTaskService/JobExecutionTaskService.spec.ts +5 -6
  26. package/src/domains/job/services/JobExecutionTaskService/JobExecutionTaskService.ts +1 -1
  27. package/src/domains/job/utils/ResolveRuntimeImageTaskDefinitionArn/ResolveRuntimeImageTaskDefinitionArn.spec.ts +64 -0
  28. package/src/domains/job/utils/ResolveRuntimeImageTaskDefinitionArn/ResolveRuntimeImageTaskDefinitionArn.ts +2 -5
  29. package/src/domains/job/utils/ResolveRuntimeImageTaskDefinitionArn/ResolveRuntimeImageTaskDefinitionArn.types.ts +1 -1
  30. package/src/domains/manifest/utils/validateRuntimeImage.spec.ts +0 -8
  31. package/src/domains/manifest/utils/validateRuntimeImage.ts +5 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@squiz/job-runner-lib",
3
- "version": "2.8.0",
3
+ "version": "2.10.0",
4
4
  "description": "",
5
5
  "main": "lib/index.js",
6
6
  "private": false,
@@ -0,0 +1,12 @@
1
+ /*!
2
+ * @license
3
+ * Copyright Squiz Australia Pty Ltd. All Rights Reserved.
4
+ */
5
+
6
+ /**
7
+ * Backlog guard defaults. A job's `begin` is rejected with 422 once it has more than
8
+ * DEFAULT_MAX_QUEUED_EXECUTIONS queued AND its oldest queued execution is older than
9
+ * DEFAULT_MAX_QUEUED_AGE_HOURS. Both conditions are required (large AND stale).
10
+ */
11
+ export const DEFAULT_MAX_QUEUED_EXECUTIONS = 500;
12
+ export const DEFAULT_MAX_QUEUED_AGE_HOURS = 2;
@@ -0,0 +1,26 @@
1
+ /*!
2
+ * @license
3
+ * Copyright Squiz Australia Pty Ltd. All Rights Reserved.
4
+ */
5
+
6
+ import { RECOMMENDED_RUNTIME_IMAGE, getRuntimeImageOrDefault } from './RuntimeImage.constants';
7
+
8
+ describe('RuntimeImage.constants', () => {
9
+ describe('getRuntimeImageOrDefault', () => {
10
+ it('returns the recommended runtime image when image is undefined', () => {
11
+ expect(getRuntimeImageOrDefault(undefined)).toEqual(RECOMMENDED_RUNTIME_IMAGE);
12
+ });
13
+
14
+ it('returns the recommended runtime image when image is null', () => {
15
+ expect(getRuntimeImageOrDefault(null)).toEqual(RECOMMENDED_RUNTIME_IMAGE);
16
+ });
17
+
18
+ it('remaps deprecated node:20 to the recommended runtime image', () => {
19
+ expect(getRuntimeImageOrDefault('node:20')).toEqual(RECOMMENDED_RUNTIME_IMAGE);
20
+ });
21
+
22
+ it('returns node:24 unchanged', () => {
23
+ expect(getRuntimeImageOrDefault('node:24')).toEqual('node:24');
24
+ });
25
+ });
26
+ });
@@ -3,24 +3,21 @@
3
3
  * Copyright Squiz Australia Pty Ltd. All Rights Reserved.
4
4
  */
5
5
 
6
- export const SUPPORTED_RUNTIME_IMAGES = ['node:20', 'node:24'] as const;
7
-
8
- // Default for jobs that pre-date runtime image support.
9
- export const LEGACY_RUNTIME_IMAGE = 'node:20';
6
+ export const SUPPORTED_RUNTIME_IMAGES = ['node:24'] as const;
10
7
 
11
8
  export const RECOMMENDED_RUNTIME_IMAGE = 'node:24';
12
9
 
13
- // Still runnable but rejected on new uploads.
14
- export const DEPRECATED_RUNTIME_IMAGES: readonly string[] = [];
10
+ // Stored on old job records; remapped to RECOMMENDED_RUNTIME_IMAGE at read/execute time. Rejected on new uploads.
11
+ export const DEPRECATED_RUNTIME_IMAGES: readonly string[] = ['node:20'];
12
+
13
+ export const isDeprecatedRuntimeImage = (image: string): boolean => DEPRECATED_RUNTIME_IMAGES.includes(image);
15
14
 
16
15
  export const getRuntimeImageOrDefault = (image: string | null | undefined): string =>
17
- image === null || image === undefined ? LEGACY_RUNTIME_IMAGE : image;
16
+ image === null || image === undefined || isDeprecatedRuntimeImage(image) ? RECOMMENDED_RUNTIME_IMAGE : image;
18
17
 
19
18
  export const isSupportedRuntimeImage = (image: string): boolean =>
20
19
  (SUPPORTED_RUNTIME_IMAGES as readonly string[]).includes(image);
21
20
 
22
- export const isDeprecatedRuntimeImage = (image: string): boolean => DEPRECATED_RUNTIME_IMAGES.includes(image);
23
-
24
21
  export const getRuntimeImagesMetadata = () => ({
25
22
  supportedRuntimeImages: [...SUPPORTED_RUNTIME_IMAGES],
26
23
  deprecatedRuntimeImages: [...DEPRECATED_RUNTIME_IMAGES],
@@ -3,6 +3,7 @@ export * from './core/pagination';
3
3
 
4
4
  export * from './constants/InjectTokens';
5
5
  export * from './constants/JobExecutionStatus.constants';
6
+ export * from './constants/QueueBacklog.constants';
6
7
  export * from './constants/Repository.constants';
7
8
  export * from './constants/RetryConfig.constants';
8
9
  export * from './constants/RuntimeImage.constants';
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { faker } from '@faker-js/faker/locale/en';
7
7
  import { ENTITY_TYPES } from '../../constants/Repository.constants';
8
- import { LEGACY_RUNTIME_IMAGE } from '../../constants/RuntimeImage.constants';
8
+ import { RECOMMENDED_RUNTIME_IMAGE } from '../../constants/RuntimeImage.constants';
9
9
  import { JobManifest } from '../JobManifestTypes';
10
10
 
11
11
  export const createMockJobManifest = (partialRecord: Partial<JobManifest> = {}): JobManifest => {
@@ -25,7 +25,7 @@ export const createMockJobManifest = (partialRecord: Partial<JobManifest> = {}):
25
25
  name: faker.lorem.word(),
26
26
  version,
27
27
  timeoutSeconds: faker.number.int({ max: 28800, min: 5 }),
28
- image: LEGACY_RUNTIME_IMAGE,
28
+ image: RECOMMENDED_RUNTIME_IMAGE,
29
29
  autoRetry: false,
30
30
  type: ENTITY_TYPES.manifest,
31
31
  ...partialRecord,
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { faker } from '@faker-js/faker/locale/en';
7
7
  import { ENTITY_TYPES } from '../../constants/Repository.constants';
8
- import { LEGACY_RUNTIME_IMAGE } from '../../constants/RuntimeImage.constants';
8
+ import { RECOMMENDED_RUNTIME_IMAGE } from '../../constants/RuntimeImage.constants';
9
9
  import { JobManifestDto, JobManifestRecord } from '../JobManifestTypes';
10
10
 
11
11
  export const createMockJobManifestDto = (partialRecord: Partial<JobManifestRecord> = {}): JobManifestDto => {
@@ -40,7 +40,7 @@ export const createMockJobManifestDto = (partialRecord: Partial<JobManifestRecor
40
40
  N: (partialRecord.timeoutSeconds ?? faker.number.int({ max: 28800, min: 5 })).toString(),
41
41
  },
42
42
  image: {
43
- S: partialRecord.image ?? LEGACY_RUNTIME_IMAGE,
43
+ S: partialRecord.image ?? RECOMMENDED_RUNTIME_IMAGE,
44
44
  },
45
45
  autoRetry: {
46
46
  BOOL: partialRecord.autoRetry ?? false,
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { faker } from '@faker-js/faker/locale/en';
7
7
  import { ENTITY_TYPES } from '../../constants/Repository.constants';
8
- import { LEGACY_RUNTIME_IMAGE } from '../../constants/RuntimeImage.constants';
8
+ import { RECOMMENDED_RUNTIME_IMAGE } from '../../constants/RuntimeImage.constants';
9
9
  import { JobManifestRecord } from '../JobManifestTypes';
10
10
 
11
11
  export const createMockJobManifestRecord = (partialRecord: Partial<JobManifestRecord> = {}): JobManifestRecord => {
@@ -29,7 +29,7 @@ export const createMockJobManifestRecord = (partialRecord: Partial<JobManifestRe
29
29
  name,
30
30
  version,
31
31
  timeoutSeconds: faker.number.int({ max: 28800, min: 5 }),
32
- image: LEGACY_RUNTIME_IMAGE,
32
+ image: RECOMMENDED_RUNTIME_IMAGE,
33
33
  autoRetry: false,
34
34
  ...partialRecord,
35
35
  };
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { faker } from '@faker-js/faker/locale/en';
7
- import { LEGACY_RUNTIME_IMAGE } from '../../constants/RuntimeImage.constants';
7
+ import { RECOMMENDED_RUNTIME_IMAGE } from '../../constants/RuntimeImage.constants';
8
8
  import { CreateJobManifestRequest } from '../JobManifestTypes';
9
9
 
10
10
  export const createMockCreateJobManifestRequest = (
@@ -26,7 +26,7 @@ export const createMockCreateJobManifestRequest = (
26
26
  name: faker.lorem.word(),
27
27
  version,
28
28
  timeoutSeconds: faker.number.int({ max: 28800, min: 5 }),
29
- image: LEGACY_RUNTIME_IMAGE,
29
+ image: RECOMMENDED_RUNTIME_IMAGE,
30
30
  autoRetry: false,
31
31
  ...partialRecord,
32
32
  };
@@ -381,6 +381,71 @@ describe('JobExecutionRepository', (): void => {
381
381
  });
382
382
  });
383
383
 
384
+ describe('getOldestQueuedExecution', () => {
385
+ const jobName = 'backed-up-job';
386
+
387
+ it('queries lsi1 for the oldest queued execution (ascending, limit 1)', async () => {
388
+ const mockJobExecution = createMockJobExecution({ jobName, status: JOB_EXECUTION_STATUS.queued });
389
+ dynamoClient.on(QueryCommand).resolvesOnce({ Items: [createMockJobExecutionDto(mockJobExecution)] });
390
+
391
+ const result = await jobExecutionRepository.getOldestQueuedExecution(jobName);
392
+
393
+ expect(result).toEqual(mockJobExecution);
394
+ expect(dynamoClient).toHaveReceivedCommandWith(QueryCommand, {
395
+ TableName: tableName,
396
+ IndexName: 'lsi1',
397
+ KeyConditionExpression: '#pk = :pk and #lsi1sk = :lsi1sk',
398
+ ExpressionAttributeNames: { '#pk': 'pk', '#lsi1sk': 'lsi1sk' },
399
+ ExpressionAttributeValues: {
400
+ ':pk': { S: `${ENTITY_TYPES.jobExecution}~${tenant}` },
401
+ ':lsi1sk': { S: `${JOB_EXECUTION_STATUS.queued}~${jobName}` },
402
+ },
403
+ ScanIndexForward: true,
404
+ Limit: 1,
405
+ });
406
+ });
407
+
408
+ it('returns null when there are no queued executions', async () => {
409
+ dynamoClient.on(QueryCommand).resolvesOnce({ Items: [] });
410
+
411
+ const result = await jobExecutionRepository.getOldestQueuedExecution(jobName);
412
+
413
+ expect(result).toBeNull();
414
+ });
415
+ });
416
+
417
+ describe('countQueuedExecutions', () => {
418
+ const jobName = 'backed-up-job';
419
+
420
+ it('counts queued executions on lsi1 bounded by limit', async () => {
421
+ dynamoClient.on(QueryCommand).resolvesOnce({ Count: 501 });
422
+
423
+ const result = await jobExecutionRepository.countQueuedExecutions(jobName, 501);
424
+
425
+ expect(result).toBe(501);
426
+ expect(dynamoClient).toHaveReceivedCommandWith(QueryCommand, {
427
+ TableName: tableName,
428
+ IndexName: 'lsi1',
429
+ Select: 'COUNT',
430
+ KeyConditionExpression: '#pk = :pk and #lsi1sk = :lsi1sk',
431
+ ExpressionAttributeNames: { '#pk': 'pk', '#lsi1sk': 'lsi1sk' },
432
+ ExpressionAttributeValues: {
433
+ ':pk': { S: `${ENTITY_TYPES.jobExecution}~${tenant}` },
434
+ ':lsi1sk': { S: `${JOB_EXECUTION_STATUS.queued}~${jobName}` },
435
+ },
436
+ Limit: 501,
437
+ });
438
+ });
439
+
440
+ it('returns 0 when DynamoDB returns no count', async () => {
441
+ dynamoClient.on(QueryCommand).resolvesOnce({});
442
+
443
+ const result = await jobExecutionRepository.countQueuedExecutions(jobName, 501);
444
+
445
+ expect(result).toBe(0);
446
+ });
447
+ });
448
+
384
449
  describe('update', () => {
385
450
  it('Updates the record properties', async () => {
386
451
  const jobExecution = createMockJobExecution({ status: JOB_EXECUTION_STATUS.queued });
@@ -9,6 +9,7 @@ import {
9
9
  QueryCommand,
10
10
  QueryCommandInput,
11
11
  QueryCommandOutput,
12
+ Select,
12
13
  UpdateItemCommand,
13
14
  UpdateItemCommandInput,
14
15
  } from '@aws-sdk/client-dynamodb';
@@ -136,6 +137,58 @@ export class JobExecutionRepository extends Repository<JobExecution, JobExecutio
136
137
  return results.map((item) => this.convertToItem(item as JobExecutionDto));
137
138
  }
138
139
 
140
+ /**
141
+ * Returns the single oldest queued execution for a job, or null if there are none.
142
+ *
143
+ * Queries the `lsi1` index (`lsi1sk = "<status>~<jobName>"`). All queued executions of one
144
+ * job share the same `lsi1sk`, so DynamoDB breaks ties by the base-table `sk`
145
+ * (`"<jobName>~<id>"`); `id` is prefixed with the ISO `timeQueued`, which sorts
146
+ * lexicographically, so ascending order is chronological and the first item is the oldest.
147
+ */
148
+ async getOldestQueuedExecution(jobName: string): Promise<JobExecution | null> {
149
+ const res = await this.dynamoDbClient.send(
150
+ new QueryCommand({
151
+ TableName: this.tableName,
152
+ IndexName: 'lsi1',
153
+ KeyConditionExpression: '#pk = :pk and #lsi1sk = :lsi1sk',
154
+ ExpressionAttributeNames: { '#pk': 'pk', '#lsi1sk': 'lsi1sk' },
155
+ ExpressionAttributeValues: {
156
+ ':pk': { S: this.pk },
157
+ ':lsi1sk': { S: `${JOB_EXECUTION_STATUS.queued}~${jobName}` },
158
+ },
159
+ ScanIndexForward: true,
160
+ Limit: 1,
161
+ }),
162
+ );
163
+
164
+ const item = res.Items?.[0];
165
+ return item ? this.convertToItem(item as JobExecutionDto) : null;
166
+ }
167
+
168
+ /**
169
+ * Counts queued executions for a job, evaluating at most `limit` items (`Select: COUNT` so no
170
+ * item data is returned). Pass a `limit` just above the threshold you care about — queued
171
+ * items are small, so a single page covers the bounded count.
172
+ */
173
+ async countQueuedExecutions(jobName: string, limit?: number): Promise<number> {
174
+ const res = await this.dynamoDbClient.send(
175
+ new QueryCommand({
176
+ TableName: this.tableName,
177
+ IndexName: 'lsi1',
178
+ Select: Select.COUNT,
179
+ KeyConditionExpression: '#pk = :pk and #lsi1sk = :lsi1sk',
180
+ ExpressionAttributeNames: { '#pk': 'pk', '#lsi1sk': 'lsi1sk' },
181
+ ExpressionAttributeValues: {
182
+ ':pk': { S: this.pk },
183
+ ':lsi1sk': { S: `${JOB_EXECUTION_STATUS.queued}~${jobName}` },
184
+ },
185
+ Limit: limit,
186
+ }),
187
+ );
188
+
189
+ return res.Count ?? 0;
190
+ }
191
+
139
192
  /**
140
193
  * Get list of records from database.
141
194
  * @abstract
@@ -13,7 +13,7 @@ import {
13
13
  } from '@aws-sdk/client-dynamodb';
14
14
  import { initIocContainer, iocContainer } from '../../../../mocks/mockIoc';
15
15
  import { ENTITY_TYPES } from '../../constants/Repository.constants';
16
- import { LEGACY_RUNTIME_IMAGE } from '../../constants/RuntimeImage.constants';
16
+ import { RECOMMENDED_RUNTIME_IMAGE } from '../../constants/RuntimeImage.constants';
17
17
  import {
18
18
  createMockClientGetResponse,
19
19
  createMockClientPutResponse,
@@ -126,7 +126,7 @@ describe('JobManifestRepository', (): void => {
126
126
  expect(mockSend).toBeCalledTimes(1);
127
127
  });
128
128
 
129
- it(`should default missing image to legacy runtime`, async (): Promise<void> => {
129
+ it(`should default missing image to the recommended runtime`, async (): Promise<void> => {
130
130
  const mockJobManifestItem = createMockJobManifestDto();
131
131
  delete (mockJobManifestItem as Partial<typeof mockJobManifestItem>).image;
132
132
 
@@ -143,12 +143,33 @@ describe('JobManifestRepository', (): void => {
143
143
  });
144
144
  expect(response).toEqual(
145
145
  expect.objectContaining({
146
- image: LEGACY_RUNTIME_IMAGE,
146
+ image: RECOMMENDED_RUNTIME_IMAGE,
147
147
  autoRetry: false,
148
148
  }),
149
149
  );
150
150
  });
151
151
 
152
+ it('should remap deprecated node:20 runtime image to node:24', async (): Promise<void> => {
153
+ const mockJobManifestItem = createMockJobManifestDto({ image: 'node:20' });
154
+
155
+ mockSend.mockImplementationOnce(
156
+ (): GetItemCommandOutput =>
157
+ createMockClientGetResponse({
158
+ Item: mockJobManifestItem,
159
+ }),
160
+ );
161
+
162
+ const response = await jobManifestRepository.findOne({
163
+ name: mockJobManifestItem.name.S as string,
164
+ version: mockJobManifestItem.version.S as string,
165
+ });
166
+ expect(response).toEqual(
167
+ expect.objectContaining({
168
+ image: RECOMMENDED_RUNTIME_IMAGE,
169
+ }),
170
+ );
171
+ });
172
+
152
173
  it(`should default missing autoRetry to false`, async (): Promise<void> => {
153
174
  const mockJobManifestItem = createMockJobManifestDto();
154
175
  delete (mockJobManifestItem as Partial<typeof mockJobManifestItem>).autoRetry;
@@ -16,7 +16,7 @@ import {
16
16
  import { initIocContainer, iocContainer } from '../../../../mocks/mockIoc';
17
17
  import { InjectTokens } from '../../constants/InjectTokens';
18
18
  import { ENTITY_TYPES } from '../../constants/Repository.constants';
19
- import { LEGACY_RUNTIME_IMAGE } from '../../constants/RuntimeImage.constants';
19
+ import { RECOMMENDED_RUNTIME_IMAGE } from '../../constants/RuntimeImage.constants';
20
20
  import {
21
21
  createMockClientGetResponse,
22
22
  createMockClientPutResponse,
@@ -176,7 +176,7 @@ describe('JobRepository', (): void => {
176
176
  expect(mockSend).toBeCalledTimes(1);
177
177
  });
178
178
 
179
- it(`should default missing image to legacy runtime`, async (): Promise<void> => {
179
+ it(`should default missing image to the recommended runtime`, async (): Promise<void> => {
180
180
  const mockJobRecord = createMockJobDto();
181
181
  delete (mockJobRecord as Partial<typeof mockJobRecord>).image;
182
182
  mockSend.mockImplementationOnce(
@@ -193,7 +193,28 @@ describe('JobRepository', (): void => {
193
193
 
194
194
  expect(response).toEqual(
195
195
  expect.objectContaining({
196
- image: LEGACY_RUNTIME_IMAGE,
196
+ image: RECOMMENDED_RUNTIME_IMAGE,
197
+ }),
198
+ );
199
+ });
200
+
201
+ it('should remap deprecated node:20 runtime image to node:24', async (): Promise<void> => {
202
+ const mockJobRecord = createMockJobDto({ image: 'node:20' });
203
+ mockSend.mockImplementationOnce(
204
+ (): GetItemCommandOutput =>
205
+ createMockClientGetResponse({
206
+ Item: mockJobRecord,
207
+ }),
208
+ );
209
+
210
+ const response = await jobRepository.findOne({
211
+ name: mockJobRecord.name.S as string,
212
+ version: mockJobRecord.version.S as string,
213
+ });
214
+
215
+ expect(response).toEqual(
216
+ expect.objectContaining({
217
+ image: RECOMMENDED_RUNTIME_IMAGE,
197
218
  }),
198
219
  );
199
220
  });
@@ -31,7 +31,7 @@ describe('JobExecutionTaskService', () => {
31
31
  shortRegion: 'au',
32
32
  shortEnvironment: 'dev',
33
33
  clusterArn: `clusterArn`,
34
- taskDefinitionArn: `legacyTaskDefinitionArn`,
34
+ taskDefinitionArn: `defaultTaskDefinitionArn`,
35
35
  taskDefinitionArnsByRuntimeImage: {
36
36
  'node:20': 'node20TaskDefinitionArn',
37
37
  'node:24': 'node24TaskDefinitionArn',
@@ -213,11 +213,11 @@ describe('JobExecutionTaskService', () => {
213
213
  defaultRunJobTaskParams.jobExecution,
214
214
  );
215
215
  expect(mockEcsClient).toHaveReceivedCommandWith(RunTaskCommand, {
216
- taskDefinition: defaultRunJobTaskParams.taskDefinitionArnsByRuntimeImage['node:20'],
216
+ taskDefinition: defaultRunJobTaskParams.taskDefinitionArnsByRuntimeImage['node:24'],
217
217
  });
218
218
  });
219
219
 
220
- it('should use the legacy task definition ARN when the node:20 map entry is missing', async () => {
220
+ it('should use the default task definition ARN when the node:24 map entry is missing', async () => {
221
221
  const mockArn = `mock-arn`;
222
222
 
223
223
  mockEcsClient.on(RunTaskCommand).resolves({ tasks: [{ taskArn: mockArn }] });
@@ -250,9 +250,8 @@ describe('JobExecutionTaskService', () => {
250
250
  await expect(
251
251
  jobExecutionTaskService.runJobExecutionTask({
252
252
  ...defaultRunJobTaskParams,
253
- taskDefinitionArnsByRuntimeImage: {
254
- 'node:20': 'node20TaskDefinitionArn',
255
- },
253
+ taskDefinitionArnsByRuntimeImage: {},
254
+ taskDefinitionArn: '',
256
255
  }),
257
256
  ).rejects.toThrow(RuntimeImageTaskDefinitionError);
258
257
  expect(mockEcsClient).not.toHaveReceivedCommand(RunTaskCommand);
@@ -61,7 +61,7 @@ export class JobExecutionTaskService {
61
61
  const taskDefinitionArn = resolveRuntimeImageTaskDefinitionArn({
62
62
  image: jobManifest?.image,
63
63
  taskDefinitionArnsByRuntimeImage: params.taskDefinitionArnsByRuntimeImage,
64
- legacyTaskDefinitionArn: params.taskDefinitionArn,
64
+ defaultTaskDefinitionArn: params.taskDefinitionArn,
65
65
  });
66
66
  const jobDetailsFileUrl = await this.createJobDetailsFile(params.tenantId, params.jobExecution, jobManifest);
67
67
  const tags = getAwsTags({
@@ -0,0 +1,64 @@
1
+ /*!
2
+ * @license
3
+ * Copyright Squiz Australia Pty Ltd. All Rights Reserved.
4
+ */
5
+
6
+ import { RuntimeImageTaskDefinitionError } from '../../errors';
7
+ import { resolveRuntimeImageTaskDefinitionArn } from './ResolveRuntimeImageTaskDefinitionArn';
8
+
9
+ describe('resolveRuntimeImageTaskDefinitionArn', () => {
10
+ const taskDefinitionArnsByRuntimeImage = {
11
+ 'node:24': 'node24TaskDefinitionArn',
12
+ };
13
+ const defaultTaskDefinitionArn = 'defaultTaskDefinitionArn';
14
+
15
+ it('resolves node:24 when the stored image is deprecated node:20', () => {
16
+ expect(
17
+ resolveRuntimeImageTaskDefinitionArn({
18
+ image: 'node:20',
19
+ taskDefinitionArnsByRuntimeImage,
20
+ defaultTaskDefinitionArn,
21
+ }),
22
+ ).toEqual('node24TaskDefinitionArn');
23
+ });
24
+
25
+ it('resolves node:24 when the stored image is missing', () => {
26
+ expect(
27
+ resolveRuntimeImageTaskDefinitionArn({
28
+ image: undefined,
29
+ taskDefinitionArnsByRuntimeImage,
30
+ defaultTaskDefinitionArn,
31
+ }),
32
+ ).toEqual('node24TaskDefinitionArn');
33
+ });
34
+
35
+ it('uses the default task definition ARN when the runtime map entry is missing', () => {
36
+ expect(
37
+ resolveRuntimeImageTaskDefinitionArn({
38
+ image: 'node:20',
39
+ taskDefinitionArnsByRuntimeImage: {},
40
+ defaultTaskDefinitionArn,
41
+ }),
42
+ ).toEqual(defaultTaskDefinitionArn);
43
+ });
44
+
45
+ it('throws when the runtime image is unsupported', () => {
46
+ expect(() =>
47
+ resolveRuntimeImageTaskDefinitionArn({
48
+ image: 'node:26',
49
+ taskDefinitionArnsByRuntimeImage,
50
+ defaultTaskDefinitionArn,
51
+ }),
52
+ ).toThrow(RuntimeImageTaskDefinitionError);
53
+ });
54
+
55
+ it('throws when no task definition ARN is configured for the runtime image', () => {
56
+ expect(() =>
57
+ resolveRuntimeImageTaskDefinitionArn({
58
+ image: 'node:24',
59
+ taskDefinitionArnsByRuntimeImage: {},
60
+ defaultTaskDefinitionArn: '',
61
+ }),
62
+ ).toThrow(RuntimeImageTaskDefinitionError);
63
+ });
64
+ });
@@ -4,7 +4,6 @@
4
4
  */
5
5
 
6
6
  import {
7
- LEGACY_RUNTIME_IMAGE,
8
7
  SUPPORTED_RUNTIME_IMAGES,
9
8
  getRuntimeImageOrDefault,
10
9
  isSupportedRuntimeImage,
@@ -15,7 +14,7 @@ import { ResolveRuntimeImageTaskDefinitionArnParams } from './ResolveRuntimeImag
15
14
  export const resolveRuntimeImageTaskDefinitionArn = ({
16
15
  image,
17
16
  taskDefinitionArnsByRuntimeImage,
18
- legacyTaskDefinitionArn,
17
+ defaultTaskDefinitionArn,
19
18
  }: ResolveRuntimeImageTaskDefinitionArnParams): string => {
20
19
  const runtimeImage = getRuntimeImageOrDefault(image);
21
20
 
@@ -25,9 +24,7 @@ export const resolveRuntimeImageTaskDefinitionArn = ({
25
24
  );
26
25
  }
27
26
 
28
- const taskDefinitionArn =
29
- taskDefinitionArnsByRuntimeImage[runtimeImage] ||
30
- (runtimeImage === LEGACY_RUNTIME_IMAGE ? legacyTaskDefinitionArn : undefined);
27
+ const taskDefinitionArn = taskDefinitionArnsByRuntimeImage[runtimeImage] || defaultTaskDefinitionArn;
31
28
 
32
29
  if (!taskDefinitionArn) {
33
30
  throw new RuntimeImageTaskDefinitionError(
@@ -6,5 +6,5 @@
6
6
  export interface ResolveRuntimeImageTaskDefinitionArnParams {
7
7
  image: string | null | undefined;
8
8
  taskDefinitionArnsByRuntimeImage: Record<string, string>;
9
- legacyTaskDefinitionArn: string;
9
+ defaultTaskDefinitionArn: string;
10
10
  }
@@ -3,7 +3,6 @@
3
3
  * Copyright Squiz Australia Pty Ltd. All Rights Reserved.
4
4
  */
5
5
 
6
- import * as runtimeImageConstants from '../../job/constants/RuntimeImage.constants';
7
6
  import {
8
7
  DEPRECATED_RUNTIME_IMAGES,
9
8
  RECOMMENDED_RUNTIME_IMAGE,
@@ -13,10 +12,6 @@ import { ManifestValidationError } from '../errors/ManifestValidationError';
13
12
  import { validateAndDefaultRuntimeImage } from './validateRuntimeImage';
14
13
 
15
14
  describe('validateAndDefaultRuntimeImage', () => {
16
- afterEach(() => {
17
- jest.restoreAllMocks();
18
- });
19
-
20
15
  it('returns the recommended runtime image when image is undefined', () => {
21
16
  expect(validateAndDefaultRuntimeImage(undefined)).toEqual(RECOMMENDED_RUNTIME_IMAGE);
22
17
  });
@@ -38,9 +33,6 @@ describe('validateAndDefaultRuntimeImage', () => {
38
33
  });
39
34
 
40
35
  it('throws ManifestValidationError when the image is deprecated', () => {
41
- jest.spyOn(runtimeImageConstants, 'isSupportedRuntimeImage').mockReturnValue(true);
42
- jest.spyOn(runtimeImageConstants, 'isDeprecatedRuntimeImage').mockReturnValue(true);
43
-
44
36
  expect(() => validateAndDefaultRuntimeImage('node:20')).toThrow(ManifestValidationError);
45
37
  expect(() => validateAndDefaultRuntimeImage('node:20')).toThrow(/deprecated/);
46
38
  });
@@ -16,16 +16,16 @@ export const validateAndDefaultRuntimeImage = (image: string | null | undefined)
16
16
  return RECOMMENDED_RUNTIME_IMAGE;
17
17
  }
18
18
 
19
- if (!isSupportedRuntimeImage(image)) {
19
+ if (isDeprecatedRuntimeImage(image)) {
20
20
  throw new ManifestValidationError(
21
- `Unsupported runtime image "${image}". Supported images are: ${SUPPORTED_RUNTIME_IMAGES.join(', ')}`,
21
+ `Runtime image "${image}" is deprecated and can no longer be used for new uploads. ` +
22
+ `Please use the recommended runtime image "${RECOMMENDED_RUNTIME_IMAGE}".`,
22
23
  );
23
24
  }
24
25
 
25
- if (isDeprecatedRuntimeImage(image)) {
26
+ if (!isSupportedRuntimeImage(image)) {
26
27
  throw new ManifestValidationError(
27
- `Runtime image "${image}" is deprecated and can no longer be used for new uploads. ` +
28
- `Please use the recommended runtime image "${RECOMMENDED_RUNTIME_IMAGE}".`,
28
+ `Unsupported runtime image "${image}". Supported images are: ${SUPPORTED_RUNTIME_IMAGES.join(', ')}`,
29
29
  );
30
30
  }
31
31