@fjall/deploy-core 2.25.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/index.d.ts +1 -1
- package/dist/src/aws/index.js +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 +2 -2
- 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/organisations/organisationalUnits.d.ts +15 -0
- package/dist/src/aws/organisations/organisationalUnits.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 +13 -2
- 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/databaseEndpointReconcile.d.ts +77 -0
- package/dist/src/orchestration/application/databaseEndpointReconcile.js +1 -0
- package/dist/src/orchestration/application/deploySessionPolicy.d.ts +9 -1
- package/dist/src/orchestration/application/deploySessionPolicy.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/dockerBuildHelper.js +1 -1
- package/dist/src/orchestration/application/ecrCacheRepository.d.ts +14 -0
- package/dist/src/orchestration/application/ecrCacheRepository.js +1 -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 +28 -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/CdkArgumentBuilder.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/CdkServiceTypes.d.ts +7 -0
- 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/cdkServiceHelpers.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/services/supporting/CdkContextBuilder.d.ts +1 -0
- package/dist/src/services/supporting/CdkContextBuilder.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/deployment/DeploymentTypes.d.ts +1 -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 +161 -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,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};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { CdkService, CdkArgumentBuilder, CdkProcessManager, CdkEventMonitor, startStackMonitoring, DEFAULT_DEPLOY_TIMEOUT_MS, isCdkError, formatInfrastructureError, getStructuralHint, getSourceContext, hasCdkDifferences, parseDiffOutput, CloudFormationService, CloudFormationError, EcsService, EcsError, EcsServiceResolver, type CfnExportsClient } from "./infrastructure/index.js";
|
|
2
|
-
export type { CdkContext, CdkOptions, CdkOutput, CheckDifferencesResult, CdkServiceOptions, ICdkProcessManager, DiffDetails, CloudFormationCallbacks, ECSServiceInfo, ECSClusterInfo, DeploymentStatus, ECSDeploymentOptions } from "./infrastructure/index.js";
|
|
1
|
+
export { CdkService, CdkArgumentBuilder, CdkProcessManager, CdkEventMonitor, startStackMonitoring, DEFAULT_DEPLOY_TIMEOUT_MS, isCdkError, formatInfrastructureError, getStructuralHint, getSourceContext, hasCdkDifferences, parseDiffOutput, CloudFormationService, CloudFormationError, CfnRegistryService, CFN_REGISTRY_SEED, EcsService, EcsError, EcsServiceResolver, type CfnExportsClient } from "./infrastructure/index.js";
|
|
2
|
+
export type { CdkContext, CdkOptions, CdkOutput, CheckDifferencesResult, CdkServiceOptions, ICdkProcessManager, DiffDetails, CloudFormationCallbacks, CfnRegistrySchemaSummary, CfnRegistryServiceOptions, CfnRegistrySummaryReader, ECSServiceInfo, ECSClusterInfo, DeploymentStatus, ECSDeploymentOptions } from "./infrastructure/index.js";
|
|
3
3
|
export { TemplateHashService, TemplateHashError, type TemplateComparisonResult } from "./supporting/index.js";
|
|
4
4
|
export { CdkContextBuilder } from "./supporting/index.js";
|
|
5
5
|
export { emitProgress, PROGRESS_MESSAGES, parseBuildPhase, buildStepContextBuildConfig, convertCloudFormationOutputsToRecord, type CloudFormationOutput } from "./supporting/index.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{CdkService as o,CdkArgumentBuilder as t,CdkProcessManager as i,CdkEventMonitor as a,startStackMonitoring as c,DEFAULT_DEPLOY_TIMEOUT_MS as s,isCdkError as
|
|
1
|
+
import{CdkService as o,CdkArgumentBuilder as t,CdkProcessManager as i,CdkEventMonitor as a,startStackMonitoring as c,DEFAULT_DEPLOY_TIMEOUT_MS as s,isCdkError as S,formatInfrastructureError as n,getStructuralHint as u,getSourceContext as E,hasCdkDifferences as d,parseDiffOutput as C,CloudFormationService as p,CloudFormationError as f,CfnRegistryService as l,CFN_REGISTRY_SEED as m,EcsService as v,EcsError as k,EcsServiceResolver as g}from"./infrastructure/index.js";import{TemplateHashService as R,TemplateHashError as T}from"./supporting/index.js";import{CdkContextBuilder as _}from"./supporting/index.js";import{emitProgress as F,PROGRESS_MESSAGES as O,parseBuildPhase as P,buildStepContextBuildConfig as h,convertCloudFormationOutputsToRecord as A}from"./supporting/index.js";import{ApplicationStackService as G}from"./application/index.js";export{G as ApplicationStackService,m as CFN_REGISTRY_SEED,t as CdkArgumentBuilder,_ as CdkContextBuilder,a as CdkEventMonitor,i as CdkProcessManager,o as CdkService,l as CfnRegistryService,f as CloudFormationError,p as CloudFormationService,s as DEFAULT_DEPLOY_TIMEOUT_MS,k as EcsError,v as EcsService,g as EcsServiceResolver,O as PROGRESS_MESSAGES,T as TemplateHashError,R as TemplateHashService,h as buildStepContextBuildConfig,A as convertCloudFormationOutputsToRecord,F as emitProgress,n as formatInfrastructureError,E as getSourceContext,u as getStructuralHint,d as hasCdkDifferences,S as isCdkError,P as parseBuildPhase,C as parseDiffOutput,c as startStackMonitoring};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{filterDangerousEnvVars as
|
|
1
|
+
import{filterDangerousEnvVars as o}from"@fjall/util";class f{buildContextArgs(r){const e=[];return r?.accountId&&e.push("-c",`accountId=${r.accountId}`),r?.environment&&e.push("-c",`environment=${r.environment}`),r?.managedAccount&&e.push("-c","managedAccount=true"),r?.accountName&&e.push("-c",`accountName=${r.accountName}`),r?.orgId&&e.push("-c",`orgId=${r.orgId}`),r?.rootId&&e.push("-c",`rootId=${r.rootId}`),r?.managementAccountId&&e.push("-c",`managementAccountId=${r.managementAccountId}`),r?.devOuId&&e.push("-c",`fjallDevOuId=${r.devOuId}`),r?.ipamPoolId&&e.push("-c",`ipamPoolId=${r.ipamPoolId}`),r?.fjallOrgId&&e.push("-c",`fjallOrgId=${r.fjallOrgId}`),r?.fjallOidcConfigured&&e.push("-c",`fjallOidcConfigured=${r.fjallOidcConfigured}`),r?.fjallAccountGlobalsConfigured&&e.push("-c",`fjallAccountGlobalsConfigured=${r.fjallAccountGlobalsConfigured}`),r?.fjallAccountTrailState&&e.push("-c",`fjallAccountTrailState=${r.fjallAccountTrailState}`),r?.orgConfig&&e.push("-c",`orgConfig=${r.orgConfig}`),r?.fjallAdoptBackupVault&&e.push("-c","fjallAdoptBackupVault=true"),r?.resolvedSecretArns&&e.push("-c",`fjallResolvedSecretArns=${r.resolvedSecretArns}`),e}buildParameterArgs(r,e){if(r===void 0)return[];const l=Object.entries(r);if(l.length===0)return[];const i=e!==void 0&&e!==""?`${e}:`:"",n=[];for(const[a,u]of l){if(!/^[A-Za-z][A-Za-z0-9]*$/.test(a))throw new Error(`Invalid CloudFormation parameter name "${a}": must match /^[A-Za-z][A-Za-z0-9]*$/ (alphanumeric, leading letter, no separators).`);if(u==="")throw new Error(`CloudFormation parameter "${a}" has an empty value.`);if(/[,\n\r]/.test(u))throw new Error(`CloudFormation parameter "${a}" value contains "," or newline \u2014 cdk's --parameters splits on "," so the deploy would silently fragment.`);n.push("--parameters",`${i}${a}=${u}`)}return n}injectCascadeCredentials(r,e){e&&(r.AWS_ACCESS_KEY_ID=e.accessKeyId,r.AWS_SECRET_ACCESS_KEY=e.secretAccessKey,delete r.AWS_SESSION_TOKEN,e.sessionToken&&(r.AWS_SESSION_TOKEN=e.sessionToken))}buildCdkEnv(r){const e={...o(process.env),CI:"true",FORCE_COLOR:"0",CDK_DISABLE_VERSION_CHECK:"1",NODE_NO_WARNINGS:"1"};return r?.context?.region&&(e.AWS_REGION=r.context.region,e.AWS_DEFAULT_REGION=r.context.region,e.CDK_DEFAULT_REGION=r.context.region),r?.context?.accountId&&(e.CDK_DEFAULT_ACCOUNT=r.context.accountId),this.injectCascadeCredentials(e,r?.credentials),e}}export{f as CdkArgumentBuilder};
|
|
@@ -11,6 +11,21 @@ export declare function startStackMonitoring(monitor: CloudFormationEventMonitor
|
|
|
11
11
|
failureReason?: string;
|
|
12
12
|
logPath?: string;
|
|
13
13
|
}>;
|
|
14
|
+
/** Grace budget for the monitor to finish analysing after a failed deploy. */
|
|
15
|
+
export declare const MONITORING_ANALYSIS_GRACE_MS = 10000;
|
|
16
|
+
/**
|
|
17
|
+
* Await the monitor's failure analysis after a failed CDK exit, bounded by a
|
|
18
|
+
* grace timeout so a hung monitor can never deadlock the deploy result (Q12).
|
|
19
|
+
*
|
|
20
|
+
* Two waits are required: `waitForStackComplete` resolves from its own poll
|
|
21
|
+
* loop the moment the stack reaches a terminal state, while the analysis is
|
|
22
|
+
* computed slightly LATER by the event poll loop (`handleStackCompletion`
|
|
23
|
+
* awaits a DescribeStacks before storing it) — so after the monitoring
|
|
24
|
+
* promise settles, the analysis is polled briefly within the same grace
|
|
25
|
+
* budget. A null `monitoringPromise` means monitoring never started (CDK
|
|
26
|
+
* failed pre-CloudFormation): return immediately, no analysis is coming.
|
|
27
|
+
*/
|
|
28
|
+
export declare function settleMonitoringAnalysis(monitor: CloudFormationEventMonitor | null, monitoringPromise: Promise<unknown> | null, graceMs?: number): Promise<FailureAnalysis | null>;
|
|
14
29
|
export declare class CdkEventMonitor {
|
|
15
30
|
private eventLogWriterFactory?;
|
|
16
31
|
private onFailureAnalysis?;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{logger as u}from"@fjall/util/logger";import{getErrorMessage as
|
|
1
|
+
import{logger as u}from"@fjall/util/logger";import{getErrorMessage as y,sleep as d}from"@fjall/util";import{CloudFormationFailureAnalyser as g}from"@fjall/util/aws";const F=18e5;function L(t,e,n){return t.waitForStackComplete(e,{timeout:F,pollInterval:2e3,onResourceUpdate:n,onStackComplete:(o,r)=>{}}).catch(o=>(u.debug("CdkService","Stack monitoring failed",{error:y(o)}),{success:!1,failureReason:"Monitoring failed"}))}const m=1e4,p=200;async function S(t,e,n=m){if(t===null)return null;if(e===null)return t.getFailureAnalysis();const o=Date.now()+n;let r;const i=new Promise(a=>{r=setTimeout(()=>a("grace-elapsed"),n)});try{if(await Promise.race([e,i])==="grace-elapsed")return t.getFailureAnalysis()}finally{r!==void 0&&clearTimeout(r)}for(;t.getFailureAnalysis()===null&&Date.now()<o;)await d(p);return t.getFailureAnalysis()}class M{eventLogWriterFactory;onFailureAnalysis;constructor(e){this.eventLogWriterFactory=e?.eventLogWriterFactory,this.onFailureAnalysis=e?.onFailureAnalysis}async createEventMonitor(e,n,o,r,i){const a=await import("../../aws/utils/cloudformationEvents.js"),s=new a.CloudFormationEventMonitor(i,{failureAnalyser:new g,eventLogWriterFactory:this.eventLogWriterFactory,...this.onFailureAnalysis!==void 0?{onFailureAnalysis:this.onFailureAnalysis}:{}}),l=`${e}-${Date.now()}`,c=r.target||r.deployType||"deployment";return s.enableLogging(l,n,o,c),s}}export{M as CdkEventMonitor,F as DEFAULT_DEPLOY_TIMEOUT_MS,m as MONITORING_ANALYSIS_GRACE_MS,S as settleMonitoringAnalysis,L as startStackMonitoring};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { FailureAnalysis } from "@fjall/util/aws";
|
|
1
2
|
import { type ResourceEvent, type CloudFormationEventMonitor } from "../../aws/utils/cloudformationEvents.js";
|
|
2
3
|
import type { StepOutput } from "../../types/deployment/DeploymentTypes.js";
|
|
3
4
|
import type { Result } from "@fjall/generator";
|
|
@@ -6,6 +7,13 @@ import type { CdkOutput } from "./CdkService.js";
|
|
|
6
7
|
* Analyse CDK deploy output and classify the result as success/skip/failure.
|
|
7
8
|
*/
|
|
8
9
|
export declare function analyseDeployOutput(cdkOutput: string, result: Result<CdkOutput, string>, stackName: string): Result<StepOutput, string>;
|
|
10
|
+
/**
|
|
11
|
+
* Compose the returned deploy error from the monitor's settled failure
|
|
12
|
+
* analysis (Q12: the analysed root cause reaches the RETURNED error, not just
|
|
13
|
+
* the `onFailureAnalysis` stream). The analysis is already masked at the
|
|
14
|
+
* monitor boundary (`maskFailureAnalysis`).
|
|
15
|
+
*/
|
|
16
|
+
export declare function formatAnalysedDeployFailure(analysis: FailureAnalysis): string;
|
|
9
17
|
/**
|
|
10
18
|
* Classify a bootstrap error into a user-friendly message.
|
|
11
19
|
*/
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
import{logger as
|
|
1
|
+
import{logger as f}from"@fjall/util/logger";import{maskSensitiveOutput as u}from"@fjall/util";import{STACK_NOT_FOUND_PATTERN as m,CDK_NO_STACKS_MATCH as a}from"../../aws/utils/cloudformationEvents.js";import{success as i,failure as l}from"@fjall/generator";import{formatInfrastructureError as k}from"./CdkErrorFormatter.js";import{startStackMonitoring as S}from"./CdkEventMonitoring.js";function x(e,t,s){const c=new RegExp(`${s}[:\\s|]*.*?(?:stack has no resources|skipping deployment)`,"i").test(e),r=e.includes("no changes")||e.includes("Stack is up to date")||e.includes("No changes to deploy"),p=e.includes(a)||!t.success&&t.error.includes(a);if(t.success)return i({message:c?"Stack has no resources, skipped":r?"Infrastructure already up to date":"Deployment successful",status:r?"NO_CHANGES":void 0,details:{output:e,skipped:c}});if(p)return i({message:"Stack not defined, skipped",status:"SKIPPED",details:{output:e,skipped:!0}});let n=u(t.error||"Deployment failed");if(e.includes("failed:")){const d=e.match(/failed: .*?: (.*?)(?:\n|$)/);d&&(n=u(d[1]))}return f.error("CdkService","CDK deployment returned failure",{errorMessage:n}),l(n)}function A(e){const t=[e.summary],{resource:s,reason:o}=e.rootCause;if(t.push(`Root cause: ${s.logicalId} \u2014 ${o}`),e.driftSuspects!==void 0&&e.driftSuspects.length>0){const c=e.driftSuspects.map(r=>`${r.logicalId} (${r.resourceType})`).join(", ");t.push(`Out-of-band deletion suspected: ${c}. Run 'fjall drift detect' to confirm; confirmed deletions are remediated with 'fjall drift repair'.`)}return t.join(`
|
|
2
|
+
`)}function P(e){return e.includes("Access Denied")||e.includes("AccessDenied")?"Access denied. Check your AWS credentials and permissions":e.includes("ExpiredToken")?"AWS credentials have expired. Please refresh your credentials":e.includes("ENOTFOUND")||e.includes("ECONNREFUSED")?"Network error. Please check your internet connection and try again":e.includes("timeout")?"Bootstrap operation timed out":k(e)}function T(e){return e.success?i({message:"Stack destroyed successfully"}):e.error.includes(m)||e.error.includes(a)?i({message:"Stack already deleted or does not exist",details:{skipped:!0}}):l(e.error||"Destroy failed")}function N(e){const t=e.match(/Deploying stack\s+([^\s]+)/);if(t)return{detected:!0,stackName:t[1]};const s=e.match(/^([^\s|]+)\s*\|\s*\d+\/\d+/m);if(s)return{detected:!0,stackName:s[1]};const o=e.match(/arn:aws:cloudformation:[^:]+:[^:]+:stack\/([^/]+)\//);return o?{detected:!0,stackName:o[1]}:{detected:!1}}function R(e,t,s,o){return c=>{if(e.cdkOutput+=c,t?.(c),!e.stackDetected){const r=N(c);r.detected&&r.stackName&&(e.actualStackName=r.stackName,e.stackDetected=!0),e.stackDetected&&s&&!e.monitoringPromise&&(e.monitoringPromise=S(s,e.actualStackName,o))}}}export{P as analyseBootstrapError,x as analyseDeployOutput,T as analyseDestroyResult,R as createEnhancedOutputCallback,N as detectStackNameFromChunk,A as formatAnalysedDeployFailure};
|