@fjall/deploy-core 2.30.0 → 2.31.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.minified +1 -1
- package/dist/src/aws/organisations/oidcProvider.js +1 -1
- package/dist/src/aws/utils/cloudformationEventHelpers.d.ts +2 -2
- package/dist/src/aws/utils/cloudformationEventHelpers.js +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.js +1 -1
- package/dist/src/orchestration/activeDeploymentGuard.d.ts +1 -1
- package/dist/src/orchestration/activeDeploymentGuard.js +3 -3
- package/dist/src/orchestration/application/applicationDeploy.js +1 -1
- package/dist/src/orchestration/application/applicationDeployHelpers.d.ts +19 -0
- package/dist/src/orchestration/application/applicationDeployHelpers.js +3 -3
- package/dist/src/orchestration/application/applicationDestroy.js +1 -1
- package/dist/src/orchestration/application/artefactCollection.d.ts +9 -1
- package/dist/src/orchestration/application/artefactCollection.js +1 -1
- package/dist/src/orchestration/application/codeOnlyDeploy.d.ts +10 -1
- package/dist/src/orchestration/application/codeOnlyDeploy.js +1 -1
- package/dist/src/orchestration/application/databaseEndpointReconcile.d.ts +2 -2
- package/dist/src/orchestration/application/databaseEndpointReconcile.js +1 -1
- package/dist/src/orchestration/application/destroyPreFlight.d.ts +33 -0
- package/dist/src/orchestration/application/destroyPreFlight.js +1 -0
- package/dist/src/orchestration/application/detectionPipeline.d.ts +1 -1
- package/dist/src/orchestration/application/detectionPipeline.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 +90 -0
- package/dist/src/orchestration/application/lambdaCodeOnlyRollout.js +1 -0
- package/dist/src/orchestration/application/plan/computeDeployPlan.d.ts +1 -1
- package/dist/src/orchestration/application/plan/computeDeployPlan.js +1 -1
- package/dist/src/orchestration/application/serviceImageTagsRollback.d.ts +1 -1
- package/dist/src/orchestration/application/serviceImageTagsRollback.js +1 -1
- package/dist/src/orchestration/application/taskDefinitionRoll.d.ts +1 -1
- package/dist/src/orchestration/application/taskDefinitionRoll.js +1 -1
- package/dist/src/orchestration/destroy.js +1 -1
- package/dist/src/orchestration/drift/driftPreFlight.js +3 -3
- package/dist/src/orchestration/index.d.ts +1 -1
- package/dist/src/orchestration/index.js +1 -1
- package/dist/src/orchestration/organisation/cascadeHelpers.js +1 -1
- package/dist/src/orchestration/organisationDeploy/infraSteps.d.ts +1 -1
- package/dist/src/orchestration/organisationDeploy/infraSteps.js +1 -1
- package/dist/src/orchestration/organisationDeploy/orgCascadeDeploy.js +4 -4
- package/dist/src/orchestration/organisationDeploy/singleComponentDeploy.js +1 -1
- package/dist/src/orchestration/probes/deleteBlockProbes.d.ts +38 -0
- package/dist/src/orchestration/probes/deleteBlockProbes.js +1 -0
- package/dist/src/orchestration/remediation/recreatePreFlight.js +1 -1
- package/dist/src/orchestration/remediation/recreateResource.js +1 -1
- package/dist/src/orchestration/remediation/removalUpdate.d.ts +74 -13
- package/dist/src/orchestration/remediation/removalUpdate.js +1 -1
- package/dist/src/orchestration/restart/restartApplication.js +1 -1
- package/dist/src/orchestration/restart/secretsDrift.js +1 -1
- package/dist/src/orchestration/serviceFactory.d.ts +2 -0
- package/dist/src/orchestration/serviceFactory.js +1 -1
- package/dist/src/orchestration/stackCleanup/stackResources.d.ts +5 -3
- package/dist/src/services/application/ApplicationStackService.d.ts +2 -1
- package/dist/src/services/application/ApplicationStackService.js +1 -1
- package/dist/src/services/infrastructure/CloudFormationService.d.ts +11 -10
- package/dist/src/services/infrastructure/CloudFormationService.js +1 -1
- package/dist/src/services/infrastructure/EcsService.d.ts +11 -9
- package/dist/src/services/infrastructure/EcsService.js +1 -1
- package/dist/src/services/infrastructure/EcsServiceResolver.d.ts +4 -4
- package/dist/src/services/infrastructure/EcsServiceResolver.js +1 -1
- package/dist/src/services/infrastructure/LambdaService.d.ts +62 -0
- package/dist/src/services/infrastructure/LambdaService.js +1 -0
- package/dist/src/types/application/ApplicationServiceTypes.d.ts +1 -0
- package/dist/src/types/application/ApplicationServiceTypes.js +1 -1
- package/dist/src/types/remediation.d.ts +1 -0
- package/dist/src/types/remediation.js +1 -1
- package/package.json +6 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
import{CloudFormationClient as
|
|
1
|
+
import{CloudFormationClient as m,DeleteStackCommand as A,DescribeStacksCommand as S,GetTemplateCommand as $,ListExportsCommand as O,UpdateStackCommand as P}from"@aws-sdk/client-cloudformation";import{stackStatusMap as C}from"../../aws/utils/stackStatus.js";import{maskSensitiveOutput as E}from"@fjall/util";import{success as d,failure as u}from"@fjall/generator";import{BaseServiceError as M}from"../../types/errors/ServiceError.js";import{logger as F}from"@fjall/util/logger";import{getErrorMessage as g,sleep as h}from"@fjall/util";import{STACK_NOT_FOUND_PATTERN as y}from"@fjall/util/aws";import{isCleanableState as R}from"../../types/constants.js";import{composeSdkAbortSignal as p,extractErrorName as D,isAborted as x}from"../../aws/organisations/types.js";class f extends M{errorType;stackName;stackStatus;constructor(e,s,r,t,a,n=!1){super(`CFN_${s.toUpperCase()}`,e,a,n),this.errorType=s,this.stackName=r,this.stackStatus=t}}class Y{aws;constructor(e){this.aws=e}classifyAwsError(e,s,r){const t=D(e),a=E(g(e));return t==="CredentialsError"||t==="UnauthorizedError"?new f(`AWS credentials error: ${a}`,"auth_error",r,void 0,e,!1):t==="Throttling"||t==="TooManyRequestsException"?new f(`AWS rate limit exceeded: ${a}`,"throttled",r,void 0,e,!0):t==="TimeoutError"?new f(`AWS request timed out: ${a}`,"timeout",r,void 0,e,!0):t==="NetworkingError"||t==="ENOTFOUND"?new f(`Network error: ${a}`,"network_error",r,void 0,e,!0):new f(`${s}: ${a}`,"unknown",r,void 0,e)}async getStackOutputs(e,s,r){s?.onStackCheck?.(e);const t=this.aws.getClient(m),a=new S({StackName:e});try{const o=(await t.send(a,{abortSignal:p(r)})).Stacks?.[0];if(!o?.Outputs)return d([]);const c=o.Outputs.map(l=>({OutputKey:l.OutputKey,OutputValue:l.OutputValue,ExportName:l.ExportName}));return s?.onOutputsRetrieved?.(e,c.length),d(c)}catch(n){if(n instanceof Error&&n.name==="ValidationError"&&n.message?.includes(y))return s?.onStackNotFound?.(e),d([]);const o=E(g(n));return u(new f(`Failed to get outputs for stack ${e}: ${o}`,"unknown",e,void 0,n))}}async getStackStatus(e,s,r){s?.onStackCheck?.(e);const t=this.aws.getClient(m),a=new S({StackName:e});try{const o=(await t.send(a,{abortSignal:p(r)})).Stacks?.[0];if(!o)return d({status:"DOES_NOT_EXIST",safeToRedeploy:"Yes",description:"Stack does not exist yet"});const c=o.StackStatus||"UNKNOWN",l=C[c]||C.UNKNOWN;return s?.onStackFound?.(e,c),d({status:c,safeToRedeploy:l.safeToRedeploy,description:l.description,statusReason:o.StackStatusReason})}catch(n){return n instanceof Error&&n.name==="ValidationError"&&n.message?.includes(y)?d({status:"DOES_NOT_EXIST",safeToRedeploy:"Yes",description:"Stack does not exist yet"}):u(this.classifyAwsError(n,`Failed to get stack status for ${e}`,e))}}async listAllExports(e,s){const r=this.aws.getClient(m),t=[];try{let a;do{if(x(s))break;const n=new O({NextToken:a}),o=await r.send(n,{abortSignal:p(s)}),c=o.Exports||[];for(const l of c)l.Name&&l.Value&&t.push({Name:l.Name,Value:l.Value});if(e?.(c))break;a=o.NextToken}while(a);return d(t)}catch(a){const n=E(g(a));return u(new f(`Failed to list exports: ${n}`,"unknown",void 0,void 0,a,!1))}}async getExportsByNames(e,s){if(e.length===0)return d(new Map);const r=new Set(e),t=new Map,a=await this.listAllExports(n=>{for(const o of n)o.Name&&r.has(o.Name)&&o.Value&&t.set(o.Name,o.Value);return t.size>=r.size},s);return a.success?d(t):u(a.error)}async listExports(e,s){const r=await this.listAllExports(void 0,s);return r.success&&e?.onExportsRetrieved?.(r.data.length),r}async deleteStack(e,s){const r=this.aws.getClient(m);try{return await r.send(new A({StackName:e}),{abortSignal:p(s)}),d(void 0)}catch(t){return u(this.classifyAwsError(t,`Failed to delete stack ${e}`,e))}}async stackExists(e,s,r){const t=s??this.aws.getClient(m);try{const n=(await t.send(new S({StackName:e}),{abortSignal:p(r)})).Stacks?.[0]?.StackStatus;return!!n&&n!=="REVIEW_IN_PROGRESS"&&!R(n)}catch(a){return a instanceof Error&&a.message?.includes(y)?!1:(F.debug("CloudFormationService","Error checking stack existence, assuming exists",{stackName:e,error:g(a)}),!0)}}async getTemplate(e,s){const r=this.aws.getClient(m);try{const t=await r.send(new $({StackName:e,TemplateStage:"Original"}),{abortSignal:p(s)});return d(t.TemplateBody??"")}catch(t){return t instanceof Error&&t.message?.includes(y)?u(new f(`Stack ${e} not found`,"stack_not_found",e)):u(this.classifyAwsError(t,`Failed to get template for stack ${e}`,e))}}async waitForDeleteComplete(e,s){const r=s?.timeoutMs??6e5,t=s?.pollIntervalMs??5e3,a=Date.now();for(;Date.now()-a<r;){const n=await this.getStackStatus(e,void 0,s?.abortSignal);if(!n.success){if(n.error.recoverable){s?.onProgress?.(`Transient error polling stack ${e}, retrying: ${E(n.error.message)}`),await h(t);continue}return u(n.error)}const o=n.data?.status;if(o==="DELETE_COMPLETE"||o==="DOES_NOT_EXIST")return d(void 0);if(o==="DELETE_FAILED")return u(new f(`Stack ${e} deletion failed: ${n.data?.statusReason||"unknown reason"}`,"stack_failed",e,o,void 0,!1));s?.onProgress?.(`Stack ${e} status: ${o??"unknown"}`),await h(t)}return u(new f(`Timed out waiting for stack ${e} deletion after ${Math.round(r/1e3)}s`,"timeout",e,void 0,void 0,!0))}async updateStackParameters(e,s,r){const t=this.aws.getClient(m);let a;try{a=await t.send(new S({StackName:e}),{abortSignal:p(r)})}catch(i){return u(this.classifyAwsError(i,`Failed to describe stack ${e} before parameter reconcile`,e))}const n=a.Stacks?.[0];if(!n)return u(new f(`Stack ${e} not found`,"stack_not_found",e));const o=n.Parameters??[],c=new Set(o.map(i=>i.ParameterKey).filter(i=>i!==void 0)),l=Object.entries(s),k=l.filter(([i])=>!c.has(i)).map(([i])=>i),T=new Map(l.filter(([i])=>c.has(i)));if(T.size===0)return d({updated:!1,skippedKeys:k});const _=o.filter(i=>i.ParameterKey!==void 0).map(i=>T.has(i.ParameterKey)?{ParameterKey:i.ParameterKey,ParameterValue:T.get(i.ParameterKey)}:{ParameterKey:i.ParameterKey,UsePreviousValue:!0});try{await t.send(new P({StackName:e,UsePreviousTemplate:!0,Parameters:_,Capabilities:["CAPABILITY_IAM","CAPABILITY_NAMED_IAM"]}),{abortSignal:p(r)})}catch(i){return i instanceof Error&&i.name==="ValidationError"&&i.message?.includes("No updates are to be performed")?d({updated:!1,skippedKeys:k}):u(this.classifyAwsError(i,`Failed to update parameters for stack ${e}`,e))}return this.waitForUpdateStackParametersComplete(e,k,r)}async waitForUpdateStackParametersComplete(e,s,r){const n=Date.now();for(;Date.now()-n<6e5;){if(x(r))return u(new f(`Aborted waiting for stack ${e} parameter update`,"timeout",e,void 0,void 0,!0));const o=await this.getStackStatus(e,void 0,r);if(!o.success){if(o.error.recoverable){await h(5e3);continue}return u(o.error)}const c=o.data?.status??"UNKNOWN";if(c==="UPDATE_COMPLETE")return d({updated:!0,skippedKeys:s});if(I(c))return u(new f(`Stack ${e} parameter update failed: ${o.data?.statusReason||"unknown reason"}`,"stack_failed",e,c,void 0,!1));await h(5e3)}return u(new f(`Timed out waiting for stack ${e} parameter update after ${Math.round(6e5/1e3)}s`,"timeout",e,void 0,void 0,!0))}}function I(w){return w.endsWith("_IN_PROGRESS")?!1:w.startsWith("UPDATE_ROLLBACK_")||w==="UPDATE_FAILED"}export{f as CloudFormationError,Y as CloudFormationService};
|
|
@@ -56,6 +56,7 @@ export interface ECSDeploymentOptions {
|
|
|
56
56
|
maxWaitTime?: number;
|
|
57
57
|
pollInterval?: number;
|
|
58
58
|
progressCallback?: (status: DeploymentStatus) => void;
|
|
59
|
+
abortSignal?: AbortSignal;
|
|
59
60
|
}
|
|
60
61
|
/**
|
|
61
62
|
* ECS service using the AwsProvider interface.
|
|
@@ -73,24 +74,25 @@ export declare class EcsService {
|
|
|
73
74
|
desiredCount?: number;
|
|
74
75
|
taskDefinition?: string;
|
|
75
76
|
forceNewDeployment?: boolean;
|
|
76
|
-
}): Promise<Result<void, EcsError>>;
|
|
77
|
-
describeServices(clusterArn: string, serviceArns: string[]): Promise<Result<ECSServiceInfo[], EcsError>>;
|
|
78
|
-
getDeploymentStatus(clusterArn: string, serviceArn: string): Promise<Result<DeploymentStatus, EcsError>>;
|
|
77
|
+
}, abortSignal?: AbortSignal): Promise<Result<void, EcsError>>;
|
|
78
|
+
describeServices(clusterArn: string, serviceArns: string[], abortSignal?: AbortSignal): Promise<Result<ECSServiceInfo[], EcsError>>;
|
|
79
|
+
getDeploymentStatus(clusterArn: string, serviceArn: string, abortSignal?: AbortSignal): Promise<Result<DeploymentStatus, EcsError>>;
|
|
79
80
|
isDeploying(clusterArn: string, serviceArn: string): Promise<Result<boolean, EcsError>>;
|
|
80
|
-
getServiceEvents(clusterArn: string, serviceArn: string): Promise<Result<Array<{
|
|
81
|
+
getServiceEvents(clusterArn: string, serviceArn: string, abortSignal?: AbortSignal): Promise<Result<Array<{
|
|
81
82
|
message: string;
|
|
82
83
|
timestamp: Date;
|
|
83
84
|
}>, EcsError>>;
|
|
84
85
|
pollDeployment(options: ECSDeploymentOptions): Promise<Result<void, EcsError>>;
|
|
85
86
|
private processDeploymentEvents;
|
|
86
|
-
listClusters(): Promise<Result<string[], EcsError>>;
|
|
87
|
-
describeClusters(clusterArns: string[]): Promise<Result<ECSClusterInfo[], EcsError>>;
|
|
88
|
-
listServices(clusterArn: string): Promise<Result<string[], EcsError>>;
|
|
89
|
-
registerTaskDefinition(taskDefinition: RegisterTaskDefinitionCommandInput): Promise<Result<string, EcsError>>;
|
|
90
|
-
getLatestTaskDefinition(family: string): Promise<Result<TaskDefinition | undefined, EcsError>>;
|
|
87
|
+
listClusters(abortSignal?: AbortSignal): Promise<Result<string[], EcsError>>;
|
|
88
|
+
describeClusters(clusterArns: string[], abortSignal?: AbortSignal): Promise<Result<ECSClusterInfo[], EcsError>>;
|
|
89
|
+
listServices(clusterArn: string, abortSignal?: AbortSignal): Promise<Result<string[], EcsError>>;
|
|
90
|
+
registerTaskDefinition(taskDefinition: RegisterTaskDefinitionCommandInput, abortSignal?: AbortSignal): Promise<Result<string, EcsError>>;
|
|
91
|
+
getLatestTaskDefinition(family: string, abortSignal?: AbortSignal): Promise<Result<TaskDefinition | undefined, EcsError>>;
|
|
91
92
|
deployService(clusterArn: string, serviceArn: string, options?: {
|
|
92
93
|
taskDefinition?: string;
|
|
93
94
|
waitForCompletion?: boolean;
|
|
94
95
|
progressCallback?: (status: DeploymentStatus) => void;
|
|
96
|
+
abortSignal?: AbortSignal;
|
|
95
97
|
}): Promise<Result<void, EcsError>>;
|
|
96
98
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{ECSClient as
|
|
1
|
+
import{ECSClient as $,UpdateServiceCommand as T,DescribeServicesCommand as b,DescribeClustersCommand as F,ListServicesCommand as R,ListClustersCommand as M,RegisterTaskDefinitionCommand as N,DescribeTaskDefinitionCommand as _}from"@aws-sdk/client-ecs";import{success as c,failure as l}from"@fjall/generator";import{BaseServiceError as I}from"../../types/errors/ServiceError.js";import{logger as D}from"@fjall/util/logger";import{getErrorMessage as w,maskSensitiveOutput as p,sleep as U}from"@fjall/util";import{getAwsErrorDetails as x}from"../../aws/utils/awsErrorHandler.js";import{composeSdkAbortSignal as g,isAborted as A}from"../../aws/organisations/types.js";class m extends I{errorType;resourceArn;constructor(n,s,e,t,r=!1){super(`ECS_${s.toUpperCase()}`,n,t,r),this.errorType=s,this.resourceArn=e}}class W{aws;constructor(n){this.aws=n}getClient(){return this.aws.getClient($)}async updateService(n,s,e,t){try{const r=this.getClient(),a=new T({cluster:n,service:s,forceNewDeployment:e?.forceNewDeployment!==!1,desiredCount:e?.desiredCount,taskDefinition:e?.taskDefinition});return D.debug("EcsService",`UpdateService request: cluster=${n}, service=${s}, forceNewDeployment=${e?.forceNewDeployment!==!1}, taskDefinition=${e?.taskDefinition??"<unchanged>"}`),await r.send(a,{abortSignal:g(t)}),c(void 0)}catch(r){const{message:a,name:o,httpCode:i}=x(r),d=p(a);D.error("EcsService",`UpdateService failed: ${o} - ${d}`,{clusterArn:n,serviceArn:s,httpCode:i,error:r});let f=`Failed to update ECS service: ${d}`;return i&&(f+=` (HTTP ${i})`),o!=="Error"&&o!=="Unknown"&&(f+=` [${o}]`),l(new m(f,"unknown",s,{clusterArn:n,error:r,httpCode:i,errorName:o},!1))}}async describeServices(n,s,e){try{const t=this.getClient(),r=new b({cluster:n,services:s,include:["TAGS"]}),a=await t.send(r,{abortSignal:g(e)});if(!a.services)return c([]);const o=a.services.map(i=>({serviceArn:i.serviceArn,serviceName:i.serviceName,clusterArn:i.clusterArn,status:i.status,desiredCount:i.desiredCount,runningCount:i.runningCount,pendingCount:i.pendingCount,deployments:i.deployments,events:i.events,taskDefinition:i.taskDefinition}));return c(o)}catch(t){const r=p(w(t));return l(new m(`Failed to describe services: ${r}`,"unknown",void 0,{clusterArn:n,serviceArns:s,error:t},!1))}}async getDeploymentStatus(n,s,e){const t=await this.describeServices(n,[s],e);if(!t.success)return l(t.error);const r=t.data;if(r.length===0)return l(new m(`Service ${s} not found in cluster ${n}`,"service_not_found",s,{clusterArn:n}));const a=r[0],o=a.deployments||[],i=o.find(C=>C.status==="PRIMARY");if(!i)return c({deploymentCompleted:!1,deploymentFailed:!0,runningCount:0,desiredCount:0,pendingCount:0,deploymentCount:o.length,message:"No primary deployment found",percentComplete:0});const d=i.desiredCount||0,f=i.runningCount||0,y=i.pendingCount||0,v=d>0?Math.round(f/d*100):0,h=o.length===1&&f===d&&y===0,k=(i.failedTasks??0)>0||i.rolloutState==="FAILED";let u=`Running: ${f}/${d}`;if(u+=`, Pending: ${y}`,o.length>1&&(u+=` (${o.length} deployments active)`),k){const C=i.rolloutStateReason||(i.failedTasks?`${i.failedTasks} task(s) failed to start`:void 0);C&&(u+=` \u2014 ${C}`)}const S=a.events&&a.events.length>0?a.events[0]?.message:void 0;return c({deploymentCompleted:h,deploymentFailed:k,runningCount:f,desiredCount:d,pendingCount:y,deploymentCount:o.length,message:u,percentComplete:v,latestEvent:S})}async isDeploying(n,s){const e=await this.getDeploymentStatus(n,s);if(!e.success)return D.debug("EcsService","Could not determine deployment status, assuming not deploying",{error:e.error.message}),c(!1);const t=e.data;return c(!t.deploymentCompleted&&!t.deploymentFailed)}async getServiceEvents(n,s,e){try{const t=this.getClient(),r=new b({cluster:n,services:[s],include:["TAGS"]}),a=await t.send(r,{abortSignal:g(e)});if(!a.services||a.services.length===0)return c([]);const i=(a.services[0].events||[]).map(d=>({message:d.message||"",timestamp:d.createdAt||new Date}));return c(i)}catch(t){const r=p(w(t));return l(new m(`Failed to get service events: ${r}`,"unknown",s,{clusterArn:n,error:t}))}}async pollDeployment(n){const{clusterArn:s,serviceArn:e,waitForCompletion:t=!0,maxWaitTime:r=6e5,pollInterval:a=5e3,progressCallback:o,abortSignal:i}=n;if(!t)return c(void 0);const d=Date.now(),f=new Date(d-5e3);let y=null,v="";const h=new Set;for(;Date.now()-d<r;){if(A(i))return l(new m("Deployment polling cancelled by abort signal","unknown",e,{clusterArn:s}));const k=await this.getDeploymentStatus(s,e,i);if(!k.success)return l(k.error);const u=k.data,S=await this.getServiceEvents(s,e,i);if(S.success){const C=this.processDeploymentEvents(S.data,u,f,y,h,o);y=C.lastEventTimestamp,C.eventsReported?v=u.message:u.message!==v&&(o?.(u),v=u.message)}if(u.deploymentCompleted)return c(void 0);if(u.deploymentFailed)return l(new m("ECS deployment failed. Check the ECS console for detailed error information","deployment_failed",e,{clusterArn:s,status:u}));await U(a)}return l(new m(`ECS deployment timed out after ${r/1e3} seconds. The deployment is still in progress. Check the ECS console for status`,"deployment_timeout",e,{clusterArn:s,maxWaitTime:r}))}processDeploymentEvents(n,s,e,t,r,a){const o=n.filter(i=>!(i.timestamp<e||t&&i.timestamp<=t||r.has(i.message)));if(o.length===0)return{lastEventTimestamp:t,eventsReported:!1};for(const i of o)r.add(i.message),a?.({...s,latestEvent:i.message});return{lastEventTimestamp:o[o.length-1].timestamp,eventsReported:!0}}async listClusters(n){try{const s=this.getClient(),e=new M({}),t=await s.send(e,{abortSignal:g(n)});return c(t.clusterArns||[])}catch(s){const e=p(w(s));return l(new m(`Failed to list clusters: ${e}`,"permission_denied",void 0,s))}}async describeClusters(n,s){try{const e=this.getClient(),t=new F({clusters:n}),r=await e.send(t,{abortSignal:g(s)});if(!r.clusters)return c([]);const a=r.clusters.map(o=>({clusterArn:o.clusterArn,clusterName:o.clusterName,status:o.status,registeredContainerInstancesCount:o.registeredContainerInstancesCount,runningTasksCount:o.runningTasksCount,pendingTasksCount:o.pendingTasksCount}));return c(a)}catch(e){const t=p(w(e));return l(new m(`Failed to describe clusters: ${t}`,"unknown",void 0,{clusterArns:n,error:e},!1))}}async listServices(n,s){try{const e=this.getClient(),t=new R({cluster:n}),r=await e.send(t,{abortSignal:g(s)});return c(r.serviceArns||[])}catch(e){const t=p(w(e));return l(new m(`Failed to list services: ${t}`,"unknown",n,{clusterArn:n,error:e},!1))}}async registerTaskDefinition(n,s){try{const e=this.getClient(),t=new N(n),r=await e.send(t,{abortSignal:g(s)});return r.taskDefinition?.taskDefinitionArn?c(r.taskDefinition.taskDefinitionArn):l(new m("Task definition registration failed - no ARN returned","task_definition_failed",void 0,n))}catch(e){const t=p(w(e));return l(new m(`Failed to register task definition: ${t}`,"task_definition_failed",void 0,{taskDefinition:n,error:e},!1))}}async getLatestTaskDefinition(n,s){try{const e=this.getClient(),t=new _({taskDefinition:n}),r=await e.send(t,{abortSignal:g(s)});return c(r.taskDefinition)}catch(e){const t=p(w(e));return l(new m(`Failed to get task definition: ${t}`,"unknown",void 0,{family:n,error:e},!1))}}async deployService(n,s,e){const t=await this.updateService(n,s,{forceNewDeployment:!0,taskDefinition:e?.taskDefinition},e?.abortSignal);if(!t.success)return l(t.error);if(e?.waitForCompletion!==!1){const r=await this.pollDeployment({clusterArn:n,serviceArn:s,waitForCompletion:!0,progressCallback:e?.progressCallback,abortSignal:e?.abortSignal});if(!r.success)return l(r.error)}return c(void 0)}}export{m as EcsError,W as EcsService};
|
|
@@ -7,10 +7,10 @@ import type { EcsError } from "./EcsService.js";
|
|
|
7
7
|
* cast across the package boundary.
|
|
8
8
|
*/
|
|
9
9
|
export interface CfnExportsClient {
|
|
10
|
-
getExportsByNames(names: string[]): Promise<Result<Map<string, string>, {
|
|
10
|
+
getExportsByNames(names: string[], abortSignal?: AbortSignal): Promise<Result<Map<string, string>, {
|
|
11
11
|
message: string;
|
|
12
12
|
}>>;
|
|
13
|
-
listExports(): Promise<Result<Array<{
|
|
13
|
+
listExports(callbacks?: undefined, abortSignal?: AbortSignal): Promise<Result<Array<{
|
|
14
14
|
Name: string;
|
|
15
15
|
Value: string;
|
|
16
16
|
}>, {
|
|
@@ -57,7 +57,7 @@ export declare class EcsServiceResolver {
|
|
|
57
57
|
name: string;
|
|
58
58
|
}>;
|
|
59
59
|
}>;
|
|
60
|
-
}): Promise<Result<{
|
|
60
|
+
}, abortSignal?: AbortSignal): Promise<Result<{
|
|
61
61
|
clusterArn?: string;
|
|
62
62
|
serviceArns: string[];
|
|
63
63
|
}, EcsError>>;
|
|
@@ -71,7 +71,7 @@ export declare class EcsServiceResolver {
|
|
|
71
71
|
* Service: `${clusterName}${serviceName}DeployableService`
|
|
72
72
|
* where clusterName typically contains the app name (e.g. "StandardsapiCompute").
|
|
73
73
|
*/
|
|
74
|
-
getDeployableClusterAndServices(appName: string): Promise<Result<{
|
|
74
|
+
getDeployableClusterAndServices(appName: string, abortSignal?: AbortSignal): Promise<Result<{
|
|
75
75
|
clusterArn?: string;
|
|
76
76
|
serviceArns: string[];
|
|
77
77
|
}, EcsError>>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{success as l}from"@fjall/generator";import{logger as
|
|
1
|
+
import{success as l}from"@fjall/generator";import{logger as u}from"@fjall/util/logger";import{getErrorMessage as p,maskSensitiveOutput as g}from"@fjall/util";import{extractEcsServiceParts as b,buildArn as y}from"../../aws/utils/arnParser.js";function m(d,o){const a=d.toLowerCase(),r=o.toLowerCase();if(r.length===0)return!1;const i=e=>!/[a-zA-Z0-9]/.test(e),n=e=>/[A-Z]/.test(e);let s=a.indexOf(r);for(;s!==-1;){const e=d[s]??"",t=s>0?d[s-1]??"":"",c=s===0||n(e)||i(t),f=s+r.length,v=d[f],h=v===void 0||n(v)||i(v);if(c&&h)return!0;s=a.indexOf(r,s+1)}return!1}class C{cfService;constructor(o){this.cfService=o}async getDeployableClusterAndServicesByManifest(o,a){try{const r=[];for(const t of o.clusters){r.push(`${t.name}DeployableCluster`);for(const c of t.services)r.push(`${t.name}${c.name}DeployableService`)}if(r.length===0)return l({serviceArns:[]});const i=await this.cfService.getExportsByNames(r,a);if(!i.success)return u.debug("EcsServiceResolver","Failed to get CloudFormation exports by name",{error:i.error.message}),l({serviceArns:[]});const n=i.data;let s;for(const t of o.clusters){const c=n.get(`${t.name}DeployableCluster`);if(c){s=c;break}}const e=[];for(const t of o.clusters)for(const c of t.services){const f=n.get(`${t.name}${c.name}DeployableService`);f&&e.push(f)}return u.debug("EcsServiceResolver",`Manifest-based discovery: found ${e.length} service(s), cluster=${s||"not found"}`),l({clusterArn:s,serviceArns:e})}catch(r){return u.warn("EcsServiceResolver","Manifest-based discovery failed",{error:g(p(r))}),l({serviceArns:[]})}}async getDeployableClusterAndServices(o,a){try{const r=await this.cfService.listExports(void 0,a);if(!r.success)return u.debug("EcsServiceResolver","Failed to list CloudFormation exports",{error:r.error.message}),l({serviceArns:[]});const n=r.data.filter(e=>{const t=e.Name??"";return m(t,o)&&t.toLowerCase().endsWith("deployableservice")}).map(e=>e.Value).filter(e=>e!==void 0);let s;if(n.length>0){const e=b(n[0]);e&&(s=y("ecs",e.region,e.accountId,`cluster/${e.clusterName}`))}return s||(s=r.data.find(t=>{const c=t.Name??"";return m(c,o)&&c.toLowerCase().endsWith("deployablecluster")})?.Value),u.debug("EcsServiceResolver",`Found ${n.length} deployable service(s) for app "${o}" in cluster ${s||"unknown"}`),l({clusterArn:s,serviceArns:n})}catch(r){return u.warn("EcsServiceResolver","Failed to get deployable services",{error:g(p(r))}),l({serviceArns:[]})}}}export{C as EcsServiceResolver,m as matchesAppNameToken};
|
|
@@ -0,0 +1,62 @@
|
|
|
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
|
+
* Set when the update was accepted but its completion could NOT be
|
|
18
|
+
* verified (the deploy role lacks `lambda:GetFunctionConfiguration`).
|
|
19
|
+
* Absent means the update was polled to `LastUpdateStatus: Successful`.
|
|
20
|
+
*/
|
|
21
|
+
unverifiedReason?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Lambda service using the AwsProvider interface. Constructor-injected
|
|
25
|
+
* provider matches the `EcsService`/`CloudFormationService` pattern.
|
|
26
|
+
*/
|
|
27
|
+
export declare class LambdaService {
|
|
28
|
+
private aws;
|
|
29
|
+
constructor(aws: AwsProvider);
|
|
30
|
+
private getClient;
|
|
31
|
+
/**
|
|
32
|
+
* Publish a new immutable version of a Lambda function pointing at
|
|
33
|
+
* whatever is currently deployed as `$LATEST`. Used right after a
|
|
34
|
+
* container-image Lambda's `cdk deploy` succeeds — CDK's `.currentVersion`
|
|
35
|
+
* can't be trusted to do this itself when the image reference is a
|
|
36
|
+
* `CfnParameter` (its logical-id hash is computed from synth-time template
|
|
37
|
+
* shape, not the parameter's deploy-time resolved value, so the hash
|
|
38
|
+
* doesn't change between deploys even though the underlying image does).
|
|
39
|
+
*/
|
|
40
|
+
publishVersion(functionName: string, abortSignal?: AbortSignal): Promise<Result<PublishedLambdaVersion, LambdaError>>;
|
|
41
|
+
/**
|
|
42
|
+
* Update a container-image Lambda function's code directly — the Lambda
|
|
43
|
+
* equivalent of ECS's `UpdateService`/`RegisterTaskDefinition` for a
|
|
44
|
+
* code-only deploy, bypassing CloudFormation entirely. Publishes a new
|
|
45
|
+
* version in the same call (`Publish: true`) since we own this API call
|
|
46
|
+
* directly here — unlike the full-deploy path, where CloudFormation
|
|
47
|
+
* performs the code update and `publishVersion` has to run as a separate
|
|
48
|
+
* step afterward.
|
|
49
|
+
*/
|
|
50
|
+
updateFunctionCode(functionName: string, imageUri: string, abortSignal?: AbortSignal): Promise<Result<UpdatedLambdaFunctionCode, LambdaError>>;
|
|
51
|
+
/**
|
|
52
|
+
* UpdateFunctionCode is accepted asynchronously: the API returns 200 with
|
|
53
|
+
* `LastUpdateStatus: InProgress`, and a container-image update can still
|
|
54
|
+
* fail afterwards (image optimisation — ImageAccessDenied, InvalidImage,
|
|
55
|
+
* ImageDeleted), leaving the function on its previous code. Poll the
|
|
56
|
+
* configuration until the update lands, mirroring the ECS rollout's
|
|
57
|
+
* `pollDeployment` semantics (same 600s budget). A role missing
|
|
58
|
+
* `lambda:GetFunctionConfiguration` degrades to accepted-but-unverified
|
|
59
|
+
* rather than failing a rollout that almost certainly succeeded.
|
|
60
|
+
*/
|
|
61
|
+
private waitForUpdateCompletion;
|
|
62
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{GetFunctionConfigurationCommand as v,LambdaClient as _,PublishVersionCommand as C,UpdateFunctionCodeCommand as $}from"@aws-sdk/client-lambda";import{success as p,failure as a}from"@fjall/generator";import{BaseServiceError as g}from"../../types/errors/ServiceError.js";import{logger as f}from"@fjall/util/logger";import{maskSensitiveOutput as m}from"@fjall/util";import{getAwsErrorDetails as b}from"../../aws/utils/awsErrorHandler.js";import{composeSdkAbortSignal as w,isAborted as A,isAccessDenied as L}from"../../aws/organisations/types.js";const h=6e5,F=5e3;function P(l,e){return e===void 0?new Promise(r=>setTimeout(r,l)):e.aborted?Promise.resolve():new Promise(r=>{const o=setTimeout(()=>{e.removeEventListener("abort",t),r()},l),t=()=>{clearTimeout(o),r()};e.addEventListener("abort",t,{once:!0})})}class d extends g{functionName;operation;constructor(e,r,o,t){super(`LAMBDA_${o.toUpperCase()}_FAILED`,e,t,!1),this.functionName=r,this.operation=o}}class R{aws;constructor(e){this.aws=e}getClient(){return this.aws.getClient(_)}async publishVersion(e,r){try{const t=await this.getClient().send(new C({FunctionName:e}),{abortSignal:w(r)});return!t.FunctionArn||!t.Version?a(new d("PublishVersion returned no function ARN or version",e,"publish_version",t)):(f.debug("LambdaService",`Published version ${t.Version} for ${e}`),p({functionArn:t.FunctionArn,version:t.Version}))}catch(o){const{message:t,name:s,httpCode:n}=b(o),i=m(t);f.error("LambdaService",`PublishVersion failed for ${e}: ${s} - ${i}`,{functionName:e,httpCode:n,error:o});let u=`Failed to publish Lambda version for '${e}': ${i}`;return n&&(u+=` (HTTP ${n})`),a(new d(u,e,"publish_version",{error:o,httpCode:n,errorName:s}))}}async updateFunctionCode(e,r,o){try{const t=this.getClient(),s=await t.send(new $({FunctionName:e,ImageUri:r,Publish:!0}),{abortSignal:w(o)});if(!s.FunctionArn||!s.Version)return a(new d("UpdateFunctionCode returned no function ARN or version",e,"update_function_code",s));const n=await this.waitForUpdateCompletion(t,e,o);return n.success?(f.debug("LambdaService",`Updated ${e} to ${r}, published version ${s.Version}`),p({functionArn:s.FunctionArn,version:s.Version,...n.data.unverifiedReason!==void 0?{unverifiedReason:n.data.unverifiedReason}:{}})):a(n.error)}catch(t){const{message:s,name:n,httpCode:i}=b(t),u=m(s);f.error("LambdaService",`UpdateFunctionCode failed for ${e}: ${n} - ${u}`,{functionName:e,imageUri:r,httpCode:i,error:t});let c=`Failed to update Lambda function code for '${e}': ${u}`;return i&&(c+=` (HTTP ${i})`),a(new d(c,e,"update_function_code",{error:t,httpCode:i,errorName:n}))}}async waitForUpdateCompletion(e,r,o){const t=Date.now();for(;Date.now()-t<h;){if(A(o))return a(new d(`Cancelled while waiting for the code update of '${r}' to complete \u2014 the update was accepted and may still land`,r,"update_function_code"));let s,n;try{const i=await e.send(new v({FunctionName:r}),{abortSignal:w(o)});s=i.LastUpdateStatus,n=i.LastUpdateStatusReason}catch(i){const{message:u,name:c}=b(i);return L(c)?p({unverifiedReason:"lambda:GetFunctionConfiguration is denied for this role, so completion of the update could not be verified"}):a(new d(`Failed to verify the code update of '${r}' completed: ${m(u)}`,r,"update_function_code",{errorName:c}))}if(s==="Successful")return p({});if(s==="Failed")return a(new d(`Code update of '${r}' was accepted but failed to apply: ${m(n??"no reason reported")}. The function is still running its previous code.`,r,"update_function_code",{statusReason:n}));await P(F,o)}return a(new d(`Code update of '${r}' did not complete within ${h/1e3}s \u2014 it may still be in progress. Check the Lambda console before retrying.`,r,"update_function_code"))}}export{d as LambdaError,R as LambdaService};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{BaseServiceError as
|
|
1
|
+
import{BaseServiceError as c}from"../errors/ServiceError.js";import{getErrorMessage as o,maskSensitiveOutput as i}from"@fjall/util";function T(t,a,e,p,r){const s=i(o(r));return new m(`${t}: ${s}`,{errorType:a,appName:e,operation:p,details:r,...r instanceof Error&&{cause:r}})}class m extends c{errorType;appName;operation;stackType;constructor(a,e){super(`APPLICATION_${e.errorType.toUpperCase()}`,a,e.details,e.recoverable??!1,e.cause),this.errorType=e.errorType,this.appName=e.appName,this.operation=e.operation,this.stackType=e.stackType}}export{m as ApplicationError,T as wrapApplicationError};
|
|
@@ -119,6 +119,7 @@ export declare const RemediationJournalRecordSchema: z.ZodObject<{
|
|
|
119
119
|
Snapshot: "Snapshot";
|
|
120
120
|
Delete: "Delete";
|
|
121
121
|
}>>>;
|
|
122
|
+
coRemovedLogicalIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
122
123
|
orphanedResources: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
123
124
|
unverifiedRemovals: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
124
125
|
completedAt: z.ZodOptional<z.ZodString>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{z as e}from"zod";const t=["captured","flip-verified","flip-applied","removal-applied","done"],
|
|
1
|
+
import{z as e}from"zod";const t=["captured","flip-verified","flip-applied","removal-applied","done"],o=e.enum(t);function m(i){return t.indexOf(i)}const r=e.object({logicalId:e.string().min(1),resourceType:e.string().min(1),physicalId:e.string().min(1)}).strict();function a(i,n){return`${i}/${n}`}function p(i,n){return i.physicalId===a(n,i.logicalId)}const s=e.object({diffGuardPassed:e.boolean(),changeSetMetadataOnly:e.boolean(),noChanges:e.boolean(),provenAt:e.string().min(1)}).strict(),l=e.enum(["forget","recreate"]),d=e.object({version:e.literal(1),opId:e.string().min(1),verb:l,accountId:e.string().min(1),region:e.string().min(1),stackName:e.string().min(1),appName:e.string().min(1).optional(),targets:e.array(r).min(1),phase:o,createdAt:e.string().min(1),updatedAt:e.string().min(1),liveTemplateDigest:e.string().min(1).optional(),forensicsPath:e.string().min(1).optional(),proof:s.optional(),consentedPhysicalNames:e.array(e.string()).optional(),retainedByRedeclaration:e.array(e.string().min(1)).optional(),fliplessTargets:e.array(e.string().min(1)).optional(),fliplessByNecessity:e.array(e.string().min(1)).optional(),recreatedResources:e.array(e.string().min(1)).optional(),snapshotPolicies:e.record(e.string().min(1),e.enum(["Snapshot","Delete"])).optional(),coRemovedLogicalIds:e.array(e.string().min(1)).optional(),orphanedResources:e.array(e.string().min(1)).optional(),unverifiedRemovals:e.array(e.string().min(1)).optional(),completedAt:e.string().min(1).optional()}).strict();export{t as REMEDIATION_PHASES,s as RemediationFlipProofSchema,d as RemediationJournalRecordSchema,o as RemediationPhaseSchema,r as RemediationTargetSchema,l as RemediationVerbWithSurgerySchema,p as isRemediationPlaceholderPhysicalId,m as remediationPhaseRank,a as remediationPlaceholderPhysicalId};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fjall/deploy-core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.31.1",
|
|
4
4
|
"description": "Shared deployment engine for Fjall — used by CLI and webapp worker",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/src/index.js",
|
|
@@ -70,11 +70,13 @@
|
|
|
70
70
|
"@aws-sdk/client-cloudformation": "^3.1038.0",
|
|
71
71
|
"@aws-sdk/client-cloudtrail": "^3.1065.0",
|
|
72
72
|
"@aws-sdk/client-cost-explorer": "^3.1038.0",
|
|
73
|
+
"@aws-sdk/client-dynamodb": "^3.1038.0",
|
|
73
74
|
"@aws-sdk/client-ec2": "^3.1038.0",
|
|
74
75
|
"@aws-sdk/client-ecr": "^3.1039.0",
|
|
75
76
|
"@aws-sdk/client-iam": "^3.1038.0",
|
|
76
77
|
"@aws-sdk/client-identitystore": "^3.1079.0",
|
|
77
78
|
"@aws-sdk/client-kms": "^3.1065.0",
|
|
79
|
+
"@aws-sdk/client-lambda": "^3.1038.0",
|
|
78
80
|
"@aws-sdk/client-organizations": "^3.1038.0",
|
|
79
81
|
"@aws-sdk/client-ram": "^3.1038.0",
|
|
80
82
|
"@aws-sdk/client-rds": "^3.1038.0",
|
|
@@ -84,8 +86,8 @@
|
|
|
84
86
|
"@aws-sdk/client-ssm": "^3.1038.0",
|
|
85
87
|
"@aws-sdk/client-sso-admin": "^3.1038.0",
|
|
86
88
|
"@aws-sdk/client-sts": "^3.1038.0",
|
|
87
|
-
"@fjall/generator": "^2.
|
|
88
|
-
"@fjall/util": "^2.
|
|
89
|
+
"@fjall/generator": "^2.31.1",
|
|
90
|
+
"@fjall/util": "^2.31.1",
|
|
89
91
|
"@smithy/node-http-handler": "^4.6.1",
|
|
90
92
|
"aws-cdk": "^2.1128.1",
|
|
91
93
|
"tsx": "^4.21.0",
|
|
@@ -96,5 +98,5 @@
|
|
|
96
98
|
"typescript": "^6.0.3",
|
|
97
99
|
"vitest": "^4.1.5"
|
|
98
100
|
},
|
|
99
|
-
"gitHead": "
|
|
101
|
+
"gitHead": "a48cfaf6e050ce1736837802dfcaf517ba39f199"
|
|
100
102
|
}
|