@squiz/job-runner-lib 2.9.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 (23) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/lib/domains/job/constants/RuntimeImage.constants.d.ts +3 -4
  3. package/lib/domains/job/constants/RuntimeImage.constants.spec.d.ts +5 -0
  4. package/lib/domains/job/utils/ResolveRuntimeImageTaskDefinitionArn/ResolveRuntimeImageTaskDefinitionArn.d.ts +1 -1
  5. package/lib/domains/job/utils/ResolveRuntimeImageTaskDefinitionArn/ResolveRuntimeImageTaskDefinitionArn.spec.d.ts +5 -0
  6. package/lib/domains/job/utils/ResolveRuntimeImageTaskDefinitionArn/ResolveRuntimeImageTaskDefinitionArn.types.d.ts +1 -1
  7. package/lib/index.js +9 -9
  8. package/lib/index.js.map +3 -3
  9. package/package.json +1 -1
  10. package/src/domains/job/constants/RuntimeImage.constants.spec.ts +26 -0
  11. package/src/domains/job/constants/RuntimeImage.constants.ts +5 -8
  12. package/src/domains/job/manifest/mocks/JobManifest.mock.ts +2 -2
  13. package/src/domains/job/manifest/mocks/JobManifestDto.mock.ts +2 -2
  14. package/src/domains/job/manifest/mocks/JobManifestRecord.mock.ts +2 -2
  15. package/src/domains/job/manifest/mocks/JobManifestRequest.mock.ts +2 -2
  16. package/src/domains/job/repository/JobManifestRepository/JobManifestRepository.spec.ts +24 -3
  17. package/src/domains/job/repository/JobRepository/JobRepository.spec.ts +24 -3
  18. package/src/domains/job/services/JobExecutionTaskService/JobExecutionTaskService.spec.ts +5 -6
  19. package/src/domains/job/services/JobExecutionTaskService/JobExecutionTaskService.ts +1 -1
  20. package/src/domains/job/utils/ResolveRuntimeImageTaskDefinitionArn/ResolveRuntimeImageTaskDefinitionArn.spec.ts +64 -0
  21. package/src/domains/job/utils/ResolveRuntimeImageTaskDefinitionArn/ResolveRuntimeImageTaskDefinitionArn.ts +2 -5
  22. package/src/domains/job/utils/ResolveRuntimeImageTaskDefinitionArn/ResolveRuntimeImageTaskDefinitionArn.types.ts +1 -1
  23. 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.9.0",
3
+ "version": "2.10.0",
4
4
  "description": "",
5
5
  "main": "lib/index.js",
6
6
  "private": false,
@@ -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.
10
+ // Stored on old job records; remapped to RECOMMENDED_RUNTIME_IMAGE at read/execute time. Rejected on new uploads.
14
11
  export const DEPRECATED_RUNTIME_IMAGES: readonly string[] = ['node:20'];
15
12
 
13
+ export const isDeprecatedRuntimeImage = (image: string): boolean => DEPRECATED_RUNTIME_IMAGES.includes(image);
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],
@@ -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
  };
@@ -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
  }
@@ -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