@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.
Files changed (137) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/lib/domains/component/errors/JobError.spec.d.ts +1 -0
  3. package/lib/domains/job/ParsePartitionKey/ParsePartitionKey.d.ts +2 -2
  4. package/lib/domains/job/ParsePartitionKey/ParsePartitionKey.types.d.ts +5 -0
  5. package/lib/domains/job/S3Repository/JobDetailsRepository.d.ts +1 -2
  6. package/lib/domains/job/S3Repository/JobDetailsRepository.spec.d.ts +1 -1
  7. package/lib/domains/job/S3Repository/JobUploadsRepository.d.ts +16 -0
  8. package/lib/domains/job/{services/JobQueueService/JobQueueService.spec.d.ts → S3Repository/JobUploadsRepository.spec.d.ts} +1 -1
  9. package/lib/domains/job/S3Repository/S3RepositoryBase.d.ts +12 -5
  10. package/lib/domains/job/S3Repository/index.d.ts +2 -1
  11. package/lib/domains/job/constants/InjectTokens.d.ts +8 -0
  12. package/lib/domains/job/constants/JobExecutionStatus.constants.d.ts +2 -1
  13. package/lib/domains/job/constants/Repository.constants.d.ts +3 -3
  14. package/lib/domains/job/constants/RetryConfig.constants.d.ts +21 -0
  15. package/lib/domains/job/index.d.ts +6 -4
  16. package/lib/domains/job/manifest/JobManifestTypes.d.ts +7 -6
  17. package/lib/domains/job/model/Base/Base.d.ts +10 -5
  18. package/lib/domains/job/model/Job/Job.d.ts +9 -6
  19. package/lib/domains/job/model/JobContext/JobContext.d.ts +6 -5
  20. package/lib/domains/job/model/JobExecution/JobExecution.d.ts +64 -9
  21. package/lib/domains/job/model/LogPayload/LogPayload.d.ts +21 -12
  22. package/lib/domains/job/model/LogPayload/index.d.ts +1 -0
  23. package/lib/domains/job/model/LogPayload/mocks/LogPayload.mock.d.ts +6 -0
  24. package/lib/domains/job/repository/AbstractRepository.d.ts +20 -11
  25. package/lib/domains/job/repository/JobContextRepository/JobContextRepository.d.ts +6 -7
  26. package/lib/domains/job/repository/JobExecutionRepository/JobExecutionRepository.d.ts +7 -17
  27. package/lib/domains/job/repository/JobExecutionRepository/JobExecutionRepository.spec.d.ts +1 -1
  28. package/lib/domains/job/repository/JobExecutionRepository/utils/convertDynamoDbItemToJobExecution/convertDynamoDbItemToJobExecution.d.ts +2 -0
  29. package/lib/domains/job/repository/JobExecutionRepository/utils/index.d.ts +1 -0
  30. package/lib/domains/job/repository/JobManifestRepository/JobManifestRepository.d.ts +5 -6
  31. package/lib/domains/job/repository/JobRepository/JobRepository.d.ts +6 -7
  32. package/lib/domains/job/repository/MultiTenantJobExecutionRepository/MultiTenantJobExecutionRepository.d.ts +9 -0
  33. package/lib/domains/job/repository/MultiTenantJobExecutionRepository/MultiTenantJobExecutionRepository.spec.d.ts +1 -0
  34. package/lib/domains/job/repository/index.d.ts +1 -0
  35. package/lib/domains/job/services/JobContextService/JobContextService.d.ts +6 -3
  36. package/lib/domains/job/services/JobExecutionTaskService/JobExecutionTaskService.d.ts +10 -6
  37. package/lib/domains/job/services/JobExecutionTaskService/JobExecutionTaskService.defs.d.ts +1 -4
  38. package/lib/domains/job/services/JobManifestService/JobManifestService.d.ts +0 -1
  39. package/lib/domains/job/services/JobStartQueueService/JobStartQueueService.d.ts +17 -0
  40. package/lib/domains/job/services/JobStartQueueService/JobStartQueueService.defs.d.ts +25 -0
  41. package/lib/domains/job/services/JobUploadService/utils/manifestValidator.d.ts +1 -2
  42. package/lib/domains/job/services/index.d.ts +2 -1
  43. package/lib/domains/manifest/services/ManifestServiceForDev.d.ts +0 -3
  44. package/lib/index.d.ts +1 -0
  45. package/lib/index.js +13 -13
  46. package/lib/index.js.map +4 -4
  47. package/lib/jest-global-setup.d.ts +6 -1
  48. package/lib/mocks/mockIoc.d.ts +3 -3
  49. package/lib/utils/buildJobRunnerModule/buildJobRunnerModule.d.ts +2 -0
  50. package/lib/utils/buildJobRunnerModule/buildJobRunnerModule.types.d.ts +14 -0
  51. package/lib/utils/index.d.ts +2 -0
  52. package/package.json +12 -11
  53. package/src/domains/component/errors/JobError.spec.ts +30 -0
  54. package/src/domains/component/errors/JobError.ts +8 -1
  55. package/src/domains/job/ParsePartitionKey/ParsePartitionKey.spec.ts +17 -8
  56. package/src/domains/job/ParsePartitionKey/ParsePartitionKey.ts +13 -16
  57. package/src/domains/job/ParsePartitionKey/ParsePartitionKey.types.ts +6 -0
  58. package/src/domains/job/S3Repository/JobDetailsRepository.spec.ts +47 -31
  59. package/src/domains/job/S3Repository/JobDetailsRepository.ts +7 -8
  60. package/src/domains/job/S3Repository/JobUploadsRepository.spec.ts +66 -0
  61. package/src/domains/job/S3Repository/JobUploadsRepository.ts +35 -0
  62. package/src/domains/job/S3Repository/S3RepositoryBase.spec.ts +13 -19
  63. package/src/domains/job/S3Repository/S3RepositoryBase.ts +24 -9
  64. package/src/domains/job/S3Repository/index.ts +2 -1
  65. package/src/domains/job/constants/InjectTokens.ts +8 -0
  66. package/src/domains/job/constants/JobExecutionStatus.constants.ts +1 -0
  67. package/src/domains/job/constants/Repository.constants.ts +3 -3
  68. package/src/domains/job/constants/RetryConfig.constants.ts +29 -0
  69. package/src/domains/job/core/pagination/services/PaginationService.spec.ts +3 -5
  70. package/src/domains/job/index.ts +6 -4
  71. package/src/domains/job/manifest/JobManifestTypes.ts +8 -6
  72. package/src/domains/job/manifest/mocks/JobManifest.mock.ts +2 -2
  73. package/src/domains/job/manifest/mocks/JobManifestDto.mock.ts +3 -2
  74. package/src/domains/job/manifest/mocks/JobManifestRecord.mock.ts +4 -3
  75. package/src/domains/job/model/Base/Base.ts +11 -5
  76. package/src/domains/job/model/Job/Job.ts +11 -6
  77. package/src/domains/job/model/Job/mocks/Job.mock.ts +2 -2
  78. package/src/domains/job/model/Job/mocks/JobDto.mock.ts +3 -2
  79. package/src/domains/job/model/Job/mocks/JobRecord.mock.ts +3 -2
  80. package/src/domains/job/model/JobContext/JobContext.ts +7 -5
  81. package/src/domains/job/model/JobContext/mocks/JobContext.mock.ts +2 -2
  82. package/src/domains/job/model/JobContext/mocks/JobContextDto.mock.ts +3 -2
  83. package/src/domains/job/model/JobContext/mocks/JobContextRecord.mock.ts +3 -2
  84. package/src/domains/job/model/JobExecution/JobExecution.ts +74 -9
  85. package/src/domains/job/model/JobExecution/mocks/JobExecution.mock.ts +7 -4
  86. package/src/domains/job/model/JobExecution/mocks/JobExecutionDto.mock.ts +32 -16
  87. package/src/domains/job/model/JobExecution/mocks/JobExecutionRecord.mock.ts +10 -4
  88. package/src/domains/job/model/LogPayload/LogPayload.ts +32 -11
  89. package/src/domains/job/model/LogPayload/index.ts +1 -0
  90. package/src/domains/job/model/LogPayload/mocks/LogPayload.mock.ts +21 -0
  91. package/src/domains/job/repository/AbstractRepository.ts +91 -59
  92. package/src/domains/job/repository/JobContextRepository/JobContextRepository.spec.ts +39 -49
  93. package/src/domains/job/repository/JobContextRepository/JobContextRepository.ts +33 -21
  94. package/src/domains/job/repository/JobExecutionRepository/JobExecutionRepository.spec.ts +343 -169
  95. package/src/domains/job/repository/JobExecutionRepository/JobExecutionRepository.ts +112 -141
  96. package/src/domains/job/repository/JobExecutionRepository/utils/convertDynamoDbItemToJobExecution/convertDynamoDbItemToJobExecution.ts +22 -0
  97. package/src/domains/job/repository/JobExecutionRepository/utils/index.ts +1 -0
  98. package/src/domains/job/repository/JobManifestRepository/JobManifestRepository.spec.ts +23 -38
  99. package/src/domains/job/repository/JobManifestRepository/JobManifestRepository.ts +29 -36
  100. package/src/domains/job/repository/JobRepository/JobRepository.spec.ts +72 -80
  101. package/src/domains/job/repository/JobRepository/JobRepository.ts +66 -70
  102. package/src/domains/job/repository/MultiTenantJobExecutionRepository/MultiTenantJobExecutionRepository.spec.ts +60 -0
  103. package/src/domains/job/repository/MultiTenantJobExecutionRepository/MultiTenantJobExecutionRepository.ts +42 -0
  104. package/src/domains/job/repository/index.ts +1 -0
  105. package/src/domains/job/services/JobContextService/JobContextService.spec.ts +73 -39
  106. package/src/domains/job/services/JobContextService/JobContextService.ts +37 -6
  107. package/src/domains/job/services/JobExecutionTaskService/JobExecutionTaskService.defs.ts +1 -4
  108. package/src/domains/job/services/JobExecutionTaskService/JobExecutionTaskService.spec.ts +120 -41
  109. package/src/domains/job/services/JobExecutionTaskService/JobExecutionTaskService.ts +95 -49
  110. package/src/domains/job/services/JobManifestService/JobManifestService.spec.ts +4 -41
  111. package/src/domains/job/services/JobManifestService/JobManifestService.ts +2 -8
  112. package/src/domains/job/services/JobStartQueueService/JobStartQueueService.defs.ts +27 -0
  113. package/src/domains/job/services/JobStartQueueService/JobStartQueueService.ts +42 -0
  114. package/src/domains/job/services/JobUploadService/JobUploadService.spec.ts +1 -1
  115. package/src/domains/job/services/JobUploadService/JobUploadService.ts +1 -1
  116. package/src/domains/job/services/JobUploadService/utils/manifestValidator.spec.ts +12 -25
  117. package/src/domains/job/services/JobUploadService/utils/manifestValidator.ts +2 -7
  118. package/src/domains/job/services/index.ts +2 -1
  119. package/src/domains/job/utils/SlackClient/SlackClient.spec.ts +0 -2
  120. package/src/domains/manifest/services/ManifestServiceForDev.spec.ts +1 -2
  121. package/src/domains/manifest/services/ManifestServiceForDev.ts +0 -4
  122. package/src/index.ts +1 -0
  123. package/src/jest-global-setup.ts +8 -4
  124. package/src/mocks/mockIoc.ts +18 -43
  125. package/src/utils/buildJobRunnerModule/buildJobRunnerModule.ts +81 -0
  126. package/src/utils/buildJobRunnerModule/buildJobRunnerModule.types.ts +13 -0
  127. package/src/utils/index.ts +2 -0
  128. package/lib/domains/job/S3Repository/S3Repository.d.ts +0 -15
  129. package/lib/domains/job/S3Repository/S3Repository.spec.d.ts +0 -5
  130. package/lib/domains/job/services/JobQueueService/JobQueueService.d.ts +0 -9
  131. package/lib/domains/job/services/JobQueueService/JobQueueService.defs.d.ts +0 -9
  132. package/src/custom-types.d.ts +0 -7
  133. package/src/domains/job/S3Repository/S3Repository.spec.ts +0 -50
  134. package/src/domains/job/S3Repository/S3Repository.ts +0 -27
  135. package/src/domains/job/services/JobQueueService/JobQueueService.defs.ts +0 -10
  136. package/src/domains/job/services/JobQueueService/JobQueueService.spec.ts +0 -190
  137. 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, PaginationService } from '../../core/pagination';
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 { PARTITION_KEYS } from '../../constants/Repository.constants';
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 mockOptions = {} as DynamoDBClientConfig;
38
- const mockClient = new DynamoDBClient(mockOptions);
38
+ const mockSend = jest.spyOn(DynamoDBClient.prototype, 'send');
39
39
 
40
- const mockSend = jest.fn();
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
- const jobContextRepository = new JobContextRepository(mockClient);
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: PARTITION_KEYS.context },
63
+ pk: { S: ENTITY_TYPES.context },
66
64
  sk: { S: newJobContextToCreate.contextName },
67
65
  name: { S: newJobContextToCreate.contextName },
68
- version: { M: environmentMap },
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: PARTITION_KEYS.context,
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 response = await jobContextService.deleteJobContext(`name`);
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
- return this.jobContextRepository.create(input);
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(name: string, input: UpdateJobContextRequest): Promise<JobContext | null> {
32
- return this.jobContextRepository.update(name, input);
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(contextName: string): Promise<void> {
75
- return await this.jobContextRepository.delete(contextName);
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 ListRunningTasksParams {
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-client-mock-jest';
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 { JobExecutionTaskService } from './JobExecutionTaskService';
6
- import { JobManifestService } from '../JobManifestService/JobManifestService';
7
- import { JobContextService } from '../JobContextService/JobContextService';
8
- import { JobDetailsRepository, S3Repository } from '../../S3Repository';
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<S3Repository>;
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<S3Repository>;
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
- mockEcsClient.on(ListTasksCommand).resolves({ taskArns: ['12345', '67890'] });
68
+ const maxTenantConcurrentTasks = 3;
69
+
70
+ mockJobExecutionRepository.list.mockResolvedValue([createMockJobExecution(), createMockJobExecution()]);
78
71
 
79
72
  const response = await jobExecutionTaskService.hasReachedMaxTenantConcurrency({
80
- clusterArn: `clusterArn`,
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
- mockEcsClient.on(ListTasksCommand).resolves({ taskArns: ['12345', '67890'] });
85
+ const maxTenantConcurrentTasks = 1;
86
+
87
+ mockJobExecutionRepository.list.mockResolvedValue([createMockJobExecution(), createMockJobExecution()]);
90
88
 
91
89
  const response = await jobExecutionTaskService.hasReachedMaxTenantConcurrency({
92
- clusterArn: `clusterArn`,
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: `${new Date().toISOString()}-jobExecutionId`,
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
- `${defaultRunJobTaskParams.tenantId}/${jobExecution.id}`,
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
- `${defaultRunJobTaskParams.tenantId}/${defaultRunJobTaskParams.jobExecution.id}`,
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
  });