@fjall/deploy-core 2.13.0 → 2.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.minified +1 -1
- package/dist/src/aws/index.d.ts +6 -2
- package/dist/src/aws/index.js +1 -1
- package/dist/src/aws/organisations/accountGlobals.d.ts +40 -0
- package/dist/src/aws/organisations/accountGlobals.js +1 -0
- package/dist/src/aws/organisations/accounts.js +1 -1
- package/dist/src/aws/organisations/importedAccounts.d.ts +16 -0
- package/dist/src/aws/organisations/importedAccounts.js +1 -0
- package/dist/src/aws/organisations/index.d.ts +3 -1
- package/dist/src/aws/organisations/index.js +1 -1
- package/dist/src/aws/organisations/rootAccess.d.ts +27 -0
- package/dist/src/aws/organisations/rootAccess.js +3 -0
- package/dist/src/aws/organisations/serviceAccess.d.ts +6 -0
- package/dist/src/aws/organisations/serviceAccess.js +1 -1
- package/dist/src/aws/organisations/types.d.ts +12 -0
- package/dist/src/aws/organisations/types.js +1 -1
- package/dist/src/aws/sts/assumeRoot.d.ts +46 -0
- package/dist/src/aws/sts/assumeRoot.js +1 -0
- package/dist/src/aws/targetReadiness.d.ts +70 -0
- package/dist/src/aws/targetReadiness.js +1 -0
- package/dist/src/aws/targetSetAdvisory.d.ts +24 -0
- package/dist/src/aws/targetSetAdvisory.js +1 -0
- package/dist/src/index.d.ts +13 -13
- package/dist/src/index.js +1 -1
- package/dist/src/orchestration/applicationDeploy.js +1 -1
- package/dist/src/orchestration/applicationDestroy.js +1 -1
- package/dist/src/orchestration/cascadeDestroyHelpers.d.ts +12 -1
- package/dist/src/orchestration/cascadeDestroyHelpers.js +1 -1
- package/dist/src/orchestration/cascadeHelpers.d.ts +21 -6
- package/dist/src/orchestration/cascadeHelpers.js +1 -1
- package/dist/src/orchestration/contextHelpers.d.ts +17 -2
- package/dist/src/orchestration/contextHelpers.js +1 -1
- package/dist/src/orchestration/index.d.ts +8 -3
- package/dist/src/orchestration/index.js +1 -1
- package/dist/src/orchestration/organisationDeploy/cascadeExecution.d.ts +28 -0
- package/dist/src/orchestration/organisationDeploy/cascadeExecution.js +1 -0
- package/dist/src/orchestration/organisationDeploy/infraSteps.d.ts +40 -0
- package/dist/src/orchestration/organisationDeploy/infraSteps.js +1 -0
- package/dist/src/orchestration/organisationDeploy/orgCascadeDeploy.d.ts +8 -0
- package/dist/src/orchestration/organisationDeploy/orgCascadeDeploy.js +5 -0
- package/dist/src/orchestration/organisationDeploy/orgContext.d.ts +12 -0
- package/dist/src/orchestration/organisationDeploy/orgContext.js +1 -0
- package/dist/src/orchestration/organisationDeploy/resolveCascadeAccounts.d.ts +15 -0
- package/dist/src/orchestration/organisationDeploy/resolveCascadeAccounts.js +1 -0
- package/dist/src/orchestration/organisationDeploy/singleComponentDeploy.d.ts +11 -0
- package/dist/src/orchestration/organisationDeploy/singleComponentDeploy.js +1 -0
- package/dist/src/orchestration/organisationDeploy/trailReconciliation.d.ts +21 -0
- package/dist/src/orchestration/organisationDeploy/trailReconciliation.js +1 -0
- package/dist/src/orchestration/organisationDeploy.d.ts +1 -5
- package/dist/src/orchestration/organisationDeploy.js +1 -5
- package/dist/src/orchestration/organisationDestroy.d.ts +1 -1
- package/dist/src/orchestration/organisationDestroy.js +2 -2
- package/dist/src/orchestration/organisationSetup.d.ts +18 -2
- package/dist/src/orchestration/organisationSetup.js +1 -1
- package/dist/src/orchestration/stackCleanup/bucketOps.d.ts +54 -0
- package/dist/src/orchestration/stackCleanup/bucketOps.js +1 -0
- package/dist/src/orchestration/stackCleanup/failedStack.d.ts +34 -0
- package/dist/src/orchestration/stackCleanup/failedStack.js +1 -0
- package/dist/src/orchestration/stackCleanup/logging.d.ts +9 -0
- package/dist/src/orchestration/stackCleanup/logging.js +1 -0
- package/dist/src/orchestration/stackCleanup/messages.d.ts +16 -0
- package/dist/src/orchestration/stackCleanup/messages.js +1 -0
- package/dist/src/orchestration/stackCleanup/orphanSweep.d.ts +25 -0
- package/dist/src/orchestration/stackCleanup/orphanSweep.js +1 -0
- package/dist/src/orchestration/stackCleanup/preEmpty.d.ts +35 -0
- package/dist/src/orchestration/stackCleanup/preEmpty.js +1 -0
- package/dist/src/orchestration/stackCleanup/stackResources.d.ts +9 -0
- package/dist/src/orchestration/stackCleanup/stackResources.js +1 -0
- package/dist/src/orchestration/stackCleanup.d.ts +13 -40
- package/dist/src/orchestration/stackCleanup.js +1 -1
- package/dist/src/orchestration/trailMigration/memberTrailCleanup.js +1 -1
- package/dist/src/orchestration/unlock/scpRemediation.d.ts +15 -0
- package/dist/src/orchestration/unlock/scpRemediation.js +1 -0
- package/dist/src/orchestration/unlock/unlockBucket.d.ts +37 -0
- package/dist/src/orchestration/unlock/unlockBucket.js +1 -0
- package/dist/src/orchestration/unlock/unlockQueue.d.ts +43 -0
- package/dist/src/orchestration/unlock/unlockQueue.js +1 -0
- package/dist/src/services/application/ApplicationStackService.d.ts +9 -10
- package/dist/src/services/application/ApplicationStackService.js +1 -1
- package/dist/src/services/application/applicationStackHelpers.d.ts +13 -8
- package/dist/src/services/application/applicationStackHelpers.js +3 -3
- package/dist/src/steps/stepRegistry.js +1 -1
- package/dist/src/types/FjallState.d.ts +7 -0
- package/dist/src/types/FjallState.js +1 -1
- package/dist/src/types/callbacks.d.ts +43 -2
- package/dist/src/types/callbacks.js +1 -0
- package/dist/src/types/deploymentEventSchema.d.ts +9 -0
- package/dist/src/types/deploymentEventSchema.js +1 -1
- package/dist/src/types/index.d.ts +5 -10
- package/dist/src/types/index.js +1 -1
- package/dist/src/types/orgConfig.d.ts +8 -2
- package/dist/src/types/params.d.ts +12 -0
- package/dist/src/types/patternDetection.d.ts +0 -25
- package/dist/src/types/patternDetection.js +1 -1
- package/dist/src/types/stepDefinitions.d.ts +2 -0
- package/dist/src/types/stepDefinitions.js +1 -1
- package/package.json +6 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
import{success as w,failure as b}from"@fjall/generator";import{getErrorMessage as y}from"@fjall/util";import{logger as
|
|
1
|
+
import{success as w,failure as b}from"@fjall/generator";import{getErrorMessage as y}from"@fjall/util";import{logger as S}from"@fjall/util/logger";import{getApplicationDestroyOrder as N,getApplicationStepName as h,getApplicationStepId as P}from"../types/operations.js";import{stubCallerIdentity as x}from"../types/deployment/index.js";import{deriveResourcesFromManifestStacks as D}from"../types/patternDetection.js";import{CdkContextBuilder as R}from"../services/supporting/CdkContextBuilder.js";import{buildParamsContext as I}from"./contextHelpers.js";import{deleteStateFile as L,readStateFile as O}from"../types/FjallState.js";async function q(n,c,r){const{callbacks:t}=n,C=Date.now(),m=c.frameworkRegistry.resolve({appPath:r.path})?.detection.pattern??null;let p;try{const e=await O(r.path);if(e!==null){const o=Object.keys(e.templateHashes);o.length>0&&(p=D(o))}}catch(e){S.debug("applicationDestroy","Could not read state file for resource detection",{error:y(e)}),t.onLog?.("Could not read state file for resource detection \u2014 falling back to pattern detection","warn")}const k=R.buildDeploymentContext({deployType:"application",target:r.appName,path:r.path,region:c.awsProvider.getRegion(),callerIdentity:x(c.awsProvider.getAccountId()),...I({orgConfig:n.orgConfig,identity:n.identity})},{verbose:n.options?.verbose},n.orgConfig),d=N({pattern:m,resources:p}),s=d.length;t.onLog?.(`Destroying ${r.appName} (${s} stacks, ${m??"standard"} pattern)`,"info");const f=[],u=[],g=await c.stackService.destroyAllStacks(k,{onOutput:e=>{t.onOutput?.(e)},onLog:t.onLog,onStackCleanupProgress:t.onStackCleanupProgress,onResourceProgress:(e,o)=>{t.onResourceProgress?.(e),o&&t.onParallelStackResourceProgress?.(o,e)},onStackStart:(e,o)=>{const a=P(e,"destroy"),l=h(e,"destroy"),i=d.indexOf(e);t.onStepStart?.(a,l,i,s)},onStackComplete:async(e,o)=>{const a=P(e,"destroy"),l=h(e,"destroy"),i=d.indexOf(e);o.success?o.data?.skipped?(u.push(o.data.stackName||e),t.onStepComplete?.(a,l,"skipped",i,s)):(f.push(o.data?.stackName||e),t.onStepComplete?.(a,l,"completed",i,s)):(t.onStepComplete?.(a,l,"error",i,s),t.onError?.(o.error))},onParallelPhaseStart:(e,o)=>{t.onLog?.(`Parallel phase: ${o}`,"info"),t.onParallelPhaseStart?.(e,o)},onParallelPhaseComplete:e=>{const o=e.filter(a=>!a.success);o.length>0&&t.onLog?.(`Parallel phase completed with ${o.length} failure(s)`,"warn"),t.onParallelPhaseComplete?.(e)}},p,n.abortSignal);if(!g.success)return t.onError?.(g.error),b(g.error);try{await L(r.path)}catch(e){S.debug("applicationDestroy","Failed to delete state file (non-critical)",{error:y(e)}),t.onLog?.("Failed to delete state file (non-critical)","warn")}return w({target:r.appName,deploymentType:"application",stacksDestroyed:f,skippedStacks:u,durationMs:Date.now()-C})}export{q as destroyApplication};
|
|
@@ -17,6 +17,17 @@ export interface CascadeDestroyAccountResult {
|
|
|
17
17
|
error?: string;
|
|
18
18
|
skipped?: boolean;
|
|
19
19
|
}
|
|
20
|
+
export interface DestroyCascadeAccountOptions {
|
|
21
|
+
/**
|
|
22
|
+
* Resolved primary (home) region of the cascade. buildParamsContext derives
|
|
23
|
+
* the account-globals skip from it, mirroring deployCascadeAccount: a
|
|
24
|
+
* non-home (secondary/DR) region destroy synthesises WITHOUT the fixed-name
|
|
25
|
+
* org-global IAM resources, matching the deployed stack shape so the destroy
|
|
26
|
+
* can never identify the primary's globals for teardown. Omitted ⇒ the
|
|
27
|
+
* globals stay in the synthesised template (home-region semantics).
|
|
28
|
+
*/
|
|
29
|
+
primaryRegion?: string;
|
|
30
|
+
}
|
|
20
31
|
/**
|
|
21
32
|
* Destroy a single cascade account (platform or member) in a specific region.
|
|
22
33
|
*
|
|
@@ -27,4 +38,4 @@ export declare function destroyCascadeAccount(params: DestroyParams, services: D
|
|
|
27
38
|
id: string;
|
|
28
39
|
name: string;
|
|
29
40
|
environment: string | null;
|
|
30
|
-
}, deployType: "platform" | "account", region: string, callbacks: DeployCallbacks): Promise<CascadeDestroyAccountResult>;
|
|
41
|
+
}, deployType: "platform" | "account", region: string, callbacks: DeployCallbacks, options?: DestroyCascadeAccountOptions): Promise<CascadeDestroyAccountResult>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{join as
|
|
1
|
+
import{join as I}from"path";import{logger as f}from"@fjall/util/logger";import{maskSensitiveOutput as i,getErrorMessage as v}from"@fjall/util";import{stubCallerIdentity as b}from"../types/deployment/index.js";import{CloudFormationClient as P,DescribeStacksCommand as _}from"@aws-sdk/client-cloudformation";import{S3Client as g}from"@aws-sdk/client-s3";import{BackupClient as x}from"@aws-sdk/client-backup";import{accountHasDisasterRecovery as F,describeSurvivingBackupVault as M,formatSurvivingVaultWarning as K}from"../aws/organisations/backup.js";import{composeSdkAbortSignal as L}from"../aws/organisations/types.js";import{ORGANISATION_TYPES as R,getOrganisationStackName as V}from"../types/operations.js";import{CdkContextBuilder as j}from"../services/supporting/CdkContextBuilder.js";import{regionSuffix as U}from"../types/FjallState.js";import{buildParamsContext as G,assumeCascadeRole as H,forwardOutput as E}from"./contextHelpers.js";import{cascadeOperationKey as W}from"./cascadeHelpers.js";import{capturedSweepBuckets as Y,cleanupFailedStack as q,preEmptyStackBuckets as z,sweepOrphanedDestroyBuckets as N}from"./stackCleanup.js";import{STACK_NOT_FOUND_PATTERN as J,STACK_FAILED_STATE_PATTERN as Q}from"../types/constants.js";async function ye(a,o,c,e,n,t,r,T){const y=Date.now(),u=W(e.name,t),l=(s,p=s)=>(r.onCascadeAccountComplete?.(u,!1,p,t),{accountName:e.name,accountId:e.id,region:t,success:!1,duration:Date.now()-y,error:s});r.onCascadeAccountStart?.(u,e.id,t,n);const S=await H(o.awsProvider,e.id,t,`fjall-cascade-destroy-${e.name}`);if(!S.success){const s=i(S.error.message);return l(`AssumeRole failed: ${s}`,s)}const{provider:d,credentials:w}=S.data,O=I(c.path,`cdk.out.${e.id}.${U(t)}`),k=j.buildDeploymentContext({deployType:n,target:c.target,path:c.path,assemblyDir:O,region:t,accountName:e.name,callerIdentity:b(e.id),...G({orgConfig:a.orgConfig,identity:a.identity,region:t,primaryRegion:T?.primaryRegion})},{verbose:a.options?.verbose},a.orgConfig);r.onCascadeAccountPhaseChange?.(u,"synth",t);const A=await o.cdkService.runCdkSynth(k,E(r));if(!A.success)return l(i(`Synth failed: ${A.error}`));r.onCascadeAccountPhaseChange?.(u,"destroy",t);const m=V(n==="platform"?R.PLATFORM:R.ACCOUNT);F(e.environment,a.orgConfig?.disasterRecoveryRegion)&&await X(d.getClient(x),t,r);const $=await z(d.getClient(P),d.getClient(g),m,r,a.abortSignal),C=Y($),D=await o.cdkService.runCdkDestroy(k,m,E(r),s=>r.onCascadeAccountResourceProgress?.(u,s,t),d,!0,w);if(!D.success){const s=D.error;if(s.includes(Q)){f.warn("cascadeDestroy",`CDK destroy failed on ${m} in failed state, retrying via CloudFormation API`,{region:t,account:e.name});try{await q(m,t,w,{accountId:e.id,abortSignal:a.abortSignal},r)}catch(B){const h=`cleanupFailedStack threw for ${m}: ${i(v(B))}`;f.warn("cascadeDestroy",h),r.onLog?.(h,"warn")}const p=await Z(m,d.getClient(P),a.abortSignal);return p.deleted?(C.length>0&&await N(d.getClient(g),C,r,a.abortSignal),r.onCascadeAccountComplete?.(u,!0,void 0,t),{accountName:e.name,accountId:e.id,region:t,success:!0,duration:Date.now()-y}):p.error?l(i(p.error)):l(i(`Stack ${m} cleanup attempted but stack still exists in ${t}`))}return l(i(s))}return C.length>0&&await N(d.getClient(g),C,r,a.abortSignal),r.onCascadeAccountComplete?.(u,!0,void 0,t),{accountName:e.name,accountId:e.id,region:t,success:!0,duration:Date.now()-y}}async function X(a,o,c){try{const e=await M(a,o);if(!e.success){f.debug("cascadeDestroy","Backup-vault survival probe failed",{region:o,error:i(e.error.message)});return}if(e.data===null)return;c.onProgress?.({type:"warning",message:K(e.data),metadata:{source:"backup-vault-survival"}}),f.warn("cascadeDestroy","Backup vault survives destroy",{region:o,vaultName:e.data.vaultName,recoveryPointCount:e.data.recoveryPointCount,lockPermanent:e.data.lockPermanent})}catch(e){f.debug("cascadeDestroy","Backup-vault survival probe threw",{region:o,error:i(v(e))})}}async function Z(a,o,c){try{const n=(await o.send(new _({StackName:a}),{abortSignal:L(c)})).Stacks?.[0]?.StackStatus;return!n||n==="DELETE_COMPLETE"?{deleted:!0}:{deleted:!1,error:`Stack still in ${n} after cleanup attempt`}}catch(e){if(e instanceof Error&&e.message?.includes(J))return{deleted:!0};const n=i(v(e));return f.debug("cascadeDestroy","Stack verification failed",{error:n}),{deleted:!1,error:`Stack verification failed: ${n}`}}}export{ye as destroyCascadeAccount};
|
|
@@ -34,6 +34,20 @@ export interface AccountRegionPair {
|
|
|
34
34
|
* on this set, else destroy sweeps regions deploy never created (or vice versa).
|
|
35
35
|
*/
|
|
36
36
|
export declare function buildRegionList(orgConfig: OrgConfig | undefined): string[];
|
|
37
|
+
/**
|
|
38
|
+
* The cascade's home (anchor) region — first entry of buildRegionList, i.e.
|
|
39
|
+
* the configured primary (or DEFAULT_REGION when none is set). Deploy and
|
|
40
|
+
* destroy fan-outs MUST share this derivation so a destroy synth mirrors the
|
|
41
|
+
* account-globals decision of the deploy that created each region's stack.
|
|
42
|
+
*/
|
|
43
|
+
export declare function cascadeHomeRegion(orgConfig: OrgConfig | undefined): string;
|
|
44
|
+
/**
|
|
45
|
+
* Operation key identifying one account x region task across the cascade
|
|
46
|
+
* lifecycle. Deploy events, destroy events, and the Ink pre-seeded pills
|
|
47
|
+
* (CLI `stackOperationUtils.fetchCascadeAccounts`) MUST share this shape so
|
|
48
|
+
* every pill matches its engine events one-for-one.
|
|
49
|
+
*/
|
|
50
|
+
export declare function cascadeOperationKey(accountName: string, region: string): string;
|
|
37
51
|
/**
|
|
38
52
|
* Build all account x region pairs for parallel cascade fan-out.
|
|
39
53
|
*/
|
|
@@ -54,7 +68,7 @@ export interface CascadeRoleProbeFailure {
|
|
|
54
68
|
* credentials are discarded. Inherits assumeCascadeRole's retry budget, so
|
|
55
69
|
* role propagation on freshly created accounts does not false-fail.
|
|
56
70
|
*/
|
|
57
|
-
export declare function probeCascadeRoles(services: DeployServices, accounts: ProviderAccount[], callbacks: DeployCallbacks): Promise<CascadeRoleProbeFailure[]>;
|
|
71
|
+
export declare function probeCascadeRoles(services: DeployServices, accounts: ProviderAccount[], callbacks: DeployCallbacks, signal?: AbortSignal): Promise<CascadeRoleProbeFailure[]>;
|
|
58
72
|
/**
|
|
59
73
|
* Deploy a single cascade account (platform or member).
|
|
60
74
|
* Assumes the target account's role, sets env credentials, and deploys.
|
|
@@ -78,11 +92,12 @@ export interface DeployCascadeAccountOptions {
|
|
|
78
92
|
/** Region to deploy in; falls back to account.region then the provider region. */
|
|
79
93
|
region?: string;
|
|
80
94
|
/**
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
95
|
+
* Resolved primary (home) region of the cascade. buildParamsContext derives
|
|
96
|
+
* the account-globals skip from it: non-home (secondary/DR) regions skip the
|
|
97
|
+
* fixed-name org-global IAM resources that would collide across regions.
|
|
98
|
+
* Omitted ⇒ globals are never skipped.
|
|
84
99
|
*/
|
|
85
|
-
|
|
100
|
+
primaryRegion?: string;
|
|
86
101
|
}
|
|
87
102
|
export declare function deployCascadeAccount(params: DeployParams, services: DeployServices, operation: OrganisationOperation, account: ProviderAccount, deployType: "platform" | "account", callbacks: DeployCallbacks, options?: DeployCascadeAccountOptions): Promise<Result<CascadeAccountResult>>;
|
|
88
103
|
/**
|
|
@@ -94,7 +109,7 @@ export declare function deployCascadeAccount(params: DeployParams, services: Dep
|
|
|
94
109
|
export declare function readPlatformIpamPoolIds(services: DeployServices, platformAccount: {
|
|
95
110
|
id: string;
|
|
96
111
|
name: string;
|
|
97
|
-
}, callbacks: DeployCallbacks): Promise<Map<string, string>>;
|
|
112
|
+
}, callbacks: DeployCallbacks, signal?: AbortSignal): Promise<Map<string, string>>;
|
|
98
113
|
/**
|
|
99
114
|
* Deploy configured domains: apex domains sequentially, then delegated
|
|
100
115
|
* domains in parallel. Delegates to the caller-provided DomainDeployProvider.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{join as L}from"path";import{success as B,failure as
|
|
1
|
+
import{join as L}from"path";import{success as B,failure as h}from"@fjall/generator";import{logger as y}from"@fjall/util/logger";import{maskSensitiveOutput as p,mapSettledWithConcurrency as U}from"@fjall/util";import{ORGANISATION_TYPES as R,getOrganisationStackName as j}from"../types/operations.js";import{CdkContextBuilder as _}from"../services/supporting/CdkContextBuilder.js";import{stubCallerIdentity as K}from"../types/deployment/index.js";import{CloudFormationService as H}from"../services/infrastructure/CloudFormationService.js";import{getCascadeStateFilePath as z,regionSuffix as G}from"../types/FjallState.js";import{BackupClient as Y}from"@aws-sdk/client-backup";import{accountHasDisasterRecovery as W,describeBackupVaultExists as X}from"../aws/organisations/backup.js";import{buildParamsContext as q,collectStackOutputs as M,assumeCascadeRole as v,forwardOutput as x}from"./contextHelpers.js";import{accountTier as T}from"@fjall/util";import{DEFAULT_REGION as V}from"../aws/utils/regions.js";const J=4;function le(o){const n=o.find(e=>T(e)==="platform"),c=o.filter(e=>T(e)==="account");return{platformAccount:n,memberAccounts:c}}function Q(o){const n=o?.primaryRegion??V,c=o?.secondaryRegions??[],e=new Set([n,...c]),i=o?.disasterRecoveryRegion;return i&&e.add(i),[...e]}function ge(o){return Q(o)[0]??V}function Z(o,n){return`${o} (${n})`}function Ce(o,n){const c=[];for(const e of n)for(const i of o)c.push({account:i,region:e});return c}import{buildCascadeRoleArn as Se}from"./contextHelpers.js";async function he(o,n,c,e){c.onLog?.(`Verifying cascade role access for ${n.length} account(s)\u2026`,"info");const i=await U(n,J,async a=>v(o.awsProvider,a.id,a.region??o.awsProvider.getRegion(),`fjall-preflight-${a.name}`,e)),s=[];return i.forEach((a,t)=>{const r=n[t];if(r){if(a.status==="rejected"){s.push({accountId:r.id,accountName:r.name,error:a.reason instanceof Error?a.reason.message:String(a.reason)});return}a.value.success||s.push({accountId:r.id,accountName:r.name,error:a.value.error.message})}}),s.length===0&&c.onLog?.(`Cascade role access verified for ${n.length} account(s)`,"info"),s}async function ye(o,n,c,e,i,s,a){const t=a?.region??e.region??n.awsProvider.getRegion(),r=Z(e.name,t),u=a?.orgConfig??o.orgConfig,l=a?.ipamPoolId;s.onCascadeAccountStart?.(r,e.id,t,i);const d=await v(n.awsProvider,e.id,t,`fjall-cascade-${e.name}`,o.abortSignal);if(!d.success)return s.onCascadeAccountComplete?.(r,!1,p(d.error.message),t),h(new Error(`Failed to assume role for ${e.name}: ${p(d.error.message)}`));const{provider:g,credentials:C}=d.data,P=L(o.workingDirectory,"fjall",i==="platform"?R.PLATFORM:R.ACCOUNT),D=L(P,`cdk.out.${e.id}.${G(t)}`),w=_.buildDeploymentContext({deployType:i,target:c.target,path:P,assemblyDir:D,environment:e.environment??void 0,region:t,accountName:e.name,callerIdentity:K(e.id),ipamPoolId:l,...q({orgConfig:u,identity:o.identity,skipOidc:o.options?.skipOidc,region:t,primaryRegion:a?.primaryRegion,trailLifecycle:e.trailLifecycle})},{verbose:o.options?.verbose},u);if(W(e.environment,u?.disasterRecoveryRegion)){const f=await X(g.getClient(Y));if(!f.success)return s.onCascadeAccountComplete?.(r,!1,p(f.error.message),t),h(new Error(`Backup vault probe failed for ${e.name}: ${p(f.error.message)}`));w.fjallAdoptBackupVault=f.data}s.onCascadeAccountPhaseChange?.(r,"synth",t);const $=await n.cdkService.runCdkSynth(w,x(s),C);if(!$.success)return s.onCascadeAccountComplete?.(r,!1,p(`Synth failed: ${$.error}`),t),h(new Error(`Synth failed for ${e.name}: ${p($.error)}`));const m=j(i==="platform"?R.PLATFORM:R.ACCOUNT),k=z(P,e.id,t),S=new H(g),{changed:b,currentHash:I}=await n.hashService.compareCascadeStack(D,m,k);if(!b&&o.options?.force!==!0&&await S.stackExists(m)){const f=await S.getStackOutputs(m);f.success||y.debug("cascadeHelpers","Failed to read outputs for skipped cascade account (non-critical)",{stackName:m,account:e.name});const F=M(f);return s.onLog?.(`${e.name}: no infrastructure changes \u2014 skipping deploy`,"info"),s.onCascadeAccountComplete?.(r,!0,void 0,t,F,!0),B({outputs:F,skipped:!0})}s.onCascadeAccountPhaseChange?.(r,"bootstrap",t);const A=await n.cdkService.runCdkBootstrap(w,x(s),C);if(!A.success)return s.onCascadeAccountComplete?.(r,!1,p(`Bootstrap failed: ${A.error}`),t),h(new Error(`Bootstrap failed for ${e.name}: ${p(A.error)}`));s.onCascadeAccountPhaseChange?.(r,"deploy",t);const O=await n.cdkService.runCdkDeploy(w,m,x(s),f=>s.onCascadeAccountResourceProgress?.(r,f,t),g,C);if(!O.success)return s.onCascadeAccountComplete?.(r,!1,p(O.error),t),h(new Error(p(O.error)));const E=await S.getStackOutputs(m);E.success||y.debug("cascadeHelpers","Failed to read cascade account stack outputs (non-critical)",{stackName:m,account:e.name});const N=M(E);return I!==void 0&&((await n.hashService.persistCascadeStack(k,m,I)).success||y.debug("cascadeHelpers","Failed to persist cascade hash state (non-critical)",{stackName:m,account:e.name})),s.onCascadeAccountComplete?.(r,!0,void 0,t,N,!1),B({outputs:N,skipped:!1})}async function Re(o,n,c,e){const i=new Map,s=o.awsProvider.getRegion(),a=await v(o.awsProvider,n.id,s,`fjall-ipam-read-${n.name}`,e);if(!a.success)return y.debug("organisationDeploy",`Cannot read Platform outputs: ${a.error.message}`),i;const t=new H(a.data.provider),r=j(R.PLATFORM),u=await t.getStackOutputs(r);if(!u.success)return y.debug("organisationDeploy",`Failed to read Platform stack outputs: ${u.error.message}`),i;const l=/^IpamPoolId(\d{12})(\w+)$/;for(const d of u.data){const g=d.OutputKey?.match(l);if(g&&d.OutputValue){const C=`${g[1]}-${g[2]}`;i.set(C,d.OutputValue)}}return i.size>0&&c.onLog?.(`Read ${i.size} IPAM pool ID(s) from Platform stack`,"info"),i}async function we(o,n){const c=o.getDomains();if(c.length===0)return{domainsDeployed:0,errors:[]};n.onCascadePhaseStart?.("domains");const e=c.filter(t=>t.type==="apex"),i=c.filter(t=>t.type==="delegated");let s=0;const a=[];for(const t of e){const r=await o.deployDomain(t.name,n);r.success?s++:a.push(`${t.name}: ${r.error.message}`)}if(i.length>0){const t=await Promise.allSettled(i.map(r=>o.deployDomain(r.name,n)));for(let r=0;r<t.length;r++){const u=t[r],l=i[r];if(!(!u||!l))if(u.status==="fulfilled")u.value.success?s++:a.push(`${l.name}: ${u.value.error.message}`);else{const d=u.reason instanceof Error?u.reason.message:String(u.reason);a.push(`${l.name}: ${d}`)}}}return n.onCascadePhaseComplete?.("domains"),{domainsDeployed:s,errors:a}}export{J as CASCADE_MAX_CONCURRENCY,Ce as buildAccountRegionPairs,Se as buildCascadeRoleArn,Q as buildRegionList,ge as cascadeHomeRegion,Z as cascadeOperationKey,ye as deployCascadeAccount,we as deployDomains,le as partitionAccounts,he as probeCascadeRoles,Re as readPlatformIpamPoolIds};
|
|
@@ -8,15 +8,30 @@ 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";
|
|
10
10
|
import type { DeployServices } from "./serviceFactory.js";
|
|
11
|
+
/**
|
|
12
|
+
* Whether a deploy targets a region other than the resolved primary. Shared
|
|
13
|
+
* predicate for the synth-time globals skip (buildParamsContext) AND the
|
|
14
|
+
* pre-deploy globals existence probe (deploySingleComponent) — the two MUST
|
|
15
|
+
* agree or a skip can pass unprobed. No primary configured (config never
|
|
16
|
+
* synced) ⇒ false: the deploy region anchors a first-ever deploy.
|
|
17
|
+
*/
|
|
18
|
+
export declare function targetsNonPrimaryRegion(region: string | undefined, primaryRegion: string | undefined): boolean;
|
|
11
19
|
/**
|
|
12
20
|
* Build the orgConfig/identity context fields shared by all deployment paths.
|
|
13
21
|
* CdkContextBuilder expects orgConfig as a JSON string and fjallOrgId as a string.
|
|
22
|
+
*
|
|
23
|
+
* Sole derivation site for `fjallAccountGlobalsConfigured`: a deploy skips the
|
|
24
|
+
* fixed-name account globals iff it targets a region other than the resolved
|
|
25
|
+
* primary (targetsNonPrimaryRegion above).
|
|
14
26
|
*/
|
|
15
27
|
export declare function buildParamsContext(params: {
|
|
16
28
|
orgConfig?: OrgConfig;
|
|
17
29
|
identity?: DeployIdentity;
|
|
18
30
|
skipOidc?: boolean;
|
|
19
|
-
|
|
31
|
+
/** Region this synth targets. Omitted ⇒ no globals flag. */
|
|
32
|
+
region?: string;
|
|
33
|
+
/** Resolved primary (home) region; callers pass their resolved anchor, never re-derive. */
|
|
34
|
+
primaryRegion?: string;
|
|
20
35
|
trailLifecycle?: TrailLifecycleState;
|
|
21
36
|
}): {
|
|
22
37
|
orgConfig?: string;
|
|
@@ -45,7 +60,7 @@ export interface CascadeAssumedRole {
|
|
|
45
60
|
* Callers handle failure (callbacks, logging, return type) — this helper
|
|
46
61
|
* only owns the assume + provider construction.
|
|
47
62
|
*/
|
|
48
|
-
export declare function assumeCascadeRole(awsProvider: SimpleAwsProvider, accountId: string, region: string, sessionName: string): Promise<Result<CascadeAssumedRole, Error>>;
|
|
63
|
+
export declare function assumeCascadeRole(awsProvider: SimpleAwsProvider, accountId: string, region: string, sessionName: string, signal?: AbortSignal): Promise<Result<CascadeAssumedRole, Error>>;
|
|
49
64
|
/**
|
|
50
65
|
* Run CDK synth and return failure with masked error if it fails.
|
|
51
66
|
* Calls `onError` on the callbacks so callers only need to handle
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{success as f,failure as
|
|
1
|
+
import{success as f,failure as i}from"@fjall/generator";import{getErrorMessage as m,maskSensitiveOutput as d,sleep as y}from"@fjall/util";import{logger as O}from"@fjall/util/logger";import{SimpleAwsProvider as R}from"../aws/SimpleAwsProvider.js";const S={account:"active",draining:"draining",org:"removed"};function w(e,r){return e!==void 0&&e!==""&&r!==void 0&&r!==""&&e!==r}function M(e){const r=w(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:S[e.trailLifecycle]}:{}}}function _(e){return r=>e.onOutput?.(r)}function L(e){return r=>e.onResourceProgress?.(r)}function D(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 E="OrganizationAccountAccessRole",l=5,$=5e3,b=3e4;function K(e){return`arn:aws:iam::${e}:role/${E}`}function T(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 P(e,r,t,n,o){if(!e.assumeRole)return i(new Error("AwsProvider does not support assumeRole"));const u=K(r),g=e.assumeRole.bind(e);let s;for(let c=0;c<=l;c++)try{s=await g(u,n);break}catch(a){const p=a instanceof Error?a.name:void 0;if(p==="AccessDenied"||p==="AccessDeniedException")return i(new Error(`Access denied assuming ${E} in account ${r}. The role may not exist or may not trust the management account.`));if(c<l){const A=Math.min($*2**c,b);if(O.debug("assumeCascadeRole",`Attempt ${c+1} failed for account ${r}, retrying in ${Math.round(A/1e3)}s`,{error:d(m(a))}),await T(A,o),o?.aborted)return i(new Error(`Aborted while retrying assume-role for account ${r}`));continue}return i(new Error(`Failed to assume role in account ${r} after ${l+1} attempts: ${d(m(a))}`))}if(!s)return i(new Error(`Failed to assume role in account ${r}`));const C=new R({accessKeyId:s.accessKeyId,secretAccessKey:s.secretAccessKey,sessionToken:s.sessionToken,region:t,accountId:r});return f({provider:C,credentials:{accessKeyId:s.accessKeyId,secretAccessKey:s.secretAccessKey,sessionToken:s.sessionToken}})}async function j(e,r,t,n){const o=await e.cdkService.runCdkSynth(r,u=>t.onCdkOutput?.(u,"synth"));if(!o.success){const u=new Error(d(`${n}: ${o.error}`));return t.onError?.(u),i(u)}return f(void 0)}async function B(e,r,t){t.onCDKBootstrap?.("bootstrapping");const n=await e.cdkService.runCdkBootstrap(r,_(t));if(!n.success){t.onCDKBootstrap?.("failed");const o=new Error(d(`Bootstrap failed: ${n.error}`));return t.onError?.(o),i(o)}return t.onCDKBootstrap?.("complete"),f(void 0)}export{P as assumeCascadeRole,B as bootstrapOrFail,K as buildCascadeRoleArn,M as buildParamsContext,D as collectStackOutputs,_ as forwardOutput,L as forwardResourceProgress,j as synthOrFail,w as targetsNonPrimaryRegion};
|
|
@@ -2,9 +2,10 @@ export { deploy } from "./deploy.js";
|
|
|
2
2
|
export { destroy } from "./destroy.js";
|
|
3
3
|
export { deployOrganisation } from "./organisationDeploy.js";
|
|
4
4
|
export { destroyOrganisation } from "./organisationDestroy.js";
|
|
5
|
-
export { cleanupFailedStack, isCleanableState, SAFE_CLEANUP_STATES } from "./stackCleanup.js";
|
|
5
|
+
export { cleanupFailedStack, emptyS3Bucket, preEmptyStackBuckets, formatQuarantineSuspectedMessage, formatRetainedBucketsMessage, isQuarantineDetail, isRetainedBucketsDetail, PRE_EMPTY_TAG_KEYS, isCleanableState, SAFE_CLEANUP_STATES } from "./stackCleanup.js";
|
|
6
|
+
export type { PreEmptyBucketsSummary, EmptyBucketOutcome } from "./stackCleanup.js";
|
|
6
7
|
export type { CascadeDestroyAccountResult } from "./cascadeDestroyHelpers.js";
|
|
7
|
-
export { partitionAccounts, buildRegionList, buildAccountRegionPairs } from "./cascadeHelpers.js";
|
|
8
|
+
export { partitionAccounts, buildRegionList, buildAccountRegionPairs, cascadeHomeRegion, cascadeOperationKey } from "./cascadeHelpers.js";
|
|
8
9
|
export type { AccountRegionPair } from "./cascadeHelpers.js";
|
|
9
10
|
export { projectScalarSummary, projectAccountRows } from "./cascadeSummary.js";
|
|
10
11
|
export type { CascadeOutcomeResult, CascadeMemberOutcome, CascadePlatformOutcome, CascadeLedger, CascadeAccountRow, CascadePlatformRow, CascadeAccountProjection } from "./cascadeSummary.js";
|
|
@@ -14,6 +15,10 @@ export { decideNextTransition, reconcileTrailMigration, ORG_TRAIL_BUCKET_OUTPUT_
|
|
|
14
15
|
export type { MemberTrailFacts, TrailMigrationTransition, TrailMigrationOutcome } from "./trailMigration/trailMigration.js";
|
|
15
16
|
export { decommissionMemberTrailStorage } from "./trailMigration/memberTrailCleanup.js";
|
|
16
17
|
export type { DecommissionClients, DecommissionInput, DecommissionOutcome } from "./trailMigration/memberTrailCleanup.js";
|
|
18
|
+
export { unlockBucket } from "./unlock/unlockBucket.js";
|
|
19
|
+
export type { UnlockBucketInput, UnlockBucketReport } from "./unlock/unlockBucket.js";
|
|
20
|
+
export { unlockQueue } from "./unlock/unlockQueue.js";
|
|
21
|
+
export type { UnlockQueueInput, UnlockQueueReport } from "./unlock/unlockQueue.js";
|
|
17
22
|
export { parseAccountsConfiguration, flattenAccountsToEnvironments, extractAllAccountNames, accountsConfigToOUTree, isStringArray, isAccountsConfig, isOuOnlyAccountBucket, OU_ONLY_ACCOUNT_BUCKETS } from "./accountsConfig.js";
|
|
18
23
|
export type { AccountsConfig } from "./accountsConfig.js";
|
|
19
24
|
export type { DockerProvider, DockerProgressCallback, DockerServiceConfig, DockerBuildParams, DockerBuildResult, ECRInitParams, ECRInitResult, TagImagesParams, TagImagesResult, TagByDigestParams } from "./dockerInterface.js";
|
|
@@ -21,6 +26,6 @@ export type { DomainDeployProvider, DomainConfig, DomainDeployResult } from "./d
|
|
|
21
26
|
export type { DeployServices } from "./serviceFactory.js";
|
|
22
27
|
export type { DetectionResult } from "./detectionPipeline.js";
|
|
23
28
|
export { runOpenNextBuild } from "./openNextBuild.js";
|
|
24
|
-
export { runOrganisationSetup } from "./organisationSetup.js";
|
|
29
|
+
export { runOrganisationSetup, ORG_SETUP_PHASES } from "./organisationSetup.js";
|
|
25
30
|
export type { OrgSetupPhase, OrgSetupCallbacks, OrgSetupConfig, OrgSetupResult } from "./organisationSetup.js";
|
|
26
31
|
export * from "./builders/index.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{deploy as
|
|
1
|
+
import{deploy as r}from"./deploy.js";import{destroy as n}from"./destroy.js";import{deployOrganisation as i}from"./organisationDeploy.js";import{destroyOrganisation as s}from"./organisationDestroy.js";import{cleanupFailedStack as p,emptyS3Bucket as m,preEmptyStackBuckets as T,formatQuarantineSuspectedMessage as l,formatRetainedBucketsMessage as f,isQuarantineDetail as A,isRetainedBucketsDetail as _,PRE_EMPTY_TAG_KEYS as d,isCleanableState as x,SAFE_CLEANUP_STATES as S}from"./stackCleanup.js";import{partitionAccounts as O,buildRegionList as g,buildAccountRegionPairs as R,cascadeHomeRegion as U,cascadeOperationKey as P}from"./cascadeHelpers.js";import{projectScalarSummary as y,projectAccountRows as B}from"./cascadeSummary.js";import{reconcileProviderAccounts as K,mergeReconciledProviderAccounts as N}from"./reconcileProviderAccounts.js";import{decideNextTransition as L,reconcileTrailMigration as M,ORG_TRAIL_BUCKET_OUTPUT_KEY as b,TRAIL_BUCKET_OUTPUT_KEY as v,TRAIL_KEY_ARN_OUTPUT_KEY as G}from"./trailMigration/trailMigration.js";import{decommissionMemberTrailStorage as Q}from"./trailMigration/memberTrailCleanup.js";import{unlockBucket as D}from"./unlock/unlockBucket.js";import{unlockQueue as H}from"./unlock/unlockQueue.js";import{parseAccountsConfiguration as h,flattenAccountsToEnvironments as q,extractAllAccountNames as z,accountsConfigToOUTree as J,isStringArray as V,isAccountsConfig as W,isOuOnlyAccountBucket as X,OU_ONLY_ACCOUNT_BUCKETS as Z}from"./accountsConfig.js";import{runOpenNextBuild as ee}from"./openNextBuild.js";import{runOrganisationSetup as re,ORG_SETUP_PHASES as te}from"./organisationSetup.js";export*from"./builders/index.js";export{te as ORG_SETUP_PHASES,b as ORG_TRAIL_BUCKET_OUTPUT_KEY,Z as OU_ONLY_ACCOUNT_BUCKETS,d as PRE_EMPTY_TAG_KEYS,S as SAFE_CLEANUP_STATES,v as TRAIL_BUCKET_OUTPUT_KEY,G as TRAIL_KEY_ARN_OUTPUT_KEY,J as accountsConfigToOUTree,R as buildAccountRegionPairs,g as buildRegionList,U as cascadeHomeRegion,P as cascadeOperationKey,p as cleanupFailedStack,L as decideNextTransition,Q as decommissionMemberTrailStorage,r as deploy,i as deployOrganisation,n as destroy,s as destroyOrganisation,m as emptyS3Bucket,z as extractAllAccountNames,q as flattenAccountsToEnvironments,l as formatQuarantineSuspectedMessage,f as formatRetainedBucketsMessage,W as isAccountsConfig,x as isCleanableState,X as isOuOnlyAccountBucket,A as isQuarantineDetail,_ as isRetainedBucketsDetail,V as isStringArray,N as mergeReconciledProviderAccounts,h as parseAccountsConfiguration,O as partitionAccounts,T as preEmptyStackBuckets,B as projectAccountRows,y as projectScalarSummary,K as reconcileProviderAccounts,M as reconcileTrailMigration,ee as runOpenNextBuild,re as runOrganisationSetup,D as unlockBucket,H as unlockQueue};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ProviderAccount } from "@fjall/util/config";
|
|
2
|
+
import type { DeployParams } from "../../types/params.js";
|
|
3
|
+
import type { OrgConfig } from "../../types/orgConfig.js";
|
|
4
|
+
import type { OrganisationOperation } from "../../types/operations.js";
|
|
5
|
+
import type { DeployServices } from "../serviceFactory.js";
|
|
6
|
+
/**
|
|
7
|
+
* Execute the cascade phases: platform deploy, IPAM pool read, domains, and
|
|
8
|
+
* the member-account fan-out, finishing with the ledger callbacks.
|
|
9
|
+
*
|
|
10
|
+
* Pushes per-target failures onto `cascadeErrors` and captured stack outputs
|
|
11
|
+
* onto `allCascadeOutputs` (both caller-owned). Returns whether any cascade
|
|
12
|
+
* target actually deployed (vs every target skipping as unchanged).
|
|
13
|
+
*/
|
|
14
|
+
export declare function executeCascade(params: DeployParams, services: DeployServices, operation: OrganisationOperation, args: {
|
|
15
|
+
providerAccounts: ProviderAccount[];
|
|
16
|
+
effectiveOrgConfig: OrgConfig | undefined;
|
|
17
|
+
totalSteps: number;
|
|
18
|
+
cascadeErrors: Array<{
|
|
19
|
+
accountId: string;
|
|
20
|
+
error: string;
|
|
21
|
+
}>;
|
|
22
|
+
allCascadeOutputs: Array<{
|
|
23
|
+
accountId: string;
|
|
24
|
+
outputs: Record<string, string>;
|
|
25
|
+
}>;
|
|
26
|
+
}): Promise<{
|
|
27
|
+
anyCascadeDeployHappened: boolean;
|
|
28
|
+
}>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{failure as U}from"@fjall/generator";import{regionSuffix as j}from"../../types/FjallState.js";import{partitionAccounts as W,deployCascadeAccount as v,readPlatformIpamPoolIds as $,deployDomains as X,buildRegionList as Y,buildAccountRegionPairs as z,cascadeHomeRegion as B,CASCADE_MAX_CONCURRENCY as G}from"../cascadeHelpers.js";import{projectScalarSummary as x}from"../cascadeSummary.js";import{getErrorMessage as J,maskSensitiveOutput as C,mapSettledWithConcurrency as K}from"@fjall/util";import{INFRA_STEPS as E}from"./infraSteps.js";async function re(d,D,N,T){const{callbacks:e}=d,{providerAccounts:q,effectiveOrgConfig:R,totalSteps:p,cascadeErrors:l,allCascadeOutputs:b}=T;let S=!1;e.onCascadeStart?.();const H=Date.now();let i=2,P=!1,g,w=!1;const h=[],M=t=>({members:h,...g!==void 0?{platform:g}:{},domainsDeployed:w,errors:l,totalDurationMs:t}),{platformAccount:a,memberAccounts:A}=W(q),O=B(d.orgConfig);if(a){const{id:t,name:o}=E.CASCADE_PLATFORM;e.onStepStart?.(t,o,i,p),e.onCascadePhaseStart?.("platform");let s;const I=Date.now();try{s=await v(d,D,N,a,"platform",e,{orgConfig:R,primaryRegion:O})}catch(c){const r=C(J(c));l.push({accountId:a.id,error:r}),e.onCascadePhaseComplete?.("platform"),e.onStepComplete?.(t,o,"error",i,p),s=U(new Error(r))}const k=Date.now()-I;if(s.success){P=!0;const c=s.data.skipped===!0;c||(S=!0),s.data.outputs&&b.push({accountId:a.id,outputs:s.data.outputs}),e.onCascadePhaseComplete?.("platform"),e.onStepComplete?.(t,o,c?"skipped":"completed",i,p),g={accountId:a.id,result:c?"skipped":"succeeded",durationMs:k}}else l.some(c=>c.accountId===a.id)||(l.push({accountId:a.id,error:C(s.error.message)}),e.onCascadePhaseComplete?.("platform"),e.onStepComplete?.(t,o,"error",i,p)),g={accountId:a.id,result:"failed",durationMs:k,error:C(s.error.message)};i++}let _=new Map;if(P&&a&&(_=await $(D,a,e,d.abortSignal)),d.domainProvider){const t=await X(d.domainProvider,e);w=t.domainsDeployed>0,w&&(S=!0);for(const o of t.errors)l.push({accountId:"domains",error:C(o)})}if(a!==void 0&&!P&&A.length>0){const{id:t,name:o}=E.CASCADE_ACCOUNTS;e.onStepStart?.(t,o,i,p),e.onStepComplete?.(t,o,"skipped",i,p),e.onLog?.("Skipping account cascade \u2014 platform deployment failed; platform is a prerequisite for member accounts.","warn")}else if(A.length>0){const{id:t,name:o}=E.CASCADE_ACCOUNTS;e.onStepStart?.(t,o,i,p),e.onCascadePhaseStart?.("accounts");const s=Y(d.orgConfig),I=z(A,s);(await K(I,G,async({account:r,region:y})=>{const m=_.get(`${r.id}-${j(y)}`),n=Date.now();return{result:await v(d,D,N,r,"account",e,{ipamPoolId:m,orgConfig:R,region:y,primaryRegion:O}),durationMs:Date.now()-n}})).forEach((r,y)=>{const m=I[y];if(!m)return;const n=m.account;if(r.status==="rejected"){const u=C(r.reason instanceof Error?r.reason.message:String(r.reason));h.push({accountId:n.id,accountName:n.name,region:m.region,result:"failed",durationMs:0,error:u}),l.push({accountId:n.id,error:u});return}const{result:f,durationMs:L}=r.value;if(f.success){const u=f.data.skipped===!0;u||(S=!0),f.data.outputs&&b.push({accountId:n.id,outputs:f.data.outputs}),h.push({accountId:n.id,accountName:n.name,region:m.region,result:u?"skipped":"succeeded",durationMs:L})}else{const u=C(f.error.message);h.push({accountId:n.id,accountName:n.name,region:m.region,result:"failed",durationMs:L,error:u}),l.push({accountId:n.id,error:u})}});const c=x(M(0));e.onCascadePhaseComplete?.("accounts"),e.onStepComplete?.(t,o,c.accountsFailed>0?"error":c.accountsSkipped===A.length?"skipped":"completed",i,p)}const F=M(Date.now()-H);return e.onCascadeComplete?.(x(F)),e.onCascadeLedger?.(F),{anyCascadeDeployHappened:S}}export{re as executeCascade};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type Result } from "@fjall/generator";
|
|
2
|
+
import type { DeployCallbacks } from "../../types/callbacks.js";
|
|
3
|
+
import type { DeployServices } from "../serviceFactory.js";
|
|
4
|
+
export declare const INFRA_STEPS: {
|
|
5
|
+
readonly CONNECT: {
|
|
6
|
+
readonly id: "connect";
|
|
7
|
+
readonly name: "Connect securely";
|
|
8
|
+
};
|
|
9
|
+
readonly PREPARE: {
|
|
10
|
+
readonly id: "prepare-environment";
|
|
11
|
+
readonly name: "Prepare environment";
|
|
12
|
+
};
|
|
13
|
+
readonly DEPLOY: {
|
|
14
|
+
readonly id: "deploy";
|
|
15
|
+
readonly name: "Deploy infrastructure";
|
|
16
|
+
};
|
|
17
|
+
readonly MONITORING: {
|
|
18
|
+
readonly id: "monitoring";
|
|
19
|
+
readonly name: "Enable monitoring";
|
|
20
|
+
};
|
|
21
|
+
readonly ORG_DEPLOY: {
|
|
22
|
+
readonly id: "organisation-deploy";
|
|
23
|
+
readonly name: "Deploying organisation";
|
|
24
|
+
};
|
|
25
|
+
readonly CASCADE_PLATFORM: {
|
|
26
|
+
readonly id: "cascade-platform";
|
|
27
|
+
readonly name: "Deploying platform";
|
|
28
|
+
};
|
|
29
|
+
readonly CASCADE_ACCOUNTS: {
|
|
30
|
+
readonly id: "cascade-accounts";
|
|
31
|
+
readonly name: "Deploying accounts";
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
export declare const INFRA_STEP_TOTAL = 4;
|
|
35
|
+
/** Complete the single-component PREPARE step as errored. */
|
|
36
|
+
export declare function failPrepareStep(callbacks: DeployCallbacks): void;
|
|
37
|
+
/** Mask a failure message, surface it via onError, and return the failure. */
|
|
38
|
+
export declare function maskAndFail(callbacks: DeployCallbacks, message: string): Result<never>;
|
|
39
|
+
/** Best-effort stack-output read: log at debug on failure, never fail. */
|
|
40
|
+
export declare function readStackOutputsBestEffort(services: DeployServices, callbacks: DeployCallbacks, stackName: string, failureLogMessage: string): Promise<Record<string, string> | undefined>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{failure as P}from"@fjall/generator";import{collectStackOutputs as R}from"../contextHelpers.js";import{maskSensitiveOutput as S}from"@fjall/util";import{INFRA_STEP_NAME as r,STEP_IDS as t,STEP_NAMES as A}from"../../types/stepDefinitions.js";const n={CONNECT:{id:t.CONNECT,name:r.CONNECT},PREPARE:{id:t.PREPARE_ENVIRONMENT,name:r.PREPARE},DEPLOY:{id:t.DEPLOY,name:r.DEPLOY},MONITORING:{id:t.MONITORING,name:r.MONITORING},ORG_DEPLOY:{id:t.ORG_DEPLOY,name:A.ORG_DEPLOY},CASCADE_PLATFORM:{id:t.CASCADE_PLATFORM,name:A.CASCADE_PLATFORM},CASCADE_ACCOUNTS:{id:t.CASCADE_ACCOUNTS,name:A.CASCADE_ACCOUNTS}},i=4;function u(E){E.onStepComplete?.(n.PREPARE.id,n.PREPARE.name,"error",1,i)}function p(E,o){const e=new Error(S(o));return E.onError?.(e),P(e)}async function c(E,o,e,C){const O=await E.cfnService.getStackOutputs(e);return O.success||o.onLog?.(C,"debug"),R(O)}export{n as INFRA_STEPS,i as INFRA_STEP_TOTAL,u as failPrepareStep,p as maskAndFail,c as readStackOutputsBestEffort};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type Result } from "@fjall/generator";
|
|
2
|
+
import type { DeployParams, DeployResult } from "../../types/params.js";
|
|
3
|
+
import type { OrganisationOperation } from "../../types/operations.js";
|
|
4
|
+
import type { DeployServices } from "../serviceFactory.js";
|
|
5
|
+
/**
|
|
6
|
+
* Full organisation deployment with cascade to platform + member accounts.
|
|
7
|
+
*/
|
|
8
|
+
export declare function deployOrgWithCascade(params: DeployParams, services: DeployServices, operation: OrganisationOperation, startTime: number): Promise<Result<DeployResult>>;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import{join as W}from"node:path";import{success as _,failure as Y}from"@fjall/generator";import{ORGANISATION_TYPES as B,getOrganisationStackName as K}from"../../types/operations.js";import{synthOrFail as q,bootstrapOrFail as z,forwardOutput as J,forwardResourceProgress as Q}from"../contextHelpers.js";import{partitionAccounts as U,probeCascadeRoles as V}from"../cascadeHelpers.js";import{accountTier as X,maskSensitiveOutput as l}from"@fjall/util";import{buildOrgContext as Z,resolveOrgDetails as v}from"./orgContext.js";import{INFRA_STEPS as L,maskAndFail as R,readStackOutputsBestEffort as x}from"./infraSteps.js";import{resolveCascadeAccounts as ee}from"./resolveCascadeAccounts.js";import{executeCascade as te}from"./cascadeExecution.js";import{reconcileOrgTrailOutputs as oe}from"./trailReconciliation.js";async function ge(i,t,O,H){const{callbacks:e,options:$}=i,{providerAccounts:u,effectiveOrgConfig:M,defaultRegion:j}=await ee(i,t),g=await v(t);if(!g.success)return R(e,g.error.message);const G=u.find(o=>X(o)==="organisation"),d=Z(i,t,O,"organisation",g.data,void 0,G?.trailLifecycle),m=$?.cascade!==!1,{platformAccount:p,memberAccounts:k}=U(u),P=m&&p!==void 0?1:0,D=m&&k.length>0?1:0,n=2+P+D,I=m?[...p!==void 0?[p]:[],...k]:[];if(I.length>0){const o=await V(t,I,e,i.abortSignal),r=p!==void 0?o.find(a=>a.accountId===p.id):void 0;if(r!==void 0)return R(e,`Pre-flight cascade role check failed for the platform account ${r.accountName} (${r.accountId}): ${r.error}`);for(const a of o)e.onProgress?.({type:"warning",message:l(`Pre-flight cascade role check failed for ${a.accountName} (${a.accountId}) \u2014 its cascade deploy is expected to fail: ${a.error}`)})}e.onCascadeAccountsReconciled?.({hasPlatformAccount:P>0,hasMemberAccounts:D>0});const{id:h,name:S}=L.PREPARE;e.onStepStart?.(h,S,0,n),e.onLog?.("Synthesising organisation infrastructure\u2026","info");const E=await q(t,d,e,"CDK synthesis failed");if(!E.success)return e.onStepComplete?.(h,S,"error",0,n),E;const N=await z(t,d,e);if(!N.success)return e.onStepComplete?.(h,S,"error",0,n),N;e.onStepComplete?.(h,S,"completed",0,n);const{id:C,name:A}=L.ORG_DEPLOY,s=K(B.ORGANISATION);let F=!0;const f=await t.hashService.getTemplateHashes(W(d.path,"cdk.out"));if(f.success){const o=await t.hashService.compareWithState(f.data,d.path);o.success?F=o.data.stackChanges.get(s)??!0:e.onLog?.(l(`Org root change detection failed \u2014 deploying to be safe: ${o.error.message}`),"warn")}else e.onLog?.(l(`Org root template hashing failed \u2014 deploying to be safe: ${f.error.message}`),"warn");const b=F||$?.force===!0||!await t.cfnService.stackExists(s);e.onOrgChangesDetected?.({hasOrgChanges:b});let w;if(b){e.onStepStart?.(C,A,1,n);const o=await t.cdkService.runCdkDeploy(d,s,J(e),Q(e),t.awsProvider);if(!o.success)return e.onStepComplete?.(C,A,"error",1,n),R(e,o.error);w=await x(t,e,s,"Failed to read org stack outputs (non-critical)");const r=f.success?f.data.get(s):void 0;if(r!==void 0){const a=await t.hashService.updateStateAfterDeploy(d.path,new Map([[s,r]]));a.success||e.onLog?.(`Warning: failed to update state file \u2014 next deploy may re-deploy the org root: ${l(a.error.message)}`,"warn")}e.onStepComplete?.(C,A,"completed",1,n)}else e.onLog?.("Organisation root: no infrastructure changes \u2014 skipping deploy","info"),w=await x(t,e,s,"Failed to read org stack outputs (non-critical)");const c=[],y=[];let T=b;if(m&&u.length>0){const{anyCascadeDeployHappened:o}=await te(i,t,O,{providerAccounts:u,effectiveOrgConfig:M,totalSteps:n,cascadeErrors:c,allCascadeOutputs:y});if(o&&(T=!0),await oe(i,t,{orgOutputs:w,allCascadeOutputs:y,orgDetails:g.data,providerAccounts:u,defaultRegion:j}),c.length>0){const r=c.map(a=>` ${a.accountId}: ${a.error}`).join(`
|
|
2
|
+
`);e.onLog?.(l(`Cascade failed for ${c.length} target(s):
|
|
3
|
+
${r}`),"warn")}}if(c.length>0){const o=c.map(a=>l(`${a.accountId}: ${a.error}`)).join(`
|
|
4
|
+
`),r=new Error(`Organisation root deployed, but the cascade failed for ${c.length} target(s):
|
|
5
|
+
${o}`);return e.onError?.(r),Y(r)}return _({target:O.target,deploymentType:"organisation",outputs:w,...y.length>0?{cascadeOutputs:y}:{},...T?{}:{noChanges:!0},durationMs:Date.now()-H})}export{ge as deployOrgWithCascade};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type Result } from "@fjall/generator";
|
|
2
|
+
import type { ProviderAccount } from "@fjall/util/config";
|
|
3
|
+
import type { DeployParams } from "../../types/params.js";
|
|
4
|
+
import type { OrganisationOperation } from "../../types/operations.js";
|
|
5
|
+
import type { DeployServices } from "../serviceFactory.js";
|
|
6
|
+
export interface OrgDetailsForSynth {
|
|
7
|
+
orgId: string;
|
|
8
|
+
rootId: string;
|
|
9
|
+
managementAccountId: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function buildOrgContext(params: DeployParams, services: DeployServices, operation: OrganisationOperation, deployType: "organisation" | "platform" | "account", orgDetails: OrgDetailsForSynth, accountName?: string, trailLifecycle?: ProviderAccount["trailLifecycle"]): import("../../types/deployment/DeploymentTypes.js").DeploymentContext;
|
|
12
|
+
export declare function resolveOrgDetails(services: DeployServices): Promise<Result<OrgDetailsForSynth>>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{success as a,failure as c}from"@fjall/generator";import{OrganizationsClient as s}from"@aws-sdk/client-organizations";import{CdkContextBuilder as u}from"../../services/supporting/CdkContextBuilder.js";import{stubCallerIdentity as m}from"../../types/deployment/index.js";import{ensureOrganisationExists as f}from"../../aws/organisations/organisation.js";import{buildParamsContext as l}from"../contextHelpers.js";function b(t,n,o,i,r,d,g){const e=n.awsProvider.getRegion();return u.buildDeploymentContext({deployType:i,target:o.target,path:o.path,region:e,accountName:d,callerIdentity:m(n.awsProvider.getAccountId()),orgId:r.orgId,rootId:r.rootId,managementAccountId:r.managementAccountId,...l({orgConfig:t.orgConfig,identity:t.identity,skipOidc:t.options?.skipOidc,...i!=="organisation"?{region:e,primaryRegion:t.orgConfig?.primaryRegion}:{},trailLifecycle:g})},{verbose:t.options?.verbose,infraOnly:t.options?.infraOnly},t.orgConfig)}async function v(t){const n=t.awsProvider.getClient(s),o=await f(n);return o.success?a({orgId:o.data.orgId,rootId:o.data.rootId,managementAccountId:o.data.managementAccountId}):c(o.error)}export{b as buildOrgContext,v as resolveOrgDetails};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ProviderAccount } from "@fjall/util/config";
|
|
2
|
+
import type { DeployParams } from "../../types/params.js";
|
|
3
|
+
import type { OrgConfig } from "../../types/orgConfig.js";
|
|
4
|
+
import type { DeployServices } from "../serviceFactory.js";
|
|
5
|
+
/**
|
|
6
|
+
* Resolve the deployable provider-account set for an organisation cascade:
|
|
7
|
+
* reconcile from AWS Organizations when the orgConfig carries no deployable
|
|
8
|
+
* accounts, stamp a home region onto every account, and compose the
|
|
9
|
+
* effective orgConfig the cascade synth context needs.
|
|
10
|
+
*/
|
|
11
|
+
export declare function resolveCascadeAccounts(params: DeployParams, services: DeployServices): Promise<{
|
|
12
|
+
providerAccounts: ProviderAccount[];
|
|
13
|
+
effectiveOrgConfig: OrgConfig | undefined;
|
|
14
|
+
defaultRegion: string;
|
|
15
|
+
}>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cascadeHomeRegion as u}from"../cascadeHelpers.js";import{reconcileProviderAccounts as l,mergeReconciledProviderAccounts as d}from"../reconcileProviderAccounts.js";import{accountTier as f,maskSensitiveOutput as s}from"@fjall/util";async function v(e,g){const{callbacks:c}=e;let o=e.orgConfig?.providerAccounts??[];if(o.length===0||o.every(n=>f(n)==="organisation")){const n=await l(g,e.workingDirectory);if(n.success){const{providerAccounts:i,missingAccountNames:t}=n.data;i.length>0&&(o=d(e.orgConfig,i).providerAccounts,c.onLog?.(`Reconciled ${i.length} account(s) from AWS Organizations`,"info")),t.length>0&&(c.onCascadeMissingAccounts?.(t),c.onProgress?.({type:"warning",message:s(`Accounts declared in ACCOUNTS but not yet in AWS Organizations (cascade will skip): ${t.join(", ")}`)}))}else c.onProgress?.({type:"warning",message:s(`Could not reconcile accounts from AWS Organizations \u2014 cascade may skip accounts: ${n.error.message}`)})}const r=u(e.orgConfig);o=o.map(n=>n.region!==void 0?n:{...n,region:r});const a=e.orgConfig!==void 0?{...e.orgConfig,providerAccounts:o}:o.length>0?{providerAccounts:o}:void 0;return{providerAccounts:o,effectiveOrgConfig:a,defaultRegion:r}}export{v as resolveCascadeAccounts};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type Result } from "@fjall/generator";
|
|
2
|
+
import type { DeployParams, DeployResult } from "../../types/params.js";
|
|
3
|
+
import type { OrganisationOperation } from "../../types/operations.js";
|
|
4
|
+
import type { DeployServices } from "../serviceFactory.js";
|
|
5
|
+
/**
|
|
6
|
+
* Deploy a single organisation component (platform or account).
|
|
7
|
+
*
|
|
8
|
+
* Emits 4 named steps from INFRASTRUCTURE_STEP_NAMES: Connect securely →
|
|
9
|
+
* Prepare environment → Deploy infrastructure → Enable monitoring.
|
|
10
|
+
*/
|
|
11
|
+
export declare function deploySingleComponent(params: DeployParams, services: DeployServices, operation: OrganisationOperation, deployType: "platform" | "account", startTime: number): Promise<Result<DeployResult>>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{success as A}from"@fjall/generator";import{BackupClient as N}from"@aws-sdk/client-backup";import{IAMClient as b}from"@aws-sdk/client-iam";import{getOrganisationStackName as w}from"../../types/operations.js";import{accountHasDisasterRecovery as D,describeBackupVaultExists as k}from"../../aws/organisations/backup.js";import{describeAccountGlobalsExist as y,buildMissingAccountGlobalsAdvisory as I}from"../../aws/organisations/accountGlobals.js";import{targetsNonPrimaryRegion as L,synthOrFail as T,bootstrapOrFail as h,forwardOutput as M,forwardResourceProgress as v}from"../contextHelpers.js";import{accountTier as F}from"@fjall/util";import{buildOrgContext as G,resolveOrgDetails as Y}from"./orgContext.js";import{INFRA_STEPS as o,INFRA_STEP_TOTAL as i,failPrepareStep as s,maskAndFail as u,readStackOutputsBestEffort as x}from"./infraSteps.js";async function W(n,r,a,c,E){const{callbacks:t}=n;t.onStepComplete?.(o.CONNECT.id,o.CONNECT.name,"completed",0,i),t.onStepStart?.(o.PREPARE.id,o.PREPARE.name,1,i);const p=r.awsProvider.getRegion(),l=n.orgConfig?.primaryRegion;if(l!==void 0&&L(p,l)){const e=await y(r.awsProvider.getClient(b),n.abortSignal);if(!e.success)return s(t),u(t,e.error.message);if(!e.data)return s(t),u(t,I({target:a.target,deployRegion:p,primaryRegion:l,...n.orgConfig!==void 0?{providerAccounts:n.orgConfig.providerAccounts}:{}}))}const g=await Y(r);if(!g.success)return s(t),u(t,g.error.message);const f=c==="account"?n.orgConfig?.providerAccounts.find(e=>e.name===a.target):n.orgConfig?.providerAccounts.find(e=>F(e)==="platform"),d=G(n,r,a,c,g.data,c==="account"?a.target:void 0,f?.trailLifecycle),m=n.orgConfig?.disasterRecoveryRegion;if(c==="account"&&(m!==void 0&&m!=="")&&(f===void 0||D(f.environment,m))){const e=await k(r.awsProvider.getClient(N));if(!e.success)return s(t),u(t,e.error.message);d.fjallAdoptBackupVault=e.data}t.onLog?.(`Synthesising ${c} infrastructure\u2026`,"info");const R=await T(r,d,t,"CDK synthesis failed");if(!R.success)return s(t),R;const P=await h(r,d,t);if(!P.success)return s(t),P;t.onStepComplete?.(o.PREPARE.id,o.PREPARE.name,"completed",1,i);const C=w(a.type);t.onStepStart?.(o.DEPLOY.id,o.DEPLOY.name,2,i);const O=await r.cdkService.runCdkDeploy(d,C,M(t),v(t),r.awsProvider);if(!O.success)return t.onStepComplete?.(o.DEPLOY.id,o.DEPLOY.name,"error",2,i),u(t,O.error);t.onStepComplete?.(o.DEPLOY.id,o.DEPLOY.name,"completed",2,i);const S=await x(r,t,C,"Failed to read stack outputs (non-critical)");return t.onStepStart?.(o.MONITORING.id,o.MONITORING.name,3,i),t.onStepComplete?.(o.MONITORING.id,o.MONITORING.name,"completed",3,i),A({target:a.target,deploymentType:"organisation",outputs:S,durationMs:Date.now()-E})}export{W as deploySingleComponent};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ProviderAccount } from "@fjall/util/config";
|
|
2
|
+
import type { DeployParams } from "../../types/params.js";
|
|
3
|
+
import type { DeployServices } from "../serviceFactory.js";
|
|
4
|
+
import type { OrgDetailsForSynth } from "./orgContext.js";
|
|
5
|
+
/**
|
|
6
|
+
* Org-trail migration reconciliation: two-invocation convergence — this
|
|
7
|
+
* run's cheap probes advance lifecycle states (persisted by the consumer
|
|
8
|
+
* via onTrailLifecycleChanged); the NEXT deploy applies them. Gated on the
|
|
9
|
+
* org root actually owning an organisation trail; failures warn and never
|
|
10
|
+
* fail the deploy.
|
|
11
|
+
*/
|
|
12
|
+
export declare function reconcileOrgTrailOutputs(params: DeployParams, services: DeployServices, args: {
|
|
13
|
+
orgOutputs: Record<string, string> | undefined;
|
|
14
|
+
allCascadeOutputs: Array<{
|
|
15
|
+
accountId: string;
|
|
16
|
+
outputs: Record<string, string>;
|
|
17
|
+
}>;
|
|
18
|
+
orgDetails: OrgDetailsForSynth;
|
|
19
|
+
providerAccounts: ProviderAccount[];
|
|
20
|
+
defaultRegion: string;
|
|
21
|
+
}): Promise<void>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{success as g,failure as O}from"@fjall/generator";import{S3Client as c}from"@aws-sdk/client-s3";import{CloudTrailClient as m}from"@aws-sdk/client-cloudtrail";import{KMSClient as f}from"@aws-sdk/client-kms";import{assumeCascadeRole as w}from"../contextHelpers.js";import{reconcileTrailMigration as I,ORG_TRAIL_BUCKET_OUTPUT_KEY as S,TRAIL_BUCKET_OUTPUT_KEY as P}from"../trailMigration/trailMigration.js";import{getErrorMessage as R,maskSensitiveOutput as _}from"@fjall/util";async function K(n,e,s){const{callbacks:u}=n,{orgOutputs:o,allCascadeOutputs:C,orgDetails:a,providerAccounts:p}=s,l=o?.[S];if(l!==void 0&&l!==""){const r=new Map;for(const t of C)(r.get(t.accountId)===void 0||P in t.outputs)&&r.set(t.accountId,t.outputs);o!==void 0&&r.set(a.managementAccountId,o);const T=async(t,d)=>{if(t.id===a.managementAccountId)return g({cloudTrailClient:e.awsProvider.getClient(m),s3Client:e.awsProvider.getClient(c),kmsClient:e.awsProvider.getClient(f)});const i=await w(e.awsProvider,t.id,d,`fjall-trail-migration-${t.name}`,n.abortSignal);return i.success?g({cloudTrailClient:i.data.provider.getClient(m),s3Client:i.data.provider.getClient(c),kmsClient:i.data.provider.getClient(f)}):O(i.error)};try{await I({managementS3Client:e.awsProvider.getClient(c),getMemberClients:T},{orgId:a.orgId,orgTrailBucketName:l,accounts:p,cascadeOutputs:r,defaultRegion:s.defaultRegion,...n.abortSignal!==void 0?{abortSignal:n.abortSignal}:{}},u)}catch(t){u.onLog?.(_(`Org-trail migration reconciliation failed (non-fatal): ${R(t)}`),"warn")}}}export{K as reconcileOrgTrailOutputs};
|
|
@@ -2,6 +2,7 @@ import { type Result } from "@fjall/generator";
|
|
|
2
2
|
import type { DeployParams, DeployResult } from "../types/params.js";
|
|
3
3
|
import type { OrganisationOperation } from "../types/operations.js";
|
|
4
4
|
import type { DeployServices } from "./serviceFactory.js";
|
|
5
|
+
export type { OrgDetailsForSynth } from "./organisationDeploy/orgContext.js";
|
|
5
6
|
/**
|
|
6
7
|
* Organisation deployment orchestration.
|
|
7
8
|
*
|
|
@@ -14,8 +15,3 @@ import type { DeployServices } from "./serviceFactory.js";
|
|
|
14
15
|
* responsibility. deploy-core receives credentials and deploys.
|
|
15
16
|
*/
|
|
16
17
|
export declare function deployOrganisation(params: DeployParams, services: DeployServices, operation: OrganisationOperation): Promise<Result<DeployResult>>;
|
|
17
|
-
export interface OrgDetailsForSynth {
|
|
18
|
-
orgId: string;
|
|
19
|
-
rootId: string;
|
|
20
|
-
managementAccountId: string;
|
|
21
|
-
}
|
|
@@ -1,5 +1 @@
|
|
|
1
|
-
import{join as _t}from"node:path";import{success as W,failure as x}from"@fjall/generator";import{OrganizationsClient as $t}from"@aws-sdk/client-organizations";import{BackupClient as Ft}from"@aws-sdk/client-backup";import{S3Client as st}from"@aws-sdk/client-s3";import{CloudTrailClient as Pt}from"@aws-sdk/client-cloudtrail";import{KMSClient as Rt}from"@aws-sdk/client-kms";import{ORGANISATION_TYPES as Z,getOrganisationStackName as ht}from"../types/operations.js";import{CdkContextBuilder as xt}from"../services/supporting/CdkContextBuilder.js";import{stubCallerIdentity as Gt}from"../types/deployment/index.js";import{ensureOrganisationExists as Yt}from"../aws/organisations/organisation.js";import{DEFAULT_REGION as Ut}from"../aws/utils/regions.js";import{accountHasDisasterRecovery as vt,describeBackupVaultExists as Bt}from"../aws/organisations/backup.js";import{assumeCascadeRole as jt,buildParamsContext as Wt,collectStackOutputs as Kt,synthOrFail as Et,bootstrapOrFail as It,forwardOutput as wt,forwardResourceProgress as yt}from"./contextHelpers.js";import{reconcileTrailMigration as Ht,ORG_TRAIL_BUCKET_OUTPUT_KEY as Vt,TRAIL_BUCKET_OUTPUT_KEY as zt}from"./trailMigration/trailMigration.js";import{partitionAccounts as Nt,deployCascadeAccount as Dt,probeCascadeRoles as qt,readPlatformIpamPoolIds as Xt,deployDomains as Jt,buildRegionList as Qt,buildAccountRegionPairs as Zt,CASCADE_MAX_CONCURRENCY as te}from"./cascadeHelpers.js";import{projectScalarSummary as Tt}from"./cascadeSummary.js";import{reconcileProviderAccounts as ee,mergeReconciledProviderAccounts as oe}from"./reconcileProviderAccounts.js";import{accountTier as dt,getErrorMessage as kt,maskSensitiveOutput as g,mapSettledWithConcurrency as ne}from"@fjall/util";import{INFRA_STEP_NAME as tt,STEP_IDS as M,STEP_NAMES as ut}from"../types/stepDefinitions.js";async function ye(o,e,n){const f=Date.now();switch(n.type){case Z.ORGANISATION:return re(o,e,n,f);case Z.PLATFORM:return Mt(o,e,n,"platform",f);case Z.ACCOUNT:return Mt(o,e,n,"account",f);default:{const t=n.type;return x(new Error(`Unsupported organisation type: ${String(t)}`))}}}function bt(o,e,n,f,t,r,u){return xt.buildDeploymentContext({deployType:f,target:n.target,path:n.path,region:e.awsProvider.getRegion(),accountName:r,callerIdentity:Gt(e.awsProvider.getAccountId()),orgId:t.orgId,rootId:t.rootId,managementAccountId:t.managementAccountId,...Wt({orgConfig:o.orgConfig,identity:o.identity,skipOidc:o.options?.skipOidc,trailLifecycle:u})},{verbose:o.options?.verbose,infraOnly:o.options?.infraOnly},o.orgConfig)}async function Lt(o){const e=o.awsProvider.getClient($t),n=await Yt(e);return n.success?W({orgId:n.data.orgId,rootId:n.data.rootId,managementAccountId:n.data.managementAccountId}):x(n.error)}const c={CONNECT:{id:M.CONNECT,name:tt.CONNECT},PREPARE:{id:M.PREPARE_ENVIRONMENT,name:tt.PREPARE},DEPLOY:{id:M.DEPLOY,name:tt.DEPLOY},MONITORING:{id:M.MONITORING,name:tt.MONITORING},ORG_DEPLOY:{id:M.ORG_DEPLOY,name:ut.ORG_DEPLOY},CASCADE_PLATFORM:{id:M.CASCADE_PLATFORM,name:ut.CASCADE_PLATFORM},CASCADE_ACCOUNTS:{id:M.CASCADE_ACCOUNTS,name:ut.CASCADE_ACCOUNTS}},h=4;function et(o){o.onStepComplete?.(c.PREPARE.id,c.PREPARE.name,"error",1,h)}function G(o,e){const n=new Error(g(e));return o.onError?.(n),x(n)}async function lt(o,e,n,f){const t=await o.cfnService.getStackOutputs(n);return t.success||e.onLog?.(f,"debug"),Kt(t)}async function Mt(o,e,n,f,t){const{callbacks:r}=o;r.onStepComplete?.(c.CONNECT.id,c.CONNECT.name,"completed",0,h),r.onStepStart?.(c.PREPARE.id,c.PREPARE.name,1,h);const u=await Lt(e);if(!u.success)return et(r),G(r,u.error.message);const K=f==="account"?o.orgConfig?.providerAccounts.find(P=>P.name===n.target):o.orgConfig?.providerAccounts.find(P=>dt(P)==="platform"),E=bt(o,e,n,f,u.data,f==="account"?n.target:void 0,K?.trailLifecycle),_=o.orgConfig?.disasterRecoveryRegion;if(f==="account"&&(_!==void 0&&_!=="")&&(K===void 0||vt(K.environment,_))){const P=await Bt(e.awsProvider.getClient(Ft));if(!P.success)return et(r),G(r,P.error.message);E.fjallAdoptBackupVault=P.data}r.onLog?.(`Synthesising ${f} infrastructure\u2026`,"info");const R=await Et(e,E,r,"CDK synthesis failed");if(!R.success)return et(r),R;const T=await It(e,E,r);if(!T.success)return et(r),T;r.onStepComplete?.(c.PREPARE.id,c.PREPARE.name,"completed",1,h);const I=ht(n.type);r.onStepStart?.(c.DEPLOY.id,c.DEPLOY.name,2,h);const Y=await e.cdkService.runCdkDeploy(E,I,wt(r),yt(r),e.awsProvider);if(!Y.success)return r.onStepComplete?.(c.DEPLOY.id,c.DEPLOY.name,"error",2,h),G(r,Y.error);r.onStepComplete?.(c.DEPLOY.id,c.DEPLOY.name,"completed",2,h);const H=await lt(e,r,I,"Failed to read stack outputs (non-critical)");return r.onStepStart?.(c.MONITORING.id,c.MONITORING.name,3,h),r.onStepComplete?.(c.MONITORING.id,c.MONITORING.name,"completed",3,h),W({target:n.target,deploymentType:"organisation",outputs:H,durationMs:Date.now()-t})}async function re(o,e,n,f){const{callbacks:t,options:r}=o;let u=o.orgConfig?.providerAccounts??[];if(u.length===0||u.every(a=>dt(a)==="organisation")){const a=await ee(e,o.workingDirectory);if(a.success){const{providerAccounts:i,missingAccountNames:l}=a.data;i.length>0&&(u=oe(o.orgConfig,i).providerAccounts,t.onLog?.(`Reconciled ${i.length} account(s) from AWS Organizations`,"info")),l.length>0&&(t.onCascadeMissingAccounts?.(l),t.onProgress?.({type:"warning",message:g(`Accounts declared in ACCOUNTS but not yet in AWS Organizations (cascade will skip): ${l.join(", ")}`)}))}else t.onProgress?.({type:"warning",message:g(`Could not reconcile accounts from AWS Organizations \u2014 cascade may skip accounts: ${a.error.message}`)})}const E=o.orgConfig?.primaryRegion??Ut;u=u.map(a=>a.region!==void 0?a:{...a,region:E});const _=o.orgConfig!==void 0?{...o.orgConfig,providerAccounts:u}:u.length>0?{providerAccounts:u}:void 0,D=await Lt(e);if(!D.success)return G(t,D.error.message);const pt=u.find(a=>dt(a)==="organisation"),R=bt(o,e,n,"organisation",D.data,void 0,pt?.trailLifecycle),T=r?.cascade!==!1,{platformAccount:I,memberAccounts:Y}=Nt(u),H=T&&I!==void 0?1:0,P=T&&Y.length>0?1:0,m=2+H+P,gt=T?[...I!==void 0?[I]:[],...Y]:[];if(gt.length>0){const a=await qt(e,gt,t),i=I!==void 0?a.find(l=>l.accountId===I.id):void 0;if(i!==void 0)return G(t,`Pre-flight cascade role check failed for the platform account ${i.accountName} (${i.accountId}): ${i.error}`);for(const l of a)t.onProgress?.({type:"warning",message:g(`Pre-flight cascade role check failed for ${l.accountName} (${l.accountId}) \u2014 its cascade deploy is expected to fail: ${l.error}`)})}t.onCascadeAccountsReconciled?.({hasPlatformAccount:H>0,hasMemberAccounts:P>0});const{id:V,name:z}=c.PREPARE;t.onStepStart?.(V,z,0,m),t.onLog?.("Synthesising organisation infrastructure\u2026","info");const ft=await Et(e,R,t,"CDK synthesis failed");if(!ft.success)return t.onStepComplete?.(V,z,"error",0,m),ft;const mt=await It(e,R,t);if(!mt.success)return t.onStepComplete?.(V,z,"error",0,m),mt;t.onStepComplete?.(V,z,"completed",0,m);const{id:ot,name:nt}=c.ORG_DEPLOY,k=ht(Z.ORGANISATION);let Ct=!0;const U=await e.hashService.getTemplateHashes(_t(R.path,"cdk.out"));if(U.success){const a=await e.hashService.compareWithState(U.data,R.path);a.success?Ct=a.data.stackChanges.get(k)??!0:t.onLog?.(g(`Org root change detection failed \u2014 deploying to be safe: ${a.error.message}`),"warn")}else t.onLog?.(g(`Org root template hashing failed \u2014 deploying to be safe: ${U.error.message}`),"warn");const rt=Ct||r?.force===!0||!await e.cfnService.stackExists(k);t.onOrgChangesDetected?.({hasOrgChanges:rt});let $;if(rt){t.onStepStart?.(ot,nt,1,m);const a=await e.cdkService.runCdkDeploy(R,k,wt(t),yt(t),e.awsProvider);if(!a.success)return t.onStepComplete?.(ot,nt,"error",1,m),G(t,a.error);$=await lt(e,t,k,"Failed to read org stack outputs (non-critical)");const i=U.success?U.data.get(k):void 0;if(i!==void 0){const l=await e.hashService.updateStateAfterDeploy(R.path,new Map([[k,i]]));l.success||t.onLog?.(`Warning: failed to update state file \u2014 next deploy may re-deploy the org root: ${g(l.error.message)}`,"warn")}t.onStepComplete?.(ot,nt,"completed",1,m)}else t.onLog?.("Organisation root: no infrastructure changes \u2014 skipping deploy","info"),$=await lt(e,t,k,"Failed to read org stack outputs (non-critical)");const C=[],v=[];let q=rt;if(T&&u.length>0){t.onCascadeStart?.();const a=Date.now();let i=2,l=!1,X,at=!1;const J=[],St=s=>({members:J,...X!==void 0?{platform:X}:{},domainsDeployed:at,errors:C,totalDurationMs:s}),{platformAccount:S,memberAccounts:Q}=Nt(u);if(S){const{id:s,name:p}=c.CASCADE_PLATFORM;t.onStepStart?.(s,p,i,m),t.onCascadePhaseStart?.("platform");let d;const F=Date.now();try{d=await Dt(o,e,n,S,"platform",t,{orgConfig:_})}catch(w){const B=g(kt(w));C.push({accountId:S.id,error:B}),t.onCascadePhaseComplete?.("platform"),t.onStepComplete?.(s,p,"error",i,m),d=x(new Error(B))}const A=Date.now()-F;if(d.success){l=!0;const w=d.data.skipped===!0;w||(q=!0),d.data.outputs&&v.push({accountId:S.id,outputs:d.data.outputs}),t.onCascadePhaseComplete?.("platform"),t.onStepComplete?.(s,p,w?"skipped":"completed",i,m),X={accountId:S.id,result:w?"skipped":"succeeded",durationMs:A}}else C.some(w=>w.accountId===S.id)||(C.push({accountId:S.id,error:g(d.error.message)}),t.onCascadePhaseComplete?.("platform"),t.onStepComplete?.(s,p,"error",i,m)),X={accountId:S.id,result:"failed",durationMs:A,error:g(d.error.message)};i++}let At=new Map;if(l&&S&&(At=await Xt(e,S,t)),o.domainProvider){const s=await Jt(o.domainProvider,t);at=s.domainsDeployed>0,at&&(q=!0);for(const p of s.errors)C.push({accountId:"domains",error:g(p)})}if(S!==void 0&&!l&&Q.length>0){const{id:s,name:p}=c.CASCADE_ACCOUNTS;t.onStepStart?.(s,p,i,m),t.onStepComplete?.(s,p,"skipped",i,m),t.onLog?.("Skipping account cascade \u2014 platform deployment failed; platform is a prerequisite for member accounts.","warn")}else if(Q.length>0){const{id:s,name:p}=c.CASCADE_ACCOUNTS;t.onStepStart?.(s,p,i,m),t.onCascadePhaseStart?.("accounts");const d=Qt(o.orgConfig),F=d[0]??E,A=Zt(Q,d);(await ne(A,te,async({account:y,region:j})=>{const b=j.replace(/-/g,""),O=At.get(`${y.id}-${b}`),L=Date.now();return{result:await Dt(o,e,n,y,"account",t,{ipamPoolId:O,orgConfig:_,region:j,skipAccountGlobals:j!==F}),durationMs:Date.now()-L}})).forEach((y,j)=>{const b=A[j];if(!b)return;const O=b.account;if(y.status==="rejected"){const N=g(y.reason instanceof Error?y.reason.message:String(y.reason));J.push({accountId:O.id,accountName:O.name,region:b.region,result:"failed",durationMs:0,error:N}),C.push({accountId:O.id,error:N});return}const{result:L,durationMs:ct}=y.value;if(L.success){const N=L.data.skipped===!0;N||(q=!0),L.data.outputs&&v.push({accountId:O.id,outputs:L.data.outputs}),J.push({accountId:O.id,accountName:O.name,region:b.region,result:N?"skipped":"succeeded",durationMs:ct})}else{const N=g(L.error.message);J.push({accountId:O.id,accountName:O.name,region:b.region,result:"failed",durationMs:ct,error:N}),C.push({accountId:O.id,error:N})}});const B=Tt(St(0));t.onCascadePhaseComplete?.("accounts"),t.onStepComplete?.(s,p,B.accountsFailed>0?"error":B.accountsSkipped===Q.length?"skipped":"completed",i,m)}const Ot=St(Date.now()-a);t.onCascadeComplete?.(Tt(Ot)),t.onCascadeLedger?.(Ot);const it=$?.[Vt];if(it!==void 0&&it!==""){const s=new Map;for(const d of v)(s.get(d.accountId)===void 0||zt in d.outputs)&&s.set(d.accountId,d.outputs);$!==void 0&&s.set(D.data.managementAccountId,$);const p=async(d,F)=>{if(d.id===D.data.managementAccountId)return W({cloudTrailClient:e.awsProvider.getClient(Pt),s3Client:e.awsProvider.getClient(st),kmsClient:e.awsProvider.getClient(Rt)});const A=await jt(e.awsProvider,d.id,F,`fjall-trail-migration-${d.name}`);return A.success?W({cloudTrailClient:A.data.provider.getClient(Pt),s3Client:A.data.provider.getClient(st),kmsClient:A.data.provider.getClient(Rt)}):x(A.error)};try{await Ht({managementS3Client:e.awsProvider.getClient(st),getMemberClients:p},{orgId:D.data.orgId,orgTrailBucketName:it,accounts:u,cascadeOutputs:s,defaultRegion:E,...o.abortSignal!==void 0?{abortSignal:o.abortSignal}:{}},t)}catch(d){t.onLog?.(g(`Org-trail migration reconciliation failed (non-fatal): ${kt(d)}`),"warn")}}if(C.length>0){const s=C.map(p=>` ${p.accountId}: ${p.error}`).join(`
|
|
2
|
-
`);t.onLog?.(g(`Cascade failed for ${C.length} target(s):
|
|
3
|
-
${s}`),"warn")}}if(C.length>0){const a=C.map(l=>g(`${l.accountId}: ${l.error}`)).join(`
|
|
4
|
-
`),i=new Error(`Organisation root deployed, but the cascade failed for ${C.length} target(s):
|
|
5
|
-
${a}`);return t.onError?.(i),x(i)}return W({target:n.target,deploymentType:"organisation",outputs:$,...v.length>0?{cascadeOutputs:v}:{},...q?{}:{noChanges:!0},durationMs:Date.now()-f})}export{ye as deployOrganisation};
|
|
1
|
+
import{failure as a}from"@fjall/generator";import{ORGANISATION_TYPES as o}from"../types/operations.js";import{deploySingleComponent as i}from"./organisationDeploy/singleComponentDeploy.js";import{deployOrgWithCascade as c}from"./organisationDeploy/orgCascadeDeploy.js";async function l(r,e,t){const n=Date.now();switch(t.type){case o.ORGANISATION:return c(r,e,t,n);case o.PLATFORM:return i(r,e,t,"platform",n);case o.ACCOUNT:return i(r,e,t,"account",n);default:{const u=t.type;return a(new Error(`Unsupported organisation type: ${String(u)}`))}}}export{l as deployOrganisation};
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Receives pre-authenticated credentials and destroys all organisation
|
|
5
5
|
* infrastructure in cascade order:
|
|
6
6
|
* 1. Member accounts across ALL regions in parallel
|
|
7
|
-
* 2. Platform account in
|
|
7
|
+
* 2. Platform account in its home region
|
|
8
8
|
* 3. Organisation root stack
|
|
9
9
|
*
|
|
10
10
|
* Auth, verification, and interactive prompts are the caller's job
|