@fjall/deploy-core 2.27.0 → 2.30.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 +16 -3
- 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/dockerBuildHelper.js +1 -1
- package/dist/src/orchestration/application/leaseGate.d.ts +30 -0
- package/dist/src/orchestration/application/leaseGate.js +1 -0
- package/dist/src/orchestration/application/openNextBuild.d.ts +1 -1
- package/dist/src/orchestration/application/openNextBuild.js +3 -3
- 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/builders/frameworkRegistry.js +1 -1
- package/dist/src/orchestration/builders/index.d.ts +1 -0
- package/dist/src/orchestration/builders/index.js +1 -1
- package/dist/src/orchestration/builders/openNextBuilder.js +1 -1
- package/dist/src/orchestration/builders/staticSiteBuilder.d.ts +27 -0
- package/dist/src/orchestration/builders/staticSiteBuilder.js +2 -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/orchestration/spawnHelpers.d.ts +9 -0
- package/dist/src/orchestration/spawnHelpers.js +1 -1
- 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/deployEvent.d.ts +10 -3
- package/dist/src/types/deployEvent.js +1 -1
- 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/detection/patternTypes.d.ts +9 -4
- package/dist/src/types/detection/patternTypes.js +1 -1
- package/dist/src/types/drift.d.ts +72 -0
- package/dist/src/types/drift.js +1 -0
- package/dist/src/types/frameworkBuilder.d.ts +14 -0
- package/dist/src/types/index.d.ts +44 -4
- package/dist/src/types/index.js +1 -1
- package/dist/src/types/operations.d.ts +2 -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 +6 -4
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Change-set escalation — Layer 2 of the replacement oracle (drift-remediation
|
|
3
|
+
* plan § 2, [G4]). Runs the review-only change-set probe ONLY on rows whose
|
|
4
|
+
* Layer-1 verdict is `conditional`/`may` on a stateful or pinned-name
|
|
5
|
+
* resource: definite createOnly hits are already proven, and stateless
|
|
6
|
+
* unpinned conditionals never ticket, so probing either buys no proof. A
|
|
7
|
+
* non-engaged deploy issues zero probe calls.
|
|
8
|
+
*
|
|
9
|
+
* Verdict precedence: change-set > registry > cdk-spec.
|
|
10
|
+
* Replacement: False → the ceremony DOWNGRADES to a warning — the row
|
|
11
|
+
* becomes an in-place update (directive 2:
|
|
12
|
+
* never block on a false positive)
|
|
13
|
+
* Replacement: True → conditional upgrades to definite, verdictSource
|
|
14
|
+
* `"change-set"`
|
|
15
|
+
* Replacement: Conditional → stays worst-case (the registry verdict stands)
|
|
16
|
+
* probe failure → the registry verdict stands, annotated
|
|
17
|
+
* `"registry (change-set unverified)"` — degrade
|
|
18
|
+
* to the conservative verdict, never to silence
|
|
19
|
+
*
|
|
20
|
+
* A resource the change set omits entirely (including the benign
|
|
21
|
+
* `didn't contain changes` outcome) will not be touched by the update —
|
|
22
|
+
* treated as `False`.
|
|
23
|
+
*/
|
|
24
|
+
import type { ChangeSetProbeRunner } from "../../../services/infrastructure/changeSetProbe.js";
|
|
25
|
+
import type { StackTemplatePair } from "./buildDeployPlan.js";
|
|
26
|
+
import type { DeployPlan, DeployPlanResourceChange } from "./types.js";
|
|
27
|
+
/**
|
|
28
|
+
* The [G4] trigger predicate: a replacement row that is neither proven
|
|
29
|
+
* (`will`) nor irrelevant (`none`), on a resource that would actually ticket
|
|
30
|
+
* — stateful (data loss) or pinned-name (CloudFormation refusal).
|
|
31
|
+
*/
|
|
32
|
+
export declare function needsChangeSetEscalation(change: DeployPlanResourceChange): boolean;
|
|
33
|
+
export interface ApplyChangeSetEscalationParams {
|
|
34
|
+
plan: DeployPlan;
|
|
35
|
+
probe: ChangeSetProbeRunner;
|
|
36
|
+
/** The diffed template pairs — the probe submits `newTemplate`. */
|
|
37
|
+
stacks: readonly StackTemplatePair[];
|
|
38
|
+
abortSignal?: AbortSignal;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Apply the change-set escalation to an overlay-corrected plan. Never throws
|
|
42
|
+
* and never fails the plan: a failed or aborted probe leaves the conservative
|
|
43
|
+
* registry verdict in place, annotated unverified. One probe per escalated
|
|
44
|
+
* stack covers every candidate row in it.
|
|
45
|
+
*/
|
|
46
|
+
export declare function applyChangeSetEscalation(params: ApplyChangeSetEscalationParams): Promise<DeployPlan>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{maskSensitiveOutput as k}from"@fjall/util";import{logger as u}from"@fjall/util/logger";import{isAborted as S}from"../../../aws/organisations/types.js";import{isDataLoss as w,isStatefulResourceType as g}from"./classify.js";import{recountActions as T}from"./registryOverlay.js";import{renderPlanSummary as C}from"./renderDeployPlan.js";const p="ChangeSetEscalation";function M(t){return t.action!=="replace"||t.retained||t.replacementMode!=="conditional"&&t.replacementMode!=="may"?!1:g(t.resourceType)||t.pinnedPhysicalName!==void 0}function b(t){return`change-set preflight proved this update applies in place (${t.replacementMode} verdict downgraded)`}function N(t){return{...t,action:"update",replacementMode:"none",destructive:!1,dataLoss:!1,propertyChanges:t.propertyChanges.map(r=>({...r,forcesReplacement:!1})),verdictSource:"change-set",interruptionNote:b(t)}}function L(t){return{...t,action:"replace",replacementMode:"will",destructive:!0,dataLoss:w({action:"replace",retained:t.retained,resourceType:t.resourceType}),verdictSource:"change-set"}}function $(t){return t.verdictSource!=="registry"?t:{...t,verdictSource:"registry (change-set unverified)"}}async function A(t){const{plan:r,probe:f,stacks:l,abortSignal:i}=t,c=r.changes.filter(M);if(c.length===0)return r;const m=new Map(l.map(e=>[e.stackName,e])),y=[...new Set(c.map(e=>e.stack))],a=new Map;for(const e of y){if(S(i)){a.set(e,"unverified");continue}const n=m.get(e);if(n===void 0){u.warn(p,"No synthesised template for escalated stack",{stackName:e}),a.set(e,"unverified");continue}const s=await f.probeStackReplacements({stackName:e,template:n.newTemplate,...i!==void 0?{abortSignal:i}:{}});if(!s.success){u.warn(p,"Change-set probe degraded to registry verdict",{stackName:e,error:k(s.error.message)}),a.set(e,"unverified");continue}a.set(e,s.data.replacementByLogicalId)}const v=new Set(c.map(e=>`${e.stack}|${e.address}`)),o=r.changes.map(e=>{if(!v.has(`${e.stack}|${e.address}`))return e;const n=a.get(e.stack);if(n===void 0||n==="unverified")return $(e);const s=n.get(e.address)??"False";return s==="False"?N(e):s==="True"?L(e):(u.warn(p,"Change set could not decide \u2014 keeping worst case",{stack:e.stack,address:e.address,resourceType:e.resourceType}),e)}),d=T(o);return d["no-op"]=r.counts["no-op"],{...r,changes:o,counts:d,hasDestructiveChanges:o.some(e=>e.destructive),summary:C({counts:d,changes:o})}}export{A as applyChangeSetEscalation,M as needsChangeSetEscalation};
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { type Result } from "@fjall/generator";
|
|
8
8
|
import type { CloudFormationError } from "../../../services/infrastructure/CloudFormationService.js";
|
|
9
|
+
import type { CfnRegistrySummaryReader } from "../../../services/infrastructure/CfnRegistryService.js";
|
|
10
|
+
import type { ChangeSetProbeRunner } from "../../../services/infrastructure/changeSetProbe.js";
|
|
9
11
|
import type { DeployPlan } from "./types.js";
|
|
10
12
|
/** The narrow CFN read surface the gate needs — CloudFormationService satisfies it. */
|
|
11
13
|
export interface CfnTemplateReader {
|
|
@@ -20,5 +22,16 @@ export interface ComputeDeployPlanParams {
|
|
|
20
22
|
cfnService: CfnTemplateReader;
|
|
21
23
|
stacks: ComputeDeployPlanStack[];
|
|
22
24
|
assemblyDigest: string;
|
|
25
|
+
/**
|
|
26
|
+
* Registry oracle for replacement-verdict recomputation. Optional — without
|
|
27
|
+
* it the plan carries raw cloudformation-diff verdicts (no `verdictSource`).
|
|
28
|
+
*/
|
|
29
|
+
registry?: CfnRegistrySummaryReader;
|
|
30
|
+
/**
|
|
31
|
+
* Change-set escalation probe [G4]. Optional — without it conditional
|
|
32
|
+
* verdicts stay worst-cased into the ceremony (the sanctioned D2 fallback).
|
|
33
|
+
*/
|
|
34
|
+
changeSetProbe?: ChangeSetProbeRunner;
|
|
35
|
+
abortSignal?: AbortSignal;
|
|
23
36
|
}
|
|
24
37
|
export declare function computeDeployPlan(params: ComputeDeployPlanParams): Promise<Result<DeployPlan, Error>>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{success as
|
|
1
|
+
import{success as i,failure as s}from"@fjall/generator";import{maskSensitiveOutput as c}from"@fjall/util";import{buildDeployPlan as f}from"./buildDeployPlan.js";import{applyChangeSetEscalation as u}from"./changeSetEscalation.js";import{applyRegistryOverlay as p}from"./registryOverlay.js";function d(e){const r=e.trim();if(r==="")return i({});try{return i(JSON.parse(r))}catch(t){return s(new Error(`Failed to parse deployed template: ${c(t instanceof Error?t.message:String(t))}`))}}async function k(e){const r=[];for(const o of e.stacks){const n=await e.cfnService.getTemplate(o.stackName);let l={};if(n.success){const a=d(n.data);if(!a.success)return s(a.error);l=a.data}else if(n.error.errorType!=="stack_not_found")return s(n.error);r.push({stackName:o.stackName,oldTemplate:l,newTemplate:o.newTemplate})}let t=f(r,e.assemblyDigest);return e.registry!==void 0&&(t=await p({plan:t,registry:e.registry,stacks:r,...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}})),e.changeSetProbe!==void 0&&(t=await u({plan:t,probe:e.changeSetProbe,stacks:r,...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}})),i(t)}export{k as computeDeployPlan};
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/** Deploy-plan computation + rendering — the approval gate's read-side core. */
|
|
2
|
-
export type
|
|
2
|
+
export { REPLACEMENT_VERDICT_SOURCES, type DeployPlan, type DeployPlanResourceChange, type ReplacementVerdictSource } from "./types.js";
|
|
3
3
|
export { buildDeployPlan, type StackTemplatePair } from "./buildDeployPlan.js";
|
|
4
|
+
export { applyRegistryOverlay, overlayResourceChange, registryVerdictForProperty, type ApplyRegistryOverlayParams, type OverlayResourceChangeResult, type RegistryVerdictDisagreement } from "./registryOverlay.js";
|
|
5
|
+
export { applyChangeSetEscalation, needsChangeSetEscalation, type ApplyChangeSetEscalationParams } from "./changeSetEscalation.js";
|
|
6
|
+
export { findPinnedPhysicalName, resourcePropertiesFromTemplate } from "./pinnedNames.js";
|
|
4
7
|
export { computeDeployPlan, type CfnTemplateReader, type ComputeDeployPlanParams, type ComputeDeployPlanStack } from "./computeDeployPlan.js";
|
|
5
8
|
export { computeAssemblyDigest, digestHead } from "./assemblyDigest.js";
|
|
6
9
|
export { classifyImpact, derivePropertyChanges, isDataLoss, isStatefulResourceType, type ImpactClassification } from "./classify.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{REPLACEMENT_VERDICT_SOURCES as o}from"./types.js";import{buildDeployPlan as t}from"./buildDeployPlan.js";import{applyRegistryOverlay as s,overlayResourceChange as n,registryVerdictForProperty as l}from"./registryOverlay.js";import{applyChangeSetEscalation as m,needsChangeSetEscalation as y}from"./changeSetEscalation.js";import{findPinnedPhysicalName as f,resourcePropertiesFromTemplate as T}from"./pinnedNames.js";import{computeDeployPlan as d}from"./computeDeployPlan.js";import{computeAssemblyDigest as x,digestHead as A}from"./assemblyDigest.js";import{classifyImpact as R,derivePropertyChanges as u,isDataLoss as C,isStatefulResourceType as L}from"./classify.js";import{renderPlanSummary as _,renderPlanLines as h,toWirePlanChanges as v}from"./renderDeployPlan.js";import{signApprovalToken as O,verifyApprovalToken as N,DEFAULT_APPROVAL_TTL_MS as V,APPROVAL_TOKEN_PATTERN as F}from"./approvalToken.js";export{F as APPROVAL_TOKEN_PATTERN,V as DEFAULT_APPROVAL_TTL_MS,o as REPLACEMENT_VERDICT_SOURCES,m as applyChangeSetEscalation,s as applyRegistryOverlay,t as buildDeployPlan,R as classifyImpact,x as computeAssemblyDigest,d as computeDeployPlan,u as derivePropertyChanges,A as digestHead,f as findPinnedPhysicalName,C as isDataLoss,L as isStatefulResourceType,y as needsChangeSetEscalation,n as overlayResourceChange,l as registryVerdictForProperty,h as renderPlanLines,_ as renderPlanSummary,T as resourcePropertiesFromTemplate,O as signApprovalToken,v as toWirePlanChanges,N as verifyApprovalToken};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deploy-plan loading — reads each changed stack's synthesised template from
|
|
3
|
+
* cdk.out and delegates to {@link computeDeployPlan} (live-template diff +
|
|
4
|
+
* registry overlay). Extracted from the approval gate so the unconditional
|
|
5
|
+
* destruction gate shares the SAME computation: `createMemoisedPlanLoader`
|
|
6
|
+
* guarantees a plan-gated deploy never diffs twice, while a non-engaged deploy
|
|
7
|
+
* computes the plan exactly once for the destruction gate.
|
|
8
|
+
*/
|
|
9
|
+
import { type Result } from "@fjall/generator";
|
|
10
|
+
import type { CfnRegistrySummaryReader } from "../../../services/infrastructure/CfnRegistryService.js";
|
|
11
|
+
import type { ChangeSetProbeRunner } from "../../../services/infrastructure/changeSetProbe.js";
|
|
12
|
+
import { type CfnTemplateReader } from "./computeDeployPlan.js";
|
|
13
|
+
import type { DeployPlan } from "./types.js";
|
|
14
|
+
export interface LoadDeployPlanParams {
|
|
15
|
+
cfnService: CfnTemplateReader;
|
|
16
|
+
cdkOutPath: string;
|
|
17
|
+
changedStacks: string[];
|
|
18
|
+
assemblyDigest: string;
|
|
19
|
+
registry?: CfnRegistrySummaryReader;
|
|
20
|
+
changeSetProbe?: ChangeSetProbeRunner;
|
|
21
|
+
abortSignal?: AbortSignal;
|
|
22
|
+
}
|
|
23
|
+
/** A zero-argument plan supplier shared between the gates. */
|
|
24
|
+
export type DeployPlanLoader = () => Promise<Result<DeployPlan, Error>>;
|
|
25
|
+
export declare function loadDeployPlan(params: LoadDeployPlanParams): Promise<Result<DeployPlan, Error>>;
|
|
26
|
+
/**
|
|
27
|
+
* Memoise the plan computation for one deploy invocation: the first call
|
|
28
|
+
* computes (template reads + live diff + registry overlay), every later call
|
|
29
|
+
* returns the same settled promise — including a failure, which both gates
|
|
30
|
+
* must observe identically.
|
|
31
|
+
*/
|
|
32
|
+
export declare function createMemoisedPlanLoader(params: LoadDeployPlanParams): DeployPlanLoader;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{join as c}from"node:path";import{readFile as a}from"node:fs/promises";import{failure as l}from"@fjall/generator";import{maskSensitiveOutput as s}from"@fjall/util";import{computeDeployPlan as f}from"./computeDeployPlan.js";async function d(e){const t=[];for(const r of e.changedStacks){const i=c(e.cdkOutPath,`${r}.template.json`);let o;try{o=JSON.parse(await a(i,"utf8"))}catch(n){return l(new Error(s(`Failed to read synthesised template for ${r}: ${n instanceof Error?n.message:String(n)}`)))}t.push({stackName:r,newTemplate:o})}return f({cfnService:e.cfnService,stacks:t,assemblyDigest:e.assemblyDigest,...e.registry!==void 0?{registry:e.registry}:{},...e.changeSetProbe!==void 0?{changeSetProbe:e.changeSetProbe}:{},...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}})}function y(e){let t;return()=>(t??=d(e),t)}export{y as createMemoisedPlanLoader,d as loadDeployPlan};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pinned-name detection: a resource is custom-named iff the property named by
|
|
3
|
+
* the registry schema's `primaryIdentifier` (fallback: the small
|
|
4
|
+
* `@fjall/util` map for types whose identifier is a generated attribute) is
|
|
5
|
+
* explicitly present in the synthesised template with a LITERAL value. The
|
|
6
|
+
* template is the source of truth — CDK-generated names arrive as intrinsics
|
|
7
|
+
* (`Ref` / `Fn::*`) or are omitted entirely, so a literal string here means
|
|
8
|
+
* the author pinned the physical name.
|
|
9
|
+
*
|
|
10
|
+
* `replacement-required ∧ custom-named ⇒ pinned-name-replacement`: the update
|
|
11
|
+
* fails at apply time with "CloudFormation cannot update a stack when a
|
|
12
|
+
* custom-named resource requires replacing" — the Phase 2 destruction gate
|
|
13
|
+
* consumes this signal; Phase 1 annotates it on the plan row.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Detect a pinned physical name on a template resource. Returns the literal
|
|
17
|
+
* name, or undefined when the resource is CDK-named (intrinsic or absent) or
|
|
18
|
+
* the naming property cannot be determined.
|
|
19
|
+
*/
|
|
20
|
+
export declare function findPinnedPhysicalName(params: {
|
|
21
|
+
resourceType: string;
|
|
22
|
+
/** The resource's template `Properties` block. */
|
|
23
|
+
properties: Record<string, unknown> | undefined;
|
|
24
|
+
/** Registry `primaryIdentifier` pointers, when the oracle resolved them. */
|
|
25
|
+
primaryIdentifier?: readonly string[];
|
|
26
|
+
}): string | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Extract a resource's `Properties` block from a parsed CloudFormation
|
|
29
|
+
* template without asserting the template's overall shape.
|
|
30
|
+
*/
|
|
31
|
+
export declare function resourcePropertiesFromTemplate(template: unknown, logicalId: string): Record<string, unknown> | undefined;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{PHYSICAL_NAME_FALLBACK_PROPERTIES as f}from"@fjall/util";const o="/properties/";function d(e){if(e===void 0||e.length!==1)return;const r=e[0];if(r===void 0||!r.startsWith(o))return;const n=r.slice(o.length);if(!(n===""||n.includes("/")))return n}function u(e){if(typeof e=="string"&&e!=="")return e;if(typeof e=="number")return String(e)}function s(e){if(e.properties===void 0)return;const r=d(e.primaryIdentifier);if(r!==void 0){const t=u(e.properties[r]);if(t!==void 0)return t}const n=f[e.resourceType];if(n!==void 0)return u(e.properties[n])}function p(e,r){if(typeof e!="object"||e===null)return;const n=e.Resources;if(typeof n!="object"||n===null)return;const t=n[r];if(typeof t!="object"||t===null)return;const i=t.Properties;if(!(typeof i!="object"||i===null||Array.isArray(i)))return i}export{s as findPinnedPhysicalName,p as resourcePropertiesFromTemplate};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry verdict overlay — Layer 1 of the replacement oracle. The
|
|
3
|
+
* cloudformation-diff verdict (Layer 0) is computed from a stale bundled CDK
|
|
4
|
+
* resource spec and is retained only as the trigger; every changed property
|
|
5
|
+
* path is recomputed against the live registry catalogue
|
|
6
|
+
* (`createOnlyProperties` / `conditionalCreateOnlyProperties`):
|
|
7
|
+
*
|
|
8
|
+
* path ∈ createOnly → `will` (destructive)
|
|
9
|
+
* path ∈ conditionalCreateOnly → `conditional` (destructive worst-case,
|
|
10
|
+
* pending the Phase 3 change-set escalation)
|
|
11
|
+
* path in NEITHER → `none` — even where cloudformation-diff
|
|
12
|
+
* said WILL_REPLACE (kills the false
|
|
13
|
+
* AWS::RDS::DBInstance.Port replacement)
|
|
14
|
+
*
|
|
15
|
+
* Nested-path conservatism: cloudformation-diff reports top-level property
|
|
16
|
+
* names, so a change to a property with a createOnly SUB-pointer
|
|
17
|
+
* (`/properties/Foo/Bar`) is `conditional`, never silently `none`.
|
|
18
|
+
*
|
|
19
|
+
* Fail-closed degradation: when the oracle is unavailable for a type, the
|
|
20
|
+
* original diff verdict stands, annotated `verdictSource: "cdk-spec
|
|
21
|
+
* (unverified)"` — the oracle never fails the deploy.
|
|
22
|
+
*
|
|
23
|
+
* Every Layer-0 / Layer-1 disagreement is logged as stale-spec telemetry.
|
|
24
|
+
*/
|
|
25
|
+
import type { CfnRegistrySchemaSummary } from "../../../services/infrastructure/CfnRegistryService.js";
|
|
26
|
+
import type { CfnRegistrySummaryReader } from "../../../services/infrastructure/CfnRegistryService.js";
|
|
27
|
+
import type { ReplacementMode } from "../../../types/deployEvent.js";
|
|
28
|
+
import type { StackTemplatePair } from "./buildDeployPlan.js";
|
|
29
|
+
import type { DeployPlan, DeployPlanResourceChange } from "./types.js";
|
|
30
|
+
type RegistryPathVerdict = "will" | "conditional" | "none";
|
|
31
|
+
/**
|
|
32
|
+
* Advisory interruption notes for in-place updates that still disrupt service.
|
|
33
|
+
* Keyed `<resourceType>#<topLevelProperty>`. Display-only — never gates.
|
|
34
|
+
* Exported for the probe-parity pin: any note teaching "pin it" must name a
|
|
35
|
+
* pair `pinProbeCoverage` covers, or the note teaches a consent the
|
|
36
|
+
* destruction gate would halt as unprobeable.
|
|
37
|
+
*/
|
|
38
|
+
export declare const INTERRUPTION_NOTES: Readonly<Record<string, string>>;
|
|
39
|
+
/** Recompute one changed top-level property's verdict from the catalogue. */
|
|
40
|
+
export declare function registryVerdictForProperty(propertyName: string, summary: CfnRegistrySchemaSummary): RegistryPathVerdict;
|
|
41
|
+
/** A Layer-0 vs Layer-1 disagreement — logged as stale-spec telemetry. */
|
|
42
|
+
export interface RegistryVerdictDisagreement {
|
|
43
|
+
stack: string;
|
|
44
|
+
address: string;
|
|
45
|
+
resourceType: string;
|
|
46
|
+
cdkSpecMode: ReplacementMode;
|
|
47
|
+
registryMode: ReplacementMode;
|
|
48
|
+
/** The changed property paths whose verdicts drove the registry mode. */
|
|
49
|
+
paths: string[];
|
|
50
|
+
}
|
|
51
|
+
export interface OverlayResourceChangeResult {
|
|
52
|
+
change: DeployPlanResourceChange;
|
|
53
|
+
disagreement?: RegistryVerdictDisagreement;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Pure per-row recomputation. `summary === undefined` means the oracle was
|
|
57
|
+
* unavailable — the diff verdict stands, annotated unverified.
|
|
58
|
+
*/
|
|
59
|
+
export declare function overlayResourceChange(change: DeployPlanResourceChange, summary: CfnRegistrySchemaSummary | undefined, templateProperties: Record<string, unknown> | undefined): OverlayResourceChangeResult;
|
|
60
|
+
/** Per-action tally — shared with the change-set escalation's recount. */
|
|
61
|
+
export declare function recountActions(changes: readonly DeployPlanResourceChange[]): {
|
|
62
|
+
create: number;
|
|
63
|
+
update: number;
|
|
64
|
+
replace: number;
|
|
65
|
+
delete: number;
|
|
66
|
+
read: number;
|
|
67
|
+
"no-op": number;
|
|
68
|
+
};
|
|
69
|
+
export interface ApplyRegistryOverlayParams {
|
|
70
|
+
plan: DeployPlan;
|
|
71
|
+
registry: CfnRegistrySummaryReader;
|
|
72
|
+
/** The diffed template pairs — pinned-name detection reads Properties. */
|
|
73
|
+
stacks: readonly StackTemplatePair[];
|
|
74
|
+
abortSignal?: AbortSignal;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Apply the registry overlay to a computed plan. Never throws and never
|
|
78
|
+
* fails the plan: unavailable summaries degrade per-row to
|
|
79
|
+
* `"cdk-spec (unverified)"`. Summaries are fetched once per distinct resource
|
|
80
|
+
* type (the service memoises per run underneath).
|
|
81
|
+
*/
|
|
82
|
+
export declare function applyRegistryOverlay(params: ApplyRegistryOverlayParams): Promise<DeployPlan>;
|
|
83
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{maskSensitiveOutput as S}from"@fjall/util";import{logger as m}from"@fjall/util/logger";import{isAborted as g}from"../../../aws/organisations/types.js";import{isDataLoss as O}from"./classify.js";import{findPinnedPhysicalName as v,resourcePropertiesFromTemplate as h}from"./pinnedNames.js";import{renderPlanSummary as R}from"./renderDeployPlan.js";const P="RegistryOverlay",C={"AWS::RDS::DBInstance#Port":"port changes apply in place with a brief interruption (instance reboot + dependent-service re-point window); to keep the live port instead, pin it \u2014 set `port` in your fjall config to the current live value and the change disappears","AWS::RDS::DBCluster#Port":"port changes apply in place with a brief interruption (cluster reboot + dependent-service re-point window); to keep the live port instead, pin it \u2014 set `port` in your fjall config to the current live value and the change disappears"},N="/properties/";function k(e,t){const o=`${N}${e}`;if(t.createOnlyProperties.includes(o))return"will";if(t.conditionalCreateOnlyProperties.includes(o))return"conditional";const i=`${o}/`;return t.createOnlyProperties.some(s=>s.startsWith(i))||t.conditionalCreateOnlyProperties.some(s=>s.startsWith(i))?"conditional":"none"}function I(e){return e.includes("will")?"will":e.includes("conditional")?"conditional":"none"}function b(e,t){const o=t.map(i=>C[`${e.resourceType}#${i}`]).filter(i=>i!==void 0);if(o.length!==0)return o.join("; ")}function T(e){return(e.action==="update"||e.action==="replace")&&!e.retained}function D(e){return T(e)||e.action==="delete"}function M(e,t,o){if(e.action==="delete"&&t!==void 0){const n=v({resourceType:e.resourceType,properties:o,primaryIdentifier:t.primaryIdentifier});return{change:{...e,...n!==void 0?{pinnedPhysicalName:n}:{}}}}if(!T(e))return{change:e};if(t===void 0||e.propertyChanges.length===0)return{change:{...e,verdictSource:"cdk-spec (unverified)"}};const i=e.propertyChanges.map(n=>({path:n.path,verdict:k(n.path,t)})),p=I(i.map(n=>n.verdict)),s=p==="none"?"update":"replace",c=p,y=p!=="none",d=O({action:s,retained:e.retained,resourceType:e.resourceType}),u=e.propertyChanges.map((n,w)=>({...n,forcesReplacement:i[w]?.verdict!=="none"})),r=i.filter(n=>n.verdict==="none").map(n=>n.path),a=p==="none"?b(e,r):void 0,l=v({resourceType:e.resourceType,properties:o,primaryIdentifier:t.primaryIdentifier}),f={...e,action:s,replacementMode:c,destructive:y,dataLoss:d,propertyChanges:u,verdictSource:"registry",...a!==void 0?{interruptionNote:a}:{},...l!==void 0?{pinnedPhysicalName:l}:{}};return e.replacementMode===c?{change:f}:{change:f,disagreement:{stack:e.stack,address:e.address,resourceType:e.resourceType,cdkSpecMode:e.replacementMode,registryMode:c,paths:i.map(n=>n.path)}}}function x(e){const t={create:0,update:0,replace:0,delete:0,read:0,"no-op":0};for(const o of e)t[o.action]+=1;return t}async function B(e){const{plan:t,registry:o,stacks:i,abortSignal:p}=e,s=new Set(t.changes.filter(D).map(r=>r.resourceType));if(s.size===0)return t;const c=new Map;for(const r of s){if(g(p))break;const a=await o.getSchemaSummary(r,p);a.success?c.set(r,a.data):m.warn(P,"Registry oracle unavailable for type",{typeName:r,error:S(a.error.message)})}const y=new Map(i.map(r=>[r.stackName,r])),d=t.changes.map(r=>{const a=y.get(r.stack),l=r.action==="delete"?h(a?.oldTemplate,r.address):h(a?.newTemplate,r.address),{change:f,disagreement:n}=M(r,c.get(r.resourceType),l);return n!==void 0&&m.warn(P,"Registry verdict disagrees with bundled CDK spec",{...n,paths:n.paths.join(",")}),f}),u=x(d);return u["no-op"]=t.counts["no-op"],{...t,changes:d,counts:u,hasDestructiveChanges:d.some(r=>r.destructive),summary:R({counts:u,changes:d})}}export{C as INTERRUPTION_NOTES,B as applyRegistryOverlay,M as overlayResourceChange,x as recountActions,k as registryVerdictForProperty};
|
|
@@ -14,5 +14,8 @@ export declare function renderPlanSummary(plan: Pick<DeployPlan, "counts" | "cha
|
|
|
14
14
|
/** One line per resource change, grouped by action, e.g.
|
|
15
15
|
* "± replace AWS::ECS::Service WebAppCompute/Service (data loss)". */
|
|
16
16
|
export declare function renderPlanLines(plan: Pick<DeployPlan, "changes">): string[];
|
|
17
|
-
/** Map a computed plan's changes down to the wire `plan.available` PlanChange[].
|
|
17
|
+
/** Map a computed plan's changes down to the wire `plan.available` PlanChange[].
|
|
18
|
+
* `verdictSource` is deliberately NOT threaded here — the wire shape is a
|
|
19
|
+
* cross-package contract; it lands with the Phase 2 destruction-ticket wire
|
|
20
|
+
* contract. */
|
|
18
21
|
export declare function toWirePlanChanges(plan: Pick<DeployPlan, "changes">): PlanChange[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
const
|
|
1
|
+
const i={create:0,update:1,replace:2,delete:3,read:4,"no-op":5},c={create:"+",update:"~",replace:"\xB1",delete:"-",read:">","no-op":" "};function p(a){const{counts:e}=a,t=[];if(e.create>0&&t.push(`${e.create} to create`),e.update>0&&t.push(`${e.update} to update`),e.replace>0&&t.push(`${e.replace} to replace`),e.delete>0&&t.push(`${e.delete} to destroy`),e.read>0&&t.push(`${e.read} to read`),t.length===0)return"No changes";const s=a.changes.filter(n=>n.destructive).length,o=a.changes.filter(n=>n.dataLoss).length,r=[];s>0&&r.push(`${s} destructive`),o>0&&r.push(`${o} with data loss`);const d=t.join(", ");return r.length>0?`${d} (${r.join(", ")})`:d}function u(a){return a.changes.slice().sort((e,t)=>i[e.action]-i[t.action]||`${e.stack}/${e.address}`.localeCompare(`${t.stack}/${t.address}`)).map(e=>{const t=[];e.replacementMode==="may"&&t.push("may replace"),e.replacementMode==="conditional"&&t.push("conditional replace"),e.dataLoss&&t.push("data loss"),e.retained&&t.push("retained"),(e.verdictSource==="cdk-spec (unverified)"||e.verdictSource==="registry (change-set unverified)")&&t.push("unverified");const s=t.length>0?` (${t.join(", ")})`:"";return`${c[e.action]} ${e.action.padEnd(7)} ${e.resourceType} ${e.stack}/${e.address}${s}`})}function l(a){return a.changes.map(e=>({address:e.address,resourceType:e.resourceType,action:e.action,stack:e.stack,replacementMode:e.replacementMode,dataLoss:e.dataLoss,...e.propertyChanges.length>0&&{propertyChanges:e.propertyChanges}}))}export{u as renderPlanLines,p as renderPlanSummary,l as toWirePlanChanges};
|
|
@@ -8,6 +8,20 @@
|
|
|
8
8
|
* maps a DeployPlan down to the wire shape.
|
|
9
9
|
*/
|
|
10
10
|
import type { ChangeCounts, PlanAction, PlanPropertyChange, ReplacementMode } from "../../../types/deployEvent.js";
|
|
11
|
+
/**
|
|
12
|
+
* Which oracle layer produced a row's replacement verdict. Precedence:
|
|
13
|
+
* change-set > registry > cdk-spec. `"change-set"` rows carry the
|
|
14
|
+
* authoritative verdict from the Phase 3 review-only change-set probe
|
|
15
|
+
* (`applyChangeSetEscalation`). `"registry (change-set unverified)"` marks a
|
|
16
|
+
* registry-conditional row the probe was escalated for but could not verify
|
|
17
|
+
* (probe denied, timed out, or aborted) — the conservative registry verdict
|
|
18
|
+
* stands. `"cdk-spec (unverified)"` marks rows the registry oracle could not
|
|
19
|
+
* verify (oracle unavailable, or a replacement verdict with no property paths
|
|
20
|
+
* to recompute from) — the stale bundled CDK spec is never silently treated
|
|
21
|
+
* as authoritative.
|
|
22
|
+
*/
|
|
23
|
+
export declare const REPLACEMENT_VERDICT_SOURCES: readonly ["registry", "registry (change-set unverified)", "change-set", "cdk-spec (unverified)"];
|
|
24
|
+
export type ReplacementVerdictSource = (typeof REPLACEMENT_VERDICT_SOURCES)[number];
|
|
11
25
|
/** A single resource change in a computed deploy plan. */
|
|
12
26
|
export interface DeployPlanResourceChange {
|
|
13
27
|
/** The stack the resource belongs to. */
|
|
@@ -28,6 +42,23 @@ export interface DeployPlanResourceChange {
|
|
|
28
42
|
retained: boolean;
|
|
29
43
|
/** Per-property change detail — paths only, never values (no secret leak). */
|
|
30
44
|
propertyChanges: PlanPropertyChange[];
|
|
45
|
+
/**
|
|
46
|
+
* Oracle layer behind the replacement verdict. Absent when no verdict
|
|
47
|
+
* question arises (creates, imports, rows the overlay never evaluated).
|
|
48
|
+
*/
|
|
49
|
+
verdictSource?: ReplacementVerdictSource;
|
|
50
|
+
/**
|
|
51
|
+
* Advisory service-interruption note for an in-place update (e.g. an RDS
|
|
52
|
+
* port change reboots the instance). Display-only — never gates.
|
|
53
|
+
*/
|
|
54
|
+
interruptionNote?: string;
|
|
55
|
+
/**
|
|
56
|
+
* Literal physical name pinned in the template (via the registry
|
|
57
|
+
* `primaryIdentifier` or the `@fjall/util` fallback map). A pinned name on
|
|
58
|
+
* a replacement row means CloudFormation will REFUSE the update
|
|
59
|
+
* ("cannot update a stack when a custom-named resource requires replacing").
|
|
60
|
+
*/
|
|
61
|
+
pinnedPhysicalName?: string;
|
|
31
62
|
}
|
|
32
63
|
/** A fully computed deploy plan, ready to render and to gate on. */
|
|
33
64
|
export interface DeployPlan {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const e=["registry","registry (change-set unverified)","change-set","cdk-spec (unverified)"];export{e as REPLACEMENT_VERDICT_SOURCES};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{openNextBuilder as
|
|
1
|
+
import{openNextBuilder as d}from"./openNextBuilder.js";import{staticSiteBuilder as n}from"./staticSiteBuilder.js";import{dockerBuilder as c}from"./dockerBuilder.js";class o{builders;constructor(e){this.builders=[...e].sort((r,t)=>r.priority-t.priority)}resolve(e,r){if(r){const t=this.builders.find(u=>u.name===r);if(!t)return null;const i=t.detect(e);return{builder:t,detection:{...i,detected:!0}}}for(const t of this.builders){const i=t.detect(e);if(i.detected)return{builder:t,detection:i}}return null}static createDefault(){return new o([d,n,c])}static create(e){return new o(e)}}export{o as FrameworkRegistry};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { dockerBuilder } from "./dockerBuilder.js";
|
|
2
2
|
export { openNextBuilder } from "./openNextBuilder.js";
|
|
3
|
+
export { staticSiteBuilder } from "./staticSiteBuilder.js";
|
|
3
4
|
export { FrameworkRegistry } from "./frameworkRegistry.js";
|
|
4
5
|
export type { ResolvedBuilder } from "./frameworkRegistry.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{dockerBuilder as o}from"./dockerBuilder.js";import{openNextBuilder as
|
|
1
|
+
import{dockerBuilder as o}from"./dockerBuilder.js";import{openNextBuilder as i}from"./openNextBuilder.js";import{staticSiteBuilder as p}from"./staticSiteBuilder.js";import{FrameworkRegistry as d}from"./frameworkRegistry.js";export{d as FrameworkRegistry,o as dockerBuilder,i as openNextBuilder,p as staticSiteBuilder};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{basename as
|
|
1
|
+
import{basename as d}from"path";import{success as i,parseInfrastructure as l,isOpenNextPatternType as c}from"@fjall/generator";import{logger as f}from"@fjall/util/logger";import{OPENNEXT_DEPLOY_ORDER as m,OPENNEXT_DESTROY_ORDER as y}from"../../types/operations.js";import{readInfrastructureContent as x}from"../../types/detection/patternDetection.js";import{runOpenNextBuild as N,BUILD_TIMEOUT_MS as O,IMPORT_MAP_TIMEOUT_MS as B}from"../application/openNextBuild.js";function p(r){return{hasNetwork:!0,hasCompute:!0,hasDatabase:r,hasStorage:!0,hasMessaging:!0,hasCdn:!0}}const M={name:"opennext",priority:10,detect(r){const t={detected:!1,pattern:null,reason:"No OpenNext pattern found",resources:p(!1),hasDatabase:!1,hasDockerfile:!1},e=x(r.appPath);if(e===null||e==="")return{...t,reason:"No infrastructure.ts found"};const a=l(e).patternResources.find(o=>c(o.type));if(a===void 0)return{...t,reason:'No PatternFactory with type: "payload" or "nextjs" in infrastructure.ts'};if(a.type==="payload")return{detected:!0,pattern:"payload",reason:'PatternFactory with type: "payload" detected',resources:p(!0),hasDatabase:!0,hasDockerfile:!1};if(a.type==="nextjs"){const o=a.config.database!==void 0;return{detected:!0,pattern:"nextjs",reason:'PatternFactory with type: "nextjs" detected',resources:p(o),hasDatabase:o,hasDockerfile:!1}}return{...t,reason:"PatternFactory found but no recognised pattern type"}},plan(r,t){const e=t.pattern==="payload"?[{name:"payload-import-map",binary:"npx",args:["payload","generate:importmap"],timeoutMs:B}]:[];return{builderName:"opennext",deployOrder:m,destroyOrder:y,parallelDestroy:!0,preBuildCommands:e,buildCommand:{name:"open-next-build",binary:"open-next",args:["build"],timeoutMs:O},requiresDockerBuild:!1,resources:t.resources}},async build(r,t,e,n){if(n?.skipBuild||n?.infraOnly)return f.debug("openNextBuilder","Build skipped",{skipBuild:n.skipBuild,infraOnly:n.infraOnly}),i(void 0);const a={kind:"application",appName:d(r)||"app",path:r},u=t.preBuildCommands.some(s=>s.name==="payload-import-map")?"payload":"nextjs";return await N(a,u,{onOpenNextBuildStart:()=>e.onBuildStart?.("opennext"),onOpenNextProgress:s=>e.onBuildProgress?.("opennext",s),onOpenNextBuildComplete:()=>e.onBuildComplete?.("opennext"),onOpenNextBuildError:s=>e.onBuildError?.("opennext",s)},void 0,n?.abortSignal)}};export{M as openNextBuilder};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StaticSiteBuilder — FrameworkBuilder implementation for static-site apps.
|
|
3
|
+
*
|
|
4
|
+
* Detects a `type: "staticsite"` PatternFactory call in infrastructure.ts,
|
|
5
|
+
* produces a single-stack (Cdn) build plan, and runs the app's own build
|
|
6
|
+
* command to produce the static artefacts that CDK's BucketDeployment then
|
|
7
|
+
* uploads to S3.
|
|
8
|
+
*
|
|
9
|
+
* Priority 50 — strictly between OpenNext (10) and Docker (100). This is
|
|
10
|
+
* load-bearing: without it the Docker catch-all (priority 100) would claim
|
|
11
|
+
* the static site and emit a container deployment plan.
|
|
12
|
+
*
|
|
13
|
+
* Unlike OpenNext, the build command and its working directory are not
|
|
14
|
+
* hardcoded — they are the app's own, read out of infrastructure.ts with the
|
|
15
|
+
* generator's AST parser (`@fjall/generator` is already a dependency, and its
|
|
16
|
+
* barrel already pulls the parser in, so this costs nothing over a regex).
|
|
17
|
+
*/
|
|
18
|
+
import type { FrameworkBuilder } from "../../types/frameworkBuilder.js";
|
|
19
|
+
/** Default static-site build timeout: 10 minutes (mirrors the OpenNext budget) */
|
|
20
|
+
export declare const STATIC_BUILD_TIMEOUT_MS = 600000;
|
|
21
|
+
/**
|
|
22
|
+
* Static-site FrameworkBuilder.
|
|
23
|
+
*
|
|
24
|
+
* Priority 50 — checked after OpenNext (more specific) and before Docker
|
|
25
|
+
* (the fallback catch-all).
|
|
26
|
+
*/
|
|
27
|
+
export declare const staticSiteBuilder: FrameworkBuilder;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{existsSync as y}from"fs";import{resolve as f}from"path";import{success as m,failure as u,parseInfrastructure as p}from"@fjall/generator";import{filterDangerousEnvVars as b,maskSensitiveOutput as d,parseShellArgs as B}from"@fjall/util";import{logger as D}from"@fjall/util/logger";import{getApplicationDeployOrder as E,getApplicationDestroyOrder as h}from"../../types/operations.js";import{readInfrastructureContent as g}from"../../types/detection/patternDetection.js";import{spawnWithTimeout as w}from"../spawnHelpers.js";const O=6e5,S={hasNetwork:!1,hasCompute:!1,hasDatabase:!1,hasStorage:!1,hasMessaging:!1,hasCdn:!0};function C(o){return p(o).patternResources.some(r=>r.type==="staticsite")}function P(o){const r=g(o);if(r===null||r==="")return;const t=p(r).patternResources.find(n=>n.type==="staticsite");if(t===void 0||t.type!=="staticsite")return;const{source:i,build:s}=t.config,a=s?.command,c=s?.outputDir;if(!(i===void 0||i===""||a===void 0||a===""||c===void 0||c===""))return{source:i,command:a,outputDir:c}}const R={name:"staticsite",priority:50,detect(o){const r={detected:!1,pattern:null,reason:"No static-site pattern found",resources:S,hasDatabase:!1,hasDockerfile:!1},t=g(o.appPath);return t===null||t===""?{...r,reason:"No infrastructure.ts found"}:C(t)?{detected:!0,pattern:"staticsite",reason:'PatternFactory with type: "staticsite" detected',resources:S,hasDatabase:!1,hasDockerfile:!1}:{...r,reason:'No PatternFactory with type: "staticsite"'}},plan(o,r){const t=P(o.appPath);let i=null;if(t!==void 0){const[s,...a]=B(t.command);s!==void 0&&(i={name:"static-site-build",binary:s,args:a,timeoutMs:O,cwd:t.source})}return{builderName:"staticsite",deployOrder:E({resources:r.resources}),destroyOrder:h({resources:r.resources}),parallelDestroy:!1,preBuildCommands:[],buildCommand:i,requiresDockerBuild:!1,artefactDir:t?.outputDir,resources:r.resources}},async build(o,r,t,i){if(i?.skipBuild||i?.infraOnly)return D.debug("staticSiteBuilder","Build skipped",{skipBuild:i.skipBuild,infraOnly:i.infraOnly}),m(void 0);const{buildCommand:s}=r;if(s===null){const e='Static-site build command could not be read from infrastructure.ts. The pattern needs an inline build block \u2014 build: { command: "npm run build", outputDir: "out" } \u2014 with literal strings, not variables or expressions.';return t.onBuildError?.("staticsite",e),u(new Error(e))}const a=f(o,s.cwd??"."),c=b(globalThis.process.env);t.onBuildStart?.("staticsite"),t.onBuildProgress?.("staticsite",d("Starting static-site build..."));const n=await w({command:s.binary,args:[...s.args],cwd:a,env:{...c,FORCE_COLOR:"0"},timeout:s.timeoutMs,...i?.abortSignal!==void 0&&{abortSignal:i.abortSignal},onStdoutData:e=>t.onBuildProgress?.("staticsite",d(e.trim())),onStderrData:e=>t.onBuildProgress?.("staticsite",d(e.trim()))});if(n.type==="timeout"){const e=`Static-site build timed out after ${s.timeoutMs/1e3} seconds`;return t.onBuildError?.("staticsite",e),u(new Error(e))}if(n.type==="aborted"){const e="Static-site build cancelled";return t.onBuildError?.("staticsite",e),u(new Error(e))}if(n.type==="enoent"){const e=`Static-site build command not found: ${s.binary}. Ensure it is installed and in PATH.`;return t.onBuildError?.("staticsite",e),u(new Error(e))}if(n.type==="spawn_error"){const e=d(`Failed to spawn static-site build: ${n.error.message}`);return t.onBuildError?.("staticsite",e),u(new Error(e))}if(n.code!==0){const e=d(n.stderr||n.stdout);return t.onBuildError?.("staticsite",`Static-site build failed (exit code ${n.code})`),u(new Error(`Static-site build failed (exit code ${n.code}):
|
|
2
|
+
${e}`))}if(r.artefactDir!==void 0){const e=f(a,r.artefactDir);if(!y(e)){const l=`Static-site build completed but output directory '${r.artefactDir}' was not found`;return t.onBuildError?.("staticsite",l),u(new Error(l))}}return t.onBuildComplete?.("staticsite"),t.onBuildProgress?.("staticsite",d("Static-site build completed successfully")),m(void 0)}};export{O as STATIC_BUILD_TIMEOUT_MS,R as staticSiteBuilder};
|
|
@@ -3,7 +3,7 @@ import type { TrailLifecycleState } from "@fjall/util/config";
|
|
|
3
3
|
import type { ResourceEvent } from "@fjall/util/aws";
|
|
4
4
|
import type { OrgConfig } from "../types/config/orgConfig.js";
|
|
5
5
|
import type { DeployIdentity } from "../types/credentials.js";
|
|
6
|
-
import type { AwsProviderCredentials } from "../aws/AwsProvider.js";
|
|
6
|
+
import type { AwsProvider, AwsProviderCredentials } from "../aws/AwsProvider.js";
|
|
7
7
|
import { SimpleAwsProvider } from "../aws/SimpleAwsProvider.js";
|
|
8
8
|
import type { DeploymentContext } from "../types/deployment/DeploymentTypes.js";
|
|
9
9
|
import type { DeployCallbacks } from "../types/callbacks.js";
|
|
@@ -76,6 +76,42 @@ export declare function buildParamsContext(params: {
|
|
|
76
76
|
* provider the Account stack legitimately owns.
|
|
77
77
|
*/
|
|
78
78
|
export declare function resolveAccountBootstrapSkipOidc(deployType: "organisation" | "platform" | "account", orgConfig: OrgConfig | undefined, explicitSkipOidc: boolean | undefined, targetIsOrganisationTier?: boolean): boolean | undefined;
|
|
79
|
+
/** Synth-time context flags the OIDC gate mirror reads — see {@link synthCarriedOidcConnector}. */
|
|
80
|
+
export interface SynthOidcContextFlags {
|
|
81
|
+
fjallOrgId?: string;
|
|
82
|
+
fjallOidcConfigured?: boolean;
|
|
83
|
+
fjallAccountGlobalsConfigured?: boolean;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Whether the synthesised Account/Platform stack carried the OidcConnector —
|
|
87
|
+
* the deploy-core mirror of the construct gate in
|
|
88
|
+
* `components/infrastructure/lib/patterns/aws/account.ts` (`receivesDeployRole()
|
|
89
|
+
* && fjallOrgId && !oidcAlreadyConfigured && !accountGlobalsConfigured`).
|
|
90
|
+
* `receivesDeployRole` is structural here: only platform/account deploys reach
|
|
91
|
+
* the post-deploy OIDC reconcile, and both patterns receive the deploy role
|
|
92
|
+
* (the Organisation pattern, which does not, only synthesises under
|
|
93
|
+
* `deployType: "organisation"`). An empty-string `fjallOrgId` reads as absent,
|
|
94
|
+
* matching both coupled sites: the construct gate tests truthiness and
|
|
95
|
+
* CdkArgumentBuilder drops the `-c fjallOrgId` argument entirely for `""`.
|
|
96
|
+
*
|
|
97
|
+
* Gates {@link reconcileOidcProviderIfCarried}: the reconcile enforces
|
|
98
|
+
* provider presence only where the current template claims ownership. A stack
|
|
99
|
+
* that deliberately skipped the connector — solo/standalone-management
|
|
100
|
+
* (Quick-Create owns its provider; restore is RECONNECT, never redeploy),
|
|
101
|
+
* non-home region, or no org identity — must never have a provider created
|
|
102
|
+
* for it on the deploy path.
|
|
103
|
+
*/
|
|
104
|
+
export declare function synthCarriedOidcConnector(context: SynthOidcContextFlags): boolean;
|
|
105
|
+
/**
|
|
106
|
+
* Post-CFN OIDC provider reconcile, gated on {@link synthCarriedOidcConnector}.
|
|
107
|
+
* A pre-2.20.0 → current template migration deletes the account-global OIDC
|
|
108
|
+
* provider during CloudFormation's cleanup phase (after the replacement
|
|
109
|
+
* adopt-or-create resource has already run), so callers MUST invoke this only
|
|
110
|
+
* after their deploy — or unchanged-skip probe — returns. Composing the gate
|
|
111
|
+
* with the reconcile here keeps any future deploy path from invoking the
|
|
112
|
+
* reconcile ungated.
|
|
113
|
+
*/
|
|
114
|
+
export declare function reconcileOidcProviderIfCarried(context: SynthOidcContextFlags, provider: Pick<AwsProvider, "getClient">, callbacks: DeployCallbacks, abortSignal?: AbortSignal): Promise<void>;
|
|
79
115
|
/** Forward onOutput callback — reduces lambda repetition across orchestration files. */
|
|
80
116
|
export declare function forwardOutput(callbacks: DeployCallbacks): (chunk: string) => void;
|
|
81
117
|
/** Forward onResourceProgress callback — reduces lambda repetition across orchestration files. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{success as f,failure as
|
|
1
|
+
import{success as f,failure as s}from"@fjall/generator";import{getErrorMessage as m,maskSensitiveOutput as a,sleep as y}from"@fjall/util";import{logger as O}from"@fjall/util/logger";import{IAMClient as R}from"@aws-sdk/client-iam";import{hasOrganisationTierAccount as S}from"../aws/organisations/accountGlobals.js";import{reconcileOidcProviderAfterDeploy as w}from"../aws/organisations/oidcProvider.js";import{SimpleAwsProvider as b}from"../aws/SimpleAwsProvider.js";const h={account:"active",draining:"draining",org:"removed"};function T(e,r){return e!==void 0&&e!==""&&r!==void 0&&r!==""&&e!==r}function F(e){const r=T(e.region,e.primaryRegion);return{...e.orgConfig!==void 0?{orgConfig:JSON.stringify(e.orgConfig)}:{},...e.identity!==void 0?{fjallOrgId:e.identity.fjallOrgId}:{},...e.skipOidc?{fjallOidcConfigured:!0}:{},...r?{fjallAccountGlobalsConfigured:!0}:{},...e.trailLifecycle!==void 0?{fjallAccountTrailState:h[e.trailLifecycle]}:{}}}function Y(e,r,t,n){if(t!==void 0)return t;if(e==="account")return S(r?.providerAccounts)?n===!0:!0}function _(e){return e.fjallOrgId!==void 0&&e.fjallOrgId!==""&&e.fjallOidcConfigured!==!0&&e.fjallAccountGlobalsConfigured!==!0}async function G(e,r,t,n){_(e)&&await w(r.getClient(R),t,n)}function $(e){return r=>e.onOutput?.(r)}function N(e){return r=>e.onResourceProgress?.(r)}function U(e){if(!e.success||e.data.length===0)return;const r={};for(const t of e.data)t.OutputKey&&t.OutputValue!==void 0&&(r[t.OutputKey]=t.OutputValue);return Object.keys(r).length>0?r:void 0}const g="OrganizationAccountAccessRole",l=5,v=5e3,K=3e4;function x(e){return`arn:aws:iam::${e}:role/${g}`}function M(e,r){return r===void 0?y(e):r.aborted?Promise.resolve():new Promise(t=>{const n=()=>{t()};r.addEventListener("abort",n,{once:!0}),y(e).then(()=>{r.removeEventListener("abort",n),t()})})}async function V(e,r,t,n,o){if(!e.assumeRole)return s(new Error("AwsProvider does not support assumeRole"));const u=x(r),E=e.assumeRole.bind(e);let i;for(let c=0;c<=l;c++)try{i=await E(u,n);break}catch(d){const p=d instanceof Error?d.name:void 0;if(p==="AccessDenied"||p==="AccessDeniedException")return s(new Error(`Access denied assuming ${g} in account ${r}. The role may not exist or may not trust the management account.`));if(c<l){const A=Math.min(v*2**c,K);if(O.debug("assumeCascadeRole",`Attempt ${c+1} failed for account ${r}, retrying in ${Math.round(A/1e3)}s`,{error:a(m(d))}),await M(A,o),o?.aborted)return s(new Error(`Aborted while retrying assume-role for account ${r}`));continue}return s(new Error(`Failed to assume role in account ${r} after ${l+1} attempts: ${a(m(d))}`))}if(!i)return s(new Error(`Failed to assume role in account ${r}`));const C=new b({accessKeyId:i.accessKeyId,secretAccessKey:i.secretAccessKey,sessionToken:i.sessionToken,region:t,accountId:r});return f({provider:C,credentials:{accessKeyId:i.accessKeyId,secretAccessKey:i.secretAccessKey,sessionToken:i.sessionToken}})}async function z(e,r,t,n){const o=await e.cdkService.runCdkSynth(r,u=>t.onCdkOutput?.(u,"synth"));if(!o.success){const u=new Error(a(`${n}: ${o.error}`));return t.onError?.(u),s(u)}return f(void 0)}async function J(e,r,t){t.onCDKBootstrap?.("bootstrapping");const n=await e.cdkService.runCdkBootstrap(r,$(t));if(!n.success){t.onCDKBootstrap?.("failed");const o=new Error(a(`Bootstrap failed: ${n.error}`));return t.onError?.(o),s(o)}return t.onCDKBootstrap?.("complete"),f(void 0)}export{V as assumeCascadeRole,J as bootstrapOrFail,x as buildCascadeRoleArn,F as buildParamsContext,U as collectStackOutputs,$ as forwardOutput,N as forwardResourceProgress,G as reconcileOidcProviderIfCarried,Y as resolveAccountBootstrapSkipOidc,_ as synthCarriedOidcConnector,z as synthOrFail,T as targetsNonPrimaryRegion};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Class 1 drift classification over a stack's last CloudFormation operation
|
|
3
|
+
* (drift-remediation plan § Phase 5 item 1). Fetches the failure events of
|
|
4
|
+
* the most recent operation via `DescribeStackEvents` (newest-first), maps
|
|
5
|
+
* them onto the shared NotFound anchors (`@fjall/util/aws` `driftSuspects`,
|
|
6
|
+
* derived from the Phase 0 BC fixture `__tests__/fixtures/bc-stack-events.json`)
|
|
7
|
+
* and returns out-of-band-deletion SUSPECTS — never verdicts; `driftProbe.ts`
|
|
8
|
+
* confirms.
|
|
9
|
+
*
|
|
10
|
+
* Suspect `statusReason`s are masked at construction (a CloudFormation status
|
|
11
|
+
* reason can embed anything a failed handler echoed), so journal writes and
|
|
12
|
+
* CLI sinks may surface them directly.
|
|
13
|
+
*/
|
|
14
|
+
import { type Result } from "@fjall/generator";
|
|
15
|
+
import { type ResourceEvent } from "@fjall/util/aws";
|
|
16
|
+
import type { AwsProvider } from "../../aws/AwsProvider.js";
|
|
17
|
+
import type { DriftSuspect } from "../../types/drift.js";
|
|
18
|
+
/**
|
|
19
|
+
* Fetch the events of the stack's most recent operation, newest-first. Pages
|
|
20
|
+
* `DescribeStackEvents` until the operation's opening event (inclusive — the
|
|
21
|
+
* shared `isOperationOpeningEvent` boundary, which a CancelUpdateStack
|
|
22
|
+
* rollback's "User Initiated" stamp does NOT satisfy) or the page budget is
|
|
23
|
+
* exhausted.
|
|
24
|
+
*/
|
|
25
|
+
export declare function fetchLastOperationEvents(aws: AwsProvider, stackName: string, abortSignal?: AbortSignal): Promise<Result<ResourceEvent[], Error>>;
|
|
26
|
+
/**
|
|
27
|
+
* Classify a stack's last-operation events into drift suspects. Pure —
|
|
28
|
+
* callers fetch events via {@link fetchLastOperationEvents} or hand over a
|
|
29
|
+
* monitor's history.
|
|
30
|
+
*/
|
|
31
|
+
export declare function classifyDriftFailure(stackName: string, events: readonly ResourceEvent[]): DriftSuspect[];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{CloudFormationClient as p,DescribeStackEventsCommand as m}from"@aws-sdk/client-cloudformation";import{success as a,failure as d}from"@fjall/generator";import{getErrorMessage as g,maskSensitiveOutput as l}from"@fjall/util";import{classifyDriftSuspectEvents as y,isOperationOpeningEvent as R}from"@fjall/util/aws";import{logger as E}from"@fjall/util/logger";import{composeSdkAbortSignal as S,isAborted as v}from"../../aws/organisations/types.js";const I="DriftClassify",h=5;function T(e){return e.includes("FAILED")}function D(e){return e.LogicalResourceId===void 0||e.ResourceType===void 0||e.ResourceStatus===void 0?null:{logicalId:e.LogicalResourceId,resourceType:e.ResourceType,status:e.ResourceStatus,timestamp:e.Timestamp??new Date(0),...e.PhysicalResourceId!==void 0&&e.PhysicalResourceId!==""?{physicalId:e.PhysicalResourceId}:{},...e.ResourceStatusReason!==void 0?{statusReason:e.ResourceStatusReason}:{}}}async function L(e,n,r){const t=e.getClient(p),o=[];let s,u=0;try{do{if(v(r))return d(new Error(`Drift event scan aborted for ${n}`));const i=await t.send(new m({StackName:n,...s!==void 0?{NextToken:s}:{}}),{abortSignal:S(r)});u+=1;for(const f of i.StackEvents??[]){const c=D(f);if(c!==null&&(o.push(c),R(c,n)))return a(o)}if(s=i.NextToken,u>=h&&s!==void 0)return E.debug(I,"Event scan truncated at page budget",{stackName:n,pages:u,eventsCollected:o.length}),a(o)}while(s!==void 0);return a(o)}catch(i){return d(new Error(`Failed to read stack events for ${n}: ${l(g(i))}`))}}function O(e,n){const r=n.filter(t=>T(t.status));return y(r).map(t=>({stackName:e,logicalId:t.logicalId,resourceType:t.resourceType,...t.physicalId!==void 0?{physicalId:t.physicalId}:{},failedStatus:t.failedStatus,statusReason:l(t.statusReason)}))}export{O as classifyDriftFailure,L as fetchLastOperationEvents};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The drift-detection pipeline for one stack: last-operation event scan →
|
|
3
|
+
* suspect classification → journal merge → probe → journal upkeep. Shared by
|
|
4
|
+
* `fjall drift detect` and the [G2] deploy pre-flight so both surfaces report
|
|
5
|
+
* identical findings for identical state.
|
|
6
|
+
*
|
|
7
|
+
* Journal upkeep: confirmed or unconfirmed findings (the wedge persists or
|
|
8
|
+
* could not be ruled out) re-record the journal so the next deploy re-probes;
|
|
9
|
+
* all-present or stack-missing findings clear it.
|
|
10
|
+
*/
|
|
11
|
+
import { type Result } from "@fjall/generator";
|
|
12
|
+
import type { AwsProvider } from "../../aws/AwsProvider.js";
|
|
13
|
+
import type { DriftFinding, DriftSuspect } from "../../types/drift.js";
|
|
14
|
+
import type { DriftProbeRunner } from "./driftProbe.js";
|
|
15
|
+
import { type DriftJournalOptions } from "./driftSuspectJournal.js";
|
|
16
|
+
export interface DetectStackDriftDeps {
|
|
17
|
+
aws: AwsProvider;
|
|
18
|
+
probe: DriftProbeRunner;
|
|
19
|
+
journal?: DriftJournalOptions;
|
|
20
|
+
/** Injectable clock for journal timestamps. */
|
|
21
|
+
now?: () => Date;
|
|
22
|
+
}
|
|
23
|
+
export interface DetectStackDriftParams {
|
|
24
|
+
stackName: string;
|
|
25
|
+
abortSignal?: AbortSignal;
|
|
26
|
+
}
|
|
27
|
+
export interface StackDriftReport {
|
|
28
|
+
stackName: string;
|
|
29
|
+
suspects: DriftSuspect[];
|
|
30
|
+
findings: DriftFinding[];
|
|
31
|
+
/** How many suspects came from the live event scan vs the journal. */
|
|
32
|
+
suspectSources: {
|
|
33
|
+
events: number;
|
|
34
|
+
journal: number;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export declare function detectStackDrift(deps: DetectStackDriftDeps, params: DetectStackDriftParams): Promise<Result<StackDriftReport, Error>>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{STACK_NOT_FOUND_PATTERN as b}from"@fjall/util/aws";import{success as g,failure as S}from"@fjall/generator";import{classifyDriftFailure as D,fetchLastOperationEvents as h}from"./classifyDriftFailure.js";import{clearDriftSuspects as j,readDriftSuspects as _,recordDriftSuspects as I}from"./driftSuspectJournal.js";async function E(e,m){const{stackName:t,abortSignal:o}=m,n=e.aws.getAccountId(),a=e.aws.getRegion(),r=await h(e.aws,t,o);let c=[];if(r.success)c=D(t,r.data);else if(!r.error.message.includes(b))return S(r.error);const l=await _(n,a,t,e.journal),v=new Set(c.map(s=>s.logicalId)),f=(l?.suspects??[]).filter(s=>!v.has(s.logicalId)),i=[...c,...f];if(i.length===0)return g({stackName:t,suspects:[],findings:[],suspectSources:{events:0,journal:0}});const u=await e.probe.probeSuspects({stackName:t,suspects:i,...o!==void 0?{abortSignal:o}:{}});if(!u.success)return S(u.error);const d=u.data,p=d.filter(s=>s.verdict==="confirmed-deleted"||s.verdict==="unconfirmed").map(({verdict:s,verdictDetail:R,probeMethod:A,verbs:N,...w})=>w);return p.length>0?await I({accountId:n,region:a,stackName:t,recordedAt:(e.now?.()??new Date).toISOString(),suspects:p},e.journal):l!==null&&await j(n,a,t,e.journal),g({stackName:t,suspects:i,findings:d,suspectSources:{events:c.length,journal:f.length}})}export{E as detectStackDrift};
|