@squiz/job-runner-lib 2.3.0 → 2.4.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 +6 -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 -3
- 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 -3
- package/src/domains/job/services/JobExecutionTaskService/JobExecutionTaskService.spec.ts +113 -31
- package/src/domains/job/services/JobExecutionTaskService/JobExecutionTaskService.ts +101 -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
|
|
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
import 'aws-sdk-client-mock-jest';
|
|
2
2
|
import { AwsClientStub, mockClient } from 'aws-sdk-client-mock';
|
|
3
|
-
import {
|
|
3
|
+
import { ConflictException, ResourceNotFoundException, ECSClient, RunTaskCommand } from '@aws-sdk/client-ecs';
|
|
4
4
|
import { faker } from '@faker-js/faker';
|
|
5
5
|
import { JobExecutionTaskService } from './JobExecutionTaskService';
|
|
6
6
|
import { JobManifestService } from '../JobManifestService/JobManifestService';
|
|
7
7
|
import { JobContextService } from '../JobContextService/JobContextService';
|
|
8
|
-
import { JobDetailsRepository,
|
|
8
|
+
import { JobDetailsRepository, JobUploadsRepository } from '../../S3Repository';
|
|
9
9
|
import { createMockJobExecution } from '../../model/JobExecution';
|
|
10
10
|
import { createMockJobContext } from '../../model/JobContext';
|
|
11
11
|
import { createMockJobManifest } from '../../manifest';
|
|
12
12
|
import { RunJobTaskParams } from './JobExecutionTaskService.defs';
|
|
13
|
+
import { JobExecutionRepository } from '../../repository';
|
|
14
|
+
import { JOB_EXECUTION_STATUS } from '../../constants/JobExecutionStatus.constants';
|
|
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();
|
|
@@ -33,8 +36,6 @@ describe('JobExecutionTaskService', () => {
|
|
|
33
36
|
};
|
|
34
37
|
|
|
35
38
|
beforeEach(() => {
|
|
36
|
-
jest.resetAllMocks();
|
|
37
|
-
|
|
38
39
|
mockEcsClient = mockClient(ECSClient);
|
|
39
40
|
mockJobManifestService = {
|
|
40
41
|
findJobManifest: jest.fn(),
|
|
@@ -42,59 +43,60 @@ describe('JobExecutionTaskService', () => {
|
|
|
42
43
|
mockJobContextService = {
|
|
43
44
|
findJobContext: jest.fn(),
|
|
44
45
|
} as unknown as jest.Mocked<JobContextService>;
|
|
46
|
+
mockJobExecutionRepository = {
|
|
47
|
+
list: jest.fn(),
|
|
48
|
+
} as unknown as jest.Mocked<JobExecutionRepository>;
|
|
45
49
|
mockJobDetailsRepository = {
|
|
46
50
|
createJobDetailsFile: jest.fn(),
|
|
47
51
|
getS3SignedUrl: jest.fn().mockResolvedValue(jobDetailsSignedUrl),
|
|
48
52
|
} as unknown as jest.Mocked<JobDetailsRepository>;
|
|
49
53
|
mockJobUploadRepository = {
|
|
50
54
|
getS3SignedUrl: jest.fn().mockResolvedValue(jobCodeArtifactSignedUrl),
|
|
51
|
-
} as unknown as jest.Mocked<
|
|
55
|
+
} as unknown as jest.Mocked<JobUploadsRepository>;
|
|
52
56
|
|
|
53
57
|
jobExecutionTaskService = new JobExecutionTaskService(
|
|
54
58
|
new ECSClient({}),
|
|
55
59
|
mockJobManifestService,
|
|
56
60
|
mockJobContextService,
|
|
61
|
+
mockJobExecutionRepository,
|
|
57
62
|
mockJobDetailsRepository,
|
|
58
63
|
mockJobUploadRepository,
|
|
59
64
|
);
|
|
60
65
|
});
|
|
61
66
|
|
|
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
67
|
describe(`hasReachedMaxTenantConcurrency`, () => {
|
|
76
68
|
it('should return false if number of running jobs is less than max', async (): Promise<void> => {
|
|
77
|
-
|
|
69
|
+
const maxTenantConcurrentTasks = 3;
|
|
70
|
+
|
|
71
|
+
mockJobExecutionRepository.list.mockResolvedValue([createMockJobExecution(), createMockJobExecution()]);
|
|
78
72
|
|
|
79
73
|
const response = await jobExecutionTaskService.hasReachedMaxTenantConcurrency({
|
|
80
|
-
|
|
81
|
-
runtimeTaskFamily: 'some_family',
|
|
82
|
-
maxTenantConcurrentTasks: 3,
|
|
74
|
+
maxTenantConcurrentTasks,
|
|
83
75
|
});
|
|
84
76
|
|
|
85
77
|
expect(response).toBe(false);
|
|
78
|
+
expect(mockJobExecutionRepository.list).toHaveBeenCalledWith({
|
|
79
|
+
limit: maxTenantConcurrentTasks,
|
|
80
|
+
consistentRead: true,
|
|
81
|
+
filters: { status: JOB_EXECUTION_STATUS.running },
|
|
82
|
+
});
|
|
86
83
|
});
|
|
87
84
|
|
|
88
85
|
it('should return true if number of running jobs is more than max', async (): Promise<void> => {
|
|
89
|
-
|
|
86
|
+
const maxTenantConcurrentTasks = 1;
|
|
87
|
+
|
|
88
|
+
mockJobExecutionRepository.list.mockResolvedValue([createMockJobExecution(), createMockJobExecution()]);
|
|
90
89
|
|
|
91
90
|
const response = await jobExecutionTaskService.hasReachedMaxTenantConcurrency({
|
|
92
|
-
|
|
93
|
-
runtimeTaskFamily: 'some_family',
|
|
94
|
-
maxTenantConcurrentTasks: 1,
|
|
91
|
+
maxTenantConcurrentTasks,
|
|
95
92
|
});
|
|
96
93
|
|
|
97
94
|
expect(response).toBe(true);
|
|
95
|
+
expect(mockJobExecutionRepository.list).toHaveBeenCalledWith({
|
|
96
|
+
limit: maxTenantConcurrentTasks,
|
|
97
|
+
consistentRead: true,
|
|
98
|
+
filters: { status: JOB_EXECUTION_STATUS.running },
|
|
99
|
+
});
|
|
98
100
|
});
|
|
99
101
|
});
|
|
100
102
|
|
|
@@ -105,7 +107,7 @@ describe('JobExecutionTaskService', () => {
|
|
|
105
107
|
const jobContext = createMockJobContext();
|
|
106
108
|
const jobManifest = createMockJobManifest();
|
|
107
109
|
const jobExecution = createMockJobExecution({
|
|
108
|
-
id:
|
|
110
|
+
id: `20250917T005949395Z-9602417782`,
|
|
109
111
|
jobName: 'jobName',
|
|
110
112
|
version: 'jobVersion',
|
|
111
113
|
});
|
|
@@ -175,15 +177,17 @@ describe('JobExecutionTaskService', () => {
|
|
|
175
177
|
{ key: 'cmp:tenant', value: 'some_tenant' },
|
|
176
178
|
],
|
|
177
179
|
taskDefinition: defaultRunJobTaskParams.taskDefinitionArn,
|
|
180
|
+
// sha1 of execution job name/id + tenant + retryCount.
|
|
181
|
+
clientToken: '4725755e08138edde031a177c36a269ecdbed5a3',
|
|
178
182
|
});
|
|
179
183
|
expect(mockJobDetailsRepository.createJobDetailsFile).toHaveBeenCalledWith(
|
|
180
|
-
|
|
184
|
+
defaultRunJobTaskParams.tenantId,
|
|
181
185
|
jobContext,
|
|
182
186
|
jobManifest,
|
|
183
187
|
jobExecution,
|
|
184
188
|
);
|
|
185
189
|
expect(mockJobUploadRepository.getS3SignedUrl).toHaveBeenCalledWith(
|
|
186
|
-
`${jobExecution.jobName}/${jobExecution.version}/export.zip`,
|
|
190
|
+
`${defaultRunJobTaskParams.tenantId}/${jobExecution.jobName}/${jobExecution.version}/export.zip`,
|
|
187
191
|
);
|
|
188
192
|
});
|
|
189
193
|
|
|
@@ -201,11 +205,89 @@ describe('JobExecutionTaskService', () => {
|
|
|
201
205
|
await jobExecutionTaskService.runJobExecutionTask(defaultRunJobTaskParams);
|
|
202
206
|
|
|
203
207
|
expect(mockJobDetailsRepository.createJobDetailsFile).toHaveBeenCalledWith(
|
|
204
|
-
|
|
208
|
+
defaultRunJobTaskParams.tenantId,
|
|
205
209
|
null,
|
|
206
210
|
null,
|
|
207
211
|
defaultRunJobTaskParams.jobExecution,
|
|
208
212
|
);
|
|
209
213
|
});
|
|
214
|
+
|
|
215
|
+
it('should handle ConflictException with a previously started task arn', async () => {
|
|
216
|
+
const mockArn = `mock-arn`;
|
|
217
|
+
const jobContext = createMockJobContext();
|
|
218
|
+
const jobManifest = createMockJobManifest();
|
|
219
|
+
const jobExecution = createMockJobExecution({
|
|
220
|
+
id: `20250917T005949395Z-9602417782`,
|
|
221
|
+
jobName: 'jobName',
|
|
222
|
+
version: 'jobVersion',
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
mockJobContextService.findJobContext.mockResolvedValue(jobContext);
|
|
226
|
+
mockJobManifestService.findJobManifest.mockResolvedValue(jobManifest);
|
|
227
|
+
mockEcsClient.on(RunTaskCommand).rejects(
|
|
228
|
+
new ConflictException({
|
|
229
|
+
message: 'The RunTask request could not be processed due to conflicts.',
|
|
230
|
+
resourceIds: [mockArn],
|
|
231
|
+
$metadata: {},
|
|
232
|
+
}),
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
const response = await jobExecutionTaskService.runJobExecutionTask({
|
|
236
|
+
...defaultRunJobTaskParams,
|
|
237
|
+
jobExecution: jobExecution,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
expect(response).toBe(mockArn);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('should handle ConflictException with no previously started task arn', async () => {
|
|
244
|
+
const jobContext = createMockJobContext();
|
|
245
|
+
const jobManifest = createMockJobManifest();
|
|
246
|
+
const jobExecution = createMockJobExecution({
|
|
247
|
+
id: `20250917T005949395Z-9602417782`,
|
|
248
|
+
jobName: 'jobName',
|
|
249
|
+
version: 'jobVersion',
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
mockJobContextService.findJobContext.mockResolvedValue(jobContext);
|
|
253
|
+
mockJobManifestService.findJobManifest.mockResolvedValue(jobManifest);
|
|
254
|
+
mockEcsClient.on(RunTaskCommand).rejects(
|
|
255
|
+
new ConflictException({
|
|
256
|
+
message: 'The RunTask request could not be processed due to conflicts.',
|
|
257
|
+
resourceIds: [],
|
|
258
|
+
$metadata: {},
|
|
259
|
+
}),
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
const response = await jobExecutionTaskService.runJobExecutionTask({
|
|
263
|
+
...defaultRunJobTaskParams,
|
|
264
|
+
jobExecution: jobExecution,
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// A ConflictException may be thrown without returning the ARN of the task.
|
|
268
|
+
expect(response).toBe(undefined);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('should re-throw unhandled errors when starting a task', async () => {
|
|
272
|
+
const expectedError = new ResourceNotFoundException({ message: faker.lorem.words(), $metadata: {} });
|
|
273
|
+
const jobContext = createMockJobContext();
|
|
274
|
+
const jobManifest = createMockJobManifest();
|
|
275
|
+
const jobExecution = createMockJobExecution({
|
|
276
|
+
id: `20250917T005949395Z-9602417782`,
|
|
277
|
+
jobName: 'jobName',
|
|
278
|
+
version: 'jobVersion',
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
mockJobContextService.findJobContext.mockResolvedValue(jobContext);
|
|
282
|
+
mockJobManifestService.findJobManifest.mockResolvedValue(jobManifest);
|
|
283
|
+
mockEcsClient.on(RunTaskCommand).rejects(expectedError);
|
|
284
|
+
|
|
285
|
+
await expect(
|
|
286
|
+
jobExecutionTaskService.runJobExecutionTask({
|
|
287
|
+
...defaultRunJobTaskParams,
|
|
288
|
+
jobExecution: jobExecution,
|
|
289
|
+
}),
|
|
290
|
+
).rejects.toThrow(expectedError);
|
|
291
|
+
});
|
|
210
292
|
});
|
|
211
293
|
});
|