@squiz/job-runner-lib 2.3.0 → 2.5.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/component/errors/JobError.spec.d.ts +1 -0
- package/lib/domains/job/ParsePartitionKey/ParsePartitionKey.d.ts +2 -2
- package/lib/domains/job/ParsePartitionKey/ParsePartitionKey.types.d.ts +5 -0
- package/lib/domains/job/S3Repository/JobDetailsRepository.d.ts +1 -2
- package/lib/domains/job/S3Repository/JobDetailsRepository.spec.d.ts +1 -1
- package/lib/domains/job/S3Repository/JobUploadsRepository.d.ts +16 -0
- package/lib/domains/job/{services/JobQueueService/JobQueueService.spec.d.ts → S3Repository/JobUploadsRepository.spec.d.ts} +1 -1
- package/lib/domains/job/S3Repository/S3RepositoryBase.d.ts +12 -5
- package/lib/domains/job/S3Repository/index.d.ts +2 -1
- package/lib/domains/job/constants/InjectTokens.d.ts +8 -0
- package/lib/domains/job/constants/JobExecutionStatus.constants.d.ts +2 -1
- package/lib/domains/job/constants/Repository.constants.d.ts +3 -3
- package/lib/domains/job/constants/RetryConfig.constants.d.ts +21 -0
- package/lib/domains/job/index.d.ts +6 -4
- package/lib/domains/job/manifest/JobManifestTypes.d.ts +7 -6
- package/lib/domains/job/model/Base/Base.d.ts +10 -5
- package/lib/domains/job/model/Job/Job.d.ts +9 -6
- package/lib/domains/job/model/JobContext/JobContext.d.ts +6 -5
- package/lib/domains/job/model/JobExecution/JobExecution.d.ts +64 -9
- package/lib/domains/job/model/LogPayload/LogPayload.d.ts +21 -12
- package/lib/domains/job/model/LogPayload/index.d.ts +1 -0
- package/lib/domains/job/model/LogPayload/mocks/LogPayload.mock.d.ts +6 -0
- package/lib/domains/job/repository/AbstractRepository.d.ts +20 -11
- package/lib/domains/job/repository/JobContextRepository/JobContextRepository.d.ts +6 -7
- package/lib/domains/job/repository/JobExecutionRepository/JobExecutionRepository.d.ts +7 -17
- package/lib/domains/job/repository/JobExecutionRepository/JobExecutionRepository.spec.d.ts +1 -1
- package/lib/domains/job/repository/JobExecutionRepository/utils/convertDynamoDbItemToJobExecution/convertDynamoDbItemToJobExecution.d.ts +2 -0
- package/lib/domains/job/repository/JobExecutionRepository/utils/index.d.ts +1 -0
- package/lib/domains/job/repository/JobManifestRepository/JobManifestRepository.d.ts +5 -6
- package/lib/domains/job/repository/JobRepository/JobRepository.d.ts +6 -7
- package/lib/domains/job/repository/MultiTenantJobExecutionRepository/MultiTenantJobExecutionRepository.d.ts +9 -0
- package/lib/domains/job/repository/MultiTenantJobExecutionRepository/MultiTenantJobExecutionRepository.spec.d.ts +1 -0
- package/lib/domains/job/repository/index.d.ts +1 -0
- package/lib/domains/job/services/JobContextService/JobContextService.d.ts +6 -3
- package/lib/domains/job/services/JobExecutionTaskService/JobExecutionTaskService.d.ts +10 -6
- package/lib/domains/job/services/JobExecutionTaskService/JobExecutionTaskService.defs.d.ts +1 -4
- package/lib/domains/job/services/JobManifestService/JobManifestService.d.ts +0 -1
- package/lib/domains/job/services/JobStartQueueService/JobStartQueueService.d.ts +17 -0
- package/lib/domains/job/services/JobStartQueueService/JobStartQueueService.defs.d.ts +25 -0
- package/lib/domains/job/services/JobUploadService/utils/manifestValidator.d.ts +1 -2
- package/lib/domains/job/services/index.d.ts +2 -1
- package/lib/domains/manifest/services/ManifestServiceForDev.d.ts +0 -3
- package/lib/index.d.ts +1 -0
- package/lib/index.js +13 -13
- package/lib/index.js.map +4 -4
- package/lib/jest-global-setup.d.ts +6 -1
- package/lib/mocks/mockIoc.d.ts +3 -3
- package/lib/utils/buildJobRunnerModule/buildJobRunnerModule.d.ts +2 -0
- package/lib/utils/buildJobRunnerModule/buildJobRunnerModule.types.d.ts +14 -0
- package/lib/utils/index.d.ts +2 -0
- package/package.json +12 -11
- package/src/domains/component/errors/JobError.spec.ts +30 -0
- package/src/domains/component/errors/JobError.ts +8 -1
- package/src/domains/job/ParsePartitionKey/ParsePartitionKey.spec.ts +17 -8
- package/src/domains/job/ParsePartitionKey/ParsePartitionKey.ts +13 -16
- package/src/domains/job/ParsePartitionKey/ParsePartitionKey.types.ts +6 -0
- package/src/domains/job/S3Repository/JobDetailsRepository.spec.ts +47 -31
- package/src/domains/job/S3Repository/JobDetailsRepository.ts +7 -8
- package/src/domains/job/S3Repository/JobUploadsRepository.spec.ts +66 -0
- package/src/domains/job/S3Repository/JobUploadsRepository.ts +35 -0
- package/src/domains/job/S3Repository/S3RepositoryBase.spec.ts +13 -19
- package/src/domains/job/S3Repository/S3RepositoryBase.ts +24 -9
- package/src/domains/job/S3Repository/index.ts +2 -1
- package/src/domains/job/constants/InjectTokens.ts +8 -0
- package/src/domains/job/constants/JobExecutionStatus.constants.ts +1 -0
- package/src/domains/job/constants/Repository.constants.ts +3 -3
- package/src/domains/job/constants/RetryConfig.constants.ts +29 -0
- package/src/domains/job/core/pagination/services/PaginationService.spec.ts +3 -5
- package/src/domains/job/index.ts +6 -4
- package/src/domains/job/manifest/JobManifestTypes.ts +8 -6
- package/src/domains/job/manifest/mocks/JobManifest.mock.ts +2 -2
- package/src/domains/job/manifest/mocks/JobManifestDto.mock.ts +3 -2
- package/src/domains/job/manifest/mocks/JobManifestRecord.mock.ts +4 -3
- package/src/domains/job/model/Base/Base.ts +11 -5
- package/src/domains/job/model/Job/Job.ts +11 -6
- package/src/domains/job/model/Job/mocks/Job.mock.ts +2 -2
- package/src/domains/job/model/Job/mocks/JobDto.mock.ts +3 -2
- package/src/domains/job/model/Job/mocks/JobRecord.mock.ts +3 -2
- package/src/domains/job/model/JobContext/JobContext.ts +7 -5
- package/src/domains/job/model/JobContext/mocks/JobContext.mock.ts +2 -2
- package/src/domains/job/model/JobContext/mocks/JobContextDto.mock.ts +3 -2
- package/src/domains/job/model/JobContext/mocks/JobContextRecord.mock.ts +3 -2
- package/src/domains/job/model/JobExecution/JobExecution.ts +74 -9
- package/src/domains/job/model/JobExecution/mocks/JobExecution.mock.ts +7 -4
- package/src/domains/job/model/JobExecution/mocks/JobExecutionDto.mock.ts +32 -16
- package/src/domains/job/model/JobExecution/mocks/JobExecutionRecord.mock.ts +10 -4
- package/src/domains/job/model/LogPayload/LogPayload.ts +32 -11
- package/src/domains/job/model/LogPayload/index.ts +1 -0
- package/src/domains/job/model/LogPayload/mocks/LogPayload.mock.ts +21 -0
- package/src/domains/job/repository/AbstractRepository.ts +91 -59
- package/src/domains/job/repository/JobContextRepository/JobContextRepository.spec.ts +39 -49
- package/src/domains/job/repository/JobContextRepository/JobContextRepository.ts +33 -21
- package/src/domains/job/repository/JobExecutionRepository/JobExecutionRepository.spec.ts +343 -169
- package/src/domains/job/repository/JobExecutionRepository/JobExecutionRepository.ts +112 -141
- package/src/domains/job/repository/JobExecutionRepository/utils/convertDynamoDbItemToJobExecution/convertDynamoDbItemToJobExecution.ts +22 -0
- package/src/domains/job/repository/JobExecutionRepository/utils/index.ts +1 -0
- package/src/domains/job/repository/JobManifestRepository/JobManifestRepository.spec.ts +23 -38
- package/src/domains/job/repository/JobManifestRepository/JobManifestRepository.ts +29 -36
- package/src/domains/job/repository/JobRepository/JobRepository.spec.ts +72 -80
- package/src/domains/job/repository/JobRepository/JobRepository.ts +66 -70
- package/src/domains/job/repository/MultiTenantJobExecutionRepository/MultiTenantJobExecutionRepository.spec.ts +60 -0
- package/src/domains/job/repository/MultiTenantJobExecutionRepository/MultiTenantJobExecutionRepository.ts +42 -0
- package/src/domains/job/repository/index.ts +1 -0
- package/src/domains/job/services/JobContextService/JobContextService.spec.ts +73 -39
- package/src/domains/job/services/JobContextService/JobContextService.ts +37 -6
- package/src/domains/job/services/JobExecutionTaskService/JobExecutionTaskService.defs.ts +1 -4
- package/src/domains/job/services/JobExecutionTaskService/JobExecutionTaskService.spec.ts +120 -41
- package/src/domains/job/services/JobExecutionTaskService/JobExecutionTaskService.ts +95 -49
- package/src/domains/job/services/JobManifestService/JobManifestService.spec.ts +4 -41
- package/src/domains/job/services/JobManifestService/JobManifestService.ts +2 -8
- package/src/domains/job/services/JobStartQueueService/JobStartQueueService.defs.ts +27 -0
- package/src/domains/job/services/JobStartQueueService/JobStartQueueService.ts +42 -0
- package/src/domains/job/services/JobUploadService/JobUploadService.spec.ts +1 -1
- package/src/domains/job/services/JobUploadService/JobUploadService.ts +1 -1
- package/src/domains/job/services/JobUploadService/utils/manifestValidator.spec.ts +12 -25
- package/src/domains/job/services/JobUploadService/utils/manifestValidator.ts +2 -7
- package/src/domains/job/services/index.ts +2 -1
- package/src/domains/job/utils/SlackClient/SlackClient.spec.ts +0 -2
- package/src/domains/manifest/services/ManifestServiceForDev.spec.ts +1 -2
- package/src/domains/manifest/services/ManifestServiceForDev.ts +0 -4
- package/src/index.ts +1 -0
- package/src/jest-global-setup.ts +8 -4
- package/src/mocks/mockIoc.ts +18 -43
- package/src/utils/buildJobRunnerModule/buildJobRunnerModule.ts +81 -0
- package/src/utils/buildJobRunnerModule/buildJobRunnerModule.types.ts +13 -0
- package/src/utils/index.ts +2 -0
- package/lib/domains/job/S3Repository/S3Repository.d.ts +0 -15
- package/lib/domains/job/S3Repository/S3Repository.spec.d.ts +0 -5
- package/lib/domains/job/services/JobQueueService/JobQueueService.d.ts +0 -9
- package/lib/domains/job/services/JobQueueService/JobQueueService.defs.d.ts +0 -9
- package/src/custom-types.d.ts +0 -7
- package/src/domains/job/S3Repository/S3Repository.spec.ts +0 -50
- package/src/domains/job/S3Repository/S3Repository.ts +0 -27
- package/src/domains/job/services/JobQueueService/JobQueueService.defs.ts +0 -10
- package/src/domains/job/services/JobQueueService/JobQueueService.spec.ts +0 -190
- package/src/domains/job/services/JobQueueService/JobQueueService.ts +0 -60
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { AttributeValue, DynamoDBClient, QueryCommand, QueryCommandOutput } from '@aws-sdk/client-dynamodb';
|
|
2
|
+
import { JobExecutionStatus } from '../../constants/JobExecutionStatus.constants';
|
|
3
|
+
import { JobExecutionDto, JobExecutionsByTenant } from '../../model/JobExecution';
|
|
4
|
+
import { parsePartitionKey } from '../../ParsePartitionKey';
|
|
5
|
+
import { convertDynamoDbItemToJobExecution } from '../JobExecutionRepository/utils';
|
|
6
|
+
import { ENTITY_TYPES } from '../../constants/Repository.constants';
|
|
7
|
+
|
|
8
|
+
export class MultiTenantJobExecutionRepository {
|
|
9
|
+
public constructor(
|
|
10
|
+
private readonly dynamoDbClient: DynamoDBClient,
|
|
11
|
+
private readonly tableName: string,
|
|
12
|
+
) {}
|
|
13
|
+
|
|
14
|
+
public async findByStatus(status: JobExecutionStatus): Promise<JobExecutionsByTenant> {
|
|
15
|
+
const result: JobExecutionsByTenant = {};
|
|
16
|
+
let startKey: Record<string, AttributeValue> | undefined = undefined;
|
|
17
|
+
|
|
18
|
+
do {
|
|
19
|
+
const res: QueryCommandOutput = await this.dynamoDbClient.send(
|
|
20
|
+
new QueryCommand({
|
|
21
|
+
KeyConditionExpression: 'gsi1pk = :gsi1pk',
|
|
22
|
+
ExpressionAttributeValues: {
|
|
23
|
+
':gsi1pk': { S: `${ENTITY_TYPES.jobExecution}~${status}` },
|
|
24
|
+
},
|
|
25
|
+
TableName: this.tableName,
|
|
26
|
+
IndexName: 'gsi1',
|
|
27
|
+
ExclusiveStartKey: startKey,
|
|
28
|
+
}),
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
res.Items?.forEach((item) => {
|
|
32
|
+
const { tenant } = parsePartitionKey(item.pk.S || '');
|
|
33
|
+
result[tenant] ||= [];
|
|
34
|
+
result[tenant].push(convertDynamoDbItemToJobExecution(item as JobExecutionDto));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
startKey = res.LastEvaluatedKey;
|
|
38
|
+
} while (startKey);
|
|
39
|
+
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -2,4 +2,5 @@ export * from './JobContextRepository/JobContextRepository';
|
|
|
2
2
|
export * from './JobExecutionRepository/JobExecutionRepository';
|
|
3
3
|
export * from './JobManifestRepository/JobManifestRepository';
|
|
4
4
|
export * from './JobRepository/JobRepository';
|
|
5
|
+
export * from './MultiTenantJobExecutionRepository/MultiTenantJobExecutionRepository';
|
|
5
6
|
export * from './AbstractRepository';
|
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
AttributeValue,
|
|
7
7
|
DeleteItemCommandOutput,
|
|
8
8
|
DynamoDBClient,
|
|
9
|
-
DynamoDBClientConfig,
|
|
10
9
|
GetItemCommandOutput,
|
|
11
10
|
PutItemCommandOutput,
|
|
12
11
|
ScanCommandOutput,
|
|
@@ -15,7 +14,7 @@ import {
|
|
|
15
14
|
import * as PaginationUtils from '../../core/pagination/utils/PaginationUtils';
|
|
16
15
|
import { faker } from '@faker-js/faker/locale/en';
|
|
17
16
|
import { JobContextService } from './JobContextService';
|
|
18
|
-
import { PAGINATION_DIRECTION_TYPE_AFTER
|
|
17
|
+
import { PAGINATION_DIRECTION_TYPE_AFTER } from '../../core/pagination';
|
|
19
18
|
import { JobContextRepository } from '../../repository';
|
|
20
19
|
import {
|
|
21
20
|
createMockCreateJobContextRequest,
|
|
@@ -31,41 +30,45 @@ import {
|
|
|
31
30
|
createMockClientPutResponse,
|
|
32
31
|
createMockClientScanResponse,
|
|
33
32
|
} from '../../core/dynamoClient.mock';
|
|
34
|
-
import {
|
|
33
|
+
import { ENTITY_TYPES } from '../../constants/Repository.constants';
|
|
34
|
+
import { initIocContainer, iocContainer } from '../../../../mocks/mockIoc';
|
|
35
|
+
import { testLogger } from '../../../../jest-global-setup';
|
|
35
36
|
|
|
36
37
|
// Mock DynamoDB Client
|
|
37
|
-
const
|
|
38
|
-
const mockClient = new DynamoDBClient(mockOptions);
|
|
38
|
+
const mockSend = jest.spyOn(DynamoDBClient.prototype, 'send');
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
mockClient.send = mockSend;
|
|
40
|
+
initIocContainer(testLogger);
|
|
42
41
|
|
|
43
|
-
let jobContextService: JobContextService;
|
|
44
|
-
let paginationService: PaginationService<any>;
|
|
45
42
|
describe('JobContextService', (): void => {
|
|
43
|
+
let jobContextService: JobContextService;
|
|
44
|
+
|
|
46
45
|
beforeAll((): void => {
|
|
47
|
-
|
|
48
|
-
paginationService = new PaginationService();
|
|
49
|
-
jobContextService = new JobContextService(jobContextRepository, paginationService);
|
|
46
|
+
jobContextService = iocContainer.get(JobContextService);
|
|
50
47
|
});
|
|
51
48
|
|
|
52
49
|
describe(`createJobContext`, (): void => {
|
|
53
|
-
beforeEach(() => {
|
|
54
|
-
jest.resetAllMocks();
|
|
55
|
-
});
|
|
56
|
-
|
|
57
50
|
it(`should return a response for createJobContext`, async (): Promise<void> => {
|
|
58
|
-
const newJobContextToCreate = createMockCreateJobContextRequest(
|
|
51
|
+
const newJobContextToCreate = createMockCreateJobContextRequest({
|
|
52
|
+
environment: {
|
|
53
|
+
secret1: faker.string.uuid(),
|
|
54
|
+
secret2: faker.string.uuid(),
|
|
55
|
+
},
|
|
56
|
+
});
|
|
59
57
|
const environmentMap: Record<string, AttributeValue> = {};
|
|
60
58
|
for (const key of Object.keys(newJobContextToCreate.environment)) {
|
|
61
59
|
environmentMap[key] = { S: newJobContextToCreate.environment[key] };
|
|
62
60
|
}
|
|
63
61
|
const mockResponse = createMockClientPutResponse({
|
|
64
62
|
Attributes: {
|
|
65
|
-
pk: { S:
|
|
63
|
+
pk: { S: ENTITY_TYPES.context },
|
|
66
64
|
sk: { S: newJobContextToCreate.contextName },
|
|
67
65
|
name: { S: newJobContextToCreate.contextName },
|
|
68
|
-
version: {
|
|
66
|
+
version: {
|
|
67
|
+
M: {
|
|
68
|
+
secret1: { S: newJobContextToCreate.environment.secret1 },
|
|
69
|
+
secret2: { S: newJobContextToCreate.environment.secret2 },
|
|
70
|
+
},
|
|
71
|
+
},
|
|
69
72
|
},
|
|
70
73
|
});
|
|
71
74
|
|
|
@@ -75,16 +78,27 @@ describe('JobContextService', (): void => {
|
|
|
75
78
|
expect(mockSend).toBeCalledTimes(1);
|
|
76
79
|
expect(response).toStrictEqual({
|
|
77
80
|
...newJobContextToCreate,
|
|
78
|
-
type:
|
|
81
|
+
type: ENTITY_TYPES.context,
|
|
82
|
+
});
|
|
83
|
+
expect(testLogger.audit).toHaveBeenCalledWith(`Created job context ${newJobContextToCreate.contextName}`, {
|
|
84
|
+
context: expect.any(String),
|
|
85
|
+
processable: true,
|
|
86
|
+
remote: true,
|
|
87
|
+
details: {
|
|
88
|
+
context: {
|
|
89
|
+
contextName: newJobContextToCreate.contextName,
|
|
90
|
+
description: newJobContextToCreate.description,
|
|
91
|
+
environment: {
|
|
92
|
+
secret1: '*redacted*',
|
|
93
|
+
secret2: '*redacted*',
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
},
|
|
79
97
|
});
|
|
80
98
|
});
|
|
81
99
|
});
|
|
82
100
|
|
|
83
101
|
describe(`deleteJobContext`, (): void => {
|
|
84
|
-
beforeEach(() => {
|
|
85
|
-
jest.resetAllMocks();
|
|
86
|
-
});
|
|
87
|
-
|
|
88
102
|
it(`should return an empty response for job context deletion`, async (): Promise<void> => {
|
|
89
103
|
mockSend.mockImplementationOnce((): DeleteItemCommandOutput => {
|
|
90
104
|
return {
|
|
@@ -92,17 +106,32 @@ describe('JobContextService', (): void => {
|
|
|
92
106
|
};
|
|
93
107
|
});
|
|
94
108
|
|
|
95
|
-
const
|
|
109
|
+
const context = createMockJobContext({
|
|
110
|
+
environment: {
|
|
111
|
+
secret1: faker.string.uuid(),
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
const response = await jobContextService.deleteJobContext(context);
|
|
96
115
|
expect(response).toBe(undefined);
|
|
97
116
|
expect(mockSend).toBeCalledTimes(1);
|
|
117
|
+
expect(testLogger.audit).toHaveBeenCalledWith(`Deleted job context ${context.contextName}`, {
|
|
118
|
+
context: expect.any(String),
|
|
119
|
+
processable: true,
|
|
120
|
+
remote: true,
|
|
121
|
+
details: {
|
|
122
|
+
context: {
|
|
123
|
+
contextName: context.contextName,
|
|
124
|
+
description: context.description,
|
|
125
|
+
environment: {
|
|
126
|
+
secret1: '*redacted*',
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
});
|
|
98
131
|
});
|
|
99
132
|
});
|
|
100
133
|
|
|
101
134
|
describe(`findJobContext`, (): void => {
|
|
102
|
-
beforeEach(() => {
|
|
103
|
-
jest.resetAllMocks();
|
|
104
|
-
});
|
|
105
|
-
|
|
106
135
|
it(`should return job context by name`, async (): Promise<void> => {
|
|
107
136
|
const mockJobContextRecord = createMockJobContextDto({
|
|
108
137
|
environment: { key: 'value' },
|
|
@@ -140,10 +169,6 @@ describe('JobContextService', (): void => {
|
|
|
140
169
|
});
|
|
141
170
|
|
|
142
171
|
describe(`listAllJobContexts`, (): void => {
|
|
143
|
-
beforeEach(() => {
|
|
144
|
-
jest.resetAllMocks();
|
|
145
|
-
});
|
|
146
|
-
|
|
147
172
|
it(`should return an empty list`, async (): Promise<void> => {
|
|
148
173
|
mockSend.mockImplementationOnce((): ScanCommandOutput => createMockClientScanResponse());
|
|
149
174
|
|
|
@@ -202,8 +227,6 @@ describe('JobContextService', (): void => {
|
|
|
202
227
|
let getContextsSpy: any;
|
|
203
228
|
|
|
204
229
|
beforeEach(() => {
|
|
205
|
-
jest.resetAllMocks();
|
|
206
|
-
|
|
207
230
|
mockJobContexts = createMockJobContexts();
|
|
208
231
|
|
|
209
232
|
mockPaginationInfo = {
|
|
@@ -235,10 +258,6 @@ describe('JobContextService', (): void => {
|
|
|
235
258
|
});
|
|
236
259
|
|
|
237
260
|
describe(`updateJobContext`, (): void => {
|
|
238
|
-
beforeEach(() => {
|
|
239
|
-
jest.resetAllMocks();
|
|
240
|
-
});
|
|
241
|
-
|
|
242
261
|
it(`should update a job context record`, async (): Promise<void> => {
|
|
243
262
|
const jobContextToUpdate = createMockJobContext();
|
|
244
263
|
const updatedJobContext: JobContext = {
|
|
@@ -267,6 +286,21 @@ describe('JobContextService', (): void => {
|
|
|
267
286
|
);
|
|
268
287
|
expect(response).toStrictEqual(updatedJobContext);
|
|
269
288
|
expect(mockSend).toBeCalledTimes(1);
|
|
289
|
+
expect(testLogger.audit).toHaveBeenCalledWith(`Updated job context ${updatedJobContext.contextName}`, {
|
|
290
|
+
context: expect.any(String),
|
|
291
|
+
processable: true,
|
|
292
|
+
remote: true,
|
|
293
|
+
details: {
|
|
294
|
+
context: {
|
|
295
|
+
contextName: updatedJobContext.contextName,
|
|
296
|
+
description: updatedJobContext.description,
|
|
297
|
+
environment: {
|
|
298
|
+
name: '*redacted*',
|
|
299
|
+
age: '*redacted*',
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
});
|
|
270
304
|
});
|
|
271
305
|
});
|
|
272
306
|
});
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { inject } from 'inversify';
|
|
7
|
+
import { produce } from 'immer';
|
|
7
8
|
import { JobContextRepository } from '../../repository';
|
|
8
9
|
import { provideSingleton } from '../../singletonProvider';
|
|
9
10
|
import {
|
|
@@ -16,20 +17,27 @@ import {
|
|
|
16
17
|
PaginationService,
|
|
17
18
|
} from '../../core/pagination';
|
|
18
19
|
import { CreateJobContextRequest, JobContext, JobContextDto, UpdateJobContextRequest } from '../../model/JobContext';
|
|
20
|
+
import { CreateLog, Logger } from '@squiz/optimization-logger';
|
|
21
|
+
import { InjectTokens } from '../../constants/InjectTokens';
|
|
19
22
|
|
|
20
23
|
@provideSingleton(JobContextService)
|
|
21
24
|
export class JobContextService {
|
|
22
25
|
constructor(
|
|
23
26
|
@inject(JobContextRepository) protected jobContextRepository: JobContextRepository,
|
|
24
27
|
@inject(PaginationService) protected paginationService: PaginationService<JobContext>,
|
|
28
|
+
@inject(InjectTokens.Logger) protected logger: Logger,
|
|
25
29
|
) {}
|
|
26
30
|
|
|
27
31
|
public async createJobContext(input: CreateJobContextRequest): Promise<JobContext> {
|
|
28
|
-
|
|
32
|
+
const context = await this.jobContextRepository.create(input);
|
|
33
|
+
this.logContextAction(`Created job context ${context.contextName}`, context);
|
|
34
|
+
return context;
|
|
29
35
|
}
|
|
30
36
|
|
|
31
|
-
public async updateJobContext(
|
|
32
|
-
|
|
37
|
+
public async updateJobContext(contextName: string, input: UpdateJobContextRequest): Promise<JobContext | null> {
|
|
38
|
+
const context = await this.jobContextRepository.update({ contextName }, input);
|
|
39
|
+
this.logContextAction(`Updated job context ${context.contextName}`, context);
|
|
40
|
+
return context;
|
|
33
41
|
}
|
|
34
42
|
|
|
35
43
|
public async listAllJobContexts(): Promise<Array<JobContext>> {
|
|
@@ -68,10 +76,33 @@ export class JobContextService {
|
|
|
68
76
|
}
|
|
69
77
|
|
|
70
78
|
public async findJobContext(contextName: string): Promise<JobContext | null> {
|
|
71
|
-
return this.jobContextRepository.findOne(contextName);
|
|
79
|
+
return this.jobContextRepository.findOne({ contextName });
|
|
72
80
|
}
|
|
73
81
|
|
|
74
|
-
public async deleteJobContext(
|
|
75
|
-
|
|
82
|
+
public async deleteJobContext(context: JobContext): Promise<void> {
|
|
83
|
+
await this.jobContextRepository.delete({ contextName: context.contextName });
|
|
84
|
+
this.logContextAction(`Deleted job context ${context.contextName}`, context);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private logContextAction(message: string, context: JobContext) {
|
|
88
|
+
const { type: _, ...maskedContext } = produce(context, (draft) => {
|
|
89
|
+
for (const key in draft.environment) {
|
|
90
|
+
draft.environment[key] = '*redacted*';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return draft;
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const log = CreateLog.create()
|
|
97
|
+
.attachMetadata({
|
|
98
|
+
details: {
|
|
99
|
+
context: maskedContext,
|
|
100
|
+
},
|
|
101
|
+
})
|
|
102
|
+
.processable()
|
|
103
|
+
.remote()
|
|
104
|
+
.create();
|
|
105
|
+
|
|
106
|
+
this.logger.audit(...log(message));
|
|
76
107
|
}
|
|
77
108
|
}
|
|
@@ -3,9 +3,7 @@ import { JobExecution } from '../../model/JobExecution';
|
|
|
3
3
|
/**
|
|
4
4
|
* The required parameters for retrieving the number of running ECS tasks
|
|
5
5
|
*/
|
|
6
|
-
export interface
|
|
7
|
-
clusterArn: string;
|
|
8
|
-
runtimeTaskFamily: string;
|
|
6
|
+
export interface HasReachedMaxTenantConcurrencyParams {
|
|
9
7
|
maxTenantConcurrentTasks: number;
|
|
10
8
|
}
|
|
11
9
|
|
|
@@ -18,7 +16,6 @@ export interface RunJobTaskParams {
|
|
|
18
16
|
shortRegion: string;
|
|
19
17
|
shortEnvironment: string;
|
|
20
18
|
clusterArn: string;
|
|
21
|
-
subnets: Array<string>;
|
|
22
19
|
taskDefinitionArn: string;
|
|
23
20
|
tenantId: string;
|
|
24
21
|
}
|
|
@@ -1,22 +1,25 @@
|
|
|
1
|
-
import 'aws-sdk
|
|
2
|
-
import { AwsClientStub, mockClient } from 'aws-sdk-client-mock';
|
|
3
|
-
import { ECSClient, ListTasksCommand, RunTaskCommand } from '@aws-sdk/client-ecs';
|
|
1
|
+
import { ConflictException, ECSClient, ResourceNotFoundException, RunTaskCommand } from '@aws-sdk/client-ecs';
|
|
4
2
|
import { faker } from '@faker-js/faker';
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { createMockJobExecution } from '../../model/JobExecution';
|
|
10
|
-
import { createMockJobContext } from '../../model/JobContext';
|
|
3
|
+
import { AwsClientStub, mockClient } from 'aws-sdk-client-mock';
|
|
4
|
+
import 'aws-sdk-client-mock-jest';
|
|
5
|
+
import { JobDetailsRepository, JobUploadsRepository } from '../../S3Repository';
|
|
6
|
+
import { JOB_EXECUTION_STATUS } from '../../constants/JobExecutionStatus.constants';
|
|
11
7
|
import { createMockJobManifest } from '../../manifest';
|
|
8
|
+
import { createMockJobContext } from '../../model/JobContext';
|
|
9
|
+
import { createMockJobExecution } from '../../model/JobExecution';
|
|
10
|
+
import { JobExecutionRepository } from '../../repository';
|
|
11
|
+
import { JobContextService } from '../JobContextService/JobContextService';
|
|
12
|
+
import { JobManifestService } from '../JobManifestService/JobManifestService';
|
|
13
|
+
import { JobExecutionTaskService } from './JobExecutionTaskService';
|
|
12
14
|
import { RunJobTaskParams } from './JobExecutionTaskService.defs';
|
|
13
15
|
|
|
14
16
|
describe('JobExecutionTaskService', () => {
|
|
15
17
|
let mockEcsClient: AwsClientStub<ECSClient>;
|
|
16
18
|
let mockJobManifestService: jest.Mocked<JobManifestService>;
|
|
17
19
|
let mockJobContextService: jest.Mocked<JobContextService>;
|
|
20
|
+
let mockJobExecutionRepository: jest.Mocked<JobExecutionRepository>;
|
|
18
21
|
let mockJobDetailsRepository: jest.Mocked<JobDetailsRepository>;
|
|
19
|
-
let mockJobUploadRepository: jest.Mocked<
|
|
22
|
+
let mockJobUploadRepository: jest.Mocked<JobUploadsRepository>;
|
|
20
23
|
let jobExecutionTaskService: JobExecutionTaskService;
|
|
21
24
|
|
|
22
25
|
const jobDetailsSignedUrl = faker.internet.url();
|
|
@@ -27,14 +30,11 @@ describe('JobExecutionTaskService', () => {
|
|
|
27
30
|
shortRegion: 'au',
|
|
28
31
|
shortEnvironment: 'dev',
|
|
29
32
|
clusterArn: `clusterArn`,
|
|
30
|
-
subnets: ['subnet1'],
|
|
31
33
|
taskDefinitionArn: `taskDefinitionArn`,
|
|
32
34
|
tenantId: 'some_tenant',
|
|
33
35
|
};
|
|
34
36
|
|
|
35
37
|
beforeEach(() => {
|
|
36
|
-
jest.resetAllMocks();
|
|
37
|
-
|
|
38
38
|
mockEcsClient = mockClient(ECSClient);
|
|
39
39
|
mockJobManifestService = {
|
|
40
40
|
findJobManifest: jest.fn(),
|
|
@@ -42,59 +42,60 @@ describe('JobExecutionTaskService', () => {
|
|
|
42
42
|
mockJobContextService = {
|
|
43
43
|
findJobContext: jest.fn(),
|
|
44
44
|
} as unknown as jest.Mocked<JobContextService>;
|
|
45
|
+
mockJobExecutionRepository = {
|
|
46
|
+
list: jest.fn(),
|
|
47
|
+
} as unknown as jest.Mocked<JobExecutionRepository>;
|
|
45
48
|
mockJobDetailsRepository = {
|
|
46
49
|
createJobDetailsFile: jest.fn(),
|
|
47
50
|
getS3SignedUrl: jest.fn().mockResolvedValue(jobDetailsSignedUrl),
|
|
48
51
|
} as unknown as jest.Mocked<JobDetailsRepository>;
|
|
49
52
|
mockJobUploadRepository = {
|
|
50
53
|
getS3SignedUrl: jest.fn().mockResolvedValue(jobCodeArtifactSignedUrl),
|
|
51
|
-
} as unknown as jest.Mocked<
|
|
54
|
+
} as unknown as jest.Mocked<JobUploadsRepository>;
|
|
52
55
|
|
|
53
56
|
jobExecutionTaskService = new JobExecutionTaskService(
|
|
54
57
|
new ECSClient({}),
|
|
55
58
|
mockJobManifestService,
|
|
56
59
|
mockJobContextService,
|
|
60
|
+
mockJobExecutionRepository,
|
|
57
61
|
mockJobDetailsRepository,
|
|
58
62
|
mockJobUploadRepository,
|
|
59
63
|
);
|
|
60
64
|
});
|
|
61
65
|
|
|
62
|
-
describe(`getTenantRunningTasks`, () => {
|
|
63
|
-
it('should 0 if no tasks were found', async (): Promise<void> => {
|
|
64
|
-
mockEcsClient.on(ListTasksCommand).resolves({});
|
|
65
|
-
const response = await (jobExecutionTaskService as any).getTenantRunningTasks({
|
|
66
|
-
clusterArn: `clusterArn`,
|
|
67
|
-
runtimeTaskFamily: 'some_family',
|
|
68
|
-
maxTenantConcurrentTasks: 3,
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
expect(response).toBe(0);
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
|
|
75
66
|
describe(`hasReachedMaxTenantConcurrency`, () => {
|
|
76
67
|
it('should return false if number of running jobs is less than max', async (): Promise<void> => {
|
|
77
|
-
|
|
68
|
+
const maxTenantConcurrentTasks = 3;
|
|
69
|
+
|
|
70
|
+
mockJobExecutionRepository.list.mockResolvedValue([createMockJobExecution(), createMockJobExecution()]);
|
|
78
71
|
|
|
79
72
|
const response = await jobExecutionTaskService.hasReachedMaxTenantConcurrency({
|
|
80
|
-
|
|
81
|
-
runtimeTaskFamily: 'some_family',
|
|
82
|
-
maxTenantConcurrentTasks: 3,
|
|
73
|
+
maxTenantConcurrentTasks,
|
|
83
74
|
});
|
|
84
75
|
|
|
85
76
|
expect(response).toBe(false);
|
|
77
|
+
expect(mockJobExecutionRepository.list).toHaveBeenCalledWith({
|
|
78
|
+
limit: maxTenantConcurrentTasks,
|
|
79
|
+
consistentRead: true,
|
|
80
|
+
filters: { status: JOB_EXECUTION_STATUS.running },
|
|
81
|
+
});
|
|
86
82
|
});
|
|
87
83
|
|
|
88
84
|
it('should return true if number of running jobs is more than max', async (): Promise<void> => {
|
|
89
|
-
|
|
85
|
+
const maxTenantConcurrentTasks = 1;
|
|
86
|
+
|
|
87
|
+
mockJobExecutionRepository.list.mockResolvedValue([createMockJobExecution(), createMockJobExecution()]);
|
|
90
88
|
|
|
91
89
|
const response = await jobExecutionTaskService.hasReachedMaxTenantConcurrency({
|
|
92
|
-
|
|
93
|
-
runtimeTaskFamily: 'some_family',
|
|
94
|
-
maxTenantConcurrentTasks: 1,
|
|
90
|
+
maxTenantConcurrentTasks,
|
|
95
91
|
});
|
|
96
92
|
|
|
97
93
|
expect(response).toBe(true);
|
|
94
|
+
expect(mockJobExecutionRepository.list).toHaveBeenCalledWith({
|
|
95
|
+
limit: maxTenantConcurrentTasks,
|
|
96
|
+
consistentRead: true,
|
|
97
|
+
filters: { status: JOB_EXECUTION_STATUS.running },
|
|
98
|
+
});
|
|
98
99
|
});
|
|
99
100
|
});
|
|
100
101
|
|
|
@@ -105,7 +106,7 @@ describe('JobExecutionTaskService', () => {
|
|
|
105
106
|
const jobContext = createMockJobContext();
|
|
106
107
|
const jobManifest = createMockJobManifest();
|
|
107
108
|
const jobExecution = createMockJobExecution({
|
|
108
|
-
id:
|
|
109
|
+
id: `20250917T005949395Z-9602417782`,
|
|
109
110
|
jobName: 'jobName',
|
|
110
111
|
version: 'jobVersion',
|
|
111
112
|
});
|
|
@@ -122,8 +123,6 @@ describe('JobExecutionTaskService', () => {
|
|
|
122
123
|
expect(response).toBe(mockArn);
|
|
123
124
|
expect(mockEcsClient).toHaveReceivedCommandWith(RunTaskCommand, {
|
|
124
125
|
cluster: 'clusterArn',
|
|
125
|
-
launchType: 'FARGATE',
|
|
126
|
-
networkConfiguration: { awsvpcConfiguration: { subnets: ['subnet1'] } },
|
|
127
126
|
overrides: {
|
|
128
127
|
containerOverrides: [
|
|
129
128
|
{
|
|
@@ -175,15 +174,17 @@ describe('JobExecutionTaskService', () => {
|
|
|
175
174
|
{ key: 'cmp:tenant', value: 'some_tenant' },
|
|
176
175
|
],
|
|
177
176
|
taskDefinition: defaultRunJobTaskParams.taskDefinitionArn,
|
|
177
|
+
// sha1 of execution job name/id + tenant + retryCount.
|
|
178
|
+
clientToken: '4725755e08138edde031a177c36a269ecdbed5a3',
|
|
178
179
|
});
|
|
179
180
|
expect(mockJobDetailsRepository.createJobDetailsFile).toHaveBeenCalledWith(
|
|
180
|
-
|
|
181
|
+
defaultRunJobTaskParams.tenantId,
|
|
181
182
|
jobContext,
|
|
182
183
|
jobManifest,
|
|
183
184
|
jobExecution,
|
|
184
185
|
);
|
|
185
186
|
expect(mockJobUploadRepository.getS3SignedUrl).toHaveBeenCalledWith(
|
|
186
|
-
`${jobExecution.jobName}/${jobExecution.version}/export.zip`,
|
|
187
|
+
`${defaultRunJobTaskParams.tenantId}/${jobExecution.jobName}/${jobExecution.version}/export.zip`,
|
|
187
188
|
);
|
|
188
189
|
});
|
|
189
190
|
|
|
@@ -201,11 +202,89 @@ describe('JobExecutionTaskService', () => {
|
|
|
201
202
|
await jobExecutionTaskService.runJobExecutionTask(defaultRunJobTaskParams);
|
|
202
203
|
|
|
203
204
|
expect(mockJobDetailsRepository.createJobDetailsFile).toHaveBeenCalledWith(
|
|
204
|
-
|
|
205
|
+
defaultRunJobTaskParams.tenantId,
|
|
205
206
|
null,
|
|
206
207
|
null,
|
|
207
208
|
defaultRunJobTaskParams.jobExecution,
|
|
208
209
|
);
|
|
209
210
|
});
|
|
211
|
+
|
|
212
|
+
it('should handle ConflictException with a previously started task arn', async () => {
|
|
213
|
+
const mockArn = `mock-arn`;
|
|
214
|
+
const jobContext = createMockJobContext();
|
|
215
|
+
const jobManifest = createMockJobManifest();
|
|
216
|
+
const jobExecution = createMockJobExecution({
|
|
217
|
+
id: `20250917T005949395Z-9602417782`,
|
|
218
|
+
jobName: 'jobName',
|
|
219
|
+
version: 'jobVersion',
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
mockJobContextService.findJobContext.mockResolvedValue(jobContext);
|
|
223
|
+
mockJobManifestService.findJobManifest.mockResolvedValue(jobManifest);
|
|
224
|
+
mockEcsClient.on(RunTaskCommand).rejects(
|
|
225
|
+
new ConflictException({
|
|
226
|
+
message: 'The RunTask request could not be processed due to conflicts.',
|
|
227
|
+
resourceIds: [mockArn],
|
|
228
|
+
$metadata: {},
|
|
229
|
+
}),
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
const response = await jobExecutionTaskService.runJobExecutionTask({
|
|
233
|
+
...defaultRunJobTaskParams,
|
|
234
|
+
jobExecution: jobExecution,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
expect(response).toBe(mockArn);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('should handle ConflictException with no previously started task arn', async () => {
|
|
241
|
+
const jobContext = createMockJobContext();
|
|
242
|
+
const jobManifest = createMockJobManifest();
|
|
243
|
+
const jobExecution = createMockJobExecution({
|
|
244
|
+
id: `20250917T005949395Z-9602417782`,
|
|
245
|
+
jobName: 'jobName',
|
|
246
|
+
version: 'jobVersion',
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
mockJobContextService.findJobContext.mockResolvedValue(jobContext);
|
|
250
|
+
mockJobManifestService.findJobManifest.mockResolvedValue(jobManifest);
|
|
251
|
+
mockEcsClient.on(RunTaskCommand).rejects(
|
|
252
|
+
new ConflictException({
|
|
253
|
+
message: 'The RunTask request could not be processed due to conflicts.',
|
|
254
|
+
resourceIds: [],
|
|
255
|
+
$metadata: {},
|
|
256
|
+
}),
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
const response = await jobExecutionTaskService.runJobExecutionTask({
|
|
260
|
+
...defaultRunJobTaskParams,
|
|
261
|
+
jobExecution: jobExecution,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// A ConflictException may be thrown without returning the ARN of the task.
|
|
265
|
+
expect(response).toBe(undefined);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('should re-throw unhandled errors when starting a task', async () => {
|
|
269
|
+
const expectedError = new ResourceNotFoundException({ message: faker.lorem.words(), $metadata: {} });
|
|
270
|
+
const jobContext = createMockJobContext();
|
|
271
|
+
const jobManifest = createMockJobManifest();
|
|
272
|
+
const jobExecution = createMockJobExecution({
|
|
273
|
+
id: `20250917T005949395Z-9602417782`,
|
|
274
|
+
jobName: 'jobName',
|
|
275
|
+
version: 'jobVersion',
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
mockJobContextService.findJobContext.mockResolvedValue(jobContext);
|
|
279
|
+
mockJobManifestService.findJobManifest.mockResolvedValue(jobManifest);
|
|
280
|
+
mockEcsClient.on(RunTaskCommand).rejects(expectedError);
|
|
281
|
+
|
|
282
|
+
await expect(
|
|
283
|
+
jobExecutionTaskService.runJobExecutionTask({
|
|
284
|
+
...defaultRunJobTaskParams,
|
|
285
|
+
jobExecution: jobExecution,
|
|
286
|
+
}),
|
|
287
|
+
).rejects.toThrow(expectedError);
|
|
288
|
+
});
|
|
210
289
|
});
|
|
211
290
|
});
|