@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.
@@ -47,7 +47,16 @@ async function fetchWithRetry(path) {
47
47
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
48
48
  });
49
49
  if (response.ok) {
50
- return await response.json();
50
+ const body = await response.text();
51
+ try {
52
+ return JSON.parse(body);
53
+ } catch {
54
+ // The body carries the parameter's value or the secret string, and
55
+ // V8's message quotes a window of it, so only its length is named.
56
+ throw new Error(
57
+ `Extension returned a body that is not valid JSON (${body.length} characters)`
58
+ );
59
+ }
51
60
  }
52
61
  // 4xx errors (e.g. secret not found) — don't retry
53
62
  if (response.status >= 400 && response.status < 500) {
@@ -2,7 +2,7 @@ import { type ICluster, type IBaseService, type TaskDefinition, type FargateTask
2
2
  import { type Connections, type ISecurityGroup } from "aws-cdk-lib/aws-ec2";
3
3
  import { type Grant, type IGrantable, type IRole } from "aws-cdk-lib/aws-iam";
4
4
  import { type IApplicationLoadBalancer, type ApplicationListener } from "aws-cdk-lib/aws-elasticloadbalancingv2";
5
- import { Construct } from "constructs";
5
+ import { Construct, type IDependable } from "constructs";
6
6
  import { type IEcsCompute } from "./interfaces/compute.js";
7
7
  import { type SecretImport } from "../../resources/aws/secrets/index.js";
8
8
  import EcsCluster, { type EcsClusterProps, type EcsIngressProfile } from "../../resources/aws/compute/ecs.js";
@@ -100,6 +100,10 @@ export declare class EcsCompute extends Construct implements IEcsCompute {
100
100
  private readonly clusterId;
101
101
  private readonly appName;
102
102
  private readonly migrationTaskDefinitions;
103
+ /** The cluster's physical name, as a literal. */
104
+ private readonly clusterName;
105
+ /** Service name → its rollout barrier, made on first use. */
106
+ private readonly rolloutBarriers;
103
107
  /**
104
108
  * Service name → exact `@fjall/schema-gate` semver, for every service that
105
109
  * materialised the gate container with the DEFAULT image this synth.
@@ -151,11 +155,6 @@ export declare class EcsCompute extends Construct implements IEcsCompute {
151
155
  * - `migrations.mode: "post-deploy"` — expected is deliberately ahead of
152
156
  * the DB during scale-up, so a materialised gate would refuse every task
153
157
  * (synth warning)
154
- * - `migrations.mode: "lifecycle-hook"` WITHOUT `separateTaskDef` — the
155
- * hook's migration RunTask reuses the service task definition, so a
156
- * materialised gate would refuse the migration task itself while the DB
157
- * still holds the pre-migration schema (synth warning; adding
158
- * `separateTaskDef` restores full gating)
159
158
  * - No eligible half survives the v1 scope filters: the relational half
160
159
  * requires `tool: "prisma"` and a postgres-family connection scheme
161
160
  * (per-half synth warnings; env injection into app containers stays on)
@@ -195,30 +194,45 @@ export declare class EcsCompute extends Construct implements IEcsCompute {
195
194
  * Without `awaitMigrationsFrom` the sibling rolls out in parallel with the
196
195
  * owner, so its gate-refused task churn can trip the deployment circuit
197
196
  * breaker before the migration lands. The gate refusing is by design (new
198
- * code must not serve the old schema); the missing DependsOn is the
199
- * author-fixable part — warn, don't disable.
197
+ * code must not serve the old schema); the missing ordering on the owner's
198
+ * rollout barrier is the author-fixable part — warn, don't disable.
200
199
  */
201
200
  private warnUngatedSiblingsOfHookOwners;
202
201
  /**
203
202
  * For each service whose `migrations.mode` is a lambda-hook variant
204
203
  * (`"lifecycle-hook"` for PRE_SCALE_UP, `"post-deploy"` for POST_SCALE_UP),
205
- * synthesise the Lambda + IAM role + log group that backs the deployment
206
- * lifecycle hook. The init-container path is unaffected — services without
207
- * `mode` (or with `mode: "init-container"`) still get the synthetic
208
- * migrate container injected by `expandMigrationsSugar`.
204
+ * synthesise the migration's own task definition and the Lambda + IAM role
205
+ * + log group that backs the deployment lifecycle hook. Every hook
206
+ * migration runs from a task definition of its own, which the service's
207
+ * task definition names in its `fjall:migration-task-definition` tag and
208
+ * its primary container's docker label: the hook reads the tag from the
209
+ * revision each deployment targets, and the label where a copy carries no
210
+ * tag. The
211
+ * init-container path is unaffected — services without `mode` (or with
212
+ * `mode: "init-container"`) still get the synthetic migrate container
213
+ * injected by `expandMigrationsSugar`.
209
214
  */
210
215
  private wireLifecycleHookMigrations;
211
216
  /**
212
- * Wire a native CloudFormation `DependsOn` from each service that declares
213
- * `awaitMigrationsFrom` to the named migrate-owning service, so CFN does not
214
- * roll out the dependent service until the target's ECS deployment — including
215
- * its PRE_SCALE_UP migrate hook — has completed. Prevents the RDS-IAM auth /
216
- * schema-mismatch race when a DB-connecting worker starts before the app's
217
- * migrations have applied, and enables a single-pass greenfield deploy.
218
- * Shape (target exists, declares hook migrations, no self / cycle) is already
219
- * guaranteed by `validateEcsProps`; the undefined guards are synth invariants.
217
+ * Orders each service that declares `awaitMigrationsFrom` after the named
218
+ * migrate-owning service's rollout, so a DB-connecting worker never starts
219
+ * before the owner's migrations have applied. Each owner gets one rollout
220
+ * barrier (`EcsRolloutBarrier`): a custom resource that completes only once
221
+ * ECS reports the owner's deployment of its task definition finished, its
222
+ * lifecycle-hook migration included, and fails once ECS rolled that
223
+ * deployment back. Every dependant's `DependsOn` names the barrier.
224
+ *
225
+ * A `DependsOn` on the owner service itself no longer orders this: since
226
+ * 2026-10-01 CloudFormation starts the dependants of an updating ECS
227
+ * service at its eventual-consistency check, 20-31 s into the update, while
228
+ * a custom resource's dependants wait for its response (FJL-901).
229
+ *
230
+ * Shape (target exists, declares hook migrations, no self / cycle) is
231
+ * already guaranteed by `validateEcsProps`.
220
232
  */
221
233
  private wireServiceMigrationDependencies;
234
+ /** The service's one rollout barrier, made on first use. */
235
+ private rolloutBarrierOf;
222
236
  /**
223
237
  * Synthesise a dedicated migration task definition for a lifecycle-hook
224
238
  * migration when `separateTaskDef` is set. Creates the migration's own
@@ -227,6 +241,19 @@ export declare class EcsCompute extends Construct implements IEcsCompute {
227
241
  * with the migration container baked in.
228
242
  */
229
243
  private synthesiseMigrationTaskDef;
244
+ /**
245
+ * The migration task definition of a lifecycle-hook migration without
246
+ * `separateTaskDef`. It runs what the hook's `RunTask` of the service's
247
+ * own task definition, its command and environment overridden, ran until
248
+ * FJL-901: as the service's own roles, at the service's size, in the
249
+ * service's log group, from the service's image, with the primary
250
+ * container's environment, entry point and secrets beneath the
251
+ * migration's own. A connected database's contributed secrets and role
252
+ * policies, such as its schema administrator's, reach only a
253
+ * `separateTaskDef` migration, whose roles are its own, so they never
254
+ * widen the service's.
255
+ */
256
+ private synthesiseInheritedMigrationTaskDef;
230
257
  private resolveMigrationSecrets;
231
258
  /** Get the ECS cluster. */
232
259
  getCluster(): ICluster;
@@ -249,9 +276,26 @@ export declare class EcsCompute extends Construct implements IEcsCompute {
249
276
  * Get the migration task definition for a service. Returns `undefined` when
250
277
  * the service has no `migrations: { mode: "lifecycle-hook", separateTaskDef }`
251
278
  * configured — escape hatch for callers that need to attach grants (e.g.
252
- * `bucket.grantReadWrite(td.taskRole)`) to the migration task role.
279
+ * `bucket.grantReadWrite(td.taskRole)`) to the migration task role. Without
280
+ * `separateTaskDef` the migration runs in a task definition of its own that
281
+ * this does not return: its task role is the service's, so a grant to it
282
+ * would widen the service.
253
283
  */
254
284
  getMigrationTaskDefinition(serviceName: string): FargateTaskDefinition | undefined;
285
+ /**
286
+ * The rollout barrier of a service: a custom resource that completes only
287
+ * once ECS reports the service's deployment of its task definition
288
+ * finished, and fails once ECS rolled that deployment back. A construct
289
+ * that depends on it is created or updated only after the service has
290
+ * rolled out, for orderings `awaitMigrationsFrom` cannot express:
291
+ * `compute.getService("worker")?.node.addDependency(compute.getRolloutBarrier("app"))`.
292
+ * A `DependsOn` on the service itself does not order this: CloudFormation
293
+ * may start a service's dependants at its eventual-consistency check, long
294
+ * before its deployment finishes (FJL-901). A service has one barrier,
295
+ * shared with `awaitMigrationsFrom`; one the cluster does not run is
296
+ * refused.
297
+ */
298
+ getRolloutBarrier(serviceName: string): IDependable;
255
299
  /** Get the security group for the cluster. */
256
300
  getSecurityGroup(): ISecurityGroup;
257
301
  /**