@fjall/components-infrastructure 3.11.0 → 4.1.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.
|
@@ -38,6 +38,18 @@ export interface IDevSubstrateProps {
|
|
|
38
38
|
* account where the repo was provisioned out-of-band.
|
|
39
39
|
*/
|
|
40
40
|
readonly adoptSlotEcr?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* When `true`, adopt an already-existing slot build-cache ECR repository
|
|
43
|
+
* (`fjall-dev-<appKebab>-slots-cache`) by reference instead of creating one.
|
|
44
|
+
* Independent of {@link adoptSlotEcr} on purpose: an app whose slots repo
|
|
45
|
+
* predates the build-cache feature runs the substrate in slot-adopt mode, yet
|
|
46
|
+
* its cache repo does NOT exist and MUST be created on the next redeploy —
|
|
47
|
+
* coupling the two would import a non-existent cache repo and the slot builds
|
|
48
|
+
* would stay permanently uncached. The producer probes each repo's existence
|
|
49
|
+
* separately (webapp `shouldAdoptSlotEcr` / `shouldAdoptCacheEcr`). Defaults to
|
|
50
|
+
* `false`: create the cache repo with the untagged-image lifecycle rule.
|
|
51
|
+
*/
|
|
52
|
+
readonly adoptCacheEcr?: boolean;
|
|
41
53
|
/**
|
|
42
54
|
* Deploy phase for the R2 two-step cert-hang guard. `"zone"` synthesises the
|
|
43
55
|
* `devDomain` zone + cross-account NS delegation only; `"full"` additionally
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { CfnOutput, Duration, Stack, Tags } from "aws-cdk-lib";
|
|
2
2
|
import { Port } from "aws-cdk-lib/aws-ec2";
|
|
3
3
|
import { Cluster } from "aws-cdk-lib/aws-ecs";
|
|
4
|
-
import { Repository, TagMutability } from "aws-cdk-lib/aws-ecr";
|
|
4
|
+
import { Repository, TagMutability, TagStatus } from "aws-cdk-lib/aws-ecr";
|
|
5
5
|
import { Effect, PolicyStatement } from "aws-cdk-lib/aws-iam";
|
|
6
6
|
import { Code, Runtime } from "aws-cdk-lib/aws-lambda";
|
|
7
7
|
import { PublicHostedZone } from "aws-cdk-lib/aws-route53";
|
|
8
8
|
import { LoadBalancerTarget } from "aws-cdk-lib/aws-route53-targets";
|
|
9
9
|
import { SqsDestination } from "aws-cdk-lib/aws-s3-notifications";
|
|
10
10
|
import { Construct } from "constructs";
|
|
11
|
+
import { buildCacheRepositoryName, CACHE_REPO_UNTAGGED_RETENTION_DAYS } from "@fjall/util/docker";
|
|
11
12
|
import { readFileSync } from "node:fs";
|
|
12
13
|
import path from "node:path";
|
|
13
14
|
import { fileURLToPath } from "node:url";
|
|
@@ -249,6 +250,31 @@ export class DevSubstrate extends Construct {
|
|
|
249
250
|
// first same-branch recreate (BUG-22).
|
|
250
251
|
tagMutability: TagMutability.MUTABLE
|
|
251
252
|
});
|
|
253
|
+
// Buildx registry cache for slot builds, named by the same helper as the app
|
|
254
|
+
// path's `<repo>-cache` so `fjall dev up` derives this URI from the slot repo's
|
|
255
|
+
// without a second wire field. Adopt is gated on its OWN `adoptCacheEcr` flag,
|
|
256
|
+
// NOT `adoptSlotEcr`: an app whose slots repo predates the cache feature
|
|
257
|
+
// redeploys in slot-adopt mode with no cache repo yet — coupling the two would
|
|
258
|
+
// import a non-existent repo and slot builds would stay permanently uncached.
|
|
259
|
+
const slotCacheRepositoryName = buildCacheRepositoryName(slotEcrRepositoryName);
|
|
260
|
+
if (props.adoptCacheEcr === true) {
|
|
261
|
+
Repository.fromRepositoryName(this, `${slotEcrId}Cache`, slotCacheRepositoryName);
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
const slotCacheRepository = new Ecr(this, `${slotEcrId}Cache`, {
|
|
265
|
+
repositoryName: slotCacheRepositoryName,
|
|
266
|
+
// buildx overwrites the per-repo cache manifest tag on every export.
|
|
267
|
+
tagMutability: TagMutability.MUTABLE
|
|
268
|
+
});
|
|
269
|
+
// Each `mode=max` export orphans the previous manifest's blobs. The
|
|
270
|
+
// dev-deploy role holds no BatchDeleteImage or PutLifecyclePolicy, so this
|
|
271
|
+
// rule is the ONLY reclaim path — without it the cache grows without bound
|
|
272
|
+
// in a customer's dev account. An adopted repo keeps its existing policy.
|
|
273
|
+
slotCacheRepository.addLifecycleRule({
|
|
274
|
+
tagStatus: TagStatus.UNTAGGED,
|
|
275
|
+
maxImageAge: Duration.days(CACHE_REPO_UNTAGGED_RETENTION_DAYS)
|
|
276
|
+
});
|
|
277
|
+
}
|
|
252
278
|
return {
|
|
253
279
|
vpc,
|
|
254
280
|
slotSecurityGroup,
|
|
@@ -4,9 +4,17 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Re-invocation safety: ECS hook responses do NOT carry state between
|
|
6
6
|
* IN_PROGRESS invocations. On every invocation we reconstruct the
|
|
7
|
-
* deterministic startedBy tag from the event's
|
|
8
|
-
* use ListTasks to find an
|
|
9
|
-
* the first invocation.
|
|
7
|
+
* deterministic startedBy tag from the event's
|
|
8
|
+
* executionDetails.targetServiceRevisionArn and use ListTasks to find an
|
|
9
|
+
* existing running migration. RunTask fires only on the first invocation.
|
|
10
|
+
*
|
|
11
|
+
* Event contract: the target revision ARN is nested under `executionDetails`
|
|
12
|
+
* and the `executionId` is top-level — see AWS "Lambda hooks for Amazon ECS
|
|
13
|
+
* service deployments" (developerguide/lambda-lifecycle-hooks.html). Reading
|
|
14
|
+
* the revision ARN from the wrong level collapses the startedBy tag to a
|
|
15
|
+
* shared constant across every deploy, so ListTasks can adopt another
|
|
16
|
+
* deployment's task (cross-deploy race). A missing revision ARN therefore
|
|
17
|
+
* FAILS the hook loudly rather than proceeding with a shared tag.
|
|
10
18
|
*
|
|
11
19
|
* CommonJS rather than ESM because Code.fromInline lands the source as
|
|
12
20
|
* `index.js`, which Lambda treats as CommonJS by default.
|
|
@@ -37,6 +45,26 @@ function startedByTag(targetServiceRevisionArn) {
|
|
|
37
45
|
return `fjall-migrate-${suffix}`.slice(0, 36);
|
|
38
46
|
}
|
|
39
47
|
|
|
48
|
+
// The revision ARN is nested under executionDetails in the ECS lifecycle-hook
|
|
49
|
+
// event; the top-level fallback tolerates a future/legacy shape without
|
|
50
|
+
// reopening the shared-tag race (nested is read first).
|
|
51
|
+
function extractTargetServiceRevisionArn(event) {
|
|
52
|
+
const nested =
|
|
53
|
+
event &&
|
|
54
|
+
event.executionDetails &&
|
|
55
|
+
event.executionDetails.targetServiceRevisionArn;
|
|
56
|
+
const topLevel = event && event.targetServiceRevisionArn;
|
|
57
|
+
return nested || topLevel || undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// executionId is top-level in the AWS contract; nested fallback is defensive.
|
|
61
|
+
function extractExecutionId(event) {
|
|
62
|
+
const topLevel = event && event.executionId;
|
|
63
|
+
const nested =
|
|
64
|
+
event && event.executionDetails && event.executionDetails.executionId;
|
|
65
|
+
return topLevel || nested || undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
40
68
|
function buildContainerOverrides(
|
|
41
69
|
name,
|
|
42
70
|
command,
|
|
@@ -97,12 +125,20 @@ async function pollTaskUntilStopped(client, clusterArn, taskArn, sleep) {
|
|
|
97
125
|
}
|
|
98
126
|
|
|
99
127
|
async function runHandler(event, deps) {
|
|
128
|
+
const targetServiceRevisionArn = extractTargetServiceRevisionArn(event);
|
|
129
|
+
if (!targetServiceRevisionArn) {
|
|
130
|
+
return {
|
|
131
|
+
hookStatus: "FAILED",
|
|
132
|
+
reason:
|
|
133
|
+
"Lifecycle event missing executionDetails.targetServiceRevisionArn"
|
|
134
|
+
};
|
|
135
|
+
}
|
|
100
136
|
const client = (deps && deps.client) || getDefaultClient();
|
|
101
137
|
const sleep = deps && deps.sleep;
|
|
102
138
|
const config = JSON.parse(
|
|
103
139
|
(deps && deps.migrateConfig) || process.env.MIGRATE_CONFIG
|
|
104
140
|
);
|
|
105
|
-
const startedBy = startedByTag(
|
|
141
|
+
const startedBy = startedByTag(targetServiceRevisionArn);
|
|
106
142
|
|
|
107
143
|
const existingTaskArn = await findExistingTaskArn(
|
|
108
144
|
client,
|
|
@@ -158,12 +194,8 @@ exports.handler = async (event) => {
|
|
|
158
194
|
JSON.stringify({
|
|
159
195
|
msg: "lifecycle-hook-invoked",
|
|
160
196
|
targetServiceRevisionArn:
|
|
161
|
-
(event
|
|
162
|
-
executionId:
|
|
163
|
-
(event &&
|
|
164
|
-
event.executionDetails &&
|
|
165
|
-
event.executionDetails.executionId) ||
|
|
166
|
-
"(missing)"
|
|
197
|
+
extractTargetServiceRevisionArn(event) || "(missing)",
|
|
198
|
+
executionId: extractExecutionId(event) || "(missing)"
|
|
167
199
|
})
|
|
168
200
|
);
|
|
169
201
|
try {
|
|
@@ -182,6 +214,8 @@ exports.handler = async (event) => {
|
|
|
182
214
|
|
|
183
215
|
exports._internals = {
|
|
184
216
|
startedByTag,
|
|
217
|
+
extractTargetServiceRevisionArn,
|
|
218
|
+
extractExecutionId,
|
|
185
219
|
buildContainerOverrides,
|
|
186
220
|
findExistingTaskArn,
|
|
187
221
|
pollTaskUntilStopped,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fjall/components-infrastructure",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -67,8 +67,8 @@
|
|
|
67
67
|
},
|
|
68
68
|
"dependencies": {
|
|
69
69
|
"@aws-sdk/client-organizations": "^3.1038.0",
|
|
70
|
-
"@fjall/generator": "^
|
|
71
|
-
"@fjall/util": "^
|
|
70
|
+
"@fjall/generator": "^4.1.0",
|
|
71
|
+
"@fjall/util": "^4.1.0",
|
|
72
72
|
"constructs": "^10.6.0"
|
|
73
73
|
},
|
|
74
74
|
"overrides": {
|
|
@@ -82,5 +82,5 @@
|
|
|
82
82
|
"engines": {
|
|
83
83
|
"node": ">=18.0.0"
|
|
84
84
|
},
|
|
85
|
-
"gitHead": "
|
|
85
|
+
"gitHead": "09dee795753839b82aedbb7d0cef1c5ef283df55"
|
|
86
86
|
}
|