@fjall/deploy-core 2.5.0 → 2.6.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/orchestration/cascadeHelpers.d.ts +19 -5
- package/dist/src/orchestration/cascadeHelpers.js +1 -1
- package/dist/src/orchestration/contextHelpers.js +1 -1
- package/dist/src/orchestration/organisationDeploy.js +5 -3
- package/dist/src/orchestration/organisationDestroy.js +2 -2
- package/dist/src/services/infrastructure/CdkService.d.ts +1 -1
- package/dist/src/services/infrastructure/CdkService.js +3 -3
- package/dist/src/services/infrastructure/cdkServiceHelpers.js +1 -1
- package/dist/src/services/infrastructure/constructMapEnrichment.d.ts +1 -1
- package/dist/src/services/infrastructure/constructMapEnrichment.js +1 -1
- package/dist/src/services/supporting/CdkContextBuilder.d.ts +2 -0
- package/dist/src/services/supporting/CdkContextBuilder.js +1 -1
- package/dist/src/types/deployment/DeploymentTypes.d.ts +16 -0
- package/package.json +4 -4
package/dist/.minified
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
118 files minified at 2026-05-
|
|
1
|
+
118 files minified at 2026-05-30T05:11:04.003Z
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import { type Result } from "@fjall/generator";
|
|
2
2
|
import type { DeployParams } from "../types/params.js";
|
|
3
|
+
import type { OrgConfig } from "../types/orgConfig.js";
|
|
3
4
|
import type { DeployCallbacks } from "../types/callbacks.js";
|
|
4
5
|
import type { OrganisationOperation } from "../types/operations.js";
|
|
5
6
|
import type { DeployServices } from "./serviceFactory.js";
|
|
6
7
|
import type { DomainDeployProvider } from "./domainInterface.js";
|
|
7
8
|
import type { ProviderAccount } from "@fjall/util/config";
|
|
9
|
+
/**
|
|
10
|
+
* Max member accounts deployed/destroyed in parallel during a cascade. Each
|
|
11
|
+
* task spawns a `cdk` process, assumes a role, synthesises and deploys — so the
|
|
12
|
+
* ceiling is the worker host's process/memory budget, NOT an AWS API limit
|
|
13
|
+
* (the accounts are distinct, so CloudFormation/STS quotas don't contend).
|
|
14
|
+
* Shared by deploy and destroy so they fan out identically.
|
|
15
|
+
*/
|
|
16
|
+
export declare const CASCADE_MAX_CONCURRENCY = 4;
|
|
8
17
|
/**
|
|
9
18
|
* Partition provider accounts into platform and member accounts.
|
|
10
19
|
* Used by both deploy and destroy orchestration.
|
|
@@ -21,11 +30,16 @@ export { buildCascadeRoleArn } from "./contextHelpers.js";
|
|
|
21
30
|
export interface CascadeAccountResult {
|
|
22
31
|
outputs?: Record<string, string>;
|
|
23
32
|
}
|
|
24
|
-
export
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
33
|
+
export interface DeployCascadeAccountOptions {
|
|
34
|
+
ipamPoolId?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Effective org config carrying the reconciled provider accounts. The cascade
|
|
37
|
+
* context must serialise THIS (not the possibly-stale `params.orgConfig`) so the
|
|
38
|
+
* synthesised app's `getConfig()` can resolve every account by name/id.
|
|
39
|
+
*/
|
|
40
|
+
orgConfig?: OrgConfig;
|
|
41
|
+
}
|
|
42
|
+
export declare function deployCascadeAccount(params: DeployParams, services: DeployServices, operation: OrganisationOperation, account: ProviderAccount, deployType: "platform" | "account", callbacks: DeployCallbacks, options?: DeployCascadeAccountOptions): Promise<Result<CascadeAccountResult>>;
|
|
29
43
|
/**
|
|
30
44
|
* Read Platform stack outputs to extract IPAM pool IDs for member accounts.
|
|
31
45
|
* Output keys follow the pattern `IpamPoolId{12-digit-accountId}{regionSuffix}`.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{success as
|
|
1
|
+
import{join as I}from"path";import{success as k,failure as C}from"@fjall/generator";import{logger as O}from"@fjall/util/logger";import{maskSensitiveOutput as f}from"@fjall/util";import{ORGANISATION_TYPES as p,getOrganisationStackName as x}from"../types/operations.js";import{CdkContextBuilder as L}from"../services/supporting/CdkContextBuilder.js";import{stubCallerIdentity as U}from"../types/deployment/index.js";import{CloudFormationService as N}from"../services/infrastructure/CloudFormationService.js";import{buildParamsContext as j,collectStackOutputs as B,assumeCascadeRole as E,forwardOutput as w}from"./contextHelpers.js";import{STRUCTURAL_ENVIRONMENTS as S}from"@fjall/util";const W=4;function Z(r){const s=r.find(e=>e.environment===S.PLATFORM),d=r.filter(e=>e.environment!==S.ROOT&&e.environment!==S.PLATFORM);return{platformAccount:s,memberAccounts:d}}import{buildCascadeRoleArn as re}from"./contextHelpers.js";async function b(r,s,d,e,i,n,c){const t=`${e.name}-${e.id}`,o=e.region??s.awsProvider.getRegion(),a=c?.orgConfig??r.orgConfig,m=c?.ipamPoolId;n.onCascadeAccountStart?.(t,e.id,o,i);const u=await E(s.awsProvider,e.id,o,`fjall-cascade-${e.name}`);if(!u.success)return n.onCascadeAccountComplete?.(t,!1,f(u.error.message),o),C(new Error(`Failed to assume role for ${e.name}: ${f(u.error.message)}`));const{provider:l,credentials:g}=u.data,h=I(r.workingDirectory,"fjall",i==="platform"?p.PLATFORM:p.ACCOUNT),T=o.replace(/-/g,""),F=I(h,`cdk.out.${e.id}.${T}`),P=L.buildDeploymentContext({deployType:i,target:d.target,path:h,assemblyDir:F,environment:e.environment,region:o,accountName:e.name,callerIdentity:U(e.id),ipamPoolId:m,...j({orgConfig:a,identity:r.identity,skipOidc:r.options?.skipOidc})},{verbose:r.options?.verbose},a);n.onCascadeAccountPhaseChange?.(t,"synth",o);const A=await s.cdkService.runCdkSynth(P,w(n),g);if(!A.success)return n.onCascadeAccountComplete?.(t,!1,f(`Synth failed: ${A.error}`),o),C(new Error(`Synth failed for ${e.name}: ${f(A.error)}`));n.onCascadeAccountPhaseChange?.(t,"bootstrap",o);const R=await s.cdkService.runCdkBootstrap(P,w(n),g);if(!R.success)return n.onCascadeAccountComplete?.(t,!1,f(`Bootstrap failed: ${R.error}`),o),C(new Error(`Bootstrap failed for ${e.name}: ${f(R.error)}`));n.onCascadeAccountPhaseChange?.(t,"deploy",o);const $=x(i==="platform"?p.PLATFORM:p.ACCOUNT),y=await s.cdkService.runCdkDeploy(P,$,w(n),M=>n.onCascadeAccountResourceProgress?.(t,M,o),l,g);if(!y.success)return n.onCascadeAccountComplete?.(t,!1,f(y.error),o),C(new Error(f(y.error)));const D=await new N(l).getStackOutputs($);D.success||O.debug("cascadeHelpers","Failed to read cascade account stack outputs (non-critical)",{stackName:$,account:e.name});const v=B(D);return n.onCascadeAccountComplete?.(t,!0,void 0,o,v),k({outputs:v})}async function ee(r,s,d){const e=new Map,i=r.awsProvider.getRegion(),n=await E(r.awsProvider,s.id,i,`fjall-ipam-read-${s.name}`);if(!n.success)return O.debug("organisationDeploy",`Cannot read Platform outputs: ${n.error.message}`),e;const c=new N(n.data.provider),t=x(p.PLATFORM),o=await c.getStackOutputs(t);if(!o.success)return O.debug("organisationDeploy",`Failed to read Platform stack outputs: ${o.error.message}`),e;const a=/^IpamPoolId(\d{12})(\w+)$/;for(const m of o.data){const u=m.OutputKey?.match(a);if(u&&m.OutputValue){const l=`${u[1]}-${u[2]}`;e.set(l,m.OutputValue)}}return e.size>0&&d.onLog?.(`Read ${e.size} IPAM pool ID(s) from Platform stack`,"info"),e}async function oe(r,s){const d=r.getDomains();if(d.length===0)return{domainsDeployed:0,errors:[]};s.onCascadePhaseStart?.("domains");const e=d.filter(t=>t.type==="apex"),i=d.filter(t=>t.type==="delegated");let n=0;const c=[];for(const t of e){const o=await r.deployDomain(t.name,s);o.success?n++:c.push(`${t.name}: ${o.error.message}`)}if(i.length>0){const t=await Promise.allSettled(i.map(o=>r.deployDomain(o.name,s)));for(let o=0;o<t.length;o++){const a=t[o],m=i[o];if(!(!a||!m))if(a.status==="fulfilled")a.value.success?n++:c.push(`${m.name}: ${a.value.error.message}`);else{const u=a.reason instanceof Error?a.reason.message:String(a.reason);c.push(`${m.name}: ${u}`)}}}return s.onCascadePhaseComplete?.("domains"),{domainsDeployed:n,errors:c}}export{W as CASCADE_MAX_CONCURRENCY,re as buildCascadeRoleArn,b as deployCascadeAccount,oe as deployDomains,Z as partitionAccounts,ee as readPlatformIpamPoolIds};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{success as
|
|
1
|
+
import{success as f,failure as u}from"@fjall/generator";import{getErrorMessage as y,maskSensitiveOutput as d,sleep as A}from"@fjall/util";import{logger as O}from"@fjall/util/logger";import{SimpleAwsProvider as R}from"../aws/SimpleAwsProvider.js";function M(e){return{...e.orgConfig!==void 0?{orgConfig:JSON.stringify(e.orgConfig)}:{},...e.identity!==void 0?{fjallOrgId:e.identity.fjallOrgId}:{},...e.skipOidc?{fjallOidcConfigured:!0}:{}}}function S(e){return r=>e.onOutput?.(r)}function k(e){return r=>e.onResourceProgress?.(r)}function T(e){if(!e.success||e.data.length===0)return;const r={};for(const t of e.data)t.OutputKey&&t.OutputValue!==void 0&&(r[t.OutputKey]=t.OutputValue);return Object.keys(r).length>0?r:void 0}const g="OrganizationAccountAccessRole",p=5,C=5e3,w=3e4;function $(e){return`arn:aws:iam::${e}:role/${g}`}async function D(e,r,t,s){if(!e.assumeRole)return u(new Error("AwsProvider does not support assumeRole"));const o=$(r),i=e.assumeRole.bind(e);let n;for(let c=0;c<=p;c++)try{n=await i(o,s);break}catch(a){const l=a instanceof Error?a.name:void 0;if(l==="AccessDenied"||l==="AccessDeniedException")return u(new Error(`Access denied assuming ${g} in account ${r}. The role may not exist or may not trust the management account.`));if(c<p){const m=Math.min(C*2**c,w);O.debug("assumeCascadeRole",`Attempt ${c+1} failed for account ${r}, retrying in ${Math.round(m/1e3)}s`,{error:d(y(a))}),await A(m);continue}return u(new Error(`Failed to assume role in account ${r} after ${p+1} attempts: ${d(y(a))}`))}if(!n)return u(new Error(`Failed to assume role in account ${r}`));const E=new R({accessKeyId:n.accessKeyId,secretAccessKey:n.secretAccessKey,sessionToken:n.sessionToken,region:t,accountId:r});return f({provider:E,credentials:{accessKeyId:n.accessKeyId,secretAccessKey:n.secretAccessKey,sessionToken:n.sessionToken}})}async function B(e,r,t,s){const o=await e.cdkService.runCdkSynth(r,i=>t.onCdkOutput?.(i,"synth"));if(!o.success){const i=new Error(d(`${s}: ${o.error}`));return t.onError?.(i),u(i)}return f(void 0)}async function P(e,r,t){t.onCDKBootstrap?.("bootstrapping");const s=await e.cdkService.runCdkBootstrap(r,S(t));if(!s.success){t.onCDKBootstrap?.("failed");const o=new Error(d(`Bootstrap failed: ${s.error}`));return t.onError?.(o),u(o)}return t.onCDKBootstrap?.("complete"),f(void 0)}export{D as assumeCascadeRole,P as bootstrapOrFail,$ as buildCascadeRoleArn,M as buildParamsContext,T as collectStackOutputs,S as forwardOutput,k as forwardResourceProgress,B as synthOrFail};
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
import{success as W,failure as
|
|
2
|
-
`);t.onLog?.(
|
|
3
|
-
${
|
|
1
|
+
import{success as W,failure as O}from"@fjall/generator";import{OrganizationsClient as lt}from"@aws-sdk/client-organizations";import{ORGANISATION_TYPES as $,getOrganisationStackName as J}from"../types/operations.js";import{CdkContextBuilder as mt}from"../services/supporting/CdkContextBuilder.js";import{stubCallerIdentity as pt}from"../types/deployment/index.js";import{ensureOrganisationExists as gt}from"../aws/organisations/organisation.js";import{buildParamsContext as ft,collectStackOutputs as Q,synthOrFail as Z,bootstrapOrFail as tt,forwardOutput as et,forwardResourceProgress as ot}from"./contextHelpers.js";import{partitionAccounts as nt,deployCascadeAccount as rt,readPlatformIpamPoolIds as Ct,deployDomains as St,CASCADE_MAX_CONCURRENCY as Et}from"./cascadeHelpers.js";import{reconcileProviderAccounts as Ot,mergeReconciledProviderAccounts as At}from"./reconcileProviderAccounts.js";import{maskSensitiveOutput as l,STRUCTURAL_ENVIRONMENTS as Rt,mapSettledWithConcurrency as Pt}from"@fjall/util";import{INFRA_STEP_NAME as G,STEP_IDS as I,STEP_NAMES as j}from"../types/stepDefinitions.js";async function Yt(o,e,r){const g=Date.now();switch(r.type){case $.ORGANISATION:return It(o,e,r,g);case $.PLATFORM:return ct(o,e,r,"platform",g);case $.ACCOUNT:return ct(o,e,r,"account",g);default:{const t=r.type;return O(new Error(`Unsupported organisation type: ${String(t)}`))}}}function at(o,e,r,g,t,a){return mt.buildDeploymentContext({deployType:g,target:r.target,path:r.path,region:e.awsProvider.getRegion(),accountName:a,callerIdentity:pt(e.awsProvider.getAccountId()),orgId:t.orgId,rootId:t.rootId,managementAccountId:t.managementAccountId,...ft({orgConfig:o.orgConfig,identity:o.identity,skipOidc:o.options?.skipOidc})},{verbose:o.options?.verbose,infraOnly:o.options?.infraOnly},o.orgConfig)}async function st(o){const e=o.awsProvider.getClient(lt),r=await gt(e);return r.success?W({orgId:r.data.orgId,rootId:r.data.rootId,managementAccountId:r.data.managementAccountId}):O(r.error)}const n={CONNECT:{id:I.CONNECT,name:G.CONNECT},PREPARE:{id:I.PREPARE_ENVIRONMENT,name:G.PREPARE},DEPLOY:{id:I.DEPLOY,name:G.DEPLOY},MONITORING:{id:I.MONITORING,name:G.MONITORING},ORG_DEPLOY:{id:I.ORG_DEPLOY,name:j.ORG_DEPLOY},CASCADE_PLATFORM:{id:I.CASCADE_PLATFORM,name:j.CASCADE_PLATFORM},CASCADE_ACCOUNTS:{id:I.CASCADE_ACCOUNTS,name:j.CASCADE_ACCOUNTS}},C=4;async function ct(o,e,r,g,t){const{callbacks:a}=o;a.onStepComplete?.(n.CONNECT.id,n.CONNECT.name,"completed",0,C),a.onStepStart?.(n.PREPARE.id,n.PREPARE.name,1,C);const c=await st(e);if(!c.success){a.onStepComplete?.(n.PREPARE.id,n.PREPARE.name,"error",1,C);const A=new Error(l(c.error.message));return a.onError?.(A),O(A)}const b=at(o,e,r,g,c.data,g==="account"?r.target:void 0);a.onLog?.(`Synthesising ${g} infrastructure\u2026`,"info");const _=await Z(e,b,a,"CDK synthesis failed");if(!_.success)return a.onStepComplete?.(n.PREPARE.id,n.PREPARE.name,"error",1,C),_;const T=await tt(e,b,a);if(!T.success)return a.onStepComplete?.(n.PREPARE.id,n.PREPARE.name,"error",1,C),T;a.onStepComplete?.(n.PREPARE.id,n.PREPARE.name,"completed",1,C);const N=J(r.type);a.onStepStart?.(n.DEPLOY.id,n.DEPLOY.name,2,C);const y=await e.cdkService.runCdkDeploy(b,N,et(a),ot(a),e.awsProvider);if(!y.success){a.onStepComplete?.(n.DEPLOY.id,n.DEPLOY.name,"error",2,C);const A=new Error(l(y.error));return a.onError?.(A),O(A)}a.onStepComplete?.(n.DEPLOY.id,n.DEPLOY.name,"completed",2,C);const w=await e.cfnService.getStackOutputs(N);w.success||a.onLog?.("Failed to read stack outputs (non-critical)","debug");const F=Q(w);return a.onStepStart?.(n.MONITORING.id,n.MONITORING.name,3,C),a.onStepComplete?.(n.MONITORING.id,n.MONITORING.name,"completed",3,C),W({target:r.target,deploymentType:"organisation",outputs:F,durationMs:Date.now()-t})}async function It(o,e,r,g){const{callbacks:t,options:a}=o;let c=o.orgConfig?.providerAccounts??[];if(c.length===0||c.every(s=>s.environment===Rt.ROOT)){const s=await Ot(e,o.workingDirectory);if(s.success){const{providerAccounts:f,missingAccountNames:S}=s.data;f.length>0&&(c=At(o.orgConfig,f).providerAccounts,t.onLog?.(`Reconciled ${f.length} account(s) from AWS Organizations`,"info")),S.length>0&&(t.onCascadeMissingAccounts?.(S),t.onProgress?.({type:"warning",message:l(`Accounts declared in ACCOUNTS but not yet in AWS Organizations (cascade will skip): ${S.join(", ")}`)}))}else t.onProgress?.({type:"warning",message:l(`Could not reconcile accounts from AWS Organizations \u2014 cascade may skip accounts: ${s.error.message}`)})}const _=o.orgConfig?.primaryRegion??e.awsProvider.getRegion();c=c.map(s=>s.region!==void 0?s:{...s,region:_});const T=o.orgConfig!==void 0?{...o.orgConfig,providerAccounts:c}:c.length>0?{providerAccounts:c}:void 0,N=await st(e);if(!N.success){const s=new Error(l(N.error.message));return t.onError?.(s),O(s)}const y=at(o,e,r,"organisation",N.data),w=a?.cascade!==!1,{platformAccount:F,memberAccounts:A}=nt(c),it=w&&F!==void 0?1:0,dt=w&&A.length>0?1:0,d=2+it+dt,{id:k,name:M}=n.PREPARE;t.onStepStart?.(k,M,0,d),t.onLog?.("Synthesising organisation infrastructure\u2026","info");const z=await Z(e,y,t,"CDK synthesis failed");if(!z.success)return t.onStepComplete?.(k,M,"error",0,d),z;const K=await tt(e,y,t);if(!K.success)return t.onStepComplete?.(k,M,"error",0,d),K;t.onStepComplete?.(k,M,"completed",0,d);const{id:x,name:U}=n.ORG_DEPLOY;t.onStepStart?.(x,U,1,d);const V=J($.ORGANISATION),B=await e.cdkService.runCdkDeploy(y,V,et(t),ot(t),e.awsProvider);if(!B.success){t.onStepComplete?.(x,U,"error",1,d);const s=new Error(l(B.error));return t.onError?.(s),O(s)}const X=await e.cfnService.getStackOutputs(V);X.success||t.onLog?.("Failed to read org stack outputs (non-critical)","debug");const ut=Q(X);t.onStepComplete?.(x,U,"completed",1,d);const u=[],Y=[];if(w&&c.length>0){t.onCascadeStart?.();let s=0,f=0,S=!1,q=!1,R=2;const{platformAccount:E,memberAccounts:v}=nt(c);if(E){const{id:m,name:p}=n.CASCADE_PLATFORM;t.onStepStart?.(m,p,R,d),t.onCascadePhaseStart?.("platform");let P;try{P=await rt(o,e,r,E,"platform",t,{orgConfig:T})}catch(i){const L=l(i instanceof Error?i.message:String(i));u.push({accountId:E.id,error:L}),t.onCascadePhaseComplete?.("platform"),t.onStepComplete?.(m,p,"error",R,d),P=O(new Error(L))}P.success?(S=!0,P.data.outputs&&Y.push({accountId:E.id,outputs:P.data.outputs}),t.onCascadePhaseComplete?.("platform"),t.onStepComplete?.(m,p,"completed",R,d)):u.some(i=>i.accountId===E.id)||(u.push({accountId:E.id,error:l(P.error.message)}),t.onCascadePhaseComplete?.("platform"),t.onStepComplete?.(m,p,"error",R,d)),R++}let H=new Map;if(S&&E&&(H=await Ct(e,E,t)),o.domainProvider){const m=await St(o.domainProvider,t);q=m.domainsDeployed>0;for(const p of m.errors)u.push({accountId:"domains",error:l(p)})}if(v.length>0){const{id:m,name:p}=n.CASCADE_ACCOUNTS;t.onStepStart?.(m,p,R,d),t.onCascadePhaseStart?.("accounts"),(await Pt(v,Et,i=>{const h=(i.region??e.awsProvider.getRegion()).replace(/-/g,""),D=H.get(`${i.id}-${h}`);return rt(o,e,r,i,"account",t,{ipamPoolId:D,orgConfig:T})})).forEach((i,L)=>{const h=v[L];if(!h)return;if(i.status==="rejected"){f++,u.push({accountId:h.id,error:l(i.reason instanceof Error?i.reason.message:String(i.reason))});return}const D=i.value;D.success?(s++,D.data.outputs&&Y.push({accountId:h.id,outputs:D.data.outputs})):(f++,u.push({accountId:h.id,error:l(D.error.message)}))}),t.onCascadePhaseComplete?.("accounts"),t.onStepComplete?.(m,p,f===0?"completed":"error",R,d)}if(t.onCascadeComplete?.({platformDeployed:S,domainsDeployed:q,accountsDeployed:s,accountsFailed:f,errors:u}),u.length>0){const m=u.map(p=>` ${p.accountId}: ${p.error}`).join(`
|
|
2
|
+
`);t.onLog?.(l(`Cascade failed for ${u.length} target(s):
|
|
3
|
+
${m}`),"warn")}}if(u.length>0){const s=u.map(S=>l(`${S.accountId}: ${S.error}`)).join(`
|
|
4
|
+
`),f=new Error(`Organisation root deployed, but the cascade failed for ${u.length} target(s):
|
|
5
|
+
${s}`);return t.onError?.(f),O(f)}return W({target:r.target,deploymentType:"organisation",outputs:ut,...Y.length>0?{cascadeOutputs:Y}:{},durationMs:Date.now()-g})}export{Yt as deployOrganisation};
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{success as R,failure as
|
|
1
|
+
import{success as R,failure as w}from"@fjall/generator";import{maskSensitiveOutput as C,getErrorMessage as A,mapSettledWithConcurrency as I}from"@fjall/util";import{stubCallerIdentity as P}from"../types/deployment/index.js";import{ORGANISATION_TYPES as T,getOrganisationStackName as _}from"../types/operations.js";import{CdkContextBuilder as $}from"../services/supporting/CdkContextBuilder.js";import{buildParamsContext as N,synthOrFail as k,forwardOutput as L,forwardResourceProgress as b}from"./contextHelpers.js";import{destroyCascadeAccount as E}from"./cascadeDestroyHelpers.js";import{partitionAccounts as v,CASCADE_MAX_CONCURRENCY as G}from"./cascadeHelpers.js";import{DEFAULT_REGION as x}from"../aws/utils/regions.js";import{STEP_IDS as M}from"../types/stepDefinitions.js";const h=M.ORG_DESTROY,S="Destroying organisation infrastructure";async function V(t,r,n){const e=Date.now(),{callbacks:o}=t,d=t.orgConfig?.providerAccounts??[],f=t.orgConfig?.primaryRegion??r.awsProvider.getRegion(),l=F(t),{platformAccount:g,memberAccounts:m}=v(d),D=t.options?.cascade!==!1;o.onLog?.(`Destroying organisation infrastructure (${d.length} accounts, ${l.length} region(s))`,"info");const s=[],p=[];if(D){if(o.onCascadeStart?.(),m.length>0){o.onCascadePhaseStart?.("accounts");const a=U(m,l),c=await I(a,G,({account:i,region:u})=>E(t,r,n,i,"account",u,o));for(let i=0;i<c.length;i++){const u=c[i],y=a[i];!u||!y||(u.status==="fulfilled"?u.value.success?p.push(`Account-${y.account.name}-${y.region}`):s.push({accountId:y.account.id,error:u.value.error??"Unknown error"}):s.push({accountId:y.account.id,error:A(u.reason)}))}}if(g){o.onCascadePhaseStart?.("platform");const a=await E(t,r,n,g,"platform",f,o);a.success?p.push("Platform"):s.push({accountId:g.id,error:a.error??"Platform destroy failed"})}if(o.onCascadeComplete?.({platformDeployed:!1,domainsDeployed:!1,accountsDeployed:0,accountsFailed:s.length,errors:s}),s.length>0){const a=s.map(i=>` ${i.accountId}: ${i.error}`).join(`
|
|
2
2
|
`),c=new Error(`Cascade destroy completed with ${s.length} failure(s):
|
|
3
|
-
${a}`);o.onError?.(c),o.onLog?.(
|
|
3
|
+
${a}`);o.onError?.(c),o.onLog?.(C(c.message),"warn")}}if(s.length>0)return o.onLog?.("Skipping organisation root stack destroy due to cascade failures","warn"),R({target:n.target,deploymentType:"organisation",stacksDestroyed:p,durationMs:Date.now()-e,warnings:s.map(c=>C(`${c.accountId}: ${c.error}`))});o.onStepStart?.(h,S,0,1);const O=await Y(t,r,n,f,o);if(O.success)p.push("Organisation"),o.onStepComplete?.(h,S,"completed",0,1);else return o.onStepComplete?.(h,S,"error",0,1),w(O.error);return R({target:n.target,deploymentType:"organisation",stacksDestroyed:p,durationMs:Date.now()-e})}async function Y(t,r,n,e,o){const d=$.buildDeploymentContext({deployType:"organisation",target:n.target,path:n.path,region:e,callerIdentity:P(r.awsProvider.getAccountId()),...N({orgConfig:t.orgConfig,identity:t.identity})},{verbose:t.options?.verbose},t.orgConfig),f=_(T.ORGANISATION);o.onLog?.("Synthesising organisation infrastructure\u2026","info");const l=await k(r,d,o,"Organisation synth failed");if(!l.success)return l;o.onLog?.(`Destroying ${f} stack\u2026`,"info");const g=await r.cdkService.runCdkDestroy(d,f,L(o),b(o),r.awsProvider,!0);if(!g.success){const m=new Error(C(`Organisation destroy failed: ${g.error}`));return o.onError?.(m),w(m)}return R(void 0)}function F(t){const r=t.orgConfig?.primaryRegion??x,n=t.orgConfig?.secondaryRegions??[],e=[r,...n],o=t.orgConfig?.disasterRecoveryRegion;return o&&!e.includes(o)&&e.push(o),e}function U(t,r){const n=[];for(const e of r)for(const o of t)n.push({account:o,region:e});return n}export{V as destroyOrganisation};
|
|
@@ -18,7 +18,7 @@ export declare class CdkService {
|
|
|
18
18
|
runImport(path: string, resourceMappingFile?: string, options?: CdkOptions): Promise<Result<CdkOutput, string>>;
|
|
19
19
|
synth(path: string, options?: CdkOptions): Promise<Result<CdkOutput, string>>;
|
|
20
20
|
bootstrap(accountId: string, region: string, options?: CdkOptions): Promise<Result<CdkOutput, string>>;
|
|
21
|
-
runCdkSynth(context: DeploymentContext, onOutput?: (chunk: string) => void): Promise<Result<StepOutput, string>>;
|
|
21
|
+
runCdkSynth(context: DeploymentContext, onOutput?: (chunk: string) => void, credentials?: CdkOptions["credentials"]): Promise<Result<StepOutput, string>>;
|
|
22
22
|
runCdkBootstrap(context: DeploymentContext, onOutput?: (chunk: string) => void, credentials?: CdkOptions["credentials"]): Promise<Result<StepOutput, string>>;
|
|
23
23
|
runCdkDiff(context: DeploymentContext, onOutput?: (chunk: string) => void): Promise<Result<StepOutput, string>>;
|
|
24
24
|
runCdkDeploy(context: DeploymentContext, stackPattern?: string, onOutput?: (chunk: string) => void, onResourceProgress?: (event: ResourceEvent) => void, aws?: AwsProvider, credentials?: CdkOptions["credentials"], parameters?: Record<string, string>): Promise<Result<StepOutput, string>>;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{existsSync as
|
|
2
|
-
`)),
|
|
3
|
-
`));const
|
|
1
|
+
import{existsSync as I}from"fs";import{join as b}from"path";import{logger as v}from"@fjall/util/logger";import{success as D,failure as s}from"@fjall/generator";import{DEFAULT_REGION as h}from"../../aws/utils/regions.js";import{getErrorMessage as k,maskSensitiveOutput as p}from"@fjall/util";import{CdkEventMonitor as R,startStackMonitoring as E}from"./CdkEventMonitoring.js";import{analyseDeployOutput as F,analyseDestroyResult as w,createEnhancedOutputCallback as O}from"./CdkOutputAnalyser.js";import{CdkCommandRunner as K}from"./CdkCommandRunner.js";import{CdkArgumentBuilder as N}from"./CdkArgumentBuilder.js";import{CdkProcessManager as T}from"./CdkProcessManager.js";import{wrapWithConstructMapEnrichment as L}from"./constructMapEnrichment.js";import{STACK_DETECTION_FALLBACK_MS as P,resolveStackName as M,getFallbackStackName as W,buildDeploymentCdkContext as C}from"./cdkServiceHelpers.js";class Z{commandRunner;eventMonitor;constructor(e){const r=e?.processManager??new T(new N);this.commandRunner=new K(r),this.eventMonitor=new R({eventLogWriterFactory:e?.eventLogWriterFactory})}dispose(){this.commandRunner.dispose()}async checkDifferences(e,r,t){return this.commandRunner.checkDifferences(e,r,t)}async deploy(e,r,t){return this.commandRunner.deploy(e,r,t)}async destroy(e,r,t){return this.commandRunner.destroy(e,r,t)}async runImport(e,r,t){return this.commandRunner.runImport(e,r,t)}async synth(e,r){return this.commandRunner.synth(e,r)}async bootstrap(e,r,t){return this.commandRunner.bootstrap(e,r,t)}async runCdkSynth(e,r,t){const n=e.callerIdentity?.Account;try{const o=await this.synth(e.path,{outputCallback:r,context:C(e,n,e.region||h),...e.assemblyDir!==void 0?{outputDir:e.assemblyDir}:{},...t!==void 0?{credentials:t}:{}});return o.success?D({message:"CloudFormation template synthesised",details:o.data.output?{synthesisTime:o.data.output}:void 0}):s(o.error||"Failed to synthesise CloudFormation template")}catch(o){return s(`CDK synth failed: ${p(k(o))}`)}}async runCdkBootstrap(e,r,t){const n=e.callerIdentity?.Account,o=e.region||h;try{if(!n)return s("No AWS account ID available");const l=b(e.path,"node_modules");if(!I(l))return s(`Dependencies not installed. Please run 'npm install' in ${e.path} before deploying.`);const u=await this.bootstrap(n,o,{outputCallback:r,credentials:t});return u.success?D({message:"AWS environment bootstrapped"}):s(u.error||"Failed to bootstrap AWS environment")}catch(l){return s(`CDK bootstrap failed: ${p(k(l))}`)}}async runCdkDiff(e,r){const t=e.callerIdentity?.Account;try{const n=await this.checkDifferences(e.path,void 0,{verbose:e.options?.verbose,outputCallback:r,context:C(e,t,e.region||h)});return n.success?D({message:"Diff check complete",details:{hasDifferences:n.data.hasDifferences,details:n.data.details}}):s(`CDK diff failed: ${n.error.message}`)}catch(n){return s(`CDK diff failed: ${p(k(n))}`)}}async runCdkDeploy(e,r,t,n,o,l,u){const f=e.callerIdentity?.Account,y=e.region||h;if(!f)return s("AWS account ID not available. Please ensure AWS credentials are properly configured.");if(!o)return s("AwsProvider is required for deployment monitoring.");const m=e.assemblyDir??b(e.path,"cdk.out"),d=L(m,n);let i=null,g;try{const c=M(r,e)??W(e);i=await this.eventMonitor.createEventMonitor("deploy",c,y,e,o),t&&(t(p(`Starting CloudFormation deployment of ${c}...
|
|
2
|
+
`)),t(`Monitoring CloudFormation events (CDK process running in background)...
|
|
3
|
+
`));const a={cdkOutput:"",actualStackName:c,stackDetected:!1,monitoringPromise:null},S=O(a,t,i,d);g=setTimeout(()=>{!a.stackDetected&&i&&!a.monitoringPromise&&(v.debug("CdkService","Fallback monitoring STARTING",{targetStackName:c,stackDetected:a.stackDetected,hasOnResourceProgress:!!n}),a.monitoringPromise=E(i,c,d))},P);const A=await this.deploy(e.path,c,{verbose:e.options?.verbose,outputCallback:S,...e.assemblyDir!==void 0?{appDir:e.assemblyDir}:{useCdkOut:!0},cdkOutputLogger:i?.getEventLogger()??void 0,context:C(e,f,y),credentials:l,...u!==void 0&&Object.keys(u).length>0&&{parameters:u}});return F(a.cdkOutput,A,a.actualStackName)}catch(c){const a=`CDK deploy failed: ${p(k(c))}`;return v.error("CdkService","CDK deployment exception",{error:a}),s(a)}finally{clearTimeout(g),i&&i.stopMonitoring()}}async runCdkDestroy(e,r,t,n,o,l,u){const f=e.callerIdentity?.Account,y=e.region||h;let m=null;try{const d=M(r,e);f&&d&&o&&(m=await this.eventMonitor.createEventMonitor("destroy",d,y,e,o),m.startMonitoring(d,g=>{n?.(g)},(g,c)=>{}));const i=await this.destroy(e.path,r,{verbose:e.options?.verbose,outputCallback:t,useCdkOut:l,cdkOutputLogger:m?.getEventLogger()??void 0,context:C(e,f,y),credentials:u});return w(i)}catch(d){return s(`CDK destroy failed: ${p(k(d))}`)}finally{m&&m.stopMonitoring()}}}export{Z as CdkService};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{getApplicationStackName as i,getOrganisationStackName as d,isApplicationStack as p}from"../../types/operations.js";const
|
|
1
|
+
import{getApplicationStackName as i,getOrganisationStackName as d,isApplicationStack as p}from"../../types/operations.js";const u=5e3;function c(o,e){if(o&&!o.includes("*"))return o;if(o){const n=o.match(/\*?(\w+)\*?/);if(n?.[1]){const a=n[1],r=e.target;return p(a)?i(r,a):`${r}${a}`}return o}}function m(o){const e=o.deployType;return e==="organisation"||e==="platform"||e==="account"?d(e):`${o.target}Network`}function g(o,e,n){return{accountId:e,region:n,environment:o.environment,managedAccount:o.isManagedAccount,accountName:o.accountName,orgId:o.orgId,rootId:o.rootId,managementAccountId:o.managementAccountId,ipamPoolId:o.ipamPoolId,fjallOrgId:o.fjallOrgId,fjallOidcConfigured:o.fjallOidcConfigured?"true":void 0,orgConfig:o.orgConfig}}export{u as STACK_DETECTION_FALLBACK_MS,g as buildDeploymentCdkContext,m as getFallbackStackName,c as resolveStackName};
|
|
@@ -4,4 +4,4 @@ import type { ResourceEvent } from "../../aws/utils/cloudformationEvents.js";
|
|
|
4
4
|
* and return a wrapped callback that enriches resource events with
|
|
5
5
|
* group and constructPath fields.
|
|
6
6
|
*/
|
|
7
|
-
export declare function wrapWithConstructMapEnrichment(
|
|
7
|
+
export declare function wrapWithConstructMapEnrichment(assemblyDir: string, onResourceProgress?: (event: ResourceEvent) => void): ((event: ResourceEvent) => void) | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{existsSync as u,readFileSync as s}from"fs";import{join as p}from"path";import{logger as i}from"@fjall/util/logger";import{enrichFromConstructMap as a,recordToConstructMap as f,FJALL_MANIFEST_FILENAME as d}from"@fjall/util/constructMap";import{getErrorMessage as m}from"@fjall/util";function C(
|
|
1
|
+
import{existsSync as u,readFileSync as s}from"fs";import{join as p}from"path";import{logger as i}from"@fjall/util/logger";import{enrichFromConstructMap as a,recordToConstructMap as f,FJALL_MANIFEST_FILENAME as d}from"@fjall/util/constructMap";import{getErrorMessage as m}from"@fjall/util";function C(o,c){if(!c)return;const t=p(o,d);let e;try{if(u(t)){const r=JSON.parse(s(t,"utf-8")),n=typeof r=="object"&&r!==null?r.resourceMap:void 0;h(n)&&(e=f(n)),e&&e.size>0&&i.debug("CdkService",`Loaded construct map with ${e.size} entries`)}}catch(r){i.debug("CdkService",`Could not read construct map: ${m(r)}`)}return!e||e.size===0?c:r=>{const n=a(r.logicalId,r.resourceType,e);c({...r,...n.group!==void 0?{group:n.group}:{},...n.constructPath!==void 0?{constructPath:n.constructPath}:{}})}}function h(o){if(typeof o!="object"||o===null||Array.isArray(o))return!1;const c=Object.values(o);if(c.length===0)return!0;const t=c[0];return typeof t=="object"&&t!==null&&"constructPath"in t&&"group"in t&&"resourceType"in t}export{C as wrapWithConstructMapEnrichment};
|
|
@@ -18,6 +18,8 @@ export declare class CdkContextBuilder {
|
|
|
18
18
|
deployType: "application" | "organisation" | "platform" | "account";
|
|
19
19
|
target: string;
|
|
20
20
|
path: string;
|
|
21
|
+
assemblyDir?: string;
|
|
22
|
+
environment?: string;
|
|
21
23
|
callerIdentity?: CallerIdentity;
|
|
22
24
|
region?: string;
|
|
23
25
|
stackOutputs?: StackOutputsRecord;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{DEFAULT_REGION as
|
|
1
|
+
import{DEFAULT_REGION as r}from"@fjall/generator";class l{static buildDeploymentContext(e,o,a){return{deployType:e.deployType,target:e.target,path:e.path,...e.assemblyDir!==void 0?{assemblyDir:e.assemblyDir}:{},...e.environment!==void 0?{environment:e.environment}:{},options:o,stackOutputs:e.stackOutputs||{},callerIdentity:e.callerIdentity,region:e.region||a?.primaryRegion||r,isManagedAccount:e.isManagedAccount,accountName:e.accountName,logPath:e.logPath,orgId:e.orgId,rootId:e.rootId,managementAccountId:e.managementAccountId,ipamPoolId:e.ipamPoolId,fjallOrgId:e.fjallOrgId,fjallOidcConfigured:e.fjallOidcConfigured,orgConfig:e.orgConfig}}static updateContext(e,o){return{...e,...o}}}export{l as CdkContextBuilder};
|
|
@@ -4,6 +4,14 @@ export interface DeploymentContext {
|
|
|
4
4
|
deployType: "application" | "organisation" | "platform" | "account";
|
|
5
5
|
target: string;
|
|
6
6
|
accountName?: string;
|
|
7
|
+
/**
|
|
8
|
+
* CDK app environment selector (`root` | `platform` | <member env>). Passed
|
|
9
|
+
* to the synthesised app as `--context environment=…`, the highest-priority
|
|
10
|
+
* source in `getConfig()`. Set per cascade account so each account synths
|
|
11
|
+
* the correct `infrastructure.ts` branch deterministically, without relying
|
|
12
|
+
* on accountName→providerAccount resolution.
|
|
13
|
+
*/
|
|
14
|
+
environment?: string;
|
|
7
15
|
region?: string;
|
|
8
16
|
callerIdentity?: CallerIdentity;
|
|
9
17
|
stackOutputs?: StackOutputsRecord;
|
|
@@ -20,6 +28,14 @@ export interface DeploymentContext {
|
|
|
20
28
|
infraOnly?: boolean;
|
|
21
29
|
};
|
|
22
30
|
path: string;
|
|
31
|
+
/**
|
|
32
|
+
* Isolated cloud-assembly directory for this deployment. When set, synth
|
|
33
|
+
* writes the assembly here (`--output`) and deploy reads it (`--app`),
|
|
34
|
+
* instead of the shared `<path>/cdk.out`. The cascade gives every account
|
|
35
|
+
* its own `cdk.out.<accountId>.<region>` so parallel member synth+deploy
|
|
36
|
+
* cannot clobber one another's templates.
|
|
37
|
+
*/
|
|
38
|
+
assemblyDir?: string;
|
|
23
39
|
logPath?: string;
|
|
24
40
|
}
|
|
25
41
|
export interface StepOutput {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fjall/deploy-core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "Shared deployment engine for Fjall — used by CLI and webapp worker",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/src/index.js",
|
|
@@ -73,8 +73,8 @@
|
|
|
73
73
|
"@aws-sdk/client-s3": "^3.1038.0",
|
|
74
74
|
"@aws-sdk/client-sso-admin": "^3.1038.0",
|
|
75
75
|
"@aws-sdk/client-sts": "^3.1038.0",
|
|
76
|
-
"@fjall/generator": "^2.
|
|
77
|
-
"@fjall/util": "^2.
|
|
76
|
+
"@fjall/generator": "^2.6.0",
|
|
77
|
+
"@fjall/util": "^2.6.0",
|
|
78
78
|
"@smithy/node-http-handler": "^4.6.1",
|
|
79
79
|
"tsx": "^4.21.0",
|
|
80
80
|
"zod": "^4.4.3"
|
|
@@ -83,5 +83,5 @@
|
|
|
83
83
|
"@types/node": "^25.6.0",
|
|
84
84
|
"vitest": "^4.1.5"
|
|
85
85
|
},
|
|
86
|
-
"gitHead": "
|
|
86
|
+
"gitHead": "93666ff94b8b1d0e360a7710e9266d275d15ee34"
|
|
87
87
|
}
|