@fjall/deploy-core 3.2.1 → 3.4.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/orchestration/application/applicationDeploy.js +1 -1
- package/dist/src/orchestration/bootstrapTags.d.ts +73 -0
- package/dist/src/orchestration/bootstrapTags.js +1 -0
- package/dist/src/orchestration/contextHelpers.d.ts +6 -1
- package/dist/src/orchestration/contextHelpers.js +1 -1
- package/dist/src/orchestration/organisation/cascadeHelpers.d.ts +11 -0
- package/dist/src/orchestration/organisation/cascadeHelpers.js +1 -1
- package/dist/src/orchestration/organisationDeploy/cascadeExecution.d.ts +3 -0
- package/dist/src/orchestration/organisationDeploy/cascadeExecution.js +1 -1
- package/dist/src/orchestration/organisationDeploy/orgCascadeDeploy.js +2 -2
- package/dist/src/orchestration/organisationDeploy/singleComponentDeploy.js +1 -1
- package/dist/src/services/infrastructure/CdkCommandRunner.js +2 -2
- package/dist/src/services/infrastructure/CdkService.d.ts +1 -1
- package/dist/src/services/infrastructure/CdkService.js +2 -2
- package/dist/src/services/infrastructure/CdkServiceTypes.d.ts +11 -0
- package/package.json +4 -4
package/dist/.minified
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
227 files minified at 2026-07-18T23:08:22.555Z
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var be=Object.defineProperty;var N=(e,n)=>be(e,"name",{value:n,configurable:!0});import{join as ce}from"path";import{CloudFormationClient as we}from"@aws-sdk/client-cloudformation";import{EC2Client as Te}from"@aws-sdk/client-ec2";import{success as P,failure as d}from"@fjall/generator";import{logger as B}from"@fjall/util/logger";import{ImageTagSchema as Ee,getErrorMessage as le,imageTagParameterName as Ne,maskSensitiveOutput as S}from"@fjall/util";import{parseDockerDeclarationsFromManifest as De}from"@fjall/util/manifest";import{stubCallerIdentity as Pe}from"../../types/deployment/index.js";import{getApplicationDeployOrder as ve,getApplicationStackName as I,getApplicationStepName as de,getApplicationStepId as ue,APPLICATION_STACKS as D}from"../../types/operations.js";import{CdkContextBuilder as Re}from"../../services/supporting/CdkContextBuilder.js";import{buildParamsContext as Ce,bootstrapOrFail as Ie}from"../contextHelpers.js";import{runDetectionPipeline as Oe}from"./detectionPipeline.js";import{STEP_IDS as _,STEP_NAMES as Ae}from"../../types/stepDefinitions.js";import{StepRegistry as Le}from"../../steps/stepRegistry.js";import{checkTargetReadiness as Me}from"../../aws/targetReadiness.js";import{cascadeHomeRegion as Be}from"../organisation/cascadeHelpers.js";import{withStepLifecycle as Fe}from"../stepLifecycle.js";import{DOCKER_BUILD_STEP_NAME as ge,runDockerBuild as $e}from"./dockerBuildHelper.js";import{getParallelPhase2Stacks as xe,getStackNamesToDeploy as je,deployParallelPhase as Ge,deployStackSequential as Ue,runDockerPreCompute as _e,runLambdaVersionPublish as He,createBuildCallbacks as Ke}from"./applicationDeployHelpers.js";import{captureDatabaseConnectionOutputs as We,captureServiceTaskDefinitions as qe,reconcileDatabaseEndpointChange as Ve}from"./databaseEndpointReconcile.js";import{deployCodeOnly as ze}from"./codeOnlyDeploy.js";import{checkDeployPathStackAvailability as pe}from"../activeDeploymentGuard.js";import{assertLeaseBeforeStack as H}from"./leaseGate.js";import{collectFullDeployArtefacts as Je}from"./artefactCollection.js";import{CfnRegistryService as Ye}from"../../services/infrastructure/CfnRegistryService.js";import{ChangeSetProbe as Qe}from"../../services/infrastructure/changeSetProbe.js";import{runApprovalGate as Xe}from"./approvalGate.js";import{buildDestructionTicket as Ze,runDestructionGate as et}from"./destructionGate.js";import{DESTRUCTION_RESUME_TTL_MS as tt,signApprovalToken as rt}from"./plan/approvalToken.js";import{computeAssemblyDigest as nt}from"./plan/assemblyDigest.js";import{createMemoisedPlanLoader as at}from"./plan/loadDeployPlan.js";import{runDriftPreFlightGate as ot}from"../drift/preFlightGate.js";import{completeForgetAfterDeploy as it}from"../remediation/forgetResource.js";import{composePostFailureRepairOffer as fe}from"../remediation/postFailureRepairOffer.js";import{renderPinReportLines as st,runPinRemediation as ct}from"../remediation/pinRemediation.js";import{runRecreatePreFlight as lt}from"../remediation/recreatePreFlight.js";import{runRecreateSurgery as dt}from"../remediation/recreateResource.js";const F="applicationDeploy";function ut(e){const n=Object.entries(e);if(n.length===0)return;const a={};for(const[t,i]of n){const h=Ne(t);a[h]=i}return a}N(ut,"buildCdkImageTagParameters");function gt(e){if(e?.mode!==void 0&&e.mode!=="rollback"){if(e.imageTag)return d(new Error(`--image-tag requires rollback mode (explicit mode '${e.mode}' was supplied alongside an image tag)`));if(e.serviceImageTags)return d(new Error(`serviceImageTags requires rollback mode (explicit mode '${e.mode}' was supplied alongside serviceImageTags)`))}if(e?.mode==="rollback"){if(e.imageTag&&e.serviceImageTags)return d(new Error("rollback mode accepts exactly one of --image-tag or serviceImageTags \u2014 they are mutually exclusive"));if(!e.imageTag&&!e.serviceImageTags)return d(new Error("rollback mode requires --image-tag <tag> or serviceImageTags naming the image(s) to roll to"))}if(e?.serviceImageTags!==void 0){if(e.serviceName)return d(new Error("serviceImageTags already names its services \u2014 --service cannot be combined with it"));const n=Object.entries(e.serviceImageTags);if(n.length===0)return d(new Error("serviceImageTags must name at least one service"));for(const[a,t]of n){const i=Ee.safeParse(t);if(!i.success)return d(new Error(`serviceImageTags.${a} is not a valid image tag: ${i.error.message}`))}}return P(void 0)}N(gt,"validateRollbackOptions");function me(e,n,a){if(e.onStackTargets===void 0||a.length===0)return;const t=n.awsProvider.getAccountId(),i=n.awsProvider.getRegion();e.onStackTargets(a.map(h=>({stackName:h,...t!==""?{accountId:t}:{},...i!==""?{region:i}:{}})))}N(me,"emitStackTargets");async function Qt(e,n,a){const{callbacks:t,options:i}=e,h=Date.now(),$=i?.mode??(i?.imageTag||i?.serviceImageTags?"rollback":i?.deployOnly?"code-only":"full");if($==="restart"){const r=new Error('mode "restart" is not a deploy \u2014 call the restart() entry point (fjall rollout) instead');return t.onError?.(r),d(r)}const z=gt(i);if(!z.success){const r=S(z.error.message),o=new Error(r);return t.onError?.(o),d(o)}const x=Re.buildDeploymentContext({deployType:"application",target:a.appName,path:a.path,region:n.awsProvider.getRegion(),callerIdentity:Pe(n.awsProvider.getAccountId()),...Ce({orgConfig:e.orgConfig,identity:e.identity,skipOidc:e.options?.skipOidc}),...e.managedDomainBindings!==void 0?{managedDomainBindings:e.managedDomainBindings}:{}},{verbose:i?.verbose,infraOnly:i?.infraOnly},e.orgConfig),b=n.frameworkRegistry.resolve({appPath:a.path});let J;const O=$==="rollback";if(b&&!O){J=b.builder.plan({appPath:a.path},b.detection);const r=Ke(t),o=await b.builder.build(a.path,J,r,{skipBuild:i?.skipBuild,infraOnly:i?.infraOnly,...e.abortSignal!==void 0&&{abortSignal:e.abortSignal}});if(!o.success){const s=new Error(S(o.error.message));return t.onError?.(s),d(s)}}if($==="code-only"||O){t.onLog?.(O?i?.serviceImageTags?`Rollback mode \u2014 rolling ${Object.keys(i.serviceImageTags).length} service(s) to their release-recorded image tags`:`Rollback mode \u2014 rolling to image tag ${i?.imageTag}`:"Deploy-only mode \u2014 skipping infrastructure pipeline","info");const r=ce(a.path,"cdk.out"),o=De(r),s=o.declaresBuild,g=b?.detection.hasDockerfile===!0,c=s||g;B.debug(F,"Deploy-only branch entered",{mode:$,imageTag:i?.imageTag,serviceImageTags:i?.serviceImageTags,appName:a.appName,appPath:a.path,cdkOutPath:r,dockerProviderAvailable:e.dockerProvider!==void 0,builderName:b?.builder.name,hasDockerfileFromManifest:s,hasDockerfileFromDisk:g,hasDockerfile:c,manifestDockerServiceCount:o.ecsServices.length,manifestDockerPaths:o.ecsServices.map(p=>p.docker.path),manifestLambdaDockerEntryCount:o.lambdaEntries.length}),!s&&!g&&t.onLog?.("No Dockerfile detected via manifest or appPath \u2014 skipping Docker build. If this app uses a cross-repo Dockerfile, ensure a full deploy has run first to populate cdk.out/fjall-manifest.json.","warn");const u=await pe([I(a.appName,D.COMPUTE)],n.cfnService,e.abortSignal);if(!u.success)return t.onError?.(u.error),d(u.error);me(t,n,[I(a.appName,D.COMPUTE)]);let y={},l;if(!O&&e.dockerProvider!==void 0&&c){B.debug(F,"Running Docker build before code-only deploy",{source:s?"manifest":"disk"});const p=await $e(e,n,a,t);if(!p.success)return d(p.error);y=p.data.contentHashTagsByService,l=p.data}else B.debug(F,"Skipping Docker build",{reason:O?"rollback":e.dockerProvider===void 0?"no dockerProvider":"no Dockerfile detected"});const k=await H(e.assertLeaseForStack,I(a.appName,D.COMPUTE),e.abortSignal);return k!==void 0?(t.onError?.(k),d(k)):ze(e,n,a,y,l)}t.onLog?.("Analysing infrastructure\u2026","info");const j=await Oe(a,n,x,t,e.abortSignal);if(!j.success){const r=new Error(S(j.error.message),{cause:j.error});return t.onError?.(r),d(r)}const m=j.data;try{await t.onDetectionComplete?.({...m,builderName:b?.builder.name??"unknown"})}catch(r){const o=new Error(S(le(r)),{cause:r});return t.onError?.(o),d(o)}const ke={deploymentType:"application",operation:"deploy",deployOnly:!1,infraOnly:i?.infraOnly??!1,hasDockerfile:m.hasDockerfile,pattern:m.pattern,resources:m.resources,...b&&{builderName:b.builder.name}},Y=Le.getSteps(ke),Q=Y.findIndex(r=>r.id===_.TARGET_READINESS),X=await Fe(t,{stepId:_.TARGET_READINESS,stepName:Ae.TARGET_READINESS,...Q>=0&&{stepIndex:Q,totalSteps:Y.length}},async()=>{if(i?.skipReadinessCheck)return t.onLog?.("Skipping target readiness check (--skip-readiness-check)","warn"),{kind:"skipped",data:void 0};const r=n.awsProvider.getAccountId(),o=e.orgConfig?.providerAccounts.find(u=>u.id===r)?.name,s=n.awsProvider.getCredentials(),g=Be(e.orgConfig),c=await Me({cloudFormation:n.awsProvider.getClient(we),ec2:new Te({region:g,...s!==void 0&&{credentials:s}})},{id:r,...o!==void 0&&{name:o}},n.awsProvider.getRegion(),e.orgConfig,e.abortSignal);return c.success?c.data.ready?{kind:"completed",data:void 0}:{kind:"error",error:new Error(S(c.data.advisory))}:{kind:"error",error:new Error(S(c.error.message),{cause:c.error})}},e.abortSignal);if(!X.success)return X;const w=ve({pattern:m.pattern,resources:m.resources}),v=w.length,Z=m.hasDockerfile&&e.dockerProvider!==void 0&&w.includes(D.COMPUTE)&&i?.infraOnly!==!0;if(!m.hasDifferences&&!i?.force&&!Z){t.onLog?.("No infrastructure changes detected","info"),t.onLog?.(i?.infraOnly===!0?"Infrastructure-only mode \u2014 application code was not rebuilt. Run with --deploy-only to deploy code changes.":"Nothing to deploy. Run with --deploy-only to redeploy application code.","info");const r=m.hasDockerfile&&e.dockerProvider!==void 0&&w.includes(D.COMPUTE);for(let s=0;s<w.length;s++){const g=w[s];r&&g===D.COMPUTE&&(t.onStepStart?.(_.DOCKER_OPERATIONS,ge),t.onStepComplete?.(_.DOCKER_OPERATIONS,ge,"skipped"));const c=ue(g,"deploy"),u=de(g,"deploy");t.onStepStart?.(c,u,s,v),t.onStepComplete?.(c,u,"skipped",s,v)}const o=await n.stackService.resolveWebsiteUrl(a.appName);return P({target:a.appName,deploymentType:"application",outputs:o?{websiteUrl:o}:void 0,noChanges:!0,artefacts:[],durationMs:Date.now()-h})}const G=ce(a.path,"cdk.out"),E=je({deployOrder:w,stackChanges:m.stackChanges,appName:a.appName,force:i?.force===!0,computeBuildPossible:Z});me(t,n,[...E]);const K=await pe([...E],n.cfnService,e.abortSignal);if(!K.success)return t.onError?.(K.error),d(K.error);const ee=nt(m.currentHashes),W=at({cfnService:n.cfnService,cdkOutPath:G,changedStacks:[...E],assemblyDigest:ee,registry:new Ye(n.awsProvider),changeSetProbe:new Qe(n.awsProvider),...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}}),q=await ot(n.awsProvider,{stackNames:[...E],cdkOutPath:G,cfnService:n.cfnService,callbacks:t,...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}});if(q.kind==="blocked"){const r=new Error(q.message);return t.onError?.(r),d(r)}const te=q.verifiedAbsentByNecessity??[],re=e.approvalGate!==void 0||i?.approvalToken!==void 0&&i.approvalToken!==""||i?.planOnly===!0;if(re){const r=await Xe({approvalGate:e.approvalGate,options:i??{},target:a.appName,cdkOutPath:G,detection:{stackChanges:m.stackChanges,currentHashes:m.currentHashes},cfnService:n.cfnService,loadPlan:W,callbacks:t,...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}});if(!r.success)return r;const o=r.data;if(o.kind==="rejected")return t.onLog?.(`Deploy rejected: ${S(o.reason)}`,"warn"),P({target:a.appName,deploymentType:"application",rejected:!0,artefacts:[],...o.planDetail!==void 0?{planDetail:o.planDetail}:{},durationMs:Date.now()-h});if(o.kind==="awaiting"){const s=await W(),g=s.success?Ze(s.data,te):void 0;return P({target:a.appName,deploymentType:"application",awaitingApproval:!0,approvalToken:o.approvalToken,assemblyDigest:o.assemblyDigest,planDetail:o.planDetail,...g!==void 0?{destructionTicket:g}:{},artefacts:[],durationMs:Date.now()-h})}}const T=await et({loadPlan:W,options:i??{},target:a.appName,callbacks:t,verifiedAbsentByNecessity:te,...e.destructionConsentGate!==void 0?{consentGate:e.destructionConsentGate}:{}});if(!T.success)return t.onError?.(T.error),d(T.error);if(T.data.kind==="withheld"){const r=re?rt({assemblyDigest:ee,ttlMs:tt}):void 0;return P({target:a.appName,deploymentType:"application",destructionPending:!0,destructionTicket:T.data.ticket,destructionOutcome:T.data.outcome,destructionHaltReasons:T.data.reasons,...r!==void 0?{destructionResumeToken:r.token,destructionResumeExpiresAt:r.expiresAt}:{},artefacts:[],durationMs:Date.now()-h})}if(T.data.kind==="proceed"){const{ticket:r,verbsByPhysicalName:o}=T.data,s=r.resources.filter(c=>o.get(c.physicalName)==="pin");if(s.length>0){t.onLog?.(`Pin remediation for ${a.appName}: probing live value(s) for ${s.length} consented pin(s) \u2014 deploy stopped for config write-back; nothing was applied`,"warn");const c=await ct(n.awsProvider,{targets:s.map(u=>({stack:u.stack,logicalId:u.logicalId,resourceType:u.resourceType,physicalName:u.physicalName,offendingProperties:u.offendingProperties})),...e.infrastructureSource!==void 0?{infrastructureContent:e.infrastructureSource.content,infrastructureFilePath:e.infrastructureSource.filePath}:{},...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}});for(const u of st(c))t.onLog?.(u,"warn");return s.length<r.resources.length&&t.onLog?.("Remaining consented change(s) were not applied \u2014 the config write-back changes the plan, so they re-consent on the next deploy","warn"),P({target:a.appName,deploymentType:"application",pinPending:!0,pinReport:c,pinDeferredConsents:r.resources.length-s.length,destructionTicket:r,artefacts:[],durationMs:Date.now()-h})}const g=r.resources.filter(c=>c.finding==="pinned-name-replacement"&&o.get(c.physicalName)==="recreate");if(g.length>0){const c=new Map;for(const l of g){const k=c.get(l.stack)??[];k.push(l),c.set(l.stack,k)}const u=await lt(n.awsProvider,{stacks:[...c].map(([l,k])=>({stackName:l,targets:k.map(p=>({logicalId:p.logicalId,resourceType:p.resourceType,physicalId:p.physicalName}))})),...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}});if(!u.success){const l=new Error(S(u.error.message),{cause:u.error});return t.onError?.(l),d(l)}const y=[];for(const[l,k]of c){const p=await H(e.assertLeaseForStack,l,e.abortSignal,"destroy");if(p!==void 0)return y.length>0&&t.onLog?.(`Surgery already completed on ${y.join("; ")} \u2014 those resources were removed and re-running the deploy resumes their journals and re-creates them.`,"warn"),t.onError?.(p),d(p);const C=k.map(f=>f.physicalName);t.onLog?.(`Recreate remediation for ${a.appName}: orchestrating the pinned-name replacement of ${C.join(", ")} on ${l}`,"warn");const M=await dt(n.awsProvider,{stackName:l,targets:k.map(f=>({logicalId:f.logicalId,resourceType:f.resourceType,physicalId:f.physicalName})),appName:a.appName,consentedPhysicalNames:C,doneJournalPolicy:"archive-and-restart",plannedReplacementLogicalIds:k.map(f=>f.logicalId),onProgress:N(f=>t.onLog?.(f,"info"),"onProgress"),...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}});if(!M.success){const f=y.length>0?` Surgery already completed on ${y.join("; ")} \u2014 those resources were removed and re-running the deploy resumes their journals and re-creates them.`:"",U=new Error(S(`${M.error.message}${f}`));return t.onError?.(U),d(U)}y.push(`${l} (${C.join(", ")})`)}}}const ne=await Ie(n,x,t);if(!ne.success)return ne;const ae=I(a.appName,D.DATABASE),V=E.has(ae),ye=V?await We(n.cfnService,a.appName,e.abortSignal):void 0,Se=V?await qe(n.ecsResolver,n.ecsService,a.appName,e.abortSignal):void 0,A={},L=new Map;let oe,ie={};const R=N(async()=>{!V||!L.has(ae)||await Ve({appName:a.appName,connectionOutputsBefore:ye,serviceTaskDefinitionsBefore:Se,services:{cfnService:n.cfnService,ecsService:n.ecsService,ecsResolver:n.ecsResolver},callbacks:t,...e.confirmDatabaseEndpointRestart!==void 0?{confirmRestart:e.confirmDatabaseEndpointRestart}:{},...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}})},"reconcileIfDatabaseReplaced");for(let r=0;r<w.length;r++){const o=w[r],s=I(a.appName,o),g=ue(o,"deploy"),c=de(o,"deploy");if(!E.has(s)){t.onStepStart?.(g,c,r,v),t.onLog?.(`Skipping ${o} \u2014 no changes detected`,"info"),t.onStepComplete?.(g,c,"skipped",r,v);continue}const u=await H(e.assertLeaseForStack,s,e.abortSignal);if(u!==void 0)return await R(),t.onError?.(u),d(u);const y=xe(w,r,a.appName,E);if(y.length>=2){const f=await Ge(y,a,n,x,t,r,v,m,A,L);if(!f.success)return await R(),d(await fe(n.awsProvider,{stackNames:y.map(U=>I(a.appName,U)),appName:a.appName,originalError:f.error,cfnService:n.cfnService,...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}}));r+=y.length-1;continue}const l=await _e(o,e,n,a,t,m.hasDockerfile);if(l!==null&&!l.success)return await R(),d(l.error);l!==null&&l.success&&(oe=l.data);const k=l!==null&&l.success?l.data.contentHashTagsByService:{},p=ut(k);if(l!==null){const f=await H(e.assertLeaseForStack,s,e.abortSignal);if(f!==void 0)return await R(),t.onError?.(f),d(f)}const C=await Ue(o,n,x,t,r,v,A,p!==void 0||e.abortSignal!==void 0?{...p!==void 0?{parameters:p}:{},...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}}:void 0);if(!C.success)return await R(),d(await fe(n.awsProvider,{stackNames:[s],appName:a.appName,originalError:C.error,cfnService:n.cfnService,...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}}));o===D.COMPUTE&&(ie=await He(a,n,t,k,e.abortSignal));const M=m.currentHashes.get(s);M&&L.set(s,M)}if(await R(),L.size>0){const r=await n.hashService.updateStateAfterDeploy(a.path,L);if(!r.success){const o=S(r.error.message);B.debug(F,"Failed to update state file",{error:o}),t.onLog?.(`Warning: failed to update state file \u2014 next deploy may re-deploy unchanged stacks: ${o}`,"warn")}}const se=await n.stackService.resolveWebsiteUrl(a.appName);se&&(A.websiteUrl=se);const he=await Je(n,a.appName,oe,t,G,ie,e.abortSignal);return await it(n.awsProvider,{stackNames:[...E],appName:a.appName,onLog:N((r,o)=>t.onLog?.(r,o),"onLog"),...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}}).catch(r=>{B.debug(F,"Forget completion sweep failed",{error:S(le(r))})}),P({target:a.appName,deploymentType:"application",outputs:Object.keys(A).length>0?A:void 0,artefacts:he,durationMs:Date.now()-h})}N(Qt,"deployApplication");export{Qt as deployApplication,me as emitStackTargets};
|
|
1
|
+
var Te=Object.defineProperty;var N=(e,n)=>Te(e,"name",{value:n,configurable:!0});import{join as ce}from"path";import{CloudFormationClient as Ee}from"@aws-sdk/client-cloudformation";import{EC2Client as Ne}from"@aws-sdk/client-ec2";import{success as D,failure as d}from"@fjall/generator";import{logger as B}from"@fjall/util/logger";import{ImageTagSchema as Pe,getErrorMessage as le,imageTagParameterName as De,maskSensitiveOutput as S}from"@fjall/util";import{parseDockerDeclarationsFromManifest as ve}from"@fjall/util/manifest";import{stubCallerIdentity as Re}from"../../types/deployment/index.js";import{getApplicationDeployOrder as Ce,getApplicationStackName as I,getApplicationStepName as de,getApplicationStepId as ue,APPLICATION_STACKS as P}from"../../types/operations.js";import{CdkContextBuilder as Ie}from"../../services/supporting/CdkContextBuilder.js";import{buildParamsContext as Oe,bootstrapOrFail as Ae}from"../contextHelpers.js";import{buildBootstrapTagsIfResolved as Le}from"../bootstrapTags.js";import{hasOrganisationTierAccount as Me}from"../../aws/organisations/accountGlobals.js";import{runDetectionPipeline as Be}from"./detectionPipeline.js";import{STEP_IDS as _,STEP_NAMES as Fe}from"../../types/stepDefinitions.js";import{StepRegistry as $e}from"../../steps/stepRegistry.js";import{checkTargetReadiness as xe}from"../../aws/targetReadiness.js";import{cascadeHomeRegion as je}from"../organisation/cascadeHelpers.js";import{withStepLifecycle as Ge}from"../stepLifecycle.js";import{DOCKER_BUILD_STEP_NAME as ge,runDockerBuild as Ue}from"./dockerBuildHelper.js";import{getParallelPhase2Stacks as _e,getStackNamesToDeploy as He,deployParallelPhase as Ke,deployStackSequential as We,runDockerPreCompute as qe,runLambdaVersionPublish as Ve,createBuildCallbacks as ze}from"./applicationDeployHelpers.js";import{captureDatabaseConnectionOutputs as Je,captureServiceTaskDefinitions as Ye,reconcileDatabaseEndpointChange as Qe}from"./databaseEndpointReconcile.js";import{deployCodeOnly as Xe}from"./codeOnlyDeploy.js";import{checkDeployPathStackAvailability as fe}from"../activeDeploymentGuard.js";import{assertLeaseBeforeStack as H}from"./leaseGate.js";import{collectFullDeployArtefacts as Ze}from"./artefactCollection.js";import{CfnRegistryService as et}from"../../services/infrastructure/CfnRegistryService.js";import{ChangeSetProbe as tt}from"../../services/infrastructure/changeSetProbe.js";import{runApprovalGate as rt}from"./approvalGate.js";import{buildDestructionTicket as nt,runDestructionGate as at}from"./destructionGate.js";import{DESTRUCTION_RESUME_TTL_MS as ot,signApprovalToken as it}from"./plan/approvalToken.js";import{computeAssemblyDigest as st}from"./plan/assemblyDigest.js";import{createMemoisedPlanLoader as ct}from"./plan/loadDeployPlan.js";import{runDriftPreFlightGate as lt}from"../drift/preFlightGate.js";import{completeForgetAfterDeploy as dt}from"../remediation/forgetResource.js";import{composePostFailureRepairOffer as pe}from"../remediation/postFailureRepairOffer.js";import{renderPinReportLines as ut,runPinRemediation as gt}from"../remediation/pinRemediation.js";import{runRecreatePreFlight as ft}from"../remediation/recreatePreFlight.js";import{runRecreateSurgery as pt}from"../remediation/recreateResource.js";const F="applicationDeploy";function mt(e){const n=Object.entries(e);if(n.length===0)return;const a={};for(const[t,i]of n){const h=De(t);a[h]=i}return a}N(mt,"buildCdkImageTagParameters");function kt(e){if(e?.mode!==void 0&&e.mode!=="rollback"){if(e.imageTag)return d(new Error(`--image-tag requires rollback mode (explicit mode '${e.mode}' was supplied alongside an image tag)`));if(e.serviceImageTags)return d(new Error(`serviceImageTags requires rollback mode (explicit mode '${e.mode}' was supplied alongside serviceImageTags)`))}if(e?.mode==="rollback"){if(e.imageTag&&e.serviceImageTags)return d(new Error("rollback mode accepts exactly one of --image-tag or serviceImageTags \u2014 they are mutually exclusive"));if(!e.imageTag&&!e.serviceImageTags)return d(new Error("rollback mode requires --image-tag <tag> or serviceImageTags naming the image(s) to roll to"))}if(e?.serviceImageTags!==void 0){if(e.serviceName)return d(new Error("serviceImageTags already names its services \u2014 --service cannot be combined with it"));const n=Object.entries(e.serviceImageTags);if(n.length===0)return d(new Error("serviceImageTags must name at least one service"));for(const[a,t]of n){const i=Pe.safeParse(t);if(!i.success)return d(new Error(`serviceImageTags.${a} is not a valid image tag: ${i.error.message}`))}}return D(void 0)}N(kt,"validateRollbackOptions");function me(e,n,a){if(e.onStackTargets===void 0||a.length===0)return;const t=n.awsProvider.getAccountId(),i=n.awsProvider.getRegion();e.onStackTargets(a.map(h=>({stackName:h,...t!==""?{accountId:t}:{},...i!==""?{region:i}:{}})))}N(me,"emitStackTargets");async function nr(e,n,a){const{callbacks:t,options:i}=e,h=Date.now(),$=i?.mode??(i?.imageTag||i?.serviceImageTags?"rollback":i?.deployOnly?"code-only":"full");if($==="restart"){const r=new Error('mode "restart" is not a deploy \u2014 call the restart() entry point (fjall rollout) instead');return t.onError?.(r),d(r)}const z=kt(i);if(!z.success){const r=S(z.error.message),o=new Error(r);return t.onError?.(o),d(o)}const x=Ie.buildDeploymentContext({deployType:"application",target:a.appName,path:a.path,region:n.awsProvider.getRegion(),callerIdentity:Re(n.awsProvider.getAccountId()),...Oe({orgConfig:e.orgConfig,identity:e.identity,skipOidc:e.options?.skipOidc}),...e.managedDomainBindings!==void 0?{managedDomainBindings:e.managedDomainBindings}:{}},{verbose:i?.verbose,infraOnly:i?.infraOnly},e.orgConfig),b=n.frameworkRegistry.resolve({appPath:a.path});let J;const O=$==="rollback";if(b&&!O){J=b.builder.plan({appPath:a.path},b.detection);const r=ze(t),o=await b.builder.build(a.path,J,r,{skipBuild:i?.skipBuild,infraOnly:i?.infraOnly,...e.abortSignal!==void 0&&{abortSignal:e.abortSignal}});if(!o.success){const s=new Error(S(o.error.message));return t.onError?.(s),d(s)}}if($==="code-only"||O){t.onLog?.(O?i?.serviceImageTags?`Rollback mode \u2014 rolling ${Object.keys(i.serviceImageTags).length} service(s) to their release-recorded image tags`:`Rollback mode \u2014 rolling to image tag ${i?.imageTag}`:"Deploy-only mode \u2014 skipping infrastructure pipeline","info");const r=ce(a.path,"cdk.out"),o=ve(r),s=o.declaresBuild,g=b?.detection.hasDockerfile===!0,c=s||g;B.debug(F,"Deploy-only branch entered",{mode:$,imageTag:i?.imageTag,serviceImageTags:i?.serviceImageTags,appName:a.appName,appPath:a.path,cdkOutPath:r,dockerProviderAvailable:e.dockerProvider!==void 0,builderName:b?.builder.name,hasDockerfileFromManifest:s,hasDockerfileFromDisk:g,hasDockerfile:c,manifestDockerServiceCount:o.ecsServices.length,manifestDockerPaths:o.ecsServices.map(f=>f.docker.path),manifestLambdaDockerEntryCount:o.lambdaEntries.length}),!s&&!g&&t.onLog?.("No Dockerfile detected via manifest or appPath \u2014 skipping Docker build. If this app uses a cross-repo Dockerfile, ensure a full deploy has run first to populate cdk.out/fjall-manifest.json.","warn");const u=await fe([I(a.appName,P.COMPUTE)],n.cfnService,e.abortSignal);if(!u.success)return t.onError?.(u.error),d(u.error);me(t,n,[I(a.appName,P.COMPUTE)]);let y={},l;if(!O&&e.dockerProvider!==void 0&&c){B.debug(F,"Running Docker build before code-only deploy",{source:s?"manifest":"disk"});const f=await Ue(e,n,a,t);if(!f.success)return d(f.error);y=f.data.contentHashTagsByService,l=f.data}else B.debug(F,"Skipping Docker build",{reason:O?"rollback":e.dockerProvider===void 0?"no dockerProvider":"no Dockerfile detected"});const k=await H(e.assertLeaseForStack,I(a.appName,P.COMPUTE),e.abortSignal);return k!==void 0?(t.onError?.(k),d(k)):Xe(e,n,a,y,l)}t.onLog?.("Analysing infrastructure\u2026","info");const j=await Be(a,n,x,t,e.abortSignal);if(!j.success){const r=new Error(S(j.error.message),{cause:j.error});return t.onError?.(r),d(r)}const m=j.data;try{await t.onDetectionComplete?.({...m,builderName:b?.builder.name??"unknown"})}catch(r){const o=new Error(S(le(r)),{cause:r});return t.onError?.(o),d(o)}const ke={deploymentType:"application",operation:"deploy",deployOnly:!1,infraOnly:i?.infraOnly??!1,hasDockerfile:m.hasDockerfile,pattern:m.pattern,resources:m.resources,...b&&{builderName:b.builder.name}},Y=$e.getSteps(ke),Q=Y.findIndex(r=>r.id===_.TARGET_READINESS),X=await Ge(t,{stepId:_.TARGET_READINESS,stepName:Fe.TARGET_READINESS,...Q>=0&&{stepIndex:Q,totalSteps:Y.length}},async()=>{if(i?.skipReadinessCheck)return t.onLog?.("Skipping target readiness check (--skip-readiness-check)","warn"),{kind:"skipped",data:void 0};const r=n.awsProvider.getAccountId(),o=e.orgConfig?.providerAccounts.find(u=>u.id===r)?.name,s=n.awsProvider.getCredentials(),g=je(e.orgConfig),c=await xe({cloudFormation:n.awsProvider.getClient(Ee),ec2:new Ne({region:g,...s!==void 0&&{credentials:s}})},{id:r,...o!==void 0&&{name:o}},n.awsProvider.getRegion(),e.orgConfig,e.abortSignal);return c.success?c.data.ready?{kind:"completed",data:void 0}:{kind:"error",error:new Error(S(c.data.advisory))}:{kind:"error",error:new Error(S(c.error.message),{cause:c.error})}},e.abortSignal);if(!X.success)return X;const w=Ce({pattern:m.pattern,resources:m.resources}),v=w.length,Z=m.hasDockerfile&&e.dockerProvider!==void 0&&w.includes(P.COMPUTE)&&i?.infraOnly!==!0;if(!m.hasDifferences&&!i?.force&&!Z){t.onLog?.("No infrastructure changes detected","info"),t.onLog?.(i?.infraOnly===!0?"Infrastructure-only mode \u2014 application code was not rebuilt. Run with --deploy-only to deploy code changes.":"Nothing to deploy. Run with --deploy-only to redeploy application code.","info");const r=m.hasDockerfile&&e.dockerProvider!==void 0&&w.includes(P.COMPUTE);for(let s=0;s<w.length;s++){const g=w[s];r&&g===P.COMPUTE&&(t.onStepStart?.(_.DOCKER_OPERATIONS,ge),t.onStepComplete?.(_.DOCKER_OPERATIONS,ge,"skipped"));const c=ue(g,"deploy"),u=de(g,"deploy");t.onStepStart?.(c,u,s,v),t.onStepComplete?.(c,u,"skipped",s,v)}const o=await n.stackService.resolveWebsiteUrl(a.appName);return D({target:a.appName,deploymentType:"application",outputs:o?{websiteUrl:o}:void 0,noChanges:!0,artefacts:[],durationMs:Date.now()-h})}const G=ce(a.path,"cdk.out"),E=He({deployOrder:w,stackChanges:m.stackChanges,appName:a.appName,force:i?.force===!0,computeBuildPossible:Z});me(t,n,[...E]);const K=await fe([...E],n.cfnService,e.abortSignal);if(!K.success)return t.onError?.(K.error),d(K.error);const ee=st(m.currentHashes),W=ct({cfnService:n.cfnService,cdkOutPath:G,changedStacks:[...E],assemblyDigest:ee,registry:new et(n.awsProvider),changeSetProbe:new tt(n.awsProvider),...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}}),q=await lt(n.awsProvider,{stackNames:[...E],cdkOutPath:G,cfnService:n.cfnService,callbacks:t,...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}});if(q.kind==="blocked"){const r=new Error(q.message);return t.onError?.(r),d(r)}const te=q.verifiedAbsentByNecessity??[],re=e.approvalGate!==void 0||i?.approvalToken!==void 0&&i.approvalToken!==""||i?.planOnly===!0;if(re){const r=await rt({approvalGate:e.approvalGate,options:i??{},target:a.appName,cdkOutPath:G,detection:{stackChanges:m.stackChanges,currentHashes:m.currentHashes},cfnService:n.cfnService,loadPlan:W,callbacks:t,...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}});if(!r.success)return r;const o=r.data;if(o.kind==="rejected")return t.onLog?.(`Deploy rejected: ${S(o.reason)}`,"warn"),D({target:a.appName,deploymentType:"application",rejected:!0,artefacts:[],...o.planDetail!==void 0?{planDetail:o.planDetail}:{},durationMs:Date.now()-h});if(o.kind==="awaiting"){const s=await W(),g=s.success?nt(s.data,te):void 0;return D({target:a.appName,deploymentType:"application",awaitingApproval:!0,approvalToken:o.approvalToken,assemblyDigest:o.assemblyDigest,planDetail:o.planDetail,...g!==void 0?{destructionTicket:g}:{},artefacts:[],durationMs:Date.now()-h})}}const T=await at({loadPlan:W,options:i??{},target:a.appName,callbacks:t,verifiedAbsentByNecessity:te,...e.destructionConsentGate!==void 0?{consentGate:e.destructionConsentGate}:{}});if(!T.success)return t.onError?.(T.error),d(T.error);if(T.data.kind==="withheld"){const r=re?it({assemblyDigest:ee,ttlMs:ot}):void 0;return D({target:a.appName,deploymentType:"application",destructionPending:!0,destructionTicket:T.data.ticket,destructionOutcome:T.data.outcome,destructionHaltReasons:T.data.reasons,...r!==void 0?{destructionResumeToken:r.token,destructionResumeExpiresAt:r.expiresAt}:{},artefacts:[],durationMs:Date.now()-h})}if(T.data.kind==="proceed"){const{ticket:r,verbsByPhysicalName:o}=T.data,s=r.resources.filter(c=>o.get(c.physicalName)==="pin");if(s.length>0){t.onLog?.(`Pin remediation for ${a.appName}: probing live value(s) for ${s.length} consented pin(s) \u2014 deploy stopped for config write-back; nothing was applied`,"warn");const c=await gt(n.awsProvider,{targets:s.map(u=>({stack:u.stack,logicalId:u.logicalId,resourceType:u.resourceType,physicalName:u.physicalName,offendingProperties:u.offendingProperties})),...e.infrastructureSource!==void 0?{infrastructureContent:e.infrastructureSource.content,infrastructureFilePath:e.infrastructureSource.filePath}:{},...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}});for(const u of ut(c))t.onLog?.(u,"warn");return s.length<r.resources.length&&t.onLog?.("Remaining consented change(s) were not applied \u2014 the config write-back changes the plan, so they re-consent on the next deploy","warn"),D({target:a.appName,deploymentType:"application",pinPending:!0,pinReport:c,pinDeferredConsents:r.resources.length-s.length,destructionTicket:r,artefacts:[],durationMs:Date.now()-h})}const g=r.resources.filter(c=>c.finding==="pinned-name-replacement"&&o.get(c.physicalName)==="recreate");if(g.length>0){const c=new Map;for(const l of g){const k=c.get(l.stack)??[];k.push(l),c.set(l.stack,k)}const u=await ft(n.awsProvider,{stacks:[...c].map(([l,k])=>({stackName:l,targets:k.map(f=>({logicalId:f.logicalId,resourceType:f.resourceType,physicalId:f.physicalName}))})),...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}});if(!u.success){const l=new Error(S(u.error.message),{cause:u.error});return t.onError?.(l),d(l)}const y=[];for(const[l,k]of c){const f=await H(e.assertLeaseForStack,l,e.abortSignal,"destroy");if(f!==void 0)return y.length>0&&t.onLog?.(`Surgery already completed on ${y.join("; ")} \u2014 those resources were removed and re-running the deploy resumes their journals and re-creates them.`,"warn"),t.onError?.(f),d(f);const C=k.map(p=>p.physicalName);t.onLog?.(`Recreate remediation for ${a.appName}: orchestrating the pinned-name replacement of ${C.join(", ")} on ${l}`,"warn");const M=await pt(n.awsProvider,{stackName:l,targets:k.map(p=>({logicalId:p.logicalId,resourceType:p.resourceType,physicalId:p.physicalName})),appName:a.appName,consentedPhysicalNames:C,doneJournalPolicy:"archive-and-restart",plannedReplacementLogicalIds:k.map(p=>p.logicalId),onProgress:N(p=>t.onLog?.(p,"info"),"onProgress"),...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}});if(!M.success){const p=y.length>0?` Surgery already completed on ${y.join("; ")} \u2014 those resources were removed and re-running the deploy resumes their journals and re-creates them.`:"",U=new Error(S(`${M.error.message}${p}`));return t.onError?.(U),d(U)}y.push(`${l} (${C.join(", ")})`)}}}const ye=e.orgConfig!==void 0&&!Me(e.orgConfig.providerAccounts),Se=Le({account:e.orgConfig?.providerAccounts.find(r=>r.id===n.awsProvider.getAccountId()),estate:ye?"solo":"org"}),ne=await Ae(n,x,t,Se);if(!ne.success)return ne;const ae=I(a.appName,P.DATABASE),V=E.has(ae),he=V?await Je(n.cfnService,a.appName,e.abortSignal):void 0,be=V?await Ye(n.ecsResolver,n.ecsService,a.appName,e.abortSignal):void 0,A={},L=new Map;let oe,ie={};const R=N(async()=>{!V||!L.has(ae)||await Qe({appName:a.appName,connectionOutputsBefore:he,serviceTaskDefinitionsBefore:be,services:{cfnService:n.cfnService,ecsService:n.ecsService,ecsResolver:n.ecsResolver},callbacks:t,...e.confirmDatabaseEndpointRestart!==void 0?{confirmRestart:e.confirmDatabaseEndpointRestart}:{},...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}})},"reconcileIfDatabaseReplaced");for(let r=0;r<w.length;r++){const o=w[r],s=I(a.appName,o),g=ue(o,"deploy"),c=de(o,"deploy");if(!E.has(s)){t.onStepStart?.(g,c,r,v),t.onLog?.(`Skipping ${o} \u2014 no changes detected`,"info"),t.onStepComplete?.(g,c,"skipped",r,v);continue}const u=await H(e.assertLeaseForStack,s,e.abortSignal);if(u!==void 0)return await R(),t.onError?.(u),d(u);const y=_e(w,r,a.appName,E);if(y.length>=2){const p=await Ke(y,a,n,x,t,r,v,m,A,L);if(!p.success)return await R(),d(await pe(n.awsProvider,{stackNames:y.map(U=>I(a.appName,U)),appName:a.appName,originalError:p.error,cfnService:n.cfnService,...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}}));r+=y.length-1;continue}const l=await qe(o,e,n,a,t,m.hasDockerfile);if(l!==null&&!l.success)return await R(),d(l.error);l!==null&&l.success&&(oe=l.data);const k=l!==null&&l.success?l.data.contentHashTagsByService:{},f=mt(k);if(l!==null){const p=await H(e.assertLeaseForStack,s,e.abortSignal);if(p!==void 0)return await R(),t.onError?.(p),d(p)}const C=await We(o,n,x,t,r,v,A,f!==void 0||e.abortSignal!==void 0?{...f!==void 0?{parameters:f}:{},...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}}:void 0);if(!C.success)return await R(),d(await pe(n.awsProvider,{stackNames:[s],appName:a.appName,originalError:C.error,cfnService:n.cfnService,...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}}));o===P.COMPUTE&&(ie=await Ve(a,n,t,k,e.abortSignal));const M=m.currentHashes.get(s);M&&L.set(s,M)}if(await R(),L.size>0){const r=await n.hashService.updateStateAfterDeploy(a.path,L);if(!r.success){const o=S(r.error.message);B.debug(F,"Failed to update state file",{error:o}),t.onLog?.(`Warning: failed to update state file \u2014 next deploy may re-deploy unchanged stacks: ${o}`,"warn")}}const se=await n.stackService.resolveWebsiteUrl(a.appName);se&&(A.websiteUrl=se);const we=await Ze(n,a.appName,oe,t,G,ie,e.abortSignal);return await dt(n.awsProvider,{stackNames:[...E],appName:a.appName,onLog:N((r,o)=>t.onLog?.(r,o),"onLog"),...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}}).catch(r=>{B.debug(F,"Forget completion sweep failed",{error:S(le(r))})}),D({target:a.appName,deploymentType:"application",outputs:Object.keys(A).length>0?A:void 0,artefacts:we,durationMs:Date.now()-h})}N(nr,"deployApplication");export{nr as deployApplication,me as emitStackTargets};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { ProviderAccount } from "@fjall/util/config";
|
|
2
|
+
/**
|
|
3
|
+
* Service tag value stamped on every CDKToolkit stack fjall bootstraps.
|
|
4
|
+
* A `fjall:`-prefixed tag also flips the bootstrap roles/bucket/ECR repo to
|
|
5
|
+
* IaC-managed in the posture scan, so the value doubles as the managed-by
|
|
6
|
+
* marker.
|
|
7
|
+
*/
|
|
8
|
+
export declare const CDK_BOOTSTRAP_SERVICE_TAG_VALUE = "CdkBootstrap";
|
|
9
|
+
export interface BootstrapTagSource {
|
|
10
|
+
/**
|
|
11
|
+
* The target account's provider-account entry from the org config, when the
|
|
12
|
+
* caller could resolve one (matched by AWS account id, or the cascade's
|
|
13
|
+
* per-account fan-out entry). Absent ⇒ the environment tag is omitted.
|
|
14
|
+
*/
|
|
15
|
+
account?: ProviderAccount;
|
|
16
|
+
/**
|
|
17
|
+
* AWS Organizations org id (`o-…`) when known — org-cascade always has it,
|
|
18
|
+
* single-component deploys best-effort. Absent/empty ⇒ the owner tag is
|
|
19
|
+
* omitted. Note the toolkit stack is stricter than synthesised stacks: a
|
|
20
|
+
* solo estate inside a FOREIGN organisation gets the foreign owner on its
|
|
21
|
+
* app stacks (via the `-c orgId` fallback) but never on the toolkit —
|
|
22
|
+
* `buildBootstrapTagsIfResolved` drops orgId for solo estates so the
|
|
23
|
+
* application path (which cannot derive it) doesn't strip it on the next
|
|
24
|
+
* deploy.
|
|
25
|
+
*/
|
|
26
|
+
orgId?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Derive the cost-allocation tag set for a `cdk bootstrap` invocation.
|
|
30
|
+
*
|
|
31
|
+
* - environment: the account's decoded workload stage; the organisation root
|
|
32
|
+
* (stage-less by design) falls back to "management". Structural accounts
|
|
33
|
+
* with no stage omit the tag rather than guess.
|
|
34
|
+
* - service: always "CdkBootstrap".
|
|
35
|
+
* - owner: the AWS Organizations org id when supplied — the same value the
|
|
36
|
+
* synthesised stacks' owner fallback resolves from `-c orgId`.
|
|
37
|
+
*
|
|
38
|
+
* CloudFormation REPLACES the toolkit stack's whole tag set on every tagged
|
|
39
|
+
* bootstrap and PRESERVES it on untagged ones, so for a given account this
|
|
40
|
+
* derivation must be fed the same inputs by every emitting path — callers
|
|
41
|
+
* that cannot supply the full picture must emit NO tags instead of a partial
|
|
42
|
+
* set, or each deploy type would strip the other's tags. That suppression
|
|
43
|
+
* policy lives in `buildBootstrapTagsIfResolved`, which every deploy seam
|
|
44
|
+
* routes through; this helper only derives.
|
|
45
|
+
*/
|
|
46
|
+
export declare function buildBootstrapTags({ account, orgId }: BootstrapTagSource): Record<string, string>;
|
|
47
|
+
export interface ResolvedBootstrapTagSource {
|
|
48
|
+
/** The looked-up provider account — `undefined` when the lookup missed. */
|
|
49
|
+
account: ProviderAccount | undefined;
|
|
50
|
+
/**
|
|
51
|
+
* The estate shape the CALLER established (org-managed = the org config
|
|
52
|
+
* carries an organisation-tier account). "solo" deliberately ignores any
|
|
53
|
+
* orgId supplied — the solo resolver can surface a FOREIGN org id the
|
|
54
|
+
* application path can never derive, and stamping it would have every app
|
|
55
|
+
* deploy's tag set strip it again.
|
|
56
|
+
*/
|
|
57
|
+
estate: "org" | "solo";
|
|
58
|
+
orgId?: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Emit-or-preserve gate over `buildBootstrapTags` for the REPLACE boundary:
|
|
62
|
+
* a tagged bootstrap replaces the toolkit stack's WHOLE tag set, so a seam
|
|
63
|
+
* whose lookups partially failed must emit `undefined` (preserve existing
|
|
64
|
+
* tags) rather than a partial set that strips what another deploy wrote.
|
|
65
|
+
*
|
|
66
|
+
* Returns tags only when every input the estate's complete set needs is
|
|
67
|
+
* positively resolved:
|
|
68
|
+
* - the account lookup missed (stale/reconciled config) → undefined
|
|
69
|
+
* - org estate without an org id (member resolver returned no org, option
|
|
70
|
+
* not threaded) → undefined — the complete set includes owner
|
|
71
|
+
* - solo estate → tags without owner regardless of orgId (see above)
|
|
72
|
+
*/
|
|
73
|
+
export declare function buildBootstrapTagsIfResolved({ account, estate, orgId }: ResolvedBootstrapTagSource): Record<string, string> | undefined;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var f=Object.defineProperty;var i=(e,n)=>f(e,"name",{value:n,configurable:!0});import{COST_ALLOCATION_TAGS as r,DEFAULT_COST_ALLOCATION_ENVIRONMENT as s}from"@fjall/util/aws";import{accountTier as u,stageFromWireEnvironment as T}from"@fjall/util";const E="CdkBootstrap";function d({account:e,orgId:n}){const o=T(e?.environment)??(e!==void 0&&u(e)==="organisation"?s:void 0);return{[r.SERVICE]:E,...o!==void 0?{[r.ENVIRONMENT]:o}:{},...n!==void 0&&n!==""?{[r.OWNER]:n}:{}}}i(d,"buildBootstrapTags");function _({account:e,estate:n,orgId:t}){if(e!==void 0){if(n==="solo")return d({account:e});if(!(t===void 0||t===""))return d({account:e,orgId:t})}}i(_,"buildBootstrapTagsIfResolved");export{E as CDK_BOOTSTRAP_SERVICE_TAG_VALUE,d as buildBootstrapTags,_ as buildBootstrapTagsIfResolved};
|
|
@@ -142,5 +142,10 @@ export declare function synthOrFail(services: DeployServices, context: Deploymen
|
|
|
142
142
|
/**
|
|
143
143
|
* Run CDK bootstrap and return failure with masked error if it fails.
|
|
144
144
|
* Emits onCDKBootstrap status callbacks and calls onError on failure.
|
|
145
|
+
*
|
|
146
|
+
* `bootstrapTags` (from `buildBootstrapTags`) stamps the CDKToolkit stack;
|
|
147
|
+
* callers that cannot derive a stable, complete set for the target account
|
|
148
|
+
* pass undefined so the existing stack tags are preserved — see
|
|
149
|
+
* `orchestration/bootstrapTags.ts` for the replace-vs-preserve contract.
|
|
145
150
|
*/
|
|
146
|
-
export declare function bootstrapOrFail(services: DeployServices, context: DeploymentContext, callbacks: DeployCallbacks): Promise<Result<void>>;
|
|
151
|
+
export declare function bootstrapOrFail(services: DeployServices, context: DeploymentContext, callbacks: DeployCallbacks, bootstrapTags?: Record<string, string>): Promise<Result<void>>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var R=Object.defineProperty;var n=(e,r)=>R(e,"name",{value:r,configurable:!0});import{success as l,failure as
|
|
1
|
+
var R=Object.defineProperty;var n=(e,r)=>R(e,"name",{value:r,configurable:!0});import{success as l,failure as c}from"@fjall/generator";import{getErrorMessage as y,maskSensitiveOutput as f,sleep as g}from"@fjall/util";import{logger as S}from"@fjall/util/logger";import{IAMClient as w}from"@aws-sdk/client-iam";import{hasOrganisationTierAccount as b}from"../aws/organisations/accountGlobals.js";import{reconcileOidcProviderAfterDeploy as h}from"../aws/organisations/oidcProvider.js";import{SimpleAwsProvider as T}from"../aws/SimpleAwsProvider.js";const _={account:"active",draining:"draining",org:"removed"};function $(e,r){return e!==void 0&&e!==""&&r!==void 0&&r!==""&&e!==r}n($,"targetsNonPrimaryRegion");function N(e){const r=$(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:_[e.trailLifecycle]}:{}}}n(N,"buildParamsContext");function U(e,r,t,o){if(t!==void 0)return t;if(e==="account")return b(r?.providerAccounts)?o===!0:!0}n(U,"resolveAccountBootstrapSkipOidc");function v(e){return e.fjallOrgId!==void 0&&e.fjallOrgId!==""&&e.fjallOidcConfigured!==!0&&e.fjallAccountGlobalsConfigured!==!0}n(v,"synthCarriedOidcConnector");async function V(e,r,t,o){v(e)&&await h(r.getClient(w),t,o)}n(V,"reconcileOidcProviderIfCarried");function K(e){return r=>e.onOutput?.(r)}n(K,"forwardOutput");function z(e){return r=>e.onResourceProgress?.(r)}n(z,"forwardResourceProgress");function J(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}n(J,"collectStackOutputs");const E="OrganizationAccountAccessRole",p=5,x=5e3,M=3e4;function j(e){return`arn:aws:iam::${e}:role/${E}`}n(j,"buildCascadeRoleArn");function k(e,r){return r===void 0?g(e):r.aborted?Promise.resolve():new Promise(t=>{const o=n(()=>{t()},"onAbort");r.addEventListener("abort",o,{once:!0}),g(e).then(()=>{r.removeEventListener("abort",o),t()})})}n(k,"sleepAbortable");async function X(e,r,t,o,s){if(!e.assumeRole)return c(new Error("AwsProvider does not support assumeRole"));const i=j(r),C=e.assumeRole.bind(e);let u;for(let d=0;d<=p;d++)try{u=await C(i,o);break}catch(a){const A=a instanceof Error?a.name:void 0;if(A==="AccessDenied"||A==="AccessDeniedException")return c(new Error(`Access denied assuming ${E} in account ${r}. The role may not exist or may not trust the management account.`));if(d<p){const m=Math.min(x*2**d,M);if(S.debug("assumeCascadeRole",`Attempt ${d+1} failed for account ${r}, retrying in ${Math.round(m/1e3)}s`,{error:f(y(a))}),await k(m,s),s?.aborted)return c(new Error(`Aborted while retrying assume-role for account ${r}`));continue}return c(new Error(`Failed to assume role in account ${r} after ${p+1} attempts: ${f(y(a))}`))}if(!u)return c(new Error(`Failed to assume role in account ${r}`));const O=new T({accessKeyId:u.accessKeyId,secretAccessKey:u.secretAccessKey,sessionToken:u.sessionToken,region:t,accountId:r});return l({provider:O,credentials:{accessKeyId:u.accessKeyId,secretAccessKey:u.secretAccessKey,sessionToken:u.sessionToken}})}n(X,"assumeCascadeRole");async function q(e,r,t,o){const s=await e.cdkService.runCdkSynth(r,i=>t.onCdkOutput?.(i,"synth"));if(!s.success){const i=new Error(f(`${o}: ${s.error}`));return t.onError?.(i),c(i)}return l(void 0)}n(q,"synthOrFail");async function H(e,r,t,o){t.onCDKBootstrap?.("bootstrapping");const s=await e.cdkService.runCdkBootstrap(r,K(t),void 0,void 0,o);if(!s.success){t.onCDKBootstrap?.("failed");const i=new Error(f(`Bootstrap failed: ${s.error}`));return t.onError?.(i),c(i)}return t.onCDKBootstrap?.("complete"),l(void 0)}n(H,"bootstrapOrFail");export{X as assumeCascadeRole,H as bootstrapOrFail,j as buildCascadeRoleArn,N as buildParamsContext,J as collectStackOutputs,K as forwardOutput,z as forwardResourceProgress,V as reconcileOidcProviderIfCarried,U as resolveAccountBootstrapSkipOidc,v as synthCarriedOidcConnector,q as synthOrFail,$ as targetsNonPrimaryRegion};
|
|
@@ -98,6 +98,17 @@ export interface DeployCascadeAccountOptions {
|
|
|
98
98
|
* Omitted ⇒ globals are never skipped.
|
|
99
99
|
*/
|
|
100
100
|
primaryRegion?: string;
|
|
101
|
+
/**
|
|
102
|
+
* AWS Organizations org id — stamped as the owner cost-allocation tag on
|
|
103
|
+
* the account's CDKToolkit stack at bootstrap
|
|
104
|
+
* (`buildBootstrapTagsIfResolved`). Omitted on an org-classified estate ⇒
|
|
105
|
+
* the bootstrap emits NO tags (the complete set includes owner; a partial
|
|
106
|
+
* set would strip it, so the gate preserves instead). Ignored when the
|
|
107
|
+
* effective org config carries no organisation-tier account — the estate
|
|
108
|
+
* classifies solo and owner must match what the application path can
|
|
109
|
+
* re-derive.
|
|
110
|
+
*/
|
|
111
|
+
orgId?: string;
|
|
101
112
|
}
|
|
102
113
|
export declare function deployCascadeAccount(params: DeployParams, services: DeployServices, operation: OrganisationOperation, account: ProviderAccount, deployType: "platform" | "account", callbacks: DeployCallbacks, options?: DeployCascadeAccountOptions): Promise<Result<CascadeAccountResult>>;
|
|
103
114
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var z=Object.defineProperty;var g=(e,n)=>z(e,"name",{value:n,configurable:!0});import{join as
|
|
1
|
+
var z=Object.defineProperty;var g=(e,n)=>z(e,"name",{value:n,configurable:!0});import{join as T}from"path";import{success as j,failure as h}from"@fjall/generator";import{logger as R}from"@fjall/util/logger";import{maskSensitiveOutput as C,mapSettledWithConcurrency as Y}from"@fjall/util";import{ORGANISATION_TYPES as P,getOrganisationStackName as H}from"../../types/operations.js";import{CdkContextBuilder as W}from"../../services/supporting/CdkContextBuilder.js";import{stubCallerIdentity as X}from"../../types/deployment/index.js";import{CloudFormationService as M}from"../../services/infrastructure/CloudFormationService.js";import{getCascadeStateFilePath as q,regionSuffix as J}from"../../types/config/FjallState.js";import{BackupClient as Q}from"@aws-sdk/client-backup";import{accountHasDisasterRecovery as Z,describeBackupVaultExists as ee}from"../../aws/organisations/backup.js";import{buildParamsContext as te,collectStackOutputs as V,assumeCascadeRole as x,forwardOutput as E,reconcileOidcProviderIfCarried as G}from"../contextHelpers.js";import{assertLeaseBeforeStack as oe}from"../application/leaseGate.js";import{deployDelegatedDomain as re}from"../domain/delegatedDomainDeploy.js";import{runDriftPreFlightGate as ne}from"../drift/preFlightGate.js";import{accountTier as U}from"@fjall/util";import{buildBootstrapTagsIfResolved as ae}from"../bootstrapTags.js";import{hasOrganisationTierAccount as se}from"../../aws/organisations/accountGlobals.js";import{DEFAULT_REGION as _}from"../../aws/utils/regions.js";const ie=4;function xe(e){const n=e.find(t=>U(t)==="platform"),d=e.filter(t=>U(t)==="account");return{platformAccount:n,memberAccounts:d}}g(xe,"partitionAccounts");function ce(e){const n=e?.primaryRegion??_,d=e?.secondaryRegions??[],t=new Set([n,...d]),i=e?.disasterRecoveryRegion;return i&&t.add(i),[...t]}g(ce,"buildRegionList");function Ee(e){return ce(e)[0]??_}g(Ee,"cascadeHomeRegion");function de(e,n){return`${e} (${n})`}g(de,"cascadeOperationKey");function Fe(e,n){const d=[];for(const t of n)for(const i of e)d.push({account:i,region:t});return d}g(Fe,"buildAccountRegionPairs");import{buildCascadeRoleArn as He}from"../contextHelpers.js";async function Ne(e,n,d,t){d.onLog?.(`Verifying cascade role access for ${n.length} account(s)\u2026`,"info");const i=await Y(n,ie,async s=>x(e.awsProvider,s.id,s.region??e.awsProvider.getRegion(),`fjall-preflight-${s.name}`,t)),o=[];return i.forEach((s,a)=>{const r=n[a];if(r){if(s.status==="rejected"){o.push({accountId:r.id,accountName:r.name,error:s.reason instanceof Error?s.reason.message:String(s.reason)});return}s.value.success||o.push({accountId:r.id,accountName:r.name,error:s.value.error.message})}}),o.length===0&&d.onLog?.(`Cascade role access verified for ${n.length} account(s)`,"info"),o}g(Ne,"probeCascadeRoles");async function be(e,n,d,t,i,o,s){const a=s?.region??t.region??n.awsProvider.getRegion(),r=de(t.name,a),c=s?.orgConfig??e.orgConfig,u=s?.ipamPoolId;o.onCascadeAccountStart?.(r,t.id,a,i);const f=await x(n.awsProvider,t.id,a,`fjall-cascade-${t.name}`,e.abortSignal);if(!f.success)return o.onCascadeAccountComplete?.(r,!1,C(f.error.message),a),h(new Error(`Failed to assume role for ${t.name}: ${C(f.error.message)}`));const{provider:l,credentials:y}=f.data,A=T(e.workingDirectory,"fjall",i==="platform"?P.PLATFORM:P.ACCOUNT),$=T(A,`cdk.out.${t.id}.${J(a)}`),S=W.buildDeploymentContext({deployType:i,target:d.target,path:A,assemblyDir:$,environment:t.environment??void 0,region:a,accountName:t.name,callerIdentity:X(t.id),ipamPoolId:u,...te({orgConfig:c,identity:e.identity,skipOidc:e.options?.skipOidc,region:a,primaryRegion:s?.primaryRegion,trailLifecycle:t.trailLifecycle})},{verbose:e.options?.verbose},c);if(Z(t.environment,c?.disasterRecoveryRegion)){const p=await ee(l.getClient(Q),e.abortSignal);if(!p.success)return o.onCascadeAccountComplete?.(r,!1,C(p.error.message),a),h(new Error(`Backup vault probe failed for ${t.name}: ${C(p.error.message)}`));S.fjallAdoptBackupVault=p.data}o.onCascadeAccountPhaseChange?.(r,"synth",a);const O=await n.cdkService.runCdkSynth(S,E(o),y);if(!O.success)return o.onCascadeAccountComplete?.(r,!1,C(`Synth failed: ${O.error}`),a),h(new Error(`Synth failed for ${t.name}: ${C(O.error)}`));const m=H(i==="platform"?P.PLATFORM:P.ACCOUNT),F=q(A,t.id,a),w=new M(l),{changed:K,currentHash:N}=await n.hashService.compareCascadeStack($,m,F);if(!K&&e.options?.force!==!0&&await w.stackExists(m,void 0,e.abortSignal)){const p=await w.getStackOutputs(m,void 0,e.abortSignal);p.success||R.debug("cascadeHelpers","Failed to read outputs for skipped cascade account (non-critical)",{stackName:m,account:t.name});const B=V(p);return await G(S,l,o,e.abortSignal),o.onLog?.(`${t.name}: no infrastructure changes \u2014 skipping deploy`,"info"),o.onCascadeAccountComplete?.(r,!0,void 0,a,B,!0),j({outputs:B,skipped:!0})}const v=await ne(l,{stackNames:[m],cdkOutPath:$,cfnService:w,callbacks:o,...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}});if(v.kind==="blocked")return o.onCascadeAccountComplete?.(r,!1,v.message,a),h(new Error(v.message));const D=await oe(e.assertLeaseForStack,m,e.abortSignal);if(D!==void 0)return o.onCascadeAccountComplete?.(r,!1,D.message,a),h(D);o.onCascadeAccountPhaseChange?.(r,"bootstrap",a);const I=await n.cdkService.runCdkBootstrap(S,E(o),y,void 0,ae({account:t,estate:se(c?.providerAccounts)?"org":"solo",...s?.orgId!==void 0?{orgId:s.orgId}:{}}));if(!I.success)return o.onCascadeAccountComplete?.(r,!1,C(`Bootstrap failed: ${I.error}`),a),h(new Error(`Bootstrap failed for ${t.name}: ${C(I.error)}`));o.onCascadeAccountPhaseChange?.(r,"deploy",a);const k=await n.cdkService.runCdkDeploy(S,m,E(o),p=>o.onCascadeAccountResourceProgress?.(r,p,a),l,y);if(!k.success)return o.onCascadeAccountComplete?.(r,!1,C(k.error),a),h(new Error(C(k.error)));const b=await w.getStackOutputs(m,void 0,e.abortSignal);b.success||R.debug("cascadeHelpers","Failed to read cascade account stack outputs (non-critical)",{stackName:m,account:t.name});const L=V(b);return await G(S,l,o,e.abortSignal),N!==void 0&&((await n.hashService.persistCascadeStack(F,m,N)).success||R.debug("cascadeHelpers","Failed to persist cascade hash state (non-critical)",{stackName:m,account:t.name})),o.onCascadeAccountComplete?.(r,!0,void 0,a,L,!1),j({outputs:L,skipped:!1})}g(be,"deployCascadeAccount");async function Le(e,n,d,t){const i=new Map,o=e.awsProvider.getRegion(),s=await x(e.awsProvider,n.id,o,`fjall-ipam-read-${n.name}`,t);if(!s.success)return R.debug("organisationDeploy",`Cannot read Platform outputs: ${s.error.message}`),i;const a=new M(s.data.provider),r=H(P.PLATFORM),c=await a.getStackOutputs(r);if(!c.success)return R.debug("organisationDeploy",`Failed to read Platform stack outputs: ${c.error.message}`),i;const u=/^IpamPoolId(\d{12})(\w+)$/;for(const f of c.data){const l=f.OutputKey?.match(u);if(l&&f.OutputValue){const y=`${l[1]}-${l[2]}`;i.set(y,f.OutputValue)}}return i.size>0&&d.onLog?.(`Read ${i.size} IPAM pool ID(s) from Platform stack`,"info"),i}g(Le,"readPlatformIpamPoolIds");async function Be(e,n,d){const t=e.getDomains();if(t.length===0)return{domainsDeployed:0,errors:[]};n.onCascadePhaseStart?.("domains");const i=t.filter(r=>r.type==="apex"),o=t.filter(r=>r.type==="delegated");let s=0;const a=[];for(const r of i){const c=await e.deployDomain(r.name,n);c.success?s++:a.push(`${r.name}: ${c.error.message}`)}if(o.length>0){const r=await Promise.allSettled(o.map(c=>{const u=e.delegatedDeployer?.(c.name);return u!==void 0?re({deployer:u,callbacks:n,...d?.abortSignal!==void 0?{abortSignal:d.abortSignal}:{},...d?.skipCaaPreflight!==void 0?{skipCaaPreflight:d.skipCaaPreflight}:{}}):e.deployDomain(c.name,n)}));for(let c=0;c<r.length;c++){const u=r[c],f=o[c];if(!(!u||!f))if(u.status==="fulfilled")u.value.success?s++:a.push(`${f.name}: ${u.value.error.message}`);else{const l=u.reason instanceof Error?u.reason.message:String(u.reason);a.push(`${f.name}: ${l}`)}}}return n.onCascadePhaseComplete?.("domains"),{domainsDeployed:s,errors:a}}g(Be,"deployDomains");export{ie as CASCADE_MAX_CONCURRENCY,Fe as buildAccountRegionPairs,He as buildCascadeRoleArn,ce as buildRegionList,Ee as cascadeHomeRegion,de as cascadeOperationKey,be as deployCascadeAccount,Be as deployDomains,xe as partitionAccounts,Ne as probeCascadeRoles,Le as readPlatformIpamPoolIds};
|
|
@@ -23,6 +23,9 @@ export declare function executeCascade(params: DeployParams, services: DeploySer
|
|
|
23
23
|
accountId: string;
|
|
24
24
|
outputs: Record<string, string>;
|
|
25
25
|
}>;
|
|
26
|
+
/** AWS Organizations org id — the owner cost-allocation tag each cascade
|
|
27
|
+
* bootstrap stamps on its account's CDKToolkit stack. */
|
|
28
|
+
orgId?: string;
|
|
26
29
|
}): Promise<{
|
|
27
30
|
anyCascadeDeployHappened: boolean;
|
|
28
31
|
}>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var W=Object.defineProperty;var b=(o,S)=>W(o,"name",{value:S,configurable:!0});import{failure as $}from"@fjall/generator";import{regionSuffix as X}from"../../types/config/FjallState.js";import{partitionAccounts as Y,deployCascadeAccount as T,readPlatformIpamPoolIds as z,deployDomains as B,buildRegionList as G,buildAccountRegionPairs as J,cascadeHomeRegion as K,CASCADE_MAX_CONCURRENCY as Q}from"../organisation/cascadeHelpers.js";import{projectScalarSummary as q}from"../organisation/cascadeSummary.js";import{getErrorMessage as V,maskSensitiveOutput as C,mapSettledWithConcurrency as Z}from"@fjall/util";import{INFRA_STEPS as E}from"./infraSteps.js";async function ie(o,S,N,H){const{callbacks:e}=o,{providerAccounts:U,effectiveOrgConfig:R,totalSteps:p,cascadeErrors:l,allCascadeOutputs:M,orgId:O}=H;let g=!1;e.onCascadeStart?.();const j=Date.now();let d=2,y=!1,h,D=!1;const I=[],_=b(t=>({members:I,...h!==void 0?{platform:h}:{},domainsDeployed:D,errors:l,totalDurationMs:t}),"buildLedger"),{platformAccount:r,memberAccounts:A}=Y(U),F=K(o.orgConfig);if(r){const{id:t,name:a}=E.CASCADE_PLATFORM;e.onStepStart?.(t,a,d,p),e.onCascadePhaseStart?.("platform");let c;const P=Date.now();try{c=await T(o,S,N,r,"platform",e,{orgConfig:R,primaryRegion:F,orgId:O})}catch(i){const n=C(V(i));l.push({accountId:r.id,error:n}),e.onCascadePhaseComplete?.("platform"),e.onStepComplete?.(t,a,"error",d,p),c=$(new Error(n))}const w=Date.now()-P;if(c.success){y=!0;const i=c.data.skipped===!0;i||(g=!0),c.data.outputs&&M.push({accountId:r.id,outputs:c.data.outputs}),e.onCascadePhaseComplete?.("platform"),e.onStepComplete?.(t,a,i?"skipped":"completed",d,p),h={accountId:r.id,result:i?"skipped":"succeeded",durationMs:w}}else l.some(i=>i.accountId===r.id)||(l.push({accountId:r.id,error:C(c.error.message)}),e.onCascadePhaseComplete?.("platform"),e.onStepComplete?.(t,a,"error",d,p)),h={accountId:r.id,result:"failed",durationMs:w,error:C(c.error.message)};d++}let L=new Map;if(y&&r&&(L=await z(S,r,e,o.abortSignal)),o.domainProvider){const t=await B(o.domainProvider,e,{...o.abortSignal!==void 0?{abortSignal:o.abortSignal}:{},...o.options?.skipCaaPreflight!==void 0?{skipCaaPreflight:o.options.skipCaaPreflight}:{}});D=t.domainsDeployed>0,D&&(g=!0);for(const a of t.errors)l.push({accountId:"domains",error:C(a)})}if(r!==void 0&&!y&&A.length>0){const{id:t,name:a}=E.CASCADE_ACCOUNTS;e.onStepStart?.(t,a,d,p),e.onStepComplete?.(t,a,"skipped",d,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:a}=E.CASCADE_ACCOUNTS;e.onStepStart?.(t,a,d,p),e.onCascadePhaseStart?.("accounts");const c=G(o.orgConfig),P=J(A,c);(await Z(P,Q,async({account:n,region:k})=>{const m=L.get(`${n.id}-${X(k)}`),s=Date.now();return{result:await T(o,S,N,n,"account",e,{ipamPoolId:m,orgConfig:R,region:k,primaryRegion:F,orgId:O}),durationMs:Date.now()-s}})).forEach((n,k)=>{const m=P[k];if(!m)return;const s=m.account;if(n.status==="rejected"){const u=C(n.reason instanceof Error?n.reason.message:String(n.reason));I.push({accountId:s.id,accountName:s.name,region:m.region,result:"failed",durationMs:0,error:u}),l.push({accountId:s.id,error:u});return}const{result:f,durationMs:x}=n.value;if(f.success){const u=f.data.skipped===!0;u||(g=!0),f.data.outputs&&M.push({accountId:s.id,outputs:f.data.outputs}),I.push({accountId:s.id,accountName:s.name,region:m.region,result:u?"skipped":"succeeded",durationMs:x})}else{const u=C(f.error.message);I.push({accountId:s.id,accountName:s.name,region:m.region,result:"failed",durationMs:x,error:u}),l.push({accountId:s.id,error:u})}});const i=q(_(0));e.onCascadePhaseComplete?.("accounts"),e.onStepComplete?.(t,a,i.accountsFailed>0?"error":i.accountsSkipped===A.length?"skipped":"completed",d,p)}const v=_(Date.now()-j);return e.onCascadeComplete?.(q(v)),e.onCascadeLedger?.(v),{anyCascadeDeployHappened:g}}b(ie,"executeCascade");export{ie as executeCascade};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
var V=Object.defineProperty;var _=(o,e)=>V(o,"name",{value:e,configurable:!0});import{join as
|
|
1
|
+
var V=Object.defineProperty;var _=(o,e)=>V(o,"name",{value:e,configurable:!0});import{join as Y}from"node:path";import{OrganizationsClient as X}from"@aws-sdk/client-organizations";import{success as Z,failure as z}from"@fjall/generator";import{ORGANISATION_TYPES as I,getOrganisationStackName as A}from"../../types/operations.js";import{synthOrFail as tt,bootstrapOrFail as et,forwardOutput as ot,forwardResourceProgress as at}from"../contextHelpers.js";import{partitionAccounts as rt,probeCascadeRoles as nt,buildRegionList as it,buildAccountRegionPairs as ct}from"../organisation/cascadeHelpers.js";import{assertLeaseBeforeStack as st}from"../application/leaseGate.js";import{accountTier as dt,maskSensitiveOutput as g}from"@fjall/util";import{findDevelopmentOuId as ut}from"../../aws/organisations/organisationalUnits.js";import{buildOrgContext as lt,resolveOrgDetails as gt}from"./orgContext.js";import{buildBootstrapTagsIfResolved as ft}from"../bootstrapTags.js";import{INFRA_STEPS as K,emitOrgStackTargets as pt,maskAndFail as O,readStackOutputsBestEffort as U}from"./infraSteps.js";import{resolveCascadeAccounts as mt}from"./resolveCascadeAccounts.js";import{executeCascade as St}from"./cascadeExecution.js";import{reconcileOrgTrailOutputs as ht}from"./trailReconciliation.js";import{runDriftPreFlightGate as Ot}from"../drift/preFlightGate.js";async function Gt(o,e,R,q){const{callbacks:t,options:T}=o,{providerAccounts:f,effectiveOrgConfig:J,defaultRegion:Q}=await mt(o,e),p=await gt(e,o.abortSignal);if(!p.success)return O(t,p.error.message);const w=await ut(e.awsProvider.getClient(X),p.data.rootId,o.abortSignal);if(!w.success)return O(t,w.error.message);const P={...p.data,...w.data!==void 0?{devOuId:w.data}:{}},E=f.find(a=>dt(a)==="organisation"),d=lt(o,e,R,"organisation",P,void 0,E?.trailLifecycle),l=T?.cascade!==!1,{platformAccount:s,memberAccounts:N}=rt(f),L=l&&s!==void 0?1:0,F=l&&N.length>0?1:0,i=2+L+F,x=l?[...s!==void 0?[s]:[],...N]:[];if(x.length>0){const a=await nt(e,x,t,o.abortSignal),r=s!==void 0?a.find(n=>n.accountId===s.id):void 0;if(r!==void 0)return O(t,`Pre-flight cascade role check failed for the platform account ${r.accountName} (${r.accountId}): ${r.error}`);for(const n of a)t.onProgress?.({type:"warning",message:g(`Pre-flight cascade role check failed for ${n.accountName} (${n.accountId}) \u2014 its cascade deploy is expected to fail: ${n.error}`)})}t.onCascadeAccountsReconciled?.({hasPlatformAccount:L>0,hasMemberAccounts:F>0});const{id:C,name:b}=K.PREPARE;t.onStepStart?.(C,b,0,i),t.onLog?.("Synthesising organisation infrastructure\u2026","info");const G=await tt(e,d,t,"CDK synthesis failed");if(!G.success)return t.onStepComplete?.(C,b,"error",0,i),G;const H=e.awsProvider.getAccountId(),$=e.awsProvider.getRegion();pt(t,[{stackName:A(I.ORGANISATION),...H!==""?{accountId:H}:{},...$!==""?{region:$}:{}},...l&&s!==void 0?[{stackName:A(I.PLATFORM),accountId:s.id,region:s.region??$}]:[],...l?ct(N,it(o.orgConfig)).map(({account:a,region:r})=>({stackName:A(I.ACCOUNT),accountId:a.id,region:r})):[]]);const M=await et(e,d,t,ft({account:E,estate:"org",orgId:P.orgId}));if(!M.success)return t.onStepComplete?.(C,b,"error",0,i),M;t.onStepComplete?.(C,b,"completed",0,i);const{id:m,name:S}=K.ORG_DEPLOY,c=A(I.ORGANISATION);let j=!0;const h=await e.hashService.getTemplateHashes(Y(d.path,"cdk.out"));if(h.success){const a=await e.hashService.compareWithState(h.data,d.path);a.success?j=a.data.stackChanges.get(c)??!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: ${h.error.message}`),"warn");const D=j||T?.force===!0||!await e.cfnService.stackExists(c,void 0,o.abortSignal);t.onOrgChangesDetected?.({hasOrgChanges:D});let y;if(D){t.onStepStart?.(m,S,1,i);const a=await Ot(e.awsProvider,{stackNames:[c],cdkOutPath:Y(d.path,"cdk.out"),cfnService:e.cfnService,callbacks:t,...o.abortSignal!==void 0?{abortSignal:o.abortSignal}:{}});if(a.kind==="blocked")return t.onStepComplete?.(m,S,"error",1,i),O(t,a.message);const r=await st(o.assertLeaseForStack,c,o.abortSignal);if(r!==void 0)return t.onStepComplete?.(m,S,"error",1,i),t.onError?.(r),z(r);const n=await e.cdkService.runCdkDeploy(d,c,ot(t),at(t),e.awsProvider);if(!n.success)return t.onStepComplete?.(m,S,"error",1,i),O(t,n.error);y=await U(e,t,c,"Failed to read org stack outputs (non-critical)",o.abortSignal);const B=h.success?h.data.get(c):void 0;if(B!==void 0){const W=await e.hashService.updateStateAfterDeploy(d.path,new Map([[c,B]]));W.success||t.onLog?.(`Warning: failed to update state file \u2014 next deploy may re-deploy the org root: ${g(W.error.message)}`,"warn")}t.onStepComplete?.(m,S,"completed",1,i)}else t.onLog?.("Organisation root: no infrastructure changes \u2014 skipping deploy","info"),y=await U(e,t,c,"Failed to read org stack outputs (non-critical)",o.abortSignal);const u=[],k=[];let v=D;if(l&&f.length>0){const{anyCascadeDeployHappened:a}=await St(o,e,R,{providerAccounts:f,effectiveOrgConfig:J,totalSteps:i,cascadeErrors:u,allCascadeOutputs:k,orgId:P.orgId});if(a&&(v=!0),await ht(o,e,{orgOutputs:y,allCascadeOutputs:k,orgDetails:p.data,providerAccounts:f,defaultRegion:Q}),u.length>0){const r=u.map(n=>` ${n.accountId}: ${n.error}`).join(`
|
|
2
2
|
`);t.onLog?.(g(`Cascade failed for ${u.length} target(s):
|
|
3
3
|
${r}`),"warn")}}if(u.length>0){const a=u.map(n=>g(`${n.accountId}: ${n.error}`)).join(`
|
|
4
4
|
`),r=new Error(`Organisation root deployed, but the cascade failed for ${u.length} target(s):
|
|
5
|
-
${a}`);return t.onError?.(r),
|
|
5
|
+
${a}`);return t.onError?.(r),z(r)}return Z({target:R.target,deploymentType:"organisation",outputs:y,artefacts:[],...k.length>0?{cascadeOutputs:k}:{},...v?{}:{noChanges:!0},durationMs:Date.now()-q})}_(Gt,"deployOrgWithCascade");export{Gt as deployOrgWithCascade};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var y=Object.defineProperty;var R=(
|
|
1
|
+
var y=Object.defineProperty;var R=(t,e)=>y(t,"name",{value:e,configurable:!0});import{join as M}from"path";import{failure as N,success as G}from"@fjall/generator";import{BackupClient as B}from"@aws-sdk/client-backup";import{IAMClient as Y}from"@aws-sdk/client-iam";import{logger as x}from"@fjall/util/logger";import{getOrganisationStackName as V}from"../../types/operations.js";import{accountHasDisasterRecovery as _,describeBackupVaultExists as j}from"../../aws/organisations/backup.js";import{describeAccountGlobalsExist as H,buildMissingAccountGlobalsAdvisory as $}from"../../aws/organisations/accountGlobals.js";import{targetsNonPrimaryRegion as K,reconcileOidcProviderIfCarried as q,synthOrFail as z,bootstrapOrFail as J,forwardOutput as Q,forwardResourceProgress as U}from"../contextHelpers.js";import{accountTier as I,getErrorMessage as W,maskSensitiveOutput as D}from"@fjall/util";import{runDriftPreFlightGate as X}from"../drift/preFlightGate.js";import{completeForgetAfterDeploy as Z}from"../remediation/forgetResource.js";import{composePostFailureRepairOffer as oo}from"../remediation/postFailureRepairOffer.js";import{buildOrgContext as to,resolveOrgDetailsForMemberComponent as eo,resolveOrgDetailsForSolo as ro}from"./orgContext.js";import{hasOrganisationTierAccount as no}from"../../aws/organisations/accountGlobals.js";import{buildBootstrapTagsIfResolved as io}from"../bootstrapTags.js";import{INFRA_STEPS as n,INFRA_STEP_TOTAL as a,emitOrgStackTargets as ao,failPrepareStep as d,maskAndFail as f,readStackOutputsBestEffort as co}from"./infraSteps.js";import{assertLeaseBeforeStack as go}from"../application/leaseGate.js";async function Lo(t,e,g,s,h){const{callbacks:o}=t;o.onStepComplete?.(n.CONNECT.id,n.CONNECT.name,"completed",0,a),o.onStepStart?.(n.PREPARE.id,n.PREPARE.name,1,a);const b=e.awsProvider.getRegion(),m=t.orgConfig?.primaryRegion;if(m!==void 0&&K(b,m)){const r=await H(e.awsProvider.getClient(Y),t.abortSignal);if(!r.success)return d(o),f(o,r.error.message);if(!r.data)return d(o),f(o,$({target:g.target,deployRegion:b,primaryRegion:m,...t.orgConfig!==void 0?{providerAccounts:t.orgConfig.providerAccounts}:{}}))}const O=no(t.orgConfig?.providerAccounts);let u;if(O){const r=await eo(e,t.abortSignal);if(!r.success)return d(o),f(o,r.error.message);u=r.data}else u=await ro(e,t.abortSignal);const c=s==="account"?t.orgConfig?.providerAccounts.find(r=>r.id===e.awsProvider.getAccountId()):t.orgConfig?.providerAccounts.find(r=>I(r)==="platform"),F=s==="account"&&c!==void 0&&I(c)==="organisation",l=to(t,e,g,s,u,s==="account"?c?.name??g.target:void 0,c?.trailLifecycle,F),S=t.orgConfig?.disasterRecoveryRegion;if(s==="account"&&(S!==void 0&&S!=="")&&(c===void 0||_(c.environment,S))){const r=await j(e.awsProvider.getClient(B),t.abortSignal);if(!r.success)return d(o),f(o,r.error.message);l.fjallAdoptBackupVault=r.data}o.onLog?.(`Synthesising ${s} infrastructure\u2026`,"info");const C=await z(e,l,o,"CDK synthesis failed");if(!C.success)return d(o),C;const i=V(g.type),w=e.awsProvider.getAccountId(),E=e.awsProvider.getRegion();ao(o,[{stackName:i,...w!==""?{accountId:w}:{},...E!==""?{region:E}:{}}]);const A=await X(e.awsProvider,{stackNames:[i],cdkOutPath:M(g.path,"cdk.out"),cfnService:e.cfnService,callbacks:o,...t.abortSignal!==void 0?{abortSignal:t.abortSignal}:{}});if(A.kind==="blocked")return d(o),f(o,A.message);const k=await J(e,l,o,io({account:c,estate:O?"org":"solo",...u?.orgId!==void 0?{orgId:u.orgId}:{}}));if(!k.success)return d(o),k;o.onStepComplete?.(n.PREPARE.id,n.PREPARE.name,"completed",1,a);const p=await go(t.assertLeaseForStack,i,t.abortSignal);if(p!==void 0)return o.onError?.(p),N(p);o.onStepStart?.(n.DEPLOY.id,n.DEPLOY.name,2,a);const P=await e.cdkService.runCdkDeploy(l,i,Q(o),U(o),e.awsProvider);if(!P.success){o.onStepComplete?.(n.DEPLOY.id,n.DEPLOY.name,"error",2,a);const r=await oo(e.awsProvider,{stackNames:[i],repairTarget:`--stack ${i}`,originalError:new Error(D(P.error)),cfnService:e.cfnService,...t.abortSignal!==void 0?{abortSignal:t.abortSignal}:{}});return o.onError?.(r),N(r)}o.onStepComplete?.(n.DEPLOY.id,n.DEPLOY.name,"completed",2,a);const L=await co(e,o,i,"Failed to read stack outputs (non-critical)",t.abortSignal);await q(l,e.awsProvider,o,t.abortSignal),o.onStepStart?.(n.MONITORING.id,n.MONITORING.name,3,a),o.onStepComplete?.(n.MONITORING.id,n.MONITORING.name,"completed",3,a),await Z(e.awsProvider,{stackNames:[i],onLog:R((r,T)=>o.onLog?.(r,T),"onLog"),...t.abortSignal!==void 0?{abortSignal:t.abortSignal}:{}}).catch(r=>{x.debug("SingleComponentDeploy","Forget completion sweep failed",{error:D(W(r))})});const v=P.data?.status==="NO_CHANGES"&&t.options?.force!==!0;return G({target:g.target,deploymentType:"organisation",outputs:L,artefacts:[],...v?{noChanges:!0}:{},durationMs:Date.now()-h})}R(Lo,"deploySingleComponent");export{Lo as deploySingleComponent};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var E=Object.defineProperty;var f=(m,s)=>E(m,"name",{value:s,configurable:!0});import{tmpdir as O}from"os";import{resolve as A}from"path";import{logger as
|
|
2
|
-
`);const r=t?.noLookups?[o.NO_LOOKUPS]:[],u=["--output",A(s,t?.outputDir??b)],a=t?.context?.region||P,n=await this.processManager.runCdkCommand(s,["synth",...r,...u,o.CI,"--quiet"],{...t,context:{...t?.context,region:a},timeout:t?.timeout||k});if(!n.success){const d=n.error?c(n.error,s):"Failed to synthesise CloudFormation template";return i(d)}return n}async bootstrap(s,t,r){const e=O();
|
|
1
|
+
var E=Object.defineProperty;var f=(m,s)=>E(m,"name",{value:s,configurable:!0});import{tmpdir as O}from"os";import{resolve as A}from"path";import{logger as l}from"@fjall/util/logger";import{success as p,failure as i}from"@fjall/generator";import{CdkError as h}from"../../types/errors/CdkError.js";import{DEFAULT_REGION as P}from"../../aws/utils/regions.js";import{CdkArgumentBuilder as g}from"./CdkArgumentBuilder.js";import{isCdkError as _,formatInfrastructureError as c}from"./CdkErrorFormatter.js";import{hasCdkDifferences as L,parseDiffOutput as R}from"./CdkOutputParser.js";import{DEFAULT_DEPLOY_TIMEOUT_MS as C}from"./CdkEventMonitoring.js";import{analyseBootstrapError as y}from"./CdkOutputAnalyser.js";const k=3e5,S=18e4,D=12e5,b="cdk.out",o=Object.freeze({APP:"--app",CI:"--ci",REQUIRE_APPROVAL:"--require-approval",APPROVAL_NEVER:"never",VERBOSE:"--verbose",NO_LOOKUPS:"--no-lookups",ALL:"--all",EXCLUSIVELY:"--exclusively"});class F{static{f(this,"CdkCommandRunner")}processManager;argumentBuilder=new g;constructor(s){this.processManager=s}dispose(){this.processManager.dispose()}async checkDifferences(s,t,r){const e=["diff"];t?e.push(t):e.push(o.ALL),e.push("--no-color"),r?.noLookups&&e.push(o.NO_LOOKUPS);const u=await this.processManager.runCdkCommand(s,e,{...r,ignoreExitCode:!0,combineOutput:!0});if(!u.success)return i(new h(c(u.error,s),"diff_failed",void 0,void 0,u.error,void 0,!1));const a=u.data.output||"";if(_(a))return i(new h(c(a,s),"diff_failed",void 0,void 0,a,void 0,!1));const n=L(a),d=R(a);return p({hasDifferences:n,output:a,details:d})}async deploy(s,t,r){const e=["deploy"];r?.appDir?e.push(o.APP,r.appDir):r?.useCdkOut&&e.push(o.APP,"cdk.out"),t?(e.push(t),r?.exclusively&&e.push(o.EXCLUSIVELY)):e.push(o.ALL),e.push(o.REQUIRE_APPROVAL,o.APPROVAL_NEVER),e.push(o.CI),e.push("--no-version-reporting"),e.push("--no-path-metadata"),e.push("--no-asset-metadata"),r?.verbose?e.push(o.VERBOSE):e.push("--progress","events"),e.push(...this.argumentBuilder.buildParameterArgs(r?.parameters,t));const u={...r,timeout:r?.timeout||C};return r?.passThroughCDK?this.processManager.runCdkCommandPassthrough(s,e,u):this.processManager.runCdkCommand(s,e,u)}async destroy(s,t,r){const e=["destroy"];r?.appDir?e.push(o.APP,r.appDir):r?.useCdkOut&&e.push(o.APP,"cdk.out"),t?e.push(t):e.push(o.ALL),e.push("--force"),r?.verbose&&e.push(o.VERBOSE);const u={...r,timeout:r?.timeout||C};return this.processManager.runCdkCommand(s,e,u)}async runImport(s,t,r){const e=["import"];r?.stacks&&r.stacks.length>0&&e.push(...r.stacks),t&&e.push("--resource-mapping",t),e.push(o.REQUIRE_APPROVAL,o.APPROVAL_NEVER),e.push(o.CI),r?.noLookups&&e.push(o.NO_LOOKUPS),r?.verbose&&(e.push(o.VERBOSE),e.push("--trace"),e.push("--debug"));const u={...r,timeout:r?.timeout||D};return r?.passThroughCDK?this.processManager.runCdkCommandPassthrough(s,e,u):this.processManager.runCdkCommand(s,e,u)}async synth(s,t){t?.outputCallback?.(`Synthesising CloudFormation template...
|
|
2
|
+
`);const r=t?.noLookups?[o.NO_LOOKUPS]:[],u=["--output",A(s,t?.outputDir??b)],a=t?.context?.region||P,n=await this.processManager.runCdkCommand(s,["synth",...r,...u,o.CI,"--quiet"],{...t,context:{...t?.context,region:a},timeout:t?.timeout||k});if(!n.success){const d=n.error?c(n.error,s):"Failed to synthesise CloudFormation template";return i(d)}return n}async bootstrap(s,t,r){const e=O();l.debug("CdkService","Starting CDK bootstrap",{accountId:s,region:t,bootstrapPath:e,target:`aws://${s}/${t}`});const u=await this.processManager.runCdkCommand(e,["bootstrap",`aws://${s}/${t}`,"--cloudformation-execution-policies","arn:aws:iam::aws:policy/AdministratorAccess",...r?.permissionsBoundary!==void 0&&r.permissionsBoundary!==""?["--custom-permissions-boundary",r.permissionsBoundary]:[],...Object.entries(r?.bootstrapTags??{}).filter(([a,n])=>a!==""&&n!=="").flatMap(([a,n])=>["--tags",`${a}=${n}`]),"-c",`accountId=${s}`,o.REQUIRE_APPROVAL,o.APPROVAL_NEVER,o.CI,"--quiet","--force"],{...r,timeout:r?.timeout||S,context:{region:t,accountId:s},credentials:r?.credentials,skipProjectCheck:!0,extraEnv:{TERM:"dumb",CDK_DISABLE_NOTICES:"true",CDK_DISABLE_PROGRESS_BAR:"true"}});return l.debug("CdkService",u.success?"Bootstrap completed successfully":"Bootstrap exited with non-zero code",{accountId:s,region:t,exitCode:u.success?u.data.exitCode:void 0,output:u.success&&u.data.output?.trim()||"(empty)",error:u.success?"(empty)":u.error.trim()||"(empty)"}),u.success?p({output:"AWS environment bootstrapped",exitCode:0}):u.error.includes("already bootstrapped")?p({output:"Environment is already bootstrapped",exitCode:0}):i(y(u.error))}}export{S as BOOTSTRAP_TIMEOUT_MS,F as CdkCommandRunner,D as IMPORT_TIMEOUT_MS,k as SYNTH_TIMEOUT_MS};
|
|
@@ -20,7 +20,7 @@ export declare class CdkService {
|
|
|
20
20
|
synth(path: string, options?: CdkOptions): Promise<Result<CdkOutput, string>>;
|
|
21
21
|
bootstrap(accountId: string, region: string, options?: CdkOptions): Promise<Result<CdkOutput, string>>;
|
|
22
22
|
runCdkSynth(context: DeploymentContext, onOutput?: (chunk: string) => void, credentials?: CdkOptions["credentials"]): Promise<Result<StepOutput, string>>;
|
|
23
|
-
runCdkBootstrap(context: DeploymentContext, onOutput?: (chunk: string) => void, credentials?: CdkOptions["credentials"], permissionsBoundary?: string): Promise<Result<StepOutput, string>>;
|
|
23
|
+
runCdkBootstrap(context: DeploymentContext, onOutput?: (chunk: string) => void, credentials?: CdkOptions["credentials"], permissionsBoundary?: string, bootstrapTags?: CdkOptions["bootstrapTags"]): Promise<Result<StepOutput, string>>;
|
|
24
24
|
runCdkDiff(context: DeploymentContext, onOutput?: (chunk: string) => void): Promise<Result<StepOutput, string>>;
|
|
25
25
|
runCdkDeploy(context: DeploymentContext, stackPattern?: string, onOutput?: (chunk: string) => void, onResourceProgress?: (event: ResourceEvent) => void, aws?: AwsProvider, credentials?: CdkOptions["credentials"], parameters?: Record<string, string>, exclusively?: boolean): Promise<Result<StepOutput, string>>;
|
|
26
26
|
runCdkDestroy(context: DeploymentContext, stackPattern?: string, onOutput?: (chunk: string) => void, onResourceProgress?: (event: ResourceEvent) => void, aws?: AwsProvider, useCdkOut?: boolean, credentials?: CdkOptions["credentials"]): Promise<Result<StepOutput, string>>;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
var
|
|
1
|
+
var K=Object.defineProperty;var M=(I,e)=>K(I,"name",{value:e,configurable:!0});import{existsSync as T}from"fs";import{join as F}from"path";import{logger as R}from"@fjall/util/logger";import{success as A,failure as o}from"@fjall/generator";import{DEFAULT_REGION as k}from"../../aws/utils/regions.js";import{getErrorMessage as D,maskSensitiveOutput as g}from"@fjall/util";import{CdkEventMonitor as L,settleMonitoringAnalysis as P,startStackMonitoring as W}from"./CdkEventMonitoring.js";import{analyseDeployOutput as $,analyseDestroyResult as _,createEnhancedOutputCallback as j,formatAnalysedDeployFailure as G}from"./CdkOutputAnalyser.js";import{recordDriftSuspects as q}from"../../orchestration/drift/driftSuspectJournal.js";import{CdkCommandRunner as B}from"./CdkCommandRunner.js";import{CdkArgumentBuilder as U}from"./CdkArgumentBuilder.js";import{CdkProcessManager as z}from"./CdkProcessManager.js";import{cancelInFlightStack as H}from"./cancelInFlightStack.js";import{isAborted as w}from"../../aws/organisations/types.js";import{wrapWithConstructMapEnrichment as J}from"./constructMapEnrichment.js";import{STACK_DETECTION_FALLBACK_MS as Q,resolveStackName as N,getFallbackStackName as V,buildDeploymentCdkContext as C}from"./cdkServiceHelpers.js";class fe{static{M(this,"CdkService")}commandRunner;eventMonitor;abortSignal;constructor(e){const t=e?.processManager??new z(new U,e?.abortSignal);this.commandRunner=new B(t),this.eventMonitor=new L({eventLogWriterFactory:e?.eventLogWriterFactory,...e?.onFailureAnalysis!==void 0?{onFailureAnalysis:e.onFailureAnalysis}:{}}),this.abortSignal=e?.abortSignal}dispose(){this.commandRunner.dispose()}async checkDifferences(e,t,r){return this.commandRunner.checkDifferences(e,t,r)}async deploy(e,t,r){return this.commandRunner.deploy(e,t,r)}async destroy(e,t,r){return this.commandRunner.destroy(e,t,r)}async runImport(e,t,r){return this.commandRunner.runImport(e,t,r)}async synth(e,t){return this.commandRunner.synth(e,t)}async bootstrap(e,t,r){return this.commandRunner.bootstrap(e,t,r)}async runCdkSynth(e,t,r){const n=e.callerIdentity?.Account;try{const a=await this.synth(e.path,{outputCallback:t,context:C(e,n,e.region||k),...e.assemblyDir!==void 0?{outputDir:e.assemblyDir}:{},...r!==void 0?{credentials:r}:{}});return a.success?A({message:"CloudFormation template synthesised",details:a.data.output?{synthesisTime:a.data.output}:void 0}):o(a.error||"Failed to synthesise CloudFormation template")}catch(a){return o(`CDK synth failed: ${g(D(a))}`)}}async runCdkBootstrap(e,t,r,n,a){const m=e.callerIdentity?.Account,f=e.region||k;try{if(!m)return o("No AWS account ID available");const d=F(e.path,"node_modules");if(!T(d))return o(`Dependencies not installed. Please run 'npm install' in ${e.path} before deploying.`);const l=await this.bootstrap(m,f,{outputCallback:t,credentials:r,...n!==void 0&&n!==""?{permissionsBoundary:n}:{},...a!==void 0&&Object.keys(a).length>0?{bootstrapTags:a}:{}});return l.success?A({message:"AWS environment bootstrapped"}):o(l.error||"Failed to bootstrap AWS environment")}catch(d){return o(`CDK bootstrap failed: ${g(D(d))}`)}}async runCdkDiff(e,t){const r=e.callerIdentity?.Account;try{const n=await this.checkDifferences(e.path,void 0,{verbose:e.options?.verbose,outputCallback:t,context:C(e,r,e.region||k)});return n.success?A({message:"Diff check complete",details:{hasDifferences:n.data.hasDifferences,details:n.data.details}}):o(`CDK diff failed: ${n.error.message}`)}catch(n){return o(`CDK diff failed: ${g(D(n))}`)}}async runCdkDeploy(e,t,r,n,a,m,f,d){const l=e.callerIdentity?.Account,c=e.region||k;if(!l)return o("AWS account ID not available. Please ensure AWS credentials are properly configured.");if(!a)return o("AwsProvider is required for deployment monitoring.");const y=e.assemblyDir??F(e.path,"cdk.out"),b=J(y,n);let s=null,S;try{const u=N(t,e)??V(e);s=await this.eventMonitor.createEventMonitor("deploy",u,c,e,a),r&&(r(g(`Starting CloudFormation deployment of ${u}...
|
|
2
2
|
`)),r(`Monitoring CloudFormation events (CDK process running in background)...
|
|
3
|
-
`));const i={cdkOutput:"",actualStackName:u,stackDetected:!1,monitoringPromise:null},E=j(i,r,s,
|
|
3
|
+
`));const i={cdkOutput:"",actualStackName:u,stackDetected:!1,monitoringPromise:null},E=j(i,r,s,b);S=setTimeout(()=>{!i.stackDetected&&s&&!i.monitoringPromise&&(R.debug("CdkService","Fallback monitoring STARTING",{targetStackName:u,stackDetected:i.stackDetected,hasOnResourceProgress:!!n}),i.monitoringPromise=W(s,u,b))},Q);const O=await this.deploy(e.path,u,{verbose:e.options?.verbose,outputCallback:E,...e.assemblyDir!==void 0?{appDir:e.assemblyDir}:{useCdkOut:!0},cdkOutputLogger:s?.getEventLogger()??void 0,context:C(e,l,c),credentials:m,...f!==void 0&&Object.keys(f).length>0&&{parameters:f},...d===!0&&{exclusively:!0}});if(w(this.abortSignal))return await H(u,c,m,r),o("Deployment cancelled");const v=$(i.cdkOutput,O,i.actualStackName);if(v.success)return v;const h=await P(s,i.monitoringPromise);return h===null?v:(h.driftSuspects!==void 0&&h.driftSuspects.length>0&&await q({accountId:l,region:c,stackName:i.actualStackName,recordedAt:new Date().toISOString(),suspects:h.driftSuspects.map(p=>({stackName:i.actualStackName,logicalId:p.logicalId,resourceType:p.resourceType,...p.physicalId!==void 0?{physicalId:p.physicalId}:{},failedStatus:p.failedStatus,statusReason:p.statusReason}))}),o(G(h)))}catch(u){const i=`CDK deploy failed: ${g(D(u))}`;return R.error("CdkService","CDK deployment exception",{error:i}),o(i)}finally{clearTimeout(S),s&&s.stopMonitoring()}}async runCdkDestroy(e,t,r,n,a,m,f){const d=e.callerIdentity?.Account,l=e.region||k;let c=null;try{const y=N(t,e);d&&y&&a&&(c=await this.eventMonitor.createEventMonitor("destroy",y,l,e,a),c.startMonitoring(y,s=>{n?.(s)},(s,S)=>{}));const b=await this.destroy(e.path,t,{verbose:e.options?.verbose,outputCallback:r,useCdkOut:m,cdkOutputLogger:c?.getEventLogger()??void 0,context:C(e,d,l),credentials:f});return w(this.abortSignal)?o("Destroy cancelled"):_(b)}catch(y){return o(`CDK destroy failed: ${g(D(y))}`)}finally{c&&c.stopMonitoring()}}}export{fe as CdkService};
|
|
@@ -86,6 +86,17 @@ export interface CdkOptions {
|
|
|
86
86
|
* boundary is known to exist. Absent/empty ⇒ today's behaviour (no boundary).
|
|
87
87
|
*/
|
|
88
88
|
permissionsBoundary?: string;
|
|
89
|
+
/**
|
|
90
|
+
* Cost-allocation tags stamped onto the CDKToolkit stack as repeatable
|
|
91
|
+
* `cdk bootstrap --tags KEY=VALUE` args (CloudFormation propagates stack
|
|
92
|
+
* tags to the bootstrap roles/bucket/ECR repo). CloudFormation REPLACES the
|
|
93
|
+
* stack's whole tag set whenever the flag is present and PRESERVES it when
|
|
94
|
+
* absent — so orchestrators must either derive the same complete set on
|
|
95
|
+
* every path that emits for an account, or omit this entirely
|
|
96
|
+
* (`orchestration/bootstrapTags.ts` owns that policy). Absent/empty ⇒ no
|
|
97
|
+
* `--tags` args (today's behaviour).
|
|
98
|
+
*/
|
|
99
|
+
bootstrapTags?: Record<string, string>;
|
|
89
100
|
/**
|
|
90
101
|
* Pass `--exclusively` to `cdk deploy <stack>`: deploy only the named
|
|
91
102
|
* stack, never the dependency stacks cdk would otherwise pull in. Set by
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fjall/deploy-core",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.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",
|
|
@@ -87,8 +87,8 @@
|
|
|
87
87
|
"@aws-sdk/client-ssm": "^3.1038.0",
|
|
88
88
|
"@aws-sdk/client-sso-admin": "^3.1038.0",
|
|
89
89
|
"@aws-sdk/client-sts": "^3.1038.0",
|
|
90
|
-
"@fjall/generator": "^3.
|
|
91
|
-
"@fjall/util": "^3.
|
|
90
|
+
"@fjall/generator": "^3.4.1",
|
|
91
|
+
"@fjall/util": "^3.4.1",
|
|
92
92
|
"@smithy/node-http-handler": "^4.6.1",
|
|
93
93
|
"aws-cdk": "^2.1128.1",
|
|
94
94
|
"tsx": "^4.21.0",
|
|
@@ -99,5 +99,5 @@
|
|
|
99
99
|
"typescript": "^6.0.3",
|
|
100
100
|
"vitest": "^4.1.5"
|
|
101
101
|
},
|
|
102
|
-
"gitHead": "
|
|
102
|
+
"gitHead": "6d8ef9582d5abe6a8199431815dae761887c18d4"
|
|
103
103
|
}
|