@fjall/deploy-core 2.30.0 → 2.30.3
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/application/applicationDeployHelpers.d.ts +18 -0
- package/dist/src/orchestration/application/applicationDeployHelpers.js +3 -3
- package/dist/src/orchestration/application/artefactCollection.d.ts +8 -1
- package/dist/src/orchestration/application/artefactCollection.js +1 -1
- package/dist/src/orchestration/application/codeOnlyDeploy.d.ts +32 -1
- package/dist/src/orchestration/application/codeOnlyDeploy.js +1 -1
- package/dist/src/orchestration/application/dockerBuildHelper.d.ts +1 -0
- package/dist/src/orchestration/application/dockerBuildHelper.js +1 -1
- package/dist/src/orchestration/application/lambdaCodeOnlyRollout.d.ts +58 -0
- package/dist/src/orchestration/application/lambdaCodeOnlyRollout.js +1 -0
- package/dist/src/orchestration/serviceFactory.d.ts +2 -0
- package/dist/src/orchestration/serviceFactory.js +1 -1
- package/dist/src/services/infrastructure/LambdaService.d.ts +45 -0
- package/dist/src/services/infrastructure/LambdaService.js +1 -0
- package/package.json +5 -4
package/dist/.minified
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
216 files minified at 2026-07-16T01:01:46.559Z
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{join as ie}from"path";import{CloudFormationClient as he}from"@aws-sdk/client-cloudformation";import{EC2Client as Se}from"@aws-sdk/client-ec2";import{success as E,failure as d}from"@fjall/generator";import{logger as M}from"@fjall/util/logger";import{ImageTagSchema as be,getErrorMessage as se,imageTagParameterName as we,maskSensitiveOutput as y}from"@fjall/util";import{parseDockerServicesFromManifest as Te}from"@fjall/util/manifest";import{stubCallerIdentity as Ne}from"../../types/deployment/index.js";import{getApplicationDeployOrder as Ee,getApplicationStackName as I,getApplicationStepName as ce,getApplicationStepId as le,APPLICATION_STACKS as P}from"../../types/operations.js";import{CdkContextBuilder as Pe}from"../../services/supporting/CdkContextBuilder.js";import{buildParamsContext as ve,bootstrapOrFail as De}from"../contextHelpers.js";import{runDetectionPipeline as Re}from"./detectionPipeline.js";import{STEP_IDS as G,STEP_NAMES as Ie}from"../../types/stepDefinitions.js";import{StepRegistry as Ce}from"../../steps/stepRegistry.js";import{checkTargetReadiness as Oe}from"../../aws/targetReadiness.js";import{cascadeHomeRegion as Ae}from"../organisation/cascadeHelpers.js";import{withStepLifecycle as Le}from"../stepLifecycle.js";import{DOCKER_BUILD_STEP_NAME as de,runDockerBuild as Me}from"./dockerBuildHelper.js";import{getParallelPhase2Stacks as Fe,getStackNamesToDeploy as $e,deployParallelPhase as xe,deployStackSequential as Be,runDockerPreCompute as je,createBuildCallbacks as Ge}from"./applicationDeployHelpers.js";import{captureDatabaseConnectionOutputs as _e,captureServiceTaskDefinitions as Ue,reconcileDatabaseEndpointChange as He}from"./databaseEndpointReconcile.js";import{deployCodeOnly as Ke}from"./codeOnlyDeploy.js";import{checkDeployPathStackAvailability as ue}from"../activeDeploymentGuard.js";import{assertLeaseBeforeStack as _}from"./leaseGate.js";import{collectFullDeployArtefacts as We}from"./artefactCollection.js";import{CfnRegistryService as qe}from"../../services/infrastructure/CfnRegistryService.js";import{ChangeSetProbe as ze}from"../../services/infrastructure/changeSetProbe.js";import{runApprovalGate as Je}from"./approvalGate.js";import{buildDestructionTicket as Ye,runDestructionGate as Qe}from"./destructionGate.js";import{DESTRUCTION_RESUME_TTL_MS as Ve,signApprovalToken as Xe}from"./plan/approvalToken.js";import{computeAssemblyDigest as Ze}from"./plan/assemblyDigest.js";import{createMemoisedPlanLoader as et}from"./plan/loadDeployPlan.js";import{runDriftPreFlightGate as tt}from"../drift/preFlightGate.js";import{completeForgetAfterDeploy as rt}from"../remediation/forgetResource.js";import{composePostFailureRepairOffer as ge}from"../remediation/postFailureRepairOffer.js";import{renderPinReportLines as at,runPinRemediation as nt}from"../remediation/pinRemediation.js";import{runRecreatePreFlight as ot}from"../remediation/recreatePreFlight.js";import{runRecreateSurgery as it}from"../remediation/recreateResource.js";const F="applicationDeploy";function st(e){const a=Object.entries(e);if(a.length===0)return;const n={};for(const[t,i]of a){const h=we(t);n[h]=i}return n}function ct(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 a=Object.entries(e.serviceImageTags);if(a.length===0)return d(new Error("serviceImageTags must name at least one service"));for(const[n,t]of a){const i=be.safeParse(t);if(!i.success)return d(new Error(`serviceImageTags.${n} is not a valid image tag: ${i.error.message}`))}}return E(void 0)}function pe(e,a,n){if(e.onStackTargets===void 0||n.length===0)return;const t=a.awsProvider.getAccountId(),i=a.awsProvider.getRegion();e.onStackTargets(n.map(h=>({stackName:h,...t!==""?{accountId:t}:{},...i!==""?{region:i}:{}})))}async function qt(e,a,n){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 J=ct(i);if(!J.success){const r=y(J.error.message),o=new Error(r);return t.onError?.(o),d(o)}const x=Pe.buildDeploymentContext({deployType:"application",target:n.appName,path:n.path,region:a.awsProvider.getRegion(),callerIdentity:Ne(a.awsProvider.getAccountId()),...ve({orgConfig:e.orgConfig,identity:e.identity,skipOidc:e.options?.skipOidc})},{verbose:i?.verbose,infraOnly:i?.infraOnly},e.orgConfig),b=a.frameworkRegistry.resolve({appPath:n.path});let B;const C=$==="rollback";if(b&&!C){B=b.builder.plan({appPath:n.path},b.detection);const r=Ge(t),o=await b.builder.build(n.path,B,r,{skipBuild:i?.skipBuild,infraOnly:i?.infraOnly,...e.abortSignal!==void 0&&{abortSignal:e.abortSignal}});if(!o.success){const s=new Error(y(o.error.message));return t.onError?.(s),d(s)}}if($==="code-only"||C){t.onLog?.(C?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=ie(n.path,"cdk.out"),o=Te(r),s=o.length>0,g=b?.detection.hasDockerfile===!0,l=s||g;M.debug(F,"Deploy-only branch entered",{mode:$,imageTag:i?.imageTag,serviceImageTags:i?.serviceImageTags,appName:n.appName,appPath:n.path,cdkOutPath:r,dockerProviderAvailable:e.dockerProvider!==void 0,builderName:b?.builder.name,hasDockerfileFromManifest:s,hasDockerfileFromDisk:g,hasDockerfile:l,manifestDockerServiceCount:o.length,manifestDockerPaths:o.map(m=>m.docker.path)}),!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 ue([I(n.appName,P.COMPUTE)],a.cfnService,e.abortSignal);if(!u.success)return t.onError?.(u.error),d(u.error);pe(t,a,[I(n.appName,P.COMPUTE)]);let k={};if(!C&&e.dockerProvider!==void 0&&l){M.debug(F,"Running Docker build before code-only deploy",{source:s?"manifest":"disk"});const m=await Me(e,a,n,t);if(!m.success)return d(m.error);k=m.data.contentHashTagsByService}else M.debug(F,"Skipping Docker build",{reason:C?"rollback":e.dockerProvider===void 0?"no dockerProvider":"no Dockerfile detected"});const c=await _(e.assertLeaseForStack,I(n.appName,P.COMPUTE),e.abortSignal);return c!==void 0?(t.onError?.(c),d(c)):Ke(e,a,n,k)}t.onLog?.("Analysing infrastructure\u2026","info");const U=await Re(n,a,x,t);if(!U.success){const r=new Error(y(U.error.message));return t.onError?.(r),d(r)}const f=U.data;try{await t.onDetectionComplete?.({...f,builderName:b?.builder.name??"unknown"})}catch(r){const o=new Error(y(se(r)));return t.onError?.(o),d(o)}const fe={deploymentType:"application",operation:"deploy",deployOnly:!1,infraOnly:i?.infraOnly??!1,hasDockerfile:f.hasDockerfile,pattern:f.pattern,resources:f.resources,...b&&{builderName:b.builder.name}},Y=Ce.getSteps(fe),Q=Y.findIndex(r=>r.id===G.TARGET_READINESS),V=await Le(t,{stepId:G.TARGET_READINESS,stepName:Ie.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=a.awsProvider.getAccountId(),o=e.orgConfig?.providerAccounts.find(u=>u.id===r)?.name,s=a.awsProvider.getCredentials(),g=Ae(e.orgConfig),l=await Oe({cloudFormation:a.awsProvider.getClient(he),ec2:new Se({region:g,...s!==void 0&&{credentials:s}})},{id:r,...o!==void 0&&{name:o}},a.awsProvider.getRegion(),e.orgConfig,e.abortSignal);return l.success?l.data.ready?{kind:"completed",data:void 0}:{kind:"error",error:new Error(y(l.data.advisory))}:{kind:"error",error:new Error(y(l.error.message))}},e.abortSignal);if(!V.success)return V;const w=B?B.deployOrder:Ee({pattern:f.pattern,resources:f.resources}),v=w.length,X=f.hasDockerfile&&e.dockerProvider!==void 0&&w.includes(P.COMPUTE)&&i?.infraOnly!==!0;if(!f.hasDifferences&&!i?.force&&!X){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=f.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?.(G.DOCKER_OPERATIONS,de),t.onStepComplete?.(G.DOCKER_OPERATIONS,de,"skipped"));const l=le(g,"deploy"),u=ce(g,"deploy");t.onStepStart?.(l,u,s,v),t.onStepComplete?.(l,u,"skipped",s,v)}const o=await a.stackService.resolveWebsiteUrl(n.appName);return E({target:n.appName,deploymentType:"application",outputs:o?{websiteUrl:o}:void 0,noChanges:!0,artefacts:[],durationMs:Date.now()-h})}const H=ie(n.path,"cdk.out"),N=$e({deployOrder:w,stackChanges:f.stackChanges,appName:n.appName,force:i?.force===!0,computeBuildPossible:X});pe(t,a,[...N]);const K=await ue([...N],a.cfnService,e.abortSignal);if(!K.success)return t.onError?.(K.error),d(K.error);const Z=Ze(f.currentHashes),W=et({cfnService:a.cfnService,cdkOutPath:H,changedStacks:[...N],assemblyDigest:Z,registry:new qe(a.awsProvider),changeSetProbe:new ze(a.awsProvider),...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}}),q=await tt(a.awsProvider,{stackNames:[...N],cdkOutPath:H,cfnService:a.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 ee=q.verifiedAbsentByNecessity??[],te=e.approvalGate!==void 0||i?.approvalToken!==void 0&&i.approvalToken!==""||i?.planOnly===!0;if(te){const r=await Je({approvalGate:e.approvalGate,options:i??{},target:n.appName,cdkOutPath:H,detection:{stackChanges:f.stackChanges,currentHashes:f.currentHashes},cfnService:a.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: ${y(o.reason)}`,"warn"),E({target:n.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?Ye(s.data,ee):void 0;return E({target:n.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 Qe({loadPlan:W,options:i??{},target:n.appName,callbacks:t,verifiedAbsentByNecessity:ee,...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=te?Xe({assemblyDigest:Z,ttlMs:Ve}):void 0;return E({target:n.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(l=>o.get(l.physicalName)==="pin");if(s.length>0){t.onLog?.(`Pin remediation for ${n.appName}: probing live value(s) for ${s.length} consented pin(s) \u2014 deploy stopped for config write-back; nothing was applied`,"warn");const l=await nt(a.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 at(l))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"),E({target:n.appName,deploymentType:"application",pinPending:!0,pinReport:l,pinDeferredConsents:r.resources.length-s.length,destructionTicket:r,artefacts:[],durationMs:Date.now()-h})}const g=r.resources.filter(l=>l.finding==="pinned-name-replacement"&&o.get(l.physicalName)==="recreate");if(g.length>0){const l=new Map;for(const c of g){const m=l.get(c.stack)??[];m.push(c),l.set(c.stack,m)}const u=await ot(a.awsProvider,{stacks:[...l].map(([c,m])=>({stackName:c,targets:m.map(S=>({logicalId:S.logicalId,resourceType:S.resourceType,physicalId:S.physicalName}))})),...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}});if(!u.success){const c=new Error(y(u.error.message));return t.onError?.(c),d(c)}const k=[];for(const[c,m]of l){const S=await _(e.assertLeaseForStack,c,e.abortSignal,"destroy");if(S!==void 0)return k.length>0&&t.onLog?.(`Surgery already completed on ${k.join("; ")} \u2014 those resources were removed and re-running the deploy resumes their journals and re-creates them.`,"warn"),t.onError?.(S),d(S);const R=m.map(p=>p.physicalName);t.onLog?.(`Recreate remediation for ${n.appName}: orchestrating the pinned-name replacement of ${R.join(", ")} on ${c}`,"warn");const L=await it(a.awsProvider,{stackName:c,targets:m.map(p=>({logicalId:p.logicalId,resourceType:p.resourceType,physicalId:p.physicalName})),appName:n.appName,consentedPhysicalNames:R,doneJournalPolicy:"archive-and-restart",plannedReplacementLogicalIds:m.map(p=>p.logicalId),onProgress:p=>t.onLog?.(p,"info"),...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}});if(!L.success){const p=k.length>0?` Surgery already completed on ${k.join("; ")} \u2014 those resources were removed and re-running the deploy resumes their journals and re-creates them.`:"",j=new Error(y(`${L.error.message}${p}`));return t.onError?.(j),d(j)}k.push(`${c} (${R.join(", ")})`)}}}const re=await De(a,x,t);if(!re.success)return re;const ae=I(n.appName,P.DATABASE),z=N.has(ae),me=z?await _e(a.cfnService,n.appName):void 0,ke=z?await Ue(a.ecsResolver,a.ecsService,n.appName):void 0,O={},A=new Map;let ne;const D=async()=>{!z||!A.has(ae)||await He({appName:n.appName,connectionOutputsBefore:me,serviceTaskDefinitionsBefore:ke,services:{cfnService:a.cfnService,ecsService:a.ecsService,ecsResolver:a.ecsResolver},callbacks:t,...e.confirmDatabaseEndpointRestart!==void 0?{confirmRestart:e.confirmDatabaseEndpointRestart}:{},...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}})};for(let r=0;r<w.length;r++){const o=w[r],s=I(n.appName,o),g=le(o,"deploy"),l=ce(o,"deploy");if(!N.has(s)){t.onStepStart?.(g,l,r,v),t.onLog?.(`Skipping ${o} \u2014 no changes detected`,"info"),t.onStepComplete?.(g,l,"skipped",r,v);continue}const u=await _(e.assertLeaseForStack,s,e.abortSignal);if(u!==void 0)return await D(),t.onError?.(u),d(u);const k=Fe(w,r,n.appName,N);if(k.length>=2){const p=await xe(k,n,a,x,t,r,v,f,O,A);if(!p.success)return await D(),d(await ge(a.awsProvider,{stackNames:k.map(j=>I(n.appName,j)),appName:n.appName,originalError:p.error,cfnService:a.cfnService,...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}}));r+=k.length-1;continue}const c=await je(o,e,a,n,t,f.hasDockerfile);if(c!==null&&!c.success)return await D(),d(c.error);c!==null&&c.success&&(ne=c.data);const m=c!==null&&c.success?c.data.contentHashTagsByService:{},S=st(m);if(c!==null){const p=await _(e.assertLeaseForStack,s,e.abortSignal);if(p!==void 0)return await D(),t.onError?.(p),d(p)}const R=await Be(o,a,x,t,r,v,O,S!==void 0?{parameters:S}:void 0);if(!R.success)return await D(),d(await ge(a.awsProvider,{stackNames:[s],appName:n.appName,originalError:R.error,cfnService:a.cfnService,...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}}));const L=f.currentHashes.get(s);L&&A.set(s,L)}if(await D(),A.size>0){const r=await a.hashService.updateStateAfterDeploy(n.path,A);if(!r.success){const o=y(r.error.message);M.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 oe=await a.stackService.resolveWebsiteUrl(n.appName);oe&&(O.websiteUrl=oe);const ye=await We(a,n.appName,ne,t);return await rt(a.awsProvider,{stackNames:[...N],appName:n.appName,onLog:(r,o)=>t.onLog?.(r,o),...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}}).catch(r=>{M.debug(F,"Forget completion sweep failed",{error:y(se(r))})}),E({target:n.appName,deploymentType:"application",outputs:Object.keys(O).length>0?O:void 0,artefacts:ye,durationMs:Date.now()-h})}export{qt as deployApplication,pe as emitStackTargets};
|
|
1
|
+
import{join as z}from"path";import{CloudFormationClient as Se}from"@aws-sdk/client-cloudformation";import{EC2Client as be}from"@aws-sdk/client-ec2";import{success as D,failure as d}from"@fjall/generator";import{logger as M}from"@fjall/util/logger";import{ImageTagSchema as we,getErrorMessage as ce,imageTagParameterName as Te,maskSensitiveOutput as S}from"@fjall/util";import{parseDockerServicesFromManifest as Ee,parseLambdaDockerEntriesFromManifest as Ne}from"@fjall/util/manifest";import{stubCallerIdentity as Pe}from"../../types/deployment/index.js";import{getApplicationDeployOrder as De,getApplicationStackName as C,getApplicationStepName as le,getApplicationStepId as de,APPLICATION_STACKS as P}from"../../types/operations.js";import{CdkContextBuilder as ve}from"../../services/supporting/CdkContextBuilder.js";import{buildParamsContext as Re,bootstrapOrFail as Ce}from"../contextHelpers.js";import{runDetectionPipeline as Ie}from"./detectionPipeline.js";import{STEP_IDS as G,STEP_NAMES as Oe}from"../../types/stepDefinitions.js";import{StepRegistry as Ae}from"../../steps/stepRegistry.js";import{checkTargetReadiness as Le}from"../../aws/targetReadiness.js";import{cascadeHomeRegion as Me}from"../organisation/cascadeHelpers.js";import{withStepLifecycle as Fe}from"../stepLifecycle.js";import{DOCKER_BUILD_STEP_NAME as ue,runDockerBuild as $e}from"./dockerBuildHelper.js";import{getParallelPhase2Stacks as Be,getStackNamesToDeploy as xe,deployParallelPhase as je,deployStackSequential as Ge,runDockerPreCompute as Ue,runLambdaVersionPublish as _e,createBuildCallbacks as He}from"./applicationDeployHelpers.js";import{captureDatabaseConnectionOutputs as Ke,captureServiceTaskDefinitions as We,reconcileDatabaseEndpointChange as qe}from"./databaseEndpointReconcile.js";import{deployCodeOnly as Ve}from"./codeOnlyDeploy.js";import{checkDeployPathStackAvailability as ge}from"../activeDeploymentGuard.js";import{assertLeaseBeforeStack as U}from"./leaseGate.js";import{collectFullDeployArtefacts as ze}from"./artefactCollection.js";import{CfnRegistryService as Je}from"../../services/infrastructure/CfnRegistryService.js";import{ChangeSetProbe as Ye}from"../../services/infrastructure/changeSetProbe.js";import{runApprovalGate as Qe}from"./approvalGate.js";import{buildDestructionTicket as Xe,runDestructionGate as Ze}from"./destructionGate.js";import{DESTRUCTION_RESUME_TTL_MS as et,signApprovalToken as tt}from"./plan/approvalToken.js";import{computeAssemblyDigest as rt}from"./plan/assemblyDigest.js";import{createMemoisedPlanLoader as at}from"./plan/loadDeployPlan.js";import{runDriftPreFlightGate as nt}from"../drift/preFlightGate.js";import{completeForgetAfterDeploy as ot}from"../remediation/forgetResource.js";import{composePostFailureRepairOffer as pe}from"../remediation/postFailureRepairOffer.js";import{renderPinReportLines as it,runPinRemediation as st}from"../remediation/pinRemediation.js";import{runRecreatePreFlight as ct}from"../remediation/recreatePreFlight.js";import{runRecreateSurgery as lt}from"../remediation/recreateResource.js";const F="applicationDeploy";function dt(e){const n=Object.entries(e);if(n.length===0)return;const a={};for(const[t,i]of n){const b=Te(t);a[b]=i}return a}function ut(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=we.safeParse(t);if(!i.success)return d(new Error(`serviceImageTags.${a} is not a valid image tag: ${i.error.message}`))}}return D(void 0)}function fe(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(b=>({stackName:b,...t!==""?{accountId:t}:{},...i!==""?{region:i}:{}})))}async function Jt(e,n,a){const{callbacks:t,options:i}=e,b=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 J=ut(i);if(!J.success){const r=S(J.error.message),o=new Error(r);return t.onError?.(o),d(o)}const B=ve.buildDeploymentContext({deployType:"application",target:a.appName,path:a.path,region:n.awsProvider.getRegion(),callerIdentity:Pe(n.awsProvider.getAccountId()),...Re({orgConfig:e.orgConfig,identity:e.identity,skipOidc:e.options?.skipOidc})},{verbose:i?.verbose,infraOnly:i?.infraOnly},e.orgConfig),w=n.frameworkRegistry.resolve({appPath:a.path});let x;const I=$==="rollback";if(w&&!I){x=w.builder.plan({appPath:a.path},w.detection);const r=He(t),o=await w.builder.build(a.path,x,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"||I){t.onLog?.(I?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=z(a.path,"cdk.out"),o=Ee(r),s=Ne(r),g=o.length>0||s.length>0,c=w?.detection.hasDockerfile===!0,u=g||c;M.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:w?.builder.name,hasDockerfileFromManifest:g,hasDockerfileFromDisk:c,hasDockerfile:u,manifestDockerServiceCount:o.length,manifestDockerPaths:o.map(h=>h.docker.path),manifestLambdaDockerEntryCount:s.length}),!g&&!c&&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 m=await ge([C(a.appName,P.COMPUTE)],n.cfnService,e.abortSignal);if(!m.success)return t.onError?.(m.error),d(m.error);fe(t,n,[C(a.appName,P.COMPUTE)]);let l={},k;if(!I&&e.dockerProvider!==void 0&&u){M.debug(F,"Running Docker build before code-only deploy",{source:g?"manifest":"disk"});const h=await $e(e,n,a,t);if(!h.success)return d(h.error);l=h.data.contentHashTagsByService,k=h.data}else M.debug(F,"Skipping Docker build",{reason:I?"rollback":e.dockerProvider===void 0?"no dockerProvider":"no Dockerfile detected"});const y=await U(e.assertLeaseForStack,C(a.appName,P.COMPUTE),e.abortSignal);return y!==void 0?(t.onError?.(y),d(y)):Ve(e,n,a,l,k)}t.onLog?.("Analysing infrastructure\u2026","info");const _=await Ie(a,n,B,t);if(!_.success){const r=new Error(S(_.error.message));return t.onError?.(r),d(r)}const f=_.data;try{await t.onDetectionComplete?.({...f,builderName:w?.builder.name??"unknown"})}catch(r){const o=new Error(S(ce(r)));return t.onError?.(o),d(o)}const me={deploymentType:"application",operation:"deploy",deployOnly:!1,infraOnly:i?.infraOnly??!1,hasDockerfile:f.hasDockerfile,pattern:f.pattern,resources:f.resources,...w&&{builderName:w.builder.name}},Y=Ae.getSteps(me),Q=Y.findIndex(r=>r.id===G.TARGET_READINESS),X=await Fe(t,{stepId:G.TARGET_READINESS,stepName:Oe.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=Me(e.orgConfig),c=await Le({cloudFormation:n.awsProvider.getClient(Se),ec2:new be({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))}},e.abortSignal);if(!X.success)return X;const T=x?x.deployOrder:De({pattern:f.pattern,resources:f.resources}),v=T.length,Z=f.hasDockerfile&&e.dockerProvider!==void 0&&T.includes(P.COMPUTE)&&i?.infraOnly!==!0;if(!f.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=f.hasDockerfile&&e.dockerProvider!==void 0&&T.includes(P.COMPUTE);for(let s=0;s<T.length;s++){const g=T[s];r&&g===P.COMPUTE&&(t.onStepStart?.(G.DOCKER_OPERATIONS,ue),t.onStepComplete?.(G.DOCKER_OPERATIONS,ue,"skipped"));const c=de(g,"deploy"),u=le(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()-b})}const H=z(a.path,"cdk.out"),N=xe({deployOrder:T,stackChanges:f.stackChanges,appName:a.appName,force:i?.force===!0,computeBuildPossible:Z});fe(t,n,[...N]);const K=await ge([...N],n.cfnService,e.abortSignal);if(!K.success)return t.onError?.(K.error),d(K.error);const ee=rt(f.currentHashes),W=at({cfnService:n.cfnService,cdkOutPath:H,changedStacks:[...N],assemblyDigest:ee,registry:new Je(n.awsProvider),changeSetProbe:new Ye(n.awsProvider),...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}}),q=await nt(n.awsProvider,{stackNames:[...N],cdkOutPath:H,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 Qe({approvalGate:e.approvalGate,options:i??{},target:a.appName,cdkOutPath:H,detection:{stackChanges:f.stackChanges,currentHashes:f.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()-b});if(o.kind==="awaiting"){const s=await W(),g=s.success?Xe(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()-b})}}const E=await Ze({loadPlan:W,options:i??{},target:a.appName,callbacks:t,verifiedAbsentByNecessity:te,...e.destructionConsentGate!==void 0?{consentGate:e.destructionConsentGate}:{}});if(!E.success)return t.onError?.(E.error),d(E.error);if(E.data.kind==="withheld"){const r=re?tt({assemblyDigest:ee,ttlMs:et}):void 0;return D({target:a.appName,deploymentType:"application",destructionPending:!0,destructionTicket:E.data.ticket,destructionOutcome:E.data.outcome,destructionHaltReasons:E.data.reasons,...r!==void 0?{destructionResumeToken:r.token,destructionResumeExpiresAt:r.expiresAt}:{},artefacts:[],durationMs:Date.now()-b})}if(E.data.kind==="proceed"){const{ticket:r,verbsByPhysicalName:o}=E.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 st(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 it(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()-b})}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 ct(n.awsProvider,{stacks:[...c].map(([l,k])=>({stackName:l,targets:k.map(y=>({logicalId:y.logicalId,resourceType:y.resourceType,physicalId:y.physicalName}))})),...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}});if(!u.success){const l=new Error(S(u.error.message));return t.onError?.(l),d(l)}const m=[];for(const[l,k]of c){const y=await U(e.assertLeaseForStack,l,e.abortSignal,"destroy");if(y!==void 0)return m.length>0&&t.onLog?.(`Surgery already completed on ${m.join("; ")} \u2014 those resources were removed and re-running the deploy resumes their journals and re-creates them.`,"warn"),t.onError?.(y),d(y);const h=k.map(p=>p.physicalName);t.onLog?.(`Recreate remediation for ${a.appName}: orchestrating the pinned-name replacement of ${h.join(", ")} on ${l}`,"warn");const L=await lt(n.awsProvider,{stackName:l,targets:k.map(p=>({logicalId:p.logicalId,resourceType:p.resourceType,physicalId:p.physicalName})),appName:a.appName,consentedPhysicalNames:h,doneJournalPolicy:"archive-and-restart",plannedReplacementLogicalIds:k.map(p=>p.logicalId),onProgress:p=>t.onLog?.(p,"info"),...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}});if(!L.success){const p=m.length>0?` Surgery already completed on ${m.join("; ")} \u2014 those resources were removed and re-running the deploy resumes their journals and re-creates them.`:"",j=new Error(S(`${L.error.message}${p}`));return t.onError?.(j),d(j)}m.push(`${l} (${h.join(", ")})`)}}}const ae=await Ce(n,B,t);if(!ae.success)return ae;const ne=C(a.appName,P.DATABASE),V=N.has(ne),ke=V?await Ke(n.cfnService,a.appName):void 0,ye=V?await We(n.ecsResolver,n.ecsService,a.appName):void 0,O={},A=new Map;let oe,ie={};const R=async()=>{!V||!A.has(ne)||await qe({appName:a.appName,connectionOutputsBefore:ke,serviceTaskDefinitionsBefore:ye,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}:{}})};for(let r=0;r<T.length;r++){const o=T[r],s=C(a.appName,o),g=de(o,"deploy"),c=le(o,"deploy");if(!N.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 U(e.assertLeaseForStack,s,e.abortSignal);if(u!==void 0)return await R(),t.onError?.(u),d(u);const m=Be(T,r,a.appName,N);if(m.length>=2){const p=await je(m,a,n,B,t,r,v,f,O,A);if(!p.success)return await R(),d(await pe(n.awsProvider,{stackNames:m.map(j=>C(a.appName,j)),appName:a.appName,originalError:p.error,cfnService:n.cfnService,...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}}));r+=m.length-1;continue}const l=await Ue(o,e,n,a,t,f.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:{},y=dt(k);if(l!==null){const p=await U(e.assertLeaseForStack,s,e.abortSignal);if(p!==void 0)return await R(),t.onError?.(p),d(p)}const h=await Ge(o,n,B,t,r,v,O,y!==void 0?{parameters:y}:void 0);if(!h.success)return await R(),d(await pe(n.awsProvider,{stackNames:[s],appName:a.appName,originalError:h.error,cfnService:n.cfnService,...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}}));o===P.COMPUTE&&(ie=await _e(a,n,t,k));const L=f.currentHashes.get(s);L&&A.set(s,L)}if(await R(),A.size>0){const r=await n.hashService.updateStateAfterDeploy(a.path,A);if(!r.success){const o=S(r.error.message);M.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&&(O.websiteUrl=se);const he=await ze(n,a.appName,oe,t,z(a.path,"cdk.out"),ie);return await ot(n.awsProvider,{stackNames:[...N],appName:a.appName,onLog:(r,o)=>t.onLog?.(r,o),...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}}).catch(r=>{M.debug(F,"Forget completion sweep failed",{error:S(ce(r))})}),D({target:a.appName,deploymentType:"application",outputs:Object.keys(O).length>0?O:void 0,artefacts:he,durationMs:Date.now()-b})}export{Jt as deployApplication,fe as emitStackTargets};
|
|
@@ -7,6 +7,7 @@ import type { DeployServices } from "../serviceFactory.js";
|
|
|
7
7
|
import type { DetectionResult } from "./detectionPipeline.js";
|
|
8
8
|
import { type DockerBuildProducts } from "./dockerBuildHelper.js";
|
|
9
9
|
import type { BuildCallbacks } from "../../types/frameworkBuilder.js";
|
|
10
|
+
import type { PublishedLambdaVersion } from "../../services/infrastructure/LambdaService.js";
|
|
10
11
|
/**
|
|
11
12
|
* Single source of the deploy loop's stack-inclusion predicate: a stack
|
|
12
13
|
* deploys when its template hash changed, when `--force` overrides the hash
|
|
@@ -55,6 +56,23 @@ export type DockerPreComputeOutcome = DockerBuildProducts;
|
|
|
55
56
|
* required for this stack.
|
|
56
57
|
*/
|
|
57
58
|
export declare function runDockerPreCompute(stack: ApplicationStack, params: DeployParams, services: DeployServices, operation: ApplicationOperation, callbacks: DeployCallbacks, hasDockerfileOnDisk: boolean): Promise<Result<DockerPreComputeOutcome> | null>;
|
|
59
|
+
/**
|
|
60
|
+
* Publish a fresh Lambda version for every container-image Lambda whose
|
|
61
|
+
* shared `image` build key was actually rebuilt this cycle.
|
|
62
|
+
*
|
|
63
|
+
* CDK's `Function.currentVersion` can't be trusted here: its `Version`
|
|
64
|
+
* logical-id hash is computed from synth-time template shape, and the image
|
|
65
|
+
* reference is a `CfnParameter` Ref (a constant token across synths, same
|
|
66
|
+
* mechanism ECS task-definition image tags use) — so the hash doesn't change
|
|
67
|
+
* between deploys even though the parameter's resolved value (and therefore
|
|
68
|
+
* the deployed image) does. An explicit `PublishVersion` call after the
|
|
69
|
+
* Compute stack deploy succeeds sidesteps that entirely.
|
|
70
|
+
*
|
|
71
|
+
* Best-effort by contract, matching `artefactCollection.ts`'s post-deploy
|
|
72
|
+
* discovery: a publish failure degrades (warns, omits that function from the
|
|
73
|
+
* returned map) rather than failing a deploy that already succeeded.
|
|
74
|
+
*/
|
|
75
|
+
export declare function runLambdaVersionPublish(operation: ApplicationOperation, services: DeployServices, callbacks: DeployCallbacks, contentHashTagsByService: Record<string, string>): Promise<Record<string, PublishedLambdaVersion>>;
|
|
58
76
|
/**
|
|
59
77
|
* Bridge DeployCallbacks to BuildCallbacks.
|
|
60
78
|
* Fires both legacy OpenNext-specific callbacks and generic build callbacks
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{success as h,failure as
|
|
2
|
-
`);return
|
|
1
|
+
import{join as b}from"node:path";import{success as h,failure as P}from"@fjall/generator";import{maskSensitiveOutput as y}from"@fjall/util";import{parseLambdaDockerEntriesFromManifest as A}from"@fjall/util/manifest";import{isAborted as B}from"../../aws/organisations/types.js";import{APPLICATION_STACKS as E,getApplicationStackName as N,getApplicationStepName as O,getApplicationStepId as w,PARALLEL_DEPLOY_GROUPS as R}from"../../types/operations.js";import{emitProgress as $}from"../../services/supporting/helpers.js";import{forwardOutput as k,forwardResourceProgress as j}from"../contextHelpers.js";import{emptyBuildProducts as D,runDockerBuild as I}from"./dockerBuildHelper.js";import{runWelcomeImageSetup as T}from"./welcomeImageHelper.js";import{withStepLifecycle as _}from"../stepLifecycle.js";function z(e){const r=new Set;for(const n of e.deployOrder){const i=N(e.appName,n),o=e.stackChanges.get(i)??!0,a=n===E.COMPUTE&&e.computeBuildPossible;(o||e.force||a)&&r.add(i)}return r}function J(e,r,n,i){const o=new Set(R.PHASE_2.stacks),a=[];for(let s=r;s<e.length&&o.has(e[s]);s++)a.push(e[s]);if(a.length<2)return[];const u=a.filter(s=>i.has(N(n,s)));return u.length>=2?u:[]}async function Q(e,r,n,i,o,a,u,s,p,f){for(let t=0;t<e.length;t++){const d=e[t],c=w(d,"deploy"),m=O(d,"deploy");o.onStepStart?.(c,m,a+t,u)}$(o,"Deploying infrastructure in parallel\u2026"),o.onParallelPhaseStart?.(e,"Storage and database resources (parallel)");const l=await n.stackService.deployStacksInParallel(e,i,{onOutput:k(o),onResourceProgress:(t,d)=>{o.onResourceProgress?.(t),d&&o.onParallelStackResourceProgress?.(d,t)},onStackComplete:(t,d,c,m)=>{const S=w(t,"deploy"),L=O(t,"deploy"),x=a+e.indexOf(t),C=B(n.abortSignal);o.onStepComplete?.(S,L,d?"completed":C?"cancelled":"error",x,u),!d&&m&&!C&&o.onError?.(new Error(y(m.message)))}});if(!l.success)return o.onParallelPhaseComplete?.([]),P(l.error);const g=l.data.filter(t=>!t.success);if(o.onParallelPhaseComplete?.(l.data.map(t=>({stack:t.stack,success:t.success,error:t.error}))),g.length>0){const t=g.map(c=>c.stack).join(", "),d=g.map(c=>`${c.stack}: ${y(c.error?.message??"Unknown error")}`).join(`
|
|
2
|
+
`);return P(new Error(`Failed to deploy stacks: ${t}
|
|
3
3
|
|
|
4
|
-
${
|
|
4
|
+
${d}`))}for(const t of l.data){if(t.success&&t.outputs)for(const[m,S]of Object.entries(t.outputs))p[m]=String(S);const d=N(r.appName,t.stack),c=s.currentHashes.get(d);c&&f.set(d,c)}return h(void 0)}async function X(e,r,n,i,o,a,u,s){const p=w(e,"deploy"),f=O(e,"deploy");return _(i,{stepId:p,stepName:f,stepIndex:o,totalSteps:a},async()=>{const l=await r.stackService.deployStack(e,n,{onOutput:k(i),onResourceProgress:j(i)},s?.parameters!==void 0?{parameters:s.parameters}:void 0);if(!l.success)return{kind:"error",error:l.error};if(l.data.outputs)for(const[g,t]of Object.entries(l.data.outputs))u[g]=String(t);return{kind:"completed",data:void 0}},r.abortSignal)}async function Z(e,r,n,i,o,a){if(e!==E.COMPUTE||!r.dockerProvider)return null;if(a){const s=await I(r,n,i,o);return s.success?h(s.data):P(s.error)}const u=await T(r,n,i,o);return u.success?h(D()):P(u.error)}async function ee(e,r,n,i){if(Object.keys(i).length===0)return{};const o=b(e.path,"cdk.out"),u=A(o).filter(p=>i[p.imageKey]!==void 0);if(u.length===0)return{};const s={};for(const p of u){const f=await r.lambdaService.publishVersion(p.name);if(!f.success){n.onLog?.(`Published image for Lambda '${p.name}', but publishing a new version failed: ${y(f.error.message)}. The function's $LATEST code is up to date; only the published-version record is affected.`,"warn");continue}s[p.name]=f.data}return s}function te(e){return{onBuildStart:r=>{e.onOpenNextBuildStart?.(),e.onLog?.(`${r} build started`,"info")},onBuildProgress:(r,n)=>{e.onOpenNextProgress?.(n)},onBuildComplete:r=>{e.onOpenNextBuildComplete?.(),e.onLog?.(`${r} build complete`,"info")},onBuildError:(r,n)=>{e.onOpenNextBuildError?.(n)}}}export{te as createBuildCallbacks,Q as deployParallelPhase,X as deployStackSequential,J as getParallelPhase2Stacks,z as getStackNamesToDeploy,Z as runDockerPreCompute,ee as runLambdaVersionPublish};
|
|
@@ -2,6 +2,7 @@ import { type ServiceArtefact } from "@fjall/util";
|
|
|
2
2
|
import type { DeployCallbacks } from "../../types/callbacks.js";
|
|
3
3
|
import type { DeployServices } from "../serviceFactory.js";
|
|
4
4
|
import type { DockerBuildProducts } from "./dockerBuildHelper.js";
|
|
5
|
+
import type { PublishedLambdaVersion } from "../../services/infrastructure/LambdaService.js";
|
|
5
6
|
/**
|
|
6
7
|
* Collect `ServiceArtefact[]` for a successful FULL deploy from the build
|
|
7
8
|
* step's products plus a best-effort post-deploy ECS resolution (the same
|
|
@@ -14,5 +15,11 @@ import type { DockerBuildProducts } from "./dockerBuildHelper.js";
|
|
|
14
15
|
* service name; `taskDefinitionArn` stays absent) and warns — it NEVER fails
|
|
15
16
|
* a deploy that already succeeded. Returns `[]` when the deploy pushed no
|
|
16
17
|
* images (welcome-image seed, legacy fallback, infra-only).
|
|
18
|
+
*
|
|
19
|
+
* `contentHashTagsByService` keys are a mix of ECS service names and Lambda
|
|
20
|
+
* `image` build keys (several Lambda functions may share one key). Keys that
|
|
21
|
+
* match a container-Lambda manifest entry are expanded into one artefact PER
|
|
22
|
+
* ACTUAL FUNCTION NAME (there is no ECS task-definition/live-service concept
|
|
23
|
+
* for them); every other key is treated as an ECS service, unchanged.
|
|
17
24
|
*/
|
|
18
|
-
export declare function collectFullDeployArtefacts(services: DeployServices, appName: string, build: DockerBuildProducts | undefined, callbacks: DeployCallbacks): Promise<ServiceArtefact[]>;
|
|
25
|
+
export declare function collectFullDeployArtefacts(services: DeployServices, appName: string, build: DockerBuildProducts | undefined, callbacks: DeployCallbacks, cdkOutPath: string, publishedLambdaVersions?: Record<string, PublishedLambdaVersion>): Promise<ServiceArtefact[]>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{maskSensitiveOutput as
|
|
1
|
+
import{maskSensitiveOutput as N}from"@fjall/util";import{parseLambdaDockerEntriesFromManifest as b}from"@fjall/util/manifest";import{logger as h}from"@fjall/util/logger";import{extractEcsServiceName as D}from"../../aws/utils/arnParser.js";import{deriveEcrRepositoryArn as k}from"./codeOnlyDeploy.js";const w="artefactCollection";async function O(s,t,n,r,o,c={}){if(n===void 0||n.repoUri===void 0)return[];const i=Object.entries(n.contentHashTagsByService);if(i.length===0)return[];const d=n.repoUri,e=k(d),u=await L(s,t,r),g=new Map;for(const a of b(o)){const f=g.get(a.imageKey)??[];f.push(a.name),g.set(a.imageKey,f)}const v=[];for(const[a,f]of i){const m=n.imageDigestsByService[a],A=g.get(a);if(A!==void 0){for(const S of A){const p=c[S];v.push({serviceName:S,imageTag:f,imageUri:`${d}:${f}`,...m!==void 0?{imageDigest:m}:{},...e!==""?{ecrRepositoryArn:e}:{},...p!==void 0?{functionArn:p.functionArn,publishedVersion:p.version}:{}})}continue}const l=C(a,u);v.push({serviceName:l?.ecsServiceName??a,imageTag:f,imageUri:`${d}:${f}`,...m!==void 0?{imageDigest:m}:{},...e!==""?{ecrRepositoryArn:e}:{},...l?.taskDefinitionArn!==void 0?{taskDefinitionArn:l.taskDefinitionArn}:{}})}return v}async function L(s,t,n){try{const r=await s.ecsResolver.getDeployableClusterAndServices(t);if(!r.success)return y(n,t,r.error.message),[];const{clusterArn:o,serviceArns:c}=r.data;if(!o||c.length===0)return h.debug(w,"No deployable ECS services discovered",{appName:t}),[];const i=await s.ecsService.describeServices(o,c);if(!i.success)return y(n,t,i.error.message),c.map(e=>D(e)).filter(e=>e!==void 0&&e!=="").map(e=>({ecsServiceName:e}));const d=[];for(const e of i.data){const u=e.serviceName??(e.serviceArn!==void 0?D(e.serviceArn):void 0);u===void 0||u===""||d.push({ecsServiceName:u,...e.taskDefinition!==void 0?{taskDefinitionArn:e.taskDefinition}:{}})}return d}catch(r){return y(n,t,r instanceof Error?r.message:String(r)),[]}}function C(s,t){const n=s.toLowerCase(),r=t.find(i=>i.ecsServiceName.toLowerCase()===n);if(r)return r;const o=t.filter(i=>i.ecsServiceName.toLowerCase().includes(n));if(o.length===1)return o[0];const c=o.filter(i=>i.ecsServiceName.toLowerCase().endsWith(`-${n}`));if(c.length===1)return c[0]}function y(s,t,n){const r=N(n);h.debug(w,"Post-deploy artefact resolution degraded",{appName:t,error:r}),s.onLog?.(`Deploy succeeded, but resolving live task definitions for release artefacts failed: ${r}. Artefacts are recorded without task definition ARNs.`,"warn")}export{O as collectFullDeployArtefacts};
|
|
@@ -3,6 +3,7 @@ import { type ManifestDockerService } from "@fjall/util/manifest";
|
|
|
3
3
|
import type { DeployParams, DeployResult } from "../../types/params.js";
|
|
4
4
|
import type { ApplicationOperation } from "../../types/operations.js";
|
|
5
5
|
import type { DeployServices } from "../serviceFactory.js";
|
|
6
|
+
import { type DockerBuildProducts } from "./dockerBuildHelper.js";
|
|
6
7
|
/**
|
|
7
8
|
* Resolve the ECR image tag the rollout should pin against for a given
|
|
8
9
|
* manifest service. Keys the content-hash tag map by the bare `<service>`
|
|
@@ -39,6 +40,28 @@ export declare function lookupContentHashTag(manifestService: ManifestDockerServ
|
|
|
39
40
|
* caller surfaces a warning on multi-service stacks.
|
|
40
41
|
*/
|
|
41
42
|
export declare function rebaseRollbackTagForService(suppliedTag: string, serviceName: string, allServiceNames: readonly string[]): string;
|
|
43
|
+
/**
|
|
44
|
+
* Converge the Compute stack's `<Service>ImageTag` CfnParameters to the tags
|
|
45
|
+
* actually rolled (F4b). A rollback mutates ECS (or a Lambda function)
|
|
46
|
+
* directly, bypassing CloudFormation; without this, the stack's declared
|
|
47
|
+
* parameters stay at their pre-rollback values and the next full deploy
|
|
48
|
+
* re-applies them, silently reverting the rollback. Runs for BOTH rollback
|
|
49
|
+
* surfaces (the `serviceImageTags` map and the legacy single `--image-tag`);
|
|
50
|
+
* never for a plain code-only deploy (no rollback tag exists there). Exported
|
|
51
|
+
* so `lambdaCodeOnlyRollout.ts`'s rollback paths can feed their rolled
|
|
52
|
+
* `imageKey`s into the SAME batched reconcile call as the ECS entries —
|
|
53
|
+
* `rolledServices` is keyed generically (`serviceName` doubles as `imageKey`
|
|
54
|
+
* for a Lambda entry, since that's the CfnParameter key
|
|
55
|
+
* `imageTagParameterName` derives from).
|
|
56
|
+
*
|
|
57
|
+
* Best-effort: the services are already rolled by the time this runs, so a
|
|
58
|
+
* reconcile failure must not fail the deploy — it surfaces a prominent
|
|
59
|
+
* warning instead.
|
|
60
|
+
*/
|
|
61
|
+
export declare function reconcileImageTagParameters(services: DeployServices, appName: string, rolledServices: ReadonlyArray<{
|
|
62
|
+
serviceName: string;
|
|
63
|
+
imageTag: string;
|
|
64
|
+
}>, callbacks: DeployParams["callbacks"], abortSignal?: AbortSignal): Promise<void>;
|
|
42
65
|
/**
|
|
43
66
|
* Code-only deployment orchestrator.
|
|
44
67
|
*
|
|
@@ -60,8 +83,16 @@ export declare function rebaseRollbackTagForService(suppliedTag: string, service
|
|
|
60
83
|
* Release. Unlike `imageTag`, map-supplied tags are never re-based — they
|
|
61
84
|
* already name the exact service — and never fall back to tag pinning; see
|
|
62
85
|
* `serviceImageTagsRollback.ts`.
|
|
86
|
+
*
|
|
87
|
+
* `dockerBuildProducts` is the FULL build-phase output (only present on the
|
|
88
|
+
* plain-code-only path, when a build actually ran this cycle — `undefined`
|
|
89
|
+
* for both rollback modes, which roll an existing tag and never rebuild). It
|
|
90
|
+
* carries `repoUri`/`imageDigestsByService` so `lambdaCodeOnlyRollout.ts`'s
|
|
91
|
+
* primary path can build a digest-pinned `ImageUri` without an extra ECR
|
|
92
|
+
* call — `contentHashTagsByService` alone (the historical 4th param) isn't
|
|
93
|
+
* enough for that.
|
|
63
94
|
*/
|
|
64
|
-
export declare function deployCodeOnly(params: DeployParams, services: DeployServices, operation: ApplicationOperation, contentHashTagsByService: Record<string, string
|
|
95
|
+
export declare function deployCodeOnly(params: DeployParams, services: DeployServices, operation: ApplicationOperation, contentHashTagsByService: Record<string, string>, dockerBuildProducts?: DockerBuildProducts): Promise<Result<DeployResult>>;
|
|
65
96
|
/**
|
|
66
97
|
* Derive an ECR repository ARN from an image URI of shape
|
|
67
98
|
* `<account>.dkr.ecr.<region>.amazonaws.com/<repo>`. Returns "" when the URI
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{join as W}from"path";import{success as H,failure as O}from"@fjall/generator";import{logger as A}from"@fjall/util/logger";import{artefactOutputKey as I,imageTagParameterName as X,maskSensitiveOutput as C}from"@fjall/util";import{parseDockerServicesFromManifest as Y}from"@fjall/util/manifest";import{APPLICATION_STACKS as x,getApplicationStackName as q,getApplicationStepId as J,getApplicationStepName as Q}from"../../types/operations.js";import{extractEcsServiceName as L}from"../../aws/utils/arnParser.js";import{isAborted as K}from"../../aws/organisations/types.js";import{stripTagOrDigest as V}from"../../services/infrastructure/EcrImageInspectorService.js";import{withStepLifecycle as Z}from"../stepLifecycle.js";import{registerAndRollTaskDefinition as B}from"./taskDefinitionRoll.js";import{matchesServiceName as ee,matchServiceImageTagsToLiveServices as te,preflightResolveServiceImageTagDigests as re,resolveServiceTaskDefinition as ie}from"./serviceImageTagsRollback.js";const N="codeOnlyDeploy";function ne(t,s){if(t)return s[t.name]}function ae(t,s){const e=s.toLowerCase();return t.find(i=>i.name.toLowerCase()===e)??t.find(i=>e.includes(i.name.toLowerCase()))}function oe(t,s,e){const i=s.toLowerCase(),n=t.toLowerCase(),d=e.map(c=>c.toLowerCase()).filter(c=>c!==""&&n.startsWith(`${c}-`)).sort((c,m)=>m.length-c.length)[0];if(d===void 0||d===i)return t;const f=t.slice(d.length);return`${i}${f}`}const se=0,ce=1;function j(t,s,e,i,n){i[I(t,"TaskDefinition")]=e.taskDefinitionArn,i[I(t,"PreviousTaskDefinition")]=e.previousTaskDefinitionArn,i[I(t,"ImageTag")]=s,i[I(t,"ImageUri")]=e.imageUri,i[I(t,"EcrRepositoryArn")]=e.ecrRepositoryArn,e.imageDigest!==void 0&&(i[I(t,"ImageDigest")]=e.imageDigest),n.push({serviceName:t,imageTag:s,imageUri:e.imageUri,taskDefinitionArn:e.taskDefinitionArn,previousTaskDefinitionArn:e.previousTaskDefinitionArn,...e.imageDigest!==void 0?{imageDigest:e.imageDigest}:{},...e.ecrRepositoryArn!==""?{ecrRepositoryArn:e.ecrRepositoryArn}:{}})}async function F(t,s,e,i,n){if(e.length===0)return;const d=q(s,x.COMPUTE),f={};for(const{serviceName:m,imageTag:l}of e)f[X(m)]=l;const c=await t.cfnService.updateStackParameters(d,f,n);if(!c.success){i.onLog?.(`Rollback succeeded but the CloudFormation ImageTag parameter reconcile failed \u2014 the next full deploy may revert this rollback. Reason: ${C(c.error.message)}`,"warn");return}c.data.skippedKeys.length>0&&(i.onLog?.(`Rollback succeeded but ${c.data.skippedKeys.length} ImageTag parameter(s) were not found on stack ${d} (${c.data.skippedKeys.join(", ")}) \u2014 the next full deploy may revert those services`,"warn"),A.debug(N,"ImageTag reconcile skipped stale parameters",{stackName:d,skippedKeys:c.data.skippedKeys}))}async function Se(t,s,e,i){const{callbacks:n,options:d}=t,f=Date.now(),c=J(x.COMPUTE,"deploy"),m=Q(x.COMPUTE,"deploy");return Z(n,{stepId:c,stepName:m,stepIndex:se,totalSteps:ce},async()=>{const l=d?.imageTag,$=d?.serviceImageTags;l?n.onLog?.(`Rolling to supplied image tag ${l}`,"info"):$&&n.onLog?.(`Rolling ${Object.keys($).length} service(s) to their release-recorded image tags`,"info");const k=Y(W(e.path,"cdk.out"));A.debug(N,"Code-only rollout starting",{explicitRollbackTag:l,serviceImageTags:$,workingDirectory:t.workingDirectory,contentHashTagsByService:i,manifestServiceCount:k.length,manifestServices:k.map(o=>({name:o.name,target:o.docker.target}))}),n.onLog?.("Discovering ECS cluster and services\u2026","info");const u=await s.ecsResolver.getDeployableClusterAndServices(e.appName);if(!u.success)return A.debug(N,"ECS discovery failed",{appName:e.appName,error:C(u.error.message)}),{kind:"error",error:u.error};const{clusterArn:D,serviceArns:r}=u.data;if(A.debug(N,"ECS discovery complete",{appName:e.appName,clusterArn:D,serviceCount:r.length,serviceArns:r}),!D||r.length===0){const o=`No deployable ECS services found for ${e.appName}`;return n.onLog?.(o,"warn"),{kind:"skipped",data:{target:e.appName,deploymentType:"application",durationMs:Date.now()-f,noChanges:!0,artefacts:[]}}}if($!==void 0)return ge(s,e,D,r,$,n,t.abortSignal,f);const a=d?.serviceName?.toLowerCase(),y=a===""?void 0:a,p=y?r.filter(o=>ee(L(o)??"",y)):r;if(y&&p.length===0){const o=r.map(w=>L(w)??"unknown").join(", ");return{kind:"error",error:new Error(`Service '${y}' not found. Available: ${o||"none"}`)}}const v={},E=[],g=[],b=r.map(o=>L(o)).filter(o=>o!==void 0&&o!=="");for(let o=0;o<p.length;o++){if(K(t.abortSignal))return{kind:"error",error:new Error(`Rollout cancelled after ${E.length}/${p.length} services`)};const w=p[o];if(!w)continue;const h=L(w)??`service-${o+1}`,G=`[${o+1}/${p.length}] ${h}`,R=ae(k,h),M=ne(R,i);let T,P;if(l!==void 0)T=oe(l,h,b),T!==l?(P=l,n.onLog?.(`Re-based image tag ${l} \u2192 ${T} for ${h}`,"info")):b.length>1&&!l.toLowerCase().startsWith(`${h.toLowerCase()}-`)&&n.onLog?.(`Image tag ${l} has no recognised service prefix; applying verbatim to ${h} \u2014 confirm it points at this service's image`,"warn");else if(M!==void 0)T=M;else{const U=`No image available to deploy for ${h}: no content-hash tag was produced this run. Run a full deploy to build the image, or pass --image-tag <tag> to roll a specific existing image.`;n.onLog?.(U,"warn"),g.push(`${h}: ${U}`);continue}A.debug(N,"Starting service rollout",{serviceName:h,serviceArn:w,imageTag:T,explicitRollbackTag:l,contentHashTag:M,manifestServiceMatched:R?.name,dockerTarget:R?.docker.target,index:o,total:p.length}),n.onECSUpdate?.(`${G}: rolling out image tag ${T}`,h);const S=await _(s,D,w,h,T,n,P,void 0,t.abortSignal);if(A.debug(N,"Service rollout completed",{serviceName:h,success:S.success,...S.success?{taskDefinitionArn:S.data.taskDefinitionArn,imageDigest:S.data.imageDigest,durationMs:S.data.durationMs}:{error:C(S.error.message)}}),!S.success){g.push(`${h}: ${C(S.error.message)}`);continue}j(h,T,S.data,v,E)}const z=Date.now()-f;if(g.length>0){const o=p.length-g.length,w=p.length,h=g.length===w?`All ${w} ECS service rollouts failed: ${g.join("; ")}`:`Partial rollout: ${o}/${w} services succeeded. Failures: ${g.join("; ")}`;return{kind:"error",error:new Error(h)}}return l!==void 0&&await F(s,e.appName,E.map(o=>({serviceName:o.serviceName,imageTag:o.imageTag})),n,t.abortSignal),{kind:"completed",data:{target:e.appName,deploymentType:"application",outputs:Object.keys(v).length>0?v:void 0,artefacts:E,durationMs:z}}},t.abortSignal)}async function ge(t,s,e,i,n,d,f,c){const m=te(i,n);for(const r of m.unmatchedLiveServiceNames)d.onLog?.(`Service ${r} is not part of this release \u2014 leaving it untouched`,"warn");for(const r of m.staleMapKeys)d.onLog?.(`Service ${r} from the release no longer exists in the stack`,"warn");if(m.matched.length===0)return{kind:"error",error:new Error("None of the services in serviceImageTags matched a live ECS service in this stack \u2014 nothing to roll")};const l=await re(t,e,m.matched,f);if(!l.success)return{kind:"error",error:l.error};const $={},k=[],u=[];for(let r=0;r<m.matched.length;r++){if(K(f))return{kind:"error",error:new Error(`Rollout cancelled after ${k.length}/${m.matched.length} services`)};const a=m.matched[r];if(!a)continue;const y=`[${r+1}/${m.matched.length}] ${a.serviceName}`,p=l.data.get(a.serviceArn);if(p===void 0){u.push(`${a.serviceName}: pre-flight digest missing after a successful pre-flight`);continue}A.debug(N,"Starting service rollout (serviceImageTags)",{serviceName:a.serviceName,serviceArn:a.serviceArn,imageTag:a.tag,mapKey:a.mapKey,index:r,total:m.matched.length}),d.onECSUpdate?.(`${y}: rolling out image tag ${a.tag}`,a.serviceName);const v=await _(t,e,a.serviceArn,a.serviceName,a.tag,d,void 0,p,f);if(A.debug(N,"Service rollout completed (serviceImageTags)",{serviceName:a.serviceName,success:v.success,...v.success?{taskDefinitionArn:v.data.taskDefinitionArn,imageDigest:v.data.imageDigest,durationMs:v.data.durationMs}:{error:C(v.error.message)}}),!v.success){u.push(`${a.serviceName}: ${C(v.error.message)}`);continue}j(a.serviceName,a.tag,v.data,$,k)}const D=Date.now()-c;if(u.length>0){const r=m.matched.length-u.length,a=m.matched.length,y=u.length===a?`All ${a} ECS service rollouts failed: ${u.join("; ")}`:`Partial rollout: ${r}/${a} services succeeded. Failures: ${u.join("; ")}`;return{kind:"error",error:new Error(y)}}return await F(t,s.appName,k.map(r=>({serviceName:r.serviceName,imageTag:r.imageTag})),d,f),{kind:"completed",data:{target:s.appName,deploymentType:"application",outputs:Object.keys($).length>0?$:void 0,artefacts:k,durationMs:D}}}async function _(t,s,e,i,n,d,f,c,m){const l=Date.now();let $,k,u,D,r;if(c!==void 0)$=c.taskDefinition.previousTaskDefArn,k=c.taskDefinition.taskDefinition,u=c.taskDefinition.containerDefinitions,D=c.taskDefinition.repoUri,r=c.imageDigest;else{const g=await ie(t,s,e,i);if(!g.success)return O(g.error);$=g.data.previousTaskDefArn,k=g.data.taskDefinition,u=g.data.containerDefinitions,D=g.data.repoUri;const b=await t.ecrImageInspector.getImageDigest(D,n,m);if(b.success)r=b.data;else{if(f!==void 0)return O(new Error(`Re-based image tag '${n}' (from supplied '${f}') was not found in ECR for ${i}: ${C(b.error.message)}. The supplied tag's build has no image for ${i} \u2014 supply that service's own tag, or pass --service to roll a single service.`));d.onLog?.(`Could not resolve image digest for ${i} (${C(b.error.message)}); falling back to tag pinning`,"warn")}}const a=g=>r!==void 0?`${g}@${r}`:`${g}:${n}`,y=u.map(g=>g.image?{...g,image:a(V(g.image))}:g),p=await B(t,{clusterArn:s,serviceArn:e,serviceName:i,taskDefinition:k,containerDefinitions:y,previousTaskDefArn:$,...r!==void 0?{imageDigest:r}:{},startedAtMs:l},d);if(!p.success)return O(p.error);const v=`${D}:${n}`,E=de(D);return H({taskDefinitionArn:p.data.newTaskDefArn,previousTaskDefinitionArn:$,imageUri:v,ecrRepositoryArn:E,imageDigest:r,durationMs:p.data.durationMs})}function de(t){const s=t.match(/^(\d{12})\.dkr\.ecr\.([a-z0-9-]+)\.amazonaws\.com\/(.+)$/);if(!s)return"";const[,e,i,n]=s;return`arn:aws:ecr:${i}:${e}:repository/${n}`}export{Se as deployCodeOnly,de as deriveEcrRepositoryArn,ne as lookupContentHashTag,oe as rebaseRollbackTagForService};
|
|
1
|
+
import{join as q}from"path";import{success as te,failure as z}from"@fjall/generator";import{logger as A}from"@fjall/util/logger";import{artefactOutputKey as M,imageTagParameterName as re,maskSensitiveOutput as N}from"@fjall/util";import{parseDockerServicesFromManifest as ne,parseLambdaDockerEntriesFromManifest as ae}from"@fjall/util/manifest";import{APPLICATION_STACKS as G,getApplicationStackName as ie,getApplicationStepId as oe,getApplicationStepName as se}from"../../types/operations.js";import{extractEcsServiceName as P}from"../../aws/utils/arnParser.js";import{isAborted as H}from"../../aws/organisations/types.js";import{stripTagOrDigest as ce}from"../../services/infrastructure/EcrImageInspectorService.js";import{withStepLifecycle as ge}from"../stepLifecycle.js";import{registerAndRollTaskDefinition as de}from"./taskDefinitionRoll.js";import{ecrRepoUri as le}from"./dockerBuildHelper.js";import{matchesServiceName as J,matchServiceImageTagsToLiveServices as me,preflightResolveServiceImageTagDigests as fe,resolveServiceTaskDefinition as ue}from"./serviceImageTagsRollback.js";import{rolloutLambdaImageTag as pe,rolloutLambdaPrimary as he,rolloutLambdaServiceImageTags as ve}from"./lambdaCodeOnlyRollout.js";const C="codeOnlyDeploy";function ke(t,r){if(t)return r[t.name]}function $e(t,r){const e=r.toLowerCase();return t.find(a=>a.name.toLowerCase()===e)??t.find(a=>e.includes(a.name.toLowerCase()))}function ye(t,r,e){const a=r.toLowerCase(),l=t.toLowerCase(),n=e.map(o=>o.toLowerCase()).filter(o=>o!==""&&l.startsWith(`${o}-`)).sort((o,f)=>f.length-o.length)[0];if(n===void 0||n===a)return t;const m=t.slice(n.length);return`${a}${m}`}const we=0,De=1;function Q(t,r,e,a,l){a[M(t,"TaskDefinition")]=e.taskDefinitionArn,a[M(t,"PreviousTaskDefinition")]=e.previousTaskDefinitionArn,a[M(t,"ImageTag")]=r,a[M(t,"ImageUri")]=e.imageUri,a[M(t,"EcrRepositoryArn")]=e.ecrRepositoryArn,e.imageDigest!==void 0&&(a[M(t,"ImageDigest")]=e.imageDigest),l.push({serviceName:t,imageTag:r,imageUri:e.imageUri,taskDefinitionArn:e.taskDefinitionArn,previousTaskDefinitionArn:e.previousTaskDefinitionArn,...e.imageDigest!==void 0?{imageDigest:e.imageDigest}:{},...e.ecrRepositoryArn!==""?{ecrRepositoryArn:e.ecrRepositoryArn}:{}})}async function x(t,r,e,a,l){if(e.length===0)return;const n=ie(r,G.COMPUTE),m={};for(const{serviceName:f,imageTag:w}of e)m[re(f)]=w;const o=await t.cfnService.updateStackParameters(n,m,l);if(!o.success){a.onLog?.(`Rollback succeeded but the CloudFormation ImageTag parameter reconcile failed \u2014 the next full deploy may revert this rollback. Reason: ${N(o.error.message)}`,"warn");return}o.data.skippedKeys.length>0&&(a.onLog?.(`Rollback succeeded but ${o.data.skippedKeys.length} ImageTag parameter(s) were not found on stack ${n} (${o.data.skippedKeys.join(", ")}) \u2014 the next full deploy may revert those services`,"warn"),A.debug(C,"ImageTag reconcile skipped stale parameters",{stackName:n,skippedKeys:o.data.skippedKeys}))}function V(t,r,e,a,l){if(e)return{kind:"error",error:e};if(t.kind==="error")return r.failures.length>0?{kind:"error",error:new Error(`${t.error.message}; Lambda rollout failures: ${r.failures.join("; ")}`)}:t;if(r.failures.length>0)return{kind:"error",error:new Error(`Lambda rollout failures: ${r.failures.join("; ")}`)};const n=t.data,m={...n.outputs??{},...r.outputs};return{kind:"completed",data:{target:a.appName,deploymentType:"application",outputs:Object.keys(m).length>0?m:void 0,artefacts:[...n.artefacts,...r.artefacts],durationMs:Date.now()-l}}}async function je(t,r,e,a,l){const{callbacks:n,options:m}=t,o=Date.now(),f=oe(G.COMPUTE,"deploy"),w=se(G.COMPUTE,"deploy");return ge(n,{stepId:f,stepName:w,stepIndex:we,totalSteps:De},async()=>{const g=m?.imageTag,h=m?.serviceImageTags;g?n.onLog?.(`Rolling to supplied image tag ${g}`,"info"):h&&n.onLog?.(`Rolling ${Object.keys(h).length} service(s) to their release-recorded image tags`,"info");const v=ne(q(e.path,"cdk.out"));A.debug(C,"Code-only rollout starting",{explicitRollbackTag:g,serviceImageTags:h,workingDirectory:t.workingDirectory,contentHashTagsByService:a,manifestServiceCount:v.length,manifestServices:v.map(s=>({name:s.name,target:s.docker.target}))}),n.onLog?.("Discovering ECS cluster and services\u2026","info");const $=await r.ecsResolver.getDeployableClusterAndServices(e.appName);if(!$.success)return A.debug(C,"ECS discovery failed",{appName:e.appName,error:N($.error.message)}),{kind:"error",error:$.error};const{clusterArn:c,serviceArns:i}=$.data;A.debug(C,"ECS discovery complete",{appName:e.appName,clusterArn:c,serviceCount:i.length,serviceArns:i});const y=ae(q(e.path,"cdk.out")),D=Array.from(new Set(y.map(s=>s.imageKey))),k=c!==void 0&&i.length>0,R=y.length>0;if(!k&&!R){const s=`No deployable ECS services or container-image Lambda functions found for ${e.appName}`;return n.onLog?.(s,"warn"),{kind:"skipped",data:{target:e.appName,deploymentType:"application",durationMs:Date.now()-o,noChanges:!0,artefacts:[]}}}const u=c??"";if(h!==void 0){const s=k?await Se(r,e,u,i,h,n,t.abortSignal,o):{kind:"completed",data:{target:e.appName,deploymentType:"application",artefacts:[],durationMs:Date.now()-o}},{outcome:p,error:d}=await ve(r,e,h,n,t.abortSignal);return p.rolledImageKeys.length>0&&await x(r,e.appName,p.rolledImageKeys,n,t.abortSignal),V(s,p,d,e,o)}const E=m?.serviceName?.toLowerCase(),I=E===""?void 0:E,T=I?i.filter(s=>J(P(s)??"",I)):i;if(I){const s=y.some(p=>J(p.name,I));if(T.length===0&&!s){const p=[...i.map(d=>P(d)??"unknown"),...y.map(d=>d.name)];return{kind:"error",error:new Error(`Service '${I}' not found. Available: ${p.join(", ")||"none"}`)}}}const K={},O=[],L=[],U=[...i.map(s=>P(s)).filter(s=>s!==void 0&&s!==""),...D];for(let s=0;s<T.length;s++){if(H(t.abortSignal))return{kind:"error",error:new Error(`Rollout cancelled after ${O.length}/${T.length} services`)};const p=T[s];if(!p)continue;const d=P(p)??`service-${s+1}`,ee=`[${s+1}/${T.length}] ${d}`,F=$e(v,d),_=ke(F,a);let b,X;if(g!==void 0)b=ye(g,d,U),b!==g?(X=g,n.onLog?.(`Re-based image tag ${g} \u2192 ${b} for ${d}`,"info")):U.length>1&&!g.toLowerCase().startsWith(`${d.toLowerCase()}-`)&&n.onLog?.(`Image tag ${g} has no recognised service prefix; applying verbatim to ${d} \u2014 confirm it points at this service's image`,"warn");else if(_!==void 0)b=_;else{const Y=`No image available to deploy for ${d}: no content-hash tag was produced this run. Run a full deploy to build the image, or pass --image-tag <tag> to roll a specific existing image.`;n.onLog?.(Y,"warn"),L.push(`${d}: ${Y}`);continue}A.debug(C,"Starting service rollout",{serviceName:d,serviceArn:p,imageTag:b,explicitRollbackTag:g,contentHashTag:_,manifestServiceMatched:F?.name,dockerTarget:F?.docker.target,index:s,total:T.length}),n.onECSUpdate?.(`${ee}: rolling out image tag ${b}`,d);const S=await Z(r,u,p,d,b,n,X,void 0,t.abortSignal);if(A.debug(C,"Service rollout completed",{serviceName:d,success:S.success,...S.success?{taskDefinitionArn:S.data.taskDefinitionArn,imageDigest:S.data.imageDigest,durationMs:S.data.durationMs}:{error:N(S.error.message)}}),!S.success){L.push(`${d}: ${N(S.error.message)}`);continue}Q(d,b,S.data,K,O)}const B=Date.now()-o;if(L.length>0){const s=T.length-L.length,p=T.length,d=L.length===p?`All ${p} ECS service rollouts failed: ${L.join("; ")}`:`Partial rollout: ${s}/${p} services succeeded. Failures: ${L.join("; ")}`;return{kind:"error",error:new Error(d)}}g!==void 0&&await x(r,e.appName,O.map(s=>({serviceName:s.serviceName,imageTag:s.imageTag})),n,t.abortSignal);const W={kind:"completed",data:{target:e.appName,deploymentType:"application",outputs:Object.keys(K).length>0?K:void 0,artefacts:O,durationMs:B}};if(!R)return W;const j=g!==void 0?await pe(r,e,g,U,n,I,t.abortSignal):await he(r,e,a,l?.imageDigestsByService??{},l?.repoUri??le(r.awsProvider.getAccountId(),r.awsProvider.getRegion(),e.appName),n,I,t.abortSignal);return g!==void 0&&j.rolledImageKeys.length>0&&await x(r,e.appName,j.rolledImageKeys,n,t.abortSignal),V(W,j,void 0,e,o)},t.abortSignal)}async function Se(t,r,e,a,l,n,m,o){const f=me(a,l);for(const c of f.unmatchedLiveServiceNames)n.onLog?.(`Service ${c} is not part of this release \u2014 leaving it untouched`,"warn");for(const c of f.staleMapKeys)n.onLog?.(`Service ${c} from the release no longer exists in the stack`,"warn");if(f.matched.length===0)return{kind:"error",error:new Error("None of the services in serviceImageTags matched a live ECS service in this stack \u2014 nothing to roll")};const w=await fe(t,e,f.matched,m);if(!w.success)return{kind:"error",error:w.error};const g={},h=[],v=[];for(let c=0;c<f.matched.length;c++){if(H(m))return{kind:"error",error:new Error(`Rollout cancelled after ${h.length}/${f.matched.length} services`)};const i=f.matched[c];if(!i)continue;const y=`[${c+1}/${f.matched.length}] ${i.serviceName}`,D=w.data.get(i.serviceArn);if(D===void 0){v.push(`${i.serviceName}: pre-flight digest missing after a successful pre-flight`);continue}A.debug(C,"Starting service rollout (serviceImageTags)",{serviceName:i.serviceName,serviceArn:i.serviceArn,imageTag:i.tag,mapKey:i.mapKey,index:c,total:f.matched.length}),n.onECSUpdate?.(`${y}: rolling out image tag ${i.tag}`,i.serviceName);const k=await Z(t,e,i.serviceArn,i.serviceName,i.tag,n,void 0,D,m);if(A.debug(C,"Service rollout completed (serviceImageTags)",{serviceName:i.serviceName,success:k.success,...k.success?{taskDefinitionArn:k.data.taskDefinitionArn,imageDigest:k.data.imageDigest,durationMs:k.data.durationMs}:{error:N(k.error.message)}}),!k.success){v.push(`${i.serviceName}: ${N(k.error.message)}`);continue}Q(i.serviceName,i.tag,k.data,g,h)}const $=Date.now()-o;if(v.length>0){const c=f.matched.length-v.length,i=f.matched.length,y=v.length===i?`All ${i} ECS service rollouts failed: ${v.join("; ")}`:`Partial rollout: ${c}/${i} services succeeded. Failures: ${v.join("; ")}`;return{kind:"error",error:new Error(y)}}return await x(t,r.appName,h.map(c=>({serviceName:c.serviceName,imageTag:c.imageTag})),n,m),{kind:"completed",data:{target:r.appName,deploymentType:"application",outputs:Object.keys(g).length>0?g:void 0,artefacts:h,durationMs:$}}}async function Z(t,r,e,a,l,n,m,o,f){const w=Date.now();let g,h,v,$,c;if(o!==void 0)g=o.taskDefinition.previousTaskDefArn,h=o.taskDefinition.taskDefinition,v=o.taskDefinition.containerDefinitions,$=o.taskDefinition.repoUri,c=o.imageDigest;else{const u=await ue(t,r,e,a);if(!u.success)return z(u.error);g=u.data.previousTaskDefArn,h=u.data.taskDefinition,v=u.data.containerDefinitions,$=u.data.repoUri;const E=await t.ecrImageInspector.getImageDigest($,l,f);if(E.success)c=E.data;else{if(m!==void 0)return z(new Error(`Re-based image tag '${l}' (from supplied '${m}') was not found in ECR for ${a}: ${N(E.error.message)}. The supplied tag's build has no image for ${a} \u2014 supply that service's own tag, or pass --service to roll a single service.`));n.onLog?.(`Could not resolve image digest for ${a} (${N(E.error.message)}); falling back to tag pinning`,"warn")}}const i=u=>c!==void 0?`${u}@${c}`:`${u}:${l}`,y=v.map(u=>u.image?{...u,image:i(ce(u.image))}:u),D=await de(t,{clusterArn:r,serviceArn:e,serviceName:a,taskDefinition:h,containerDefinitions:y,previousTaskDefArn:g,...c!==void 0?{imageDigest:c}:{},startedAtMs:w},n);if(!D.success)return z(D.error);const k=`${$}:${l}`,R=Te($);return te({taskDefinitionArn:D.data.newTaskDefArn,previousTaskDefinitionArn:g,imageUri:k,ecrRepositoryArn:R,imageDigest:c,durationMs:D.data.durationMs})}function Te(t){const r=t.match(/^(\d{12})\.dkr\.ecr\.([a-z0-9-]+)\.amazonaws\.com\/(.+)$/);if(!r)return"";const[,e,a,l]=r;return`arn:aws:ecr:${a}:${e}:repository/${l}`}export{je as deployCodeOnly,Te as deriveEcrRepositoryArn,ke as lookupContentHashTag,ye as rebaseRollbackTagForService,x as reconcileImageTagParameters};
|
|
@@ -4,6 +4,7 @@ import type { DeployCallbacks } from "../../types/callbacks.js";
|
|
|
4
4
|
import type { ApplicationOperation } from "../../types/operations.js";
|
|
5
5
|
import type { DeployServices } from "../serviceFactory.js";
|
|
6
6
|
export declare const DOCKER_BUILD_STEP_NAME = "Building and pushing Docker image";
|
|
7
|
+
export declare function ecrRepoUri(accountId: string, region: string, appName: string): string;
|
|
7
8
|
/**
|
|
8
9
|
* Everything the build phase knows about what it pushed. The tag map drives
|
|
9
10
|
* the CDK `<Service>ImageTag` parameters; the digest map + repo URI feed the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{isAbsolute as H,join as _,dirname as M,resolve as K}from"node:path";import{existsSync as j}from"node:fs";import{success as P,failure as h}from"@fjall/generator";import{deriveContentHashTag as z,maskSensitiveOutput as b,toKebab as x}from"@fjall/util";import{evaluateBakeGuard as X,evaluateResolvedBuildArgValues as Y,acknowledgedBuildArgKeys as q,createBakeWarningDeduper as J,buildCacheRepositoryName as U,BAKE_GUARD_EXPOSURE_CLAUSE as Q}from"@fjall/util/docker";import{runWithBoundedConcurrency as Z,resolveBuildConcurrency as ee}from"@fjall/util/buildConcurrency";import{logger as T}from"@fjall/util/logger";import{parseDockerServicesFromManifest as re}from"@fjall/util/manifest";import{STEP_IDS as A}from"../../types/stepDefinitions.js";import{isAborted as G}from"../../aws/organisations/types.js";import{resolveBuildSecrets as te,resolveSecretRefValue as ne,resolveBuildSecretSessionProvider as oe,sourcedRefsFromBuildArgs as ie}from"./buildSecretResolver.js";import{partitionForRegion as se}from"./buildSecretSession.js";import{resolveBuildArgs as ae}from"./buildArgResolver.js";import{validateBuildGroup as de}from"./buildGroupValidator.js";import{ensureBuildCacheRepository as ce}from"./ecrCacheRepository.js";const I="dockerBuildHelper",C="Building and pushing Docker image";function ue(e,n,t){const r=_(n,e);if(j(r))return{path:r,base:n};if(t!==""&&t!==n){const i=_(t,e);if(j(i))return{path:i,base:t}}return{path:r,base:n}}function le(e,n,t){const r=e.docker.path,i=e.docker.context;let s,a;if(H(r))s=r,a=n;else{const p=ue(r,n,t);s=p.path,a=p.base}return{buildContext:i?H(i)?K(i):K(a,i):K(M(s)),dockerfilePath:s,target:e.docker.target,resolvedViaFallback:a!==n}}function fe(e){return`${e.buildContext}|${e.dockerfilePath}|${e.target??""}`}function ge(e,n){return`Dockerfile not found for service '${e}' at ${n}. Checked the synth directory and the working directory. If your Dockerfile lives at the project root with the CDK app in a subdirectory, set an absolute docker.context (the build context must share the Dockerfile's directory).`}function me(e){return e.startsWith("cn-")?"amazonaws.com.cn":"amazonaws.com"}function V(e,n){return`${e}.dkr.ecr.${n}.${me(n)}`}function pe(e,n,t){return`${V(e,n)}/${x(t)}`}function he(e,n,t){return`${V(e,n)}/${U(x(t))}`}function be(e,n,t){const r=new Map;for(const i of e){const s=le(i,n,t),a=fe(s),m=r.get(a);m?m.members.push(i):r.set(a,{identity:s,members:[i]})}return Array.from(r.values())}async function ve(e,n,t,r,i){const s=await e.buildAndPush({appName:n.appName,appPath:n.path,region:t,accountId:r},(a,m,p,y)=>{i.onDockerProgress?.(b(a),m,p,y)});return s.success?P({imageUri:s.data.imageUri,imageTag:s.data.imageTag}):h(s.error)}function F(){return{contentHashTagsByService:{},imageDigestsByService:{}}}async function Se(e,n,t,r,i,s,a,m,p,y,E,f,R,w){const l=e.members[0];if(!j(e.identity.dockerfilePath))return h(new Error(ge(l.name,e.identity.dockerfilePath)));e.identity.resolvedViaFallback&&m.onLog?.(`Building service '${l.name}' from ${e.identity.dockerfilePath} (resolved at the project root).`,"info");const v=[...l.docker.buildSecrets??[],...R??[]],N=de({serviceName:l.name,dockerfilePath:e.identity.dockerfilePath,buildContext:e.identity.buildContext,...e.identity.target!==void 0&&{target:e.identity.target},...l.docker.buildArgs!==void 0&&{buildArgs:l.docker.buildArgs},...v.length>0&&{buildSecrets:v},members:e.members.map(o=>({serviceName:o.name,...o.docker.buildArgs!==void 0&&{buildArgs:o.docker.buildArgs},...o.docker.buildSecrets!==void 0&&{buildSecrets:o.docker.buildSecrets}}))});if(!N.success)return h(N.error);const d=X(l.docker.buildArgs,x(t.appName));for(const o of d.warnings){const S=b(o.message);w(S)&&m.onLog?.(S,"warn")}if(d.findings.length>0)return h(new Error(b(d.findings.map(o=>o.message).join(" "))));const u=v,D=ie(l.docker.buildArgs),L={appName:t.appName,region:r,accountId:i,partition:se(r)},$=await oe(p,[...u,...D],L);if(!$.success)return h($.error);const O=$.data,k=await ae({manifestBuildArgs:l.docker.buildArgs,serviceDir:e.identity.buildContext,appDir:t.path,...E!==void 0&&{stage:E},shellEnv:process.env,...f!==void 0&&{flagBuildArgs:f},resolveSourcedValue:(o,S)=>ne(O,{id:o,...S},y)});if(!k.success)return h(k.error);const B=k.data.buildArgs;for(const o of k.data.inferredDotenvKeys){const S=b(`buildArg "${o}" was auto-declared from a .env file and will be baked into the image \u2014 ${Q}.`);w(S)&&m.onLog?.(S,"warn")}const c=Y(B,x(t.appName),q(l.docker.buildArgs));if(c.length>0)return h(new Error(b(c.map(o=>o.message).join(" "))));let g;if(u.length>0){const o=await te(O,u,y);if(!o.success)return h(o.error);g=o.data}try{if(G(y))return h(new Error(`Build of service '${l.name}' was aborted before the docker build started`));const o={appName:t.appName,appPath:e.identity.buildContext,region:r,accountId:i,buildContext:e.identity.buildContext,dockerfilePath:e.identity.dockerfilePath,pushByDigest:!0,serviceName:l.name,...a!==void 0&&{cacheRepoUri:a},...e.identity.target!==void 0&&{target:e.identity.target},...Object.keys(B).length>0&&{buildArgs:B},...g!==void 0&&{buildSecrets:g.resolved}};return await ye(e,o,n,s,m)}finally{g!==void 0&&await g.cleanup().catch(()=>m.onLog?.("Build-secret cleanup failed.","warn"))}}async function ye(e,n,t,r,i){const s=await t.buildAndPush(n,(f,R,w,l)=>{i.onDockerProgress?.(b(f),R,w,l)});if(!s.success)return h(s.error);const a=s.data.imageDigest;if(typeof a!="string"||!a.startsWith("sha256:"))return h(new Error(`Buildx push succeeded but returned an invalid digest: ${b(String(a))}`));const m={},p={},y=[];for(const f of e.members){const R=f.docker.target,w=z(a,f.name,R);if(!w.success)return h(w.error);const l=w.data,v=f.name;m[v]=l,p[v]=a,y.push(`${r}:${l}`)}const E=await t.tagByDigest({sourceImage:r,digest:a,tags:y});return E.success?P({contentHashTagsByService:m,imageDigestsByService:p}):h(E.error)}async function Le(e,n,t,r){const i=e.dockerProvider;if(!i)return P(F());const s=n.awsProvider.getAccountId();if(!s)return r.onLog?.("Skipping Docker build \u2014 account ID not available","warn"),P(F());const a=n.awsProvider.getRegion(),m=_(t.path,"cdk.out"),p=re(m),y=pe(s,a,t.appName);if(T.debug(I,"runDockerBuild starting",{appName:t.appName,appPath:t.path,accountId:s,region:a,cdkOutPath:m,manifestServiceCount:p.length,manifestServices:p.map(d=>({name:d.name,path:d.docker.path,context:d.docker.context,target:d.docker.target})),stepId:A.DOCKER_OPERATIONS,stepName:C}),r.onStepStart?.(A.DOCKER_OPERATIONS,C),p.length===0){r.onLog?.("Initialising ECR repository\u2026","info");const d=await i.initialiseECR({appName:t.appName,region:a,accountId:s});d.success||r.onLog?.(`ECR initialisation failed: ${b(d.error.message)}`,"warn"),r.onLog?.("Building and pushing Docker image\u2026","info");const u=await ve(i,t,a,s,r);if(!u.success){T.debug(I,"Docker buildAndPush (legacy) failed",{appName:t.appName,error:b(u.error.message)}),r.onStepComplete?.(A.DOCKER_OPERATIONS,C,"error");const D=new Error(b(u.error.message));return r.onError?.(D),h(D)}return r.onStepComplete?.(A.DOCKER_OPERATIONS,C,"completed"),r.onLog?.(`Docker image pushed: ${u.data.imageUri}`,"info"),P(F())}const E=await i.preflight?.();if(E!==void 0&&!E.success){r.onStepComplete?.(A.DOCKER_OPERATIONS,C,"error");const d=new Error(b(E.error.message));return r.onError?.(d),h(d)}const f=be(p,t.path,e.workingDirectory);let R=he(s,a,t.appName);const w=await ce(n.awsProvider,U(x(t.appName)),e.abortSignal);w.success||(R=void 0,r.onLog?.(`Building without registry cache \u2014 ${w.error.message}`,"warn")),r.onLog?.(`Building and pushing ${p.length} service image(s) in ${f.length} group(s)\u2026`,"info");let l=!1;if(i.beginEcrSession!==void 0){const d=await i.beginEcrSession({accountId:s,region:a});if(!d.success){r.onStepComplete?.(A.DOCKER_OPERATIONS,C,"error");const u=new Error(b(d.error.message));return r.onError?.(u),h(u)}l=!0}const v=new AbortController,N=()=>v.abort();e.abortSignal!==void 0&&(e.abortSignal.aborted?v.abort():e.abortSignal.addEventListener("abort",N,{once:!0}));try{const d=()=>G(e.abortSignal)?"the deployment was cancelled":"a sibling group failed";let u;const D=ee(f.length),L=J(),$=f.map((c,g)=>async()=>{const o=c.members.map(W=>W.name).join(", ");if(v.signal.aborted)throw new Error(`Build of group ${g+1}/${f.length} (${o}) was not started \u2014 ${d()}`);r.onLog?.(`Building group ${g+1}/${f.length} (${o})\u2026`,"info");const S=await Se(c,i,t,a,s,y,R,r,n.awsProvider,v.signal,e.options?.environment,e.options?.flagBuildArgs,e.options?.flagBuildSecrets,L);if(!S.success)throw u===void 0&&(u={error:S.error,leader:c.members[0]?.name,members:o}),v.abort(),S.error;return S.data}),O=await Z($,D,{abortOnFirstFailure:!0,abortSignal:v.signal});if(u===void 0)for(let c=0;c<f.length;c++){const g=O[c];if(!(g!==void 0&&g.success)){u={error:g!==void 0&&!g.success?g.error:new Error(`Build of group ${c+1}/${f.length} never ran \u2014 ${d()}`),leader:f[c].members[0]?.name,members:f[c].members.map(o=>o.name).join(", ")};break}}if(u!==void 0){T.debug(I,"Docker buildAndPush failed",{appName:t.appName,leader:u.leader,members:u.members,error:b(u.error.message)}),r.onStepComplete?.(A.DOCKER_OPERATIONS,C,"error");const c=new Error(b(u.error.message));return r.onError?.(c),h(c)}const k={},B={};for(const c of O)if(!(c===void 0||!c.success)){for(const[g,o]of Object.entries(c.data.contentHashTagsByService))k[g]=o;for(const[g,o]of Object.entries(c.data.imageDigestsByService))B[g]=o}return T.debug(I,"Docker buildAndPush succeeded",{appName:t.appName,services:p.map(c=>c.name),groupCount:f.length,contentHashTags:k}),r.onStepComplete?.(A.DOCKER_OPERATIONS,C,"completed"),r.onLog?.(`Docker images pushed for ${p.length} service(s)`,"info"),P({contentHashTagsByService:k,imageDigestsByService:B,repoUri:y})}finally{e.abortSignal?.removeEventListener("abort",N),l&&i.endEcrSession!==void 0&&await i.endEcrSession().catch(d=>{T.debug(I,"endEcrSession cleanup failed",{error:b(d instanceof Error?d.message:String(d))})})}}export{C as DOCKER_BUILD_STEP_NAME,F as emptyBuildProducts,Le as runDockerBuild};
|
|
1
|
+
import{isAbsolute as H,join as _,dirname as W,resolve as K}from"node:path";import{existsSync as F}from"node:fs";import{success as P,failure as h}from"@fjall/generator";import{deriveContentHashTag as z,maskSensitiveOutput as b,toKebab as x}from"@fjall/util";import{evaluateBakeGuard as X,evaluateResolvedBuildArgValues as Y,acknowledgedBuildArgKeys as q,createBakeWarningDeduper as J,buildCacheRepositoryName as U,BAKE_GUARD_EXPOSURE_CLAUSE as Q}from"@fjall/util/docker";import{runWithBoundedConcurrency as Z,resolveBuildConcurrency as ee}from"@fjall/util/buildConcurrency";import{logger as T}from"@fjall/util/logger";import{parseDockerServicesFromManifest as re,parseLambdaDockerServicesFromManifest as te}from"@fjall/util/manifest";import{STEP_IDS as A}from"../../types/stepDefinitions.js";import{isAborted as G}from"../../aws/organisations/types.js";import{resolveBuildSecrets as ne,resolveSecretRefValue as oe,resolveBuildSecretSessionProvider as ie,sourcedRefsFromBuildArgs as se}from"./buildSecretResolver.js";import{partitionForRegion as ae}from"./buildSecretSession.js";import{resolveBuildArgs as de}from"./buildArgResolver.js";import{validateBuildGroup as ce}from"./buildGroupValidator.js";import{ensureBuildCacheRepository as ue}from"./ecrCacheRepository.js";const I="dockerBuildHelper",C="Building and pushing Docker image";function le(e,n,t){const r=_(n,e);if(F(r))return{path:r,base:n};if(t!==""&&t!==n){const i=_(t,e);if(F(i))return{path:i,base:t}}return{path:r,base:n}}function fe(e,n,t){const r=e.docker.path,i=e.docker.context;let s,a;if(H(r))s=r,a=n;else{const p=le(r,n,t);s=p.path,a=p.base}return{buildContext:i?H(i)?K(i):K(a,i):K(W(s)),dockerfilePath:s,target:e.docker.target,resolvedViaFallback:a!==n}}function ge(e){return`${e.buildContext}|${e.dockerfilePath}|${e.target??""}`}function me(e,n){return`Dockerfile not found for service '${e}' at ${n}. Checked the synth directory and the working directory. If your Dockerfile lives at the project root with the CDK app in a subdirectory, set an absolute docker.context (the build context must share the Dockerfile's directory).`}function pe(e){return e.startsWith("cn-")?"amazonaws.com.cn":"amazonaws.com"}function M(e,n){return`${e}.dkr.ecr.${n}.${pe(n)}`}function he(e,n,t){return`${M(e,n)}/${x(t)}`}function be(e,n,t){return`${M(e,n)}/${U(x(t))}`}function ve(e,n,t){const r=new Map;for(const i of e){const s=fe(i,n,t),a=ge(s),g=r.get(a);g?g.members.push(i):r.set(a,{identity:s,members:[i]})}return Array.from(r.values())}async function Se(e,n,t,r,i){const s=await e.buildAndPush({appName:n.appName,appPath:n.path,region:t,accountId:r},(a,g,p,y)=>{i.onDockerProgress?.(b(a),g,p,y)});return s.success?P({imageUri:s.data.imageUri,imageTag:s.data.imageTag}):h(s.error)}function j(){return{contentHashTagsByService:{},imageDigestsByService:{}}}async function ye(e,n,t,r,i,s,a,g,p,y,E,f,k,w){const l=e.members[0];if(!F(e.identity.dockerfilePath))return h(new Error(me(l.name,e.identity.dockerfilePath)));e.identity.resolvedViaFallback&&g.onLog?.(`Building service '${l.name}' from ${e.identity.dockerfilePath} (resolved at the project root).`,"info");const v=[...l.docker.buildSecrets??[],...k??[]],N=ce({serviceName:l.name,dockerfilePath:e.identity.dockerfilePath,buildContext:e.identity.buildContext,...e.identity.target!==void 0&&{target:e.identity.target},...l.docker.buildArgs!==void 0&&{buildArgs:l.docker.buildArgs},...v.length>0&&{buildSecrets:v},members:e.members.map(o=>({serviceName:o.name,...o.docker.buildArgs!==void 0&&{buildArgs:o.docker.buildArgs},...o.docker.buildSecrets!==void 0&&{buildSecrets:o.docker.buildSecrets}}))});if(!N.success)return h(N.error);const d=X(l.docker.buildArgs,x(t.appName));for(const o of d.warnings){const S=b(o.message);w(S)&&g.onLog?.(S,"warn")}if(d.findings.length>0)return h(new Error(b(d.findings.map(o=>o.message).join(" "))));const u=v,D=se(l.docker.buildArgs),L={appName:t.appName,region:r,accountId:i,partition:ae(r)},$=await ie(p,[...u,...D],L);if(!$.success)return h($.error);const O=$.data,R=await de({manifestBuildArgs:l.docker.buildArgs,serviceDir:e.identity.buildContext,appDir:t.path,...E!==void 0&&{stage:E},shellEnv:process.env,...f!==void 0&&{flagBuildArgs:f},resolveSourcedValue:(o,S)=>oe(O,{id:o,...S},y)});if(!R.success)return h(R.error);const B=R.data.buildArgs;for(const o of R.data.inferredDotenvKeys){const S=b(`buildArg "${o}" was auto-declared from a .env file and will be baked into the image \u2014 ${Q}.`);w(S)&&g.onLog?.(S,"warn")}const c=Y(B,x(t.appName),q(l.docker.buildArgs));if(c.length>0)return h(new Error(b(c.map(o=>o.message).join(" "))));let m;if(u.length>0){const o=await ne(O,u,y);if(!o.success)return h(o.error);m=o.data}try{if(G(y))return h(new Error(`Build of service '${l.name}' was aborted before the docker build started`));const o={appName:t.appName,appPath:e.identity.buildContext,region:r,accountId:i,buildContext:e.identity.buildContext,dockerfilePath:e.identity.dockerfilePath,pushByDigest:!0,serviceName:l.name,...a!==void 0&&{cacheRepoUri:a},...e.identity.target!==void 0&&{target:e.identity.target},...Object.keys(B).length>0&&{buildArgs:B},...m!==void 0&&{buildSecrets:m.resolved}};return await we(e,o,n,s,g)}finally{m!==void 0&&await m.cleanup().catch(()=>g.onLog?.("Build-secret cleanup failed.","warn"))}}async function we(e,n,t,r,i){const s=await t.buildAndPush(n,(f,k,w,l)=>{i.onDockerProgress?.(b(f),k,w,l)});if(!s.success)return h(s.error);const a=s.data.imageDigest;if(typeof a!="string"||!a.startsWith("sha256:"))return h(new Error(`Buildx push succeeded but returned an invalid digest: ${b(String(a))}`));const g={},p={},y=[];for(const f of e.members){const k=f.docker.target,w=z(a,f.name,k);if(!w.success)return h(w.error);const l=w.data,v=f.name;g[v]=l,p[v]=a,y.push(`${r}:${l}`)}const E=await t.tagByDigest({sourceImage:r,digest:a,tags:y});return E.success?P({contentHashTagsByService:g,imageDigestsByService:p}):h(E.error)}async function _e(e,n,t,r){const i=e.dockerProvider;if(!i)return P(j());const s=n.awsProvider.getAccountId();if(!s)return r.onLog?.("Skipping Docker build \u2014 account ID not available","warn"),P(j());const a=n.awsProvider.getRegion(),g=_(t.path,"cdk.out"),p=[...re(g),...te(g)],y=he(s,a,t.appName);if(T.debug(I,"runDockerBuild starting",{appName:t.appName,appPath:t.path,accountId:s,region:a,cdkOutPath:g,manifestServiceCount:p.length,manifestServices:p.map(d=>({name:d.name,path:d.docker.path,context:d.docker.context,target:d.docker.target})),stepId:A.DOCKER_OPERATIONS,stepName:C}),r.onStepStart?.(A.DOCKER_OPERATIONS,C),p.length===0){r.onLog?.("Initialising ECR repository\u2026","info");const d=await i.initialiseECR({appName:t.appName,region:a,accountId:s});d.success||r.onLog?.(`ECR initialisation failed: ${b(d.error.message)}`,"warn"),r.onLog?.("Building and pushing Docker image\u2026","info");const u=await Se(i,t,a,s,r);if(!u.success){T.debug(I,"Docker buildAndPush (legacy) failed",{appName:t.appName,error:b(u.error.message)}),r.onStepComplete?.(A.DOCKER_OPERATIONS,C,"error");const D=new Error(b(u.error.message));return r.onError?.(D),h(D)}return r.onStepComplete?.(A.DOCKER_OPERATIONS,C,"completed"),r.onLog?.(`Docker image pushed: ${u.data.imageUri}`,"info"),P(j())}const E=await i.preflight?.();if(E!==void 0&&!E.success){r.onStepComplete?.(A.DOCKER_OPERATIONS,C,"error");const d=new Error(b(E.error.message));return r.onError?.(d),h(d)}const f=ve(p,t.path,e.workingDirectory);let k=be(s,a,t.appName);const w=await ue(n.awsProvider,U(x(t.appName)),e.abortSignal);w.success||(k=void 0,r.onLog?.(`Building without registry cache \u2014 ${w.error.message}`,"warn")),r.onLog?.(`Building and pushing ${p.length} service image(s) in ${f.length} group(s)\u2026`,"info");let l=!1;if(i.beginEcrSession!==void 0){const d=await i.beginEcrSession({accountId:s,region:a});if(!d.success){r.onStepComplete?.(A.DOCKER_OPERATIONS,C,"error");const u=new Error(b(d.error.message));return r.onError?.(u),h(u)}l=!0}const v=new AbortController,N=()=>v.abort();e.abortSignal!==void 0&&(e.abortSignal.aborted?v.abort():e.abortSignal.addEventListener("abort",N,{once:!0}));try{const d=()=>G(e.abortSignal)?"the deployment was cancelled":"a sibling group failed";let u;const D=ee(f.length),L=J(),$=f.map((c,m)=>async()=>{const o=c.members.map(V=>V.name).join(", ");if(v.signal.aborted)throw new Error(`Build of group ${m+1}/${f.length} (${o}) was not started \u2014 ${d()}`);r.onLog?.(`Building group ${m+1}/${f.length} (${o})\u2026`,"info");const S=await ye(c,i,t,a,s,y,k,r,n.awsProvider,v.signal,e.options?.environment,e.options?.flagBuildArgs,e.options?.flagBuildSecrets,L);if(!S.success)throw u===void 0&&(u={error:S.error,leader:c.members[0]?.name,members:o}),v.abort(),S.error;return S.data}),O=await Z($,D,{abortOnFirstFailure:!0,abortSignal:v.signal});if(u===void 0)for(let c=0;c<f.length;c++){const m=O[c];if(!(m!==void 0&&m.success)){u={error:m!==void 0&&!m.success?m.error:new Error(`Build of group ${c+1}/${f.length} never ran \u2014 ${d()}`),leader:f[c].members[0]?.name,members:f[c].members.map(o=>o.name).join(", ")};break}}if(u!==void 0){T.debug(I,"Docker buildAndPush failed",{appName:t.appName,leader:u.leader,members:u.members,error:b(u.error.message)}),r.onStepComplete?.(A.DOCKER_OPERATIONS,C,"error");const c=new Error(b(u.error.message));return r.onError?.(c),h(c)}const R={},B={};for(const c of O)if(!(c===void 0||!c.success)){for(const[m,o]of Object.entries(c.data.contentHashTagsByService))R[m]=o;for(const[m,o]of Object.entries(c.data.imageDigestsByService))B[m]=o}return T.debug(I,"Docker buildAndPush succeeded",{appName:t.appName,services:p.map(c=>c.name),groupCount:f.length,contentHashTags:R}),r.onStepComplete?.(A.DOCKER_OPERATIONS,C,"completed"),r.onLog?.(`Docker images pushed for ${p.length} service(s)`,"info"),P({contentHashTagsByService:R,imageDigestsByService:B,repoUri:y})}finally{e.abortSignal?.removeEventListener("abort",N),l&&i.endEcrSession!==void 0&&await i.endEcrSession().catch(d=>{T.debug(I,"endEcrSession cleanup failed",{error:b(d instanceof Error?d.message:String(d))})})}}export{C as DOCKER_BUILD_STEP_NAME,he as ecrRepoUri,j as emptyBuildProducts,_e as runDockerBuild};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { type ServiceArtefact } from "@fjall/util";
|
|
2
|
+
import type { DeployParams } from "../../types/params.js";
|
|
3
|
+
import type { ApplicationOperation } from "../../types/operations.js";
|
|
4
|
+
import type { DeployServices } from "../serviceFactory.js";
|
|
5
|
+
/**
|
|
6
|
+
* Lambda-parallel rollout logic for `deployCodeOnly`'s three modes, mirroring
|
|
7
|
+
* the ECS rollout helpers in `codeOnlyDeploy.ts`. Unlike ECS, Lambda needs no
|
|
8
|
+
* CloudFormation-export-based discovery step: `computeLambda.ts` sets
|
|
9
|
+
* `functionName: id` verbatim on the underlying `Function` construct, and
|
|
10
|
+
* `compute.ts`'s manifest branch sets `manifestLambda.name = id` — the SAME
|
|
11
|
+
* `id`. So the manifest's `ManifestLambdaDockerEntry.name` IS the live
|
|
12
|
+
* function's exact `FunctionName`; `parseLambdaDockerEntriesFromManifest` is
|
|
13
|
+
* a complete, sufficient "what to update, and by what name" source.
|
|
14
|
+
*/
|
|
15
|
+
export interface LambdaRolloutOutcome {
|
|
16
|
+
outputs: Record<string, string>;
|
|
17
|
+
artefacts: ServiceArtefact[];
|
|
18
|
+
failures: string[];
|
|
19
|
+
/**
|
|
20
|
+
* Distinct `{imageKey, tag}` pairs actually rolled, deduped by imageKey —
|
|
21
|
+
* feed straight into `reconcileImageTagParameters` (rollback modes only;
|
|
22
|
+
* the primary path never reconciles, matching ECS's existing asymmetry).
|
|
23
|
+
*/
|
|
24
|
+
rolledImageKeys: Array<{
|
|
25
|
+
serviceName: string;
|
|
26
|
+
imageTag: string;
|
|
27
|
+
}>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Primary path: roll every container Lambda whose `imageKey` was rebuilt
|
|
31
|
+
* this cycle. Builds a digest-pinned `ImageUri` directly from
|
|
32
|
+
* `imageDigestsByService` — no ECR `BatchGetImage` call needed, since
|
|
33
|
+
* `runDockerBuild` already resolved that digest for anything it just built.
|
|
34
|
+
*/
|
|
35
|
+
export declare function rolloutLambdaPrimary(services: DeployServices, operation: ApplicationOperation, contentHashTagsByService: Record<string, string>, imageDigestsByService: Record<string, string>, repoUri: string, callbacks: DeployParams["callbacks"], targetService: string | undefined, abortSignal?: AbortSignal): Promise<LambdaRolloutOutcome>;
|
|
36
|
+
/**
|
|
37
|
+
* `--image-tag <tag>` rollback: re-base the supplied tag onto each Lambda's
|
|
38
|
+
* `imageKey` family via `rebaseRollbackTagForService` — reused UNCHANGED,
|
|
39
|
+
* it's pure string manipulation with no ECS-specific logic. `allFamilyNames`
|
|
40
|
+
* MUST be the combined ECS-service-name ∪ Lambda-imageKey set so a supplied
|
|
41
|
+
* tag re-bases correctly across both entity types in one rollback; passing
|
|
42
|
+
* only ECS names here would let an ECS-family tag silently misapply to an
|
|
43
|
+
* unrelated Lambda.
|
|
44
|
+
*/
|
|
45
|
+
export declare function rolloutLambdaImageTag(services: DeployServices, operation: ApplicationOperation, explicitRollbackTag: string, allFamilyNames: readonly string[], callbacks: DeployParams["callbacks"], targetService: string | undefined, abortSignal?: AbortSignal): Promise<LambdaRolloutOutcome>;
|
|
46
|
+
/**
|
|
47
|
+
* `serviceImageTags` map rollback: exact-name match against manifest Lambda
|
|
48
|
+
* entries (simpler than ECS's ARN-suffix matching — Lambda function names
|
|
49
|
+
* are deterministic and exact, no live-ARN fuzziness to resolve). Preflights
|
|
50
|
+
* every matched entry's digest BEFORE touching any function, mirroring
|
|
51
|
+
* `preflightResolveServiceImageTagDigests`'s atomic-fail-closed behaviour: a
|
|
52
|
+
* single bad tag aborts the whole rollback before any `UpdateFunctionCode`
|
|
53
|
+
* call.
|
|
54
|
+
*/
|
|
55
|
+
export declare function rolloutLambdaServiceImageTags(services: DeployServices, operation: ApplicationOperation, serviceImageTags: Record<string, string>, callbacks: DeployParams["callbacks"], abortSignal?: AbortSignal): Promise<{
|
|
56
|
+
outcome: LambdaRolloutOutcome;
|
|
57
|
+
error?: Error;
|
|
58
|
+
}>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{join as I}from"path";import{artefactOutputKey as h,maskSensitiveOutput as $}from"@fjall/util";import{parseLambdaDockerEntriesFromManifest as w}from"@fjall/util/manifest";import{isAborted as b}from"../../aws/organisations/types.js";import{ecrRepoUri as k}from"./dockerBuildHelper.js";import{rebaseRollbackTagForService as L,deriveEcrRepositoryArn as P}from"./codeOnlyDeploy.js";import{matchesServiceName as K}from"./serviceImageTagsRollback.js";function R(){return{outputs:{},artefacts:[],failures:[],rolledImageKeys:[]}}function O(e,t,n,c,i,a,d){const r=P(i),u=`${i}:${c}`;e.outputs[h(t,"ImageTag")]=c,e.outputs[h(t,"ImageUri")]=u,e.outputs[h(t,"ImageDigest")]=a,r!==""&&(e.outputs[h(t,"EcrRepositoryArn")]=r),e.artefacts.push({serviceName:t,imageTag:c,imageUri:u,imageDigest:a,...r!==""?{ecrRepositoryArn:r}:{},functionArn:d.functionArn,publishedVersion:d.version}),e.rolledImageKeys.some(m=>m.serviceName===n)||e.rolledImageKeys.push({serviceName:n,imageTag:c})}async function v(e,t,n,c,i,a,d){d.onLog?.(`Updating Lambda function ${n.name} to ${a}`,"info");const r=await e.lambdaService.updateFunctionCode(n.name,`${c}@${i}`);if(!r.success){t.failures.push(`${n.name}: ${$(r.error.message)}`);return}O(t,n.name,n.imageKey,a,c,i,r.data)}function A(e,t){return t?e.filter(n=>K(n.name,t)):e}async function x(e,t,n,c,i,a,d,r){const u=R(),m=I(t.path,"cdk.out"),p=A(w(m),d);for(const f of p){if(b(r)){u.failures.push(`${f.name}: rollout cancelled before this function was reached`);continue}const l=n[f.imageKey],o=c[f.imageKey];l===void 0||o===void 0||await v(e,u,f,i,o,l,a)}return u}async function M(e,t,n,c,i,a,d){const r=R(),u=I(t.path,"cdk.out"),m=A(w(u),a);if(m.length===0)return r;const p=e.awsProvider.getAccountId(),f=e.awsProvider.getRegion(),l=k(p,f,t.appName);for(const o of m){if(b(d)){r.failures.push(`${o.name}: rollout cancelled before this function was reached`);continue}const g=L(n,o.imageKey,c),s=await e.ecrImageInspector.getImageDigest(l,g,d);if(!s.success||s.data===void 0){const y=s.success?"not an ECR repository":$(s.error.message);r.failures.push(`${o.name}: image tag '${g}' (rebased from '${n}') could not be resolved to a digest: ${y}`);continue}await v(e,r,o,l,s.data,g,i)}return r}async function j(e,t,n,c,i){const a=R(),d=I(t.path,"cdk.out"),u=w(d).map(o=>({entry:o,tag:n[o.name]})).filter(o=>o.tag!==void 0);if(u.length===0)return{outcome:a};const m=e.awsProvider.getAccountId(),p=e.awsProvider.getRegion(),f=k(m,p,t.appName),l=new Map;for(const{entry:o,tag:g}of u){const s=await e.ecrImageInspector.getImageDigest(f,g,i);if(!s.success||s.data===void 0){const y=s.success?"not an ECR repository":$(s.error.message);return{outcome:a,error:new Error(`Lambda function '${o.name}': image tag '${g}' could not be resolved to a digest: ${y}. No function was rolled.`)}}l.set(o.name,s.data)}for(const{entry:o,tag:g}of u){if(b(i)){a.failures.push(`${o.name}: rollout cancelled before this function was reached`);continue}const s=l.get(o.name);s!==void 0&&await v(e,a,o,f,s,g,c)}return{outcome:a}}export{M as rolloutLambdaImageTag,x as rolloutLambdaPrimary,j as rolloutLambdaServiceImageTags};
|
|
@@ -3,6 +3,7 @@ import { CdkService } from "../services/infrastructure/CdkService.js";
|
|
|
3
3
|
import { CloudFormationService } from "../services/infrastructure/CloudFormationService.js";
|
|
4
4
|
import { EcsService } from "../services/infrastructure/EcsService.js";
|
|
5
5
|
import { EcsServiceResolver } from "../services/infrastructure/EcsServiceResolver.js";
|
|
6
|
+
import { LambdaService } from "../services/infrastructure/LambdaService.js";
|
|
6
7
|
import { EcrImageInspectorService } from "../services/infrastructure/EcrImageInspectorService.js";
|
|
7
8
|
import { ApplicationStackService } from "../services/application/ApplicationStackService.js";
|
|
8
9
|
import { TemplateHashService } from "../services/supporting/TemplateHashService.js";
|
|
@@ -14,6 +15,7 @@ export interface DeployServices {
|
|
|
14
15
|
cfnService: CloudFormationService;
|
|
15
16
|
ecsService: EcsService;
|
|
16
17
|
ecsResolver: EcsServiceResolver;
|
|
18
|
+
lambdaService: LambdaService;
|
|
17
19
|
ecrImageInspector: EcrImageInspectorService;
|
|
18
20
|
stackService: ApplicationStackService;
|
|
19
21
|
hashService: TemplateHashService;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{SimpleAwsProvider as
|
|
1
|
+
import{SimpleAwsProvider as v}from"../aws/SimpleAwsProvider.js";import{CdkService as f}from"../services/infrastructure/CdkService.js";import{CdkArgumentBuilder as p}from"../services/infrastructure/CdkArgumentBuilder.js";import{CdkProcessManager as w}from"../services/infrastructure/CdkProcessManager.js";import{CloudFormationService as g}from"../services/infrastructure/CloudFormationService.js";import{EcsService as u}from"../services/infrastructure/EcsService.js";import{EcsServiceResolver as b}from"../services/infrastructure/EcsServiceResolver.js";import{LambdaService as k}from"../services/infrastructure/LambdaService.js";import{EcrImageInspectorService as y}from"../services/infrastructure/EcrImageInspectorService.js";import{ApplicationStackService as A}from"../services/application/ApplicationStackService.js";import{TemplateHashService as C}from"../services/supporting/TemplateHashService.js";import{FrameworkRegistry as F}from"./builders/frameworkRegistry.js";function j(e){const r=new v(e.awsCredentials,e.mintScopedBuildSecretSession);r.exportToEnv();const c=new p,i=new w(c,e.abortSignal),n=new f({processManager:i,...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{},...e.callbacks.onFailureAnalysis!==void 0?{onFailureAnalysis:e.callbacks.onFailureAnalysis}:{}}),o=new g(r),t=new u(r),s=new b(o),a=new k(r),l=new y(r),m=new A(n,o,r),S=new C,d=F.createDefault();return{awsProvider:r,cdkService:n,cfnService:o,ecsService:t,ecsResolver:s,lambdaService:a,ecrImageInspector:l,stackService:m,hashService:S,frameworkRegistry:d,...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{},dispose(){i.dispose()}}}export{j as createDeployServices};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { AwsProvider } from "../../aws/AwsProvider.js";
|
|
2
|
+
import { type Result } from "@fjall/generator";
|
|
3
|
+
import { BaseServiceError } from "../../types/errors/ServiceError.js";
|
|
4
|
+
export declare class LambdaError extends BaseServiceError {
|
|
5
|
+
readonly functionName: string;
|
|
6
|
+
readonly operation: "publish_version" | "update_function_code";
|
|
7
|
+
constructor(message: string, functionName: string, operation: "publish_version" | "update_function_code", details?: unknown);
|
|
8
|
+
}
|
|
9
|
+
export interface PublishedLambdaVersion {
|
|
10
|
+
functionArn: string;
|
|
11
|
+
version: string;
|
|
12
|
+
}
|
|
13
|
+
export interface UpdatedLambdaFunctionCode {
|
|
14
|
+
functionArn: string;
|
|
15
|
+
version: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Lambda service using the AwsProvider interface. Constructor-injected
|
|
19
|
+
* provider matches the `EcsService`/`CloudFormationService` pattern.
|
|
20
|
+
*/
|
|
21
|
+
export declare class LambdaService {
|
|
22
|
+
private aws;
|
|
23
|
+
constructor(aws: AwsProvider);
|
|
24
|
+
private getClient;
|
|
25
|
+
/**
|
|
26
|
+
* Publish a new immutable version of a Lambda function pointing at
|
|
27
|
+
* whatever is currently deployed as `$LATEST`. Used right after a
|
|
28
|
+
* container-image Lambda's `cdk deploy` succeeds — CDK's `.currentVersion`
|
|
29
|
+
* can't be trusted to do this itself when the image reference is a
|
|
30
|
+
* `CfnParameter` (its logical-id hash is computed from synth-time template
|
|
31
|
+
* shape, not the parameter's deploy-time resolved value, so the hash
|
|
32
|
+
* doesn't change between deploys even though the underlying image does).
|
|
33
|
+
*/
|
|
34
|
+
publishVersion(functionName: string): Promise<Result<PublishedLambdaVersion, LambdaError>>;
|
|
35
|
+
/**
|
|
36
|
+
* Update a container-image Lambda function's code directly — the Lambda
|
|
37
|
+
* equivalent of ECS's `UpdateService`/`RegisterTaskDefinition` for a
|
|
38
|
+
* code-only deploy, bypassing CloudFormation entirely. Publishes a new
|
|
39
|
+
* version in the same call (`Publish: true`) since we own this API call
|
|
40
|
+
* directly here — unlike the full-deploy path, where CloudFormation
|
|
41
|
+
* performs the code update and `publishVersion` has to run as a separate
|
|
42
|
+
* step afterward.
|
|
43
|
+
*/
|
|
44
|
+
updateFunctionCode(functionName: string, imageUri: string): Promise<Result<UpdatedLambdaFunctionCode, LambdaError>>;
|
|
45
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LambdaClient as h,PublishVersionCommand as $,UpdateFunctionCodeCommand as f}from"@aws-sdk/client-lambda";import{success as l,failure as a}from"@fjall/generator";import{BaseServiceError as g}from"../../types/errors/ServiceError.js";import{logger as d}from"@fjall/util/logger";import{maskSensitiveOutput as p}from"@fjall/util";import{getAwsErrorDetails as m}from"../../aws/utils/awsErrorHandler.js";class c extends g{functionName;operation;constructor(e,t,r,o){super(`LAMBDA_${r.toUpperCase()}_FAILED`,e,o,!1),this.functionName=t,this.operation=r}}class V{aws;constructor(e){this.aws=e}getClient(){return this.aws.getClient(h)}async publishVersion(e){try{const r=await this.getClient().send(new $({FunctionName:e}));return!r.FunctionArn||!r.Version?a(new c("PublishVersion returned no function ARN or version",e,"publish_version",r)):(d.debug("LambdaService",`Published version ${r.Version} for ${e}`),l({functionArn:r.FunctionArn,version:r.Version}))}catch(t){const{message:r,name:o,httpCode:n}=m(t),s=p(r);d.error("LambdaService",`PublishVersion failed for ${e}: ${o} - ${s}`,{functionName:e,httpCode:n,error:t});let i=`Failed to publish Lambda version for '${e}': ${s}`;return n&&(i+=` (HTTP ${n})`),a(new c(i,e,"publish_version",{error:t,httpCode:n,errorName:o}))}}async updateFunctionCode(e,t){try{const o=await this.getClient().send(new f({FunctionName:e,ImageUri:t,Publish:!0}));return!o.FunctionArn||!o.Version?a(new c("UpdateFunctionCode returned no function ARN or version",e,"update_function_code",o)):(d.debug("LambdaService",`Updated ${e} to ${t}, published version ${o.Version}`),l({functionArn:o.FunctionArn,version:o.Version}))}catch(r){const{message:o,name:n,httpCode:s}=m(r),i=p(o);d.error("LambdaService",`UpdateFunctionCode failed for ${e}: ${n} - ${i}`,{functionName:e,imageUri:t,httpCode:s,error:r});let u=`Failed to update Lambda function code for '${e}': ${i}`;return s&&(u+=` (HTTP ${s})`),a(new c(u,e,"update_function_code",{error:r,httpCode:s,errorName:n}))}}}export{c as LambdaError,V as LambdaService};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fjall/deploy-core",
|
|
3
|
-
"version": "2.30.
|
|
3
|
+
"version": "2.30.3",
|
|
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",
|
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
"@aws-sdk/client-iam": "^3.1038.0",
|
|
76
76
|
"@aws-sdk/client-identitystore": "^3.1079.0",
|
|
77
77
|
"@aws-sdk/client-kms": "^3.1065.0",
|
|
78
|
+
"@aws-sdk/client-lambda": "^3.1038.0",
|
|
78
79
|
"@aws-sdk/client-organizations": "^3.1038.0",
|
|
79
80
|
"@aws-sdk/client-ram": "^3.1038.0",
|
|
80
81
|
"@aws-sdk/client-rds": "^3.1038.0",
|
|
@@ -84,8 +85,8 @@
|
|
|
84
85
|
"@aws-sdk/client-ssm": "^3.1038.0",
|
|
85
86
|
"@aws-sdk/client-sso-admin": "^3.1038.0",
|
|
86
87
|
"@aws-sdk/client-sts": "^3.1038.0",
|
|
87
|
-
"@fjall/generator": "^2.30.
|
|
88
|
-
"@fjall/util": "^2.30.
|
|
88
|
+
"@fjall/generator": "^2.30.3",
|
|
89
|
+
"@fjall/util": "^2.30.3",
|
|
89
90
|
"@smithy/node-http-handler": "^4.6.1",
|
|
90
91
|
"aws-cdk": "^2.1128.1",
|
|
91
92
|
"tsx": "^4.21.0",
|
|
@@ -96,5 +97,5 @@
|
|
|
96
97
|
"typescript": "^6.0.3",
|
|
97
98
|
"vitest": "^4.1.5"
|
|
98
99
|
},
|
|
99
|
-
"gitHead": "
|
|
100
|
+
"gitHead": "d398edc0c0edf661d7ab6e5a47623529f68dfd2a"
|
|
100
101
|
}
|