@fjall/deploy-core 2.27.0 → 2.29.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.
- package/dist/.minified +1 -1
- package/dist/src/aws/organisations/backup.d.ts +7 -2
- package/dist/src/aws/organisations/backup.js +2 -2
- package/dist/src/aws/organisations/index.d.ts +1 -1
- package/dist/src/aws/organisations/index.js +1 -1
- package/dist/src/aws/organisations/oidcProvider.d.ts +37 -0
- package/dist/src/aws/organisations/oidcProvider.js +1 -1
- package/dist/src/aws/organisations/organisation.d.ts +21 -0
- package/dist/src/aws/organisations/organisation.js +1 -1
- package/dist/src/aws/utils/cloudformationEventHelpers.d.ts +45 -0
- package/dist/src/aws/utils/cloudformationEventHelpers.js +1 -1
- package/dist/src/aws/utils/cloudformationEventTypes.d.ts +1 -1
- package/dist/src/aws/utils/cloudformationEvents.d.ts +2 -1
- package/dist/src/aws/utils/cloudformationEvents.js +1 -1
- package/dist/src/aws/utils/stackStatus.js +1 -1
- package/dist/src/index.d.ts +12 -1
- package/dist/src/index.js +1 -1
- package/dist/src/orchestration/activeDeploymentGuard.d.ts +41 -0
- package/dist/src/orchestration/activeDeploymentGuard.js +8 -4
- package/dist/src/orchestration/application/applicationDeploy.d.ts +9 -0
- package/dist/src/orchestration/application/applicationDeploy.js +1 -1
- package/dist/src/orchestration/application/applicationDeployHelpers.d.ts +20 -3
- package/dist/src/orchestration/application/applicationDeployHelpers.js +3 -3
- package/dist/src/orchestration/application/applicationDestroy.js +1 -1
- package/dist/src/orchestration/application/approvalGate.d.ts +15 -1
- package/dist/src/orchestration/application/approvalGate.js +1 -1
- package/dist/src/orchestration/application/destructionGate.d.ts +164 -0
- package/dist/src/orchestration/application/destructionGate.js +5 -0
- package/dist/src/orchestration/application/leaseGate.d.ts +30 -0
- package/dist/src/orchestration/application/leaseGate.js +1 -0
- package/dist/src/orchestration/application/plan/approvalToken.d.ts +15 -0
- package/dist/src/orchestration/application/plan/approvalToken.js +1 -1
- package/dist/src/orchestration/application/plan/changeSetEscalation.d.ts +46 -0
- package/dist/src/orchestration/application/plan/changeSetEscalation.js +1 -0
- package/dist/src/orchestration/application/plan/computeDeployPlan.d.ts +13 -0
- package/dist/src/orchestration/application/plan/computeDeployPlan.js +1 -1
- package/dist/src/orchestration/application/plan/index.d.ts +4 -1
- package/dist/src/orchestration/application/plan/index.js +1 -1
- package/dist/src/orchestration/application/plan/loadDeployPlan.d.ts +32 -0
- package/dist/src/orchestration/application/plan/loadDeployPlan.js +1 -0
- package/dist/src/orchestration/application/plan/pinnedNames.d.ts +31 -0
- package/dist/src/orchestration/application/plan/pinnedNames.js +1 -0
- package/dist/src/orchestration/application/plan/registryOverlay.d.ts +83 -0
- package/dist/src/orchestration/application/plan/registryOverlay.js +1 -0
- package/dist/src/orchestration/application/plan/renderDeployPlan.d.ts +4 -1
- package/dist/src/orchestration/application/plan/renderDeployPlan.js +1 -1
- package/dist/src/orchestration/application/plan/types.d.ts +31 -0
- package/dist/src/orchestration/application/plan/types.js +1 -0
- package/dist/src/orchestration/contextHelpers.d.ts +37 -1
- package/dist/src/orchestration/contextHelpers.js +1 -1
- package/dist/src/orchestration/drift/classifyDriftFailure.d.ts +31 -0
- package/dist/src/orchestration/drift/classifyDriftFailure.js +1 -0
- package/dist/src/orchestration/drift/detectStackDrift.d.ts +37 -0
- package/dist/src/orchestration/drift/detectStackDrift.js +1 -0
- package/dist/src/orchestration/drift/driftPreFlight.d.ts +73 -0
- package/dist/src/orchestration/drift/driftPreFlight.js +7 -0
- package/dist/src/orchestration/drift/driftProbe.d.ts +113 -0
- package/dist/src/orchestration/drift/driftProbe.js +1 -0
- package/dist/src/orchestration/drift/driftSuspectJournal.d.ts +40 -0
- package/dist/src/orchestration/drift/driftSuspectJournal.js +1 -0
- package/dist/src/orchestration/drift/preFlightGate.d.ts +44 -0
- package/dist/src/orchestration/drift/preFlightGate.js +2 -0
- package/dist/src/orchestration/index.d.ts +19 -0
- package/dist/src/orchestration/index.js +1 -1
- package/dist/src/orchestration/organisation/cascadeDestroyHelpers.d.ts +7 -0
- package/dist/src/orchestration/organisation/cascadeDestroyHelpers.js +1 -1
- package/dist/src/orchestration/organisation/cascadeHelpers.js +1 -1
- package/dist/src/orchestration/organisation/organisationDestroy.js +3 -3
- package/dist/src/orchestration/organisationDeploy/infraSteps.d.ts +8 -1
- package/dist/src/orchestration/organisationDeploy/infraSteps.js +1 -1
- package/dist/src/orchestration/organisationDeploy/orgCascadeDeploy.js +5 -5
- package/dist/src/orchestration/organisationDeploy/orgContext.d.ts +21 -2
- package/dist/src/orchestration/organisationDeploy/orgContext.js +1 -1
- package/dist/src/orchestration/organisationDeploy/singleComponentDeploy.js +1 -1
- package/dist/src/orchestration/remediation/driftRepairGate.d.ts +66 -0
- package/dist/src/orchestration/remediation/driftRepairGate.js +1 -0
- package/dist/src/orchestration/remediation/forensicsCapture.d.ts +49 -0
- package/dist/src/orchestration/remediation/forensicsCapture.js +1 -0
- package/dist/src/orchestration/remediation/forgetResource.d.ts +423 -0
- package/dist/src/orchestration/remediation/forgetResource.js +1 -0
- package/dist/src/orchestration/remediation/pinRemediation.d.ts +121 -0
- package/dist/src/orchestration/remediation/pinRemediation.js +1 -0
- package/dist/src/orchestration/remediation/postFailureRepairOffer.d.ts +77 -0
- package/dist/src/orchestration/remediation/postFailureRepairOffer.js +14 -0
- package/dist/src/orchestration/remediation/recreatePreFlight.d.ts +47 -0
- package/dist/src/orchestration/remediation/recreatePreFlight.js +1 -0
- package/dist/src/orchestration/remediation/recreateResource.d.ts +110 -0
- package/dist/src/orchestration/remediation/recreateResource.js +1 -0
- package/dist/src/orchestration/remediation/remediationJournal.d.ts +88 -0
- package/dist/src/orchestration/remediation/remediationJournal.js +2 -0
- package/dist/src/orchestration/remediation/removalUpdate.d.ts +135 -0
- package/dist/src/orchestration/remediation/removalUpdate.js +1 -0
- package/dist/src/orchestration/remediation/retainFlip.d.ts +294 -0
- package/dist/src/orchestration/remediation/retainFlip.js +1 -0
- package/dist/src/services/application/ApplicationStackService.d.ts +5 -3
- package/dist/src/services/application/ApplicationStackService.js +1 -1
- package/dist/src/services/application/applicationStackHelpers.d.ts +8 -6
- package/dist/src/services/application/applicationStackHelpers.js +3 -3
- package/dist/src/services/index.d.ts +2 -2
- package/dist/src/services/index.js +1 -1
- package/dist/src/services/infrastructure/CdkEventMonitoring.d.ts +15 -0
- package/dist/src/services/infrastructure/CdkEventMonitoring.js +1 -1
- package/dist/src/services/infrastructure/CdkOutputAnalyser.d.ts +8 -0
- package/dist/src/services/infrastructure/CdkOutputAnalyser.js +2 -1
- package/dist/src/services/infrastructure/CdkService.js +3 -3
- package/dist/src/services/infrastructure/CfnRegistryService.d.ts +68 -0
- package/dist/src/services/infrastructure/CfnRegistryService.js +1 -0
- package/dist/src/services/infrastructure/CloudFormationService.d.ts +5 -2
- package/dist/src/services/infrastructure/CloudFormationService.js +1 -1
- package/dist/src/services/infrastructure/EcsServiceResolver.d.ts +23 -3
- package/dist/src/services/infrastructure/EcsServiceResolver.js +1 -1
- package/dist/src/services/infrastructure/cfnRegistrySeed.d.ts +10 -0
- package/dist/src/services/infrastructure/cfnRegistrySeed.js +1 -0
- package/dist/src/services/infrastructure/changeSetProbe.d.ts +88 -0
- package/dist/src/services/infrastructure/changeSetProbe.js +1 -0
- package/dist/src/services/infrastructure/index.d.ts +2 -0
- package/dist/src/services/infrastructure/index.js +1 -1
- package/dist/src/types/callbackKeys.d.ts +1 -1
- package/dist/src/types/callbackKeys.js +1 -1
- package/dist/src/types/callbacks.d.ts +22 -0
- package/dist/src/types/config/entitlements.d.ts +9 -0
- package/dist/src/types/config/orgConfig.d.ts +6 -0
- package/dist/src/types/deploymentEventSchema.d.ts +1 -0
- package/dist/src/types/deploymentEventSchema.js +1 -1
- package/dist/src/types/destruction.d.ts +212 -0
- package/dist/src/types/destruction.js +1 -0
- package/dist/src/types/drift.d.ts +72 -0
- package/dist/src/types/drift.js +1 -0
- package/dist/src/types/index.d.ts +8 -2
- package/dist/src/types/index.js +1 -1
- package/dist/src/types/params.d.ts +142 -0
- package/dist/src/types/remediation.d.ts +126 -0
- package/dist/src/types/remediation.js +1 -0
- package/package.json +5 -4
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The surgical removal update — the recreate orchestration's middle phase
|
|
3
|
+
* (drift-remediation plan § Phase 7): derive a removal template from the LIVE
|
|
4
|
+
* template (`GetTemplate`, never synth) with the target resources deleted, so
|
|
5
|
+
* CloudFormation deletes the physical resources under the snapshot-flip's
|
|
6
|
+
* DeletionPolicy (Snapshot where supported) and frees the pinned names for
|
|
7
|
+
* the converge deploy to re-create.
|
|
8
|
+
*
|
|
9
|
+
* Derivation is deliberately conservative:
|
|
10
|
+
* - target resources are removed; each remaining resource's `DependsOn`
|
|
11
|
+
* edges to a target are pruned (a dangling DependsOn fails validation);
|
|
12
|
+
* - Outputs whose subtree references a target (Ref / Fn::GetAtt / Fn::Sub)
|
|
13
|
+
* are removed (a dangling output reference fails validation);
|
|
14
|
+
* - a REMAINING RESOURCE that references a target fails the derivation
|
|
15
|
+
* closed with the referencing paths (paths only, never values) — silently
|
|
16
|
+
* deleting or rewriting a live sibling resource is never fjall's call.
|
|
17
|
+
*
|
|
18
|
+
* `proveRemovalTargetsOnly` mirrors the retain flip's review change set: a
|
|
19
|
+
* `fjall-preflight-` change set must report ONLY `Remove` actions on the
|
|
20
|
+
* target logical IDs before the apply may run. The apply itself reuses
|
|
21
|
+
* `applyRetainFlip` (template-agnostic UpdateStack + TOCTOU digest re-check +
|
|
22
|
+
* poll) under a "Recreate-removal" label.
|
|
23
|
+
*
|
|
24
|
+
* Failure messages are pre-masked at construction (all content is
|
|
25
|
+
* identifier-only: logical IDs, paths, stack names).
|
|
26
|
+
*/
|
|
27
|
+
import { type Result } from "@fjall/generator";
|
|
28
|
+
import type { AwsProvider } from "../../aws/AwsProvider.js";
|
|
29
|
+
import { type RetainFlipOptions } from "./retainFlip.js";
|
|
30
|
+
/**
|
|
31
|
+
* The logical IDs of RESOURCES that reference any target via Ref, Fn::GetAtt
|
|
32
|
+
* or Fn::Sub (DependsOn excluded — it names an ordering edge, not a value
|
|
33
|
+
* CloudFormation must resolve). The forget arm's defect-#5 referrer guard:
|
|
34
|
+
* CloudFormation re-resolves a modified resource's Ref/GetAtt for its
|
|
35
|
+
* dependents at execution time, which fails for a DEAD target — so a flip
|
|
36
|
+
* UpdateStack on a policy-pending dead target with live referrers can never
|
|
37
|
+
* succeed and must fail closed before any mutation. Reuses the removal
|
|
38
|
+
* derivation's canonical reference traversal (single source of truth).
|
|
39
|
+
*
|
|
40
|
+
* Unlike the removal derivation (where targets vanish together, so
|
|
41
|
+
* target-to-target references are moot), the flip MODIFIES targets in place —
|
|
42
|
+
* a target referencing a modified dead sibling is re-evaluated exactly like
|
|
43
|
+
* an outside referrer (the live incident proved even DEAD referrers fail the
|
|
44
|
+
* re-resolution), so distinct targets referencing each other are reported.
|
|
45
|
+
*
|
|
46
|
+
* Scan covers the Resources section only. An Output referencing a modified
|
|
47
|
+
* dead target is NOT flagged: whether CloudFormation re-evaluates Outputs at
|
|
48
|
+
* execution time is unproven live, and Output-references-stateful-resource is
|
|
49
|
+
* so common in CDK synth that guarding on it would block legitimate flips; a
|
|
50
|
+
* missed referrer degrades to a clean rollback, never an unsafe mutation
|
|
51
|
+
* (residual recorded in the flipless ADR, 2026-07-12).
|
|
52
|
+
*/
|
|
53
|
+
export declare function findTargetReferrers(liveTemplateBody: string, targets: readonly string[], stackName: string): Result<string[], Error>;
|
|
54
|
+
export interface DeadReferenceFromTarget {
|
|
55
|
+
/** The policy-pending target whose own properties carry the reference. */
|
|
56
|
+
pendingTarget: string;
|
|
57
|
+
/** The dead siblings it references (Ref / Fn::GetAtt / Fn::Sub). */
|
|
58
|
+
deadReferences: string[];
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The policy-pending targets whose OWN properties reference another dead
|
|
62
|
+
* target. The defect-#5 guard's outbound direction: the second live incident
|
|
63
|
+
* (2026-07-12, KMS alias flip) proved CloudFormation also resolves a MODIFIED
|
|
64
|
+
* resource's own outbound Ref/GetAtt at execution time — even when the
|
|
65
|
+
* referenced resource is not itself modified — so a flip UpdateStack on a
|
|
66
|
+
* pending target referencing ANY dead sibling rolls back exactly like the
|
|
67
|
+
* inbound referrer case. `findTargetReferrers` cannot see this shape: it
|
|
68
|
+
* scans for references TO the pending set, and the doomed reference points
|
|
69
|
+
* FROM the pending target at a sibling outside that set.
|
|
70
|
+
*
|
|
71
|
+
* `deadTargets` is the caller's known-deleted set (for the forget arm: the
|
|
72
|
+
* op's targets minus any retained-by-redeclaration, which are proven
|
|
73
|
+
* physically present and safe to reference). Self-references are excluded to
|
|
74
|
+
* mirror the intra-set precedent, though CloudFormation rejects them anyway.
|
|
75
|
+
*/
|
|
76
|
+
export declare function findDeadReferencesFromTargets(liveTemplateBody: string, pendingTargets: readonly string[], deadTargets: readonly string[], stackName: string): Result<DeadReferenceFromTarget[], Error>;
|
|
77
|
+
/** An Output the derivation stripped that carries an `Export` block — the
|
|
78
|
+
* pre-flight must prove no other stack imports it before any mutation. */
|
|
79
|
+
export interface StrippedExport {
|
|
80
|
+
outputName: string;
|
|
81
|
+
/** The literal export name; undefined when `Export.Name` is not a plain
|
|
82
|
+
* string (unresolvable locally — the pre-flight fails closed on it). */
|
|
83
|
+
exportName?: string;
|
|
84
|
+
}
|
|
85
|
+
export interface RemovalDerivation {
|
|
86
|
+
removalTemplateBody: string;
|
|
87
|
+
liveTemplateDigest: string;
|
|
88
|
+
/** Targets already absent from the live template (removal landed). */
|
|
89
|
+
alreadyAbsent: string[];
|
|
90
|
+
/** Stripped Outputs carrying an Export — cross-stack consumers of these
|
|
91
|
+
* make the removal update fail mid-deploy (CFN refuses to drop an export
|
|
92
|
+
* while imports exist); `runRecreatePreFlight` checks them via ListImports. */
|
|
93
|
+
strippedExports: StrippedExport[];
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Derive the removal template: live template minus the target resources,
|
|
97
|
+
* with DependsOn edges pruned and target-referencing Outputs dropped. A
|
|
98
|
+
* remaining RESOURCE referencing a target fails closed with the paths.
|
|
99
|
+
*/
|
|
100
|
+
export declare function deriveRemovalTemplate(liveTemplateBody: string, targets: readonly string[], stackName: string): Result<RemovalDerivation, Error>;
|
|
101
|
+
export interface RemovalDeletionFailure {
|
|
102
|
+
logicalId: string;
|
|
103
|
+
/** Masked at construction — a status reason can embed handler output. */
|
|
104
|
+
statusReason?: string;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* GetTemplate absence alone cannot prove the physical deletions succeeded:
|
|
108
|
+
* CloudFormation deletes removed resources during the update-CLEANUP phase,
|
|
109
|
+
* and a DELETE_FAILED there leaves the stack UPDATE_COMPLETE with the target
|
|
110
|
+
* gone from the template but orphaned alive — still holding its pinned name,
|
|
111
|
+
* so the converge's re-create would collide. Scan the just-settled
|
|
112
|
+
* operation's events for DELETE_FAILED on the targets. Only meaningful
|
|
113
|
+
* immediately after this run's removal update (the scan reads the LAST
|
|
114
|
+
* operation's events).
|
|
115
|
+
*/
|
|
116
|
+
export declare function findRemovalDeletionFailures(aws: AwsProvider, stackName: string, targets: readonly string[], abortSignal?: AbortSignal): Promise<Result<RemovalDeletionFailure[], Error>>;
|
|
117
|
+
export interface RemovalProofResult {
|
|
118
|
+
/** CFN reported the removal contains no changes — the caller MUST re-verify
|
|
119
|
+
* target absence via GetTemplate, never assume the removal landed. */
|
|
120
|
+
noChanges: boolean;
|
|
121
|
+
}
|
|
122
|
+
export interface ProveRemovalTargetsOnlyParams {
|
|
123
|
+
stackName: string;
|
|
124
|
+
removalTemplateBody: string;
|
|
125
|
+
targets: readonly string[];
|
|
126
|
+
/** Live parameter keys, echoed UsePreviousValue (see retainFlip). */
|
|
127
|
+
parameterKeys: readonly string[];
|
|
128
|
+
abortSignal?: AbortSignal;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Review-only `fjall-preflight-` change set proving every reported resource
|
|
132
|
+
* change is a `Remove` on a target logical ID. Nothing is executed; the
|
|
133
|
+
* change set is deleted in `finally`.
|
|
134
|
+
*/
|
|
135
|
+
export declare function proveRemovalTargetsOnly(aws: AwsProvider, params: ProveRemovalTargetsOnlyParams, options?: RetainFlipOptions): Promise<Result<RemovalProofResult, Error>>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{randomUUID as I}from"node:crypto";import{CloudFormationClient as k,CreateChangeSetCommand as _,DeleteChangeSetCommand as F,DescribeChangeSetCommand as O}from"@aws-sdk/client-cloudformation";import{success as h,failure as m}from"@fjall/generator";import{getErrorMessage as N,maskSensitiveOutput as A}from"@fjall/util";import{logger as L}from"@fjall/util/logger";import{composeSdkAbortSignal as $,isAborted as R}from"../../aws/organisations/types.js";import{PREFLIGHT_CHANGE_SET_PREFIX as j}from"../../services/infrastructure/changeSetProbe.js";import{sleepAbortable as x}from"../../util/sleepAbortable.js";import{fetchLastOperationEvents as M}from"../drift/classifyDriftFailure.js";import{composeFlipCapabilities as P,computeTemplateDigest as U,echoPreviousParameters as G,resolveTemplateLocation as H,deleteStagedTemplate as J}from"./retainFlip.js";const B="RemovalUpdate",K=/didn't contain changes|No updates are to be performed/i,z=new Set(["CREATE_COMPLETE","FAILED"]),V=3e3,X=9e4;function v(a,o){let r;try{r=JSON.parse(a)}catch{return m(new Error(`The live template for ${o} is not JSON (YAML-authored stacks are not supported by the removal update) \u2014 aborting to manual repair.`))}return typeof r!="object"||r===null||Array.isArray(r)?m(new Error(`The live template for ${o} did not parse to an object \u2014 aborting to manual repair.`)):h(r)}function S(a,o,r){if(typeof a=="string")return[];if(Array.isArray(a))return a.flatMap((s,c)=>S(s,o,`${r}[${c}]`));if(typeof a!="object"||a===null)return[];const t=a,e=[],u=t.Ref;typeof u=="string"&&o.has(u)&&e.push(`${r}/Ref(${u})`);const n=t["Fn::GetAtt"];typeof n=="string"&&o.has(n.split(".")[0]??"")&&e.push(`${r}/Fn::GetAtt(${n.split(".")[0]??""})`),Array.isArray(n)&&typeof n[0]=="string"&&o.has(n[0])&&e.push(`${r}/Fn::GetAtt(${n[0]})`);const f=t["Fn::Sub"],i=typeof f=="string"?f:Array.isArray(f)&&typeof f[0]=="string"?f[0]:void 0;if(i!==void 0)for(const s of i.matchAll(/\$\{([^!}][^}]*)\}/g)){const c=(s[1]??"").split(".")[0]??"";o.has(c)&&e.push(`${r}/Fn::Sub(${c})`)}for(const[s,c]of Object.entries(t))e.push(...S(c,o,`${r}/${s}`));return e}function se(a,o,r){const t=v(a,r);if(!t.success)return t;const e=new Set(o),u=[];for(const[n,f]of Object.entries(t.data.Resources??{})){const i=e.has(n)?new Set([...e].filter(s=>s!==n)):e;i.size!==0&&S(f,i,n).length>0&&u.push(n)}return h(u)}function ie(a,o,r,t){const e=v(a,t);if(!e.success)return e;const u=e.data.Resources??{},n=[];for(const f of o){const i=u[f];if(i===void 0)continue;const s=r.filter(c=>c!==f&&S(i,new Set([c]),f).length>0);s.length>0&&n.push({pendingTarget:f,deadReferences:s})}return h(n)}function Y(a,o){const r=a.DependsOn;if(typeof r=="string"){o.has(r)&&delete a.DependsOn;return}if(Array.isArray(r)){const t=r.filter(e=>typeof e!="string"||!o.has(e));t.length===0?delete a.DependsOn:t.length!==r.length&&(a.DependsOn=t)}}function ce(a,o,r){const t=v(a,r);if(!t.success)return t;const e=t.data,u=e.Resources??{},n=new Set(o),f=o.filter(l=>u[l]===void 0);for(const l of o)delete u[l];const i=[];for(const[l,p]of Object.entries(u))Y(p,n),i.push(...S(p,n,l));if(i.length>0)return m(new Error(`Cannot orchestrate the recreate for ${r}: other resources still reference ${[...n].join(", ")} at ${i.slice(0,10).join(", ")}${i.length>10?` (+${i.length-10} more)`:""} \u2014 removing the target would break them. Restructure the dependency or perform the replacement manually.`));const s=[],c=e.Outputs;if(typeof c=="object"&&c!==null){for(const[l,p]of Object.entries(c))if(S(p,n,`Outputs/${l}`).length>0){const d=typeof p=="object"&&p!==null?p.Export:void 0;if(typeof d=="object"&&d!==null){const g=d.Name;s.push({outputName:l,...typeof g=="string"?{exportName:g}:{}})}delete c[l]}Object.keys(c).length===0&&delete e.Outputs}return h({removalTemplateBody:JSON.stringify(e),liveTemplateDigest:U(a),alreadyAbsent:f,strippedExports:s})}async function le(a,o,r,t){const e=await M(a,o,t);if(!e.success)return e;const u=new Set(r);return h(e.data.filter(n=>u.has(n.logicalId)&&n.status==="DELETE_FAILED").map(n=>({logicalId:n.logicalId,...n.statusReason!==void 0?{statusReason:A(n.statusReason)}:{}})))}async function fe(a,o,r){const{stackName:t,abortSignal:e}=o,u=r?.pollIntervalMs??V,n=r?.changeSetTimeoutMs??X,f=r?.now??(()=>Date.now()),i=a.getClient(k),s=`${j}removal-${I()}`;let c;try{const l=await H(a,s,o.removalTemplateBody,e);if(!l.success)return l;c=l.data.key,await i.send(new _({StackName:t,ChangeSetName:s,ChangeSetType:"UPDATE",IncludeNestedStacks:!0,Capabilities:P([]),Description:"fjall recreate-removal targets-only proof \u2014 review-only, never executed",...G(o.parameterKeys),...l.data.location}),{abortSignal:$(e)});const p=f();let d,g;for(;!R(e);){const y=await i.send(new O({StackName:t,ChangeSetName:s}),{abortSignal:$(e)});if(d=y.Status,g=y.StatusReason,d!==void 0&&z.has(d))break;if(f()-p>=n)return m(new Error(`Recreate-removal proof timed out after ${n}ms for ${t} (last status: ${d??"unknown"})`));await x(u,e)}if(R(e))return m(new Error(`Recreate-removal proof aborted while polling ${t}`));if(d==="FAILED")return g!==void 0&&K.test(g)?h({noChanges:!0}):m(new Error(`Recreate-removal proof change set failed for ${t}: ${A(g??"no status reason reported")}`));const T=[],D=new Set(o.targets);let b;do{if(R(e))return m(new Error(`Recreate-removal proof aborted while reading ${t}`));const y=await i.send(new O({StackName:t,ChangeSetName:s,...b!==void 0?{NextToken:b}:{}}),{abortSignal:$(e)});for(const C of y.Changes??[]){if(C.Type!=="Resource")continue;const w=C.ResourceChange,E=w?.LogicalResourceId;if(!(w===void 0||E===void 0)){if(!D.has(E)){T.push(`${E} (not a removal target)`);continue}w.Action!=="Remove"&&T.push(`${E} (action ${w.Action??"unknown"})`)}}b=y.NextToken}while(b!==void 0);return T.length>0?m(new Error(`Recreate removal is NOT targets-only for ${t} \u2014 the review change set reported changes at ${[...new Set(T)].join(", ")}. Aborting before the removal ran; the snapshot flip has already been applied, so re-run the deploy to resume once the change is understood.`)):h({noChanges:!1})}catch(l){return m(new Error(`Recreate-removal proof failed for ${t}: ${A(N(l))}`))}finally{await i.send(new F({StackName:t,ChangeSetName:s}),{abortSignal:$()}).catch(l=>{L.warn(B,"Failed to delete recreate-removal proof change set",{stackName:t,changeSetName:s,error:A(N(l))})}),c!==void 0&&await J(a,c)}}export{ce as deriveRemovalTemplate,ie as findDeadReferencesFromTargets,le as findRemovalDeletionFailures,se as findTargetReferrers,fe as proveRemovalTargetsOnly};
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The retain flip — phase 2/3 of the two-deploy `forget` orchestration
|
|
3
|
+
* (drift-remediation plan § 3.2). The flip template derives from the LIVE
|
|
4
|
+
* template (`GetTemplate`, never synth): the ONLY mutations are
|
|
5
|
+
* `DeletionPolicy: Retain` + `UpdateReplacePolicy: Retain` on the target
|
|
6
|
+
* logical IDs plus the in-template `fjall:forget` metadata marker [G3] — the
|
|
7
|
+
* AWS-side resume state that survives machine loss.
|
|
8
|
+
*
|
|
9
|
+
* Two proof guards, both required before the flip may apply:
|
|
10
|
+
* (a) `verifyRetainFlipDiff` — a pure diff-guard that hard-fails if anything
|
|
11
|
+
* beyond the allowed keys differs from the fetched template;
|
|
12
|
+
* (b) `proveFlipMetadataOnly` — a `fjall-preflight-` review change set that
|
|
13
|
+
* proves every reported change targets only
|
|
14
|
+
* `{DeletionPolicy, UpdateReplacePolicy, Metadata}` on the target IDs.
|
|
15
|
+
* Any property-level delta aborts to `repair` with the offending paths
|
|
16
|
+
* attached (paths only, never values). Non-target changes are tolerated ONLY
|
|
17
|
+
* as transitive dynamic reference re-evaluations caused by the targets —
|
|
18
|
+
* CloudFormation conservatively re-reports every Ref/GetAtt dependent of a
|
|
19
|
+
* metadata-touched resource (2026-07-12 live defect #4: the BC Account
|
|
20
|
+
* stack's bucket, bucket policy and key alias re-evaluated off a flipped KMS
|
|
21
|
+
* key, wrongly aborting the consented forget) — see
|
|
22
|
+
* `classifyProofViolations`.
|
|
23
|
+
*
|
|
24
|
+
* [G10] `didn't contain changes` is NOT taken as "flip already live" —
|
|
25
|
+
* `GetTemplate` re-verification (`readRetainFlipState`) is the sole authority
|
|
26
|
+
* for the already-applied/resume-skip branch.
|
|
27
|
+
*
|
|
28
|
+
* FLIPLESS fast-path (2026-07-12 live defect #5): the UpdateStack here is
|
|
29
|
+
* reserved for `policyPendingTargets` — targets whose live policies actually
|
|
30
|
+
* need changing. Targets already carrying the spec's policy (the CDK default
|
|
31
|
+
* for stateful types is Retain) are advanced by the orchestrators WITHOUT any
|
|
32
|
+
* UpdateStack and WITHOUT a marker: CloudFormation re-resolves Ref/GetAtt
|
|
33
|
+
* dependents of any modified dead resource at execution time, so even a
|
|
34
|
+
* metadata-only Modify of a dead, referenced target rolls the update back —
|
|
35
|
+
* and the policy the update would assert is already live, so the mutation
|
|
36
|
+
* guarantees nothing the live template does not already prove. The same
|
|
37
|
+
* incident's OUTBOUND direction (a policy-pending target whose OWN references
|
|
38
|
+
* resolve against dead siblings — modifying it is structurally impossible)
|
|
39
|
+
* is advanced flipless BY NECESSITY once the target itself is verified
|
|
40
|
+
* physically deleted: the converge's removal of an already-deleted resource
|
|
41
|
+
* is a physical no-op, so the Retain guarantee is not needed (the forget
|
|
42
|
+
* orchestrator's classification; journal `fliplessByNecessity`).
|
|
43
|
+
*
|
|
44
|
+
* The flip `UpdateStack` OMITS the `Tags` parameter (CloudFormation preserves
|
|
45
|
+
* existing stack tags, so no tag-driven resource updates can ride along),
|
|
46
|
+
* echoes the stack's capabilities unioned with the proof's fixed set, and
|
|
47
|
+
* echoes `UsePreviousValue: true` for every live stack parameter — omitting
|
|
48
|
+
* `Parameters` would resolve the template DEFAULTS (e.g. `<Service>ImageTag`
|
|
49
|
+
* back to "latest"), turning the metadata-only flip into a real resource
|
|
50
|
+
* update. The proof change set echoes the same parameters for the same
|
|
51
|
+
* reason. Immediately before the UpdateStack, the live template digest is
|
|
52
|
+
* re-checked against the template the flip derived from — the proof change
|
|
53
|
+
* set is review-only and never executed, so nothing else binds the apply to
|
|
54
|
+
* the proven bytes.
|
|
55
|
+
*
|
|
56
|
+
* [G8] boundary audit (2026-07-08): the flip's mutations —
|
|
57
|
+
* cloudformation:CreateChangeSet/DescribeChangeSet/DeleteChangeSet/UpdateStack
|
|
58
|
+
* — target the app's OWN stack, which the deploy session policy's
|
|
59
|
+
* CFN-mutation Deny exempts via its `stack/${appPascal}*` NotResource
|
|
60
|
+
* (`deploySessionPolicy.ts`); template staging rides the inherit-admin allow
|
|
61
|
+
* like the change-set probe's. A repair aimed at a foreign `--stack` under an
|
|
62
|
+
* OIDC deploy session correctly AccessDenies — defence-in-depth, not a gap.
|
|
63
|
+
* No session-policy change needed.
|
|
64
|
+
*
|
|
65
|
+
* Failure messages are pre-masked at construction.
|
|
66
|
+
*/
|
|
67
|
+
import { Capability, type Change } from "@aws-sdk/client-cloudformation";
|
|
68
|
+
import { type Result } from "@fjall/generator";
|
|
69
|
+
import type { AwsProvider } from "../../aws/AwsProvider.js";
|
|
70
|
+
/** The in-template resume marker [G3]. */
|
|
71
|
+
export declare const FORGET_MARKER_KEY = "fjall:forget";
|
|
72
|
+
/** The recreate orchestration's in-template resume marker (Phase 7) — a
|
|
73
|
+
* distinct key so the two surgeries' resume/ownership scans never adopt each
|
|
74
|
+
* other's markers. */
|
|
75
|
+
export declare const RECREATE_MARKER_KEY = "fjall:recreate";
|
|
76
|
+
export type FlipDeletionPolicy = "Retain" | "Snapshot" | "Delete";
|
|
77
|
+
/**
|
|
78
|
+
* What the flip stamps: the marker key and the DeletionPolicy /
|
|
79
|
+
* UpdateReplacePolicy value per target. The forget flip retains everything;
|
|
80
|
+
* the recreate snapshot-flip stamps Snapshot where the resource type supports
|
|
81
|
+
* it and Delete otherwise. Every read/derive/diff helper below takes the spec
|
|
82
|
+
* so both surgeries share one proven flip machine; the default is the forget
|
|
83
|
+
* spec, keeping every pre-Phase-7 call site byte-identical.
|
|
84
|
+
*/
|
|
85
|
+
export interface FlipSpec {
|
|
86
|
+
markerKey: string;
|
|
87
|
+
policyForTarget: (logicalId: string) => FlipDeletionPolicy;
|
|
88
|
+
}
|
|
89
|
+
export declare const FORGET_FLIP_SPEC: FlipSpec;
|
|
90
|
+
/**
|
|
91
|
+
* Proof and apply MUST request the same capability set: a proof that
|
|
92
|
+
* hardcodes a superset the apply lacks means a transform-bearing stack proves
|
|
93
|
+
* clean and then dies at UpdateStack with InsufficientCapabilities. Union of
|
|
94
|
+
* the stack's recorded capabilities (invalid values filtered) with the fixed
|
|
95
|
+
* proof set — one helper, both call sites.
|
|
96
|
+
*/
|
|
97
|
+
export declare function composeFlipCapabilities(recorded: readonly string[]): Capability[];
|
|
98
|
+
/**
|
|
99
|
+
* Echo `UsePreviousValue: true` for every live stack parameter on both flip
|
|
100
|
+
* CFN calls (mirrors `CloudFormationService.updateStackParameters`). Deployed
|
|
101
|
+
* Compute stacks carry `<Service>ImageTag` CfnParameters whose live value is
|
|
102
|
+
* a content hash but whose template default is "latest" — omitting
|
|
103
|
+
* `Parameters` resolves the DEFAULTS, so the proof would report Image →
|
|
104
|
+
* repo:latest → Replacement on the task definition and hard-fail every
|
|
105
|
+
* attempt, and the apply would roll images back. Zero-parameter stacks omit
|
|
106
|
+
* the property entirely.
|
|
107
|
+
*/
|
|
108
|
+
export declare function echoPreviousParameters(parameterKeys: readonly string[]): {
|
|
109
|
+
Parameters: Array<{
|
|
110
|
+
ParameterKey: string;
|
|
111
|
+
UsePreviousValue: true;
|
|
112
|
+
}>;
|
|
113
|
+
} | Record<string, never>;
|
|
114
|
+
export interface RetainFlipOptions {
|
|
115
|
+
pollIntervalMs?: number;
|
|
116
|
+
changeSetTimeoutMs?: number;
|
|
117
|
+
updateTimeoutMs?: number;
|
|
118
|
+
/** Injectable clock for timeout tests. */
|
|
119
|
+
now?: () => number;
|
|
120
|
+
}
|
|
121
|
+
export declare function computeTemplateDigest(templateBody: string): string;
|
|
122
|
+
export interface RetainFlipState {
|
|
123
|
+
/** Every target carries the spec's policy + marker. */
|
|
124
|
+
allFlipped: boolean;
|
|
125
|
+
/** Targets no longer present in the live template (removal landed). */
|
|
126
|
+
absentTargets: string[];
|
|
127
|
+
/** Targets present but not (fully) flipped. */
|
|
128
|
+
unflippedTargets: string[];
|
|
129
|
+
/**
|
|
130
|
+
* Targets present whose DeletionPolicy/UpdateReplacePolicy do NOT match the
|
|
131
|
+
* spec — the ones an UpdateStack would actually have to change. Disjoint
|
|
132
|
+
* from the flipless set (present, policies satisfied, marker absent =
|
|
133
|
+
* `unflippedTargets` minus this list). The POLICIES are the safety property
|
|
134
|
+
* the flip exists to guarantee; the marker is resume/ownership bookkeeping —
|
|
135
|
+
* so this list, not `unflippedTargets`, gates the flip UpdateStack and the
|
|
136
|
+
* [G2] partition's flip-live check (2026-07-12 live defect #5:
|
|
137
|
+
* CloudFormation re-resolves Ref/GetAtt dependents of ANY modified dead
|
|
138
|
+
* resource at execution time, so an UpdateStack that merely re-states an
|
|
139
|
+
* already-live Retain policy to add a marker rolls back — and is
|
|
140
|
+
* unnecessary, because the policy it would assert is already live).
|
|
141
|
+
*/
|
|
142
|
+
policyPendingTargets: string[];
|
|
143
|
+
/** Distinct opIds found in the targets' markers (the spec's key only). */
|
|
144
|
+
markerOpIds: string[];
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* [G3]/[G10] authority helper: read the flip state straight from the live
|
|
148
|
+
* template. Shared by the flip-applied verify, resume re-derivation, and the
|
|
149
|
+
* [G2] pre-flight's mid-surgery check.
|
|
150
|
+
*/
|
|
151
|
+
export declare function readRetainFlipState(liveTemplateBody: string, targets: readonly string[], stackName: string, spec?: FlipSpec): Result<RetainFlipState, Error>;
|
|
152
|
+
/**
|
|
153
|
+
* [G3] recovery scan: every logical ID in the live template whose
|
|
154
|
+
* fjall:forget marker carries the given opId. Recovery is LIVE-TEMPLATE
|
|
155
|
+
* scoped: markers embed only `{opId, at}` per-resource, so a marked resource
|
|
156
|
+
* that was since stripped (converged away, manually edited) is invisible here
|
|
157
|
+
* and the recovered set may be a PROPER SUBSET of the operation's original
|
|
158
|
+
* target set. Callers that adopt a foreign operation from this scan MUST
|
|
159
|
+
* prove completeness via hash identity — `computeRemediationOpId(stackName,
|
|
160
|
+
* recovered)` reproducing the marker's opId is the only proof the full set
|
|
161
|
+
* was recovered; adopting a subset mints a different opId whose surgery
|
|
162
|
+
* meets the original markers as foreign and livelocks.
|
|
163
|
+
*/
|
|
164
|
+
export declare function findMarkedResourcesByOpId(liveTemplateBody: string, opId: string, stackName: string, markerKey?: string): Result<string[], Error>;
|
|
165
|
+
/**
|
|
166
|
+
* CFN resource Type per logical ID, read from the live template — the
|
|
167
|
+
* adoption seam records these on targets recovered from markers alone (no
|
|
168
|
+
* journal, no drift finding to carry the type). Unreadable template or
|
|
169
|
+
* missing/typeless resource simply omits the entry.
|
|
170
|
+
*/
|
|
171
|
+
export declare function readResourceTypes(liveTemplateBody: string, logicalIds: readonly string[], stackName: string): Record<string, string>;
|
|
172
|
+
export interface DeriveRetainFlipParams {
|
|
173
|
+
liveTemplateBody: string;
|
|
174
|
+
stackName: string;
|
|
175
|
+
targets: readonly string[];
|
|
176
|
+
opId: string;
|
|
177
|
+
/** ISO-8601 stamp recorded in the marker. */
|
|
178
|
+
at: string;
|
|
179
|
+
/** Marker key + per-target policy — defaults to the forget flip. */
|
|
180
|
+
spec?: FlipSpec;
|
|
181
|
+
}
|
|
182
|
+
export interface RetainFlipDerivation {
|
|
183
|
+
flipTemplateBody: string;
|
|
184
|
+
liveTemplateDigest: string;
|
|
185
|
+
/** True when every target already carried the policy + OUR opId's marker. */
|
|
186
|
+
alreadyApplied: boolean;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Build the flip template from the live template: mutate ONLY
|
|
190
|
+
* DeletionPolicy/UpdateReplacePolicy → the spec's per-target policy and stamp
|
|
191
|
+
* the spec's marker on the target logical IDs. A target carrying a DIFFERENT
|
|
192
|
+
* operation's marker fails closed — two overlapping surgeries on one resource
|
|
193
|
+
* means the operator's model of the stack is wrong.
|
|
194
|
+
*/
|
|
195
|
+
export declare function deriveRetainFlipTemplate(params: DeriveRetainFlipParams): Result<RetainFlipDerivation, Error>;
|
|
196
|
+
/**
|
|
197
|
+
* Pure diff-guard (§3.2 guard a): strip the allowed flip keys from both
|
|
198
|
+
* templates and require byte-identical remainders. Any residual difference
|
|
199
|
+
* hard-fails with the offending paths (paths only, never values).
|
|
200
|
+
*/
|
|
201
|
+
export declare function verifyRetainFlipDiff(liveTemplateBody: string, flipTemplateBody: string, targets: readonly string[], stackName: string, markerKey?: string): Result<void, Error>;
|
|
202
|
+
export interface FlipProofResult {
|
|
203
|
+
/** True when CFN reported the flip contains no changes at all [G10] —
|
|
204
|
+
* the caller MUST re-verify via GetTemplate, never assume flip-live. */
|
|
205
|
+
noChanges: boolean;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Classify the proof change set's reported changes; an empty return proves
|
|
209
|
+
* the flip metadata-only. Targets may only carry Modify actions scoped to
|
|
210
|
+
* `ALLOWED_FLIP_ATTRIBUTES` with no replacement. Non-targets are tolerated
|
|
211
|
+
* ONLY via `isReferenceReEvaluationOf`, computed to a fixpoint because the
|
|
212
|
+
* re-evaluation propagates transitively (2026-07-12 live defect #4: the BC
|
|
213
|
+
* Account stack's bucket policy re-evaluated off the bucket's ARN, which
|
|
214
|
+
* itself re-evaluated off the flipped KMS key's ARN). A logical ID reported
|
|
215
|
+
* by several change entries is tolerated only if EVERY entry qualifies.
|
|
216
|
+
*/
|
|
217
|
+
export declare function classifyProofViolations(changes: readonly Change[], targets: readonly string[]): string[];
|
|
218
|
+
export interface ProveFlipMetadataOnlyParams {
|
|
219
|
+
stackName: string;
|
|
220
|
+
flipTemplateBody: string;
|
|
221
|
+
targets: readonly string[];
|
|
222
|
+
/** Live parameter keys, echoed UsePreviousValue on the change set — see
|
|
223
|
+
* `echoPreviousParameters`. */
|
|
224
|
+
parameterKeys: readonly string[];
|
|
225
|
+
abortSignal?: AbortSignal;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Change-set proof (§3.2 guard b): create a review-only `fjall-preflight-`
|
|
229
|
+
* change set for the flip template and prove every reported change targets
|
|
230
|
+
* only {DeletionPolicy, UpdateReplacePolicy, Metadata} on the target IDs —
|
|
231
|
+
* tolerating non-target changes only where `classifyProofViolations` proves
|
|
232
|
+
* them transitive dynamic reference re-evaluations of the targets.
|
|
233
|
+
* Nothing is ever executed; the change set is deleted in `finally`.
|
|
234
|
+
*/
|
|
235
|
+
export declare function proveFlipMetadataOnly(aws: AwsProvider, params: ProveFlipMetadataOnlyParams, options?: RetainFlipOptions): Promise<Result<FlipProofResult, Error>>;
|
|
236
|
+
export interface ApplyRetainFlipParams {
|
|
237
|
+
stackName: string;
|
|
238
|
+
flipTemplateBody: string;
|
|
239
|
+
/** The stack's existing capabilities — unioned with the proof's fixed set
|
|
240
|
+
* via `composeFlipCapabilities` so proof and apply request the same set. */
|
|
241
|
+
capabilities: readonly string[];
|
|
242
|
+
/** Live parameter keys, echoed UsePreviousValue on the UpdateStack — see
|
|
243
|
+
* `echoPreviousParameters`. */
|
|
244
|
+
parameterKeys: readonly string[];
|
|
245
|
+
/** Digest of the live template the flip derived from — re-checked against
|
|
246
|
+
* a fresh GetTemplate immediately before the UpdateStack (the proof change
|
|
247
|
+
* set is never executed, so nothing else binds apply to proof). */
|
|
248
|
+
derivedFromTemplateDigest: string;
|
|
249
|
+
/** Names the resume command in the mid-surgery abort message. */
|
|
250
|
+
opId: string;
|
|
251
|
+
/** Failure-message label — the recreate arm's removal update reuses this
|
|
252
|
+
* apply machine and labels itself honestly. Default "Retain-flip". */
|
|
253
|
+
operationLabel?: string;
|
|
254
|
+
/** Mid-surgery digest-mismatch cure line — defaults to the forget arm's
|
|
255
|
+
* `fjall drift resume <opId>` teach. */
|
|
256
|
+
resumeHint?: string;
|
|
257
|
+
abortSignal?: AbortSignal;
|
|
258
|
+
}
|
|
259
|
+
export type ApplyRetainFlipOutcome = {
|
|
260
|
+
outcome: "updated";
|
|
261
|
+
}
|
|
262
|
+
/** CFN reported "No updates are to be performed" — the caller re-verifies
|
|
263
|
+
* the flip via GetTemplate [G10], never assumes it landed. */
|
|
264
|
+
| {
|
|
265
|
+
outcome: "no-updates";
|
|
266
|
+
};
|
|
267
|
+
/**
|
|
268
|
+
* Execute the flip via direct SDK UpdateStack — flip TemplateBody, Tags
|
|
269
|
+
* OMITTED (CFN preserves existing stack tags), capabilities echoed — then
|
|
270
|
+
* poll to a terminal status. A metadata-only update issues no call against
|
|
271
|
+
* the dead TARGET itself, but CloudFormation re-resolves Ref/GetAtt
|
|
272
|
+
* attributes of every modified resource for its dependents at execution time
|
|
273
|
+
* — which fails for a dead target with live referrers (2026-07-12 live
|
|
274
|
+
* defect #5: "Unable to retrieve Arn attribute"). Callers MUST therefore
|
|
275
|
+
* reserve this UpdateStack for policy-pending targets and guard the
|
|
276
|
+
* referrer topology first (the forget orchestrator's flipless fast-path +
|
|
277
|
+
* referrer guard; the recreate arm's pre-flight removal derivation).
|
|
278
|
+
*/
|
|
279
|
+
export declare function applyRetainFlip(aws: AwsProvider, params: ApplyRetainFlipParams, options?: RetainFlipOptions): Promise<Result<ApplyRetainFlipOutcome, Error>>;
|
|
280
|
+
/**
|
|
281
|
+
* Inline body up to CFN's 51,200-byte limit; larger templates stage in the
|
|
282
|
+
* CDK bootstrap assets bucket (the change-set probe's staging shape). Shared
|
|
283
|
+
* with the recreate arm's removal update (`removalUpdate.ts`).
|
|
284
|
+
*/
|
|
285
|
+
export declare function resolveTemplateLocation(aws: AwsProvider, keySuffix: string, templateBody: string, abortSignal?: AbortSignal): Promise<Result<{
|
|
286
|
+
location: {
|
|
287
|
+
TemplateBody: string;
|
|
288
|
+
} | {
|
|
289
|
+
TemplateURL: string;
|
|
290
|
+
};
|
|
291
|
+
key?: string;
|
|
292
|
+
}, Error>>;
|
|
293
|
+
/** Best-effort cleanup of the staged template — logged, never thrown. */
|
|
294
|
+
export declare function deleteStagedTemplate(aws: AwsProvider, key: string): Promise<void>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{createHash as J,randomUUID as C}from"node:crypto";import{Capability as I,CloudFormationClient as _,CreateChangeSetCommand as V,DeleteChangeSetCommand as W,DescribeChangeSetCommand as M,DescribeStacksCommand as X,GetTemplateCommand as z,UpdateStackCommand as q}from"@aws-sdk/client-cloudformation";import{DeleteObjectCommand as Q,PutObjectCommand as Z,S3Client as P}from"@aws-sdk/client-s3";import{success as y,failure as g}from"@fjall/generator";import{getErrorMessage as b,maskSensitiveOutput as w}from"@fjall/util";import{logger as O}from"@fjall/util/logger";import{composeSdkAbortSignal as S,isAborted as $}from"../../aws/organisations/types.js";import{bootstrapAssetsBucketName as v,MAX_INLINE_TEMPLATE_BYTES as ee,PREFLIGHT_CHANGE_SET_PREFIX as te}from"../../services/infrastructure/changeSetProbe.js";import{sleepAbortable as N}from"../../util/sleepAbortable.js";const j="RetainFlip",k="fjall:forget",we="fjall:recreate",D={markerKey:k,policyForTarget:()=>"Retain"},ne=new Set(["DeletionPolicy","UpdateReplacePolicy","Metadata"]),L=/didn't contain changes|No updates are to be performed/i,re=new Set(["CREATE_COMPLETE","FAILED"]),oe=new Set(Object.values(I));function ae(e){return oe.has(e)}const ie=[I.CAPABILITY_IAM,I.CAPABILITY_NAMED_IAM,I.CAPABILITY_AUTO_EXPAND];function F(e){return[...new Set([...e.filter(ae),...ie])]}function U(e){return e.length===0?{}:{Parameters:e.map(n=>({ParameterKey:n,UsePreviousValue:!0}))}}const x=3e3,se=9e4,ce=10*6e4;function A(e,n){let r;try{r=JSON.parse(e)}catch{return g(new Error(`The live template for ${n} is not JSON (YAML-authored stacks are not supported by the retain flip) \u2014 aborting to manual repair.`))}return typeof r!="object"||r===null||Array.isArray(r)?g(new Error(`The live template for ${n} did not parse to an object \u2014 aborting to manual repair.`)):y(r)}function B(e){return J("sha256").update(e).digest("hex")}function le(e,n,r,t=D){const o=A(e,r);if(!o.success)return o;const a=o.data.Resources??{},i=[],s=[],c=[],f=new Set;for(const p of n){const l=a[p];if(l===void 0){i.push(p);continue}const u=K(l,t.markerKey);u!==void 0&&f.add(u.opId);const d=t.policyForTarget(p),m=l.DeletionPolicy===d&&l.UpdateReplacePolicy===d;m||c.push(p),(!m||u===void 0)&&s.push(p)}return y({allFlipped:i.length===0&&s.length===0,absentTargets:i,unflippedTargets:s,policyPendingTargets:c,markerOpIds:[...f]})}function K(e,n){const r=e.Metadata;if(typeof r!="object"||r===null)return;const t=r[n];if(typeof t!="object"||t===null)return;const o=t.opId;return typeof o=="string"?{opId:o}:void 0}function be(e,n,r,t=k){const o=A(e,r);if(!o.success)return o;const a=[];for(const[i,s]of Object.entries(o.data.Resources??{}))K(s,t)?.opId===n&&a.push(i);return y(a)}function Ae(e,n,r){const t=A(e,r);if(!t.success)return{};const o=t.data.Resources??{},a={};for(const i of n){const s=o[i]?.Type;typeof s=="string"&&(a[i]=s)}return a}function Re(e){const{liveTemplateBody:n,stackName:r,targets:t,opId:o,at:a}=e,i=e.spec??D,s=A(n,r);if(!s.success)return s;const c=le(n,t,r,i);if(!c.success)return c;if(c.data.absentTargets.length>0)return g(new Error(`Cannot derive the retain flip for ${r}: the live template no longer contains ${c.data.absentTargets.join(", ")}.`));const f=c.data.markerOpIds.filter(u=>u!==o);if(f.length>0)return g(new Error(`Cannot derive the retain flip for ${r}: target resources carry a ${i.markerKey} marker from a different operation (${f.join(", ")}) \u2014 resolve that operation first (fjall drift resume <opId>).`));const p=s.data,l=p.Resources??{};for(const u of t){const d=l[u];if(d===void 0)continue;const m=i.policyForTarget(u);d.DeletionPolicy=m,d.UpdateReplacePolicy=m;const h=typeof d.Metadata=="object"&&d.Metadata!==null?d.Metadata:{};h[i.markerKey]={opId:o,at:a},d.Metadata=h}return y({flipTemplateBody:JSON.stringify(p),liveTemplateDigest:B(n),alreadyApplied:c.data.allFlipped})}function Ie(e,n,r,t,o=k){const a=A(e,t);if(!a.success)return a;const i=A(n,t);if(!i.success)return i;const s=new Set(r);for(const f of[a.data,i.data]){const p=f.Resources??{};for(const l of Object.keys(p)){if(!s.has(l))continue;const u=p[l];if(u===void 0)continue;delete u.DeletionPolicy,delete u.UpdateReplacePolicy;const d=u.Metadata;typeof d=="object"&&d!==null&&(delete d[o],Object.keys(d).length===0&&delete u.Metadata)}}const c=G(a.data,i.data,"");return c.length>0?g(new Error(`Retain-flip diff-guard failed for ${t}: the flip template differs from the live template beyond the allowed keys at ${c.slice(0,10).join(", ")}${c.length>10?` (+${c.length-10} more)`:""} \u2014 aborting to manual repair, nothing was mutated.`)):y(void 0)}function G(e,n,r){if(e===n)return[];if(!(typeof e=="object"&&e!==null)||!(typeof n=="object"&&n!==null)||Array.isArray(e)!==Array.isArray(n))return JSON.stringify(e)===JSON.stringify(n)?[]:[r||"(root)"];const a=new Set([...Object.keys(e),...Object.keys(n)]),i=[];for(const s of a)i.push(...G(e[s],n[s],r===""?s:`${r}/${s}`));return i}function fe(e,n){if(e.Action!=="Modify"||e.Replacement==="True"||e.Replacement==="Conditional")return!1;const r=e.Details??[];if(r.length===0)return!1;const t=new Set(r.flatMap(a=>a.Target?.Attribute!==void 0?[a.Target.Attribute]:[]));return(e.Scope??[]).every(a=>t.has(a))?r.every(a=>{if(a.Evaluation!=="Dynamic"||a.ChangeSource!=="ResourceReference"&&a.ChangeSource!=="ResourceAttribute")return!1;const i=a.CausingEntity?.split(".")[0];return i!==void 0&&i!==""&&n.has(i)}):!1}function ue(e,n){const r=new Set(n),t=[],o=new Map;for(const s of e){if(s.Type!=="Resource")continue;const c=s.ResourceChange,f=c?.LogicalResourceId;if(c===void 0||f===void 0)continue;if(!r.has(f)){const l=o.get(f)??[];l.push(c),o.set(f,l);continue}if(c.Action!=="Modify"){t.push(`${f} (action ${c.Action??"unknown"})`);continue}if(c.Replacement==="True"||c.Replacement==="Conditional"){t.push(`${f} (replacement ${c.Replacement})`);continue}const p=[...c.Scope??[],...(c.Details??[]).flatMap(l=>l.Target?.Attribute!==void 0?[l.Target.Attribute]:[])];for(const l of p)ne.has(l)||t.push(`${f}/${l}`)}const a=new Set(n);let i=!0;for(;i;){i=!1;for(const[s,c]of o)a.has(s)||c.every(f=>fe(f,a))&&(a.add(s),i=!0)}for(const s of o.keys())a.has(s)||t.push(`${s} (not a flip target)`);return t}async function $e(e,n,r){const{stackName:t,abortSignal:o}=n,a=r?.pollIntervalMs??x,i=r?.changeSetTimeoutMs??se,s=r?.now??(()=>Date.now()),c=e.getClient(_),f=`${te}flip-${C()}`;let p;try{const l=await Y(e,f,n.flipTemplateBody,o);if(!l.success)return l;p=l.data.key,await c.send(new V({StackName:t,ChangeSetName:f,ChangeSetType:"UPDATE",IncludeNestedStacks:!0,Capabilities:F([]),Description:"fjall retain-flip metadata-only proof \u2014 review-only, never executed",...U(n.parameterKeys),...l.data.location}),{abortSignal:S(o)});const u=s();let d,m;for(;!$(o);){const R=await c.send(new M({StackName:t,ChangeSetName:f}),{abortSignal:S(o)});if(d=R.Status,m=R.StatusReason,d!==void 0&&re.has(d))break;if(s()-u>=i)return g(new Error(`Retain-flip proof timed out after ${i}ms for ${t} (last status: ${d??"unknown"})`));await N(a,o)}if($(o))return g(new Error(`Retain-flip proof aborted while polling ${t}`));if(d==="FAILED")return m!==void 0&&L.test(m)?y({noChanges:!0}):g(new Error(`Retain-flip proof change set failed for ${t}: ${w(m??"no status reason reported")}`));const h=[];let T;do{if($(o))return g(new Error(`Retain-flip proof aborted while reading ${t}`));const R=await c.send(new M({StackName:t,ChangeSetName:f,...T!==void 0?{NextToken:T}:{}}),{abortSignal:S(o)});h.push(...R.Changes??[]),T=R.NextToken}while(T!==void 0);const E=ue(h,n.targets);return E.length>0?g(new Error(`Retain flip is NOT metadata-only for ${t} \u2014 the review change set reported property-level changes at ${[...new Set(E)].join(", ")}. Aborting to manual repair; nothing was mutated.`)):y({noChanges:!1})}catch(l){return g(new Error(`Retain-flip proof failed for ${t}: ${w(b(l))}`))}finally{await c.send(new W({StackName:t,ChangeSetName:f}),{abortSignal:S()}).catch(l=>{O.warn(j,"Failed to delete retain-flip proof change set",{stackName:t,changeSetName:f,error:w(b(l))})}),p!==void 0&&await H(e,p)}}async function ke(e,n,r){const{stackName:t,abortSignal:o}=n,a=n.operationLabel??"Retain-flip",i=r?.pollIntervalMs??x,s=r?.updateTimeoutMs??ce,c=r?.now??(()=>Date.now()),f=e.getClient(_),p=`flip-apply-${C()}`;let l;try{const u=await Y(e,p,n.flipTemplateBody,o);if(!u.success)return u;l=u.data.key;const d=await f.send(new z({StackName:t,TemplateStage:"Original"}),{abortSignal:S(o)});if(B(d.TemplateBody??"")!==n.derivedFromTemplateDigest)return g(new Error(`${a} apply aborted for ${t}: the live template changed mid-surgery (after the metadata-only proof) \u2014 nothing was mutated. ${n.resumeHint??`Re-run 'fjall drift resume ${n.opId}' to re-verify from the current template.`}`));try{await f.send(new q({StackName:t,Capabilities:F(n.capabilities),...U(n.parameterKeys),...u.data.location}),{abortSignal:S(o)})}catch(h){if(L.test(b(h)))return y({outcome:"no-updates"});throw h}const m=c();for(;!$(o);){const T=(await f.send(new X({StackName:t}),{abortSignal:S(o)})).Stacks?.[0],E=T?.StackStatus??"UNKNOWN";if(E==="UPDATE_COMPLETE")return y({outcome:"updated"});if(!E.endsWith("_IN_PROGRESS"))return g(new Error(`${a} UpdateStack on ${t} ended ${E}: ${w(T?.StackStatusReason??"no status reason reported")}`));if(c()-m>=s)return g(new Error(`${a} UpdateStack on ${t} did not settle within ${s}ms (last status: ${E}). ${n.resumeHint??"Re-run 'fjall drift resume' once the stack settles."}`));await N(i,o)}return g(new Error(`${a} UpdateStack aborted while polling ${t}`))}catch(u){return g(new Error(`${a} UpdateStack failed for ${t}: ${w(b(u))}`))}finally{l!==void 0&&await H(e,l)}}async function Y(e,n,r,t){if(Buffer.byteLength(r,"utf8")<=ee)return y({location:{TemplateBody:r}});const o=e.getRegion(),a=v(e.getAccountId(),o),i=`fjall-preflight/${n}.template.json`;try{return await e.getClient(P).send(new Z({Bucket:a,Key:i,Body:r,ContentType:"application/json"}),{abortSignal:S(t)}),y({location:{TemplateURL:`https://${a}.s3.${o}.amazonaws.com/${i}`},key:i})}catch(s){return g(new Error(`Retain flip could not stage the template in ${a}: ${w(b(s))}`))}}async function H(e,n){const r=v(e.getAccountId(),e.getRegion());try{await e.getClient(P).send(new Q({Bucket:r,Key:n}),{abortSignal:S()})}catch(t){O.warn(j,"Failed to delete staged retain-flip template",{bucket:r,key:n,error:w(b(t))})}}export{D as FORGET_FLIP_SPEC,k as FORGET_MARKER_KEY,we as RECREATE_MARKER_KEY,ke as applyRetainFlip,ue as classifyProofViolations,F as composeFlipCapabilities,B as computeTemplateDigest,H as deleteStagedTemplate,Re as deriveRetainFlipTemplate,U as echoPreviousParameters,be as findMarkedResourcesByOpId,$e as proveFlipMetadataOnly,Ae as readResourceTypes,le as readRetainFlipState,Y as resolveTemplateLocation,Ie as verifyRetainFlipDiff};
|
|
@@ -3,6 +3,8 @@ import type { CloudFormationService } from "../infrastructure/CloudFormationServ
|
|
|
3
3
|
import type { AwsProvider } from "../../aws/AwsProvider.js";
|
|
4
4
|
import type { DeployCallbacks } from "../../types/callbacks.js";
|
|
5
5
|
import type { ResourceEvent } from "../../types/events.js";
|
|
6
|
+
import type { DeployParams } from "../../types/params.js";
|
|
7
|
+
import { type LeaseDeniedError } from "../../orchestration/application/leaseGate.js";
|
|
6
8
|
import type { DeploymentContext } from "../../types/deployment/DeploymentTypes.js";
|
|
7
9
|
import { type ApplicationStack } from "../../types/operations.js";
|
|
8
10
|
import type { ParallelDeploymentResult } from "../../types/deployment/parallel.js";
|
|
@@ -62,7 +64,7 @@ export declare class ApplicationStackService {
|
|
|
62
64
|
/**
|
|
63
65
|
* Destroy a specific stack type
|
|
64
66
|
*/
|
|
65
|
-
destroyStack(stackType: ApplicationStack, context: DeploymentContext, callbacks?: StackCallbacks, useCdkOut?: boolean, abortSignal?: AbortSignal): Promise<Result<StackDeploymentData, ApplicationError>>;
|
|
67
|
+
destroyStack(stackType: ApplicationStack, context: DeploymentContext, callbacks?: StackCallbacks, useCdkOut?: boolean, abortSignal?: AbortSignal, assertLeaseForStack?: DeployParams["assertLeaseForStack"]): Promise<Result<StackDeploymentData, ApplicationError | LeaseDeniedError>>;
|
|
66
68
|
/**
|
|
67
69
|
* Destroy multiple stacks in parallel within a destroy phase.
|
|
68
70
|
*
|
|
@@ -84,8 +86,8 @@ export declare class ApplicationStackService {
|
|
|
84
86
|
* Destroy all stacks for an application in reverse order.
|
|
85
87
|
* Supports parallel destruction for OpenNext patterns (nextjs/payload).
|
|
86
88
|
*/
|
|
87
|
-
destroyAllStacks(context: DeploymentContext, callbacks?: DestroyAllStacksCallbacks, resources?: AppResourceFlags, abortSignal?: AbortSignal): Promise<Result<{
|
|
89
|
+
destroyAllStacks(context: DeploymentContext, callbacks?: DestroyAllStacksCallbacks, resources?: AppResourceFlags, abortSignal?: AbortSignal, assertLeaseForStack?: DeployParams["assertLeaseForStack"]): Promise<Result<{
|
|
88
90
|
message: string;
|
|
89
|
-
}, ApplicationError>>;
|
|
91
|
+
}, ApplicationError | LeaseDeniedError>>;
|
|
90
92
|
}
|
|
91
93
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{CloudFormationClient as R}from"@aws-sdk/client-cloudformation";import{S3Client as v}from"@aws-sdk/client-s3";import{capturedSweepBuckets as D,preEmptyStackBuckets as N,sweepOrphanedDestroyBuckets as F}from"../../orchestration/stackCleanup.js";import{STACK_NOT_FOUND_PATTERN as _,CDK_NO_STACKS_MATCH as L}from"../../types/constants.js";import{getApplicationStackName as
|
|
1
|
+
import{CloudFormationClient as R}from"@aws-sdk/client-cloudformation";import{S3Client as v}from"@aws-sdk/client-s3";import{capturedSweepBuckets as D,preEmptyStackBuckets as N,sweepOrphanedDestroyBuckets as F}from"../../orchestration/stackCleanup.js";import{STACK_NOT_FOUND_PATTERN as _,CDK_NO_STACKS_MATCH as L}from"../../types/constants.js";import{assertLeaseBeforeStack as B}from"../../orchestration/application/leaseGate.js";import{getApplicationStackName as h}from"../../types/operations.js";import{success as f,failure as y}from"@fjall/generator";import{ApplicationError as w}from"../../types/application/ApplicationServiceTypes.js";import{logger as l}from"@fjall/util/logger";import{maskSensitiveOutput as P}from"@fjall/util";import{convertCloudFormationOutputsToRecord as O}from"../supporting/helpers.js";import{destroyAllStacks as E,mapSettledResults as A,resolveWebsiteUrl as $}from"./applicationStackHelpers.js";class J{cdkService;cloudFormationService;aws;constructor(e,o,t){this.cdkService=e,this.cloudFormationService=o,this.aws=t}async runCdkSynth(e){return this.cdkService.runCdkSynth(e)}async deployStack(e,o,t,n){const u=o.target,i=h(u,e),c=await this.cdkService.runCdkDeploy(o,i,t?.onOutput,t?.onResourceProgress,this.aws,void 0,n?.parameters);if(c.success){const s=c.data;l.debug("ApplicationStackService","CDK deploy result",{stackName:i,stackType:e,success:!0,message:s.message,status:s.status,skipped:s.details?.skipped,hasOutput:!!s.details?.output});const a=await this.cloudFormationService.getStackOutputs(i);l.debug("ApplicationStackService","Stack outputs after deploy",{stackName:i,hasOutputs:a.success&&a.data.length>0,outputCount:a.success?a.data.length:0});const r=a.success&&a.data.length>0?O(a.data):void 0;return f({stackType:e,stackName:i,skipped:s.details?.skipped===!0,outputs:r})}else{const s=P(c.error||`Failed to deploy ${e} infrastructure`);return l.error("ApplicationStackService","Stack deployment failed",{stackType:e,target:o.target,error:s}),y(new w(s,{errorType:"deployment_failed",appName:o.target,operation:"deployStack",stackType:e}))}}async deployStacksInParallel(e,o,t){const n=t?.onOutput,u=t?.onResourceProgress,i=t?.onStackComplete,c=await Promise.allSettled(e.map(async s=>{const a=Date.now(),r=await this.deployStack(s,o,{onOutput:n?p=>n(p,s):void 0,onResourceProgress:u?p=>u(p,s):void 0}),S=Date.now()-a,d={stack:s,success:r.success,error:r.success?void 0:r.error,duration:S,...r.success&&r.data.outputs!==void 0?{outputs:r.data.outputs}:{}};return!r.success&&r.error&&l.debug("deployStacksInParallel",`Stack ${s} failed`,{error:r.error.message}),i?.(s,r.success,S,r.success?void 0:r.error),d}));return f(A(c,e))}async destroyStack(e,o,t,n,u,i){const c=o.target,s=h(c,e);let a=[],r=n===!0;this.aws!==void 0&&!r&&(r=(await this.cdkService.runCdkSynth(o)).success);const S=await B(i,s,u,"destroy");if(S!==void 0)return y(S);if(this.aws!==void 0)if(r){const m=await N(this.aws.getClient(R),this.aws.getClient(v),s,{onLog:t?.onLog,onStackCleanupProgress:t?.onStackCleanupProgress},u);a=D(m)}else l.debug("ApplicationStackService","Skipping pre-empty pass after failed validation synth \u2014 the destroy will surface the error",{stackName:s});else l.debug("ApplicationStackService","No AWS provider available, skipping pre-empty pass",{stackName:s});const d=n===!0||r&&o.assemblyDir===void 0,p=await this.cdkService.runCdkDestroy(o,s,t?.onOutput,t?.onResourceProgress,this.aws,d);if(p.success)return this.aws!==void 0&&a.length>0&&await F(this.aws.getClient(v),a,{onLog:t?.onLog,onStackCleanupProgress:t?.onStackCleanupProgress},u),f({stackType:e,stackName:s,skipped:!1,outputs:void 0});{const m=P(p.error||`Failed to destroy ${e} stack`);return l.error("ApplicationStackService","Stack destroy failed",{stackType:e,target:o.target,error:m}),y(new w(m,{errorType:"destroy_failed",appName:o.target,operation:"destroyStack",stackType:e}))}}async destroyStacksInParallel(e,o,t,n,u){const i=t?.onOutput,c=t?.onResourceProgress,s=t?.onStackComplete,a=await Promise.allSettled(e.map(async r=>{const S=Date.now(),d=await this.destroyStack(r,o,{onOutput:i?k=>i(k,r):void 0,onResourceProgress:c?k=>c(k,r):void 0,onLog:t?.onLog,onStackCleanupProgress:t?.onStackCleanupProgress},n,u),p=Date.now()-S,m=d.success?"":d.error?.message??"",C=m.includes(_)||m.includes(L),g={stack:r,success:d.success||C,error:d.success||C?void 0:d.error,duration:p};return!g.success&&g.error&&l.debug("destroyStacksInParallel",`Stack ${r} failed`,{error:g.error.message}),s?.(r,g.success,p,g.success?void 0:g.error),g}));return f(A(a,e))}async getStackOutputs(e,o){const t=h(e,o),n=await this.cloudFormationService.getStackOutputs(t);return n.success?f(O(n.data)):y(new w(`Failed to get outputs for ${t}`,{errorType:"stack_error",appName:e,operation:"getStackOutputs",stackType:o,details:n.error}))}async resolveWebsiteUrl(e){return $(e,this.cloudFormationService)}async destroyAllStacks(e,o,t,n,u){return E(this,e,o,t,n,u)}}export{J as ApplicationStackService};
|
|
@@ -8,6 +8,8 @@ import { ApplicationError, type StackDeploymentData } from "../../types/applicat
|
|
|
8
8
|
import type { ApplicationStackService } from "./ApplicationStackService.js";
|
|
9
9
|
import type { CloudFormationService } from "../infrastructure/CloudFormationService.js";
|
|
10
10
|
import type { DeployCallbacks } from "../../types/callbacks.js";
|
|
11
|
+
import type { DeployParams } from "../../types/params.js";
|
|
12
|
+
import { LeaseDeniedError } from "../../orchestration/application/leaseGate.js";
|
|
11
13
|
/** Shared with ApplicationStackService.destroyAllStacks — the two signatures must not drift. */
|
|
12
14
|
export interface DestroyAllStacksCallbacks {
|
|
13
15
|
onOutput?: (chunk: string, stackId?: ApplicationStack) => void;
|
|
@@ -15,30 +17,30 @@ export interface DestroyAllStacksCallbacks {
|
|
|
15
17
|
onLog?: DeployCallbacks["onLog"];
|
|
16
18
|
onStackCleanupProgress?: DeployCallbacks["onStackCleanupProgress"];
|
|
17
19
|
onStackStart?: (stackType: ApplicationStack, stackName: string) => void;
|
|
18
|
-
onStackComplete?: (stackType: ApplicationStack, result: Result<StackDeploymentData, ApplicationError>) => void | Promise<void>;
|
|
20
|
+
onStackComplete?: (stackType: ApplicationStack, result: Result<StackDeploymentData, ApplicationError | LeaseDeniedError>) => void | Promise<void>;
|
|
19
21
|
onParallelPhaseStart?: (stacks: readonly ApplicationStack[], description: string) => void;
|
|
20
22
|
onParallelPhaseComplete?: (results: ParallelDeploymentResult[]) => void;
|
|
21
23
|
}
|
|
22
24
|
/**
|
|
23
25
|
* Convert "doesn't exist" errors to success for destroy operations.
|
|
24
26
|
*/
|
|
25
|
-
export declare function convertDestroyResultIfNotExists(result: Result<StackDeploymentData, ApplicationError>, stackType: ApplicationStack, appName: string): Result<StackDeploymentData, ApplicationError>;
|
|
27
|
+
export declare function convertDestroyResultIfNotExists(result: Result<StackDeploymentData, ApplicationError | LeaseDeniedError>, stackType: ApplicationStack, appName: string): Result<StackDeploymentData, ApplicationError | LeaseDeniedError>;
|
|
26
28
|
/**
|
|
27
29
|
* Handle destroy errors and determine if we should continue or fail
|
|
28
30
|
*/
|
|
29
|
-
export declare function handleDestroyError(result: Result<StackDeploymentData, ApplicationError>, stackType: ApplicationStack): {
|
|
31
|
+
export declare function handleDestroyError(result: Result<StackDeploymentData, ApplicationError | LeaseDeniedError>, stackType: ApplicationStack): {
|
|
30
32
|
continue: boolean;
|
|
31
33
|
result: Result<{
|
|
32
34
|
message: string;
|
|
33
|
-
}, ApplicationError>;
|
|
35
|
+
}, ApplicationError | LeaseDeniedError>;
|
|
34
36
|
} | null;
|
|
35
37
|
/**
|
|
36
38
|
* Destroy all stacks for an application in reverse order.
|
|
37
39
|
* Supports parallel destruction for OpenNext patterns (nextjs/payload).
|
|
38
40
|
*/
|
|
39
|
-
export declare function destroyAllStacks(service: ApplicationStackService, context: DeploymentContext, callbacks?: DestroyAllStacksCallbacks, resources?: AppResourceFlags, abortSignal?: AbortSignal): Promise<Result<{
|
|
41
|
+
export declare function destroyAllStacks(service: ApplicationStackService, context: DeploymentContext, callbacks?: DestroyAllStacksCallbacks, resources?: AppResourceFlags, abortSignal?: AbortSignal, assertLeaseForStack?: DeployParams["assertLeaseForStack"]): Promise<Result<{
|
|
40
42
|
message: string;
|
|
41
|
-
}, ApplicationError>>;
|
|
43
|
+
}, ApplicationError | LeaseDeniedError>>;
|
|
42
44
|
/**
|
|
43
45
|
* Map Promise.allSettled results to ParallelDeploymentResult[], handling
|
|
44
46
|
* rejected promises with a fallback stack identity.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{maskSensitiveOutput as
|
|
2
|
-
`);return
|
|
1
|
+
import{maskSensitiveOutput as k}from"@fjall/util";import{STACK_NOT_FOUND_PATTERN as A,CDK_NO_STACKS_MATCH as _,INFRASTRUCTURE_FILENAME as I}from"../../types/constants.js";import{APPLICATION_STACKS as L,getApplicationStackName as g,getApplicationDestroyOrder as V,getParallelDestroyGroups as v}from"../../types/operations.js";import{FrameworkRegistry as B}from"../../orchestration/builders/frameworkRegistry.js";import{success as T,failure as l,isFailure as $}from"@fjall/generator";import{ApplicationError as h}from"../../types/application/ApplicationServiceTypes.js";import{assertLeaseBeforeStack as E,LeaseDeniedError as j}from"../../orchestration/application/leaseGate.js";import{logger as x}from"@fjall/util/logger";function U(t,r,e){if(t.success)return t;const n=$(t)?t.error.message:"";return n.includes(A)||n.includes(_)?T({stackName:g(e,r),stackType:r,outputs:{},skipped:!0}):t}function K(t,r){if(t.success)return null;if($(t)&&t.error instanceof j)return{continue:!1,result:l(t.error)};const e=$(t)?k(t.error.message):"";if(e.includes("TSError")||e.includes("Unable to compile TypeScript")||e.includes("Subprocess exited with error")){const n=e.match(/infrastructure\.ts\(\d+,\d+\): error TS\d+: .+/),s=n?n[0]:`Check ${I} for syntax errors`;return{continue:!1,result:l(new h(`CDK synthesis failed: ${s}`,{errorType:"synth_failed",operation:`destroy-${r}`}))}}return e.includes(A)||e.includes(_)?{continue:!0,result:T({message:"Stack already deleted"})}:{continue:!1,result:l(new h(`Failed to destroy ${r} stack: ${e}`,{errorType:"destroy_failed",operation:`destroy-${r}`}))}}async function X(t,r,e,n,s,p){const o=r.target,O=B.createDefault().resolve({appPath:r.path});let N,R;if(O){const u=O.builder.plan({appPath:r.path},O.detection);N=u.parallelDestroy,R=u.destroyOrder}else N=!1,R=V({pattern:null,resources:n});if(N){const u=v();for(const D of u){const c=D.stacks;if(c.length===1){const a=c[0],S=g(o,a),y=await E(p,S,s,"destroy");if(y!==void 0)return l(y);e?.onStackStart?.(a,S);const m=await t.destroyStack(a,r,{onOutput:e?.onOutput?f=>e.onOutput?.(f,a):void 0,onResourceProgress:e?.onResourceProgress?f=>e.onResourceProgress?.(f,a):void 0,onLog:e?.onLog,onStackCleanupProgress:e?.onStackCleanupProgress},void 0,s,p),i=U(m,a,o);e?.onStackComplete&&await e.onStackComplete(a,i);const d=K(i,a);if(d){if(d.continue)continue;return d.result}}else{const a=await E(p,g(o,c[0]),s,"destroy");if(a!==void 0)return l(a);const S=await t.runCdkSynth(r);if(!S.success)return l(new h(S.error,{errorType:"synth_failed",appName:o,operation:"destroy-parallel-synth"}));const y=await E(p,g(o,c[0]),s,"destroy");if(y!==void 0)return l(y);e?.onParallelPhaseStart?.(c,D.description);for(const i of c)e?.onStackStart?.(i,g(o,i));const m=await t.destroyStacksInParallel(c,r,{onOutput:e?.onOutput,onResourceProgress:e?.onResourceProgress,onLog:e?.onLog,onStackCleanupProgress:e?.onStackCleanupProgress,onStackComplete:async(i,d,f,C)=>{const P=C?.message!==void 0?k(C.message):"Stack destruction failed",M=d?T({stackName:g(o,i),stackType:i,outputs:{}}):l(C instanceof h?C:new h(P,{errorType:"destroy_failed",appName:o,operation:`destroy-${i}`}));await e?.onStackComplete?.(i,M)}},!0,s);if(m.success){e?.onParallelPhaseComplete?.(m.data);const i=m.data.filter(d=>!d.success);if(i.length>0){const d=i.filter(f=>f.error&&!f.error.message.includes(A)&&!f.error.message.includes(_));if(d.length>0){const f=d.map(P=>P.stack).join(", "),C=d.map(P=>`${P.stack}: ${P.error?.message??"Unknown error"}`).join(`
|
|
2
|
+
`);return l(new h(`Failed to destroy stacks: ${f}
|
|
3
3
|
|
|
4
|
-
${
|
|
4
|
+
${C}`,{errorType:"destroy_failed",appName:o,operation:"destroy-parallel"}))}}}}}}else for(const u of R){const D=g(o,u),c=await E(p,D,s,"destroy");if(c!==void 0)return l(c);e?.onStackStart?.(u,D);const a=await t.destroyStack(u,r,{onOutput:e?.onOutput?m=>e.onOutput?.(m,u):void 0,onResourceProgress:e?.onResourceProgress?m=>e.onResourceProgress?.(m,u):void 0,onLog:e?.onLog,onStackCleanupProgress:e?.onStackCleanupProgress},void 0,s,p),S=U(a,u,o);e?.onStackComplete&&await e.onStackComplete(u,S);const y=K(S,u);if(y){if(y.continue)continue;return y.result}}return T({message:"All stacks destroyed"})}function Y(t,r){return t.map((e,n)=>e.status==="fulfilled"?e.value:{stack:r[n]??L.NETWORK,success:!1,error:e.reason instanceof Error?e.reason:new Error(String(e.reason)),duration:0})}async function Z(t,r){try{const e=g(t,L.COMPUTE),n=await r.getStackOutputs(e);if(n.success&&n.data.length>0){const o=n.data.find(O=>O.OutputKey?.includes("LoadBalancerUrl"));if(o?.OutputValue)return o.OutputValue.toLowerCase();const w=n.data.find(O=>O.OutputKey?.includes("LoadBalancerDnsName"));if(w?.OutputValue)return`http://${w.OutputValue.toLowerCase()}`}const s=g(t,L.CDN),p=await r.getStackOutputs(s);if(p.success&&p.data.length>0){const o=p.data.find(w=>w.OutputKey?.includes("DistributionDomainName"));if(o?.OutputValue)return`https://${o.OutputValue.toLowerCase()}`}return}catch(e){x.warn("ApplicationStackService","URL resolution failed",{error:k(e instanceof Error?e.message:String(e))});return}}export{U as convertDestroyResultIfNotExists,X as destroyAllStacks,K as handleDestroyError,Y as mapSettledResults,Z as resolveWebsiteUrl};
|