@fjall/deploy-core 2.8.0 → 2.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.minified +1 -1
- package/dist/src/aws/organisations/backup.d.ts +24 -0
- package/dist/src/aws/organisations/backup.js +1 -1
- package/dist/src/orchestration/cascadeHelpers.js +1 -1
- package/dist/src/orchestration/organisationDeploy.js +5 -5
- package/dist/src/services/infrastructure/CdkArgumentBuilder.js +1 -1
- package/dist/src/services/infrastructure/CdkServiceTypes.d.ts +1 -0
- package/dist/src/services/infrastructure/cdkServiceHelpers.js +1 -1
- package/dist/src/types/deployment/DeploymentTypes.d.ts +1 -0
- package/package.json +4 -4
package/dist/.minified
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
120 files minified at 2026-06-
|
|
1
|
+
120 files minified at 2026-06-02T21:42:55.155Z
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type BackupClient } from "@aws-sdk/client-backup";
|
|
2
2
|
import { type Result } from "@fjall/generator";
|
|
3
|
+
export declare const BACKUP_VAULT_NAME = "backupVault";
|
|
3
4
|
export interface BackupGlobalSettings {
|
|
4
5
|
enableCrossAccountBackup?: boolean;
|
|
5
6
|
enableDelegatedAdministrator?: boolean;
|
|
@@ -10,3 +11,26 @@ export interface BackupGlobalSettings {
|
|
|
10
11
|
* Idempotent — safe to call repeatedly with the same settings.
|
|
11
12
|
*/
|
|
12
13
|
export declare function updateBackupGlobalSettings(client: BackupClient, settings?: BackupGlobalSettings): Promise<Result<void>>;
|
|
14
|
+
/**
|
|
15
|
+
* Whether an account's synthesised stack will create the DR backup vault — and
|
|
16
|
+
* so whether the adopt-vs-create probe needs to run for it. Mirror of the
|
|
17
|
+
* DisasterRecovery construct gate in @fjall/components-infrastructure
|
|
18
|
+
* patterns/aws/account.ts: the vault is synthesised only when
|
|
19
|
+
* `disasterRecoveryRegion` is set AND the account is production or compliance.
|
|
20
|
+
*
|
|
21
|
+
* Probing accounts that synthesise no vault (platform/staging/development) is
|
|
22
|
+
* dead work that fails on a `backup:DescribeBackupVault` permission those
|
|
23
|
+
* accounts never grant. Keep this predicate in lockstep with account.ts:122-125.
|
|
24
|
+
*/
|
|
25
|
+
export declare function accountHasDisasterRecovery(environment: string | undefined, disasterRecoveryRegion: string | undefined): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Probe whether the disaster-recovery backup vault already exists in the target
|
|
28
|
+
* account/region, to decide adopt-vs-create. A retained, vault-locked DR vault
|
|
29
|
+
* survives stack teardown and collides on the next CREATE.
|
|
30
|
+
*
|
|
31
|
+
* Returns success(true) when present and success(false) ONLY on
|
|
32
|
+
* ResourceNotFoundException. Every other error (AccessDenied, throttling)
|
|
33
|
+
* propagates as failure — a mis-read "exists" would flip a vault-less region
|
|
34
|
+
* into a broken by-reference deploy.
|
|
35
|
+
*/
|
|
36
|
+
export declare function describeBackupVaultExists(client: BackupClient): Promise<Result<boolean>>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{UpdateGlobalSettingsCommand as
|
|
1
|
+
import{DescribeBackupVaultCommand as c,UpdateGlobalSettingsCommand as l}from"@aws-sdk/client-backup";import{success as r,failure as n}from"@fjall/generator";import{getErrorMessage as o,maskSensitiveOutput as p}from"@fjall/util";import{SDK_TIMEOUT_MS as i}from"./types.js";const s="backupVault",d={enableCrossAccountBackup:!0,enableDelegatedAdministrator:!0,enableMpa:!1};async function S(e,t){try{const a={...d,...t},u={isCrossAccountBackupEnabled:a.enableCrossAccountBackup.toString(),isDelegatedAdministratorEnabled:a.enableDelegatedAdministrator.toString(),isMpaEnabled:a.enableMpa.toString()};return await e.send(new l({GlobalSettings:u}),{abortSignal:AbortSignal.timeout(i)}),r(void 0)}catch(a){return n(new Error(`Failed to update backup global settings: ${o(a)}`))}}function A(e,t){return t===void 0||t===""?!1:e==="production"||e==="compliance"}async function E(e){try{return await e.send(new c({BackupVaultName:s}),{abortSignal:AbortSignal.timeout(i)}),r(!0)}catch(t){return t instanceof Error&&t.name==="ResourceNotFoundException"?r(!1):n(new Error(`Failed to describe backup vault "${s}": ${p(o(t))}`))}}export{s as BACKUP_VAULT_NAME,A as accountHasDisasterRecovery,E as describeBackupVaultExists,S as updateBackupGlobalSettings};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{join as T}from"path";import{success as L,failure as
|
|
1
|
+
import{join as T}from"path";import{success as L,failure as C}from"@fjall/generator";import{logger as R}from"@fjall/util/logger";import{maskSensitiveOutput as m}from"@fjall/util";import{ORGANISATION_TYPES as h,getOrganisationStackName as M}from"../types/operations.js";import{CdkContextBuilder as V}from"../services/supporting/CdkContextBuilder.js";import{stubCallerIdentity as _}from"../types/deployment/index.js";import{CloudFormationService as B}from"../services/infrastructure/CloudFormationService.js";import{getCascadeStateFilePath as G}from"../types/FjallState.js";import{BackupClient as z}from"@aws-sdk/client-backup";import{accountHasDisasterRecovery as K,describeBackupVaultExists as Y}from"../aws/organisations/backup.js";import{buildParamsContext as X,collectStackOutputs as U,assumeCascadeRole as b,forwardOutput as k}from"./contextHelpers.js";import{STRUCTURAL_ENVIRONMENTS as v}from"@fjall/util";import{DEFAULT_REGION as q}from"../aws/utils/regions.js";const de=4;function pe(s){const n=s.find(e=>e.environment===v.PLATFORM),i=s.filter(e=>e.environment!==v.ROOT&&e.environment!==v.PLATFORM);return{platformAccount:n,memberAccounts:i}}function me(s){const n=s?.primaryRegion??q,i=s?.secondaryRegions??[],e=[n,...i],a=s?.disasterRecoveryRegion;return a&&!e.includes(a)&&e.push(a),e}function fe(s,n){const i=[];for(const e of n)for(const a of s)i.push({account:a,region:e});return i}import{buildCascadeRoleArn as Ae}from"./contextHelpers.js";async function le(s,n,i,e,a,r,u){const t=u?.region??e.region??n.awsProvider.getRegion(),o=`${e.name} (${t})`,c=u?.orgConfig??s.orgConfig,f=u?.ipamPoolId;r.onCascadeAccountStart?.(o,e.id,t,a);const d=await b(n.awsProvider,e.id,t,`fjall-cascade-${e.name}`);if(!d.success)return r.onCascadeAccountComplete?.(o,!1,m(d.error.message),t),C(new Error(`Failed to assume role for ${e.name}: ${m(d.error.message)}`));const{provider:g,credentials:y}=d.data,P=T(s.workingDirectory,"fjall",a==="platform"?h.PLATFORM:h.ACCOUNT),j=t.replace(/-/g,""),D=T(P,`cdk.out.${e.id}.${j}`),A=V.buildDeploymentContext({deployType:a,target:i.target,path:P,assemblyDir:D,environment:e.environment,region:t,accountName:e.name,callerIdentity:_(e.id),ipamPoolId:f,...X({orgConfig:c,identity:s.identity,skipOidc:s.options?.skipOidc,skipAccountGlobals:u?.skipAccountGlobals})},{verbose:s.options?.verbose},c);if(K(e.environment,c?.disasterRecoveryRegion)){const p=await Y(g.getClient(z));if(!p.success)return r.onCascadeAccountComplete?.(o,!1,m(p.error.message),t),C(new Error(`Backup vault probe failed for ${e.name}: ${m(p.error.message)}`));A.fjallAdoptBackupVault=p.data}r.onCascadeAccountPhaseChange?.(o,"synth",t);const w=await n.cdkService.runCdkSynth(A,k(r),y);if(!w.success)return r.onCascadeAccountComplete?.(o,!1,m(`Synth failed: ${w.error}`),t),C(new Error(`Synth failed for ${e.name}: ${m(w.error)}`));const l=M(a==="platform"?h.PLATFORM:h.ACCOUNT),x=G(P,e.id,t),O=new B(g),{changed:H,currentHash:E}=await n.hashService.compareCascadeStack(D,l,x);if(!H&&s.options?.force!==!0&&await O.stackExists(l)){const p=await O.getStackOutputs(l);p.success||R.debug("cascadeHelpers","Failed to read outputs for skipped cascade account (non-critical)",{stackName:l,account:e.name});const N=U(p);return r.onLog?.(`${e.name}: no infrastructure changes \u2014 skipping deploy`,"info"),r.onCascadeAccountComplete?.(o,!0,void 0,t,N,!0),L({outputs:N,skipped:!0})}r.onCascadeAccountPhaseChange?.(o,"bootstrap",t);const S=await n.cdkService.runCdkBootstrap(A,k(r),y);if(!S.success)return r.onCascadeAccountComplete?.(o,!1,m(`Bootstrap failed: ${S.error}`),t),C(new Error(`Bootstrap failed for ${e.name}: ${m(S.error)}`));r.onCascadeAccountPhaseChange?.(o,"deploy",t);const $=await n.cdkService.runCdkDeploy(A,l,k(r),p=>r.onCascadeAccountResourceProgress?.(o,p,t),g,y);if(!$.success)return r.onCascadeAccountComplete?.(o,!1,m($.error),t),C(new Error(m($.error)));const I=await O.getStackOutputs(l);I.success||R.debug("cascadeHelpers","Failed to read cascade account stack outputs (non-critical)",{stackName:l,account:e.name});const F=U(I);return E!==void 0&&((await n.hashService.persistCascadeStack(x,l,E)).success||R.debug("cascadeHelpers","Failed to persist cascade hash state (non-critical)",{stackName:l,account:e.name})),r.onCascadeAccountComplete?.(o,!0,void 0,t,F,!1),L({outputs:F,skipped:!1})}async function ge(s,n,i){const e=new Map,a=s.awsProvider.getRegion(),r=await b(s.awsProvider,n.id,a,`fjall-ipam-read-${n.name}`);if(!r.success)return R.debug("organisationDeploy",`Cannot read Platform outputs: ${r.error.message}`),e;const u=new B(r.data.provider),t=M(h.PLATFORM),o=await u.getStackOutputs(t);if(!o.success)return R.debug("organisationDeploy",`Failed to read Platform stack outputs: ${o.error.message}`),e;const c=/^IpamPoolId(\d{12})(\w+)$/;for(const f of o.data){const d=f.OutputKey?.match(c);if(d&&f.OutputValue){const g=`${d[1]}-${d[2]}`;e.set(g,f.OutputValue)}}return e.size>0&&i.onLog?.(`Read ${e.size} IPAM pool ID(s) from Platform stack`,"info"),e}async function Ce(s,n){const i=s.getDomains();if(i.length===0)return{domainsDeployed:0,errors:[]};n.onCascadePhaseStart?.("domains");const e=i.filter(t=>t.type==="apex"),a=i.filter(t=>t.type==="delegated");let r=0;const u=[];for(const t of e){const o=await s.deployDomain(t.name,n);o.success?r++:u.push(`${t.name}: ${o.error.message}`)}if(a.length>0){const t=await Promise.allSettled(a.map(o=>s.deployDomain(o.name,n)));for(let o=0;o<t.length;o++){const c=t[o],f=a[o];if(!(!c||!f))if(c.status==="fulfilled")c.value.success?r++:u.push(`${f.name}: ${c.value.error.message}`);else{const d=c.reason instanceof Error?c.reason.message:String(c.reason);u.push(`${f.name}: ${d}`)}}}return n.onCascadePhaseComplete?.("domains"),{domainsDeployed:r,errors:u}}export{de as CASCADE_MAX_CONCURRENCY,fe as buildAccountRegionPairs,Ae as buildCascadeRoleArn,me as buildRegionList,le as deployCascadeAccount,Ce as deployDomains,pe as partitionAccounts,ge as readPlatformIpamPoolIds};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import{join as It}from"node:path";import{success as ot,failure as y}from"@fjall/generator";import{OrganizationsClient as yt}from"@aws-sdk/client-organizations";import{ORGANISATION_TYPES as H,getOrganisationStackName as gt}from"../types/operations.js";import{CdkContextBuilder as wt}from"../services/supporting/CdkContextBuilder.js";import{stubCallerIdentity as Nt}from"../types/deployment/index.js";import{ensureOrganisationExists as Dt}from"../aws/organisations/organisation.js";import{buildParamsContext as Tt,collectStackOutputs as nt,synthOrFail as mt,bootstrapOrFail as ft,forwardOutput as Ct,forwardResourceProgress as St}from"./contextHelpers.js";import{partitionAccounts as Ot,deployCascadeAccount as At,readPlatformIpamPoolIds as kt,deployDomains as Lt,buildRegionList as bt,buildAccountRegionPairs as Mt,CASCADE_MAX_CONCURRENCY as _t}from"./cascadeHelpers.js";import{projectScalarSummary as Et}from"./cascadeSummary.js";import{reconcileProviderAccounts as $t,mergeReconciledProviderAccounts as Gt}from"./reconcileProviderAccounts.js";import{maskSensitiveOutput as i,STRUCTURAL_ENVIRONMENTS as Yt,mapSettledWithConcurrency as Ft}from"@fjall/util";import{INFRA_STEP_NAME as K,STEP_IDS as k,STEP_NAMES as rt}from"../types/stepDefinitions.js";async function Zt(o,e,a){const f=Date.now();switch(a.type){case H.ORGANISATION:return xt(o,e,a,f);case H.PLATFORM:return ht(o,e,a,"platform",f);case H.ACCOUNT:return ht(o,e,a,"account",f);default:{const t=a.type;return y(new Error(`Unsupported organisation type: ${String(t)}`))}}}function Rt(o,e,a,f,t,s){return wt.buildDeploymentContext({deployType:f,target:a.target,path:a.path,region:e.awsProvider.getRegion(),accountName:s,callerIdentity:Nt(e.awsProvider.getAccountId()),orgId:t.orgId,rootId:t.rootId,managementAccountId:t.managementAccountId,...Tt({orgConfig:o.orgConfig,identity:o.identity,skipOidc:o.options?.skipOidc})},{verbose:o.options?.verbose,infraOnly:o.options?.infraOnly},o.orgConfig)}async function Pt(o){const e=o.awsProvider.getClient(yt),a=await Dt(e);return a.success?ot({orgId:a.data.orgId,rootId:a.data.rootId,managementAccountId:a.data.managementAccountId}):y(a.error)}const r={CONNECT:{id:k.CONNECT,name:K.CONNECT},PREPARE:{id:k.PREPARE_ENVIRONMENT,name:K.PREPARE},DEPLOY:{id:k.DEPLOY,name:K.DEPLOY},MONITORING:{id:k.MONITORING,name:K.MONITORING},ORG_DEPLOY:{id:k.ORG_DEPLOY,name:rt.ORG_DEPLOY},CASCADE_PLATFORM:{id:k.CASCADE_PLATFORM,name:rt.CASCADE_PLATFORM},CASCADE_ACCOUNTS:{id:k.CASCADE_ACCOUNTS,name:rt.CASCADE_ACCOUNTS}},O=4;async function ht(o,e,a,f,t){const{callbacks:s}=o;s.onStepComplete?.(r.CONNECT.id,r.CONNECT.name,"completed",0,O),s.onStepStart?.(r.PREPARE.id,r.PREPARE.name,1,O);const d=await Pt(e);if(!d.success){s.onStepComplete?.(r.PREPARE.id,r.PREPARE.name,"error",1,O);const w=new Error(i(d.error.message));return s.onError?.(w),y(w)}const x=Rt(o,e,a,f,d.data,f==="account"?a.target:void 0);s.onLog?.(`Synthesising ${f} infrastructure\u2026`,"info");const M=await mt(e,x,s,"CDK synthesis failed");if(!M.success)return s.onStepComplete?.(r.PREPARE.id,r.PREPARE.name,"error",1,O),M;const _=await ft(e,x,s);if(!_.success)return s.onStepComplete?.(r.PREPARE.id,r.PREPARE.name,"error",1,O),_;s.onStepComplete?.(r.PREPARE.id,r.PREPARE.name,"completed",1,O);const L=gt(a.type);s.onStepStart?.(r.DEPLOY.id,r.DEPLOY.name,2,O);const P=await e.cdkService.runCdkDeploy(x,L,Ct(s),St(s),e.awsProvider);if(!P.success){s.onStepComplete?.(r.DEPLOY.id,r.DEPLOY.name,"error",2,O);const w=new Error(i(P.error));return s.onError?.(w),y(w)}s.onStepComplete?.(r.DEPLOY.id,r.DEPLOY.name,"completed",2,O);const b=await e.cfnService.getStackOutputs(L);b.success||s.onLog?.("Failed to read stack outputs (non-critical)","debug");const V=nt(b);return s.onStepStart?.(r.MONITORING.id,r.MONITORING.name,3,O),s.onStepComplete?.(r.MONITORING.id,r.MONITORING.name,"completed",3,O),ot({target:a.target,deploymentType:"organisation",outputs:V,durationMs:Date.now()-t})}async function xt(o,e,a,f){const{callbacks:t,options:s}=o;let d=o.orgConfig?.providerAccounts??[];if(d.length===0||d.every(n=>n.environment===Yt.ROOT)){const n=await $t(e,o.workingDirectory);if(n.success){const{providerAccounts:c,missingAccountNames:g}=n.data;c.length>0&&(d=Gt(o.orgConfig,c).providerAccounts,t.onLog?.(`Reconciled ${c.length} account(s) from AWS Organizations`,"info")),g.length>0&&(t.onCascadeMissingAccounts?.(g),t.onProgress?.({type:"warning",message:i(`Accounts declared in ACCOUNTS but not yet in AWS Organizations (cascade will skip): ${g.join(", ")}`)}))}else t.onProgress?.({type:"warning",message:i(`Could not reconcile accounts from AWS Organizations \u2014 cascade may skip accounts: ${n.error.message}`)})}const M=o.orgConfig?.primaryRegion??e.awsProvider.getRegion();d=d.map(n=>n.region!==void 0?n:{...n,region:M});const _=o.orgConfig!==void 0?{...o.orgConfig,providerAccounts:d}:d.length>0?{providerAccounts:d}:void 0,L=await Pt(e);if(!L.success){const n=new Error(i(L.error.message));return t.onError?.(n),y(n)}const P=Rt(o,e,a,"organisation",L.data),b=s?.cascade!==!1,{platformAccount:V,memberAccounts:w}=Ot(d),at=b&&V!==void 0?1:0,st=b&&w.length>0?1:0,l=2+at+st;t.onCascadeAccountsReconciled?.({hasPlatformAccount:at>0,hasMemberAccounts:st>0});const{id:U,name:v}=r.PREPARE;t.onStepStart?.(U,v,0,l),t.onLog?.("Synthesising organisation infrastructure\u2026","info");const ct=await mt(e,P,t,"CDK synthesis failed");if(!ct.success)return t.onStepComplete?.(U,v,"error",0,l),ct;const it=await ft(e,P,t);if(!it.success)return t.onStepComplete?.(U,v,"error",0,l),it;t.onStepComplete?.(U,v,"completed",0,l);const{id:B,name:X}=r.ORG_DEPLOY,N=gt(H.ORGANISATION);let dt=!0;const $=await e.hashService.getTemplateHashes(It(P.path,"cdk.out"));if($.success){const n=await e.hashService.compareWithState($.data,P.path);n.success?dt=n.data.stackChanges.get(N)??!0:t.onLog?.(i(`Org root change detection failed \u2014 deploying to be safe: ${n.error.message}`),"warn")}else t.onLog?.(i(`Org root template hashing failed \u2014 deploying to be safe: ${$.error.message}`),"warn");const q=dt||s?.force===!0||!await e.cfnService.stackExists(N);t.onOrgChangesDetected?.({hasOrgChanges:q});let J;if(q){t.onStepStart?.(B,X,1,l);const n=await e.cdkService.runCdkDeploy(P,N,Ct(t),St(t),e.awsProvider);if(!n.success){t.onStepComplete?.(B,X,"error",1,l);const A=new Error(i(n.error));return t.onError?.(A),y(A)}const c=await e.cfnService.getStackOutputs(N);c.success||t.onLog?.("Failed to read org stack outputs (non-critical)","debug"),J=nt(c);const g=$.success?$.data.get(N):void 0;if(g!==void 0){const A=await e.hashService.updateStateAfterDeploy(P.path,new Map([[N,g]]));A.success||t.onLog?.(`Warning: failed to update state file \u2014 next deploy may re-deploy the org root: ${i(A.error.message)}`,"warn")}t.onStepComplete?.(B,X,"completed",1,l)}else{t.onLog?.("Organisation root: no infrastructure changes \u2014 skipping deploy","info");const n=await e.cfnService.getStackOutputs(N);n.success||t.onLog?.("Failed to read org stack outputs (non-critical)","debug"),J=nt(n)}const p=[],W=[];let j=q;if(b&&d.length>0){t.onCascadeStart?.();const n=Date.now();let c=2,g=!1,A,Q=!1;const z=[],ut=u=>({members:z,...A!==void 0?{platform:A}:{},domainsDeployed:Q,errors:p,totalDurationMs:u}),{platformAccount:E,memberAccounts:Z}=Ot(d);if(E){const{id:u,name:m}=r.CASCADE_PLATFORM;t.onStepStart?.(u,m,c,l),t.onCascadePhaseStart?.("platform");let C;const tt=Date.now();try{C=await At(o,e,a,E,"platform",t,{orgConfig:_})}catch(R){const Y=i(R instanceof Error?R.message:String(R));p.push({accountId:E.id,error:Y}),t.onCascadePhaseComplete?.("platform"),t.onStepComplete?.(u,m,"error",c,l),C=y(new Error(Y))}const G=Date.now()-tt;if(C.success){g=!0;const R=C.data.skipped===!0;R||(j=!0),C.data.outputs&&W.push({accountId:E.id,outputs:C.data.outputs}),t.onCascadePhaseComplete?.("platform"),t.onStepComplete?.(u,m,R?"skipped":"completed",c,l),A={accountId:E.id,result:R?"skipped":"succeeded",durationMs:G}}else p.some(R=>R.accountId===E.id)||(p.push({accountId:E.id,error:i(C.error.message)}),t.onCascadePhaseComplete?.("platform"),t.onStepComplete?.(u,m,"error",c,l)),A={accountId:E.id,result:"failed",durationMs:G,error:i(C.error.message)};c++}let lt=new Map;if(g&&E&&(lt=await kt(e,E,t)),o.domainProvider){const u=await Lt(o.domainProvider,t);Q=u.domainsDeployed>0,Q&&(j=!0);for(const m of u.errors)p.push({accountId:"domains",error:i(m)})}if(Z.length>0){const{id:u,name:m}=r.CASCADE_ACCOUNTS;t.onStepStart?.(u,m,c,l),t.onCascadePhaseStart?.("accounts");const C=bt(o.orgConfig),tt=C[0]??M,G=Mt(Z,C);(await Ft(G,_t,async({account:h,region:F})=>{const D=F.replace(/-/g,""),S=lt.get(`${h.id}-${D}`),T=Date.now();return{result:await At(o,e,a,h,"account",t,{ipamPoolId:S,orgConfig:_,region:F,skipAccountGlobals:F!==tt}),durationMs:Date.now()-T}})).forEach((h,F)=>{const D=G[F];if(!D)return;const S=D.account;if(h.status==="rejected"){const I=i(h.reason instanceof Error?h.reason.message:String(h.reason));z.push({accountId:S.id,accountName:S.name,region:D.region,result:"failed",durationMs:0,error:I}),p.push({accountId:S.id,error:I});return}const{result:T,durationMs:et}=h.value;if(T.success){const I=T.data.skipped===!0;I||(j=!0),T.data.outputs&&W.push({accountId:S.id,outputs:T.data.outputs}),z.push({accountId:S.id,accountName:S.name,region:D.region,result:I?"skipped":"succeeded",durationMs:et})}else{const I=i(T.error.message);z.push({accountId:S.id,accountName:S.name,region:D.region,result:"failed",durationMs:et,error:I}),p.push({accountId:S.id,error:I})}});const Y=Et(ut(0));t.onCascadePhaseComplete?.("accounts"),t.onStepComplete?.(u,m,Y.accountsFailed>0?"error":Y.accountsSkipped===Z.length?"skipped":"completed",c,l)}const pt=ut(Date.now()-n);if(t.onCascadeComplete?.(Et(pt)),t.onCascadeLedger?.(pt),p.length>0){const u=p.map(m=>` ${m.accountId}: ${m.error}`).join(`
|
|
2
|
-
`);
|
|
3
|
-
${u}`),"warn")}}if(
|
|
4
|
-
`),c=new Error(`Organisation root deployed, but the cascade failed for ${
|
|
5
|
-
${
|
|
1
|
+
import{join as Ie}from"node:path";import{success as re,failure as h}from"@fjall/generator";import{OrganizationsClient as we}from"@aws-sdk/client-organizations";import{BackupClient as Ne}from"@aws-sdk/client-backup";import{ORGANISATION_TYPES as K,getOrganisationStackName as me}from"../types/operations.js";import{CdkContextBuilder as ye}from"../services/supporting/CdkContextBuilder.js";import{stubCallerIdentity as De}from"../types/deployment/index.js";import{ensureOrganisationExists as ke}from"../aws/organisations/organisation.js";import{accountHasDisasterRecovery as Te,describeBackupVaultExists as be}from"../aws/organisations/backup.js";import{buildParamsContext as Le,collectStackOutputs as ae,synthOrFail as ge,bootstrapOrFail as fe,forwardOutput as Ce,forwardResourceProgress as Se}from"./contextHelpers.js";import{partitionAccounts as Ae,deployCascadeAccount as Ee,readPlatformIpamPoolIds as Me,deployDomains as _e,buildRegionList as $e,buildAccountRegionPairs as Fe,CASCADE_MAX_CONCURRENCY as Ge}from"./cascadeHelpers.js";import{projectScalarSummary as Oe}from"./cascadeSummary.js";import{reconcileProviderAccounts as Ye,mergeReconciledProviderAccounts as ve}from"./reconcileProviderAccounts.js";import{maskSensitiveOutput as i,STRUCTURAL_ENVIRONMENTS as xe,mapSettledWithConcurrency as Ue}from"@fjall/util";import{INFRA_STEP_NAME as X,STEP_IDS as b,STEP_NAMES as se}from"../types/stepDefinitions.js";async function at(o,t,s){const g=Date.now();switch(s.type){case K.ORGANISATION:return We(o,t,s,g);case K.PLATFORM:return he(o,t,s,"platform",g);case K.ACCOUNT:return he(o,t,s,"account",g);default:{const e=s.type;return h(new Error(`Unsupported organisation type: ${String(e)}`))}}}function Re(o,t,s,g,e,r){return ye.buildDeploymentContext({deployType:g,target:s.target,path:s.path,region:t.awsProvider.getRegion(),accountName:r,callerIdentity:De(t.awsProvider.getAccountId()),orgId:e.orgId,rootId:e.rootId,managementAccountId:e.managementAccountId,...Le({orgConfig:o.orgConfig,identity:o.identity,skipOidc:o.options?.skipOidc})},{verbose:o.options?.verbose,infraOnly:o.options?.infraOnly},o.orgConfig)}async function Pe(o){const t=o.awsProvider.getClient(we),s=await ke(t);return s.success?re({orgId:s.data.orgId,rootId:s.data.rootId,managementAccountId:s.data.managementAccountId}):h(s.error)}const n={CONNECT:{id:b.CONNECT,name:X.CONNECT},PREPARE:{id:b.PREPARE_ENVIRONMENT,name:X.PREPARE},DEPLOY:{id:b.DEPLOY,name:X.DEPLOY},MONITORING:{id:b.MONITORING,name:X.MONITORING},ORG_DEPLOY:{id:b.ORG_DEPLOY,name:se.ORG_DEPLOY},CASCADE_PLATFORM:{id:b.CASCADE_PLATFORM,name:se.CASCADE_PLATFORM},CASCADE_ACCOUNTS:{id:b.CASCADE_ACCOUNTS,name:se.CASCADE_ACCOUNTS}},S=4;async function he(o,t,s,g,e){const{callbacks:r}=o;r.onStepComplete?.(n.CONNECT.id,n.CONNECT.name,"completed",0,S),r.onStepStart?.(n.PREPARE.id,n.PREPARE.name,1,S);const p=await Pe(t);if(!p.success){r.onStepComplete?.(n.PREPARE.id,n.PREPARE.name,"error",1,S);const l=new Error(i(p.error.message));return r.onError?.(l),h(l)}const $=Re(o,t,s,g,p.data,g==="account"?s.target:void 0),L=o.orgConfig?.disasterRecoveryRegion,W=L!==void 0&&L!=="",M=g==="account"?o.orgConfig?.providerAccounts.find(l=>l.name===s.target):void 0;if(g==="account"&&W&&(M===void 0||Te(M.environment,L))){const l=await be(t.awsProvider.getClient(Ne));if(!l.success){r.onStepComplete?.(n.PREPARE.id,n.PREPARE.name,"error",1,S);const y=new Error(i(l.error.message));return r.onError?.(y),h(y)}$.fjallAdoptBackupVault=l.data}r.onLog?.(`Synthesising ${g} infrastructure\u2026`,"info");const _=await ge(t,$,r,"CDK synthesis failed");if(!_.success)return r.onStepComplete?.(n.PREPARE.id,n.PREPARE.name,"error",1,S),_;const j=await fe(t,$,r);if(!j.success)return r.onStepComplete?.(n.PREPARE.id,n.PREPARE.name,"error",1,S),j;r.onStepComplete?.(n.PREPARE.id,n.PREPARE.name,"completed",1,S);const V=me(s.type);r.onStepStart?.(n.DEPLOY.id,n.DEPLOY.name,2,S);const F=await t.cdkService.runCdkDeploy($,V,Ce(r),Se(r),t.awsProvider);if(!F.success){r.onStepComplete?.(n.DEPLOY.id,n.DEPLOY.name,"error",2,S);const l=new Error(i(F.error));return r.onError?.(l),h(l)}r.onStepComplete?.(n.DEPLOY.id,n.DEPLOY.name,"completed",2,S);const G=await t.cfnService.getStackOutputs(V);G.success||r.onLog?.("Failed to read stack outputs (non-critical)","debug");const d=ae(G);return r.onStepStart?.(n.MONITORING.id,n.MONITORING.name,3,S),r.onStepComplete?.(n.MONITORING.id,n.MONITORING.name,"completed",3,S),re({target:s.target,deploymentType:"organisation",outputs:d,durationMs:Date.now()-e})}async function We(o,t,s,g){const{callbacks:e,options:r}=o;let p=o.orgConfig?.providerAccounts??[];if(p.length===0||p.every(a=>a.environment===xe.ROOT)){const a=await Ye(t,o.workingDirectory);if(a.success){const{providerAccounts:c,missingAccountNames:C}=a.data;c.length>0&&(p=ve(o.orgConfig,c).providerAccounts,e.onLog?.(`Reconciled ${c.length} account(s) from AWS Organizations`,"info")),C.length>0&&(e.onCascadeMissingAccounts?.(C),e.onProgress?.({type:"warning",message:i(`Accounts declared in ACCOUNTS but not yet in AWS Organizations (cascade will skip): ${C.join(", ")}`)}))}else e.onProgress?.({type:"warning",message:i(`Could not reconcile accounts from AWS Organizations \u2014 cascade may skip accounts: ${a.error.message}`)})}const L=o.orgConfig?.primaryRegion??t.awsProvider.getRegion();p=p.map(a=>a.region!==void 0?a:{...a,region:L});const W=o.orgConfig!==void 0?{...o.orgConfig,providerAccounts:p}:p.length>0?{providerAccounts:p}:void 0,M=await Pe(t);if(!M.success){const a=new Error(i(M.error.message));return e.onError?.(a),h(a)}const N=Re(o,t,s,"organisation",M.data),_=r?.cascade!==!1,{platformAccount:j,memberAccounts:V}=Ae(p),F=_&&j!==void 0?1:0,G=_&&V.length>0?1:0,d=2+F+G;e.onCascadeAccountsReconciled?.({hasPlatformAccount:F>0,hasMemberAccounts:G>0});const{id:l,name:y}=n.PREPARE;e.onStepStart?.(l,y,0,d),e.onLog?.("Synthesising organisation infrastructure\u2026","info");const ce=await ge(t,N,e,"CDK synthesis failed");if(!ce.success)return e.onStepComplete?.(l,y,"error",0,d),ce;const ie=await fe(t,N,e);if(!ie.success)return e.onStepComplete?.(l,y,"error",0,d),ie;e.onStepComplete?.(l,y,"completed",0,d);const{id:J,name:Q}=n.ORG_DEPLOY,D=me(K.ORGANISATION);let de=!0;const Y=await t.hashService.getTemplateHashes(Ie(N.path,"cdk.out"));if(Y.success){const a=await t.hashService.compareWithState(Y.data,N.path);a.success?de=a.data.stackChanges.get(D)??!0:e.onLog?.(i(`Org root change detection failed \u2014 deploying to be safe: ${a.error.message}`),"warn")}else e.onLog?.(i(`Org root template hashing failed \u2014 deploying to be safe: ${Y.error.message}`),"warn");const Z=de||r?.force===!0||!await t.cfnService.stackExists(D);e.onOrgChangesDetected?.({hasOrgChanges:Z});let ee;if(Z){e.onStepStart?.(J,Q,1,d);const a=await t.cdkService.runCdkDeploy(N,D,Ce(e),Se(e),t.awsProvider);if(!a.success){e.onStepComplete?.(J,Q,"error",1,d);const R=new Error(i(a.error));return e.onError?.(R),h(R)}const c=await t.cfnService.getStackOutputs(D);c.success||e.onLog?.("Failed to read org stack outputs (non-critical)","debug"),ee=ae(c);const C=Y.success?Y.data.get(D):void 0;if(C!==void 0){const R=await t.hashService.updateStateAfterDeploy(N.path,new Map([[D,C]]));R.success||e.onLog?.(`Warning: failed to update state file \u2014 next deploy may re-deploy the org root: ${i(R.error.message)}`,"warn")}e.onStepComplete?.(J,Q,"completed",1,d)}else{e.onLog?.("Organisation root: no infrastructure changes \u2014 skipping deploy","info");const a=await t.cfnService.getStackOutputs(D);a.success||e.onLog?.("Failed to read org stack outputs (non-critical)","debug"),ee=ae(a)}const f=[],H=[];let z=Z;if(_&&p.length>0){e.onCascadeStart?.();const a=Date.now();let c=2,C=!1,R,te=!1;const B=[],ue=u=>({members:B,...R!==void 0?{platform:R}:{},domainsDeployed:te,errors:f,totalDurationMs:u}),{platformAccount:A,memberAccounts:q}=Ae(p);if(A){const{id:u,name:m}=n.CASCADE_PLATFORM;e.onStepStart?.(u,m,c,d),e.onCascadePhaseStart?.("platform");let E;const oe=Date.now();try{E=await Ee(o,t,s,A,"platform",e,{orgConfig:W})}catch(P){const x=i(P instanceof Error?P.message:String(P));f.push({accountId:A.id,error:x}),e.onCascadePhaseComplete?.("platform"),e.onStepComplete?.(u,m,"error",c,d),E=h(new Error(x))}const v=Date.now()-oe;if(E.success){C=!0;const P=E.data.skipped===!0;P||(z=!0),E.data.outputs&&H.push({accountId:A.id,outputs:E.data.outputs}),e.onCascadePhaseComplete?.("platform"),e.onStepComplete?.(u,m,P?"skipped":"completed",c,d),R={accountId:A.id,result:P?"skipped":"succeeded",durationMs:v}}else f.some(P=>P.accountId===A.id)||(f.push({accountId:A.id,error:i(E.error.message)}),e.onCascadePhaseComplete?.("platform"),e.onStepComplete?.(u,m,"error",c,d)),R={accountId:A.id,result:"failed",durationMs:v,error:i(E.error.message)};c++}let le=new Map;if(C&&A&&(le=await Me(t,A,e)),o.domainProvider){const u=await _e(o.domainProvider,e);te=u.domainsDeployed>0,te&&(z=!0);for(const m of u.errors)f.push({accountId:"domains",error:i(m)})}if(A!==void 0&&!C&&q.length>0){const{id:u,name:m}=n.CASCADE_ACCOUNTS;e.onStepStart?.(u,m,c,d),e.onStepComplete?.(u,m,"skipped",c,d),e.onLog?.("Skipping account cascade \u2014 platform deployment failed; platform is a prerequisite for member accounts.","warn")}else if(q.length>0){const{id:u,name:m}=n.CASCADE_ACCOUNTS;e.onStepStart?.(u,m,c,d),e.onCascadePhaseStart?.("accounts");const E=$e(o.orgConfig),oe=E[0]??L,v=Fe(q,E);(await Ue(v,Ge,async({account:I,region:U})=>{const k=U.replace(/-/g,""),O=le.get(`${I.id}-${k}`),T=Date.now();return{result:await Ee(o,t,s,I,"account",e,{ipamPoolId:O,orgConfig:W,region:U,skipAccountGlobals:U!==oe}),durationMs:Date.now()-T}})).forEach((I,U)=>{const k=v[U];if(!k)return;const O=k.account;if(I.status==="rejected"){const w=i(I.reason instanceof Error?I.reason.message:String(I.reason));B.push({accountId:O.id,accountName:O.name,region:k.region,result:"failed",durationMs:0,error:w}),f.push({accountId:O.id,error:w});return}const{result:T,durationMs:ne}=I.value;if(T.success){const w=T.data.skipped===!0;w||(z=!0),T.data.outputs&&H.push({accountId:O.id,outputs:T.data.outputs}),B.push({accountId:O.id,accountName:O.name,region:k.region,result:w?"skipped":"succeeded",durationMs:ne})}else{const w=i(T.error.message);B.push({accountId:O.id,accountName:O.name,region:k.region,result:"failed",durationMs:ne,error:w}),f.push({accountId:O.id,error:w})}});const x=Oe(ue(0));e.onCascadePhaseComplete?.("accounts"),e.onStepComplete?.(u,m,x.accountsFailed>0?"error":x.accountsSkipped===q.length?"skipped":"completed",c,d)}const pe=ue(Date.now()-a);if(e.onCascadeComplete?.(Oe(pe)),e.onCascadeLedger?.(pe),f.length>0){const u=f.map(m=>` ${m.accountId}: ${m.error}`).join(`
|
|
2
|
+
`);e.onLog?.(i(`Cascade failed for ${f.length} target(s):
|
|
3
|
+
${u}`),"warn")}}if(f.length>0){const a=f.map(C=>i(`${C.accountId}: ${C.error}`)).join(`
|
|
4
|
+
`),c=new Error(`Organisation root deployed, but the cascade failed for ${f.length} target(s):
|
|
5
|
+
${a}`);return e.onError?.(c),h(c)}return re({target:s.target,deploymentType:"organisation",outputs:ee,...H.length>0?{cascadeOutputs:H}:{},...z?{}:{noChanges:!0},durationMs:Date.now()-g})}export{at as deployOrganisation};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{filterDangerousEnvVars as
|
|
1
|
+
import{filterDangerousEnvVars as u}from"@fjall/util";class s{buildContextArgs(r){const a=[];return r?.accountId&&a.push("-c",`accountId=${r.accountId}`),r?.environment&&a.push("-c",`environment=${r.environment}`),r?.managedAccount&&a.push("-c","managedAccount=true"),r?.accountName&&a.push("-c",`accountName=${r.accountName}`),r?.orgId&&a.push("-c",`orgId=${r.orgId}`),r?.rootId&&a.push("-c",`rootId=${r.rootId}`),r?.managementAccountId&&a.push("-c",`managementAccountId=${r.managementAccountId}`),r?.ipamPoolId&&a.push("-c",`ipamPoolId=${r.ipamPoolId}`),r?.fjallOrgId&&a.push("-c",`fjallOrgId=${r.fjallOrgId}`),r?.fjallOidcConfigured&&a.push("-c",`fjallOidcConfigured=${r.fjallOidcConfigured}`),r?.fjallAccountGlobalsConfigured&&a.push("-c",`fjallAccountGlobalsConfigured=${r.fjallAccountGlobalsConfigured}`),r?.orgConfig&&a.push("-c",`orgConfig=${r.orgConfig}`),r?.fjallAdoptBackupVault&&a.push("-c","fjallAdoptBackupVault=true"),a}buildParameterArgs(r){if(r===void 0)return[];const a=Object.entries(r);if(a.length===0)return[];const o=[];for(const[e,n]of a){if(!/^[A-Za-z][A-Za-z0-9]*$/.test(e))throw new Error(`Invalid CloudFormation parameter name "${e}": must match /^[A-Za-z][A-Za-z0-9]*$/ (alphanumeric, leading letter, no separators).`);if(n==="")throw new Error(`CloudFormation parameter "${e}" has an empty value.`);if(/[,\n\r]/.test(n))throw new Error(`CloudFormation parameter "${e}" value contains "," or newline \u2014 cdk's --parameters splits on "," so the deploy would silently fragment.`);o.push("--parameters",`${e}=${n}`)}return o}injectCascadeCredentials(r,a){a&&(r.AWS_ACCESS_KEY_ID=a.accessKeyId,r.AWS_SECRET_ACCESS_KEY=a.secretAccessKey,delete r.AWS_SESSION_TOKEN,a.sessionToken&&(r.AWS_SESSION_TOKEN=a.sessionToken))}buildCdkEnv(r){const a={...u(process.env),CI:"true",FORCE_COLOR:"0",CDK_DISABLE_VERSION_CHECK:"1"};return r?.context?.region&&(a.AWS_REGION=r.context.region,a.AWS_DEFAULT_REGION=r.context.region,a.CDK_DEFAULT_REGION=r.context.region),r?.context?.accountId&&(a.CDK_DEFAULT_ACCOUNT=r.context.accountId),this.injectCascadeCredentials(a,r?.credentials),a}}export{s as CdkArgumentBuilder};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{getApplicationStackName as i,getOrganisationStackName as u,isApplicationStack as
|
|
1
|
+
import{getApplicationStackName as i,getOrganisationStackName as u,isApplicationStack as l}from"../../types/operations.js";const p=5e3;function t(a,o){if(a&&!a.includes("*"))return a;if(a){const e=a.match(/\*?(\w+)\*?/);if(e?.[1]){const n=e[1],r=o.target;return l(n)?i(r,n):`${r}${n}`}return a}}function c(a){const o=a.deployType;return o==="organisation"||o==="platform"||o==="account"?u(o):`${a.target}Network`}function f(a,o,e){return{accountId:o,region:e,environment:a.environment,managedAccount:a.isManagedAccount,accountName:a.accountName,orgId:a.orgId,rootId:a.rootId,managementAccountId:a.managementAccountId,ipamPoolId:a.ipamPoolId,fjallOrgId:a.fjallOrgId,fjallOidcConfigured:a.fjallOidcConfigured?"true":void 0,fjallAccountGlobalsConfigured:a.fjallAccountGlobalsConfigured?"true":void 0,orgConfig:a.orgConfig,fjallAdoptBackupVault:a.fjallAdoptBackupVault?"true":void 0}}export{p as STACK_DETECTION_FALLBACK_MS,f as buildDeploymentCdkContext,c as getFallbackStackName,t as resolveStackName};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fjall/deploy-core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.1",
|
|
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.9.1",
|
|
77
|
+
"@fjall/util": "^2.9.1",
|
|
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": "a97423cf3df727994364a0907fa2b5c544a86b0d"
|
|
87
87
|
}
|