@fjall/deploy-core 2.24.0 → 2.27.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.
Files changed (47) hide show
  1. package/dist/.minified +1 -1
  2. package/dist/src/aws/index.d.ts +1 -1
  3. package/dist/src/aws/index.js +1 -1
  4. package/dist/src/aws/organisations/index.d.ts +1 -1
  5. package/dist/src/aws/organisations/index.js +1 -1
  6. package/dist/src/aws/organisations/organisationalUnits.d.ts +15 -0
  7. package/dist/src/aws/organisations/organisationalUnits.js +1 -1
  8. package/dist/src/index.d.ts +4 -2
  9. package/dist/src/index.js +1 -1
  10. package/dist/src/orchestration/application/applicationDeploy.js +1 -1
  11. package/dist/src/orchestration/application/codeOnlyDeploy.d.ts +6 -0
  12. package/dist/src/orchestration/application/codeOnlyDeploy.js +1 -1
  13. package/dist/src/orchestration/application/databaseEndpointReconcile.d.ts +77 -0
  14. package/dist/src/orchestration/application/databaseEndpointReconcile.js +1 -0
  15. package/dist/src/orchestration/application/deploySessionPolicy.d.ts +9 -1
  16. package/dist/src/orchestration/application/deploySessionPolicy.js +1 -1
  17. package/dist/src/orchestration/application/dockerBuildHelper.js +1 -1
  18. package/dist/src/orchestration/application/ecrCacheRepository.d.ts +14 -0
  19. package/dist/src/orchestration/application/ecrCacheRepository.js +1 -0
  20. package/dist/src/orchestration/application/serviceImageTagsRollback.d.ts +56 -0
  21. package/dist/src/orchestration/application/serviceImageTagsRollback.js +1 -0
  22. package/dist/src/orchestration/application/taskDefinitionRoll.d.ts +33 -0
  23. package/dist/src/orchestration/application/taskDefinitionRoll.js +1 -0
  24. package/dist/src/orchestration/index.d.ts +4 -0
  25. package/dist/src/orchestration/index.js +1 -1
  26. package/dist/src/orchestration/organisationDeploy/orgCascadeDeploy.js +3 -3
  27. package/dist/src/orchestration/organisationDeploy/orgContext.d.ts +7 -0
  28. package/dist/src/orchestration/organisationDeploy/orgContext.js +1 -1
  29. package/dist/src/orchestration/restart/restartApplication.d.ts +69 -0
  30. package/dist/src/orchestration/restart/restartApplication.js +1 -0
  31. package/dist/src/orchestration/restart/secretsDrift.d.ts +54 -0
  32. package/dist/src/orchestration/restart/secretsDrift.js +1 -0
  33. package/dist/src/services/infrastructure/CdkArgumentBuilder.js +1 -1
  34. package/dist/src/services/infrastructure/CdkServiceTypes.d.ts +7 -0
  35. package/dist/src/services/infrastructure/CloudFormationService.d.ts +17 -0
  36. package/dist/src/services/infrastructure/CloudFormationService.js +1 -1
  37. package/dist/src/services/infrastructure/EcrImageInspectorService.d.ts +11 -1
  38. package/dist/src/services/infrastructure/EcrImageInspectorService.js +1 -1
  39. package/dist/src/services/infrastructure/SsmParameterMetadataService.d.ts +23 -0
  40. package/dist/src/services/infrastructure/SsmParameterMetadataService.js +1 -0
  41. package/dist/src/services/infrastructure/cdkServiceHelpers.js +1 -1
  42. package/dist/src/services/supporting/CdkContextBuilder.d.ts +1 -0
  43. package/dist/src/services/supporting/CdkContextBuilder.js +1 -1
  44. package/dist/src/types/deployment/DeploymentTypes.d.ts +1 -0
  45. package/dist/src/types/index.d.ts +1 -1
  46. package/dist/src/types/params.d.ts +27 -0
  47. package/package.json +4 -4
@@ -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,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";
@@ -1 +1 @@
1
- import{deploy as r}from"./deploy.js";import{destroy as i}from"./destroy.js";import{deployOrganisation as s}from"./organisation/organisationDeploy.js";import{destroyOrganisation as a}from"./organisation/organisationDestroy.js";import{cleanupFailedStack as u,emptyS3Bucket as p,preEmptyStackBuckets as m,formatQuarantineSuspectedMessage as f,formatRetainedBucketsMessage as d,isQuarantineDetail as A,isRetainedBucketsDetail as S,PRE_EMPTY_TAG_KEYS as T,isCleanableState as x,SAFE_CLEANUP_STATES as P}from"./stackCleanup.js";import{partitionAccounts as y,buildRegionList as g,buildAccountRegionPairs as E,cascadeHomeRegion as R,cascadeOperationKey as B}from"./organisation/cascadeHelpers.js";import{projectScalarSummary as k,projectAccountRows as U}from"./organisation/cascadeSummary.js";import{reconcileProviderAccounts as C,mergeReconciledProviderAccounts as L}from"./organisation/reconcileProviderAccounts.js";import{decideNextTransition as D,reconcileTrailMigration as N,ORG_TRAIL_BUCKET_OUTPUT_KEY as b,TRAIL_BUCKET_OUTPUT_KEY as Y,TRAIL_KEY_ARN_OUTPUT_KEY as M}from"./trailMigration/trailMigration.js";import{decommissionMemberTrailStorage as G}from"./trailMigration/memberTrailCleanup.js";import{unlockBucket as Q}from"./unlock/unlockBucket.js";import{unlockQueue as H}from"./unlock/unlockQueue.js";import{triageBucketPolicy as j,isEnforceSslStatement as w}from"./unlock/bucketPolicyTriage.js";import{toResourcePolicyStatements as q}from"./unlock/toResourcePolicyStatements.js";import{restoreBucketPolicy as J,synthesiseEnforceSslDocument as X,ensureEnforceSsl as Z}from"./unlock/restoreBucketPolicy.js";import{restoreAndReconcileQuarantinedBucket as ee}from"./unlock/restoreAndReconcileQuarantinedBucket.js";import{parseAccountsConfiguration as re,flattenAccountsToEnvironments as te,extractAllAccountNames as ie,accountsConfigToOUTree as ne,isStringArray as se,isAccountsConfig as ce,isOuOnlyAccountBucket as ae,OU_ONLY_ACCOUNT_BUCKETS as le}from"./organisation/accountsConfig.js";import{resolveBuildSecrets as pe,resolveSecretRefValue as me,resolveBuildSecretSessionProvider as fe,sourcedRefsFromBuildArgs as de}from"./application/buildSecretResolver.js";import{buildBuildSecretSessionPolicy as Se,partitionForRegion as Te}from"./application/buildSecretSession.js";import{buildDeploySessionPolicy as Pe}from"./application/deploySessionPolicy.js";import{resolveBuildArgs as ye}from"./application/buildArgResolver.js";import{validateBuildGroup as Ee}from"./application/buildGroupValidator.js";import{runOpenNextBuild as Be}from"./application/openNextBuild.js";import{runOrganisationSetup as ke,ORG_SETUP_PHASES as Ue}from"./organisation/organisationSetup.js";export*from"./builders/index.js";import{buildDeployPlan as Le,computeDeployPlan as Ke,computeAssemblyDigest as De,digestHead as Ne,classifyImpact as be,derivePropertyChanges as Ye,isDataLoss as Me,isStatefulResourceType as Fe,renderPlanSummary as Ge,renderPlanLines as Ie,toWirePlanChanges as Qe,signApprovalToken as he,verifyApprovalToken as He,DEFAULT_APPROVAL_TTL_MS as Ve,APPROVAL_TOKEN_PATTERN as je}from"./application/plan/index.js";import{runApprovalGate as We}from"./application/approvalGate.js";export{je as APPROVAL_TOKEN_PATTERN,Ve as DEFAULT_APPROVAL_TTL_MS,Ue as ORG_SETUP_PHASES,b as ORG_TRAIL_BUCKET_OUTPUT_KEY,le as OU_ONLY_ACCOUNT_BUCKETS,T as PRE_EMPTY_TAG_KEYS,P as SAFE_CLEANUP_STATES,Y as TRAIL_BUCKET_OUTPUT_KEY,M as TRAIL_KEY_ARN_OUTPUT_KEY,ne as accountsConfigToOUTree,E as buildAccountRegionPairs,Se as buildBuildSecretSessionPolicy,Le as buildDeployPlan,Pe as buildDeploySessionPolicy,g as buildRegionList,R as cascadeHomeRegion,B as cascadeOperationKey,be as classifyImpact,u as cleanupFailedStack,De as computeAssemblyDigest,Ke as computeDeployPlan,D as decideNextTransition,G as decommissionMemberTrailStorage,r as deploy,s as deployOrganisation,Ye as derivePropertyChanges,i as destroy,a as destroyOrganisation,Ne as digestHead,p as emptyS3Bucket,Z as ensureEnforceSsl,ie as extractAllAccountNames,te as flattenAccountsToEnvironments,f as formatQuarantineSuspectedMessage,d as formatRetainedBucketsMessage,ce as isAccountsConfig,x as isCleanableState,Me as isDataLoss,w as isEnforceSslStatement,ae as isOuOnlyAccountBucket,A as isQuarantineDetail,S as isRetainedBucketsDetail,Fe as isStatefulResourceType,se as isStringArray,L as mergeReconciledProviderAccounts,re as parseAccountsConfiguration,y as partitionAccounts,Te as partitionForRegion,m as preEmptyStackBuckets,U as projectAccountRows,k as projectScalarSummary,C as reconcileProviderAccounts,N as reconcileTrailMigration,Ie as renderPlanLines,Ge as renderPlanSummary,ye as resolveBuildArgs,fe as resolveBuildSecretSessionProvider,pe as resolveBuildSecrets,me as resolveSecretRefValue,ee as restoreAndReconcileQuarantinedBucket,J as restoreBucketPolicy,We as runApprovalGate,Be as runOpenNextBuild,ke as runOrganisationSetup,he as signApprovalToken,de as sourcedRefsFromBuildArgs,X as synthesiseEnforceSslDocument,q as toResourcePolicyStatements,Qe as toWirePlanChanges,j as triageBucketPolicy,Q as unlockBucket,H as unlockQueue,Ee as validateBuildGroup,He as verifyApprovalToken};
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};
@@ -1,5 +1,5 @@
1
- import{join as W}from"node:path";import{success as _,failure as Y}from"@fjall/generator";import{ORGANISATION_TYPES as B,getOrganisationStackName as K}from"../../types/operations.js";import{synthOrFail as q,bootstrapOrFail as z,forwardOutput as J,forwardResourceProgress as Q}from"../contextHelpers.js";import{partitionAccounts as U,probeCascadeRoles as V}from"../organisation/cascadeHelpers.js";import{accountTier as X,maskSensitiveOutput as l}from"@fjall/util";import{buildOrgContext as Z,resolveOrgDetails as v}from"./orgContext.js";import{INFRA_STEPS as L,maskAndFail as R,readStackOutputsBestEffort as x}from"./infraSteps.js";import{resolveCascadeAccounts as ee}from"./resolveCascadeAccounts.js";import{executeCascade as te}from"./cascadeExecution.js";import{reconcileOrgTrailOutputs as oe}from"./trailReconciliation.js";async function ge(s,t,O,H){const{callbacks:e,options:$}=s,{providerAccounts:u,effectiveOrgConfig:M,defaultRegion:j}=await ee(s,t),g=await v(t,s.abortSignal);if(!g.success)return R(e,g.error.message);const G=u.find(o=>X(o)==="organisation"),d=Z(s,t,O,"organisation",g.data,void 0,G?.trailLifecycle),m=$?.cascade!==!1,{platformAccount:p,memberAccounts:k}=U(u),P=m&&p!==void 0?1:0,D=m&&k.length>0?1:0,n=2+P+D,I=m?[...p!==void 0?[p]:[],...k]:[];if(I.length>0){const o=await V(t,I,e,s.abortSignal),r=p!==void 0?o.find(a=>a.accountId===p.id):void 0;if(r!==void 0)return R(e,`Pre-flight cascade role check failed for the platform account ${r.accountName} (${r.accountId}): ${r.error}`);for(const a of o)e.onProgress?.({type:"warning",message:l(`Pre-flight cascade role check failed for ${a.accountName} (${a.accountId}) \u2014 its cascade deploy is expected to fail: ${a.error}`)})}e.onCascadeAccountsReconciled?.({hasPlatformAccount:P>0,hasMemberAccounts:D>0});const{id:h,name:S}=L.PREPARE;e.onStepStart?.(h,S,0,n),e.onLog?.("Synthesising organisation infrastructure\u2026","info");const E=await q(t,d,e,"CDK synthesis failed");if(!E.success)return e.onStepComplete?.(h,S,"error",0,n),E;const N=await z(t,d,e);if(!N.success)return e.onStepComplete?.(h,S,"error",0,n),N;e.onStepComplete?.(h,S,"completed",0,n);const{id:C,name:b}=L.ORG_DEPLOY,c=K(B.ORGANISATION);let F=!0;const f=await t.hashService.getTemplateHashes(W(d.path,"cdk.out"));if(f.success){const o=await t.hashService.compareWithState(f.data,d.path);o.success?F=o.data.stackChanges.get(c)??!0:e.onLog?.(l(`Org root change detection failed \u2014 deploying to be safe: ${o.error.message}`),"warn")}else e.onLog?.(l(`Org root template hashing failed \u2014 deploying to be safe: ${f.error.message}`),"warn");const A=F||$?.force===!0||!await t.cfnService.stackExists(c);e.onOrgChangesDetected?.({hasOrgChanges:A});let w;if(A){e.onStepStart?.(C,b,1,n);const o=await t.cdkService.runCdkDeploy(d,c,J(e),Q(e),t.awsProvider);if(!o.success)return e.onStepComplete?.(C,b,"error",1,n),R(e,o.error);w=await x(t,e,c,"Failed to read org stack outputs (non-critical)");const r=f.success?f.data.get(c):void 0;if(r!==void 0){const a=await t.hashService.updateStateAfterDeploy(d.path,new Map([[c,r]]));a.success||e.onLog?.(`Warning: failed to update state file \u2014 next deploy may re-deploy the org root: ${l(a.error.message)}`,"warn")}e.onStepComplete?.(C,b,"completed",1,n)}else e.onLog?.("Organisation root: no infrastructure changes \u2014 skipping deploy","info"),w=await x(t,e,c,"Failed to read org stack outputs (non-critical)");const i=[],y=[];let T=A;if(m&&u.length>0){const{anyCascadeDeployHappened:o}=await te(s,t,O,{providerAccounts:u,effectiveOrgConfig:M,totalSteps:n,cascadeErrors:i,allCascadeOutputs:y});if(o&&(T=!0),await oe(s,t,{orgOutputs:w,allCascadeOutputs:y,orgDetails:g.data,providerAccounts:u,defaultRegion:j}),i.length>0){const r=i.map(a=>` ${a.accountId}: ${a.error}`).join(`
2
- `);e.onLog?.(l(`Cascade failed for ${i.length} target(s):
1
+ import{join as Y}from"node:path";import{OrganizationsClient as v}from"@aws-sdk/client-organizations";import{success as z,failure as B}from"@fjall/generator";import{ORGANISATION_TYPES as K,getOrganisationStackName as q}from"../../types/operations.js";import{synthOrFail as J,bootstrapOrFail as Q,forwardOutput as U,forwardResourceProgress as V}from"../contextHelpers.js";import{partitionAccounts as X,probeCascadeRoles as Z}from"../organisation/cascadeHelpers.js";import{accountTier as tt,maskSensitiveOutput as l}from"@fjall/util";import{findDevelopmentOuId as et}from"../../aws/organisations/organisationalUnits.js";import{buildOrgContext as ot,resolveOrgDetails as at}from"./orgContext.js";import{INFRA_STEPS as x,maskAndFail as C,readStackOutputsBestEffort as H}from"./infraSteps.js";import{resolveCascadeAccounts as rt}from"./resolveCascadeAccounts.js";import{executeCascade as nt}from"./cascadeExecution.js";import{reconcileOrgTrailOutputs as st}from"./trailReconciliation.js";async function yt(n,e,b,M){const{callbacks:t,options:k}=n,{providerAccounts:u,effectiveOrgConfig:j,defaultRegion:G}=await rt(n,e),p=await at(e,n.abortSignal);if(!p.success)return C(t,p.error.message);const m=await et(e.awsProvider.getClient(v),p.data.rootId,n.abortSignal);if(!m.success)return C(t,m.error.message);const W={...p.data,...m.data!==void 0?{devOuId:m.data}:{}},_=u.find(o=>tt(o)==="organisation"),d=ot(n,e,b,"organisation",W,void 0,_?.trailLifecycle),h=k?.cascade!==!1,{platformAccount:f,memberAccounts:I}=X(u),P=h&&f!==void 0?1:0,D=h&&I.length>0?1:0,s=2+P+D,E=h?[...f!==void 0?[f]:[],...I]:[];if(E.length>0){const o=await Z(e,E,t,n.abortSignal),r=f!==void 0?o.find(a=>a.accountId===f.id):void 0;if(r!==void 0)return C(t,`Pre-flight cascade role check failed for the platform account ${r.accountName} (${r.accountId}): ${r.error}`);for(const a of o)t.onProgress?.({type:"warning",message:l(`Pre-flight cascade role check failed for ${a.accountName} (${a.accountId}) \u2014 its cascade deploy is expected to fail: ${a.error}`)})}t.onCascadeAccountsReconciled?.({hasPlatformAccount:P>0,hasMemberAccounts:D>0});const{id:S,name:O}=x.PREPARE;t.onStepStart?.(S,O,0,s),t.onLog?.("Synthesising organisation infrastructure\u2026","info");const N=await J(e,d,t,"CDK synthesis failed");if(!N.success)return t.onStepComplete?.(S,O,"error",0,s),N;const F=await Q(e,d,t);if(!F.success)return t.onStepComplete?.(S,O,"error",0,s),F;t.onStepComplete?.(S,O,"completed",0,s);const{id:A,name:R}=x.ORG_DEPLOY,c=q(K.ORGANISATION);let T=!0;const g=await e.hashService.getTemplateHashes(Y(d.path,"cdk.out"));if(g.success){const o=await e.hashService.compareWithState(g.data,d.path);o.success?T=o.data.stackChanges.get(c)??!0:t.onLog?.(l(`Org root change detection failed \u2014 deploying to be safe: ${o.error.message}`),"warn")}else t.onLog?.(l(`Org root template hashing failed \u2014 deploying to be safe: ${g.error.message}`),"warn");const $=T||k?.force===!0||!await e.cfnService.stackExists(c);t.onOrgChangesDetected?.({hasOrgChanges:$});let w;if($){t.onStepStart?.(A,R,1,s);const o=await e.cdkService.runCdkDeploy(d,c,U(t),V(t),e.awsProvider);if(!o.success)return t.onStepComplete?.(A,R,"error",1,s),C(t,o.error);w=await H(e,t,c,"Failed to read org stack outputs (non-critical)");const r=g.success?g.data.get(c):void 0;if(r!==void 0){const a=await e.hashService.updateStateAfterDeploy(d.path,new Map([[c,r]]));a.success||t.onLog?.(`Warning: failed to update state file \u2014 next deploy may re-deploy the org root: ${l(a.error.message)}`,"warn")}t.onStepComplete?.(A,R,"completed",1,s)}else t.onLog?.("Organisation root: no infrastructure changes \u2014 skipping deploy","info"),w=await H(e,t,c,"Failed to read org stack outputs (non-critical)");const i=[],y=[];let L=$;if(h&&u.length>0){const{anyCascadeDeployHappened:o}=await nt(n,e,b,{providerAccounts:u,effectiveOrgConfig:j,totalSteps:s,cascadeErrors:i,allCascadeOutputs:y});if(o&&(L=!0),await st(n,e,{orgOutputs:w,allCascadeOutputs:y,orgDetails:p.data,providerAccounts:u,defaultRegion:G}),i.length>0){const r=i.map(a=>` ${a.accountId}: ${a.error}`).join(`
2
+ `);t.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,artefacts:[],...y.length>0?{cascadeOutputs:y}:{},...T?{}:{noChanges:!0},durationMs:Date.now()-H})}export{ge as deployOrgWithCascade};
5
+ ${o}`);return t.onError?.(r),B(r)}return z({target:b.target,deploymentType:"organisation",outputs:w,artefacts:[],...y.length>0?{cascadeOutputs:y}:{},...L?{}:{noChanges:!0},durationMs:Date.now()-M})}export{yt as deployOrgWithCascade};
@@ -7,6 +7,13 @@ export interface OrgDetailsForSynth {
7
7
  orgId: string;
8
8
  rootId: string;
9
9
  managementAccountId: string;
10
+ /**
11
+ * Development OU id, resolved only for organisation-tier synths (the dev
12
+ * isolation SCP is synthesised by the Organisation stack alone). Absent =
13
+ * the org has no development OU, or the deploy path never synthesises the
14
+ * org stack.
15
+ */
16
+ devOuId?: string;
10
17
  }
11
18
  export declare function buildOrgContext(params: DeployParams, services: DeployServices, operation: OrganisationOperation, deployType: "organisation" | "platform" | "account", orgDetails: OrgDetailsForSynth | undefined, accountName?: string, trailLifecycle?: ProviderAccount["trailLifecycle"], targetIsOrganisationTier?: boolean): import("../../types/deployment/DeploymentTypes.js").DeploymentContext;
12
19
  export declare function resolveOrgDetails(services: DeployServices, abortSignal?: AbortSignal): Promise<Result<OrgDetailsForSynth>>;
@@ -1 +1 @@
1
- import{success as s,failure as u}from"@fjall/generator";import{OrganizationsClient as a}from"@aws-sdk/client-organizations";import{CdkContextBuilder as l}from"../../services/supporting/CdkContextBuilder.js";import{stubCallerIdentity as m}from"../../types/deployment/index.js";import{ensureOrganisationExists as I,describeOrganisation as f}from"../../aws/organisations/organisation.js";import{buildParamsContext as C,resolveAccountBootstrapSkipOidc as O}from"../contextHelpers.js";function w(o,n,r,t,e,d,c,g){const i=n.awsProvider.getRegion();return l.buildDeploymentContext({deployType:t,target:r.target,path:r.path,region:i,accountName:d,callerIdentity:m(n.awsProvider.getAccountId()),orgId:e?.orgId,rootId:e?.rootId,managementAccountId:e?.managementAccountId,...C({orgConfig:o.orgConfig,identity:o.identity,skipOidc:O(t,o.orgConfig,o.options?.skipOidc,g),...t!=="organisation"?{region:i,primaryRegion:o.orgConfig?.primaryRegion}:{},trailLifecycle:c})},{verbose:o.options?.verbose,infraOnly:o.options?.infraOnly},o.orgConfig)}async function P(o,n){const r=o.awsProvider.getClient(a),t=await I(r,n);return t.success?s({orgId:t.data.orgId,rootId:t.data.rootId,managementAccountId:t.data.managementAccountId}):u(t.error)}async function k(o,n){const r=o.awsProvider.getClient(a),t=await f(r,n,{tolerateAccessDenied:!0});if(!(!t.success||t.data===null))return{orgId:t.data.orgId,rootId:t.data.rootId,managementAccountId:t.data.managementAccountId}}export{w as buildOrgContext,P as resolveOrgDetails,k as resolveOrgDetailsForSolo};
1
+ import{success as s,failure as u}from"@fjall/generator";import{OrganizationsClient as d}from"@aws-sdk/client-organizations";import{CdkContextBuilder as l}from"../../services/supporting/CdkContextBuilder.js";import{stubCallerIdentity as I}from"../../types/deployment/index.js";import{ensureOrganisationExists as m,describeOrganisation as f}from"../../aws/organisations/organisation.js";import{buildParamsContext as C,resolveAccountBootstrapSkipOidc as O}from"../contextHelpers.js";function w(o,n,e,t,r,a,c,g){const i=n.awsProvider.getRegion();return l.buildDeploymentContext({deployType:t,target:e.target,path:e.path,region:i,accountName:a,callerIdentity:I(n.awsProvider.getAccountId()),orgId:r?.orgId,rootId:r?.rootId,managementAccountId:r?.managementAccountId,devOuId:r?.devOuId,...C({orgConfig:o.orgConfig,identity:o.identity,skipOidc:O(t,o.orgConfig,o.options?.skipOidc,g),...t!=="organisation"?{region:i,primaryRegion:o.orgConfig?.primaryRegion}:{},trailLifecycle:c})},{verbose:o.options?.verbose,infraOnly:o.options?.infraOnly},o.orgConfig)}async function P(o,n){const e=o.awsProvider.getClient(d),t=await m(e,n);return t.success?s({orgId:t.data.orgId,rootId:t.data.rootId,managementAccountId:t.data.managementAccountId}):u(t.error)}async function k(o,n){const e=o.awsProvider.getClient(d),t=await f(e,n,{tolerateAccessDenied:!0});if(!(!t.success||t.data===null))return{orgId:t.data.orgId,rootId:t.data.rootId,managementAccountId:t.data.managementAccountId}}export{w as buildOrgContext,P as resolveOrgDetails,k as resolveOrgDetailsForSolo};
@@ -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 +1 @@
1
- import{filterDangerousEnvVars as i}from"@fjall/util";class d{buildContextArgs(r){const e=[];return r?.accountId&&e.push("-c",`accountId=${r.accountId}`),r?.environment&&e.push("-c",`environment=${r.environment}`),r?.managedAccount&&e.push("-c","managedAccount=true"),r?.accountName&&e.push("-c",`accountName=${r.accountName}`),r?.orgId&&e.push("-c",`orgId=${r.orgId}`),r?.rootId&&e.push("-c",`rootId=${r.rootId}`),r?.managementAccountId&&e.push("-c",`managementAccountId=${r.managementAccountId}`),r?.ipamPoolId&&e.push("-c",`ipamPoolId=${r.ipamPoolId}`),r?.fjallOrgId&&e.push("-c",`fjallOrgId=${r.fjallOrgId}`),r?.fjallOidcConfigured&&e.push("-c",`fjallOidcConfigured=${r.fjallOidcConfigured}`),r?.fjallAccountGlobalsConfigured&&e.push("-c",`fjallAccountGlobalsConfigured=${r.fjallAccountGlobalsConfigured}`),r?.fjallAccountTrailState&&e.push("-c",`fjallAccountTrailState=${r.fjallAccountTrailState}`),r?.orgConfig&&e.push("-c",`orgConfig=${r.orgConfig}`),r?.fjallAdoptBackupVault&&e.push("-c","fjallAdoptBackupVault=true"),r?.resolvedSecretArns&&e.push("-c",`fjallResolvedSecretArns=${r.resolvedSecretArns}`),e}buildParameterArgs(r,e){if(r===void 0)return[];const l=Object.entries(r);if(l.length===0)return[];const o=e!==void 0&&e!==""?`${e}:`:"",u=[];for(const[a,n]of l){if(!/^[A-Za-z][A-Za-z0-9]*$/.test(a))throw new Error(`Invalid CloudFormation parameter name "${a}": must match /^[A-Za-z][A-Za-z0-9]*$/ (alphanumeric, leading letter, no separators).`);if(n==="")throw new Error(`CloudFormation parameter "${a}" has an empty value.`);if(/[,\n\r]/.test(n))throw new Error(`CloudFormation parameter "${a}" value contains "," or newline \u2014 cdk's --parameters splits on "," so the deploy would silently fragment.`);u.push("--parameters",`${o}${a}=${n}`)}return u}injectCascadeCredentials(r,e){e&&(r.AWS_ACCESS_KEY_ID=e.accessKeyId,r.AWS_SECRET_ACCESS_KEY=e.secretAccessKey,delete r.AWS_SESSION_TOKEN,e.sessionToken&&(r.AWS_SESSION_TOKEN=e.sessionToken))}buildCdkEnv(r){const e={...i(process.env),CI:"true",FORCE_COLOR:"0",CDK_DISABLE_VERSION_CHECK:"1",NODE_NO_WARNINGS:"1"};return r?.context?.region&&(e.AWS_REGION=r.context.region,e.AWS_DEFAULT_REGION=r.context.region,e.CDK_DEFAULT_REGION=r.context.region),r?.context?.accountId&&(e.CDK_DEFAULT_ACCOUNT=r.context.accountId),this.injectCascadeCredentials(e,r?.credentials),e}}export{d as CdkArgumentBuilder};
1
+ import{filterDangerousEnvVars as o}from"@fjall/util";class f{buildContextArgs(r){const e=[];return r?.accountId&&e.push("-c",`accountId=${r.accountId}`),r?.environment&&e.push("-c",`environment=${r.environment}`),r?.managedAccount&&e.push("-c","managedAccount=true"),r?.accountName&&e.push("-c",`accountName=${r.accountName}`),r?.orgId&&e.push("-c",`orgId=${r.orgId}`),r?.rootId&&e.push("-c",`rootId=${r.rootId}`),r?.managementAccountId&&e.push("-c",`managementAccountId=${r.managementAccountId}`),r?.devOuId&&e.push("-c",`fjallDevOuId=${r.devOuId}`),r?.ipamPoolId&&e.push("-c",`ipamPoolId=${r.ipamPoolId}`),r?.fjallOrgId&&e.push("-c",`fjallOrgId=${r.fjallOrgId}`),r?.fjallOidcConfigured&&e.push("-c",`fjallOidcConfigured=${r.fjallOidcConfigured}`),r?.fjallAccountGlobalsConfigured&&e.push("-c",`fjallAccountGlobalsConfigured=${r.fjallAccountGlobalsConfigured}`),r?.fjallAccountTrailState&&e.push("-c",`fjallAccountTrailState=${r.fjallAccountTrailState}`),r?.orgConfig&&e.push("-c",`orgConfig=${r.orgConfig}`),r?.fjallAdoptBackupVault&&e.push("-c","fjallAdoptBackupVault=true"),r?.resolvedSecretArns&&e.push("-c",`fjallResolvedSecretArns=${r.resolvedSecretArns}`),e}buildParameterArgs(r,e){if(r===void 0)return[];const l=Object.entries(r);if(l.length===0)return[];const i=e!==void 0&&e!==""?`${e}:`:"",n=[];for(const[a,u]of l){if(!/^[A-Za-z][A-Za-z0-9]*$/.test(a))throw new Error(`Invalid CloudFormation parameter name "${a}": must match /^[A-Za-z][A-Za-z0-9]*$/ (alphanumeric, leading letter, no separators).`);if(u==="")throw new Error(`CloudFormation parameter "${a}" has an empty value.`);if(/[,\n\r]/.test(u))throw new Error(`CloudFormation parameter "${a}" value contains "," or newline \u2014 cdk's --parameters splits on "," so the deploy would silently fragment.`);n.push("--parameters",`${i}${a}=${u}`)}return n}injectCascadeCredentials(r,e){e&&(r.AWS_ACCESS_KEY_ID=e.accessKeyId,r.AWS_SECRET_ACCESS_KEY=e.secretAccessKey,delete r.AWS_SESSION_TOKEN,e.sessionToken&&(r.AWS_SESSION_TOKEN=e.sessionToken))}buildCdkEnv(r){const e={...o(process.env),CI:"true",FORCE_COLOR:"0",CDK_DISABLE_VERSION_CHECK:"1",NODE_NO_WARNINGS:"1"};return r?.context?.region&&(e.AWS_REGION=r.context.region,e.AWS_DEFAULT_REGION=r.context.region,e.CDK_DEFAULT_REGION=r.context.region),r?.context?.accountId&&(e.CDK_DEFAULT_ACCOUNT=r.context.accountId),this.injectCascadeCredentials(e,r?.credentials),e}}export{f as CdkArgumentBuilder};
@@ -10,6 +10,13 @@ export interface CdkContext {
10
10
  orgId?: string;
11
11
  rootId?: string;
12
12
  managementAccountId?: string;
13
+ /**
14
+ * Development OU id for the dev-isolation SCP (G6). Emitted as
15
+ * `-c fjallDevOuId=<id>`; keep the key literal in sync with the reader,
16
+ * CDK_CONTEXT_KEYS.DEV_OU_ID in @fjall/components-infrastructure
17
+ * (`utils/cdkContext.ts`).
18
+ */
19
+ devOuId?: string;
13
20
  ipamPoolId?: string;
14
21
  fjallOrgId?: string;
15
22
  fjallOidcConfigured?: string;
@@ -93,4 +93,21 @@ export declare class CloudFormationService {
93
93
  pollIntervalMs?: number;
94
94
  onProgress?: (message: string) => void;
95
95
  }): Promise<Result<void, CloudFormationError>>;
96
+ /**
97
+ * Converge a subset of a stack's parameters to new values, preserving every
98
+ * other parameter via `UsePreviousValue` (F4b — post-rollback CFN ImageTag
99
+ * reconcile). Keys in `parameterOverrides` absent from the stack's current
100
+ * parameters are reported as `skippedKeys` rather than failing the call —
101
+ * a stale key (a service the stack no longer has) should not block
102
+ * reconciling the keys that do exist.
103
+ */
104
+ updateStackParameters(stackName: string, parameterOverrides: Record<string, string>, abortSignal?: AbortSignal): Promise<Result<{
105
+ updated: boolean;
106
+ skippedKeys: string[];
107
+ }, CloudFormationError>>;
108
+ /**
109
+ * Poll stack status until the parameter-only update completes, fails, or
110
+ * times out.
111
+ */
112
+ private waitForUpdateStackParametersComplete;
96
113
  }
@@ -1 +1 @@
1
- import{CloudFormationClient as d,DeleteStackCommand as h,DescribeStacksCommand as m,GetTemplateCommand as k,ListExportsCommand as y}from"@aws-sdk/client-cloudformation";import{stackStatusMap as S}from"../../aws/utils/stackStatus.js";import{maskSensitiveOutput as f}from"@fjall/util";import{success as i,failure as c}from"@fjall/generator";import{BaseServiceError as T}from"../../types/errors/ServiceError.js";import{logger as x}from"@fjall/util/logger";import{getErrorMessage as p,sleep as E}from"@fjall/util";import{STACK_NOT_FOUND_PATTERN as w}from"@fjall/util/aws";import{isCleanableState as C}from"../../types/constants.js";import{extractErrorName as O}from"../../aws/organisations/types.js";class l extends T{errorType;stackName;stackStatus;constructor(t,e,s,n,r,o=!1){super(`CFN_${e.toUpperCase()}`,t,r,o),this.errorType=e,this.stackName=s,this.stackStatus=n}}class I{aws;constructor(t){this.aws=t}classifyAwsError(t,e,s){const n=O(t),r=f(p(t));return n==="CredentialsError"||n==="UnauthorizedError"?new l(`AWS credentials error: ${r}`,"auth_error",s,void 0,t,!1):n==="Throttling"||n==="TooManyRequestsException"?new l(`AWS rate limit exceeded: ${r}`,"throttled",s,void 0,t,!0):n==="NetworkingError"||n==="ENOTFOUND"?new l(`Network error: ${r}`,"network_error",s,void 0,t,!0):new l(`${e}: ${r}`,"unknown",s,void 0,t)}async getStackOutputs(t,e){e?.onStackCheck?.(t);const s=this.aws.getClient(d),n=new m({StackName:t});try{const o=(await s.send(n)).Stacks?.[0];if(!o?.Outputs)return i([]);const a=o.Outputs.map(u=>({OutputKey:u.OutputKey,OutputValue:u.OutputValue,ExportName:u.ExportName}));return e?.onOutputsRetrieved?.(t,a.length),i(a)}catch(r){if(r instanceof Error&&r.name==="ValidationError"&&r.message?.includes(w))return e?.onStackNotFound?.(t),i([]);const o=f(p(r));return c(new l(`Failed to get outputs for stack ${t}: ${o}`,"unknown",t,void 0,r))}}async getStackStatus(t,e){e?.onStackCheck?.(t);const s=this.aws.getClient(d),n=new m({StackName:t});try{const o=(await s.send(n)).Stacks?.[0];if(!o)return i({status:"DOES_NOT_EXIST",safeToRedeploy:"Yes",description:"Stack does not exist yet"});const a=o.StackStatus||"UNKNOWN",u=S[a]||S.UNKNOWN;return e?.onStackFound?.(t,a),i({status:a,safeToRedeploy:u.safeToRedeploy,description:u.description,statusReason:o.StackStatusReason})}catch(r){return r instanceof Error&&r.name==="ValidationError"&&r.message?.includes(w)?i({status:"DOES_NOT_EXIST",safeToRedeploy:"Yes",description:"Stack does not exist yet"}):c(this.classifyAwsError(r,`Failed to get stack status for ${t}`,t))}}async listAllExports(t){const e=this.aws.getClient(d),s=[];try{let n;do{const r=new y({NextToken:n}),o=await e.send(r),a=o.Exports||[];for(const u of a)u.Name&&u.Value&&s.push({Name:u.Name,Value:u.Value});if(t?.(a))break;n=o.NextToken}while(n);return i(s)}catch(n){const r=f(p(n));return c(new l(`Failed to list exports: ${r}`,"unknown",void 0,void 0,n,!1))}}async getExportsByNames(t){if(t.length===0)return i(new Map);const e=new Set(t),s=new Map,n=await this.listAllExports(r=>{for(const o of r)o.Name&&e.has(o.Name)&&o.Value&&s.set(o.Name,o.Value);return s.size>=e.size});return n.success?i(s):c(n.error)}async listExports(t){const e=await this.listAllExports();return e.success&&t?.onExportsRetrieved?.(e.data.length),e}async deleteStack(t){const e=this.aws.getClient(d);try{return await e.send(new h({StackName:t})),i(void 0)}catch(s){return c(this.classifyAwsError(s,`Failed to delete stack ${t}`,t))}}async stackExists(t,e){const s=e??this.aws.getClient(d);try{const r=(await s.send(new m({StackName:t}))).Stacks?.[0]?.StackStatus;return!!r&&r!=="REVIEW_IN_PROGRESS"&&!C(r)}catch(n){return n instanceof Error&&n.message?.includes(w)?!1:(x.debug("CloudFormationService","Error checking stack existence, assuming exists",{stackName:t,error:p(n)}),!0)}}async getTemplate(t){const e=this.aws.getClient(d);try{const s=await e.send(new k({StackName:t,TemplateStage:"Original"}));return i(s.TemplateBody??"")}catch(s){return s instanceof Error&&s.message?.includes(w)?c(new l(`Stack ${t} not found`,"stack_not_found",t)):c(this.classifyAwsError(s,`Failed to get template for stack ${t}`,t))}}async waitForDeleteComplete(t,e){const s=e?.timeoutMs??6e5,n=e?.pollIntervalMs??5e3,r=Date.now();for(;Date.now()-r<s;){const o=await this.getStackStatus(t);if(!o.success){if(o.error.recoverable){e?.onProgress?.(`Transient error polling stack ${t}, retrying: ${f(o.error.message)}`),await E(n);continue}return c(o.error)}const a=o.data?.status;if(a==="DELETE_COMPLETE"||a==="DOES_NOT_EXIST")return i(void 0);if(a==="DELETE_FAILED")return c(new l(`Stack ${t} deletion failed: ${o.data?.statusReason||"unknown reason"}`,"stack_failed",t,a,void 0,!1));e?.onProgress?.(`Stack ${t} status: ${a??"unknown"}`),await E(n)}return c(new l(`Timed out waiting for stack ${t} deletion after ${Math.round(s/1e3)}s`,"timeout",t,void 0,void 0,!0))}}export{l as CloudFormationError,I as CloudFormationService};
1
+ import{CloudFormationClient as f,DeleteStackCommand as _,DescribeStacksCommand as m,GetTemplateCommand as A,ListExportsCommand as O,UpdateStackCommand as P}from"@aws-sdk/client-cloudformation";import{stackStatusMap as T}from"../../aws/utils/stackStatus.js";import{maskSensitiveOutput as w}from"@fjall/util";import{success as d,failure as c}from"@fjall/generator";import{BaseServiceError as $}from"../../types/errors/ServiceError.js";import{logger as M}from"@fjall/util/logger";import{getErrorMessage as S,sleep as E}from"@fjall/util";import{STACK_NOT_FOUND_PATTERN as g}from"@fjall/util/aws";import{isCleanableState as F}from"../../types/constants.js";import{composeSdkAbortSignal as C,extractErrorName as R,isAborted as D}from"../../aws/organisations/types.js";class l extends ${errorType;stackName;stackStatus;constructor(e,r,t,n,s,a=!1){super(`CFN_${r.toUpperCase()}`,e,s,a),this.errorType=r,this.stackName=t,this.stackStatus=n}}class Y{aws;constructor(e){this.aws=e}classifyAwsError(e,r,t){const n=R(e),s=w(S(e));return n==="CredentialsError"||n==="UnauthorizedError"?new l(`AWS credentials error: ${s}`,"auth_error",t,void 0,e,!1):n==="Throttling"||n==="TooManyRequestsException"?new l(`AWS rate limit exceeded: ${s}`,"throttled",t,void 0,e,!0):n==="NetworkingError"||n==="ENOTFOUND"?new l(`Network error: ${s}`,"network_error",t,void 0,e,!0):new l(`${r}: ${s}`,"unknown",t,void 0,e)}async getStackOutputs(e,r){r?.onStackCheck?.(e);const t=this.aws.getClient(f),n=new m({StackName:e});try{const a=(await t.send(n)).Stacks?.[0];if(!a?.Outputs)return d([]);const o=a.Outputs.map(u=>({OutputKey:u.OutputKey,OutputValue:u.OutputValue,ExportName:u.ExportName}));return r?.onOutputsRetrieved?.(e,o.length),d(o)}catch(s){if(s instanceof Error&&s.name==="ValidationError"&&s.message?.includes(g))return r?.onStackNotFound?.(e),d([]);const a=w(S(s));return c(new l(`Failed to get outputs for stack ${e}: ${a}`,"unknown",e,void 0,s))}}async getStackStatus(e,r){r?.onStackCheck?.(e);const t=this.aws.getClient(f),n=new m({StackName:e});try{const a=(await t.send(n)).Stacks?.[0];if(!a)return d({status:"DOES_NOT_EXIST",safeToRedeploy:"Yes",description:"Stack does not exist yet"});const o=a.StackStatus||"UNKNOWN",u=T[o]||T.UNKNOWN;return r?.onStackFound?.(e,o),d({status:o,safeToRedeploy:u.safeToRedeploy,description:u.description,statusReason:a.StackStatusReason})}catch(s){return s instanceof Error&&s.name==="ValidationError"&&s.message?.includes(g)?d({status:"DOES_NOT_EXIST",safeToRedeploy:"Yes",description:"Stack does not exist yet"}):c(this.classifyAwsError(s,`Failed to get stack status for ${e}`,e))}}async listAllExports(e){const r=this.aws.getClient(f),t=[];try{let n;do{const s=new O({NextToken:n}),a=await r.send(s),o=a.Exports||[];for(const u of o)u.Name&&u.Value&&t.push({Name:u.Name,Value:u.Value});if(e?.(o))break;n=a.NextToken}while(n);return d(t)}catch(n){const s=w(S(n));return c(new l(`Failed to list exports: ${s}`,"unknown",void 0,void 0,n,!1))}}async getExportsByNames(e){if(e.length===0)return d(new Map);const r=new Set(e),t=new Map,n=await this.listAllExports(s=>{for(const a of s)a.Name&&r.has(a.Name)&&a.Value&&t.set(a.Name,a.Value);return t.size>=r.size});return n.success?d(t):c(n.error)}async listExports(e){const r=await this.listAllExports();return r.success&&e?.onExportsRetrieved?.(r.data.length),r}async deleteStack(e){const r=this.aws.getClient(f);try{return await r.send(new _({StackName:e})),d(void 0)}catch(t){return c(this.classifyAwsError(t,`Failed to delete stack ${e}`,e))}}async stackExists(e,r){const t=r??this.aws.getClient(f);try{const s=(await t.send(new m({StackName:e}))).Stacks?.[0]?.StackStatus;return!!s&&s!=="REVIEW_IN_PROGRESS"&&!F(s)}catch(n){return n instanceof Error&&n.message?.includes(g)?!1:(M.debug("CloudFormationService","Error checking stack existence, assuming exists",{stackName:e,error:S(n)}),!0)}}async getTemplate(e){const r=this.aws.getClient(f);try{const t=await r.send(new A({StackName:e,TemplateStage:"Original"}));return d(t.TemplateBody??"")}catch(t){return t instanceof Error&&t.message?.includes(g)?c(new l(`Stack ${e} not found`,"stack_not_found",e)):c(this.classifyAwsError(t,`Failed to get template for stack ${e}`,e))}}async waitForDeleteComplete(e,r){const t=r?.timeoutMs??6e5,n=r?.pollIntervalMs??5e3,s=Date.now();for(;Date.now()-s<t;){const a=await this.getStackStatus(e);if(!a.success){if(a.error.recoverable){r?.onProgress?.(`Transient error polling stack ${e}, retrying: ${w(a.error.message)}`),await E(n);continue}return c(a.error)}const o=a.data?.status;if(o==="DELETE_COMPLETE"||o==="DOES_NOT_EXIST")return d(void 0);if(o==="DELETE_FAILED")return c(new l(`Stack ${e} deletion failed: ${a.data?.statusReason||"unknown reason"}`,"stack_failed",e,o,void 0,!1));r?.onProgress?.(`Stack ${e} status: ${o??"unknown"}`),await E(n)}return c(new l(`Timed out waiting for stack ${e} deletion after ${Math.round(t/1e3)}s`,"timeout",e,void 0,void 0,!0))}async updateStackParameters(e,r,t){const n=this.aws.getClient(f);let s;try{s=await n.send(new m({StackName:e}),{abortSignal:C(t)})}catch(i){return c(this.classifyAwsError(i,`Failed to describe stack ${e} before parameter reconcile`,e))}const a=s.Stacks?.[0];if(!a)return c(new l(`Stack ${e} not found`,"stack_not_found",e));const o=a.Parameters??[],u=new Set(o.map(i=>i.ParameterKey).filter(i=>i!==void 0)),k=Object.entries(r),h=k.filter(([i])=>!u.has(i)).map(([i])=>i),y=new Map(k.filter(([i])=>u.has(i)));if(y.size===0)return d({updated:!1,skippedKeys:h});const x=o.filter(i=>i.ParameterKey!==void 0).map(i=>y.has(i.ParameterKey)?{ParameterKey:i.ParameterKey,ParameterValue:y.get(i.ParameterKey)}:{ParameterKey:i.ParameterKey,UsePreviousValue:!0});try{await n.send(new P({StackName:e,UsePreviousTemplate:!0,Parameters:x,Capabilities:["CAPABILITY_IAM","CAPABILITY_NAMED_IAM"]}),{abortSignal:C(t)})}catch(i){return i instanceof Error&&i.name==="ValidationError"&&i.message?.includes("No updates are to be performed")?d({updated:!1,skippedKeys:h}):c(this.classifyAwsError(i,`Failed to update parameters for stack ${e}`,e))}return this.waitForUpdateStackParametersComplete(e,h,t)}async waitForUpdateStackParametersComplete(e,r,t){const a=Date.now();for(;Date.now()-a<6e5;){if(D(t))return c(new l(`Aborted waiting for stack ${e} parameter update`,"timeout",e,void 0,void 0,!0));const o=await this.getStackStatus(e);if(!o.success){if(o.error.recoverable){await E(5e3);continue}return c(o.error)}const u=o.data?.status??"UNKNOWN";if(u==="UPDATE_COMPLETE")return d({updated:!0,skippedKeys:r});if(I(u))return c(new l(`Stack ${e} parameter update failed: ${o.data?.statusReason||"unknown reason"}`,"stack_failed",e,u,void 0,!1));await E(5e3)}return c(new l(`Timed out waiting for stack ${e} parameter update after ${Math.round(6e5/1e3)}s`,"timeout",e,void 0,void 0,!0))}}function I(p){return p.endsWith("_IN_PROGRESS")?!1:p.startsWith("UPDATE_ROLLBACK_")||p==="UPDATE_FAILED"}export{l as CloudFormationError,Y as CloudFormationService};
@@ -28,5 +28,15 @@ export declare class EcrImageInspectorService {
28
28
  * third-party registries). Returns a failure only when the ECR call
29
29
  * itself errors or the tag exists but ECR returned no digest.
30
30
  */
31
- getImageDigest(imageUri: string, imageTag: string): Promise<Result<string | undefined, Error>>;
31
+ getImageDigest(imageUri: string, imageTag: string, abortSignal?: AbortSignal): Promise<Result<string | undefined, Error>>;
32
+ /**
33
+ * Reverse lookup: the tags attached to an image digest. Needed by the
34
+ * restart operation — after a digest-pinned rollback the live container
35
+ * image reference carries no tag, but the restart Release's artefacts must
36
+ * record one for later `serviceImageTags` rollbacks to consume.
37
+ *
38
+ * Returns `success(undefined)` for non-ECR URIs (caller degrades),
39
+ * `success([])` when the image exists but is untagged.
40
+ */
41
+ getImageTagsByDigest(imageUri: string, imageDigest: string, abortSignal?: AbortSignal): Promise<Result<string[] | undefined, Error>>;
32
42
  }
@@ -1 +1 @@
1
- import{ECRClient as m,BatchGetImageCommand as p}from"@aws-sdk/client-ecr";import{success as n,failure as s}from"@fjall/generator";import{logger as g}from"@fjall/util/logger";import{getErrorMessage as I}from"@fjall/util";const E="EcrImageInspector",C=/^(\d{12})\.dkr\.ecr\.([a-z0-9-]+)\.amazonaws\.com\/(.+)$/;function R(t){const e=t.lastIndexOf("@");if(e>0)return t.slice(0,e);const r=t.lastIndexOf(":");return r>0?t.slice(0,r):t}class G{awsProvider;constructor(e){this.awsProvider=e}async getImageDigest(e,r){const c=R(e).match(C);if(!c)return g.debug(E,"Image URI is not ECR \u2014 skipping digest lookup",{imageUri:e}),n(void 0);const[,,,o]=c;if(!o)return n(void 0);try{const a=await this.awsProvider.getClient(m).send(new p({repositoryName:o,imageIds:[{imageTag:r}]})),d=a.failures??[];if(d.length>0){const l=d.map(f=>`${f.failureCode??"unknown"}:${f.failureReason??""}`).join("; ");return s(new Error(`ECR BatchGetImage rejected ${o}:${r} \u2014 ${l}`))}const u=a.images?.[0]?.imageId?.imageDigest;return u?n(u):s(new Error(`ECR BatchGetImage returned no digest for ${o}:${r}`))}catch(i){return s(new Error(`ECR digest lookup failed for ${o}:${r} \u2014 ${I(i)}`))}}}export{G as EcrImageInspectorService,R as stripTagOrDigest};
1
+ import{ECRClient as m,BatchGetImageCommand as h,DescribeImagesCommand as k}from"@aws-sdk/client-ecr";import{success as o,failure as c}from"@fjall/generator";import{logger as f}from"@fjall/util/logger";import{getErrorMessage as p}from"@fjall/util";import{composeSdkAbortSignal as I}from"../../aws/organisations/types.js";const E="EcrImageInspector",C=/^(\d{12})\.dkr\.ecr\.([a-z0-9-]+)\.amazonaws\.com\/(.+)$/;function $(n){const r=n.lastIndexOf("@");if(r>0)return n.slice(0,r);const e=n.lastIndexOf(":");return e>0?n.slice(0,e):n}class G{awsProvider;constructor(r){this.awsProvider=r}async getImageDigest(r,e,d){const s=$(r).match(C);if(!s)return f.debug(E,"Image URI is not ECR \u2014 skipping digest lookup",{imageUri:r}),o(void 0);const[,,,t]=s;if(!t)return o(void 0);try{const u=await this.awsProvider.getClient(m).send(new h({repositoryName:t,imageIds:[{imageTag:e}]}),{abortSignal:I(d)}),a=u.failures??[];if(a.length>0){const R=a.map(l=>`${l.failureCode??"unknown"}:${l.failureReason??""}`).join("; ");return c(new Error(`ECR BatchGetImage rejected ${t}:${e} \u2014 ${R}`))}const g=u.images?.[0]?.imageId?.imageDigest;return g?o(g):c(new Error(`ECR BatchGetImage returned no digest for ${t}:${e}`))}catch(i){return c(new Error(`ECR digest lookup failed for ${t}:${e} \u2014 ${p(i)}`))}}async getImageTagsByDigest(r,e,d){const s=$(r).match(C);if(!s)return f.debug(E,"Image URI is not ECR \u2014 skipping tag lookup by digest",{imageUri:r}),o(void 0);const[,,,t]=s;if(!t)return o(void 0);try{const a=(await this.awsProvider.getClient(m).send(new k({repositoryName:t,imageIds:[{imageDigest:e}]}),{abortSignal:I(d)})).imageDetails?.[0];return a?o(a.imageTags??[]):c(new Error(`ECR DescribeImages returned no image for ${t}@${e}`))}catch(i){return c(new Error(`ECR tag lookup failed for ${t}@${e} \u2014 ${p(i)}`))}}}export{G as EcrImageInspectorService,$ as stripTagOrDigest};
@@ -0,0 +1,23 @@
1
+ import { type Result } from "@fjall/generator";
2
+ import type { AwsProvider } from "../../aws/AwsProvider.js";
3
+ export interface SsmParameterMetadata {
4
+ name: string;
5
+ version?: number;
6
+ lastModifiedDate?: Date;
7
+ }
8
+ /**
9
+ * Metadata-only SSM reads for the secrets-drift probe. Deliberately uses
10
+ * DescribeParameters (never GetParameter/GetParameters): drift needs only
11
+ * `LastModifiedDate` + `Version`, and a value-bearing call would put secret
12
+ * contents in memory for no reason.
13
+ */
14
+ export declare class SsmParameterMetadataService {
15
+ private readonly awsProvider;
16
+ constructor(awsProvider: AwsProvider);
17
+ /**
18
+ * Describe parameters by exact name. Names absent from the result simply
19
+ * do not exist (or are not visible) — callers treat them as unknown, not
20
+ * as an error.
21
+ */
22
+ describeParametersByName(names: string[], abortSignal?: AbortSignal): Promise<Result<SsmParameterMetadata[], Error>>;
23
+ }
@@ -0,0 +1 @@
1
+ import{SSMClient as u,DescribeParametersCommand as l}from"@aws-sdk/client-ssm";import{success as m,failure as p}from"@fjall/generator";import{getErrorMessage as S,maskSensitiveOutput as P}from"@fjall/util";import{composeSdkAbortSignal as b,isAborted as c}from"../../aws/organisations/types.js";const o=50;class v{awsProvider;constructor(r){this.awsProvider=r}async describeParametersByName(r,i){if(r.length===0)return m([]);try{const a=this.awsProvider.getClient(u),n=[];for(let t=0;t<r.length&&!c(i);t+=o){const f=r.slice(t,t+o);let s;do{if(c(i))break;const d=await a.send(new l({ParameterFilters:[{Key:"Name",Option:"Equals",Values:f}],MaxResults:o,...s!==void 0?{NextToken:s}:{}}),{abortSignal:b(i)});for(const e of d.Parameters??[])e.Name!==void 0&&n.push({name:e.Name,...e.Version!==void 0?{version:Number(e.Version)}:{},...e.LastModifiedDate!==void 0?{lastModifiedDate:e.LastModifiedDate}:{}});s=d.NextToken}while(s!==void 0)}return m(n)}catch(a){return p(new Error(`SSM DescribeParameters failed: ${P(S(a))}`))}}}export{v as SsmParameterMetadataService};
@@ -1 +1 @@
1
- import{getApplicationStackName as l,getOrganisationStackName as i,isApplicationStack as u}from"../../types/operations.js";const t=5e3;function c(a,e){if(a&&!a.includes("*"))return a;if(a){const o=a.match(/\*?(\w+)\*?/);if(o?.[1]){const n=o[1],r=e.target;return u(n)?l(r,n):`${r}${n}`}return a}}function f(a){const e=a.deployType;return e==="organisation"||e==="platform"||e==="account"?i(e):`${a.target}Network`}function p(a,e,o){return{accountId:e,region:o,environment:a.environment,managedAccount:a.isManagedAccount,accountName:a.accountName,orgId:a.orgId,rootId:a.rootId,managementAccountId:a.managementAccountId,ipamPoolId:a.ipamPoolId,fjallOrgId:a.fjallOrgId,fjallOidcConfigured:a.fjallOidcConfigured?"true":void 0,fjallAccountGlobalsConfigured:a.fjallAccountGlobalsConfigured?"true":void 0,fjallAccountTrailState:a.fjallAccountTrailState,orgConfig:a.orgConfig,fjallAdoptBackupVault:a.fjallAdoptBackupVault?"true":void 0,resolvedSecretArns:a.resolvedSecretArns}}export{t as STACK_DETECTION_FALLBACK_MS,p as buildDeploymentCdkContext,f as getFallbackStackName,c as resolveStackName};
1
+ import{getApplicationStackName as l,getOrganisationStackName as u,isApplicationStack as d}from"../../types/operations.js";const t=5e3;function c(a,e){if(a&&!a.includes("*"))return a;if(a){const o=a.match(/\*?(\w+)\*?/);if(o?.[1]){const n=o[1],r=e.target;return d(n)?l(r,n):`${r}${n}`}return a}}function f(a){const e=a.deployType;return e==="organisation"||e==="platform"||e==="account"?u(e):`${a.target}Network`}function p(a,e,o){return{accountId:e,region:o,environment:a.environment,managedAccount:a.isManagedAccount,accountName:a.accountName,orgId:a.orgId,rootId:a.rootId,managementAccountId:a.managementAccountId,devOuId:a.devOuId,ipamPoolId:a.ipamPoolId,fjallOrgId:a.fjallOrgId,fjallOidcConfigured:a.fjallOidcConfigured?"true":void 0,fjallAccountGlobalsConfigured:a.fjallAccountGlobalsConfigured?"true":void 0,fjallAccountTrailState:a.fjallAccountTrailState,orgConfig:a.orgConfig,fjallAdoptBackupVault:a.fjallAdoptBackupVault?"true":void 0,resolvedSecretArns:a.resolvedSecretArns}}export{t as STACK_DETECTION_FALLBACK_MS,p as buildDeploymentCdkContext,f as getFallbackStackName,c as resolveStackName};
@@ -29,6 +29,7 @@ export declare class CdkContextBuilder {
29
29
  orgId?: string;
30
30
  rootId?: string;
31
31
  managementAccountId?: string;
32
+ devOuId?: string;
32
33
  ipamPoolId?: string;
33
34
  fjallOrgId?: string;
34
35
  fjallOidcConfigured?: boolean;
@@ -1 +1 @@
1
- import{DEFAULT_REGION as l}from"@fjall/generator";class d{static buildDeploymentContext(e,o,a){return{deployType:e.deployType,target:e.target,path:e.path,...e.assemblyDir!==void 0?{assemblyDir:e.assemblyDir}:{},...e.environment!==void 0?{environment:e.environment}:{},options:o,stackOutputs:e.stackOutputs||{},callerIdentity:e.callerIdentity,region:e.region||a?.primaryRegion||l,isManagedAccount:e.isManagedAccount,accountName:e.accountName,logPath:e.logPath,orgId:e.orgId,rootId:e.rootId,managementAccountId:e.managementAccountId,ipamPoolId:e.ipamPoolId,fjallOrgId:e.fjallOrgId,fjallOidcConfigured:e.fjallOidcConfigured,fjallAccountGlobalsConfigured:e.fjallAccountGlobalsConfigured,fjallAccountTrailState:e.fjallAccountTrailState,orgConfig:e.orgConfig}}static updateContext(e,o){return{...e,...o}}}export{d as CdkContextBuilder};
1
+ import{DEFAULT_REGION as l}from"@fjall/generator";class t{static buildDeploymentContext(e,o,a){return{deployType:e.deployType,target:e.target,path:e.path,...e.assemblyDir!==void 0?{assemblyDir:e.assemblyDir}:{},...e.environment!==void 0?{environment:e.environment}:{},options:o,stackOutputs:e.stackOutputs||{},callerIdentity:e.callerIdentity,region:e.region||a?.primaryRegion||l,isManagedAccount:e.isManagedAccount,accountName:e.accountName,logPath:e.logPath,orgId:e.orgId,rootId:e.rootId,managementAccountId:e.managementAccountId,devOuId:e.devOuId,ipamPoolId:e.ipamPoolId,fjallOrgId:e.fjallOrgId,fjallOidcConfigured:e.fjallOidcConfigured,fjallAccountGlobalsConfigured:e.fjallAccountGlobalsConfigured,fjallAccountTrailState:e.fjallAccountTrailState,orgConfig:e.orgConfig}}static updateContext(e,o){return{...e,...o}}}export{t as CdkContextBuilder};
@@ -19,6 +19,7 @@ export interface DeploymentContext {
19
19
  orgId?: string;
20
20
  rootId?: string;
21
21
  managementAccountId?: string;
22
+ devOuId?: string;
22
23
  ipamPoolId?: string;
23
24
  fjallOrgId?: string;
24
25
  fjallOidcConfigured?: boolean;
@@ -10,7 +10,7 @@ export { parseDeployEvent, parseDeployContractVersion, type ParsedDeployEvent }
10
10
  export type { ProgressEvent, ProgressEventType, ResourceEvent, AwsAuthResult, CascadeDeploymentResult, CascadePhase, BuildPushStartEvent, BuildPushProgressEvent, BuildPushCompleteEvent, TaskDefRegisteredEvent, ECSCompleteEvent, MigrationsStartEvent, MigrationsCompleteEvent, TrailMigrationPhase, TrailMigrationStatus, TrailMigrationPhaseEvent } from "./events.js";
11
11
  export { TRAIL_MIGRATION_PHASES, TRAIL_MIGRATION_STATUSES } from "./events.js";
12
12
  export type { ApiClientInterface, EntitlementsData } from "./apiClient.js";
13
- export type { DeployParams, DeployOptions, DeploymentType, DeployResult, DestroyParams, DestroyOptions, DestroyResult } from "./params.js";
13
+ export type { DeployParams, DeployOptions, DatabaseEndpointRestartRequest, DeploymentType, DeployResult, DestroyParams, DestroyOptions, DestroyResult } from "./params.js";
14
14
  export type { ApprovalGate, ApprovalGatePlanDetail, ApprovalRequest, GateResolution } from "./approval.js";
15
15
  export type { OrgConfig, ProviderAccount, RootAccessManagementMode, SSOSession } from "./config/orgConfig.js";
16
16
  export type { Entitlements } from "./config/entitlements.js";