@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.
- package/CHANGELOG.md +12 -0
- package/lib/domains/job/constants/QueueBacklog.constants.d.ts +11 -0
- package/lib/domains/job/constants/RuntimeImage.constants.d.ts +3 -4
- package/lib/domains/job/constants/RuntimeImage.constants.spec.d.ts +5 -0
- package/lib/domains/job/index.d.ts +1 -0
- package/lib/domains/job/repository/JobExecutionRepository/JobExecutionRepository.d.ts +15 -0
- package/lib/domains/job/utils/ResolveRuntimeImageTaskDefinitionArn/ResolveRuntimeImageTaskDefinitionArn.d.ts +1 -1
- package/lib/domains/job/utils/ResolveRuntimeImageTaskDefinitionArn/ResolveRuntimeImageTaskDefinitionArn.spec.d.ts +5 -0
- package/lib/domains/job/utils/ResolveRuntimeImageTaskDefinitionArn/ResolveRuntimeImageTaskDefinitionArn.types.d.ts +1 -1
- package/lib/index.js +9 -9
- package/lib/index.js.map +4 -4
- package/package.json +1 -1
- package/src/domains/job/constants/QueueBacklog.constants.ts +12 -0
- package/src/domains/job/constants/RuntimeImage.constants.spec.ts +26 -0
- package/src/domains/job/constants/RuntimeImage.constants.ts +6 -9
- package/src/domains/job/index.ts +1 -0
- package/src/domains/job/manifest/mocks/JobManifest.mock.ts +2 -2
- package/src/domains/job/manifest/mocks/JobManifestDto.mock.ts +2 -2
- package/src/domains/job/manifest/mocks/JobManifestRecord.mock.ts +2 -2
- package/src/domains/job/manifest/mocks/JobManifestRequest.mock.ts +2 -2
- package/src/domains/job/repository/JobExecutionRepository/JobExecutionRepository.spec.ts +65 -0
- package/src/domains/job/repository/JobExecutionRepository/JobExecutionRepository.ts +53 -0
- package/src/domains/job/repository/JobManifestRepository/JobManifestRepository.spec.ts +24 -3
- package/src/domains/job/repository/JobRepository/JobRepository.spec.ts +24 -3
- package/src/domains/job/services/JobExecutionTaskService/JobExecutionTaskService.spec.ts +5 -6
- package/src/domains/job/services/JobExecutionTaskService/JobExecutionTaskService.ts +1 -1
- package/src/domains/job/utils/ResolveRuntimeImageTaskDefinitionArn/ResolveRuntimeImageTaskDefinitionArn.spec.ts +64 -0
- package/src/domains/job/utils/ResolveRuntimeImageTaskDefinitionArn/ResolveRuntimeImageTaskDefinitionArn.ts +2 -5
- package/src/domains/job/utils/ResolveRuntimeImageTaskDefinitionArn/ResolveRuntimeImageTaskDefinitionArn.types.ts +1 -1
- package/src/domains/manifest/utils/validateRuntimeImage.spec.ts +0 -8
- package/src/domains/manifest/utils/validateRuntimeImage.ts +5 -5
package/package.json
CHANGED
|
@@ -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:
|
|
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
|
-
//
|
|
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 ?
|
|
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],
|
package/src/domains/job/index.ts
CHANGED
|
@@ -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 {
|
|
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:
|
|
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 {
|
|
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 ??
|
|
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 {
|
|
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:
|
|
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 {
|
|
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:
|
|
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 {
|
|
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
|
|
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:
|
|
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 {
|
|
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
|
|
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:
|
|
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: `
|
|
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:
|
|
216
|
+
taskDefinition: defaultRunJobTaskParams.taskDefinitionArnsByRuntimeImage['node:24'],
|
|
217
217
|
});
|
|
218
218
|
});
|
|
219
219
|
|
|
220
|
-
it('should use the
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
@@ -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 (
|
|
19
|
+
if (isDeprecatedRuntimeImage(image)) {
|
|
20
20
|
throw new ManifestValidationError(
|
|
21
|
-
`
|
|
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 (
|
|
26
|
+
if (!isSupportedRuntimeImage(image)) {
|
|
26
27
|
throw new ManifestValidationError(
|
|
27
|
-
`
|
|
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
|
|