@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
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import{join as
|
|
2
|
-
`);t.onLog?.(
|
|
3
|
-
${
|
|
4
|
-
`),
|
|
5
|
-
${o}`);return t.onError?.(
|
|
1
|
+
import{join as _}from"node:path";import{OrganizationsClient as Q}from"@aws-sdk/client-organizations";import{success as V,failure as v}from"@fjall/generator";import{ORGANISATION_TYPES as A,getOrganisationStackName as R}from"../../types/operations.js";import{synthOrFail as X,bootstrapOrFail as Z,forwardOutput as tt,forwardResourceProgress as et}from"../contextHelpers.js";import{partitionAccounts as ot,probeCascadeRoles as at,buildRegionList as rt,buildAccountRegionPairs as nt}from"../organisation/cascadeHelpers.js";import{assertLeaseBeforeStack as ct}from"../application/leaseGate.js";import{accountTier as it,maskSensitiveOutput as g}from"@fjall/util";import{findDevelopmentOuId as st}from"../../aws/organisations/organisationalUnits.js";import{buildOrgContext as dt,resolveOrgDetails as ut}from"./orgContext.js";import{INFRA_STEPS as B,emitOrgStackTargets as lt,maskAndFail as O,readStackOutputsBestEffort as Y}from"./infraSteps.js";import{resolveCascadeAccounts as gt}from"./resolveCascadeAccounts.js";import{executeCascade as ft}from"./cascadeExecution.js";import{reconcileOrgTrailOutputs as pt}from"./trailReconciliation.js";import{runDriftPreFlightGate as mt}from"../drift/preFlightGate.js";async function Et(n,e,I,z){const{callbacks:t,options:D}=n,{providerAccounts:f,effectiveOrgConfig:K,defaultRegion:U}=await gt(n,e),p=await ut(e,n.abortSignal);if(!p.success)return O(t,p.error.message);const w=await st(e.awsProvider.getClient(Q),p.data.rootId,n.abortSignal);if(!w.success)return O(t,w.error.message);const q={...p.data,...w.data!==void 0?{devOuId:w.data}:{}},J=f.find(o=>it(o)==="organisation"),d=dt(n,e,I,"organisation",q,void 0,J?.trailLifecycle),l=D?.cascade!==!1,{platformAccount:s,memberAccounts:P}=ot(f),E=l&&s!==void 0?1:0,L=l&&P.length>0?1:0,c=2+E+L,T=l?[...s!==void 0?[s]:[],...P]:[];if(T.length>0){const o=await at(e,T,t,n.abortSignal),a=s!==void 0?o.find(r=>r.accountId===s.id):void 0;if(a!==void 0)return O(t,`Pre-flight cascade role check failed for the platform account ${a.accountName} (${a.accountId}): ${a.error}`);for(const r of o)t.onProgress?.({type:"warning",message:g(`Pre-flight cascade role check failed for ${r.accountName} (${r.accountId}) \u2014 its cascade deploy is expected to fail: ${r.error}`)})}t.onCascadeAccountsReconciled?.({hasPlatformAccount:E>0,hasMemberAccounts:L>0});const{id:C,name:y}=B.PREPARE;t.onStepStart?.(C,y,0,c),t.onLog?.("Synthesising organisation infrastructure\u2026","info");const F=await X(e,d,t,"CDK synthesis failed");if(!F.success)return t.onStepComplete?.(C,y,"error",0,c),F;const x=e.awsProvider.getAccountId(),N=e.awsProvider.getRegion();lt(t,[{stackName:R(A.ORGANISATION),...x!==""?{accountId:x}:{},...N!==""?{region:N}:{}},...l&&s!==void 0?[{stackName:R(A.PLATFORM),accountId:s.id,region:s.region??N}]:[],...l?nt(P,rt(n.orgConfig)).map(({account:o,region:a})=>({stackName:R(A.ACCOUNT),accountId:o.id,region:a})):[]]);const G=await Z(e,d,t);if(!G.success)return t.onStepComplete?.(C,y,"error",0,c),G;t.onStepComplete?.(C,y,"completed",0,c);const{id:m,name:h}=B.ORG_DEPLOY,i=R(A.ORGANISATION);let H=!0;const S=await e.hashService.getTemplateHashes(_(d.path,"cdk.out"));if(S.success){const o=await e.hashService.compareWithState(S.data,d.path);o.success?H=o.data.stackChanges.get(i)??!0:t.onLog?.(g(`Org root change detection failed \u2014 deploying to be safe: ${o.error.message}`),"warn")}else t.onLog?.(g(`Org root template hashing failed \u2014 deploying to be safe: ${S.error.message}`),"warn");const $=H||D?.force===!0||!await e.cfnService.stackExists(i);t.onOrgChangesDetected?.({hasOrgChanges:$});let k;if($){t.onStepStart?.(m,h,1,c);const o=await mt(e.awsProvider,{stackNames:[i],cdkOutPath:_(d.path,"cdk.out"),cfnService:e.cfnService,callbacks:t,...n.abortSignal!==void 0?{abortSignal:n.abortSignal}:{}});if(o.kind==="blocked")return t.onStepComplete?.(m,h,"error",1,c),O(t,o.message);const a=await ct(n.assertLeaseForStack,i,n.abortSignal);if(a!==void 0)return t.onStepComplete?.(m,h,"error",1,c),t.onError?.(a),v(a);const r=await e.cdkService.runCdkDeploy(d,i,tt(t),et(t),e.awsProvider);if(!r.success)return t.onStepComplete?.(m,h,"error",1,c),O(t,r.error);k=await Y(e,t,i,"Failed to read org stack outputs (non-critical)");const j=S.success?S.data.get(i):void 0;if(j!==void 0){const W=await e.hashService.updateStateAfterDeploy(d.path,new Map([[i,j]]));W.success||t.onLog?.(`Warning: failed to update state file \u2014 next deploy may re-deploy the org root: ${g(W.error.message)}`,"warn")}t.onStepComplete?.(m,h,"completed",1,c)}else t.onLog?.("Organisation root: no infrastructure changes \u2014 skipping deploy","info"),k=await Y(e,t,i,"Failed to read org stack outputs (non-critical)");const u=[],b=[];let M=$;if(l&&f.length>0){const{anyCascadeDeployHappened:o}=await ft(n,e,I,{providerAccounts:f,effectiveOrgConfig:K,totalSteps:c,cascadeErrors:u,allCascadeOutputs:b});if(o&&(M=!0),await pt(n,e,{orgOutputs:k,allCascadeOutputs:b,orgDetails:p.data,providerAccounts:f,defaultRegion:U}),u.length>0){const a=u.map(r=>` ${r.accountId}: ${r.error}`).join(`
|
|
2
|
+
`);t.onLog?.(g(`Cascade failed for ${u.length} target(s):
|
|
3
|
+
${a}`),"warn")}}if(u.length>0){const o=u.map(r=>g(`${r.accountId}: ${r.error}`)).join(`
|
|
4
|
+
`),a=new Error(`Organisation root deployed, but the cascade failed for ${u.length} target(s):
|
|
5
|
+
${o}`);return t.onError?.(a),v(a)}return V({target:I.target,deploymentType:"organisation",outputs:k,artefacts:[],...b.length>0?{cascadeOutputs:b}:{},...M?{}:{noChanges:!0},durationMs:Date.now()-z})}export{Et as deployOrgWithCascade};
|
|
@@ -5,7 +5,11 @@ import type { OrganisationOperation } from "../../types/operations.js";
|
|
|
5
5
|
import type { DeployServices } from "../serviceFactory.js";
|
|
6
6
|
export interface OrgDetailsForSynth {
|
|
7
7
|
orgId: string;
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Absent when the deploy credentials cannot list roots — a member-account
|
|
10
|
+
* single-component deploy. Only the Organisation stack consumes it.
|
|
11
|
+
*/
|
|
12
|
+
rootId?: string;
|
|
9
13
|
managementAccountId: string;
|
|
10
14
|
/**
|
|
11
15
|
* Development OU id, resolved only for organisation-tier synths (the dev
|
|
@@ -16,7 +20,22 @@ export interface OrgDetailsForSynth {
|
|
|
16
20
|
devOuId?: string;
|
|
17
21
|
}
|
|
18
22
|
export declare function buildOrgContext(params: DeployParams, services: DeployServices, operation: OrganisationOperation, deployType: "organisation" | "platform" | "account", orgDetails: OrgDetailsForSynth | undefined, accountName?: string, trailLifecycle?: ProviderAccount["trailLifecycle"], targetIsOrganisationTier?: boolean): import("../../types/deployment/DeploymentTypes.js").DeploymentContext;
|
|
19
|
-
export declare function resolveOrgDetails(services: DeployServices, abortSignal?: AbortSignal): Promise<Result<OrgDetailsForSynth
|
|
23
|
+
export declare function resolveOrgDetails(services: DeployServices, abortSignal?: AbortSignal): Promise<Result<OrgDetailsForSynth & {
|
|
24
|
+
rootId: string;
|
|
25
|
+
}>>;
|
|
26
|
+
/**
|
|
27
|
+
* Resolve org details for a single-component (account/platform) deploy in an
|
|
28
|
+
* org-managed estate. Read-only and member-safe: DescribeOrganization is
|
|
29
|
+
* callable from any account in the organisation, while ListRoots is
|
|
30
|
+
* management-account-only — the cascade path resolves full details under
|
|
31
|
+
* management credentials, but a standalone `deploy account` runs under the
|
|
32
|
+
* target account's own credentials and must not require (or create)
|
|
33
|
+
* organisation-level access. The Account and Platform stacks consume only
|
|
34
|
+
* orgId (the exported OrganisationId output), so a best-effort rootId is
|
|
35
|
+
* sufficient here. Resolves undefined when no organisation exists yet — the
|
|
36
|
+
* component synthesises standalone, exactly like the solo path.
|
|
37
|
+
*/
|
|
38
|
+
export declare function resolveOrgDetailsForMemberComponent(services: DeployServices, abortSignal?: AbortSignal): Promise<Result<OrgDetailsForSynth | undefined>>;
|
|
20
39
|
/**
|
|
21
40
|
* Resolve org details for a SOLO-estate single-component deploy. Read-only — a
|
|
22
41
|
* standalone account deploy must NEVER create an organisation (the mutating
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{success as
|
|
1
|
+
import{success as i,failure as c}from"@fjall/generator";import{OrganizationsClient as d}from"@aws-sdk/client-organizations";import{CdkContextBuilder as l}from"../../services/supporting/CdkContextBuilder.js";import{stubCallerIdentity as I}from"../../types/deployment/index.js";import{ensureOrganisationExists as m,describeOrganisation as f,describeOrganisationAsMember as C}from"../../aws/organisations/organisation.js";import{buildParamsContext as O,resolveAccountBootstrapSkipOidc as v}from"../contextHelpers.js";function P(o,n,e,t,r,s,u,g){const a=n.awsProvider.getRegion();return l.buildDeploymentContext({deployType:t,target:e.target,path:e.path,region:a,accountName:s,callerIdentity:I(n.awsProvider.getAccountId()),orgId:r?.orgId,rootId:r?.rootId,managementAccountId:r?.managementAccountId,devOuId:r?.devOuId,...O({orgConfig:o.orgConfig,identity:o.identity,skipOidc:v(t,o.orgConfig,o.options?.skipOidc,g),...t!=="organisation"?{region:a,primaryRegion:o.orgConfig?.primaryRegion}:{},trailLifecycle:u})},{verbose:o.options?.verbose,infraOnly:o.options?.infraOnly},o.orgConfig)}async function k(o,n){const e=o.awsProvider.getClient(d),t=await m(e,n);return t.success?i({orgId:t.data.orgId,rootId:t.data.rootId,managementAccountId:t.data.managementAccountId}):c(t.error)}async function R(o,n){const e=o.awsProvider.getClient(d),t=await C(e,n);return t.success?t.data===null?i(void 0):i({orgId:t.data.orgId,managementAccountId:t.data.managementAccountId,...t.data.rootId!==void 0?{rootId:t.data.rootId}:{}}):c(t.error)}async function h(o,n){const e=o.awsProvider.getClient(d),t=await f(e,n,{tolerateAccessDenied:!0});if(!(!t.success||t.data===null))return{orgId:t.data.orgId,rootId:t.data.rootId,managementAccountId:t.data.managementAccountId}}export{P as buildOrgContext,k as resolveOrgDetails,R as resolveOrgDetailsForMemberComponent,h as resolveOrgDetailsForSolo};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{success as
|
|
1
|
+
import{join as h}from"path";import{failure as A,success as v}from"@fjall/generator";import{BackupClient as y}from"@aws-sdk/client-backup";import{IAMClient as T}from"@aws-sdk/client-iam";import{logger as M}from"@fjall/util/logger";import{getOrganisationStackName as G}from"../../types/operations.js";import{accountHasDisasterRecovery as Y,describeBackupVaultExists as x}from"../../aws/organisations/backup.js";import{describeAccountGlobalsExist as B,buildMissingAccountGlobalsAdvisory as V}from"../../aws/organisations/accountGlobals.js";import{targetsNonPrimaryRegion as _,reconcileOidcProviderIfCarried as j,synthOrFail as $,bootstrapOrFail as H,forwardOutput as K,forwardResourceProgress as q}from"../contextHelpers.js";import{accountTier as k,getErrorMessage as z,maskSensitiveOutput as N}from"@fjall/util";import{runDriftPreFlightGate as J}from"../drift/preFlightGate.js";import{completeForgetAfterDeploy as Q}from"../remediation/forgetResource.js";import{composePostFailureRepairOffer as U}from"../remediation/postFailureRepairOffer.js";import{buildOrgContext as W,resolveOrgDetailsForMemberComponent as X,resolveOrgDetailsForSolo as Z}from"./orgContext.js";import{hasOrganisationTierAccount as tt}from"../../aws/organisations/accountGlobals.js";import{INFRA_STEPS as n,INFRA_STEP_TOTAL as a,emitOrgStackTargets as ot,failPrepareStep as c,maskAndFail as l,readStackOutputsBestEffort as et}from"./infraSteps.js";import{assertLeaseBeforeStack as rt}from"../application/leaseGate.js";async function Et(o,r,d,g,D){const{callbacks:t}=o;t.onStepComplete?.(n.CONNECT.id,n.CONNECT.name,"completed",0,a),t.onStepStart?.(n.PREPARE.id,n.PREPARE.name,1,a);const P=r.awsProvider.getRegion(),f=o.orgConfig?.primaryRegion;if(f!==void 0&&_(P,f)){const e=await B(r.awsProvider.getClient(T),o.abortSignal);if(!e.success)return c(t),l(t,e.error.message);if(!e.data)return c(t),l(t,V({target:d.target,deployRegion:P,primaryRegion:f,...o.orgConfig!==void 0?{providerAccounts:o.orgConfig.providerAccounts}:{}}))}let m;if(tt(o.orgConfig?.providerAccounts)){const e=await X(r,o.abortSignal);if(!e.success)return c(t),l(t,e.error.message);m=e.data}else m=await Z(r,o.abortSignal);const s=g==="account"?o.orgConfig?.providerAccounts.find(e=>e.id===r.awsProvider.getAccountId()):o.orgConfig?.providerAccounts.find(e=>k(e)==="platform"),I=g==="account"&&s!==void 0&&k(s)==="organisation",u=W(o,r,d,g,m,g==="account"?s?.name??d.target:void 0,s?.trailLifecycle,I),S=o.orgConfig?.disasterRecoveryRegion;if(g==="account"&&(S!==void 0&&S!=="")&&(s===void 0||Y(s.environment,S))){const e=await x(r.awsProvider.getClient(y),o.abortSignal);if(!e.success)return c(t),l(t,e.error.message);u.fjallAdoptBackupVault=e.data}t.onLog?.(`Synthesising ${g} infrastructure\u2026`,"info");const R=await $(r,u,t,"CDK synthesis failed");if(!R.success)return c(t),R;const i=G(d.type),b=r.awsProvider.getAccountId(),O=r.awsProvider.getRegion();ot(t,[{stackName:i,...b!==""?{accountId:b}:{},...O!==""?{region:O}:{}}]);const w=await J(r.awsProvider,{stackNames:[i],cdkOutPath:h(d.path,"cdk.out"),cfnService:r.cfnService,callbacks:t,...o.abortSignal!==void 0?{abortSignal:o.abortSignal}:{}});if(w.kind==="blocked")return c(t),l(t,w.message);const C=await H(r,u,t);if(!C.success)return c(t),C;t.onStepComplete?.(n.PREPARE.id,n.PREPARE.name,"completed",1,a);const p=await rt(o.assertLeaseForStack,i,o.abortSignal);if(p!==void 0)return t.onError?.(p),A(p);t.onStepStart?.(n.DEPLOY.id,n.DEPLOY.name,2,a);const E=await r.cdkService.runCdkDeploy(u,i,K(t),q(t),r.awsProvider);if(!E.success){t.onStepComplete?.(n.DEPLOY.id,n.DEPLOY.name,"error",2,a);const e=await U(r.awsProvider,{stackNames:[i],repairTarget:`--stack ${i}`,originalError:new Error(N(E.error)),cfnService:r.cfnService,...o.abortSignal!==void 0?{abortSignal:o.abortSignal}:{}});return t.onError?.(e),A(e)}t.onStepComplete?.(n.DEPLOY.id,n.DEPLOY.name,"completed",2,a);const F=await et(r,t,i,"Failed to read stack outputs (non-critical)");return await j(u,r.awsProvider,t,o.abortSignal),t.onStepStart?.(n.MONITORING.id,n.MONITORING.name,3,a),t.onStepComplete?.(n.MONITORING.id,n.MONITORING.name,"completed",3,a),await Q(r.awsProvider,{stackNames:[i],onLog:(e,L)=>t.onLog?.(e,L),...o.abortSignal!==void 0?{abortSignal:o.abortSignal}:{}}).catch(e=>{M.debug("SingleComponentDeploy","Forget completion sweep failed",{error:N(z(e))})}),v({target:d.target,deploymentType:"organisation",outputs:F,artefacts:[],durationMs:Date.now()-D})}export{Et as deploySingleComponent};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `fjall drift repair` ceremony — the drift-flavoured twin of the deploy
|
|
3
|
+
* destruction gate. Builds a `DestructionTicket` from CONFIRMED drift
|
|
4
|
+
* findings (never unconfirmed — fail-closed verdicts refuse remediation) and
|
|
5
|
+
* evaluates `--remediate` consents through the SAME engine primitives
|
|
6
|
+
* (`evaluateDestructionConsents`, digest TOCTOU, [G6] verdict table,
|
|
7
|
+
* `executableRemediationVerbs`) so the two ceremonies cannot drift apart.
|
|
8
|
+
*
|
|
9
|
+
* Deliberately no plan loader and no interactive resolver: repair consents
|
|
10
|
+
* arrive as flags ([G5] — the agent surface teaches the flag SHAPE, the
|
|
11
|
+
* operator types the physical name). All output lines are identifier-only
|
|
12
|
+
* (physical names, verbs) — pre-masked by construction.
|
|
13
|
+
*/
|
|
14
|
+
import { type DriftFinding } from "../../types/drift.js";
|
|
15
|
+
import type { DestructionConsent, DestructionConsentReport, DestructionOutcome, DestructionTicket, RemediationVerb } from "../../types/destruction.js";
|
|
16
|
+
/**
|
|
17
|
+
* Build the repair ticket from confirmed-deleted findings. Returns undefined
|
|
18
|
+
* when nothing is confirmed (unconfirmed/stack-missing stay repair-only and
|
|
19
|
+
* never ticket). `physicalName` is the probed live physical id ([G6] — the
|
|
20
|
+
* exact string the operator must type); findings without one (membership
|
|
21
|
+
* listing could not resolve an id) fall back to the
|
|
22
|
+
* `remediationPlaceholderPhysicalId` address, same as the plan gate — the
|
|
23
|
+
* shared builder is what keeps this [G6] address byte-agreed with the
|
|
24
|
+
* journal target's physicalId, so the consent covers the target operated on.
|
|
25
|
+
*/
|
|
26
|
+
export declare function buildDriftRepairTicket(findings: readonly DriftFinding[]): DestructionTicket | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* [I1] The verbs a drift-DETECT surface may ADVERTISE for one finding — the
|
|
29
|
+
* verdict vocabulary reduced to what this gate would let execute. The
|
|
30
|
+
* drift-detect twin of `advertisableRemediationVerbs`: a confirmed-deleted
|
|
31
|
+
* finding tickets as `resource-deleted` (see `buildDriftRepairTicket` above),
|
|
32
|
+
* whose only execution arm is forget, so advertising recreate sells a consent
|
|
33
|
+
* the execution check withholds (consent-not-executable). Non-ticketing
|
|
34
|
+
* verdicts (unconfirmed, stack-missing, present) keep their vocabulary
|
|
35
|
+
* unchanged — they never enter the consent ceremony, so there is no gate to
|
|
36
|
+
* disagree with and `repair` stays the honest report-only teach.
|
|
37
|
+
* Consumers: the CLI drift-detect agent block rows + confirmed-verbs help
|
|
38
|
+
* line and the human `formatVerbList` line.
|
|
39
|
+
*/
|
|
40
|
+
export declare function advertisableDriftVerbs(finding: Pick<DriftFinding, "verdict" | "verbs">): RemediationVerb[];
|
|
41
|
+
export interface RunDriftRepairGateParams {
|
|
42
|
+
ticket: DestructionTicket;
|
|
43
|
+
consents: readonly DestructionConsent[];
|
|
44
|
+
/** Digest the consents were minted against (agent two-step) — TOCTOU. */
|
|
45
|
+
consentDigest?: string;
|
|
46
|
+
}
|
|
47
|
+
export type DriftRepairGateOutcome =
|
|
48
|
+
/** Every resource consented with an executable verb — run the surgery. */
|
|
49
|
+
{
|
|
50
|
+
kind: "proceed";
|
|
51
|
+
/** The consented verb per exact physical name. */
|
|
52
|
+
verbsByPhysicalName: ReadonlyMap<string, RemediationVerb>;
|
|
53
|
+
report: DestructionConsentReport;
|
|
54
|
+
} | {
|
|
55
|
+
kind: "withheld";
|
|
56
|
+
outcome: DestructionOutcome;
|
|
57
|
+
report: DestructionConsentReport;
|
|
58
|
+
reasons: string[];
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Evaluate repair consents — the exact fail-closed semantics of the deploy
|
|
62
|
+
* gate (matched/unmatched/wrong-verb rows, uncovered fails closed, digest
|
|
63
|
+
* mismatch stales everything, abort verb = operator decline, non-executable
|
|
64
|
+
* verb withholds truthfully).
|
|
65
|
+
*/
|
|
66
|
+
export declare function runDriftRepairGate(params: RunDriftRepairGateParams): DriftRepairGateOutcome;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{remediationPlaceholderPhysicalId as u}from"../../types/remediation.js";import{computeTicketDigest as f,evaluateDestructionConsents as p,executableRemediationVerbs as n,legalRemediationVerbs as m}from"../application/destructionGate.js";const b="drift-repair";function g(c){const i=c.filter(e=>e.verdict==="confirmed-deleted").map(e=>({stack:e.stackName,logicalId:e.logicalId,resourceType:e.resourceType,physicalName:e.physicalId??u(e.stackName,e.logicalId),finding:"resource-deleted",cause:`deleted outside CloudFormation \u2014 ${e.verdictDetail}`,offendingProperties:[],verdictSource:"drift-probe",dataLossRisk:!1,verbs:[...m("resource-deleted")]}));if(i.length!==0)return{ticketDigest:f(b,i),resources:i}}function D(c){if(c.verdict!=="confirmed-deleted")return[...c.verbs];const i=n("resource-deleted");return c.verbs.filter(e=>i.includes(e))}function N(c){const{ticket:i,consents:e,consentDigest:d}=c,o=p({ticket:i,consents:e,...d!==void 0?{consentDigest:d}:{}}),l=new Map(i.resources.map(t=>[t.physicalName,t])),s=[],a=[];if(o.satisfied)for(const t of o.rows){if(t.verdict!=="matched")continue;const r=l.get(t.physicalName);if(r!==void 0){if(t.verb==="abort"){s.push(`${t.physicalName}: aborted by operator consent`);continue}n(r.finding).includes(t.verb)||a.push(`${t.physicalName}: verb '${t.verb}' is accepted vocabulary but not yet executable via 'fjall drift repair' for ${r.finding} \u2014 executable today: ${n(r.finding).join(", ")||"none (abort only)"}`)}}return o.satisfied&&s.length===0&&a.length===0?{kind:"proceed",verbsByPhysicalName:new Map(o.rows.filter(r=>r.verdict==="matched").map(r=>[r.physicalName,r.verb])),report:o}:{kind:"withheld",outcome:s.length>0?"declined":a.length>0?"consent-not-executable":o.rows.length>0||o.digestMismatch?"consents-invalid":"awaiting-consent",report:o,reasons:[...s,...a]}}export{D as advertisableDriftVerbs,g as buildDriftRepairTicket,N as runDriftRepairGate};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forensics barrier for remediation surgery (drift-remediation plan § 3.2
|
|
3
|
+
* phase 1, Q13). Persists the pre-mutation evidence — `GetTemplate(Original)`,
|
|
4
|
+
* `DescribeStacks`, the last 50 stack events, and the drift-probe findings —
|
|
5
|
+
* to `~/.fjall/forensics/remediation/<opId>/<capturedAt>/` BEFORE any
|
|
6
|
+
* mutation. The capture-stamp segment scopes the bundle to ONE incarnation of
|
|
7
|
+
* the opId (opId has no incarnation component): an archive-and-restart of the
|
|
8
|
+
* same stack + logical-ID set captures into its OWN directory instead of
|
|
9
|
+
* replacing the prior incarnation's evidence, so an archived done journal's
|
|
10
|
+
* `forensicsPath` keeps resolving to the artefacts it recorded.
|
|
11
|
+
*
|
|
12
|
+
* Capture failure ABORTS the operation (the `onPolicyCaptured` precedent,
|
|
13
|
+
* unlockBucket.ts): no capture → no mutate. Every artefact is masked before
|
|
14
|
+
* persisting (boundary 4 — stack-event reasons and template bodies can embed
|
|
15
|
+
* credential values) and written atomically (tmp + rename).
|
|
16
|
+
*
|
|
17
|
+
* Failure messages are pre-masked at construction — callers may surface them
|
|
18
|
+
* to callback/log sinks directly.
|
|
19
|
+
*/
|
|
20
|
+
import { type Result } from "@fjall/generator";
|
|
21
|
+
import type { AwsProvider } from "../../aws/AwsProvider.js";
|
|
22
|
+
import type { DriftFinding } from "../../types/drift.js";
|
|
23
|
+
export interface ForensicsCaptureOptions {
|
|
24
|
+
/** Override the forensics root — tests point this at a temp dir. */
|
|
25
|
+
forensicsDir?: string;
|
|
26
|
+
/** Injectable clock for the manifest timestamp. */
|
|
27
|
+
now?: () => Date;
|
|
28
|
+
}
|
|
29
|
+
export declare function defaultRemediationForensicsDir(): string;
|
|
30
|
+
export interface CaptureRemediationForensicsParams {
|
|
31
|
+
stackName: string;
|
|
32
|
+
opId: string;
|
|
33
|
+
/** The drift-probe evidence that motivated the surgery. */
|
|
34
|
+
findings: readonly DriftFinding[];
|
|
35
|
+
abortSignal?: AbortSignal;
|
|
36
|
+
}
|
|
37
|
+
export interface RemediationForensicsBundle {
|
|
38
|
+
forensicsPath: string;
|
|
39
|
+
files: string[];
|
|
40
|
+
/** The raw live template body — reused by the flip derivation so the flip
|
|
41
|
+
* provably derives from the same bytes the forensics preserved. */
|
|
42
|
+
liveTemplateBody: string;
|
|
43
|
+
/** The stack's current capabilities, echoed on the flip UpdateStack. */
|
|
44
|
+
capabilities: string[];
|
|
45
|
+
/** Live parameter keys — echoed UsePreviousValue on both flip CFN calls. */
|
|
46
|
+
parameterKeys: string[];
|
|
47
|
+
stackStatus: string;
|
|
48
|
+
}
|
|
49
|
+
export declare function captureRemediationForensics(aws: AwsProvider, params: CaptureRemediationForensicsParams, options?: ForensicsCaptureOptions): Promise<Result<RemediationForensicsBundle, Error>>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{CloudFormationClient as F,DescribeStackEventsCommand as O,DescribeStacksCommand as D,GetTemplateCommand as B}from"@aws-sdk/client-cloudformation";import{mkdir as K,rename as _,writeFile as x}from"node:fs/promises";import{homedir as I}from"node:os";import{join as u}from"node:path";import{success as J,failure as c}from"@fjall/generator";import{getErrorMessage as $,maskSensitiveOutput as p}from"@fjall/util";import{composeSdkAbortSignal as g,isAborted as M}from"../../aws/organisations/types.js";const S=50,R=20;function G(){return u(I(),".fjall","forensics","remediation")}async function q(A,k,b){const{stackName:t,opId:w,abortSignal:o}=k,l=A.getClient(F);let m,v,h,y,E,d,T;try{const e=await l.send(new B({StackName:t,TemplateStage:"Original"}),{abortSignal:g(o)});if(e.TemplateBody===void 0||e.TemplateBody==="")return c(new Error(`Forensics capture aborted the remediation of ${t}: GetTemplate returned an empty template body \u2014 nothing was mutated.`));m=e.TemplateBody;const n=(await l.send(new D({StackName:t}),{abortSignal:g(o)})).Stacks?.[0];if(n===void 0)return c(new Error(`Forensics capture aborted the remediation of ${t}: DescribeStacks returned no stack \u2014 nothing was mutated.`));v=n,h=n.Capabilities??[],y=(n.Parameters??[]).flatMap(r=>r.ParameterKey!==void 0?[r.ParameterKey]:[]),E=n.StackStatus??"UNKNOWN";const s=[];let a,C=0;do{if(M(o))throw new Error(`aborted while reading the stack events of ${t}`);const r=await l.send(new O({StackName:t,...a!==void 0?{NextToken:a}:{}}),{abortSignal:g(o)});s.push(...r.StackEvents??[]),a=r.NextToken,C+=1}while(a!==void 0&&s.length<S&&C<R);T=a!==void 0||s.length>S,d=s.slice(0,S)}catch(e){return c(new Error(`Forensics capture failed for ${t} \u2014 aborting before any mutation: ${p($(e))}`))}const P=b?.forensicsDir??G(),N=(b?.now?.()??new Date).toISOString(),i=u(P,w,N.replace(/[^0-9A-Za-z.-]/g,"")),j=[{name:"live-template.json",content:m},{name:"stack-description.json",content:JSON.stringify(v,null,2)},{name:"stack-events.json",content:JSON.stringify(d,null,2)},{name:"drift-findings.json",content:JSON.stringify(k.findings,null,2)},{name:"manifest.json",content:JSON.stringify({opId:w,stackName:t,capturedAt:N,files:["live-template.json","stack-description.json","stack-events.json","drift-findings.json"],...T?{eventsTruncated:!0,eventsCaptured:d.length}:{}},null,2)}];try{await K(i,{recursive:!0});for(const e of j){const f=u(i,e.name),n=`${f}.tmp-${process.pid}`;await x(n,p(e.content),"utf8"),await _(n,f)}}catch(e){return c(new Error(`Forensics capture could not persist the evidence bundle for ${t} at ${i} \u2014 aborting before any mutation: ${p($(e))}`))}return J({forensicsPath:i,files:j.map(e=>e.name),liveTemplateBody:m,capabilities:h,parameterKeys:y,stackStatus:E})}export{q as captureRemediationForensics,G as defaultRemediationForensicsDir};
|