@fjall/components-infrastructure 39.0.5 → 39.0.7
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/dist/lib/layers/secrets-resolver/bin/resolve-secrets.mjs +10 -1
- package/dist/lib/patterns/aws/computeEcs.d.ts +65 -21
- package/dist/lib/patterns/aws/computeEcs.js +243 -95
- package/dist/lib/patterns/aws/computeEcsTypes.d.ts +49 -25
- package/dist/lib/resources/aws/compute/ecs.d.ts +3 -0
- package/dist/lib/resources/aws/compute/ecs.js +4 -0
- package/dist/lib/resources/aws/compute/ecsLifecycleHookMigration.d.ts +32 -62
- package/dist/lib/resources/aws/compute/ecsLifecycleHookMigration.js +96 -50
- package/dist/lib/resources/aws/compute/ecsRolloutBarrier.d.ts +61 -0
- package/dist/lib/resources/aws/compute/ecsRolloutBarrier.js +185 -0
- package/dist/lib/resources/aws/compute/ecsRolloutBarrierLambda.source.cjs +475 -0
- package/dist/lib/resources/aws/compute/ecsTaskDefinition.d.ts +19 -8
- package/dist/lib/resources/aws/compute/ecsTaskDefinition.js +16 -7
- package/dist/lib/resources/aws/compute/ecsValidation.d.ts +3 -3
- package/dist/lib/resources/aws/compute/ecsValidation.js +3 -3
- package/dist/lib/resources/aws/compute/lambda.d.ts +6 -0
- package/dist/lib/resources/aws/compute/lambda.js +18 -12
- package/dist/lib/resources/aws/compute/lifecycleHookLambda.source.cjs +329 -82
- package/package.json +5 -7
|
@@ -1,15 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ECS deployment lifecycle hook (PRE_SCALE_UP) runner.
|
|
3
|
-
* at synth time by ecsLifecycleHookMigration.ts.
|
|
2
|
+
* ECS deployment lifecycle hook (PRE_SCALE_UP or POST_SCALE_UP) runner.
|
|
3
|
+
* Inlined into a Lambda at synth time by ecsLifecycleHookMigration.ts.
|
|
4
|
+
*
|
|
5
|
+
* Everything the hook runs comes from the deployment it serves, never from
|
|
6
|
+
* the Lambda's own configuration (FJL-901). ECS names the deployment's target
|
|
7
|
+
* service revision; the revision gives the cluster, the service's task
|
|
8
|
+
* definition and its network configuration; the service's task definition
|
|
9
|
+
* names the migration task definition in its fjall:migration-task-definition
|
|
10
|
+
* tag, or, on a revision registered by copying another (whose tags the copy
|
|
11
|
+
* drops), in its primary container's io.fjall.migration-task-definition
|
|
12
|
+
* docker label; and the migration task definition carries its own command,
|
|
13
|
+
* entry point and environment, plus, when the migration has security groups
|
|
14
|
+
* of its own, their ids in its fjall:migration-security-groups tag. So the hook
|
|
15
|
+
* never runs one release's migration for another release's code, however its
|
|
16
|
+
* own update and the service's are ordered. The Lambda changes only when its
|
|
17
|
+
* code does.
|
|
4
18
|
*
|
|
5
19
|
* Re-invocation safety: ECS hook responses do NOT carry state between
|
|
6
20
|
* IN_PROGRESS invocations. On every invocation we reconstruct the
|
|
7
21
|
* deterministic startedBy tag from the event's
|
|
8
|
-
* executionDetails.targetServiceRevisionArn and use ListTasks to find
|
|
9
|
-
*
|
|
22
|
+
* executionDetails.targetServiceRevisionArn and use ListTasks to find the
|
|
23
|
+
* migration task already started for it, running or stopped. RunTask fires
|
|
24
|
+
* only when there is none.
|
|
10
25
|
*
|
|
11
26
|
* Event contract: the target revision ARN is nested under `executionDetails`
|
|
12
|
-
* and the `executionId` is top-level
|
|
27
|
+
* and the `executionId` is top-level - see AWS "Lambda hooks for Amazon ECS
|
|
13
28
|
* service deployments" (developerguide/lambda-lifecycle-hooks.html). Reading
|
|
14
29
|
* the revision ARN from the wrong level collapses the startedBy tag to a
|
|
15
30
|
* shared constant across every deploy, so ListTasks can adopt another
|
|
@@ -23,19 +38,62 @@ const {
|
|
|
23
38
|
ECSClient,
|
|
24
39
|
RunTaskCommand,
|
|
25
40
|
DescribeTasksCommand,
|
|
26
|
-
ListTasksCommand
|
|
41
|
+
ListTasksCommand,
|
|
42
|
+
DescribeServiceRevisionsCommand,
|
|
43
|
+
DescribeTaskDefinitionCommand,
|
|
44
|
+
ListTagsForResourceCommand
|
|
27
45
|
} = require("@aws-sdk/client-ecs");
|
|
28
46
|
|
|
29
47
|
const POLL_INTERVAL_MS = 5_000;
|
|
30
48
|
const MAX_POLLS_PER_INVOCATION = 20;
|
|
31
49
|
const HOOK_CALLBACK_DELAY_SECONDS = 30;
|
|
32
50
|
|
|
51
|
+
/**
|
|
52
|
+
* How many times one invocation reads a service revision ECS reports
|
|
53
|
+
* MISSING before failing: a revision the deployment just created may not be
|
|
54
|
+
* visible to DescribeServiceRevisions at once.
|
|
55
|
+
*/
|
|
56
|
+
const MAX_REVISION_READS = 6;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The tag on the service's task definition that names the migration task
|
|
60
|
+
* definition. MIGRATION_TASK_DEFINITION_TAG_KEY in @fjall/util/migration,
|
|
61
|
+
* which inline code cannot import; a test pins the two together.
|
|
62
|
+
*/
|
|
63
|
+
const MIGRATION_TASK_DEFINITION_TAG_KEY = "fjall:migration-task-definition";
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The docker label on the service's primary container that names the same
|
|
67
|
+
* migration task definition as the tag. A revision registered by copying
|
|
68
|
+
* another, as an engine before 40.0.0 registers one for a restart, a
|
|
69
|
+
* code-only deploy or a reconcile, keeps the container definitions and drops
|
|
70
|
+
* the tags, so the label is read when the tag is absent.
|
|
71
|
+
* MIGRATION_TASK_DEFINITION_LABEL_KEY in @fjall/util/migration.
|
|
72
|
+
*/
|
|
73
|
+
const MIGRATION_TASK_DEFINITION_LABEL_KEY =
|
|
74
|
+
"io.fjall.migration-task-definition";
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The tag on a migration task definition that names, space-separated, the
|
|
78
|
+
* security groups its task runs in, when they are not the service's.
|
|
79
|
+
* MIGRATION_SECURITY_GROUPS_TAG_KEY in @fjall/util/migration.
|
|
80
|
+
*/
|
|
81
|
+
const MIGRATION_SECURITY_GROUPS_TAG_KEY = "fjall:migration-security-groups";
|
|
82
|
+
|
|
83
|
+
const TASK_DEFINITION_ARN =
|
|
84
|
+
/^arn:[^:]+:ecs:[^:]+:\d{12}:task-definition\/[^:]+:\d+$/;
|
|
85
|
+
const SECURITY_GROUP_ID = /^sg-[0-9a-f]+$/;
|
|
86
|
+
|
|
33
87
|
let _defaultClient;
|
|
34
88
|
function getDefaultClient() {
|
|
35
89
|
if (!_defaultClient) _defaultClient = new ECSClient({});
|
|
36
90
|
return _defaultClient;
|
|
37
91
|
}
|
|
38
92
|
|
|
93
|
+
function defaultSleep(ms) {
|
|
94
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
95
|
+
}
|
|
96
|
+
|
|
39
97
|
function startedByTag(targetServiceRevisionArn) {
|
|
40
98
|
// ECS startedBy is bounded to 36 chars; revision id alone is shorter.
|
|
41
99
|
const suffix =
|
|
@@ -65,31 +123,194 @@ function extractExecutionId(event) {
|
|
|
65
123
|
return topLevel || nested || undefined;
|
|
66
124
|
}
|
|
67
125
|
|
|
68
|
-
function
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
126
|
+
function failed(reason) {
|
|
127
|
+
return { hookStatus: "FAILED", reason };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The cluster, the service's task definition and the network configuration
|
|
132
|
+
* of the deployment's target revision, or a FAILED result naming why.
|
|
133
|
+
*/
|
|
134
|
+
async function readServiceRevision(client, revisionArn, sleep) {
|
|
135
|
+
let lastReason = "no failure reported";
|
|
136
|
+
for (let attempt = 0; attempt < MAX_REVISION_READS; attempt++) {
|
|
137
|
+
const answer = await client.send(
|
|
138
|
+
new DescribeServiceRevisionsCommand({
|
|
139
|
+
serviceRevisionArns: [revisionArn]
|
|
140
|
+
})
|
|
141
|
+
);
|
|
142
|
+
const revision = (answer.serviceRevisions || []).find(
|
|
143
|
+
(candidate) => candidate.serviceRevisionArn === revisionArn
|
|
144
|
+
);
|
|
145
|
+
if (revision !== undefined) {
|
|
146
|
+
const awsvpcConfiguration =
|
|
147
|
+
revision.networkConfiguration &&
|
|
148
|
+
revision.networkConfiguration.awsvpcConfiguration;
|
|
149
|
+
if (
|
|
150
|
+
typeof revision.clusterArn !== "string" ||
|
|
151
|
+
typeof revision.taskDefinition !== "string" ||
|
|
152
|
+
awsvpcConfiguration === undefined
|
|
153
|
+
) {
|
|
154
|
+
return {
|
|
155
|
+
failure: failed(
|
|
156
|
+
`Service revision ${revisionArn} names no cluster, task definition or awsvpc network configuration`
|
|
157
|
+
)
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
clusterArn: revision.clusterArn,
|
|
162
|
+
taskDefinition: revision.taskDefinition,
|
|
163
|
+
awsvpcConfiguration
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
const failure = answer.failures && answer.failures[0];
|
|
167
|
+
lastReason = failure && failure.reason ? failure.reason : lastReason;
|
|
168
|
+
if (lastReason !== "MISSING") break;
|
|
169
|
+
if (attempt < MAX_REVISION_READS - 1) await sleep(POLL_INTERVAL_MS);
|
|
83
170
|
}
|
|
84
|
-
return
|
|
171
|
+
return {
|
|
172
|
+
failure: failed(`DescribeServiceRevisions(${revisionArn}): ${lastReason}`)
|
|
173
|
+
};
|
|
85
174
|
}
|
|
86
175
|
|
|
87
|
-
async function
|
|
176
|
+
async function tagValue(client, resourceArn, key) {
|
|
177
|
+
const answer = await client.send(
|
|
178
|
+
new ListTagsForResourceCommand({ resourceArn })
|
|
179
|
+
);
|
|
180
|
+
const tag = (answer.tags || []).find((candidate) => candidate.key === key);
|
|
181
|
+
return tag === undefined ? undefined : tag.value;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** The distinct values the task definition's containers give a docker label. */
|
|
185
|
+
async function labelValues(client, taskDefinitionArn, key) {
|
|
186
|
+
const answer = await client.send(
|
|
187
|
+
new DescribeTaskDefinitionCommand({ taskDefinition: taskDefinitionArn })
|
|
188
|
+
);
|
|
189
|
+
const containers =
|
|
190
|
+
(answer.taskDefinition && answer.taskDefinition.containerDefinitions) || [];
|
|
191
|
+
const values = containers
|
|
192
|
+
.map((container) => container.dockerLabels && container.dockerLabels[key])
|
|
193
|
+
.filter((value) => typeof value === "string");
|
|
194
|
+
return [...new Set(values)];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* The migration task definition the service's task definition names, and
|
|
199
|
+
* which of the two named it: its tag, else its primary container's docker
|
|
200
|
+
* label; or a FAILED result naming why.
|
|
201
|
+
*/
|
|
202
|
+
async function namedMigration(client, serviceTaskDefinition) {
|
|
203
|
+
const tagged = await tagValue(
|
|
204
|
+
client,
|
|
205
|
+
serviceTaskDefinition,
|
|
206
|
+
MIGRATION_TASK_DEFINITION_TAG_KEY
|
|
207
|
+
);
|
|
208
|
+
const migrationNamedBy = tagged !== undefined ? "tag" : "label";
|
|
209
|
+
const [source, named] =
|
|
210
|
+
tagged !== undefined
|
|
211
|
+
? [`${MIGRATION_TASK_DEFINITION_TAG_KEY} tag`, [tagged]]
|
|
212
|
+
: [
|
|
213
|
+
`${MIGRATION_TASK_DEFINITION_LABEL_KEY} docker label`,
|
|
214
|
+
await labelValues(
|
|
215
|
+
client,
|
|
216
|
+
serviceTaskDefinition,
|
|
217
|
+
MIGRATION_TASK_DEFINITION_LABEL_KEY
|
|
218
|
+
)
|
|
219
|
+
];
|
|
220
|
+
if (named.length === 0) {
|
|
221
|
+
return {
|
|
222
|
+
failure: failed(
|
|
223
|
+
`Task definition ${serviceTaskDefinition} carries neither a ${MIGRATION_TASK_DEFINITION_TAG_KEY} tag nor a ${MIGRATION_TASK_DEFINITION_LABEL_KEY} docker label naming its migration task definition`
|
|
224
|
+
)
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
if (named.length > 1) {
|
|
228
|
+
return {
|
|
229
|
+
failure: failed(
|
|
230
|
+
`The containers of ${serviceTaskDefinition} name different migration task definitions in their ${source}s`
|
|
231
|
+
)
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
const [migrationTaskDefinition] = named;
|
|
235
|
+
if (!TASK_DEFINITION_ARN.test(migrationTaskDefinition)) {
|
|
236
|
+
return {
|
|
237
|
+
failure: failed(
|
|
238
|
+
`The ${source} on ${serviceTaskDefinition} is not a task-definition ARN`
|
|
239
|
+
)
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
return { migrationTaskDefinition, migrationNamedBy };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* The migration task definition the service's task definition names, what
|
|
247
|
+
* named it, and the security groups its task runs in, or a FAILED result
|
|
248
|
+
* naming why.
|
|
249
|
+
*/
|
|
250
|
+
async function readMigrationPlan(client, serviceTaskDefinition) {
|
|
251
|
+
const named = await namedMigration(client, serviceTaskDefinition);
|
|
252
|
+
if (named.failure) return named;
|
|
253
|
+
const { migrationTaskDefinition } = named;
|
|
254
|
+
const groups = await tagValue(
|
|
255
|
+
client,
|
|
256
|
+
migrationTaskDefinition,
|
|
257
|
+
MIGRATION_SECURITY_GROUPS_TAG_KEY
|
|
258
|
+
);
|
|
259
|
+
if (groups === undefined) return named;
|
|
260
|
+
const securityGroups = groups.split(" ").filter((id) => id !== "");
|
|
261
|
+
if (
|
|
262
|
+
securityGroups.length === 0 ||
|
|
263
|
+
!securityGroups.every((id) => SECURITY_GROUP_ID.test(id))
|
|
264
|
+
) {
|
|
265
|
+
return {
|
|
266
|
+
failure: failed(
|
|
267
|
+
`The ${MIGRATION_SECURITY_GROUPS_TAG_KEY} tag on ${migrationTaskDefinition} names no security group ids`
|
|
268
|
+
)
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
return { ...named, securityGroups };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function tasksStartedBy(client, clusterArn, startedBy, desiredStatus) {
|
|
88
275
|
const result = await client.send(
|
|
89
|
-
new ListTasksCommand({ cluster: clusterArn, startedBy })
|
|
276
|
+
new ListTasksCommand({ cluster: clusterArn, startedBy, desiredStatus })
|
|
90
277
|
);
|
|
91
|
-
|
|
92
|
-
|
|
278
|
+
return result.taskArns || [];
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* The migration task already started for this revision: a running one, or
|
|
283
|
+
* else the newest stopped one. ListTasks answers RUNNING tasks unless asked,
|
|
284
|
+
* and a re-invocation can land after the task stopped; ECS keeps a stopped
|
|
285
|
+
* task listable for about an hour, longer than any one deployment's hook.
|
|
286
|
+
*/
|
|
287
|
+
async function findExistingTaskArn(client, clusterArn, startedBy) {
|
|
288
|
+
const running = await tasksStartedBy(
|
|
289
|
+
client,
|
|
290
|
+
clusterArn,
|
|
291
|
+
startedBy,
|
|
292
|
+
"RUNNING"
|
|
293
|
+
);
|
|
294
|
+
if (running.length > 0) return running[0];
|
|
295
|
+
const stopped = await tasksStartedBy(
|
|
296
|
+
client,
|
|
297
|
+
clusterArn,
|
|
298
|
+
startedBy,
|
|
299
|
+
"STOPPED"
|
|
300
|
+
);
|
|
301
|
+
if (stopped.length <= 1) return stopped[0];
|
|
302
|
+
const desc = await client.send(
|
|
303
|
+
new DescribeTasksCommand({ cluster: clusterArn, tasks: stopped })
|
|
304
|
+
);
|
|
305
|
+
const newest = (desc.tasks || [])
|
|
306
|
+
.slice()
|
|
307
|
+
.sort((a, b) => timeOf(b.createdAt) - timeOf(a.createdAt))[0];
|
|
308
|
+
return newest && newest.taskArn ? newest.taskArn : stopped[0];
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function timeOf(value) {
|
|
312
|
+
const time = value === undefined ? NaN : new Date(value).getTime();
|
|
313
|
+
return Number.isFinite(time) ? time : 0;
|
|
93
314
|
}
|
|
94
315
|
|
|
95
316
|
function describeFailureReason(desc) {
|
|
@@ -102,11 +323,10 @@ function describeFailureReason(desc) {
|
|
|
102
323
|
// ARN RunTask just returned. Until the task has been seen once, a miss is
|
|
103
324
|
// "not visible yet" and the poll continues; once it has been seen, a miss is
|
|
104
325
|
// the task vanishing, which is loud. A task never seen inside one
|
|
105
|
-
// invocation's poll budget is loud too
|
|
326
|
+
// invocation's poll budget is loud too - that is a hundred seconds, not
|
|
106
327
|
// consistency lag.
|
|
107
328
|
async function pollTaskUntilStopped(client, clusterArn, taskArn, sleep) {
|
|
108
|
-
const sleepFn =
|
|
109
|
-
sleep || ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
329
|
+
const sleepFn = sleep || defaultSleep;
|
|
110
330
|
let seen = false;
|
|
111
331
|
let lastMissReason = "no failure reported";
|
|
112
332
|
for (let i = 0; i < MAX_POLLS_PER_INVOCATION; i++) {
|
|
@@ -122,18 +342,16 @@ async function pollTaskUntilStopped(client, clusterArn, taskArn, sleep) {
|
|
|
122
342
|
? task.containers[0].exitCode
|
|
123
343
|
: undefined;
|
|
124
344
|
if (exitCode === 0) return { hookStatus: "SUCCEEDED" };
|
|
125
|
-
return
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
};
|
|
345
|
+
return failed(
|
|
346
|
+
`Task exited ${exitCode}: ${task.stoppedReason || "no reason"}`
|
|
347
|
+
);
|
|
129
348
|
}
|
|
130
349
|
} else {
|
|
131
350
|
lastMissReason = describeFailureReason(desc);
|
|
132
351
|
if (seen) {
|
|
133
|
-
return
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
};
|
|
352
|
+
return failed(
|
|
353
|
+
`DescribeTasks returned no task after it had been seen (${lastMissReason})`
|
|
354
|
+
);
|
|
137
355
|
}
|
|
138
356
|
}
|
|
139
357
|
if (i < MAX_POLLS_PER_INVOCATION - 1) {
|
|
@@ -141,10 +359,9 @@ async function pollTaskUntilStopped(client, clusterArn, taskArn, sleep) {
|
|
|
141
359
|
}
|
|
142
360
|
}
|
|
143
361
|
if (!seen) {
|
|
144
|
-
return
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
};
|
|
362
|
+
return failed(
|
|
363
|
+
`DescribeTasks returned no task in ${MAX_POLLS_PER_INVOCATION} polls (${lastMissReason})`
|
|
364
|
+
);
|
|
148
365
|
}
|
|
149
366
|
return {
|
|
150
367
|
hookStatus: "IN_PROGRESS",
|
|
@@ -152,70 +369,90 @@ async function pollTaskUntilStopped(client, clusterArn, taskArn, sleep) {
|
|
|
152
369
|
};
|
|
153
370
|
}
|
|
154
371
|
|
|
372
|
+
/**
|
|
373
|
+
* The hook's verdict, and the cluster and migration it ran, for its log. The
|
|
374
|
+
* log says whether the tag or the label named the migration: a revision
|
|
375
|
+
* copied without its tags runs the migration its copied label names.
|
|
376
|
+
*/
|
|
155
377
|
async function runHandler(event, deps) {
|
|
156
378
|
const targetServiceRevisionArn = extractTargetServiceRevisionArn(event);
|
|
157
379
|
if (!targetServiceRevisionArn) {
|
|
158
380
|
return {
|
|
159
|
-
|
|
160
|
-
reason:
|
|
381
|
+
result: failed(
|
|
161
382
|
"Lifecycle event missing executionDetails.targetServiceRevisionArn"
|
|
383
|
+
)
|
|
162
384
|
};
|
|
163
385
|
}
|
|
164
386
|
const client = (deps && deps.client) || getDefaultClient();
|
|
165
|
-
const sleep = deps && deps.sleep;
|
|
166
|
-
|
|
167
|
-
|
|
387
|
+
const sleep = (deps && deps.sleep) || defaultSleep;
|
|
388
|
+
|
|
389
|
+
const revision = await readServiceRevision(
|
|
390
|
+
client,
|
|
391
|
+
targetServiceRevisionArn,
|
|
392
|
+
sleep
|
|
168
393
|
);
|
|
394
|
+
if (revision.failure) return { result: revision.failure };
|
|
395
|
+
const plan = await readMigrationPlan(client, revision.taskDefinition);
|
|
396
|
+
if (plan.failure) return { result: plan.failure };
|
|
397
|
+
const ran = {
|
|
398
|
+
clusterArn: revision.clusterArn,
|
|
399
|
+
migrationTaskDefinition: plan.migrationTaskDefinition,
|
|
400
|
+
migrationNamedBy: plan.migrationNamedBy
|
|
401
|
+
};
|
|
169
402
|
const startedBy = startedByTag(targetServiceRevisionArn);
|
|
170
403
|
|
|
171
404
|
const existingTaskArn = await findExistingTaskArn(
|
|
172
405
|
client,
|
|
173
|
-
|
|
406
|
+
revision.clusterArn,
|
|
174
407
|
startedBy
|
|
175
408
|
);
|
|
176
409
|
if (existingTaskArn) {
|
|
177
|
-
return
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
410
|
+
return {
|
|
411
|
+
...ran,
|
|
412
|
+
result: await pollTaskUntilStopped(
|
|
413
|
+
client,
|
|
414
|
+
revision.clusterArn,
|
|
415
|
+
existingTaskArn,
|
|
416
|
+
sleep
|
|
417
|
+
)
|
|
418
|
+
};
|
|
183
419
|
}
|
|
184
420
|
|
|
185
|
-
const
|
|
421
|
+
const awsvpcConfiguration =
|
|
422
|
+
plan.securityGroups === undefined
|
|
423
|
+
? revision.awsvpcConfiguration
|
|
424
|
+
: {
|
|
425
|
+
...revision.awsvpcConfiguration,
|
|
426
|
+
securityGroups: plan.securityGroups
|
|
427
|
+
};
|
|
428
|
+
const launched = await client.send(
|
|
186
429
|
new RunTaskCommand({
|
|
187
|
-
cluster:
|
|
188
|
-
taskDefinition:
|
|
430
|
+
cluster: revision.clusterArn,
|
|
431
|
+
taskDefinition: plan.migrationTaskDefinition,
|
|
189
432
|
launchType: "FARGATE",
|
|
190
|
-
networkConfiguration: {
|
|
191
|
-
awsvpcConfiguration: config.networkConfiguration
|
|
192
|
-
},
|
|
193
|
-
overrides: {
|
|
194
|
-
containerOverrides: buildContainerOverrides(
|
|
195
|
-
config.name,
|
|
196
|
-
config.command,
|
|
197
|
-
config.hasSeparateMigrationTaskDef ? undefined : config.image,
|
|
198
|
-
config.hasSeparateMigrationTaskDef ? undefined : config.entryPoint,
|
|
199
|
-
config.environment
|
|
200
|
-
)
|
|
201
|
-
},
|
|
433
|
+
networkConfiguration: { awsvpcConfiguration },
|
|
202
434
|
startedBy
|
|
203
435
|
})
|
|
204
436
|
);
|
|
205
437
|
const taskArn =
|
|
206
|
-
|
|
438
|
+
launched.tasks && launched.tasks[0] ? launched.tasks[0].taskArn : undefined;
|
|
207
439
|
if (!taskArn) {
|
|
208
|
-
return {
|
|
209
|
-
hookStatus: "FAILED",
|
|
210
|
-
reason: "RunTask did not return a task ARN"
|
|
211
|
-
};
|
|
440
|
+
return { ...ran, result: failed("RunTask did not return a task ARN") };
|
|
212
441
|
}
|
|
213
|
-
|
|
214
|
-
|
|
442
|
+
return {
|
|
443
|
+
...ran,
|
|
444
|
+
result: await pollTaskUntilStopped(
|
|
445
|
+
client,
|
|
446
|
+
revision.clusterArn,
|
|
447
|
+
taskArn,
|
|
448
|
+
sleep
|
|
449
|
+
)
|
|
450
|
+
};
|
|
215
451
|
}
|
|
216
452
|
|
|
217
|
-
|
|
218
|
-
|
|
453
|
+
/** The handler, with its dependencies injectable: it never rejects. */
|
|
454
|
+
async function handle(event, deps) {
|
|
455
|
+
// Log invocation context + outcome - ECS hooks only surface "hook execution
|
|
219
456
|
// failure" to the deployment; without these the Lambda log is silent and
|
|
220
457
|
// the failure cause is unrecoverable.
|
|
221
458
|
console.log(
|
|
@@ -227,8 +464,10 @@ exports.handler = async (event) => {
|
|
|
227
464
|
})
|
|
228
465
|
);
|
|
229
466
|
try {
|
|
230
|
-
const result = await runHandler(event);
|
|
231
|
-
console.log(
|
|
467
|
+
const { result, ...ran } = await runHandler(event, deps);
|
|
468
|
+
console.log(
|
|
469
|
+
JSON.stringify({ msg: "lifecycle-hook-result", ...ran, ...result })
|
|
470
|
+
);
|
|
232
471
|
return result;
|
|
233
472
|
} catch (err) {
|
|
234
473
|
const reason = err && err.message ? err.message : String(err);
|
|
@@ -236,19 +475,27 @@ exports.handler = async (event) => {
|
|
|
236
475
|
console.error(
|
|
237
476
|
JSON.stringify({ msg: "lifecycle-hook-crashed", reason, stack })
|
|
238
477
|
);
|
|
239
|
-
return
|
|
478
|
+
return failed(`Runner crashed: ${reason}`);
|
|
240
479
|
}
|
|
241
|
-
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
exports.handler = (event) => handle(event);
|
|
242
483
|
|
|
243
484
|
exports._internals = {
|
|
244
485
|
startedByTag,
|
|
245
486
|
extractTargetServiceRevisionArn,
|
|
246
487
|
extractExecutionId,
|
|
247
|
-
|
|
488
|
+
readServiceRevision,
|
|
489
|
+
readMigrationPlan,
|
|
248
490
|
findExistingTaskArn,
|
|
249
491
|
pollTaskUntilStopped,
|
|
250
492
|
runHandler,
|
|
493
|
+
handle,
|
|
494
|
+
MIGRATION_TASK_DEFINITION_TAG_KEY,
|
|
495
|
+
MIGRATION_TASK_DEFINITION_LABEL_KEY,
|
|
496
|
+
MIGRATION_SECURITY_GROUPS_TAG_KEY,
|
|
251
497
|
POLL_INTERVAL_MS,
|
|
252
498
|
MAX_POLLS_PER_INVOCATION,
|
|
499
|
+
MAX_REVISION_READS,
|
|
253
500
|
HOOK_CALLBACK_DELAY_SECONDS
|
|
254
501
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fjall/components-infrastructure",
|
|
3
|
-
"version": "39.0.
|
|
3
|
+
"version": "39.0.7",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/fjall-tech/fjall.git",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"clean:node": "rm -rf ./node_modules",
|
|
41
41
|
"build:cert-gen-lambda": "node lib/lambda-assets/cert-generator/src/build.mjs",
|
|
42
42
|
"build": "node ../../scripts/run-build.mjs --purpose=build -- npm run build:steps",
|
|
43
|
-
"build:steps": "npm run clean && npm run build:cert-gen-lambda && tsc && cp -r lib/layers dist/lib/layers && mkdir -p dist/lib/lambda-assets/cert-generator/asset && cp lib/lambda-assets/cert-generator/asset/index.js dist/lib/lambda-assets/cert-generator/asset/index.js && cp lib/lambda-assets/cert-generator/asset/package.json dist/lib/lambda-assets/cert-generator/asset/package.json && mkdir -p dist/lib/lambda-assets/identity-store-user/asset && cp lib/lambda-assets/identity-store-user/asset/index.js dist/lib/lambda-assets/identity-store-user/asset/index.js && cp lib/lambda-assets/identity-store-user/asset/package.json dist/lib/lambda-assets/identity-store-user/asset/package.json && mkdir -p dist/lib/lambda-assets/static-site-forms/asset && cp lib/lambda-assets/static-site-forms/asset/index.js dist/lib/lambda-assets/static-site-forms/asset/index.js && cp lib/lambda-assets/static-site-forms/asset/package.json dist/lib/lambda-assets/static-site-forms/asset/package.json && cp lib/resources/aws/compute/lifecycleHookLambda.source.cjs dist/lib/resources/aws/compute/lifecycleHookLambda.source.cjs && cp lib/resources/aws/compute/ec2GracefulTerminationLambda.source.cjs dist/lib/resources/aws/compute/ec2GracefulTerminationLambda.source.cjs && cp lib/resources/aws/compute/persistentDataVolumeLambda.source.cjs dist/lib/resources/aws/compute/persistentDataVolumeLambda.source.cjs && cp lib/patterns/aws/devSubstrate.waker.source.cjs dist/lib/patterns/aws/devSubstrate.waker.source.cjs && node ../../scripts/stamp-build.mjs",
|
|
43
|
+
"build:steps": "npm run clean && npm run build:cert-gen-lambda && tsc && cp -r lib/layers dist/lib/layers && mkdir -p dist/lib/lambda-assets/cert-generator/asset && cp lib/lambda-assets/cert-generator/asset/index.js dist/lib/lambda-assets/cert-generator/asset/index.js && cp lib/lambda-assets/cert-generator/asset/package.json dist/lib/lambda-assets/cert-generator/asset/package.json && mkdir -p dist/lib/lambda-assets/identity-store-user/asset && cp lib/lambda-assets/identity-store-user/asset/index.js dist/lib/lambda-assets/identity-store-user/asset/index.js && cp lib/lambda-assets/identity-store-user/asset/package.json dist/lib/lambda-assets/identity-store-user/asset/package.json && mkdir -p dist/lib/lambda-assets/static-site-forms/asset && cp lib/lambda-assets/static-site-forms/asset/index.js dist/lib/lambda-assets/static-site-forms/asset/index.js && cp lib/lambda-assets/static-site-forms/asset/package.json dist/lib/lambda-assets/static-site-forms/asset/package.json && cp lib/resources/aws/compute/lifecycleHookLambda.source.cjs dist/lib/resources/aws/compute/lifecycleHookLambda.source.cjs && cp lib/resources/aws/compute/ecsRolloutBarrierLambda.source.cjs dist/lib/resources/aws/compute/ecsRolloutBarrierLambda.source.cjs && cp lib/resources/aws/compute/ec2GracefulTerminationLambda.source.cjs dist/lib/resources/aws/compute/ec2GracefulTerminationLambda.source.cjs && cp lib/resources/aws/compute/persistentDataVolumeLambda.source.cjs dist/lib/resources/aws/compute/persistentDataVolumeLambda.source.cjs && cp lib/patterns/aws/devSubstrate.waker.source.cjs dist/lib/patterns/aws/devSubstrate.waker.source.cjs && node ../../scripts/stamp-build.mjs",
|
|
44
44
|
"prepack": "node ../../scripts/check-dist-freshness.mjs",
|
|
45
45
|
"prepublishOnly": "node ../../scripts/audit-package-tarball.mjs",
|
|
46
46
|
"watch": "tsc -w",
|
|
@@ -65,6 +65,7 @@
|
|
|
65
65
|
"format:check": "prettier --check \"lib/**/*.{ts,tsx,js,jsx,json}\" \"scripts/**/*.mts\" \"deploy-tests/**/*.mts\""
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
|
+
"@aws-sdk/client-ecs": "^3.1098.0",
|
|
68
69
|
"@aws-sdk/client-elastic-load-balancing-v2": "^3.1098.0",
|
|
69
70
|
"@aws-sdk/client-iam": "^3.1098.0",
|
|
70
71
|
"@aws-sdk/client-identitystore": "^3.1098.0",
|
|
@@ -80,14 +81,11 @@
|
|
|
80
81
|
},
|
|
81
82
|
"dependencies": {
|
|
82
83
|
"@aws-sdk/client-organizations": "^3.1098.0",
|
|
83
|
-
"@fjall/generator": "^39.0.
|
|
84
|
-
"@fjall/util": "^39.0.
|
|
84
|
+
"@fjall/generator": "^39.0.7",
|
|
85
|
+
"@fjall/util": "^39.0.7",
|
|
85
86
|
"constructs": "^10.7.2",
|
|
86
87
|
"zod": "^4.4.3"
|
|
87
88
|
},
|
|
88
|
-
"overrides": {
|
|
89
|
-
"@smithy/core": "2.5.5"
|
|
90
|
-
},
|
|
91
89
|
"peerDependencies": {
|
|
92
90
|
"aws-cdk": "^2.1134.0",
|
|
93
91
|
"aws-cdk-lib": "^2.265.0",
|