alchemy 2.0.0-beta.68 → 2.0.0-beta.69
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/bin/exec.js +124 -16
- package/bin/exec.js.map +1 -1
- package/lib/Apply.d.ts.map +1 -1
- package/lib/Apply.js +33 -0
- package/lib/Apply.js.map +1 -1
- package/lib/Cli/LoggingCli.d.ts.map +1 -1
- package/lib/Cli/LoggingCli.js +9 -1
- package/lib/Cli/LoggingCli.js.map +1 -1
- package/lib/Cloudflare/Access/Group.d.ts +11 -2
- package/lib/Cloudflare/Access/Group.d.ts.map +1 -1
- package/lib/Cloudflare/Access/Group.js.map +1 -1
- package/lib/Cloudflare/Access/Policy.d.ts +11 -2
- package/lib/Cloudflare/Access/Policy.d.ts.map +1 -1
- package/lib/Cloudflare/Access/Policy.js.map +1 -1
- package/lib/Cloudflare/Account/Member.d.ts +1 -1
- package/lib/Cloudflare/Account/Member.d.ts.map +1 -1
- package/lib/Cloudflare/Website/StaticSite.d.ts.map +1 -1
- package/lib/Cloudflare/Website/StaticSite.js +8 -1
- package/lib/Cloudflare/Website/StaticSite.js.map +1 -1
- package/lib/Plan.d.ts +23 -4
- package/lib/Plan.d.ts.map +1 -1
- package/lib/Plan.js +226 -22
- package/lib/Plan.js.map +1 -1
- package/lib/Rename.d.ts +132 -0
- package/lib/Rename.d.ts.map +1 -0
- package/lib/Rename.js +117 -0
- package/lib/Rename.js.map +1 -0
- package/lib/Resource.d.ts +9 -0
- package/lib/Resource.d.ts.map +1 -1
- package/lib/Resource.js +11 -0
- package/lib/Resource.js.map +1 -1
- package/lib/State/HttpStateApi.d.ts +14 -14
- package/lib/Sync.d.ts.map +1 -1
- package/lib/Sync.js +1 -0
- package/lib/Sync.js.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/package.json +4 -4
- package/src/Apply.ts +47 -0
- package/src/Cli/LoggingCli.ts +10 -1
- package/src/Cloudflare/Access/Group.ts +16 -2
- package/src/Cloudflare/Access/Policy.ts +16 -2
- package/src/Cloudflare/Website/StaticSite.ts +8 -1
- package/src/Plan.ts +284 -28
- package/src/Rename.ts +135 -0
- package/src/Resource.ts +27 -0
- package/src/Sync.ts +1 -0
- package/src/index.ts +1 -0
|
@@ -24,6 +24,20 @@ import type { Providers } from "../Providers.ts";
|
|
|
24
24
|
export type GroupRule =
|
|
25
25
|
zeroTrust.CreateAccessGroupForAccountRequest["include"][number];
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* One arm of the exclude-side rule union, and its require-side twin.
|
|
29
|
+
* Cloudflare's spec types the exclude/require rule lists separately from
|
|
30
|
+
* include — a few rule kinds (e.g. the GitHub-organization rule) carry the
|
|
31
|
+
* raw wire shape there — so these props use the SDK's own unions rather
|
|
32
|
+
* than reusing {@link GroupRule}.
|
|
33
|
+
*/
|
|
34
|
+
export type GroupExcludeRule = NonNullable<
|
|
35
|
+
zeroTrust.CreateAccessGroupForAccountRequest["exclude"]
|
|
36
|
+
>[number];
|
|
37
|
+
export type GroupRequireRule = NonNullable<
|
|
38
|
+
zeroTrust.CreateAccessGroupForAccountRequest["require"]
|
|
39
|
+
>[number];
|
|
40
|
+
|
|
27
41
|
export type GroupProps = {
|
|
28
42
|
/**
|
|
29
43
|
* Display name for the group. Used as a stable identifier so the provider
|
|
@@ -42,12 +56,12 @@ export type GroupProps = {
|
|
|
42
56
|
* Rules combined with logical NOT. A user matching any Exclude rule does
|
|
43
57
|
* not match the group, even if they satisfied an Include rule.
|
|
44
58
|
*/
|
|
45
|
-
exclude?:
|
|
59
|
+
exclude?: GroupExcludeRule[];
|
|
46
60
|
/**
|
|
47
61
|
* Rules combined with logical AND. A user must satisfy every Require rule
|
|
48
62
|
* in addition to an Include rule.
|
|
49
63
|
*/
|
|
50
|
-
require?:
|
|
64
|
+
require?: GroupRequireRule[];
|
|
51
65
|
/**
|
|
52
66
|
* Whether this is the default group for the Zero Trust organization.
|
|
53
67
|
*
|
|
@@ -21,6 +21,20 @@ import type { Providers } from "../Providers.ts";
|
|
|
21
21
|
*/
|
|
22
22
|
export type PolicyRule = zeroTrust.CreateAccessPolicyRequest["include"][number];
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* One arm of the exclude-side rule union, and its require-side twin.
|
|
26
|
+
* Cloudflare's spec types the exclude/require rule lists separately from
|
|
27
|
+
* include — a few rule kinds (e.g. the GitHub-organization rule) carry the
|
|
28
|
+
* raw wire shape there — so these props use the SDK's own unions rather
|
|
29
|
+
* than reusing {@link PolicyRule}.
|
|
30
|
+
*/
|
|
31
|
+
export type PolicyExcludeRule = NonNullable<
|
|
32
|
+
zeroTrust.CreateAccessPolicyRequest["exclude"]
|
|
33
|
+
>[number];
|
|
34
|
+
export type PolicyRequireRule = NonNullable<
|
|
35
|
+
zeroTrust.CreateAccessPolicyRequest["require"]
|
|
36
|
+
>[number];
|
|
37
|
+
|
|
24
38
|
/**
|
|
25
39
|
* Decision Cloudflare Access takes when a request matches this policy.
|
|
26
40
|
*
|
|
@@ -61,12 +75,12 @@ export type PolicyProps = {
|
|
|
61
75
|
* Rules combined with logical NOT. A request matching any exclude rule is
|
|
62
76
|
* rejected by the policy even if it satisfied an include rule.
|
|
63
77
|
*/
|
|
64
|
-
exclude?:
|
|
78
|
+
exclude?: PolicyExcludeRule[];
|
|
65
79
|
/**
|
|
66
80
|
* Rules combined with logical AND. A request must satisfy every require
|
|
67
81
|
* rule in addition to an include rule.
|
|
68
82
|
*/
|
|
69
|
-
require?:
|
|
83
|
+
require?: PolicyRequireRule[];
|
|
70
84
|
/**
|
|
71
85
|
* Duration of issued session tokens. Format: `300ms`, `2h45m`, etc. When
|
|
72
86
|
* unset, applications using this policy fall back to their own configured
|
|
@@ -6,6 +6,7 @@ import * as Command from "../../Command/index.ts";
|
|
|
6
6
|
import type { Input, InputProps } from "../../Input.ts";
|
|
7
7
|
import * as Namespace from "../../Namespace.ts";
|
|
8
8
|
import * as Output from "../../Output.ts";
|
|
9
|
+
import { renamedFrom } from "../../Rename.ts";
|
|
9
10
|
import {
|
|
10
11
|
effectClass,
|
|
11
12
|
isYieldableEffectLike,
|
|
@@ -297,6 +298,12 @@ const makeStaticSite = <
|
|
|
297
298
|
// Pure-static sites (neither `main` nor `script`) deploy as
|
|
298
299
|
// assets-only Workers: no script is uploaded and Cloudflare's asset
|
|
299
300
|
// layer serves every request itself.
|
|
301
|
+
//
|
|
302
|
+
// The Worker's FQN was `<id>/Worker` before #1053 flattened it to
|
|
303
|
+
// `<id>`; `renamedFrom` migrates the pre-existing state row to the new
|
|
304
|
+
// FQN instead of letting the engine plan a create+delete replacement
|
|
305
|
+
// (a new physical name, a torn-down workers.dev URL, and a
|
|
306
|
+
// custom-domain handover that crashes the deploy).
|
|
300
307
|
return yield* Worker<Bindings, WorkerAssetsConfig, Req>(id, {
|
|
301
308
|
...props,
|
|
302
309
|
assets: build
|
|
@@ -311,7 +318,7 @@ const makeStaticSite = <
|
|
|
311
318
|
// state with a stub Attributes shape.
|
|
312
319
|
dev: dev ? { mode: "external", url: dev.url } : undefined,
|
|
313
320
|
script: props.script,
|
|
314
|
-
});
|
|
321
|
+
}).pipe(renamedFrom(`${id}/Worker`));
|
|
315
322
|
});
|
|
316
323
|
|
|
317
324
|
/**
|
package/src/Plan.ts
CHANGED
|
@@ -117,9 +117,31 @@ export interface BaseNode<
|
|
|
117
117
|
bindings: BindingNode<R["Binding"]>[];
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Base for the apply-side nodes (create/update/replace/noop) — the nodes a
|
|
122
|
+
* DECLARED resource plans to. Only these can carry a rename: a `Delete`
|
|
123
|
+
* node is an orphaned row with no declaration, so nothing can claim to
|
|
124
|
+
* have renamed it (migrating rows are excluded from the orphan pass
|
|
125
|
+
* entirely).
|
|
126
|
+
*/
|
|
127
|
+
export interface ApplyNodeBase<
|
|
128
|
+
R extends ResourceLike<string> = ResourceLike<string>,
|
|
129
|
+
> extends BaseNode<R> {
|
|
130
|
+
/**
|
|
131
|
+
* Set when this resource's persisted row was found under former FQNs
|
|
132
|
+
* (`renamedFrom(...)`) — the migration source (no row at the current FQN
|
|
133
|
+
* yet) and/or stale leftovers from interrupted migrations (same
|
|
134
|
+
* `instanceId` as the resource's row; a rename chain with repeated
|
|
135
|
+
* partial failures can leave several). Apply persists the move up-front,
|
|
136
|
+
* before any lifecycle operation runs: commit `state` at the current
|
|
137
|
+
* FQN, then delete every former row.
|
|
138
|
+
*/
|
|
139
|
+
renamedFrom?: string[] | undefined;
|
|
140
|
+
}
|
|
141
|
+
|
|
120
142
|
export interface Create<
|
|
121
143
|
R extends ResourceLike = ResourceLike,
|
|
122
|
-
> extends
|
|
144
|
+
> extends ApplyNodeBase<R> {
|
|
123
145
|
action: "create";
|
|
124
146
|
props: R["Props"];
|
|
125
147
|
state: CreatingResourceState | undefined;
|
|
@@ -127,7 +149,7 @@ export interface Create<
|
|
|
127
149
|
|
|
128
150
|
export interface Update<
|
|
129
151
|
R extends ResourceLike = ResourceLike,
|
|
130
|
-
> extends
|
|
152
|
+
> extends ApplyNodeBase<R> {
|
|
131
153
|
action: "update";
|
|
132
154
|
/** True while this is the first reconcile after a cold adoption. */
|
|
133
155
|
adopting?: boolean;
|
|
@@ -151,14 +173,14 @@ export interface Delete<
|
|
|
151
173
|
|
|
152
174
|
export interface NoopUpdate<
|
|
153
175
|
R extends ResourceLike = ResourceLike,
|
|
154
|
-
> extends
|
|
176
|
+
> extends ApplyNodeBase<R> {
|
|
155
177
|
action: "noop";
|
|
156
178
|
state: CreatedResourceState | UpdatedResourceState;
|
|
157
179
|
}
|
|
158
180
|
|
|
159
181
|
export interface Replace<
|
|
160
182
|
R extends ResourceLike = ResourceLike,
|
|
161
|
-
> extends
|
|
183
|
+
> extends ApplyNodeBase<R> {
|
|
162
184
|
action: "replace";
|
|
163
185
|
props: any;
|
|
164
186
|
deleteFirst: boolean;
|
|
@@ -344,6 +366,211 @@ export const make = <A>(
|
|
|
344
366
|
{ concurrency: "unbounded" },
|
|
345
367
|
);
|
|
346
368
|
|
|
369
|
+
// Snapshot of every persisted row, keyed by FQN. The rename resolution
|
|
370
|
+
// below reads from this map instead of issuing per-FQN `state.get`s —
|
|
371
|
+
// every renamedFrom-decorated resource (each StaticSite carries one
|
|
372
|
+
// forever) would otherwise add two round-trips per resource to every
|
|
373
|
+
// plan against a remote state store. Plan is read-only, so the
|
|
374
|
+
// snapshot cannot go stale within this run.
|
|
375
|
+
const persistedRows = new Map(
|
|
376
|
+
resourceFqns.map((fqn, i) => [fqn, oldResources[i]]),
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
// ── FQN renames ──────────────────────────────────────────────────────
|
|
380
|
+
// Map every former FQN claimed via `renamedFrom(...)` to its claimant's
|
|
381
|
+
// current FQN. Two resources claiming the same former FQN is ambiguous
|
|
382
|
+
// and fatal. A former FQN MAY still be actively declared — that is the
|
|
383
|
+
// "old id reused by a new resource" case: the rename claim wins the
|
|
384
|
+
// row (it is an explicit user statement that the row was theirs), and
|
|
385
|
+
// the reusing resource plans a fresh create.
|
|
386
|
+
const formerFqnClaims = new Map<string, string>();
|
|
387
|
+
for (const resource of resources) {
|
|
388
|
+
for (const formerFqn of resource.FormerFqns ?? []) {
|
|
389
|
+
if (formerFqn === resource.FQN) continue;
|
|
390
|
+
const claimant = formerFqnClaims.get(formerFqn);
|
|
391
|
+
if (claimant !== undefined && claimant !== resource.FQN) {
|
|
392
|
+
return yield* Effect.die(
|
|
393
|
+
new Error(
|
|
394
|
+
`Resources '${claimant}' and '${resource.FQN}' both claim ` +
|
|
395
|
+
`former FQN '${formerFqn}' via renamedFrom(...). A former ` +
|
|
396
|
+
"FQN can migrate to exactly one resource — remove the " +
|
|
397
|
+
"decoration from one of them.",
|
|
398
|
+
),
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
formerFqnClaims.set(formerFqn, resource.FQN);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Resolve every rename migration up-front against the state snapshot,
|
|
406
|
+
// BEFORE any node is built — other resources' planning depends on the
|
|
407
|
+
// outcome (a resource declared at a former FQN whose row is migrating
|
|
408
|
+
// away must start from scratch).
|
|
409
|
+
//
|
|
410
|
+
// For each renamer, in former-id declaration order (most recent
|
|
411
|
+
// first):
|
|
412
|
+
//
|
|
413
|
+
// - `source` is the row that defines the resource's physical identity:
|
|
414
|
+
// the row at its own FQN when present AND type-matching, otherwise
|
|
415
|
+
// the first type-matching former row (→ `moved`).
|
|
416
|
+
// - every OTHER type-matching former row sharing `source.instanceId`
|
|
417
|
+
// is a leftover from an interrupted migration (only migration copies
|
|
418
|
+
// instanceIds) — collected for state-only cleanup.
|
|
419
|
+
// - former rows with a different instanceId belong to someone else and
|
|
420
|
+
// are left to normal orphan handling; former rows with a different
|
|
421
|
+
// resourceType (modulo registered type-aliases) can never be this
|
|
422
|
+
// resource's row and are skipped entirely.
|
|
423
|
+
// - a FOREIGN-typed row at the renamer's own FQN blocks the migration
|
|
424
|
+
// fatally: landing the migrated row there would silently abandon
|
|
425
|
+
// that row's cloud resource.
|
|
426
|
+
const renameMigrations = new Map<
|
|
427
|
+
string,
|
|
428
|
+
{ row: ResourceState; renamedFrom: string[]; moved: boolean }
|
|
429
|
+
>();
|
|
430
|
+
const migratedRowFqns = new Set<string>();
|
|
431
|
+
|
|
432
|
+
const resolveRenamer = Effect.fn(function* (resource: ResourceLike) {
|
|
433
|
+
const provider = Option.getOrUndefined(
|
|
434
|
+
yield* tryFindProviderByType(resource.Type),
|
|
435
|
+
);
|
|
436
|
+
const allowedTypes = new Set([
|
|
437
|
+
resource.Type,
|
|
438
|
+
...(provider?.aliases ?? []),
|
|
439
|
+
]);
|
|
440
|
+
const persisted = persistedRows.get(resource.FQN);
|
|
441
|
+
const persistedRow = isActionState(persisted)
|
|
442
|
+
? undefined
|
|
443
|
+
: (persisted as ResourceState | undefined);
|
|
444
|
+
// A row at this resource's own FQN that a PREVIOUSLY RESOLVED
|
|
445
|
+
// renamer claimed (a same-deploy shift: A→B while B→C — C took the
|
|
446
|
+
// row at B) is moving away: it is not ours to keep and it does not
|
|
447
|
+
// block the migration landing here.
|
|
448
|
+
const rowTaken = migratedRowFqns.has(resource.FQN);
|
|
449
|
+
const ownRow =
|
|
450
|
+
!rowTaken &&
|
|
451
|
+
persistedRow !== undefined &&
|
|
452
|
+
allowedTypes.has(persistedRow.resourceType)
|
|
453
|
+
? persistedRow
|
|
454
|
+
: undefined;
|
|
455
|
+
|
|
456
|
+
let source: ResourceState | undefined = ownRow;
|
|
457
|
+
let adopted: ResourceState | undefined = ownRow;
|
|
458
|
+
const renamedFrom: string[] = [];
|
|
459
|
+
for (const formerFqn of resource.FormerFqns!) {
|
|
460
|
+
if (formerFqnClaims.get(formerFqn) !== resource.FQN) continue;
|
|
461
|
+
// The same former id may be listed twice (or resolve identically);
|
|
462
|
+
// collect each former row once.
|
|
463
|
+
if (renamedFrom.includes(formerFqn)) continue;
|
|
464
|
+
const formerPersisted = persistedRows.get(formerFqn);
|
|
465
|
+
if (formerPersisted === undefined || isActionState(formerPersisted)) {
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
const formerRow = formerPersisted as ResourceState;
|
|
469
|
+
if (!allowedTypes.has(formerRow.resourceType)) continue;
|
|
470
|
+
if (source === undefined) {
|
|
471
|
+
source = formerRow;
|
|
472
|
+
adopted = {
|
|
473
|
+
...formerRow,
|
|
474
|
+
fqn: resource.FQN,
|
|
475
|
+
logicalId: resource.LogicalId,
|
|
476
|
+
namespace: resource.Namespace,
|
|
477
|
+
} as ResourceState;
|
|
478
|
+
renamedFrom.push(formerFqn);
|
|
479
|
+
} else if (source.instanceId === formerRow.instanceId) {
|
|
480
|
+
renamedFrom.push(formerFqn);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
if (renamedFrom.length === 0) return;
|
|
484
|
+
if (persistedRow !== undefined && ownRow === undefined && !rowTaken) {
|
|
485
|
+
return yield* Effect.die(
|
|
486
|
+
new Error(
|
|
487
|
+
`Cannot migrate '${renamedFrom[0]}' to '${resource.FQN}': a ` +
|
|
488
|
+
`state row of a different type ('${persistedRow.resourceType}') ` +
|
|
489
|
+
`already occupies '${resource.FQN}'. Migrating over it would ` +
|
|
490
|
+
"silently abandon that row's cloud resource. Delete or " +
|
|
491
|
+
"rename the conflicting resource first, then re-deploy.",
|
|
492
|
+
),
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
renameMigrations.set(resource.FQN, {
|
|
496
|
+
row: adopted!,
|
|
497
|
+
renamedFrom,
|
|
498
|
+
moved: adopted !== ownRow,
|
|
499
|
+
});
|
|
500
|
+
for (const formerFqn of renamedFrom) migratedRowFqns.add(formerFqn);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
// Resolve renamers in claim-dependency order: when resource R's OWN
|
|
504
|
+
// FQN is claimed as a former id by resource S (a same-deploy shift:
|
|
505
|
+
// A→B while B→C), S must resolve first — whether S takes R's row
|
|
506
|
+
// decides whether R still owns it, or falls back to ITS former rows.
|
|
507
|
+
// Iterate to fixpoint; anything left is a claim CYCLE (a swap: A⇄B),
|
|
508
|
+
// which cannot be persisted safely — the migrations would overwrite
|
|
509
|
+
// and delete each other's rows — and dies loudly.
|
|
510
|
+
let pendingRenamers = resources.filter((r) => r.FormerFqns?.length);
|
|
511
|
+
const resolvedRenamers = new Set<string>();
|
|
512
|
+
while (pendingRenamers.length > 0) {
|
|
513
|
+
const ready = pendingRenamers.filter((resource) => {
|
|
514
|
+
const claimant = formerFqnClaims.get(resource.FQN);
|
|
515
|
+
return claimant === undefined || resolvedRenamers.has(claimant);
|
|
516
|
+
});
|
|
517
|
+
if (ready.length === 0) {
|
|
518
|
+
return yield* Effect.die(
|
|
519
|
+
new Error(
|
|
520
|
+
`Rename cycle detected among [${pendingRenamers
|
|
521
|
+
.map((r) => `'${r.FQN}'`)
|
|
522
|
+
.join(
|
|
523
|
+
", ",
|
|
524
|
+
)}]: their renamedFrom(...) declarations claim each other's ` +
|
|
525
|
+
"FQNs. Swapping ids in one deploy is not supported — rename " +
|
|
526
|
+
"through a temporary id across two deploys instead.",
|
|
527
|
+
),
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
for (const resource of ready) {
|
|
531
|
+
yield* resolveRenamer(resource);
|
|
532
|
+
resolvedRenamers.add(resource.FQN);
|
|
533
|
+
}
|
|
534
|
+
pendingRenamers = pendingRenamers.filter(
|
|
535
|
+
(r) => !resolvedRenamers.has(r.FQN),
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Fetch the persisted row for a declared resource, resolving renames
|
|
541
|
+
* (see the pre-computed `renameMigrations` above):
|
|
542
|
+
*
|
|
543
|
+
* - a renamer plans from its migrated row (`renamedFrom` rides onto
|
|
544
|
+
* the plan node; apply persists the move before any lifecycle op)
|
|
545
|
+
* - a resource declared at a former FQN whose row is migrating away
|
|
546
|
+
* starts from scratch — the row is NOT its state, whatever the FQN
|
|
547
|
+
* says
|
|
548
|
+
*/
|
|
549
|
+
const getPersistedRow = Effect.fn(function* (
|
|
550
|
+
resource: Pick<ResourceLike, "FQN">,
|
|
551
|
+
) {
|
|
552
|
+
const migration = renameMigrations.get(resource.FQN);
|
|
553
|
+
if (migration !== undefined) {
|
|
554
|
+
return {
|
|
555
|
+
row: migration.row,
|
|
556
|
+
renamedFrom: migration.renamedFrom,
|
|
557
|
+
renameMoved: migration.moved,
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
const persisted = yield* state.get({
|
|
561
|
+
stack: stackName,
|
|
562
|
+
stage: stage,
|
|
563
|
+
fqn: resource.FQN,
|
|
564
|
+
});
|
|
565
|
+
const row = isActionState(persisted)
|
|
566
|
+
? undefined
|
|
567
|
+
: (persisted as ResourceState | undefined);
|
|
568
|
+
if (row !== undefined && migratedRowFqns.has(resource.FQN)) {
|
|
569
|
+
return { row: undefined, renamedFrom: undefined, renameMoved: false };
|
|
570
|
+
}
|
|
571
|
+
return { row, renamedFrom: undefined, renameMoved: false };
|
|
572
|
+
});
|
|
573
|
+
|
|
347
574
|
const resolvedResources: Record<string, Effect.Effect<any>> = {};
|
|
348
575
|
|
|
349
576
|
const resolveResource = (
|
|
@@ -367,16 +594,10 @@ export const make = <A>(
|
|
|
367
594
|
const props = materializeStableRefs(
|
|
368
595
|
yield* resolveInput(resource.Props),
|
|
369
596
|
);
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
});
|
|
375
|
-
const oldState: ResourceState | undefined = isActionState(
|
|
376
|
-
persisted,
|
|
377
|
-
)
|
|
378
|
-
? undefined
|
|
379
|
-
: (persisted as ResourceState | undefined);
|
|
597
|
+
// Falls back to the row at a former FQN (`renamedFrom`) so a
|
|
598
|
+
// renamed resource's stable attributes keep flowing to
|
|
599
|
+
// downstream diffs across the migration.
|
|
600
|
+
const { row: oldState } = yield* getPersistedRow(resource);
|
|
380
601
|
|
|
381
602
|
if (!oldState || oldState.status === "creating") {
|
|
382
603
|
return resourceExpr;
|
|
@@ -865,17 +1086,18 @@ export const make = <A>(
|
|
|
865
1086
|
// `bindingOutputs`, not these plan-time shapes.
|
|
866
1087
|
const newBindings: ResourceBinding[] =
|
|
867
1088
|
materializeStableRefs(applyBindings);
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
//
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
1089
|
+
// The row is looked up at the resource's FQN with a fallback to
|
|
1090
|
+
// its former FQNs (`renamedFrom`); a row found under a former
|
|
1091
|
+
// FQN arrives here already remapped to the new identity, and
|
|
1092
|
+
// `renamedFrom` rides onto the plan node so apply persists the
|
|
1093
|
+
// move. (A Task previously holding this FQN is treated as no
|
|
1094
|
+
// prior state — its row is reaped by `actionDeletions` below.)
|
|
1095
|
+
const {
|
|
1096
|
+
row: persistedRow,
|
|
1097
|
+
renamedFrom,
|
|
1098
|
+
renameMoved,
|
|
1099
|
+
} = yield* getPersistedRow(resource);
|
|
1100
|
+
let oldState: ResourceState | undefined = persistedRow;
|
|
879
1101
|
|
|
880
1102
|
// Engine-level adoption. When there is no prior state, always
|
|
881
1103
|
// consult `provider.read` (if implemented) so the engine — not
|
|
@@ -925,8 +1147,20 @@ export const make = <A>(
|
|
|
925
1147
|
// SDK protocol layer. Resources whose props depend on
|
|
926
1148
|
// not-yet-created upstreams cannot themselves be pre-existing
|
|
927
1149
|
// — there's nothing to adopt.
|
|
1150
|
+
// A resource declared at a former FQN whose row just migrated
|
|
1151
|
+
// away is genuinely NEW by declaration — skip the probe. Its
|
|
1152
|
+
// predecessor's physical resource still carries tags branded
|
|
1153
|
+
// with THIS logical id (the migrated row's reconcile hasn't
|
|
1154
|
+
// re-branded them yet), so a tag-based `read` would find it
|
|
1155
|
+
// and silently adopt the very resource that was renamed away.
|
|
1156
|
+
const reusesMigratedFqn = migratedRowFqns.has(fqn);
|
|
928
1157
|
let forceUpdateAfterAdoption = false;
|
|
929
|
-
if (
|
|
1158
|
+
if (
|
|
1159
|
+
oldState === undefined &&
|
|
1160
|
+
provider.read &&
|
|
1161
|
+
isResolved(news) &&
|
|
1162
|
+
!reusesMigratedFqn
|
|
1163
|
+
) {
|
|
930
1164
|
const adoptInstanceId = yield* generateInstanceId();
|
|
931
1165
|
const readResult = yield* provider
|
|
932
1166
|
.read({
|
|
@@ -1019,6 +1253,7 @@ export const make = <A>(
|
|
|
1019
1253
|
bindings: bindingDiffs,
|
|
1020
1254
|
downstream,
|
|
1021
1255
|
mode,
|
|
1256
|
+
renamedFrom,
|
|
1022
1257
|
}) as any as T;
|
|
1023
1258
|
|
|
1024
1259
|
// Plan against the persisted state we have, not the ideal final state we
|
|
@@ -1159,8 +1394,16 @@ export const make = <A>(
|
|
|
1159
1394
|
// would noop and any drift between the existing cloud
|
|
1160
1395
|
// resource and `news` — including foreign-owned tags after a
|
|
1161
1396
|
// takeover — would persist).
|
|
1397
|
+
//
|
|
1398
|
+
// A row that just migrated from a former FQN (`renameMoved`)
|
|
1399
|
+
// gets the same treatment: its cloud resource is still
|
|
1400
|
+
// branded with the OLD logical id's tags, and if the old id
|
|
1401
|
+
// is being reused by a new resource, leaving them stale
|
|
1402
|
+
// would let the reuser's future adoption probes match the
|
|
1403
|
+
// wrong physical resource.
|
|
1162
1404
|
Effect.map((diff) =>
|
|
1163
|
-
forceUpdateAfterAdoption
|
|
1405
|
+
(forceUpdateAfterAdoption || renameMoved) &&
|
|
1406
|
+
diff.action === "noop"
|
|
1164
1407
|
? ({ action: "update" } satisfies UpdateDiff)
|
|
1165
1408
|
: diff,
|
|
1166
1409
|
),
|
|
@@ -1519,6 +1762,15 @@ export const make = <A>(
|
|
|
1519
1762
|
if (isActionState(persisted)) return;
|
|
1520
1763
|
const oldState = persisted as ResourceState | undefined;
|
|
1521
1764
|
if (oldState) {
|
|
1765
|
+
// A row being migrated by a rename (`renameMigrations`) is
|
|
1766
|
+
// moving, not orphaned — apply drops it state-only after
|
|
1767
|
+
// committing the migrated row at its new FQN. Rows at former
|
|
1768
|
+
// FQNs that did NOT migrate (foreign type, different
|
|
1769
|
+
// instanceId, unclaimed) are absent from this set and fall
|
|
1770
|
+
// through to normal orphan deletion.
|
|
1771
|
+
if (migratedRowFqns.has(fqn)) {
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1522
1774
|
const { logicalId } = parseFqn(fqn);
|
|
1523
1775
|
const resourceType = oldState.resourceType;
|
|
1524
1776
|
// A "zombie" row references a type with no registered provider
|
|
@@ -1566,6 +1818,7 @@ export const make = <A>(
|
|
|
1566
1818
|
RemovalPolicy: oldState.removalPolicy,
|
|
1567
1819
|
Adopt: undefined,
|
|
1568
1820
|
Mode: oldState.providerMode,
|
|
1821
|
+
FormerFqns: undefined,
|
|
1569
1822
|
RuntimeContext: undefined!,
|
|
1570
1823
|
Providers: undefined,
|
|
1571
1824
|
} as ResourceLike,
|
|
@@ -1752,7 +2005,10 @@ export const printPlan = (plan: Plan): string => {
|
|
|
1752
2005
|
const downstream = node.state?.downstream?.length
|
|
1753
2006
|
? ` → [${node.state?.downstream.join(", ")}]`
|
|
1754
2007
|
: "";
|
|
1755
|
-
|
|
2008
|
+
const renamed = node.renamedFrom?.length
|
|
2009
|
+
? ` (renamed from ${node.renamedFrom.join(", ")})`
|
|
2010
|
+
: "";
|
|
2011
|
+
lines.push(`│ [${symbol}] ${id} (${type})${downstream}${renamed}`);
|
|
1756
2012
|
}
|
|
1757
2013
|
if (resourceIds.length === 0) {
|
|
1758
2014
|
lines.push("│ (none)");
|
package/src/Rename.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import * as Context from "effect/Context";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A former id a resource was previously declared under:
|
|
6
|
+
*
|
|
7
|
+
* - a bare `string` resolves against the ambient namespace, exactly like
|
|
8
|
+
* the resource's own `id` argument does
|
|
9
|
+
* - `{ fqn: "..." }` is absolute — the full FQN as persisted in state,
|
|
10
|
+
* ignoring any surrounding namespace. Needed when a resource moved
|
|
11
|
+
* BETWEEN namespaces (a relative id can only address the current
|
|
12
|
+
* namespace's subtree).
|
|
13
|
+
*/
|
|
14
|
+
export type FormerId = string | { fqn: string };
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* RenamePolicy carries the former ids a resource was previously declared
|
|
18
|
+
* under. It is captured from the ambient context at registration time (like
|
|
19
|
+
* `AdoptPolicy` / `RemovalPolicy`) and resolved against the same namespace
|
|
20
|
+
* as the resource's own id — see `ResourceLike.FormerFqns`.
|
|
21
|
+
*/
|
|
22
|
+
export class RenamePolicy extends Context.Service<
|
|
23
|
+
RenamePolicy,
|
|
24
|
+
readonly FormerId[]
|
|
25
|
+
>()("RenamePolicy") {}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Declare the logical id(s) this resource was previously registered under,
|
|
29
|
+
* so the engine migrates its persisted state row instead of planning a
|
|
30
|
+
* create+delete replacement when the id changes.
|
|
31
|
+
*
|
|
32
|
+
* ```ts
|
|
33
|
+
* // was: Bucket("Bucket") — the row migrates and the deploy plans a noop
|
|
34
|
+
* const bucket = yield* Bucket("Assets").pipe(renamedFrom("Bucket"));
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* A bare string resolves against the ambient namespace, exactly like the
|
|
38
|
+
* resource's own `id`:
|
|
39
|
+
*
|
|
40
|
+
* ```ts
|
|
41
|
+
* // FQN `Site/Assets`, former FQN `Site/Bucket`
|
|
42
|
+
* yield* Bucket("Assets").pipe(
|
|
43
|
+
* renamedFrom("Bucket"),
|
|
44
|
+
* Namespace.push("Site"),
|
|
45
|
+
* );
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
48
|
+
* Moved between namespaces? Pass `{ fqn }` — the absolute FQN exactly as
|
|
49
|
+
* persisted in state, ignoring the ambient namespace:
|
|
50
|
+
*
|
|
51
|
+
* ```ts
|
|
52
|
+
* // former FQN `LegacySite/Assets` (NOT `NewSite/LegacySite/Assets`)
|
|
53
|
+
* yield* Bucket("Assets").pipe(
|
|
54
|
+
* renamedFrom({ fqn: "LegacySite/Assets" }),
|
|
55
|
+
* Namespace.push("NewSite"),
|
|
56
|
+
* );
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* Renamed more than once? List every former id, most recent first — the
|
|
60
|
+
* planner checks them in order and migrates from the first matching row:
|
|
61
|
+
*
|
|
62
|
+
* ```ts
|
|
63
|
+
* // rename history: Bucket → StaticAssets → Assets
|
|
64
|
+
* yield* Bucket("Assets").pipe(renamedFrom("StaticAssets", "Bucket"));
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
67
|
+
* Migration semantics, by state-row shape (see Plan's rename resolution;
|
|
68
|
+
* `new` = the row at the resource's FQN, `old` = a row at a former FQN):
|
|
69
|
+
*
|
|
70
|
+
* ```text
|
|
71
|
+
* new old → outcome
|
|
72
|
+
* ──────────────────────────────────────────────────────────────────────
|
|
73
|
+
* — row → migrate: the old row
|
|
74
|
+
* IS the resource's
|
|
75
|
+
* state; apply moves it
|
|
76
|
+
* before any lifecycle
|
|
77
|
+
* op, and ONE update
|
|
78
|
+
* reconcile re-brands
|
|
79
|
+
* the physical resource
|
|
80
|
+
* under the new logical
|
|
81
|
+
* id (never a create)
|
|
82
|
+
* row, same instanceId row → interrupted
|
|
83
|
+
* migration: leftovers
|
|
84
|
+
* dropped state-only —
|
|
85
|
+
* ALL of them in one
|
|
86
|
+
* apply — the physical
|
|
87
|
+
* resource is never
|
|
88
|
+
* touched
|
|
89
|
+
* row, diff instanceId row → someone else's row (a
|
|
90
|
+
* resource reused the
|
|
91
|
+
* old name after the
|
|
92
|
+
* rename shipped):
|
|
93
|
+
* ignored, normal
|
|
94
|
+
* orphan handling
|
|
95
|
+
* row, diff resourceType row → FATAL: migrating over
|
|
96
|
+
* the foreign-typed row
|
|
97
|
+
* would silently
|
|
98
|
+
* abandon its cloud
|
|
99
|
+
* resource — resolve
|
|
100
|
+
* the collision first
|
|
101
|
+
* any row, diff resourceType → never migrated —
|
|
102
|
+
* cannot be this
|
|
103
|
+
* resource's row,
|
|
104
|
+
* whatever its FQN says
|
|
105
|
+
* ```
|
|
106
|
+
*
|
|
107
|
+
* The old id can be REUSED by a new resource in the same deploy — the
|
|
108
|
+
* rename claim wins the row (it is an explicit statement that the row was
|
|
109
|
+
* the renamer's), and the reusing resource is created fresh:
|
|
110
|
+
*
|
|
111
|
+
* ```ts
|
|
112
|
+
* // `Assets` keeps the physical resource previously known as `Bucket`;
|
|
113
|
+
* // this `Bucket` is a brand-new one.
|
|
114
|
+
* yield* Bucket("Assets").pipe(renamedFrom("Bucket"));
|
|
115
|
+
* yield* Bucket("Bucket");
|
|
116
|
+
* ```
|
|
117
|
+
*
|
|
118
|
+
* Renames may SHIFT through each other in one deploy — each row follows
|
|
119
|
+
* its resource (resolved in claim-dependency order):
|
|
120
|
+
*
|
|
121
|
+
* ```ts
|
|
122
|
+
* // the resource at `A` is now `B`; the resource at `B` is now `C`
|
|
123
|
+
* yield* Bucket("B").pipe(renamedFrom("A"));
|
|
124
|
+
* yield* Bucket("C").pipe(renamedFrom("B"));
|
|
125
|
+
* ```
|
|
126
|
+
*
|
|
127
|
+
* A SWAP (`A` ⇄ `B`) is a rename cycle and fails the plan loudly — the two
|
|
128
|
+
* migrations would overwrite and delete each other's rows. Rename through
|
|
129
|
+
* a temporary id across two deploys instead. Two resources claiming the
|
|
130
|
+
* same former FQN also fail loudly.
|
|
131
|
+
*/
|
|
132
|
+
export const renamedFrom =
|
|
133
|
+
(...formerIds: [FormerId, ...FormerId[]]) =>
|
|
134
|
+
<A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
|
|
135
|
+
Effect.provideService(effect, RenamePolicy, formerIds);
|