@fjall/deploy-core 2.23.1 → 2.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.minified +1 -1
- package/dist/src/aws/index.d.ts +2 -2
- package/dist/src/aws/index.js +1 -1
- package/dist/src/aws/organisations/identityCentreSnapshot.d.ts +47 -0
- package/dist/src/aws/organisations/identityCentreSnapshot.js +1 -0
- package/dist/src/aws/organisations/index.d.ts +5 -1
- package/dist/src/aws/organisations/index.js +1 -1
- package/dist/src/aws/organisations/pagination.d.ts +28 -0
- package/dist/src/aws/organisations/pagination.js +1 -0
- package/dist/src/index.d.ts +9 -5
- package/dist/src/index.js +1 -1
- package/dist/src/orchestration/application/applicationDeploy.js +1 -1
- package/dist/src/orchestration/application/applicationDeployHelpers.d.ts +11 -12
- package/dist/src/orchestration/application/applicationDeployHelpers.js +2 -2
- package/dist/src/orchestration/application/approvalGate.d.ts +3 -1
- package/dist/src/orchestration/application/approvalGate.js +1 -1
- package/dist/src/orchestration/application/artefactCollection.d.ts +18 -0
- package/dist/src/orchestration/application/artefactCollection.js +1 -0
- package/dist/src/orchestration/application/buildSecretSession.d.ts +8 -0
- package/dist/src/orchestration/application/buildSecretSession.js +1 -1
- package/dist/src/orchestration/application/codeOnlyDeploy.d.ts +13 -0
- package/dist/src/orchestration/application/codeOnlyDeploy.js +1 -1
- package/dist/src/orchestration/application/deploySessionPolicy.d.ts +46 -0
- package/dist/src/orchestration/application/deploySessionPolicy.js +1 -0
- package/dist/src/orchestration/application/dockerBuildHelper.d.ts +21 -5
- package/dist/src/orchestration/application/dockerBuildHelper.js +1 -1
- package/dist/src/orchestration/application/serviceImageTagsRollback.d.ts +56 -0
- package/dist/src/orchestration/application/serviceImageTagsRollback.js +1 -0
- package/dist/src/orchestration/application/taskDefinitionRoll.d.ts +33 -0
- package/dist/src/orchestration/application/taskDefinitionRoll.js +1 -0
- package/dist/src/orchestration/deploy.js +1 -1
- package/dist/src/orchestration/dockerInterface.d.ts +5 -2
- package/dist/src/orchestration/index.d.ts +5 -0
- package/dist/src/orchestration/index.js +1 -1
- package/dist/src/orchestration/organisationDeploy/orgCascadeDeploy.js +1 -1
- package/dist/src/orchestration/organisationDeploy/singleComponentDeploy.js +1 -1
- package/dist/src/orchestration/restart/restartApplication.d.ts +69 -0
- package/dist/src/orchestration/restart/restartApplication.js +1 -0
- package/dist/src/orchestration/restart/secretsDrift.d.ts +54 -0
- package/dist/src/orchestration/restart/secretsDrift.js +1 -0
- package/dist/src/services/infrastructure/CdkCommandRunner.js +2 -2
- package/dist/src/services/infrastructure/CdkService.d.ts +1 -1
- package/dist/src/services/infrastructure/CdkService.js +2 -2
- package/dist/src/services/infrastructure/CdkServiceTypes.d.ts +9 -0
- package/dist/src/services/infrastructure/CloudFormationService.d.ts +17 -0
- package/dist/src/services/infrastructure/CloudFormationService.js +1 -1
- package/dist/src/services/infrastructure/EcrImageInspectorService.d.ts +11 -1
- package/dist/src/services/infrastructure/EcrImageInspectorService.js +1 -1
- package/dist/src/services/infrastructure/SsmParameterMetadataService.d.ts +23 -0
- package/dist/src/services/infrastructure/SsmParameterMetadataService.js +1 -0
- package/dist/src/types/approval.d.ts +15 -0
- package/dist/src/types/index.d.ts +1 -1
- package/dist/src/types/params.d.ts +39 -1
- package/package.json +5 -4
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type Result } from "@fjall/generator";
|
|
2
|
+
/**
|
|
3
|
+
* The AWS-account/region context a deploy session's inline policy is scoped to.
|
|
4
|
+
* `appName` is the deploy target; `partition` is derived via `partitionForRegion`
|
|
5
|
+
* (re-exported from `buildSecretSession.js`).
|
|
6
|
+
*/
|
|
7
|
+
export interface DeploySessionContext {
|
|
8
|
+
appName: string;
|
|
9
|
+
region: string;
|
|
10
|
+
accountId: string;
|
|
11
|
+
partition: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Build a defence-in-depth inline session policy that narrows a 1-hour deploy
|
|
15
|
+
* session's DIRECT CloudFormation/ECR mutation to the target app's stack-name
|
|
16
|
+
* prefix and ECR repository. The connected `FjallDeploy<orgId>` role carries
|
|
17
|
+
* `AdministratorAccess` and the OIDC subject encodes no resource narrowing, so
|
|
18
|
+
* without this the session is an org-wide admin bearer credential.
|
|
19
|
+
*
|
|
20
|
+
* Two accepted v1 limitations (not app-confinement guarantees):
|
|
21
|
+
* - Session policies do not propagate across `sts:AssumeRole`, so mutation via
|
|
22
|
+
* the CDK deploy/cfn-exec roles is out of this policy's reach — the real
|
|
23
|
+
* confinement is the Phase E `FjallDeployBoundary` permission boundary.
|
|
24
|
+
* - The `stack/${appPascal}*` prefix wildcard over-grants a prefix-collision
|
|
25
|
+
* sibling app (`Web` vs `WebApp`) — same low-risk, same-account class as the
|
|
26
|
+
* ECR baseline-prefix note in `buildSecretSession.ts`.
|
|
27
|
+
*
|
|
28
|
+
* DENY-list, not allow-list (deliberate). `fjall deploy` runs `cdk deploy`: the
|
|
29
|
+
* caller's session performs synth lookups, asset uploads to the CDK asset bucket,
|
|
30
|
+
* ECR pushes, `iam:PassRole` on the cfn-exec role, and `CreateStack`/`UpdateStack`.
|
|
31
|
+
* CloudFormation then provisions all downstream resources under the pre-bootstrapped
|
|
32
|
+
* cfn-exec role (AdministratorAccess), NOT under this session. An allow-list scoped
|
|
33
|
+
* to "app stacks + ECR" would strip synth/asset/PassRole and break every deploy, so
|
|
34
|
+
* we instead lead with `Allow * on *` (inherit the role's admin) and Deny only
|
|
35
|
+
* FOREIGN CloudFormation-stack mutation and FOREIGN ECR mutation.
|
|
36
|
+
*
|
|
37
|
+
* Naming is coupled to `getApplicationStackName` (`../../types/operations.js`):
|
|
38
|
+
* every one of an app's stacks is named `toPascalCase(appName) + stack`, so a
|
|
39
|
+
* CloudFormation stack-ARN wildcard on the `toPascalCase(appName)` prefix covers
|
|
40
|
+
* all six. If that derivation ever changes, this wildcard must change with it. The
|
|
41
|
+
* ECR repo is `toKebab(appName)` (matching the ECR construct at
|
|
42
|
+
* components/infrastructure/lib/app.ts) — never a re-implemented `.toLowerCase()`.
|
|
43
|
+
*
|
|
44
|
+
* Fails closed when the serialised policy would exceed the AWS inline-policy limit.
|
|
45
|
+
*/
|
|
46
|
+
export declare function buildDeploySessionPolicy(ctx: DeploySessionContext): Result<string>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{success as p,failure as s}from"@fjall/generator";import{toKebab as m,toPascalCase as d}from"@fjall/util";import{MAX_INLINE_POLICY_CHARS as r}from"./buildSecretSession.js";const u=["cloudformation:CreateStack","cloudformation:UpdateStack","cloudformation:DeleteStack","cloudformation:CreateChangeSet","cloudformation:ExecuteChangeSet","cloudformation:DeleteChangeSet","cloudformation:CancelUpdateStack","cloudformation:ContinueUpdateRollback","cloudformation:RollbackStack","cloudformation:SetStackPolicy","cloudformation:UpdateTerminationProtection"],f=["ecr:PutImage","ecr:InitiateLayerUpload","ecr:UploadLayerPart","ecr:CompleteLayerUpload","ecr:BatchDeleteImage","ecr:DeleteRepository","ecr:SetRepositoryPolicy","ecr:DeleteRepositoryPolicy"];function $(a){const{region:o,accountId:e,partition:t}=a,n=d(a.appName),i=m(a.appName),l=[{Sid:"FjallDeployInheritAdmin",Effect:"Allow",Action:"*",Resource:"*"},{Sid:"FjallDeployDenyForeignStackMutation",Effect:"Deny",Action:u,NotResource:[`arn:${t}:cloudformation:${o}:${e}:stack/${n}*/*`,`arn:${t}:cloudformation:${o}:${e}:stack/CDKToolkit/*`]},{Sid:"FjallDeployDenyForeignEcrMutation",Effect:"Deny",Action:f,NotResource:[`arn:${t}:ecr:${o}:${e}:repository/${i}`,`arn:${t}:ecr:${o}:${e}:repository/cdk-*`]}],c=JSON.stringify({Version:"2012-10-17",Statement:l});return c.length>r?s(new Error(`Deploy session policy is ${c.length} chars, exceeding the ${r}-char AWS inline-policy limit.`)):p(c)}export{$ as buildDeploySessionPolicy};
|
|
@@ -4,6 +4,22 @@ 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
|
+
/**
|
|
8
|
+
* Everything the build phase knows about what it pushed. The tag map drives
|
|
9
|
+
* the CDK `<Service>ImageTag` parameters; the digest map + repo URI feed the
|
|
10
|
+
* full-deploy `ServiceArtefact[]` collection.
|
|
11
|
+
*/
|
|
12
|
+
export interface DockerBuildProducts {
|
|
13
|
+
contentHashTagsByService: Record<string, string>;
|
|
14
|
+
imageDigestsByService: Record<string, string>;
|
|
15
|
+
/**
|
|
16
|
+
* ECR repository URI the images were pushed to. Absent on the legacy
|
|
17
|
+
* single-image fallback path (no manifest), where no content-hash tags are
|
|
18
|
+
* produced either.
|
|
19
|
+
*/
|
|
20
|
+
repoUri?: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function emptyBuildProducts(): DockerBuildProducts;
|
|
7
23
|
/**
|
|
8
24
|
* Run Docker build and push before deploying the Compute stack.
|
|
9
25
|
*
|
|
@@ -16,12 +32,12 @@ export declare const DOCKER_BUILD_STEP_NAME = "Building and pushing Docker image
|
|
|
16
32
|
* After each group's push, the helper derives a content-hash tag from the
|
|
17
33
|
* digest (`deriveContentHashTag` at `@fjall/util`) and applies it via
|
|
18
34
|
* `tagByDigest` (manifest-list create, no layer re-upload). The resulting
|
|
19
|
-
* `<service>` → `<content-hash-tag>` map
|
|
20
|
-
*
|
|
35
|
+
* `DockerBuildProducts` carries the `<service>` → `<content-hash-tag>` map
|
|
36
|
+
* (plumbed into the CDK deploy via `--parameters <Service>ImageTag=<tag>`)
|
|
37
|
+
* plus the per-service digests and repo URI for artefact collection.
|
|
21
38
|
*
|
|
22
39
|
* Falls back to the legacy single-image path (`<appPath>/Dockerfile` →
|
|
23
40
|
* `<appName>:latest`) when the manifest is absent. The legacy path returns
|
|
24
|
-
*
|
|
25
|
-
* for that branch.
|
|
41
|
+
* empty products; the orchestrator skips parameter plumbing for that branch.
|
|
26
42
|
*/
|
|
27
|
-
export declare function runDockerBuild(params: DeployParams, services: DeployServices, operation: ApplicationOperation, callbacks: DeployCallbacks): Promise<Result<
|
|
43
|
+
export declare function runDockerBuild(params: DeployParams, services: DeployServices, operation: ApplicationOperation, callbacks: DeployCallbacks): Promise<Result<DockerBuildProducts>>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{isAbsolute as
|
|
1
|
+
import{isAbsolute as K,join as $,dirname as H,resolve as x}from"node:path";import{existsSync as T}from"node:fs";import{success as k,failure as f}from"@fjall/generator";import{deriveContentHashTag as F,maskSensitiveOutput as m,toKebab as I}from"@fjall/util";import{evaluateBakeGuard as U,evaluateResolvedBuildArgValues as G,acknowledgedBuildArgKeys as V,BAKE_GUARD_EXPOSURE_CLAUSE as M}from"@fjall/util/docker";import{logger as P}from"@fjall/util/logger";import{parseDockerServicesFromManifest as W}from"@fjall/util/manifest";import{STEP_IDS as b}from"../../types/stepDefinitions.js";import{resolveBuildSecrets as z,resolveSecretRefValue as X,resolveBuildSecretSessionProvider as Y,sourcedRefsFromBuildArgs as q}from"./buildSecretResolver.js";import{partitionForRegion as J}from"./buildSecretSession.js";import{resolveBuildArgs as Q}from"./buildArgResolver.js";import{validateBuildGroup as Z}from"./buildGroupValidator.js";const C="dockerBuildHelper",E="Building and pushing Docker image";function ee(e,s,r){const t=$(s,e);if(T(t))return{path:t,base:s};if(r!==""&&r!==s){const o=$(r,e);if(T(o))return{path:o,base:r}}return{path:t,base:s}}function te(e,s,r){const t=e.docker.path,o=e.docker.context;let a,i;if(K(t))a=t,i=s;else{const c=ee(t,s,r);a=c.path,i=c.base}return{buildContext:o?K(o)?x(o):x(i,o):x(H(a)),dockerfilePath:a,target:e.docker.target,resolvedViaFallback:i!==s}}function re(e){return`${e.buildContext}|${e.dockerfilePath}|${e.target??""}`}function ne(e,s){return`Dockerfile not found for service '${e}' at ${s}. 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 ie(e){return e.startsWith("cn-")?"amazonaws.com.cn":"amazonaws.com"}function oe(e,s,r){return`${e}.dkr.ecr.${s}.${ie(s)}/${I(r)}`}function se(e,s,r){const t=new Map;for(const o of e){const a=te(o,s,r),i=re(a),p=t.get(i);p?p.members.push(o):t.set(i,{identity:a,members:[o]})}return Array.from(t.values())}async function ae(e,s,r,t,o){const a=await e.buildAndPush({appName:s.appName,appPath:s.path,region:r,accountId:t},(i,p,c,v)=>{o.onDockerProgress?.(m(i),p,c,v)});return a.success?k({imageUri:a.data.imageUri,imageTag:a.data.imageTag}):f(a.error)}function L(){return{contentHashTagsByService:{},imageDigestsByService:{}}}async function de(e,s,r,t,o,a,i,p,c,v,y,h){const u=e.members[0];if(!T(e.identity.dockerfilePath))return f(new Error(ne(u.name,e.identity.dockerfilePath)));e.identity.resolvedViaFallback&&i.onLog?.(`Building service '${u.name}' from ${e.identity.dockerfilePath} (resolved at the project root).`,"info");const n=[...u.docker.buildSecrets??[],...h??[]],g=Z({serviceName:u.name,dockerfilePath:e.identity.dockerfilePath,buildContext:e.identity.buildContext,...e.identity.target!==void 0&&{target:e.identity.target},...u.docker.buildArgs!==void 0&&{buildArgs:u.docker.buildArgs},...n.length>0&&{buildSecrets:n},members:e.members.map(d=>({serviceName:d.name,...d.docker.buildArgs!==void 0&&{buildArgs:d.docker.buildArgs},...d.docker.buildSecrets!==void 0&&{buildSecrets:d.docker.buildSecrets}}))});if(!g.success)return f(g.error);const l=U(u.docker.buildArgs,I(r.appName));for(const d of l.warnings)i.onLog?.(m(d.message),"warn");if(l.findings.length>0)return f(new Error(m(l.findings.map(d=>d.message).join(" "))));const R=n,B=q(u.docker.buildArgs),A={appName:r.appName,region:t,accountId:o,partition:J(t)},S=await Y(p,[...R,...B],A);if(!S.success)return f(S.error);const D=S.data,N=await Q({manifestBuildArgs:u.docker.buildArgs,serviceDir:e.identity.buildContext,appDir:r.path,...v!==void 0&&{stage:v},shellEnv:process.env,...y!==void 0&&{flagBuildArgs:y},resolveSourcedValue:(d,j)=>X(D,{id:d,...j},c)});if(!N.success)return f(N.error);const O=N.data.buildArgs;for(const d of N.data.inferredDotenvKeys)i.onLog?.(m(`buildArg "${d}" was auto-declared from a .env file and will be baked into the image \u2014 ${M}.`),"warn");const _=G(O,I(r.appName),V(u.docker.buildArgs));if(_.length>0)return f(new Error(m(_.map(d=>d.message).join(" "))));let w;if(R.length>0){const d=await z(D,R,c);if(!d.success)return f(d.error);w=d.data}try{const d={appName:r.appName,appPath:e.identity.buildContext,region:t,accountId:o,buildContext:e.identity.buildContext,dockerfilePath:e.identity.dockerfilePath,pushByDigest:!0,serviceName:u.name,...e.identity.target!==void 0&&{target:e.identity.target},...Object.keys(O).length>0&&{buildArgs:O},...w!==void 0&&{buildSecrets:w.resolved}};return await ce(e,d,s,a,i)}finally{w!==void 0&&await w.cleanup().catch(()=>i.onLog?.("Build-secret cleanup failed.","warn"))}}async function ce(e,s,r,t,o){const a=await r.buildAndPush(s,(h,u,n,g)=>{o.onDockerProgress?.(m(h),u,n,g)});if(!a.success)return f(a.error);const i=a.data.imageDigest;if(typeof i!="string"||!i.startsWith("sha256:"))return f(new Error(`Buildx push succeeded but returned an invalid digest: ${m(String(i))}`));const p={},c={},v=[];for(const h of e.members){const u=h.docker.target,n=F(i,h.name,u);if(!n.success)return f(n.error);const g=n.data,l=h.name;p[l]=g,c[l]=i,v.push(`${t}:${g}`)}const y=await r.tagByDigest({sourceImage:t,digest:i,tags:v});return y.success?k({contentHashTagsByService:p,imageDigestsByService:c}):f(y.error)}async function Re(e,s,r,t){const o=e.dockerProvider;if(!o)return k(L());const a=s.awsProvider.getAccountId();if(!a)return t.onLog?.("Skipping Docker build \u2014 account ID not available","warn"),k(L());const i=s.awsProvider.getRegion(),p=$(r.path,"cdk.out"),c=W(p),v=oe(a,i,r.appName);if(P.debug(C,"runDockerBuild starting",{appName:r.appName,appPath:r.path,accountId:a,region:i,cdkOutPath:p,manifestServiceCount:c.length,manifestServices:c.map(n=>({name:n.name,path:n.docker.path,context:n.docker.context,target:n.docker.target})),stepId:b.DOCKER_OPERATIONS,stepName:E}),t.onStepStart?.(b.DOCKER_OPERATIONS,E),c.length===0){t.onLog?.("Initialising ECR repository\u2026","info");const n=await o.initialiseECR({appName:r.appName,region:i,accountId:a});n.success||t.onLog?.(`ECR initialisation failed: ${m(n.error.message)}`,"warn"),t.onLog?.("Building and pushing Docker image\u2026","info");const g=await ae(o,r,i,a,t);if(!g.success){P.debug(C,"Docker buildAndPush (legacy) failed",{appName:r.appName,error:m(g.error.message)}),t.onStepComplete?.(b.DOCKER_OPERATIONS,E,"error");const l=new Error(m(g.error.message));return t.onError?.(l),f(l)}return t.onStepComplete?.(b.DOCKER_OPERATIONS,E,"completed"),t.onLog?.(`Docker image pushed: ${g.data.imageUri}`,"info"),k(L())}const y=await o.preflight?.();if(y!==void 0&&!y.success){t.onStepComplete?.(b.DOCKER_OPERATIONS,E,"error");const n=new Error(m(y.error.message));return t.onError?.(n),f(n)}const h=se(c,r.path,e.workingDirectory);t.onLog?.(`Building and pushing ${c.length} service image(s) in ${h.length} group(s)\u2026`,"info");let u=!1;if(o.beginEcrSession!==void 0){const n=await o.beginEcrSession({accountId:a,region:i});if(!n.success){t.onStepComplete?.(b.DOCKER_OPERATIONS,E,"error");const g=new Error(m(n.error.message));return t.onError?.(g),f(g)}u=!0}try{const n={},g={};for(let l=0;l<h.length;l++){const R=h[l],B=R.members.map(S=>S.name).join(", ");t.onLog?.(`Building group ${l+1}/${h.length} (${B})\u2026`,"info");const A=await de(R,o,r,i,a,v,t,s.awsProvider,e.abortSignal,e.options?.environment,e.options?.flagBuildArgs,e.options?.flagBuildSecrets);if(!A.success){P.debug(C,"Docker buildAndPush failed",{appName:r.appName,leader:R.members[0]?.name,members:B,error:m(A.error.message)}),t.onStepComplete?.(b.DOCKER_OPERATIONS,E,"error");const S=new Error(m(A.error.message));return t.onError?.(S),f(S)}for(const[S,D]of Object.entries(A.data.contentHashTagsByService))n[S]=D;for(const[S,D]of Object.entries(A.data.imageDigestsByService))g[S]=D}return P.debug(C,"Docker buildAndPush succeeded",{appName:r.appName,services:c.map(l=>l.name),groupCount:h.length,contentHashTags:n}),t.onStepComplete?.(b.DOCKER_OPERATIONS,E,"completed"),t.onLog?.(`Docker images pushed for ${c.length} service(s)`,"info"),k({contentHashTagsByService:n,imageDigestsByService:g,repoUri:v})}finally{u&&o.endEcrSession!==void 0&&await o.endEcrSession().catch(n=>{P.debug(C,"endEcrSession cleanup failed",{error:m(n instanceof Error?n.message:String(n))})})}}export{E as DOCKER_BUILD_STEP_NAME,L as emptyBuildProducts,Re as runDockerBuild};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { TaskDefinition } from "@aws-sdk/client-ecs";
|
|
2
|
+
import { type Result } from "@fjall/generator";
|
|
3
|
+
import type { DeployServices } from "../serviceFactory.js";
|
|
4
|
+
/**
|
|
5
|
+
* Case-insensitive service-name match shared by the single `--service`
|
|
6
|
+
* filter and the `serviceImageTags` map matcher: a live ECS service name
|
|
7
|
+
* matches a candidate when it equals the candidate outright, or ends with it
|
|
8
|
+
* (the ECS service name can carry a cluster/app prefix the candidate omits).
|
|
9
|
+
*/
|
|
10
|
+
export declare function matchesServiceName(liveServiceName: string, candidate: string): boolean;
|
|
11
|
+
export interface MatchedServiceImageTag {
|
|
12
|
+
serviceArn: string;
|
|
13
|
+
serviceName: string;
|
|
14
|
+
tag: string;
|
|
15
|
+
mapKey: string;
|
|
16
|
+
}
|
|
17
|
+
export interface ServiceImageTagsMatchResult {
|
|
18
|
+
matched: MatchedServiceImageTag[];
|
|
19
|
+
/** Live ECS services with no corresponding entry in the supplied map. */
|
|
20
|
+
unmatchedLiveServiceNames: string[];
|
|
21
|
+
/** Map entries that named no live ECS service in this stack. */
|
|
22
|
+
staleMapKeys: string[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Match every live ECS service against the `serviceImageTags` map (F12).
|
|
26
|
+
* A live service without a map entry is left untouched (not rolled); a map
|
|
27
|
+
* entry naming no live service is stale (the Release recorded a service the
|
|
28
|
+
* stack no longer runs). Both are reported for the caller to warn on —
|
|
29
|
+
* neither is fatal individually; the caller fails only when NOTHING matched.
|
|
30
|
+
*/
|
|
31
|
+
export declare function matchServiceImageTagsToLiveServices(serviceArns: readonly string[], serviceImageTags: Record<string, string>): ServiceImageTagsMatchResult;
|
|
32
|
+
export interface ResolvedServiceTaskDefinition {
|
|
33
|
+
previousTaskDefArn: string;
|
|
34
|
+
taskDefinition: TaskDefinition;
|
|
35
|
+
containerDefinitions: NonNullable<TaskDefinition["containerDefinitions"]>;
|
|
36
|
+
repoUri: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Fetch a service's current task definition and derive its ECR repository
|
|
40
|
+
* URI from the first container image. Shared by the pre-flight digest
|
|
41
|
+
* resolution (map mode) and the rollout itself (legacy `--image-tag` mode) so
|
|
42
|
+
* the describe → task-definition → repo-URI derivation lives in one place.
|
|
43
|
+
*/
|
|
44
|
+
export declare function resolveServiceTaskDefinition(services: Pick<DeployServices, "ecsService">, clusterArn: string, serviceArn: string, serviceName: string): Promise<Result<ResolvedServiceTaskDefinition, Error>>;
|
|
45
|
+
export interface PreResolvedRollout {
|
|
46
|
+
taskDefinition: ResolvedServiceTaskDefinition;
|
|
47
|
+
imageDigest: string;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Resolve an ECR digest for every matched (service, tag) pair BEFORE any
|
|
51
|
+
* service is rolled (F12). A map-supplied tag is meant to be exact — unlike
|
|
52
|
+
* the legacy `--image-tag` rebase path, it must NEVER fall back to tag
|
|
53
|
+
* pinning, so a missing digest (an ECR error, or a non-ECR repository
|
|
54
|
+
* returning no digest) fails the whole pre-flight and rolls nothing.
|
|
55
|
+
*/
|
|
56
|
+
export declare function preflightResolveServiceImageTagDigests(services: DeployServices, clusterArn: string, matched: readonly MatchedServiceImageTag[], abortSignal?: AbortSignal): Promise<Result<Map<string, PreResolvedRollout>, Error>>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{success as h,failure as u}from"@fjall/generator";import{maskSensitiveOutput as p}from"@fjall/util";import{extractEcsServiceName as S}from"../../aws/utils/arnParser.js";import{isAborted as y}from"../../aws/organisations/types.js";import{stripTagOrDigest as E}from"../../services/infrastructure/EcrImageInspectorService.js";function $(c,g){const o=c.toLowerCase(),r=g.toLowerCase();return o===r||o.endsWith(r)}function L(c,g){const o=Object.entries(g),r=[],f=[],n=new Set,s=c.map(e=>{const t=S(e)??e;return{serviceArn:e,serviceName:t,lowerName:t.toLowerCase()}}),i=new Map,a=new Set;for(const e of s){const t=o.find(([d])=>d.toLowerCase()===e.lowerName);t!==void 0&&(i.set(e.serviceArn,t),a.add(t[0]))}const v=new Map,m=new Map;for(const e of s){if(i.has(e.serviceArn))continue;const t=o.filter(([d])=>!a.has(d)&&$(e.serviceName,d)).sort(([d],[l])=>l.length-d.length)[0];t!==void 0&&(v.set(e.serviceArn,t),m.set(t[0],(m.get(t[0])??0)+1))}for(const e of s){const t=v.get(e.serviceArn),d=i.get(e.serviceArn)??(t!==void 0&&m.get(t[0])===1?t:void 0);if(d===void 0){f.push(e.serviceName);continue}const[l,D]=d;n.add(l),r.push({serviceArn:e.serviceArn,serviceName:e.serviceName,tag:D,mapKey:l})}const w=o.map(([e])=>e).filter(e=>!n.has(e));return{matched:r,unmatchedLiveServiceNames:f,staleMapKeys:w}}async function k(c,g,o,r){const f=await c.ecsService.describeServices(g,[o]);if(!f.success)return u(new Error(`Failed to describe service ${r}: ${p(f.error.message)}`));const n=f.data[0];if(!n?.taskDefinition)return u(new Error(`Service ${r} has no task definition`));const s=n.taskDefinition,i=await c.ecsService.getLatestTaskDefinition(s);if(!i.success)return u(new Error(`Failed to fetch task definition for ${r}: ${p(i.error.message)}`));const a=i.data;if(!a||!a.containerDefinitions||a.containerDefinitions.length===0)return u(new Error(`Task definition for ${r} has no container definitions`));const v=a.containerDefinitions,m=v.find(e=>e.image)?.image;if(!m)return u(new Error(`Task definition for ${r} has no container image \u2014 cannot derive repository URI`));const w=E(m);return h({previousTaskDefArn:s,taskDefinition:a,containerDefinitions:v,repoUri:w})}async function I(c,g,o,r){const f=new Map;for(const n of o){if(y(r))return u(new Error("Pre-flight cancelled. No service was rolled."));const s=await k(c,g,n.serviceArn,n.serviceName);if(!s.success)return u(new Error(`Pre-flight failed resolving the current task definition for ${n.serviceName} (tag ${n.tag}): ${p(s.error.message)}. No service was rolled.`));const i=await c.ecrImageInspector.getImageDigest(s.data.repoUri,n.tag,r);if(!i.success||i.data===void 0){const a=i.success?"ECR returned no digest for this tag (non-ECR repository, or the image is missing)":p(i.error.message);return u(new Error(`Pre-flight digest resolution failed for ${n.serviceName} (tag ${n.tag}): ${a}. No service was rolled.`))}f.set(n.serviceArn,{taskDefinition:s.data,imageDigest:i.data})}return h(f)}export{L as matchServiceImageTagsToLiveServices,$ as matchesServiceName,I as preflightResolveServiceImageTagDigests,k as resolveServiceTaskDefinition};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type Result } from "@fjall/generator";
|
|
2
|
+
import type { ContainerDefinition, TaskDefinition } from "@aws-sdk/client-ecs";
|
|
3
|
+
import type { DeployCallbacks } from "../../types/callbacks.js";
|
|
4
|
+
import type { DeployServices } from "../serviceFactory.js";
|
|
5
|
+
export interface RollTaskDefinitionInput {
|
|
6
|
+
clusterArn: string;
|
|
7
|
+
serviceArn: string;
|
|
8
|
+
serviceName: string;
|
|
9
|
+
/** The resolved live task definition the new revision is copied from. */
|
|
10
|
+
taskDefinition: TaskDefinition;
|
|
11
|
+
/** Container definitions for the new revision (image-rewritten or unchanged). */
|
|
12
|
+
containerDefinitions: ContainerDefinition[];
|
|
13
|
+
previousTaskDefArn: string;
|
|
14
|
+
/** Forwarded to the onTaskDefRegistered event when the roll is digest-pinned. */
|
|
15
|
+
imageDigest?: string;
|
|
16
|
+
/** Epoch ms the per-service operation started — keeps event durations inclusive of resolution time. */
|
|
17
|
+
startedAtMs: number;
|
|
18
|
+
}
|
|
19
|
+
export interface RollTaskDefinitionOk {
|
|
20
|
+
newTaskDefArn: string;
|
|
21
|
+
durationMs: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Register a new task-definition revision copied from a resolved live one,
|
|
25
|
+
* point the service at it, and poll the ECS deployment to completion.
|
|
26
|
+
*
|
|
27
|
+
* Shared tail of the code-only/rollback rollout AND the restart operation —
|
|
28
|
+
* the RegisterTaskDefinition field-copy list below is a coupled value: a
|
|
29
|
+
* task-definition field omitted here is silently dropped from the new
|
|
30
|
+
* revision on BOTH paths, so it must have exactly one home.
|
|
31
|
+
*/
|
|
32
|
+
export declare function registerAndRollTaskDefinition(services: Pick<DeployServices, "ecsService">, input: RollTaskDefinitionInput, callbacks: DeployCallbacks): Promise<Result<RollTaskDefinitionOk, Error>>;
|
|
33
|
+
export declare function extractRevision(taskDefArn: string): number;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{success as w,failure as l}from"@fjall/generator";import{logger as S}from"@fjall/util/logger";import{maskSensitiveOutput as p}from"@fjall/util";const $="taskDefinitionRoll";async function T(n,a,r){const{clusterArn:f,serviceArn:s,serviceName:t,taskDefinition:e,containerDefinitions:A,previousTaskDefArn:g,imageDigest:C,startedAtMs:R}=a,m=e.family;if(!m)return l(new Error(`Task definition for ${t} has no family`));const d=await n.ecsService.registerTaskDefinition({family:m,taskRoleArn:e.taskRoleArn,executionRoleArn:e.executionRoleArn,networkMode:e.networkMode,containerDefinitions:A,volumes:e.volumes,placementConstraints:e.placementConstraints,requiresCompatibilities:e.requiresCompatibilities,cpu:e.cpu,memory:e.memory,runtimePlatform:e.runtimePlatform,proxyConfiguration:e.proxyConfiguration,inferenceAccelerators:e.inferenceAccelerators,pidMode:e.pidMode,ipcMode:e.ipcMode,ephemeralStorage:e.ephemeralStorage});if(!d.success)return l(new Error(`Failed to register task definition for ${t}: ${d.error.message}`));const i=d.data;r.onTaskDefRegistered?.({serviceName:t,taskDefinitionArn:i,previousTaskDefinitionArn:g,revision:y(i),family:m,...C!==void 0?{imageDigest:C}:{}});const D=await n.ecsService.updateService(f,s,{taskDefinition:i,forceNewDeployment:!0});if(!D.success)return l(new Error(`Failed to update service ${t}: ${D.error.message}`));const k=await n.ecsService.pollDeployment({clusterArn:f,serviceArn:s,waitForCompletion:!0,progressCallback:o=>{const v=o.latestEvent?` \u2014 ${p(o.latestEvent)}`:"";r.onECSProgress?.(`${t}: ${p(o.message)}${v}`,o.percentComplete)}}),c=Date.now()-R;if(!k.success){const o=p(k.error.message);return r.onECSComplete?.({serviceName:t,serviceArn:s,success:!1,taskDefinitionArn:i,durationMs:c,reason:o}),l(new Error(`ECS rollout failed for ${t}: ${o}`))}const u=await n.ecsService.getDeploymentStatus(f,s);return r.onECSComplete?.({serviceName:t,serviceArn:s,success:!0,taskDefinitionArn:i,durationMs:c,finalRunningCount:u.success?u.data.runningCount:void 0,finalDesiredCount:u.success?u.data.desiredCount:void 0}),S.debug($,`Rolled ${t} successfully`,{serviceArn:s,newTaskDefArn:i,previousTaskDefArn:g,durationMs:c}),w({newTaskDefArn:i,durationMs:c})}function y(n){const a=n.lastIndexOf(":");if(a<0)return 0;const r=n.slice(a+1);return/^\d+$/.test(r)?parseInt(r,10):0}export{y as extractRevision,T as registerAndRollTaskDefinition};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{success as u,failure as y}from"@fjall/generator";import{isApplicationOperation as f}from"../types/operations.js";import{isAborted as w}from"../aws/organisations/types.js";import{checkRegionEnabled as b,regionDisabledMessage as A}from"../aws/regions/regionOptStatus.js";import{createDeployServices as k}from"./serviceFactory.js";import{resolveOperation as m}from"./resolveOperation.js";import{deployApplication as D}from"./application/applicationDeploy.js";import{deployOrganisation as v}from"./organisation/organisationDeploy.js";async function N(e){const s=Date.now(),l=await b(e.awsCredentials.region,e.awsCredentials,e.abortSignal);if(l==="disabled"){const i=new Error(A(e.awsCredentials.region));return e.callbacks.onError?.(i),y(i)}l==="enabling"&&e.callbacks.onProgress?.({type:"warning",message:`Region ${e.awsCredentials.region} is still activating (opt-in status ENABLING); the deploy may fail until activation completes.`});let r;const g=e.callbacks.onFailureAnalysis,a={...e,callbacks:{...e.callbacks,onFailureAnalysis:i=>{r=i,g?.(i)}}},c=k(a),o=await m(e.target,e.workingDirectory);if(!o.success)return e.callbacks.onError?.(o.error),o;const n=o.data,p=f(n)?"application":"organisation",t=f(n)?await D(a,c,n):await v(a,c,n);if(w(e.abortSignal))return u({target:e.target,deploymentType:p,cancelled:!0,durationMs:Date.now()-s});if(!t.success)return t;const d=r!==void 0&&t.data.failureAnalysis===void 0?{...t.data,failureAnalysis:r}:t.data;return u({...d,...d.durationMs===void 0?{durationMs:Date.now()-s}:{}})}export{N as deploy};
|
|
1
|
+
import{success as u,failure as y}from"@fjall/generator";import{isApplicationOperation as f}from"../types/operations.js";import{isAborted as w}from"../aws/organisations/types.js";import{checkRegionEnabled as b,regionDisabledMessage as A}from"../aws/regions/regionOptStatus.js";import{createDeployServices as k}from"./serviceFactory.js";import{resolveOperation as m}from"./resolveOperation.js";import{deployApplication as D}from"./application/applicationDeploy.js";import{deployOrganisation as v}from"./organisation/organisationDeploy.js";async function N(e){const s=Date.now(),l=await b(e.awsCredentials.region,e.awsCredentials,e.abortSignal);if(l==="disabled"){const i=new Error(A(e.awsCredentials.region));return e.callbacks.onError?.(i),y(i)}l==="enabling"&&e.callbacks.onProgress?.({type:"warning",message:`Region ${e.awsCredentials.region} is still activating (opt-in status ENABLING); the deploy may fail until activation completes.`});let r;const g=e.callbacks.onFailureAnalysis,a={...e,callbacks:{...e.callbacks,onFailureAnalysis:i=>{r=i,g?.(i)}}},c=k(a),o=await m(e.target,e.workingDirectory);if(!o.success)return e.callbacks.onError?.(o.error),o;const n=o.data,p=f(n)?"application":"organisation",t=f(n)?await D(a,c,n):await v(a,c,n);if(w(e.abortSignal))return u({target:e.target,deploymentType:p,cancelled:!0,artefacts:[],durationMs:Date.now()-s});if(!t.success)return t;const d=r!==void 0&&t.data.failureAnalysis===void 0?{...t.data,failureAnalysis:r}:t.data;return u({...d,...d.durationMs===void 0?{durationMs:Date.now()-s}:{}})}export{N as deploy};
|
|
@@ -104,8 +104,11 @@ export interface ECRInitResult {
|
|
|
104
104
|
}
|
|
105
105
|
/**
|
|
106
106
|
* Parameters for tagging existing ECR images for ECS services.
|
|
107
|
-
* Used
|
|
108
|
-
*
|
|
107
|
+
* Used on the non-Dockerfile welcome-image path: a create-once,
|
|
108
|
+
* identical-manifest copy of the seeded `:latest` image to
|
|
109
|
+
* `:<serviceName>-latest`, safe on IMMUTABLE repositories. Those tags are
|
|
110
|
+
* read only by stacks synthesised before 2026-05-13 — newer stacks default
|
|
111
|
+
* their ImageTag parameter to `:latest`.
|
|
109
112
|
*/
|
|
110
113
|
export interface TagImagesParams {
|
|
111
114
|
appName: string;
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
export { deploy } from "./deploy.js";
|
|
2
2
|
export { destroy } from "./destroy.js";
|
|
3
|
+
export { restart, probeSecretsDrift } from "./restart/restartApplication.js";
|
|
4
|
+
export type { RestartParams, RestartResult, SecretsDriftProbeParams } from "./restart/restartApplication.js";
|
|
5
|
+
export { computeSecretsDrift, deriveStaleParameters, extractConsumedSsmParameters, ssmParameterNameFromValueFrom } from "./restart/secretsDrift.js";
|
|
6
|
+
export type { ApplicationSecretsDrift, ServiceSecretsDrift, StaleParameter, SecretsDriftServices } from "./restart/secretsDrift.js";
|
|
3
7
|
export { deployOrganisation } from "./organisation/organisationDeploy.js";
|
|
4
8
|
export { destroyOrganisation } from "./organisation/organisationDestroy.js";
|
|
5
9
|
export { cleanupFailedStack, emptyS3Bucket, preEmptyStackBuckets, formatQuarantineSuspectedMessage, formatRetainedBucketsMessage, isQuarantineDetail, isRetainedBucketsDetail, PRE_EMPTY_TAG_KEYS, isCleanableState, SAFE_CLEANUP_STATES } from "./stackCleanup.js";
|
|
@@ -32,6 +36,7 @@ export type { AccountsConfig } from "./organisation/accountsConfig.js";
|
|
|
32
36
|
export type { DockerProvider, DockerProgressCallback, DockerServiceConfig, DockerBuildParams, DockerBuildResult, ResolvedBuildSecret, ECRInitParams, ECRInitResult, TagImagesParams, TagImagesResult, TagByDigestParams } from "./dockerInterface.js";
|
|
33
37
|
export { resolveBuildSecrets, resolveSecretRefValue, resolveBuildSecretSessionProvider, sourcedRefsFromBuildArgs, type ResolvedBuildSecrets } from "./application/buildSecretResolver.js";
|
|
34
38
|
export { buildBuildSecretSessionPolicy, partitionForRegion, type BuildSecretRef, type BuildSecretSessionContext } from "./application/buildSecretSession.js";
|
|
39
|
+
export { buildDeploySessionPolicy, type DeploySessionContext } from "./application/deploySessionPolicy.js";
|
|
35
40
|
export { resolveBuildArgs, type ResolveBuildArgsContext, type ResolveBuildArgsResult, type SourcedBuildArgResolver } from "./application/buildArgResolver.js";
|
|
36
41
|
export { validateBuildGroup, type ValidateBuildGroupInput, type BuildGroupMemberInputs } from "./application/buildGroupValidator.js";
|
|
37
42
|
export type { DomainDeployProvider, DomainConfig, DomainDeployResult } from "./domainInterface.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{deploy as
|
|
1
|
+
import{deploy as o}from"./deploy.js";import{destroy as i}from"./destroy.js";import{restart as a,probeSecretsDrift as n}from"./restart/restartApplication.js";import{computeSecretsDrift as l,deriveStaleParameters as m,extractConsumedSsmParameters as u,ssmParameterNameFromValueFrom as p}from"./restart/secretsDrift.js";import{deployOrganisation as d}from"./organisation/organisationDeploy.js";import{destroyOrganisation as A}from"./organisation/organisationDestroy.js";import{cleanupFailedStack as T,emptyS3Bucket as P,preEmptyStackBuckets as _,formatQuarantineSuspectedMessage as y,formatRetainedBucketsMessage as g,isQuarantineDetail as E,isRetainedBucketsDetail as R,PRE_EMPTY_TAG_KEYS as B,isCleanableState as O,SAFE_CLEANUP_STATES as k}from"./stackCleanup.js";import{partitionAccounts as U,buildRegionList as C,buildAccountRegionPairs as L,cascadeHomeRegion as D,cascadeOperationKey as K}from"./organisation/cascadeHelpers.js";import{projectScalarSummary as b,projectAccountRows as F}from"./organisation/cascadeSummary.js";import{reconcileProviderAccounts as M,mergeReconciledProviderAccounts as G}from"./organisation/reconcileProviderAccounts.js";import{decideNextTransition as Q,reconcileTrailMigration as V,ORG_TRAIL_BUCKET_OUTPUT_KEY as h,TRAIL_BUCKET_OUTPUT_KEY as H,TRAIL_KEY_ARN_OUTPUT_KEY as j}from"./trailMigration/trailMigration.js";import{decommissionMemberTrailStorage as W}from"./trailMigration/memberTrailCleanup.js";import{unlockBucket as z}from"./unlock/unlockBucket.js";import{unlockQueue as X}from"./unlock/unlockQueue.js";import{triageBucketPolicy as $,isEnforceSslStatement as ee}from"./unlock/bucketPolicyTriage.js";import{toResourcePolicyStatements as oe}from"./unlock/toResourcePolicyStatements.js";import{restoreBucketPolicy as ie,synthesiseEnforceSslDocument as se,ensureEnforceSsl as ae}from"./unlock/restoreBucketPolicy.js";import{restoreAndReconcileQuarantinedBucket as ce}from"./unlock/restoreAndReconcileQuarantinedBucket.js";import{parseAccountsConfiguration as me,flattenAccountsToEnvironments as ue,extractAllAccountNames as pe,accountsConfigToOUTree as fe,isStringArray as de,isAccountsConfig as Se,isOuOnlyAccountBucket as Ae,OU_ONLY_ACCOUNT_BUCKETS as xe}from"./organisation/accountsConfig.js";import{resolveBuildSecrets as Pe,resolveSecretRefValue as _e,resolveBuildSecretSessionProvider as ye,sourcedRefsFromBuildArgs as ge}from"./application/buildSecretResolver.js";import{buildBuildSecretSessionPolicy as Re,partitionForRegion as Be}from"./application/buildSecretSession.js";import{buildDeploySessionPolicy as ke}from"./application/deploySessionPolicy.js";import{resolveBuildArgs as Ue}from"./application/buildArgResolver.js";import{validateBuildGroup as Le}from"./application/buildGroupValidator.js";import{runOpenNextBuild as Ke}from"./application/openNextBuild.js";import{runOrganisationSetup as be,ORG_SETUP_PHASES as Fe}from"./organisation/organisationSetup.js";export*from"./builders/index.js";import{buildDeployPlan as Ge,computeDeployPlan as Ie,computeAssemblyDigest as Qe,digestHead as Ve,classifyImpact as he,derivePropertyChanges as He,isDataLoss as je,isStatefulResourceType as we,renderPlanSummary as We,renderPlanLines as qe,toWirePlanChanges as ze,signApprovalToken as Je,verifyApprovalToken as Xe,DEFAULT_APPROVAL_TTL_MS as Ze,APPROVAL_TOKEN_PATTERN as $e}from"./application/plan/index.js";import{runApprovalGate as rr}from"./application/approvalGate.js";export{$e as APPROVAL_TOKEN_PATTERN,Ze as DEFAULT_APPROVAL_TTL_MS,Fe as ORG_SETUP_PHASES,h as ORG_TRAIL_BUCKET_OUTPUT_KEY,xe as OU_ONLY_ACCOUNT_BUCKETS,B as PRE_EMPTY_TAG_KEYS,k as SAFE_CLEANUP_STATES,H as TRAIL_BUCKET_OUTPUT_KEY,j as TRAIL_KEY_ARN_OUTPUT_KEY,fe as accountsConfigToOUTree,L as buildAccountRegionPairs,Re as buildBuildSecretSessionPolicy,Ge as buildDeployPlan,ke as buildDeploySessionPolicy,C as buildRegionList,D as cascadeHomeRegion,K as cascadeOperationKey,he as classifyImpact,T as cleanupFailedStack,Qe as computeAssemblyDigest,Ie as computeDeployPlan,l as computeSecretsDrift,Q as decideNextTransition,W as decommissionMemberTrailStorage,o as deploy,d as deployOrganisation,He as derivePropertyChanges,m as deriveStaleParameters,i as destroy,A as destroyOrganisation,Ve as digestHead,P as emptyS3Bucket,ae as ensureEnforceSsl,pe as extractAllAccountNames,u as extractConsumedSsmParameters,ue as flattenAccountsToEnvironments,y as formatQuarantineSuspectedMessage,g as formatRetainedBucketsMessage,Se as isAccountsConfig,O as isCleanableState,je as isDataLoss,ee as isEnforceSslStatement,Ae as isOuOnlyAccountBucket,E as isQuarantineDetail,R as isRetainedBucketsDetail,we as isStatefulResourceType,de as isStringArray,G as mergeReconciledProviderAccounts,me as parseAccountsConfiguration,U as partitionAccounts,Be as partitionForRegion,_ as preEmptyStackBuckets,n as probeSecretsDrift,F as projectAccountRows,b as projectScalarSummary,M as reconcileProviderAccounts,V as reconcileTrailMigration,qe as renderPlanLines,We as renderPlanSummary,Ue as resolveBuildArgs,ye as resolveBuildSecretSessionProvider,Pe as resolveBuildSecrets,_e as resolveSecretRefValue,a as restart,ce as restoreAndReconcileQuarantinedBucket,ie as restoreBucketPolicy,rr as runApprovalGate,Ke as runOpenNextBuild,be as runOrganisationSetup,Je as signApprovalToken,ge as sourcedRefsFromBuildArgs,p as ssmParameterNameFromValueFrom,se as synthesiseEnforceSslDocument,oe as toResourcePolicyStatements,ze as toWirePlanChanges,$ as triageBucketPolicy,z as unlockBucket,X as unlockQueue,Le as validateBuildGroup,Xe as verifyApprovalToken};
|
|
@@ -2,4 +2,4 @@ import{join as W}from"node:path";import{success as _,failure as Y}from"@fjall/ge
|
|
|
2
2
|
`);e.onLog?.(l(`Cascade failed for ${i.length} target(s):
|
|
3
3
|
${r}`),"warn")}}if(i.length>0){const o=i.map(a=>l(`${a.accountId}: ${a.error}`)).join(`
|
|
4
4
|
`),r=new Error(`Organisation root deployed, but the cascade failed for ${i.length} target(s):
|
|
5
|
-
${o}`);return e.onError?.(r),Y(r)}return _({target:O.target,deploymentType:"organisation",outputs:w,...y.length>0?{cascadeOutputs:y}:{},...T?{}:{noChanges:!0},durationMs:Date.now()-H})}export{ge as deployOrgWithCascade};
|
|
5
|
+
${o}`);return e.onError?.(r),Y(r)}return _({target:O.target,deploymentType:"organisation",outputs:w,artefacts:[],...y.length>0?{cascadeOutputs:y}:{},...T?{}:{noChanges:!0},durationMs:Date.now()-H})}export{ge as deployOrgWithCascade};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{success as N}from"@fjall/generator";import{BackupClient as w}from"@aws-sdk/client-backup";import{IAMClient as D}from"@aws-sdk/client-iam";import{getOrganisationStackName as I}from"../../types/operations.js";import{accountHasDisasterRecovery as k,describeBackupVaultExists as y}from"../../aws/organisations/backup.js";import{describeAccountGlobalsExist as L,buildMissingAccountGlobalsAdvisory as T}from"../../aws/organisations/accountGlobals.js";import{targetsNonPrimaryRegion as h,synthOrFail as v,bootstrapOrFail as F,forwardOutput as M,forwardResourceProgress as G}from"../contextHelpers.js";import{accountTier as S}from"@fjall/util";import{buildOrgContext as Y,resolveOrgDetails as x,resolveOrgDetailsForSolo as B}from"./orgContext.js";import{hasOrganisationTierAccount as V}from"../../aws/organisations/accountGlobals.js";import{INFRA_STEPS as o,INFRA_STEP_TOTAL as i,failPrepareStep as c,maskAndFail as d,readStackOutputsBestEffort as _}from"./infraSteps.js";async function ot(n,
|
|
1
|
+
import{success as N}from"@fjall/generator";import{BackupClient as w}from"@aws-sdk/client-backup";import{IAMClient as D}from"@aws-sdk/client-iam";import{getOrganisationStackName as I}from"../../types/operations.js";import{accountHasDisasterRecovery as k,describeBackupVaultExists as y}from"../../aws/organisations/backup.js";import{describeAccountGlobalsExist as L,buildMissingAccountGlobalsAdvisory as T}from"../../aws/organisations/accountGlobals.js";import{targetsNonPrimaryRegion as h,synthOrFail as v,bootstrapOrFail as F,forwardOutput as M,forwardResourceProgress as G}from"../contextHelpers.js";import{accountTier as S}from"@fjall/util";import{buildOrgContext as Y,resolveOrgDetails as x,resolveOrgDetailsForSolo as B}from"./orgContext.js";import{hasOrganisationTierAccount as V}from"../../aws/organisations/accountGlobals.js";import{INFRA_STEPS as o,INFRA_STEP_TOTAL as i,failPrepareStep as c,maskAndFail as d,readStackOutputsBestEffort as _}from"./infraSteps.js";async function ot(r,n,a,s,E){const{callbacks:t}=r;t.onStepComplete?.(o.CONNECT.id,o.CONNECT.name,"completed",0,i),t.onStepStart?.(o.PREPARE.id,o.PREPARE.name,1,i);const R=n.awsProvider.getRegion(),l=r.orgConfig?.primaryRegion;if(l!==void 0&&h(R,l)){const e=await L(n.awsProvider.getClient(D),r.abortSignal);if(!e.success)return c(t),d(t,e.error.message);if(!e.data)return c(t),d(t,T({target:a.target,deployRegion:R,primaryRegion:l,...r.orgConfig!==void 0?{providerAccounts:r.orgConfig.providerAccounts}:{}}))}let f;if(V(r.orgConfig?.providerAccounts)){const e=await x(n,r.abortSignal);if(!e.success)return c(t),d(t,e.error.message);f=e.data}else f=await B(n,r.abortSignal);const u=s==="account"?r.orgConfig?.providerAccounts.find(e=>e.name===a.target):r.orgConfig?.providerAccounts.find(e=>S(e)==="platform"),A=s==="account"&&u!==void 0&&S(u)==="organisation",g=Y(r,n,a,s,f,s==="account"?a.target:void 0,u?.trailLifecycle,A),m=r.orgConfig?.disasterRecoveryRegion;if(s==="account"&&(m!==void 0&&m!=="")&&(u===void 0||k(u.environment,m))){const e=await y(n.awsProvider.getClient(w),r.abortSignal);if(!e.success)return c(t),d(t,e.error.message);g.fjallAdoptBackupVault=e.data}t.onLog?.(`Synthesising ${s} infrastructure\u2026`,"info");const p=await v(n,g,t,"CDK synthesis failed");if(!p.success)return c(t),p;const O=await F(n,g,t);if(!O.success)return c(t),O;t.onStepComplete?.(o.PREPARE.id,o.PREPARE.name,"completed",1,i);const P=I(a.type);t.onStepStart?.(o.DEPLOY.id,o.DEPLOY.name,2,i);const C=await n.cdkService.runCdkDeploy(g,P,M(t),G(t),n.awsProvider);if(!C.success)return t.onStepComplete?.(o.DEPLOY.id,o.DEPLOY.name,"error",2,i),d(t,C.error);t.onStepComplete?.(o.DEPLOY.id,o.DEPLOY.name,"completed",2,i);const b=await _(n,t,P,"Failed to read stack outputs (non-critical)");return t.onStepStart?.(o.MONITORING.id,o.MONITORING.name,3,i),t.onStepComplete?.(o.MONITORING.id,o.MONITORING.name,"completed",3,i),N({target:a.target,deploymentType:"organisation",outputs:b,artefacts:[],durationMs:Date.now()-E})}export{ot as deploySingleComponent};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { type Result } from "@fjall/generator";
|
|
2
|
+
import { type ServiceArtefact } from "@fjall/util";
|
|
3
|
+
import type { AwsCredentials } from "../../types/credentials.js";
|
|
4
|
+
import type { DeployCallbacks } from "../../types/callbacks.js";
|
|
5
|
+
import { EcsService } from "../../services/infrastructure/EcsService.js";
|
|
6
|
+
import { EcsServiceResolver } from "../../services/infrastructure/EcsServiceResolver.js";
|
|
7
|
+
import { EcrImageInspectorService } from "../../services/infrastructure/EcrImageInspectorService.js";
|
|
8
|
+
import { SsmParameterMetadataService } from "../../services/infrastructure/SsmParameterMetadataService.js";
|
|
9
|
+
import { type ApplicationSecretsDrift } from "./secretsDrift.js";
|
|
10
|
+
export interface RestartParams {
|
|
11
|
+
/** Application name. */
|
|
12
|
+
target: string;
|
|
13
|
+
awsCredentials: AwsCredentials;
|
|
14
|
+
callbacks: DeployCallbacks;
|
|
15
|
+
/**
|
|
16
|
+
* Pre-resolved ECS targets (the webapp worker knows them from the DB).
|
|
17
|
+
* When omitted, services are discovered from CloudFormation exports.
|
|
18
|
+
*/
|
|
19
|
+
knownServices?: {
|
|
20
|
+
clusterArn: string;
|
|
21
|
+
serviceArns: string[];
|
|
22
|
+
};
|
|
23
|
+
/** Restrict the restart to a single ECS service by name (case-insensitive). */
|
|
24
|
+
serviceName?: string;
|
|
25
|
+
abortSignal?: AbortSignal;
|
|
26
|
+
}
|
|
27
|
+
export interface RestartResult {
|
|
28
|
+
target: string;
|
|
29
|
+
/**
|
|
30
|
+
* Per-service image identity of what is now running — the same images as
|
|
31
|
+
* before the restart, re-registered under a fresh task-definition revision.
|
|
32
|
+
* A service whose image tag could not be recovered (untagged digest) is
|
|
33
|
+
* omitted, with a warning.
|
|
34
|
+
*/
|
|
35
|
+
artefacts: ServiceArtefact[];
|
|
36
|
+
/**
|
|
37
|
+
* Version of each consumed SSM parameter at restart time — what the
|
|
38
|
+
* relaunched tasks read. Recorded onto the restart Release.
|
|
39
|
+
*/
|
|
40
|
+
appliedParameterVersions: Record<string, number>;
|
|
41
|
+
/** Drift state observed BEFORE the restart (what this restart healed). */
|
|
42
|
+
preRestartDrift?: ApplicationSecretsDrift;
|
|
43
|
+
durationMs: number;
|
|
44
|
+
cancelled?: boolean;
|
|
45
|
+
}
|
|
46
|
+
export interface RestartServices {
|
|
47
|
+
ecsService: EcsService;
|
|
48
|
+
ecsResolver: EcsServiceResolver;
|
|
49
|
+
ecrImageInspector: EcrImageInspectorService;
|
|
50
|
+
ssmService: SsmParameterMetadataService;
|
|
51
|
+
}
|
|
52
|
+
export declare function createRestartServices(awsCredentials: AwsCredentials): RestartServices;
|
|
53
|
+
export type SecretsDriftProbeParams = Omit<RestartParams, "callbacks">;
|
|
54
|
+
/**
|
|
55
|
+
* Read-only secrets-drift probe over an application's live ECS services —
|
|
56
|
+
* the same resolution ladder as `restart()` without rolling anything.
|
|
57
|
+
* Used by the CLI's pre-confirm drift display and the webapp's drift pulse.
|
|
58
|
+
*/
|
|
59
|
+
export declare function probeSecretsDrift(params: SecretsDriftProbeParams, injectedServices?: RestartServices): Promise<Result<ApplicationSecretsDrift, Error>>;
|
|
60
|
+
/**
|
|
61
|
+
* Restart an application's ECS services without building or deploying:
|
|
62
|
+
* re-register each service's live task definition unchanged and roll the
|
|
63
|
+
* service onto the new revision. Relaunched tasks re-resolve their SSM
|
|
64
|
+
* `valueFrom` secrets at start-up — this is the lightweight rollout primitive
|
|
65
|
+
* that makes a secret rotation take effect (R2). The revision bump is what
|
|
66
|
+
* lets the secrets-drift probe self-heal: drift compares parameter
|
|
67
|
+
* modification time against task-definition registration time.
|
|
68
|
+
*/
|
|
69
|
+
export declare function restart(params: RestartParams, injectedServices?: RestartServices): Promise<Result<RestartResult>>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{success as $,failure as g}from"@fjall/generator";import{CONTENT_HASH_TAG_PATTERN as b,maskSensitiveOutput as R}from"@fjall/util";import{logger as x}from"@fjall/util/logger";import{SimpleAwsProvider as O}from"../../aws/SimpleAwsProvider.js";import{CloudFormationService as L}from"../../services/infrastructure/CloudFormationService.js";import{EcsService as P}from"../../services/infrastructure/EcsService.js";import{EcsServiceResolver as M}from"../../services/infrastructure/EcsServiceResolver.js";import{EcrImageInspectorService as _,stripTagOrDigest as G}from"../../services/infrastructure/EcrImageInspectorService.js";import{SsmParameterMetadataService as F}from"../../services/infrastructure/SsmParameterMetadataService.js";import{extractEcsServiceName as k}from"../../aws/utils/arnParser.js";import{isAborted as H}from"../../aws/organisations/types.js";import{matchesServiceName as U,resolveServiceTaskDefinition as V}from"../application/serviceImageTagsRollback.js";import{deriveEcrRepositoryArn as j}from"../application/codeOnlyDeploy.js";import{registerAndRollTaskDefinition as B}from"../application/taskDefinitionRoll.js";import{computeSecretsDrift as y}from"./secretsDrift.js";const Y="restartApplication";function h(n){const e=new O(n),i=new L(e);return{ecsService:new P(e),ecsResolver:new M(i),ecrImageInspector:new _(e),ssmService:new F(e)}}async function C(n,e){const{target:i}=e;let r,s;if(e.knownServices!==void 0)r=e.knownServices.clusterArn,s=e.knownServices.serviceArns;else{const t=await n.getDeployableClusterAndServices(i);if(!t.success)return g(new Error(`Failed to discover ECS services for ${i}: ${R(t.error.message)}`));r=t.data.clusterArn,s=t.data.serviceArns}if(r===void 0||s.length===0)return g(new Error(`No running ECS services found for ${i} \u2014 nothing to restart. Deploy the application first.`));if(e.serviceName!==void 0){const t=s.filter(o=>{const a=k(o);return a!==void 0&&e.serviceName!==void 0?U(a,e.serviceName):!1});if(t.length===0){const o=s.map(a=>k(a)).filter(a=>a!==void 0).join(", ");return g(new Error(`No ECS service matching "${e.serviceName}" found for ${i}. Live services: ${o||"none"}`))}s=t}return $({clusterArn:r,serviceArns:s})}async function ce(n,e){const{ecsService:i,ecsResolver:r,ssmService:s}=e??h(n.awsCredentials),t=await C(r,n);return t.success?y({ecsService:i,ssmService:s},t.data,n.abortSignal):g(t.error)}async function fe(n,e){const{target:i,callbacks:r,abortSignal:s}=n,t=Date.now(),{ecsService:o,ecsResolver:a,ecrImageInspector:S,ssmService:u}=e??h(n.awsCredentials),f=await C(a,n);if(!f.success)return r.onError?.(f.error),g(f.error);const{clusterArn:w,serviceArns:c}=f.data,d=await y({ecsService:o,ssmService:u},{clusterArn:w,serviceArns:c},s);let l,E={};d.success?(l=d.data,E=d.data.parameterVersions):r.onLog?.(`Could not compute secrets drift before restart: ${R(d.error.message)}. Restarting anyway.`,"warn");const A=[];for(const D of c){if(H(s))return $({target:i,artefacts:A,appliedParameterVersions:E,...l!==void 0?{preRestartDrift:l}:{},durationMs:Date.now()-t,cancelled:!0});const I=Date.now(),m=k(D)??D;r.onLog?.(`Restarting ${m}\u2026`,"info");const v=await V({ecsService:o},w,D,m);if(!v.success){const p=new Error(`Restart failed for ${m}: ${R(v.error.message)}`);return r.onError?.(p),g(p)}const T=await B({ecsService:o},{clusterArn:w,serviceArn:D,serviceName:m,taskDefinition:v.data.taskDefinition,containerDefinitions:v.data.containerDefinitions,previousTaskDefArn:v.data.previousTaskDefArn,startedAtMs:I},r);if(!T.success){const p=new Error(`Restart failed for ${m}: ${R(T.error.message)}`);return r.onError?.(p),g(p)}const N=await q(S,m,v.data.containerDefinitions,T.data.newTaskDefArn,v.data.previousTaskDefArn,s);N!==void 0?A.push(N):r.onLog?.(`Restarted ${m}, but its image tag could not be recovered \u2014 the restart release will omit this service's artefact.`,"warn")}return x.debug(Y,`Restarted ${i}`,{services:c.length,artefacts:A.length}),$({target:i,artefacts:A,appliedParameterVersions:E,...l!==void 0?{preRestartDrift:l}:{},durationMs:Date.now()-t})}async function q(n,e,i,r,s,t){const o=i.find(c=>c.image)?.image;if(o===void 0)return;const a=G(o),S=j(a);let u,f;const w=o.lastIndexOf("@");if(w>0){f=o.slice(w+1);const c=await n.getImageTagsByDigest(a,f,t);if(c.success&&c.data!==void 0){const d=c.data;u=d.find(l=>b.test(l))??d[0]}}else{const c=o.lastIndexOf(":");if(c>0){u=o.slice(c+1);const d=await n.getImageDigest(a,u,t);d.success&&(f=d.data)}}if(!(u===void 0||u===""))return{serviceName:e,imageTag:u,imageUri:`${a}:${u}`,taskDefinitionArn:r,previousTaskDefinitionArn:s,...f!==void 0?{imageDigest:f}:{},...S!==""?{ecrRepositoryArn:S}:{}}}export{h as createRestartServices,ce as probeSecretsDrift,fe as restart};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type Result } from "@fjall/generator";
|
|
2
|
+
import type { TaskDefinition } from "@aws-sdk/client-ecs";
|
|
3
|
+
import type { EcsService } from "../../services/infrastructure/EcsService.js";
|
|
4
|
+
import type { SsmParameterMetadata, SsmParameterMetadataService } from "../../services/infrastructure/SsmParameterMetadataService.js";
|
|
5
|
+
export interface StaleParameter {
|
|
6
|
+
name: string;
|
|
7
|
+
lastModifiedDate: string;
|
|
8
|
+
version?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface ServiceSecretsDrift {
|
|
11
|
+
serviceName: string;
|
|
12
|
+
serviceArn: string;
|
|
13
|
+
taskDefinitionArn?: string;
|
|
14
|
+
taskDefRegisteredAt?: string;
|
|
15
|
+
/** SSM parameter names the live task definition injects (`secrets[].valueFrom`). */
|
|
16
|
+
consumedParameters: string[];
|
|
17
|
+
stale: boolean;
|
|
18
|
+
staleParameters: StaleParameter[];
|
|
19
|
+
}
|
|
20
|
+
export interface ApplicationSecretsDrift {
|
|
21
|
+
services: ServiceSecretsDrift[];
|
|
22
|
+
anyStale: boolean;
|
|
23
|
+
/** Latest known Version per consumed parameter name, across all services. */
|
|
24
|
+
parameterVersions: Record<string, number>;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Normalise an ECS `secrets[].valueFrom` reference to an SSM parameter name.
|
|
28
|
+
* Returns undefined for non-SSM references (Secrets Manager ARNs) — those are
|
|
29
|
+
* resolved by ECS from a different store and are out of drift scope.
|
|
30
|
+
*/
|
|
31
|
+
export declare function ssmParameterNameFromValueFrom(valueFrom: string): string | undefined;
|
|
32
|
+
/** Collect the unique SSM parameter names a task definition injects. */
|
|
33
|
+
export declare function extractConsumedSsmParameters(taskDefinition: TaskDefinition): string[];
|
|
34
|
+
/**
|
|
35
|
+
* Pure staleness comparator: a parameter is stale for a service when it was
|
|
36
|
+
* modified AFTER the service's live task definition was registered — running
|
|
37
|
+
* tasks resolved the old value at launch and nothing has rolled them since.
|
|
38
|
+
* Self-heals on any roll (fresh registeredAt) and catches out-of-band writes
|
|
39
|
+
* (webapp, console, another machine) because the inputs are AWS ground truth.
|
|
40
|
+
*/
|
|
41
|
+
export declare function deriveStaleParameters(taskDefRegisteredAt: Date | undefined, consumedNames: string[], parameters: readonly SsmParameterMetadata[]): StaleParameter[];
|
|
42
|
+
export interface SecretsDriftServices {
|
|
43
|
+
ecsService: Pick<EcsService, "describeServices" | "getLatestTaskDefinition">;
|
|
44
|
+
ssmService: Pick<SsmParameterMetadataService, "describeParametersByName">;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Compute per-service secrets drift for an application's live ECS services.
|
|
48
|
+
* Fails only when live state cannot be read at all; a service with no
|
|
49
|
+
* consumed SSM parameters reports clean.
|
|
50
|
+
*/
|
|
51
|
+
export declare function computeSecretsDrift(services: SecretsDriftServices, target: {
|
|
52
|
+
clusterArn: string;
|
|
53
|
+
serviceArns: string[];
|
|
54
|
+
}, abortSignal?: AbortSignal): Promise<Result<ApplicationSecretsDrift, Error>>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{success as g,failure as S}from"@fjall/generator";import{extractEcsServiceName as D}from"../../aws/utils/arnParser.js";import{isAborted as h}from"../../aws/organisations/types.js";const N=/^arn:aws[a-z-]*:ssm:[a-z0-9-]*:\d{12}:parameter/;function k(t){if(t.startsWith("arn:")){const n=t.match(N);if(!n)return;const r=t.slice(n[0].length);return r.startsWith("/")?r:`/${r}`}return t}function w(t){const n=new Set;for(const r of t.containerDefinitions??[])for(const a of r.secrets??[]){if(a.valueFrom===void 0)continue;const s=k(a.valueFrom);s!==void 0&&n.add(s)}return[...n].sort()}function M(t,n,r){if(t===void 0)return[];const a=new Map(r.map(o=>[o.name,o])),s=[];for(const o of n){const i=a.get(o);i?.lastModifiedDate!==void 0&&i.lastModifiedDate.getTime()>t.getTime()&&s.push({name:o,lastModifiedDate:i.lastModifiedDate.toISOString(),...i.version!==void 0?{version:i.version}:{}})}return s}async function E(t,n,r){const a=await t.ecsService.describeServices(n.clusterArn,n.serviceArns);if(!a.success)return S(new Error(`Failed to describe services: ${a.error.message}`));const s=[],o=new Set;for(const e of a.data){if(h(r))break;const c=e.serviceArn??"",d=e.serviceName??(c!==""?D(c):void 0);if(d===void 0||d==="")continue;const m=e.taskDefinition;if(m===void 0){s.push({serviceName:d,serviceArn:c,consumed:[]});continue}const f=await t.ecsService.getLatestTaskDefinition(m);if(!f.success||f.data===void 0){s.push({serviceName:d,serviceArn:c,taskDefinitionArn:m,consumed:[]});continue}const l=w(f.data);l.forEach(v=>o.add(v)),s.push({serviceName:d,serviceArn:c,taskDefinitionArn:m,...f.data.registeredAt!==void 0?{registeredAt:f.data.registeredAt}:{},consumed:l})}const i=await t.ssmService.describeParametersByName([...o].sort(),r);if(!i.success)return S(i.error);const u=i.data,p={};for(const e of u)e.version!==void 0&&(p[e.name]=e.version);const A=s.map(e=>{const c=M(e.registeredAt,e.consumed,u);return{serviceName:e.serviceName,serviceArn:e.serviceArn,...e.taskDefinitionArn!==void 0?{taskDefinitionArn:e.taskDefinitionArn}:{},...e.registeredAt!==void 0?{taskDefRegisteredAt:e.registeredAt.toISOString()}:{},consumedParameters:e.consumed,stale:c.length>0,staleParameters:c}});return g({services:A,anyStale:A.some(e=>e.stale),parameterVersions:p})}export{E as computeSecretsDrift,M as deriveStaleParameters,w as extractConsumedSsmParameters,k as ssmParameterNameFromValueFrom};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{tmpdir as l}from"os";import{join as C}from"path";import{logger as m}from"@fjall/util/logger";import{success as
|
|
2
|
-
`);const r=
|
|
1
|
+
import{tmpdir as l}from"os";import{join as C}from"path";import{logger as m}from"@fjall/util/logger";import{success as p,failure as i}from"@fjall/generator";import{CdkError as f}from"../../types/errors/CdkError.js";import{DEFAULT_REGION as O}from"../../aws/utils/regions.js";import{CdkArgumentBuilder as E}from"./CdkArgumentBuilder.js";import{isCdkError as A,formatInfrastructureError as c}from"./CdkErrorFormatter.js";import{hasCdkDifferences as P,parseDiffOutput as _}from"./CdkOutputParser.js";import{DEFAULT_DEPLOY_TIMEOUT_MS as h}from"./CdkEventMonitoring.js";import{analyseBootstrapError as g}from"./CdkOutputAnalyser.js";const R=3e5,y=18e4,L=12e5,k="cdk.out",o=Object.freeze({APP:"--app",CI:"--ci",REQUIRE_APPROVAL:"--require-approval",APPROVAL_NEVER:"never",VERBOSE:"--verbose",NO_LOOKUPS:"--no-lookups",ALL:"--all"});class K{processManager;argumentBuilder=new E;constructor(u){this.processManager=u}dispose(){this.processManager.dispose()}async checkDifferences(u,s,r){const e=["diff"];s?e.push(s):e.push(o.ALL),e.push("--no-color"),r?.noLookups&&e.push(o.NO_LOOKUPS);const t=await this.processManager.runCdkCommand(u,e,{...r,ignoreExitCode:!0,combineOutput:!0});if(!t.success)return i(new f(c(t.error,u),"diff_failed",void 0,void 0,t.error,void 0,!1));const n=t.data.output||"";if(A(n))return i(new f(c(n,u),"diff_failed",void 0,void 0,n,void 0,!1));const a=P(n),d=_(n);return p({hasDifferences:a,output:n,details:d})}async deploy(u,s,r){const e=["deploy"];r?.appDir?e.push(o.APP,r.appDir):r?.useCdkOut&&e.push(o.APP,"cdk.out"),s?e.push(s):e.push(o.ALL),e.push(o.REQUIRE_APPROVAL,o.APPROVAL_NEVER),e.push(o.CI),e.push("--no-version-reporting"),e.push("--no-path-metadata"),e.push("--no-asset-metadata"),r?.verbose?e.push(o.VERBOSE):e.push("--progress","events"),e.push(...this.argumentBuilder.buildParameterArgs(r?.parameters,s));const t={...r,timeout:r?.timeout||h};return r?.passThroughCDK?this.processManager.runCdkCommandPassthrough(u,e,t):this.processManager.runCdkCommand(u,e,t)}async destroy(u,s,r){const e=["destroy"];r?.appDir?e.push(o.APP,r.appDir):r?.useCdkOut&&e.push(o.APP,"cdk.out"),s?e.push(s):e.push(o.ALL),e.push("--force"),r?.verbose&&e.push(o.VERBOSE);const t={...r,timeout:r?.timeout||h};return this.processManager.runCdkCommand(u,e,t)}async runImport(u,s,r){const e=["import"];r?.stacks&&r.stacks.length>0&&e.push(...r.stacks),s&&e.push("--resource-mapping",s),e.push(o.REQUIRE_APPROVAL,o.APPROVAL_NEVER),e.push(o.CI),r?.noLookups&&e.push(o.NO_LOOKUPS),r?.verbose&&(e.push(o.VERBOSE),e.push("--trace"),e.push("--debug"));const t={...r,timeout:r?.timeout||L};return r?.passThroughCDK?this.processManager.runCdkCommandPassthrough(u,e,t):this.processManager.runCdkCommand(u,e,t)}async synth(u,s){s?.outputCallback?.(`Synthesising CloudFormation template...
|
|
2
|
+
`);const r=s?.noLookups?[o.NO_LOOKUPS]:[],t=["--output",s?.outputDir??C(u,k)],n=s?.context?.region||O,a=await this.processManager.runCdkCommand(u,["synth",...r,...t,o.CI,"--quiet"],{...s,context:{...s?.context,region:n},timeout:s?.timeout||R});if(!a.success){const d=a.error?c(a.error,u):"Failed to synthesise CloudFormation template";return i(d)}return a}async bootstrap(u,s,r){const e=l();m.debug("CdkService","Starting CDK bootstrap",{accountId:u,region:s,bootstrapPath:e,target:`aws://${u}/${s}`});const t=await this.processManager.runCdkCommand(e,["bootstrap",`aws://${u}/${s}`,"--cloudformation-execution-policies","arn:aws:iam::aws:policy/AdministratorAccess",...r?.permissionsBoundary!==void 0&&r.permissionsBoundary!==""?["--custom-permissions-boundary",r.permissionsBoundary]:[],"-c",`accountId=${u}`,o.REQUIRE_APPROVAL,o.APPROVAL_NEVER,o.CI,"--quiet","--force"],{...r,timeout:r?.timeout||y,context:{region:s,accountId:u},credentials:r?.credentials,skipProjectCheck:!0,extraEnv:{TERM:"dumb",CDK_DISABLE_NOTICES:"true",CDK_DISABLE_PROGRESS_BAR:"true"}});return m.debug("CdkService",t.success?"Bootstrap completed successfully":"Bootstrap exited with non-zero code",{accountId:u,region:s,exitCode:t.success?t.data.exitCode:void 0,output:t.success&&t.data.output?.trim()||"(empty)",error:t.success?"(empty)":t.error.trim()||"(empty)"}),t.success?p({output:"AWS environment bootstrapped",exitCode:0}):t.error.includes("already bootstrapped")?p({output:"Environment is already bootstrapped",exitCode:0}):i(g(t.error))}}export{y as BOOTSTRAP_TIMEOUT_MS,K as CdkCommandRunner,L as IMPORT_TIMEOUT_MS,R as SYNTH_TIMEOUT_MS};
|
|
@@ -20,7 +20,7 @@ export declare class CdkService {
|
|
|
20
20
|
synth(path: string, options?: CdkOptions): Promise<Result<CdkOutput, string>>;
|
|
21
21
|
bootstrap(accountId: string, region: string, options?: CdkOptions): Promise<Result<CdkOutput, string>>;
|
|
22
22
|
runCdkSynth(context: DeploymentContext, onOutput?: (chunk: string) => void, credentials?: CdkOptions["credentials"]): Promise<Result<StepOutput, string>>;
|
|
23
|
-
runCdkBootstrap(context: DeploymentContext, onOutput?: (chunk: string) => void, credentials?: CdkOptions["credentials"]): Promise<Result<StepOutput, string>>;
|
|
23
|
+
runCdkBootstrap(context: DeploymentContext, onOutput?: (chunk: string) => void, credentials?: CdkOptions["credentials"], permissionsBoundary?: string): Promise<Result<StepOutput, string>>;
|
|
24
24
|
runCdkDiff(context: DeploymentContext, onOutput?: (chunk: string) => void): Promise<Result<StepOutput, string>>;
|
|
25
25
|
runCdkDeploy(context: DeploymentContext, stackPattern?: string, onOutput?: (chunk: string) => void, onResourceProgress?: (event: ResourceEvent) => void, aws?: AwsProvider, credentials?: CdkOptions["credentials"], parameters?: Record<string, string>): Promise<Result<StepOutput, string>>;
|
|
26
26
|
runCdkDestroy(context: DeploymentContext, stackPattern?: string, onOutput?: (chunk: string) => void, onResourceProgress?: (event: ResourceEvent) => void, aws?: AwsProvider, useCdkOut?: boolean, credentials?: CdkOptions["credentials"]): Promise<Result<StepOutput, string>>;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{existsSync as F}from"fs";import{join as D}from"path";import{logger as S}from"@fjall/util/logger";import{success as C,failure as a}from"@fjall/generator";import{DEFAULT_REGION as h}from"../../aws/utils/regions.js";import{getErrorMessage as k,maskSensitiveOutput as
|
|
1
|
+
import{existsSync as F}from"fs";import{join as D}from"path";import{logger as S}from"@fjall/util/logger";import{success as C,failure as a}from"@fjall/generator";import{DEFAULT_REGION as h}from"../../aws/utils/regions.js";import{getErrorMessage as k,maskSensitiveOutput as y}from"@fjall/util";import{CdkEventMonitor as R,startStackMonitoring as w}from"./CdkEventMonitoring.js";import{analyseDeployOutput as E,analyseDestroyResult as K,createEnhancedOutputCallback as N}from"./CdkOutputAnalyser.js";import{CdkCommandRunner as O}from"./CdkCommandRunner.js";import{CdkArgumentBuilder as T}from"./CdkArgumentBuilder.js";import{CdkProcessManager as L}from"./CdkProcessManager.js";import{cancelInFlightStack as P}from"./cancelInFlightStack.js";import{isAborted as v}from"../../aws/organisations/types.js";import{wrapWithConstructMapEnrichment as W}from"./constructMapEnrichment.js";import{STACK_DETECTION_FALLBACK_MS as $,resolveStackName as M,getFallbackStackName as _,buildDeploymentCdkContext as b}from"./cdkServiceHelpers.js";class te{commandRunner;eventMonitor;abortSignal;constructor(e){const r=e?.processManager??new L(new T,e?.abortSignal);this.commandRunner=new O(r),this.eventMonitor=new R({eventLogWriterFactory:e?.eventLogWriterFactory,...e?.onFailureAnalysis!==void 0?{onFailureAnalysis:e.onFailureAnalysis}:{}}),this.abortSignal=e?.abortSignal}dispose(){this.commandRunner.dispose()}async checkDifferences(e,r,t){return this.commandRunner.checkDifferences(e,r,t)}async deploy(e,r,t){return this.commandRunner.deploy(e,r,t)}async destroy(e,r,t){return this.commandRunner.destroy(e,r,t)}async runImport(e,r,t){return this.commandRunner.runImport(e,r,t)}async synth(e,r){return this.commandRunner.synth(e,r)}async bootstrap(e,r,t){return this.commandRunner.bootstrap(e,r,t)}async runCdkSynth(e,r,t){const n=e.callerIdentity?.Account;try{const o=await this.synth(e.path,{outputCallback:r,context:b(e,n,e.region||h),...e.assemblyDir!==void 0?{outputDir:e.assemblyDir}:{},...t!==void 0?{credentials:t}:{}});return o.success?C({message:"CloudFormation template synthesised",details:o.data.output?{synthesisTime:o.data.output}:void 0}):a(o.error||"Failed to synthesise CloudFormation template")}catch(o){return a(`CDK synth failed: ${y(k(o))}`)}}async runCdkBootstrap(e,r,t,n){const o=e.callerIdentity?.Account,f=e.region||h;try{if(!o)return a("No AWS account ID available");const c=D(e.path,"node_modules");if(!F(c))return a(`Dependencies not installed. Please run 'npm install' in ${e.path} before deploying.`);const l=await this.bootstrap(o,f,{outputCallback:r,credentials:t,...n!==void 0&&n!==""?{permissionsBoundary:n}:{}});return l.success?C({message:"AWS environment bootstrapped"}):a(l.error||"Failed to bootstrap AWS environment")}catch(c){return a(`CDK bootstrap failed: ${y(k(c))}`)}}async runCdkDiff(e,r){const t=e.callerIdentity?.Account;try{const n=await this.checkDifferences(e.path,void 0,{verbose:e.options?.verbose,outputCallback:r,context:b(e,t,e.region||h)});return n.success?C({message:"Diff check complete",details:{hasDifferences:n.data.hasDifferences,details:n.data.details}}):a(`CDK diff failed: ${n.error.message}`)}catch(n){return a(`CDK diff failed: ${y(k(n))}`)}}async runCdkDeploy(e,r,t,n,o,f,c){const l=e.callerIdentity?.Account,p=e.region||h;if(!l)return a("AWS account ID not available. Please ensure AWS credentials are properly configured.");if(!o)return a("AwsProvider is required for deployment monitoring.");const m=e.assemblyDir??D(e.path,"cdk.out"),d=W(m,n);let u=null,g;try{const i=M(r,e)??_(e);u=await this.eventMonitor.createEventMonitor("deploy",i,p,e,o),t&&(t(y(`Starting CloudFormation deployment of ${i}...
|
|
2
2
|
`)),t(`Monitoring CloudFormation events (CDK process running in background)...
|
|
3
|
-
`));const s={cdkOutput:"",actualStackName:i,stackDetected:!1,monitoringPromise:null},A=N(s,t,
|
|
3
|
+
`));const s={cdkOutput:"",actualStackName:i,stackDetected:!1,monitoringPromise:null},A=N(s,t,u,d);g=setTimeout(()=>{!s.stackDetected&&u&&!s.monitoringPromise&&(S.debug("CdkService","Fallback monitoring STARTING",{targetStackName:i,stackDetected:s.stackDetected,hasOnResourceProgress:!!n}),s.monitoringPromise=w(u,i,d))},$);const I=await this.deploy(e.path,i,{verbose:e.options?.verbose,outputCallback:A,...e.assemblyDir!==void 0?{appDir:e.assemblyDir}:{useCdkOut:!0},cdkOutputLogger:u?.getEventLogger()??void 0,context:b(e,l,p),credentials:f,...c!==void 0&&Object.keys(c).length>0&&{parameters:c}});return v(this.abortSignal)?(await P(i,p,f,t),a("Deployment cancelled")):E(s.cdkOutput,I,s.actualStackName)}catch(i){const s=`CDK deploy failed: ${y(k(i))}`;return S.error("CdkService","CDK deployment exception",{error:s}),a(s)}finally{clearTimeout(g),u&&u.stopMonitoring()}}async runCdkDestroy(e,r,t,n,o,f,c){const l=e.callerIdentity?.Account,p=e.region||h;let m=null;try{const d=M(r,e);l&&d&&o&&(m=await this.eventMonitor.createEventMonitor("destroy",d,p,e,o),m.startMonitoring(d,g=>{n?.(g)},(g,i)=>{}));const u=await this.destroy(e.path,r,{verbose:e.options?.verbose,outputCallback:t,useCdkOut:f,cdkOutputLogger:m?.getEventLogger()??void 0,context:b(e,l,p),credentials:c});return v(this.abortSignal)?a("Destroy cancelled"):K(u)}catch(d){return a(`CDK destroy failed: ${y(k(d))}`)}finally{m&&m.stopMonitoring()}}}export{te as CdkService};
|