@fjall/deploy-core 2.12.0 → 2.14.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/cloudtrail/orgTrailDelivery.d.ts +44 -0
- package/dist/src/aws/cloudtrail/orgTrailDelivery.js +1 -0
- package/dist/src/aws/index.d.ts +6 -2
- package/dist/src/aws/index.js +1 -1
- package/dist/src/aws/organisations/accountGlobals.d.ts +40 -0
- package/dist/src/aws/organisations/accountGlobals.js +1 -0
- package/dist/src/aws/organisations/accounts.d.ts +3 -1
- package/dist/src/aws/organisations/accounts.js +1 -1
- package/dist/src/aws/organisations/backup.d.ts +2 -1
- package/dist/src/aws/organisations/backup.js +2 -2
- package/dist/src/aws/organisations/importedAccounts.d.ts +16 -0
- package/dist/src/aws/organisations/importedAccounts.js +1 -0
- package/dist/src/aws/organisations/index.d.ts +3 -1
- package/dist/src/aws/organisations/index.js +1 -1
- package/dist/src/aws/organisations/organisationalUnits.d.ts +1 -1
- package/dist/src/aws/organisations/policies.js +1 -1
- package/dist/src/aws/organisations/rootAccess.d.ts +27 -0
- package/dist/src/aws/organisations/rootAccess.js +3 -0
- package/dist/src/aws/organisations/serviceAccess.d.ts +6 -0
- package/dist/src/aws/organisations/serviceAccess.js +1 -1
- package/dist/src/aws/organisations/types.d.ts +18 -0
- package/dist/src/aws/organisations/types.js +1 -1
- package/dist/src/aws/sts/assumeRoot.d.ts +46 -0
- package/dist/src/aws/sts/assumeRoot.js +1 -0
- package/dist/src/aws/targetReadiness.d.ts +70 -0
- package/dist/src/aws/targetReadiness.js +1 -0
- package/dist/src/aws/targetSetAdvisory.d.ts +24 -0
- package/dist/src/aws/targetSetAdvisory.js +1 -0
- package/dist/src/events/index.d.ts +2 -0
- package/dist/src/events/index.js +1 -1
- package/dist/src/index.d.ts +18 -14
- package/dist/src/index.js +1 -1
- package/dist/src/orchestration/accountsConfig.d.ts +11 -0
- package/dist/src/orchestration/accountsConfig.js +1 -1
- package/dist/src/orchestration/applicationDeploy.js +1 -1
- package/dist/src/orchestration/applicationDestroy.js +1 -1
- package/dist/src/orchestration/cascadeDestroyHelpers.d.ts +12 -1
- package/dist/src/orchestration/cascadeDestroyHelpers.js +1 -1
- package/dist/src/orchestration/cascadeHelpers.d.ts +36 -5
- package/dist/src/orchestration/cascadeHelpers.js +1 -1
- package/dist/src/orchestration/contextHelpers.d.ts +20 -2
- package/dist/src/orchestration/contextHelpers.js +1 -1
- package/dist/src/orchestration/index.d.ts +13 -4
- package/dist/src/orchestration/index.js +1 -1
- package/dist/src/orchestration/organisationDeploy/cascadeExecution.d.ts +28 -0
- package/dist/src/orchestration/organisationDeploy/cascadeExecution.js +1 -0
- package/dist/src/orchestration/organisationDeploy/infraSteps.d.ts +40 -0
- package/dist/src/orchestration/organisationDeploy/infraSteps.js +1 -0
- package/dist/src/orchestration/organisationDeploy/orgCascadeDeploy.d.ts +8 -0
- package/dist/src/orchestration/organisationDeploy/orgCascadeDeploy.js +5 -0
- package/dist/src/orchestration/organisationDeploy/orgContext.d.ts +12 -0
- package/dist/src/orchestration/organisationDeploy/orgContext.js +1 -0
- package/dist/src/orchestration/organisationDeploy/resolveCascadeAccounts.d.ts +15 -0
- package/dist/src/orchestration/organisationDeploy/resolveCascadeAccounts.js +1 -0
- package/dist/src/orchestration/organisationDeploy/singleComponentDeploy.d.ts +11 -0
- package/dist/src/orchestration/organisationDeploy/singleComponentDeploy.js +1 -0
- package/dist/src/orchestration/organisationDeploy/trailReconciliation.d.ts +21 -0
- package/dist/src/orchestration/organisationDeploy/trailReconciliation.js +1 -0
- package/dist/src/orchestration/organisationDeploy.d.ts +1 -5
- package/dist/src/orchestration/organisationDeploy.js +1 -5
- package/dist/src/orchestration/organisationDestroy.d.ts +1 -1
- package/dist/src/orchestration/organisationDestroy.js +2 -2
- package/dist/src/orchestration/organisationSetup.d.ts +23 -3
- package/dist/src/orchestration/organisationSetup.js +1 -1
- package/dist/src/orchestration/reconcileProviderAccounts.js +1 -1
- package/dist/src/orchestration/stackCleanup/bucketOps.d.ts +54 -0
- package/dist/src/orchestration/stackCleanup/bucketOps.js +1 -0
- package/dist/src/orchestration/stackCleanup/failedStack.d.ts +34 -0
- package/dist/src/orchestration/stackCleanup/failedStack.js +1 -0
- package/dist/src/orchestration/stackCleanup/logging.d.ts +9 -0
- package/dist/src/orchestration/stackCleanup/logging.js +1 -0
- package/dist/src/orchestration/stackCleanup/messages.d.ts +16 -0
- package/dist/src/orchestration/stackCleanup/messages.js +1 -0
- package/dist/src/orchestration/stackCleanup/orphanSweep.d.ts +25 -0
- package/dist/src/orchestration/stackCleanup/orphanSweep.js +1 -0
- package/dist/src/orchestration/stackCleanup/preEmpty.d.ts +35 -0
- package/dist/src/orchestration/stackCleanup/preEmpty.js +1 -0
- package/dist/src/orchestration/stackCleanup/stackResources.d.ts +9 -0
- package/dist/src/orchestration/stackCleanup/stackResources.js +1 -0
- package/dist/src/orchestration/stackCleanup.d.ts +13 -33
- package/dist/src/orchestration/stackCleanup.js +1 -1
- package/dist/src/orchestration/trailMigration/memberTrailCleanup.d.ts +43 -0
- package/dist/src/orchestration/trailMigration/memberTrailCleanup.js +1 -0
- package/dist/src/orchestration/trailMigration/trailMigration.d.ts +64 -0
- package/dist/src/orchestration/trailMigration/trailMigration.js +1 -0
- package/dist/src/orchestration/unlock/scpRemediation.d.ts +15 -0
- package/dist/src/orchestration/unlock/scpRemediation.js +1 -0
- package/dist/src/orchestration/unlock/unlockBucket.d.ts +37 -0
- package/dist/src/orchestration/unlock/unlockBucket.js +1 -0
- package/dist/src/orchestration/unlock/unlockQueue.d.ts +43 -0
- package/dist/src/orchestration/unlock/unlockQueue.js +1 -0
- package/dist/src/services/application/ApplicationStackService.d.ts +9 -10
- package/dist/src/services/application/ApplicationStackService.js +1 -1
- package/dist/src/services/application/applicationStackHelpers.d.ts +13 -8
- package/dist/src/services/application/applicationStackHelpers.js +3 -3
- package/dist/src/services/infrastructure/CdkArgumentBuilder.js +1 -1
- package/dist/src/services/infrastructure/CdkServiceTypes.d.ts +1 -0
- package/dist/src/services/infrastructure/cdkServiceHelpers.js +1 -1
- package/dist/src/services/supporting/CdkContextBuilder.d.ts +1 -0
- package/dist/src/services/supporting/CdkContextBuilder.js +1 -1
- package/dist/src/steps/stepRegistry.js +1 -1
- package/dist/src/types/FjallState.d.ts +7 -0
- package/dist/src/types/FjallState.js +1 -1
- package/dist/src/types/callbackKeys.d.ts +1 -1
- package/dist/src/types/callbackKeys.js +1 -1
- package/dist/src/types/callbacks.d.ts +58 -3
- package/dist/src/types/callbacks.js +1 -0
- package/dist/src/types/deployment/DeploymentTypes.d.ts +1 -0
- package/dist/src/types/deploymentEventSchema.d.ts +28 -3
- package/dist/src/types/deploymentEventSchema.js +1 -1
- package/dist/src/types/events.d.ts +12 -0
- package/dist/src/types/events.js +1 -0
- package/dist/src/types/index.d.ts +7 -11
- package/dist/src/types/index.js +1 -1
- package/dist/src/types/orgConfig.d.ts +8 -2
- package/dist/src/types/params.d.ts +18 -0
- package/dist/src/types/patternDetection.d.ts +0 -25
- package/dist/src/types/patternDetection.js +1 -1
- package/dist/src/types/stepDefinitions.d.ts +2 -0
- package/dist/src/types/stepDefinitions.js +1 -1
- package/dist/src/util/index.d.ts +1 -0
- package/dist/src/util/index.js +1 -1
- package/dist/src/util/sleepAbortable.d.ts +8 -0
- package/dist/src/util/sleepAbortable.js +1 -0
- package/package.json +8 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{success as m,failure as k}from"@fjall/generator";import{logger as h}from"@fjall/util/logger";import{getErrorMessage as n,maskSensitiveOutput as d}from"@fjall/util";import{isAborted as y}from"../../aws/organisations/types.js";import{STACK_NOT_FOUND_PATTERN as g}from"../../types/constants.js";import{classifyBucketForPreEmpty as S,emptyS3Bucket as B}from"./bucketOps.js";import{STACK_CLEANUP_LOG as w,warnViaCallbacks as a}from"./logging.js";import{formatRetainedBucketsMessage as A}from"./messages.js";import{collectStackResources as E}from"./stackResources.js";async function b(i,c,s,o,p){let u;try{u=await E(i,s,e=>e.ResourceType==="AWS::S3::Bucket",e=>e.PhysicalResourceId,p)}catch(e){if(e instanceof Error&&e.message?.includes(g))return h.debug(w,`Stack ${s} does not exist, nothing to pre-empty`),m({matched:[],skipped:[]});const r=`Could not list ${s} resources for the pre-empty pass: ${d(n(e))}`;return a(r,o),k(new Error(r))}const t={matched:[],skipped:[]};for(const e of u){if(y(p))break;try{const r=await S(c,e,o,p);r==="matched"?(t.matched.push(e),await B(c,e,o,p)):(r==="retained"||r==="unreadable")&&t.skipped.push(e)}catch(r){const f=`Pre-empty pass failed for bucket ${e}: ${d(n(r))}`;a(f,o)}}return t.skipped.length>0&&o?.onStackCleanupProgress?.(s,"retained-buckets",{retainedBuckets:[...t.skipped],message:A(s,t.skipped)}),m(t)}function v(i){return i.success?i.data.matched:[]}export{v as capturedSweepBuckets,b as preEmptyStackBuckets};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type CloudFormationClient } from "@aws-sdk/client-cloudformation";
|
|
2
|
+
/** Paginate ListStackResources with a bounded page limit and collect matching resource IDs. */
|
|
3
|
+
export declare function collectStackResources(cfnClient: CloudFormationClient, stackName: string, filter: (resource: {
|
|
4
|
+
ResourceType?: string;
|
|
5
|
+
ResourceStatus?: string;
|
|
6
|
+
}) => boolean, getId: (resource: {
|
|
7
|
+
PhysicalResourceId?: string;
|
|
8
|
+
LogicalResourceId?: string;
|
|
9
|
+
}) => string | undefined, abortSignal?: AbortSignal): Promise<string[]>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{ListStackResourcesCommand as S}from"@aws-sdk/client-cloudformation";import{logger as k}from"@fjall/util/logger";import{composeSdkAbortSignal as l,isAborted as p}from"../../aws/organisations/types.js";import{STACK_CLEANUP_LOG as u}from"./logging.js";async function w(n,o,a,m,t){const r=[];let e,f=0;do{if(p(t))break;if(f++>=100){k.warn(u,"Reached 100 page limit listing stack resources",{stackName:o});break}const s=await n.send(new S({StackName:o,NextToken:e}),{abortSignal:l(t)});for(const c of s.StackResourceSummaries??[])if(a(c)){const i=m(c);i&&r.push(i)}e=s.NextToken}while(e);return r}export{w as collectStackResources};
|
|
@@ -1,36 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Stack-cleanup barrel — implementations live in `stackCleanup/<concern>.ts`.
|
|
3
|
+
* This path is the stable import surface for the production consumers
|
|
4
|
+
* (orchestration barrel, cascade/organisation destroy, trail migration,
|
|
5
|
+
* ApplicationStackService); keep every public symbol re-exported here.
|
|
3
6
|
*
|
|
4
|
-
*
|
|
5
|
-
* -
|
|
6
|
-
* - ROLLBACK_COMPLETE -- creation failed, rollback succeeded
|
|
7
|
-
* - DELETE_FAILED -- deletion already started
|
|
8
|
-
*
|
|
9
|
-
* Never touches UPDATE_ROLLBACK_FAILED (has live resources from previous deploy).
|
|
10
|
-
*
|
|
11
|
-
* Ported from cli/src/services/utils/stackCleanup.ts for deploy-core consumers
|
|
12
|
-
* (webapp worker, CLI via deploy-core).
|
|
13
|
-
*/
|
|
14
|
-
import type { DeployCallbacks } from "../types/callbacks.js";
|
|
15
|
-
import { SAFE_CLEANUP_STATES, isCleanableState } from "../types/constants.js";
|
|
16
|
-
export { SAFE_CLEANUP_STATES, isCleanableState };
|
|
17
|
-
interface StackCleanupCredentials {
|
|
18
|
-
accessKeyId: string;
|
|
19
|
-
secretAccessKey: string;
|
|
20
|
-
sessionToken?: string;
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Clean up a CloudFormation stack stuck in a failed state.
|
|
24
|
-
* Best-effort: all errors are caught and logged, never throws.
|
|
25
|
-
*
|
|
26
|
-
* Flow:
|
|
27
|
-
* 1. Check stack status -- skip if not in SAFE_CLEANUP_STATES
|
|
28
|
-
* 2. Find and empty S3 buckets blocking deletion
|
|
29
|
-
* 3. Delete the stack
|
|
30
|
-
* 4. Poll for DELETE_COMPLETE (5min timeout, 5s interval)
|
|
31
|
-
* 5. If DELETE_FAILED again, retry once with RetainResources for non-S3 failures
|
|
7
|
+
* Originally ported from cli/src/services/utils/stackCleanup.ts for
|
|
8
|
+
* deploy-core consumers (webapp worker, CLI via deploy-core).
|
|
32
9
|
*/
|
|
33
|
-
export
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
10
|
+
export { SAFE_CLEANUP_STATES, isCleanableState } from "../types/constants.js";
|
|
11
|
+
export { isQuarantineDetail, isRetainedBucketsDetail } from "../types/callbacks.js";
|
|
12
|
+
export { BUCKET_GONE_ERROR_NAMES, PRE_EMPTY_TAG_KEYS, emptyS3Bucket, isBucketGoneError, type EmptyBucketOutcome } from "./stackCleanup/bucketOps.js";
|
|
13
|
+
export { formatQuarantineSuspectedMessage, formatRetainedBucketsMessage } from "./stackCleanup/messages.js";
|
|
14
|
+
export { capturedSweepBuckets, preEmptyStackBuckets, type PreEmptyBucketsSummary } from "./stackCleanup/preEmpty.js";
|
|
15
|
+
export { cleanupFailedStack } from "./stackCleanup/failedStack.js";
|
|
16
|
+
export { sweepOrphanedDestroyBuckets, type OrphanSweepSummary } from "./stackCleanup/orphanSweep.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{SAFE_CLEANUP_STATES as r,isCleanableState as a}from"../types/constants.js";import{isQuarantineDetail as p,isRetainedBucketsDetail as s}from"../types/callbacks.js";import{BUCKET_GONE_ERROR_NAMES as u,PRE_EMPTY_TAG_KEYS as E,emptyS3Bucket as i,isBucketGoneError as m}from"./stackCleanup/bucketOps.js";import{formatQuarantineSuspectedMessage as n,formatRetainedBucketsMessage as f}from"./stackCleanup/messages.js";import{capturedSweepBuckets as B,preEmptyStackBuckets as _}from"./stackCleanup/preEmpty.js";import{cleanupFailedStack as d}from"./stackCleanup/failedStack.js";import{sweepOrphanedDestroyBuckets as R}from"./stackCleanup/orphanSweep.js";export{u as BUCKET_GONE_ERROR_NAMES,E as PRE_EMPTY_TAG_KEYS,r as SAFE_CLEANUP_STATES,B as capturedSweepBuckets,d as cleanupFailedStack,i as emptyS3Bucket,n as formatQuarantineSuspectedMessage,f as formatRetainedBucketsMessage,m as isBucketGoneError,a as isCleanableState,p as isQuarantineDetail,s as isRetainedBucketsDetail,_ as preEmptyStackBuckets,R as sweepOrphanedDestroyBuckets};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type S3Client } from "@aws-sdk/client-s3";
|
|
2
|
+
import { type CloudTrailClient } from "@aws-sdk/client-cloudtrail";
|
|
3
|
+
import { type KMSClient } from "@aws-sdk/client-kms";
|
|
4
|
+
import { type Result } from "@fjall/generator";
|
|
5
|
+
import type { DeployCallbacks } from "../../types/callbacks.js";
|
|
6
|
+
export interface DecommissionClients {
|
|
7
|
+
cloudTrailClient: CloudTrailClient;
|
|
8
|
+
s3Client: S3Client;
|
|
9
|
+
kmsClient: KMSClient;
|
|
10
|
+
}
|
|
11
|
+
export interface DecommissionInput {
|
|
12
|
+
accountId: string;
|
|
13
|
+
/** Trail name the CFN draining deploy should have deleted. */
|
|
14
|
+
trailName: string;
|
|
15
|
+
bucketName: string;
|
|
16
|
+
/** CMK to schedule for deletion; omitted ⇒ KMS step is skipped. */
|
|
17
|
+
keyArn?: string;
|
|
18
|
+
acknowledgeTrailHistoryLoss: boolean;
|
|
19
|
+
abortSignal?: AbortSignal;
|
|
20
|
+
}
|
|
21
|
+
export type DecommissionOutcome = {
|
|
22
|
+
outcome: "decommissioned";
|
|
23
|
+
} | {
|
|
24
|
+
outcome: "blocked-trail-still-exists";
|
|
25
|
+
} | {
|
|
26
|
+
outcome: "blocked-awaiting-acknowledgement";
|
|
27
|
+
bucketName: string;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Decommission a drained member account's trail storage: empty + delete the
|
|
31
|
+
* retained bucket, then schedule the CMK for deletion. Every step probes
|
|
32
|
+
* first and treats already-gone as success, so the whole function converges
|
|
33
|
+
* on re-run after any partial failure.
|
|
34
|
+
*
|
|
35
|
+
* The trail resource itself is never deleted here — CloudFormation owns it,
|
|
36
|
+
* and a trail still present means the draining deploy has not happened
|
|
37
|
+
* (or rolled back), so nothing is touched.
|
|
38
|
+
*
|
|
39
|
+
* Bucket deletion is SDK-side by design: the bucket is RETAIN in the
|
|
40
|
+
* template, and CloudFormation must never be pointed at a non-empty
|
|
41
|
+
* CloudTrail bucket (delivery continues mid-delete, wedging the stack).
|
|
42
|
+
*/
|
|
43
|
+
export declare function decommissionMemberTrailStorage(clients: DecommissionClients, input: DecommissionInput, callbacks?: DeployCallbacks): Promise<Result<DecommissionOutcome>>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{DeleteBucketCommand as f,HeadBucketCommand as y,ListObjectVersionsCommand as k}from"@aws-sdk/client-s3";import{DescribeTrailsCommand as g}from"@aws-sdk/client-cloudtrail";import{DescribeKeyCommand as w,ScheduleKeyDeletionCommand as S}from"@aws-sdk/client-kms";import{success as o,failure as a}from"@fjall/generator";import{getErrorMessage as s,maskSensitiveOutput as c}from"@fjall/util";import{logger as E}from"@fjall/util/logger";import{composeSdkAbortSignal as l,extractErrorName as m}from"../../aws/organisations/types.js";import{emptyS3Bucket as C,isBucketGoneError as b}from"../stackCleanup.js";const p=14,N=new Set(["NotFoundException","KMSInvalidStateException"]);async function D(t,e,n){try{const r=await t.send(new g({trailNameList:[e]}),{abortSignal:l(n)});return o((r.trailList??[]).length>0)}catch(r){return m(r)==="TrailNotFoundException"?o(!1):a(new Error(`Failed to probe trail ${e}: ${c(s(r))}`))}}async function h(t,e,n){try{return await t.send(new y({Bucket:e}),{abortSignal:l(n)}),o(!0)}catch(r){return b(r)?o(!1):a(new Error(`Failed to probe bucket ${e}: ${c(s(r))}`))}}async function K(t,e,n){try{const r=await t.send(new k({Bucket:e,MaxKeys:1}),{abortSignal:l(n)});return(r.Versions??[]).length===0&&(r.DeleteMarkers??[]).length===0}catch(r){return E.debug("memberTrailCleanup",`Emptiness probe failed for ${e}; treating as not proven empty`,{error:c(s(r))}),!1}}async function M(t,e,n){try{const i=(await t.send(new w({KeyId:e}),{abortSignal:l(n)})).KeyMetadata?.KeyState;if(i==="PendingDeletion"||i==="PendingReplicaDeletion")return o(void 0)}catch(r){return m(r)==="NotFoundException"?o(void 0):a(new Error(`Failed to probe CMK: ${c(s(r))}`))}try{return await t.send(new S({KeyId:e,PendingWindowInDays:p}),{abortSignal:l(n)}),o(void 0)}catch(r){return N.has(m(r))?o(void 0):a(new Error(`Failed to schedule CMK deletion: ${c(s(r))}`))}}async function O(t,e,n){const r=await D(t.cloudTrailClient,e.trailName,e.abortSignal);if(!r.success)return a(r.error);if(r.data)return o({outcome:"blocked-trail-still-exists"});const i=await h(t.s3Client,e.bucketName,e.abortSignal);if(!i.success)return a(i.error);if(i.data){if(!await K(t.s3Client,e.bucketName,e.abortSignal)&&e.acknowledgeTrailHistoryLoss!==!0)return o({outcome:"blocked-awaiting-acknowledgement",bucketName:e.bucketName});await C(t.s3Client,e.bucketName,n,e.abortSignal);try{await t.s3Client.send(new f({Bucket:e.bucketName}),{abortSignal:l(e.abortSignal)})}catch(u){if(!b(u))return a(new Error(`Failed to delete bucket ${e.bucketName}: ${c(s(u))}`))}}if(e.keyArn!==void 0&&e.keyArn!==""){const d=await M(t.kmsClient,e.keyArn,e.abortSignal);if(!d.success)return a(d.error)}return o({outcome:"decommissioned"})}export{O as decommissionMemberTrailStorage};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { type S3Client } from "@aws-sdk/client-s3";
|
|
2
|
+
import { type Result } from "@fjall/generator";
|
|
3
|
+
import { ORG_TRAIL_BUCKET_OUTPUT_KEY, TRAIL_BUCKET_OUTPUT_KEY, TRAIL_KEY_ARN_OUTPUT_KEY, type ProviderAccount, type TrailLifecycleState } from "@fjall/util/config";
|
|
4
|
+
import { type DecommissionClients, type DecommissionOutcome } from "./memberTrailCleanup.js";
|
|
5
|
+
import type { DeployCallbacks } from "../../types/callbacks.js";
|
|
6
|
+
export { ORG_TRAIL_BUCKET_OUTPUT_KEY, TRAIL_BUCKET_OUTPUT_KEY, TRAIL_KEY_ARN_OUTPUT_KEY };
|
|
7
|
+
export interface MemberTrailFacts {
|
|
8
|
+
/**
|
|
9
|
+
* Org-trail delivery verified for this account. `undefined` = could not be
|
|
10
|
+
* determined (probe failed or recency scan truncated) — never advance on it.
|
|
11
|
+
*/
|
|
12
|
+
orgDeliveryVerified?: boolean;
|
|
13
|
+
/** Outcome of this run's decommission attempt (draining accounts only). */
|
|
14
|
+
decommissionOutcome?: DecommissionOutcome["outcome"] | "failed";
|
|
15
|
+
}
|
|
16
|
+
export type TrailMigrationTransition = {
|
|
17
|
+
action: "advance";
|
|
18
|
+
to: TrailLifecycleState;
|
|
19
|
+
} | {
|
|
20
|
+
action: "hold";
|
|
21
|
+
reason: string;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Pure transition truth table for the org-trail migration. Uncertainty never
|
|
25
|
+
* advances state: a probe that failed or could not complete holds the account
|
|
26
|
+
* where it is and reports why.
|
|
27
|
+
*/
|
|
28
|
+
export declare function decideNextTransition(state: TrailLifecycleState | undefined, facts: MemberTrailFacts): TrailMigrationTransition;
|
|
29
|
+
export interface TrailMigrationDeps {
|
|
30
|
+
/** Management-account S3 client — the org trail bucket lives there. */
|
|
31
|
+
managementS3Client: S3Client;
|
|
32
|
+
/** Assume the member role and build the decommission clients. */
|
|
33
|
+
getMemberClients: (account: ProviderAccount, region: string) => Promise<Result<DecommissionClients>>;
|
|
34
|
+
}
|
|
35
|
+
export interface TrailMigrationInput {
|
|
36
|
+
orgId: string;
|
|
37
|
+
orgTrailBucketName: string;
|
|
38
|
+
/** Cascade-deployed accounts (platform + members). */
|
|
39
|
+
accounts: ProviderAccount[];
|
|
40
|
+
/** accountId → home-region stack outputs captured by this run's cascade. */
|
|
41
|
+
cascadeOutputs: Map<string, Record<string, string>>;
|
|
42
|
+
defaultRegion: string;
|
|
43
|
+
recencyWindowMs?: number;
|
|
44
|
+
abortSignal?: AbortSignal;
|
|
45
|
+
}
|
|
46
|
+
export interface TrailMigrationOutcome {
|
|
47
|
+
accountId: string;
|
|
48
|
+
accountName: string;
|
|
49
|
+
from: TrailLifecycleState | undefined;
|
|
50
|
+
/** Set only when the account advanced this run. */
|
|
51
|
+
to?: TrailLifecycleState;
|
|
52
|
+
heldReason?: string;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Reconcile every cascade account's trail lifecycle against AWS ground truth.
|
|
56
|
+
* Two-invocation convergence by design: deploy N creates the org trail
|
|
57
|
+
* (overlap with member trails is sanctioned for up to a day), deploy N+1's
|
|
58
|
+
* cheap probes advance states. Facts are re-derived from AWS on every run —
|
|
59
|
+
* the persisted lifecycle is an intent cache, so lost persistence self-heals.
|
|
60
|
+
*
|
|
61
|
+
* Never throws and never fails the deploy: every problem is reported through
|
|
62
|
+
* callbacks and the account simply holds its current state.
|
|
63
|
+
*/
|
|
64
|
+
export declare function reconcileTrailMigration(deps: TrailMigrationDeps, input: TrailMigrationInput, callbacks: DeployCallbacks): Promise<TrailMigrationOutcome[]>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{maskSensitiveOutput as h}from"@fjall/util";import{ACCOUNT_TRAIL_NAME as N,ORG_TRAIL_BUCKET_OUTPUT_KEY as $,TRAIL_BUCKET_OUTPUT_KEY as p,TRAIL_KEY_ARN_OUTPUT_KEY as T}from"@fjall/util/config";import{verifyOrgTrailDelivery as L}from"../../aws/cloudtrail/orgTrailDelivery.js";import{decommissionMemberTrailStorage as R}from"./memberTrailCleanup.js";function w(r,i){if(r==="account")return{action:"hold",reason:"pinned to a per-account trail"};if(r==="org")return{action:"hold",reason:"already migrated to the org trail"};if(r===void 0)return i.orgDeliveryVerified===!0?{action:"advance",to:"draining"}:{action:"hold",reason:i.orgDeliveryVerified===!1?"org-trail delivery not yet verified (first delivery can take up to 24h)":"org-trail delivery could not be determined"};switch(i.decommissionOutcome){case"decommissioned":return{action:"advance",to:"org"};case"blocked-trail-still-exists":return{action:"hold",reason:"per-account trail still exists \u2014 draining deploy not yet applied"};case"blocked-awaiting-acknowledgement":return{action:"hold",reason:"awaiting acknowledgeTrailHistoryLoss before discarding history"};case"failed":return{action:"hold",reason:"decommission attempt failed"};default:return{action:"hold",reason:"decommission was not attempted"}}}function a(r,i){r.onTrailMigrationPhase?.({...i,...i.detail!==void 0?{detail:h(i.detail)}:{}})}async function C(r,i,o){const d=[],m=i.accounts.filter(e=>e.trailLifecycle===void 0),I=i.accounts.filter(e=>e.trailLifecycle==="draining"),f=new Map;if(m.length>0){a(o,{accountId:"*",accountName:"organisation",phase:"verify-delivery",status:"started",detail:`Checking org-trail delivery for ${m.length} account(s) in s3://${i.orgTrailBucketName}`});const e=await L(r.managementS3Client,{bucketName:i.orgTrailBucketName,orgId:i.orgId,accountIds:m.map(n=>n.id),...i.recencyWindowMs!==void 0?{recencyWindowMs:i.recencyWindowMs}:{},...i.abortSignal!==void 0?{abortSignal:i.abortSignal}:{}});if(e.success){for(const n of e.data.perAccount)f.set(n.accountId,n.recentDelivery===void 0?void 0:n.delivered&&n.recentDelivery);a(o,{accountId:"*",accountName:"organisation",phase:"verify-delivery",status:"completed"})}else a(o,{accountId:"*",accountName:"organisation",phase:"verify-delivery",status:"failed",detail:e.error.message})}for(const e of m){const n={...f.get(e.id)!==void 0?{orgDeliveryVerified:f.get(e.id)}:{}},s=w(void 0,n);s.action==="advance"?(o.onTrailLifecycleChanged?.(e.id,void 0,s.to),o.onLog?.(`${e.name}: org-trail delivery verified \u2014 trail lifecycle advanced to "draining" (the next deploy removes its per-account trail)`,"info"),d.push({accountId:e.id,accountName:e.name,from:void 0,to:s.to})):d.push({accountId:e.id,accountName:e.name,from:void 0,heldReason:s.reason})}for(const e of I){const n=e.region??i.defaultRegion,s=i.cascadeOutputs.get(e.id),c=s?.[p],y=s?.[T];if(c===void 0||c===""){const t=`stack output ${p} unavailable for this run`;o.onProgress?.({type:"warning",message:`${e.name}: trail decommission skipped \u2014 ${t}. The account stays "draining"; the next deploy retries.`}),d.push({accountId:e.id,accountName:e.name,from:"draining",heldReason:t});continue}a(o,{accountId:e.id,accountName:e.name,phase:"decommission",status:"started"});let l;const u=await r.getMemberClients(e,n);if(!u.success)l={decommissionOutcome:"failed"},o.onProgress?.({type:"warning",message:h(`${e.name}: trail decommission failed \u2014 ${u.error.message}. The account stays "draining"; the next deploy retries.`)}),a(o,{accountId:e.id,accountName:e.name,phase:"decommission",status:"failed",detail:u.error.message});else{const t=await R(u.data,{accountId:e.id,trailName:N,bucketName:c,...y!==void 0&&y!==""?{keyArn:y}:{},acknowledgeTrailHistoryLoss:e.acknowledgeTrailHistoryLoss===!0,...i.abortSignal!==void 0?{abortSignal:i.abortSignal}:{}},o);if(!t.success)l={decommissionOutcome:"failed"},o.onProgress?.({type:"warning",message:h(`${e.name}: trail decommission failed \u2014 ${t.error.message}. The account stays "draining"; the next deploy retries.`)}),a(o,{accountId:e.id,accountName:e.name,phase:"decommission",status:"failed",detail:t.error.message});else if(l={decommissionOutcome:t.data.outcome},t.data.outcome==="blocked-awaiting-acknowledgement"){const v=`${e.name}: per-account trail removed, but its log history in s3://${c} will be DISCARDED on decommission. Back the bucket up if the history matters, then set acknowledgeTrailHistoryLoss: true for account ${e.id} and redeploy.`;o.onProgress?.({type:"warning",message:v}),a(o,{accountId:e.id,accountName:e.name,phase:"decommission",status:"blocked",detail:v})}else t.data.outcome==="blocked-trail-still-exists"?a(o,{accountId:e.id,accountName:e.name,phase:"decommission",status:"blocked",detail:"per-account trail still exists \u2014 the draining deploy has not been applied yet"}):a(o,{accountId:e.id,accountName:e.name,phase:"decommission",status:"completed",detail:`bucket s3://${c} removed; CMK scheduled for deletion`})}const g=w("draining",l);g.action==="advance"?(o.onTrailLifecycleChanged?.(e.id,"draining",g.to),o.onLog?.(`${e.name}: trail storage decommissioned \u2014 trail lifecycle advanced to "org"`,"info"),d.push({accountId:e.id,accountName:e.name,from:"draining",to:g.to})):d.push({accountId:e.id,accountName:e.name,from:"draining",heldReason:g.reason})}return d}export{$ as ORG_TRAIL_BUCKET_OUTPUT_KEY,p as TRAIL_BUCKET_OUTPUT_KEY,T as TRAIL_KEY_ARN_OUTPUT_KEY,w as decideNextTransition,C as reconcileTrailMigration};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resource a root-task unlock acts on. The kind only varies the message
|
|
3
|
+
* wording — both unlock engines share one remediation story (the SCP
|
|
4
|
+
* carve-out covers the five s3/sqs policy-management actions together).
|
|
5
|
+
*/
|
|
6
|
+
export interface UnlockResource {
|
|
7
|
+
kind: "bucket" | "queue";
|
|
8
|
+
name: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Failure copy for an AccessDenied hitting a root session mid-unlock.
|
|
12
|
+
* Coupled value at 2 sites (unlockBucket + unlockQueue): the remediation
|
|
13
|
+
* story must stay identical, so the copy lives here rather than per-engine.
|
|
14
|
+
*/
|
|
15
|
+
export declare function formatScpSuspectedFailure(action: string, resource: UnlockResource, accountId: string): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function n(t,e,o){return`Access denied for the root session calling ${t} on ${e.kind} ${e.name} (account ${o}). If the organisation's SCPs predate the root-task unlock carve-out, DenyRootUserActions may still block the five s3/sqs policy-management actions \u2014 re-deploy the organisation to refresh its SCPs, then retry.`}export{n as formatScpSuspectedFailure};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { STSClient } from "@aws-sdk/client-sts";
|
|
2
|
+
import { type Result } from "@fjall/generator";
|
|
3
|
+
export interface UnlockBucketInput {
|
|
4
|
+
/** Member account holding the quarantined bucket. */
|
|
5
|
+
accountId: string;
|
|
6
|
+
bucketName: string;
|
|
7
|
+
/** Bucket region — also pins the regional STS endpoint requirement. */
|
|
8
|
+
region: string;
|
|
9
|
+
abortSignal?: AbortSignal;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* "no-policy" means the bucket carries no policy at all — it is not
|
|
13
|
+
* policy-locked and there is nothing to unlock. Consumers phrase this
|
|
14
|
+
* differently from a real unlock.
|
|
15
|
+
*/
|
|
16
|
+
export type UnlockBucketReport = {
|
|
17
|
+
outcome: "unlocked";
|
|
18
|
+
capturedPolicy: string;
|
|
19
|
+
warning: string;
|
|
20
|
+
} | {
|
|
21
|
+
outcome: "no-policy";
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Remove a stranded deny policy from a quarantined bucket via an STS
|
|
25
|
+
* assume-root session bounded by the S3UnlockBucketPolicy root-task policy.
|
|
26
|
+
*
|
|
27
|
+
* Ordering is load-bearing: the stranded policy is captured with
|
|
28
|
+
* GetBucketPolicy BEFORE DeleteBucketPolicy destroys it — the captured JSON
|
|
29
|
+
* is the only forensic record of what the policy carried, and the operator
|
|
30
|
+
* needs it to re-put legitimate statements.
|
|
31
|
+
*
|
|
32
|
+
* The STS client must use management-account or delegated-admin credentials
|
|
33
|
+
* and carry an explicit region (assumeRootForTask enforces both). Session
|
|
34
|
+
* credentials are credential VALUES — they exist only inside this function
|
|
35
|
+
* and never reach a sink; error strings are masked.
|
|
36
|
+
*/
|
|
37
|
+
export declare function unlockBucket(stsClient: STSClient, input: UnlockBucketInput): Promise<Result<UnlockBucketReport>>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{S3Client as w,GetBucketPolicyCommand as b,DeleteBucketPolicyCommand as h}from"@aws-sdk/client-s3";import{success as l,failure as t}from"@fjall/generator";import{getErrorMessage as k,maskSensitiveOutput as m}from"@fjall/util";import{composeSdkAbortSignal as y,extractErrorName as p,isAccessDenied as f}from"../../aws/organisations/types.js";import{assumeRootForTask as E,MAX_ROOT_SESSION_SECONDS as B}from"../../aws/sts/assumeRoot.js";import{formatScpSuspectedFailure as S}from"./scpRemediation.js";const P="arn:aws:iam::aws:policy/root-task/S3UnlockBucketPolicy";function N(n){return`The unlock deleted the ENTIRE bucket policy on ${n} \u2014 including any enforceSSL deny statements and legitimate access grants it carried, not just the stranded deny. Review the captured policy and re-put sane statements (TLS-only deny, intended access grants) before relying on the bucket.`}async function L(n,g){const{accountId:c,bucketName:e,region:i,abortSignal:a}=g;if(i==="")return t(new Error("A bucket region is required to unlock a bucket."));const u=await E(n,{targetAccountId:c,taskPolicyArn:P,durationSeconds:B,abortSignal:a});if(!u.success)return t(u.error);const d=new w({region:i,credentials:u.data.credentials});let o;try{o=(await d.send(new b({Bucket:e}),{abortSignal:y(a)})).Policy}catch(r){const s=p(r);return s==="NoSuchBucketPolicy"?l({outcome:"no-policy"}):s==="NoSuchBucket"?t(new Error(`Bucket ${e} was not found in region ${i} \u2014 check the bucket name and region.`)):f(s)?t(new Error(S("GetBucketPolicy",{kind:"bucket",name:e},c))):t(new Error(`Failed to capture the bucket policy on ${e}: ${m(k(r))}`))}if(o===void 0||o==="")return l({outcome:"no-policy"});try{await d.send(new h({Bucket:e}),{abortSignal:y(a)})}catch(r){return f(p(r))?t(new Error(S("DeleteBucketPolicy",{kind:"bucket",name:e},c))):t(new Error(`Captured the bucket policy on ${e} but failed to delete it: ${m(k(r))}`))}return l({outcome:"unlocked",capturedPolicy:o,warning:N(e)})}export{L as unlockBucket};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { STSClient } from "@aws-sdk/client-sts";
|
|
2
|
+
import { type Result } from "@fjall/generator";
|
|
3
|
+
export interface UnlockQueueInput {
|
|
4
|
+
/** Member account holding the policy-locked queue. */
|
|
5
|
+
accountId: string;
|
|
6
|
+
queueName: string;
|
|
7
|
+
/** Queue region — also pins the regional STS endpoint requirement. */
|
|
8
|
+
region: string;
|
|
9
|
+
abortSignal?: AbortSignal;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* "no-policy" means the queue carries no resource policy at all — it is not
|
|
13
|
+
* policy-locked and there is nothing to unlock. Consumers phrase this
|
|
14
|
+
* differently from a real unlock.
|
|
15
|
+
*/
|
|
16
|
+
export type UnlockQueueReport = {
|
|
17
|
+
outcome: "unlocked";
|
|
18
|
+
capturedPolicy: string;
|
|
19
|
+
warning: string;
|
|
20
|
+
} | {
|
|
21
|
+
outcome: "no-policy";
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Remove a stranded deny policy from a locked SQS queue via an STS
|
|
25
|
+
* assume-root session bounded by the SQSUnlockQueuePolicy root-task policy.
|
|
26
|
+
*
|
|
27
|
+
* Ordering is load-bearing: the stranded policy is captured with
|
|
28
|
+
* GetQueueAttributes BEFORE SetQueueAttributes clears it — the captured JSON
|
|
29
|
+
* is the only forensic record of what the policy carried, and the operator
|
|
30
|
+
* needs it to re-put legitimate statements. SQS has no DeleteQueuePolicy
|
|
31
|
+
* API; setting Policy to the empty string is the documented removal path.
|
|
32
|
+
*
|
|
33
|
+
* The queue URL is composed deterministically from region/account/name
|
|
34
|
+
* because GetQueueUrl is not in SQSUnlockQueuePolicy. This assumes the
|
|
35
|
+
* standard aws partition URL shape (sqs.<region>.amazonaws.com) — GovCloud
|
|
36
|
+
* and CN partitions are out of scope for the unlock engines.
|
|
37
|
+
*
|
|
38
|
+
* The STS client must use management-account or delegated-admin credentials
|
|
39
|
+
* and carry an explicit region (assumeRootForTask enforces both). Session
|
|
40
|
+
* credentials are credential VALUES — they exist only inside this function
|
|
41
|
+
* and never reach a sink; error strings are masked.
|
|
42
|
+
*/
|
|
43
|
+
export declare function unlockQueue(stsClient: STSClient, input: UnlockQueueInput): Promise<Result<UnlockQueueReport>>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{SQSClient as A,GetQueueAttributesCommand as Q,SetQueueAttributesCommand as N}from"@aws-sdk/client-sqs";import{success as d,failure as t}from"@fjall/generator";import{getErrorMessage as S,maskSensitiveOutput as p}from"@fjall/util";import{composeSdkAbortSignal as f,extractErrorName as w,isAccessDenied as g}from"../../aws/organisations/types.js";import{assumeRootForTask as q,MAX_ROOT_SESSION_SECONDS as b}from"../../aws/sts/assumeRoot.js";import{formatScpSuspectedFailure as y}from"./scpRemediation.js";const h="arn:aws:iam::aws:policy/root-task/SQSUnlockQueuePolicy",k=new Set(["QueueDoesNotExist","AWS.SimpleQueueService.NonExistentQueue"]);function O(i){return`The unlock cleared the ENTIRE queue policy on ${i} \u2014 including any legitimate access grants it carried (cross-account SendMessage permissions, S3/SNS/EventBridge event-source allow statements), not just the stranded deny. Review the captured policy and re-put sane statements before relying on the queue.`}async function v(i,E){const{accountId:o,queueName:e,region:n,abortSignal:s}=E;if(n==="")return t(new Error("A queue region is required to unlock a queue."));const a=await q(i,{targetAccountId:o,taskPolicyArn:h,durationSeconds:b,abortSignal:s});if(!a.success)return t(a.error);const c=new A({region:n,credentials:a.data.credentials}),l=`https://sqs.${n}.amazonaws.com/${o}/${e}`;let u;try{u=(await c.send(new Q({QueueUrl:l,AttributeNames:["Policy"]}),{abortSignal:f(s)})).Attributes?.Policy}catch(r){const m=w(r);return k.has(m)?t(new Error(`Queue ${e} was not found in region ${n} \u2014 check the queue name and region.`)):g(m)?t(new Error(y("GetQueueAttributes",{kind:"queue",name:e},o))):t(new Error(`Failed to capture the queue policy on ${e}: ${p(S(r))}`))}if(u===void 0||u==="")return d({outcome:"no-policy"});try{await c.send(new N({QueueUrl:l,Attributes:{Policy:""}}),{abortSignal:f(s)})}catch(r){return g(w(r))?t(new Error(y("SetQueueAttributes",{kind:"queue",name:e},o))):t(new Error(`Captured the queue policy on ${e} but failed to clear it: ${p(S(r))}`))}return d({outcome:"unlocked",capturedPolicy:u,warning:O(e)})}export{v as unlockQueue};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CdkService } from "../infrastructure/CdkService.js";
|
|
2
2
|
import type { CloudFormationService } from "../infrastructure/CloudFormationService.js";
|
|
3
3
|
import type { AwsProvider } from "../../aws/AwsProvider.js";
|
|
4
|
+
import type { DeployCallbacks } from "../../types/callbacks.js";
|
|
4
5
|
import type { ResourceEvent } from "../../types/events.js";
|
|
5
6
|
import type { DeploymentContext } from "../../types/deployment/DeploymentTypes.js";
|
|
6
7
|
import { type ApplicationStack } from "../../types/operations.js";
|
|
@@ -8,9 +9,12 @@ import type { ParallelDeploymentResult } from "../../types/deployment/parallel.j
|
|
|
8
9
|
import type { AppResourceFlags } from "../../types/patternDetection.js";
|
|
9
10
|
import { type Result } from "@fjall/generator";
|
|
10
11
|
import { ApplicationError, type StackDeploymentData } from "../../types/application/ApplicationServiceTypes.js";
|
|
12
|
+
import { type DestroyAllStacksCallbacks } from "./applicationStackHelpers.js";
|
|
11
13
|
interface StackCallbacks {
|
|
12
14
|
onOutput?: (chunk: string) => void;
|
|
13
15
|
onResourceProgress?: (event: ResourceEvent) => void;
|
|
16
|
+
onLog?: DeployCallbacks["onLog"];
|
|
17
|
+
onStackCleanupProgress?: DeployCallbacks["onStackCleanupProgress"];
|
|
14
18
|
}
|
|
15
19
|
interface DeployStackOptions {
|
|
16
20
|
/**
|
|
@@ -23,6 +27,8 @@ interface DeployStackOptions {
|
|
|
23
27
|
interface ParallelStackCallbacks {
|
|
24
28
|
onOutput?: (chunk: string, stackId: ApplicationStack) => void;
|
|
25
29
|
onResourceProgress?: (event: ResourceEvent, stackId: ApplicationStack) => void;
|
|
30
|
+
onLog?: DeployCallbacks["onLog"];
|
|
31
|
+
onStackCleanupProgress?: DeployCallbacks["onStackCleanupProgress"];
|
|
26
32
|
onStackComplete?: (stack: ApplicationStack, success: boolean, duration: number, error?: Error) => void;
|
|
27
33
|
}
|
|
28
34
|
/**
|
|
@@ -56,7 +62,7 @@ export declare class ApplicationStackService {
|
|
|
56
62
|
/**
|
|
57
63
|
* Destroy a specific stack type
|
|
58
64
|
*/
|
|
59
|
-
destroyStack(stackType: ApplicationStack, context: DeploymentContext, callbacks?: StackCallbacks, useCdkOut?: boolean): Promise<Result<StackDeploymentData, ApplicationError>>;
|
|
65
|
+
destroyStack(stackType: ApplicationStack, context: DeploymentContext, callbacks?: StackCallbacks, useCdkOut?: boolean, abortSignal?: AbortSignal): Promise<Result<StackDeploymentData, ApplicationError>>;
|
|
60
66
|
/**
|
|
61
67
|
* Destroy multiple stacks in parallel within a destroy phase.
|
|
62
68
|
*
|
|
@@ -64,7 +70,7 @@ export declare class ApplicationStackService {
|
|
|
64
70
|
* be run beforehand. Multiple CDK commands cannot run in parallel because they
|
|
65
71
|
* all try to synthesise to the same cdk.out directory.
|
|
66
72
|
*/
|
|
67
|
-
destroyStacksInParallel(stacks: readonly ApplicationStack[], context: DeploymentContext, callbacks?: ParallelStackCallbacks, useCdkOut?: boolean): Promise<Result<ParallelDeploymentResult[], ApplicationError>>;
|
|
73
|
+
destroyStacksInParallel(stacks: readonly ApplicationStack[], context: DeploymentContext, callbacks?: ParallelStackCallbacks, useCdkOut?: boolean, abortSignal?: AbortSignal): Promise<Result<ParallelDeploymentResult[], ApplicationError>>;
|
|
68
74
|
/**
|
|
69
75
|
* Get stack outputs for a deployed stack
|
|
70
76
|
*/
|
|
@@ -78,14 +84,7 @@ export declare class ApplicationStackService {
|
|
|
78
84
|
* Destroy all stacks for an application in reverse order.
|
|
79
85
|
* Supports parallel destruction for OpenNext patterns (nextjs/payload).
|
|
80
86
|
*/
|
|
81
|
-
destroyAllStacks(context: DeploymentContext, callbacks?: {
|
|
82
|
-
onOutput?: (chunk: string, stackId?: ApplicationStack) => void;
|
|
83
|
-
onResourceProgress?: (event: ResourceEvent, stackId?: ApplicationStack) => void;
|
|
84
|
-
onStackStart?: (stackType: ApplicationStack, stackName: string) => void;
|
|
85
|
-
onStackComplete?: (stackType: ApplicationStack, result: Result<StackDeploymentData, ApplicationError>) => void | Promise<void>;
|
|
86
|
-
onParallelPhaseStart?: (stacks: readonly ApplicationStack[], description: string) => void;
|
|
87
|
-
onParallelPhaseComplete?: (results: ParallelDeploymentResult[]) => void;
|
|
88
|
-
}, resources?: AppResourceFlags): Promise<Result<{
|
|
87
|
+
destroyAllStacks(context: DeploymentContext, callbacks?: DestroyAllStacksCallbacks, resources?: AppResourceFlags, abortSignal?: AbortSignal): Promise<Result<{
|
|
89
88
|
message: string;
|
|
90
89
|
}, ApplicationError>>;
|
|
91
90
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{STACK_NOT_FOUND_PATTERN as
|
|
1
|
+
import{CloudFormationClient as R}from"@aws-sdk/client-cloudformation";import{S3Client as v}from"@aws-sdk/client-s3";import{capturedSweepBuckets as D,preEmptyStackBuckets as N,sweepOrphanedDestroyBuckets as F}from"../../orchestration/stackCleanup.js";import{STACK_NOT_FOUND_PATTERN as _,CDK_NO_STACKS_MATCH as L}from"../../types/constants.js";import{getApplicationStackName as y}from"../../types/operations.js";import{success as g,failure as k}from"@fjall/generator";import{ApplicationError as h}from"../../types/application/ApplicationServiceTypes.js";import{logger as d}from"@fjall/util/logger";import{maskSensitiveOutput as P}from"@fjall/util";import{convertCloudFormationOutputsToRecord as O}from"../supporting/helpers.js";import{destroyAllStacks as $,mapSettledResults as A,resolveWebsiteUrl as B}from"./applicationStackHelpers.js";class z{cdkService;cloudFormationService;aws;constructor(e,s,t){this.cdkService=e,this.cloudFormationService=s,this.aws=t}async runCdkSynth(e){return this.cdkService.runCdkSynth(e)}async deployStack(e,s,t,a){const c=s.target,i=y(c,e),n=await this.cdkService.runCdkDeploy(s,i,t?.onOutput,t?.onResourceProgress,this.aws,void 0,a?.parameters);if(n.success){const o=n.data;d.debug("ApplicationStackService","CDK deploy result",{stackName:i,stackType:e,success:!0,message:o.message,status:o.status,skipped:o.details?.skipped,hasOutput:!!o.details?.output});const u=await this.cloudFormationService.getStackOutputs(i);d.debug("ApplicationStackService","Stack outputs after deploy",{stackName:i,hasOutputs:u.success&&u.data.length>0,outputCount:u.success?u.data.length:0});const r=u.success&&u.data.length>0?O(u.data):void 0;return g({stackType:e,stackName:i,skipped:o.details?.skipped===!0,outputs:r})}else{const o=P(n.error||`Failed to deploy ${e} infrastructure`);return d.error("ApplicationStackService","Stack deployment failed",{stackType:e,target:s.target,error:o}),k(new h(o,{errorType:"deployment_failed",appName:s.target,operation:"deployStack",stackType:e}))}}async deployStacksInParallel(e,s,t){const a=t?.onOutput,c=t?.onResourceProgress,i=t?.onStackComplete,n=await Promise.allSettled(e.map(async o=>{const u=Date.now(),r=await this.deployStack(o,s,{onOutput:a?m=>a(m,o):void 0,onResourceProgress:c?m=>c(m,o):void 0}),S=Date.now()-u,p={stack:o,success:r.success,error:r.success?void 0:r.error,duration:S,...r.success&&r.data.outputs!==void 0?{outputs:r.data.outputs}:{}};return!r.success&&r.error&&d.debug("deployStacksInParallel",`Stack ${o} failed`,{error:r.error.message}),i?.(o,r.success,S,r.success?void 0:r.error),p}));return g(A(n,e))}async destroyStack(e,s,t,a,c){const i=s.target,n=y(i,e);let o=[];if(this.aws!==void 0)if(a===!0||(await this.cdkService.runCdkSynth(s)).success){const S=await N(this.aws.getClient(R),this.aws.getClient(v),n,{onLog:t?.onLog,onStackCleanupProgress:t?.onStackCleanupProgress},c);o=D(S)}else d.debug("ApplicationStackService","Skipping pre-empty pass after failed validation synth \u2014 the destroy will surface the error",{stackName:n});else d.debug("ApplicationStackService","No AWS provider available, skipping pre-empty pass",{stackName:n});const u=await this.cdkService.runCdkDestroy(s,n,t?.onOutput,t?.onResourceProgress,this.aws,a);if(u.success)return this.aws!==void 0&&o.length>0&&await F(this.aws.getClient(v),o,{onLog:t?.onLog,onStackCleanupProgress:t?.onStackCleanupProgress},c),g({stackType:e,stackName:n,skipped:!1,outputs:void 0});{const r=P(u.error||`Failed to destroy ${e} stack`);return d.error("ApplicationStackService","Stack destroy failed",{stackType:e,target:s.target,error:r}),k(new h(r,{errorType:"destroy_failed",appName:s.target,operation:"destroyStack",stackType:e}))}}async destroyStacksInParallel(e,s,t,a,c){const i=t?.onOutput,n=t?.onResourceProgress,o=t?.onStackComplete,u=await Promise.allSettled(e.map(async r=>{const S=Date.now(),p=await this.destroyStack(r,s,{onOutput:i?f=>i(f,r):void 0,onResourceProgress:n?f=>n(f,r):void 0,onLog:t?.onLog,onStackCleanupProgress:t?.onStackCleanupProgress},a,c),m=Date.now()-S,C=p.success?"":p.error?.message??"",w=C.includes(_)||C.includes(L),l={stack:r,success:p.success||w,error:p.success||w?void 0:p.error,duration:m};return!l.success&&l.error&&d.debug("destroyStacksInParallel",`Stack ${r} failed`,{error:l.error.message}),o?.(r,l.success,m,l.success?void 0:l.error),l}));return g(A(u,e))}async getStackOutputs(e,s){const t=y(e,s),a=await this.cloudFormationService.getStackOutputs(t);return a.success?g(O(a.data)):k(new h(`Failed to get outputs for ${t}`,{errorType:"stack_error",appName:e,operation:"getStackOutputs",stackType:s,details:a.error}))}async resolveWebsiteUrl(e){return B(e,this.cloudFormationService)}async destroyAllStacks(e,s,t,a){return $(this,e,s,t,a)}}export{z as ApplicationStackService};
|
|
@@ -7,6 +7,18 @@ import { type Result } from "@fjall/generator";
|
|
|
7
7
|
import { ApplicationError, type StackDeploymentData } from "../../types/application/ApplicationServiceTypes.js";
|
|
8
8
|
import type { ApplicationStackService } from "./ApplicationStackService.js";
|
|
9
9
|
import type { CloudFormationService } from "../infrastructure/CloudFormationService.js";
|
|
10
|
+
import type { DeployCallbacks } from "../../types/callbacks.js";
|
|
11
|
+
/** Shared with ApplicationStackService.destroyAllStacks — the two signatures must not drift. */
|
|
12
|
+
export interface DestroyAllStacksCallbacks {
|
|
13
|
+
onOutput?: (chunk: string, stackId?: ApplicationStack) => void;
|
|
14
|
+
onResourceProgress?: (event: ResourceEvent, stackId?: ApplicationStack) => void;
|
|
15
|
+
onLog?: DeployCallbacks["onLog"];
|
|
16
|
+
onStackCleanupProgress?: DeployCallbacks["onStackCleanupProgress"];
|
|
17
|
+
onStackStart?: (stackType: ApplicationStack, stackName: string) => void;
|
|
18
|
+
onStackComplete?: (stackType: ApplicationStack, result: Result<StackDeploymentData, ApplicationError>) => void | Promise<void>;
|
|
19
|
+
onParallelPhaseStart?: (stacks: readonly ApplicationStack[], description: string) => void;
|
|
20
|
+
onParallelPhaseComplete?: (results: ParallelDeploymentResult[]) => void;
|
|
21
|
+
}
|
|
10
22
|
/**
|
|
11
23
|
* Convert "doesn't exist" errors to success for destroy operations.
|
|
12
24
|
*/
|
|
@@ -24,14 +36,7 @@ export declare function handleDestroyError(result: Result<StackDeploymentData, A
|
|
|
24
36
|
* Destroy all stacks for an application in reverse order.
|
|
25
37
|
* Supports parallel destruction for OpenNext patterns (nextjs/payload).
|
|
26
38
|
*/
|
|
27
|
-
export declare function destroyAllStacks(service: ApplicationStackService, context: DeploymentContext, callbacks?: {
|
|
28
|
-
onOutput?: (chunk: string, stackId?: ApplicationStack) => void;
|
|
29
|
-
onResourceProgress?: (event: ResourceEvent, stackId?: ApplicationStack) => void;
|
|
30
|
-
onStackStart?: (stackType: ApplicationStack, stackName: string) => void;
|
|
31
|
-
onStackComplete?: (stackType: ApplicationStack, result: Result<StackDeploymentData, ApplicationError>) => void | Promise<void>;
|
|
32
|
-
onParallelPhaseStart?: (stacks: readonly ApplicationStack[], description: string) => void;
|
|
33
|
-
onParallelPhaseComplete?: (results: ParallelDeploymentResult[]) => void;
|
|
34
|
-
}, resources?: AppResourceFlags): Promise<Result<{
|
|
39
|
+
export declare function destroyAllStacks(service: ApplicationStackService, context: DeploymentContext, callbacks?: DestroyAllStacksCallbacks, resources?: AppResourceFlags, abortSignal?: AbortSignal): Promise<Result<{
|
|
35
40
|
message: string;
|
|
36
41
|
}, ApplicationError>>;
|
|
37
42
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{maskSensitiveOutput as
|
|
2
|
-
`);return
|
|
1
|
+
import{maskSensitiveOutput as T}from"@fjall/util";import{STACK_NOT_FOUND_PATTERN as D,CDK_NO_STACKS_MATCH as N,INFRASTRUCTURE_FILENAME as $}from"../../types/constants.js";import{APPLICATION_STACKS as R,getApplicationStackName as g,getApplicationDestroyOrder as U,getParallelDestroyGroups as K}from"../../types/operations.js";import{FrameworkRegistry as F}from"../../orchestration/builders/frameworkRegistry.js";import{success as w,failure as P,isFailure as A}from"@fjall/generator";import{ApplicationError as C}from"../../types/application/ApplicationServiceTypes.js";import{logger as M}from"@fjall/util/logger";function E(t,r,e){if(t.success)return t;const s=A(t)?t.error.message:"";return s.includes(D)||s.includes(N)?w({stackName:g(e,r),stackType:r,outputs:{},skipped:!0}):t}function L(t,r){if(t.success)return null;const e=A(t)?T(t.error.message):"";if(e.includes("TSError")||e.includes("Unable to compile TypeScript")||e.includes("Subprocess exited with error")){const s=e.match(/infrastructure\.ts\(\d+,\d+\): error TS\d+: .+/),d=s?s[0]:`Check ${$} for syntax errors`;return{continue:!1,result:P(new C(`CDK synthesis failed: ${d}`,{errorType:"synth_failed",operation:`destroy-${r}`}))}}return e.includes(D)||e.includes(N)?{continue:!0,result:w({message:"Stack already deleted"})}:{continue:!1,result:P(new C(`Failed to destroy ${r} stack: ${e}`,{errorType:"destroy_failed",operation:`destroy-${r}`}))}}async function W(t,r,e,s,d){const u=r.target,f=F.createDefault().resolve({appPath:r.path});let y,k;if(f){const a=f.builder.plan({appPath:r.path},f.detection);y=a.parallelDestroy,k=a.destroyOrder}else y=!1,k=U({pattern:null,resources:s});if(y){const a=K();for(const h of a){const m=h.stacks;if(m.length===1){const n=m[0],l=g(u,n);e?.onStackStart?.(n,l);const o=await t.destroyStack(n,r,{onOutput:e?.onOutput?c=>e.onOutput?.(c,n):void 0,onResourceProgress:e?.onResourceProgress?c=>e.onResourceProgress?.(c,n):void 0,onLog:e?.onLog,onStackCleanupProgress:e?.onStackCleanupProgress},void 0,d),i=E(o,n,u);e?.onStackComplete&&await e.onStackComplete(n,i);const p=L(i,n);if(p){if(p.continue)continue;return p.result}}else{const n=await t.runCdkSynth(r);if(!n.success)return P(new C(n.error,{errorType:"synth_failed",appName:u,operation:"destroy-parallel-synth"}));e?.onParallelPhaseStart?.(m,h.description);for(const o of m)e?.onStackStart?.(o,g(u,o));const l=await t.destroyStacksInParallel(m,r,{onOutput:e?.onOutput,onResourceProgress:e?.onResourceProgress,onLog:e?.onLog,onStackCleanupProgress:e?.onStackCleanupProgress,onStackComplete:async(o,i,p,c)=>{const O=c?.message!==void 0?T(c.message):"Stack destruction failed",_=i?w({stackName:g(u,o),stackType:o,outputs:{}}):P(c instanceof C?c:new C(O,{errorType:"destroy_failed",appName:u,operation:`destroy-${o}`}));await e?.onStackComplete?.(o,_)}},!0,d);if(l.success){e?.onParallelPhaseComplete?.(l.data);const o=l.data.filter(i=>!i.success);if(o.length>0){const i=o.filter(p=>p.error&&!p.error.message.includes(D)&&!p.error.message.includes(N));if(i.length>0){const p=i.map(O=>O.stack).join(", "),c=i.map(O=>`${O.stack}: ${O.error?.message??"Unknown error"}`).join(`
|
|
2
|
+
`);return P(new C(`Failed to destroy stacks: ${p}
|
|
3
3
|
|
|
4
|
-
${
|
|
4
|
+
${c}`,{errorType:"destroy_failed",appName:u,operation:"destroy-parallel"}))}}}}}}else for(const a of k){const h=g(u,a);e?.onStackStart?.(a,h);const m=await t.destroyStack(a,r,{onOutput:e?.onOutput?o=>e.onOutput?.(o,a):void 0,onResourceProgress:e?.onResourceProgress?o=>e.onResourceProgress?.(o,a):void 0,onLog:e?.onLog,onStackCleanupProgress:e?.onStackCleanupProgress},void 0,d),n=E(m,a,u);e?.onStackComplete&&await e.onStackComplete(a,n);const l=L(n,a);if(l){if(l.continue)continue;return l.result}}return w({message:"All stacks destroyed"})}function H(t,r){return t.map((e,s)=>e.status==="fulfilled"?e.value:{stack:r[s]??R.NETWORK,success:!1,error:e.reason instanceof Error?e.reason:new Error(String(e.reason)),duration:0})}async function q(t,r){try{const e=g(t,R.COMPUTE),s=await r.getStackOutputs(e);if(s.success&&s.data.length>0){const S=s.data.find(y=>y.OutputKey?.includes("LoadBalancerUrl"));if(S?.OutputValue)return S.OutputValue.toLowerCase();const f=s.data.find(y=>y.OutputKey?.includes("LoadBalancerDnsName"));if(f?.OutputValue)return`http://${f.OutputValue.toLowerCase()}`}const d=g(t,R.CDN),u=await r.getStackOutputs(d);if(u.success&&u.data.length>0){const S=u.data.find(f=>f.OutputKey?.includes("DistributionDomainName"));if(S?.OutputValue)return`https://${S.OutputValue.toLowerCase()}`}return}catch(e){M.warn("ApplicationStackService","URL resolution failed",{error:T(e instanceof Error?e.message:String(e))});return}}export{E as convertDestroyResultIfNotExists,W as destroyAllStacks,L as handleDestroyError,H as mapSettledResults,q as resolveWebsiteUrl};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{filterDangerousEnvVars as u}from"@fjall/util";class s{buildContextArgs(r){const
|
|
1
|
+
import{filterDangerousEnvVars as u}from"@fjall/util";class s{buildContextArgs(r){const a=[];return r?.accountId&&a.push("-c",`accountId=${r.accountId}`),r?.environment&&a.push("-c",`environment=${r.environment}`),r?.managedAccount&&a.push("-c","managedAccount=true"),r?.accountName&&a.push("-c",`accountName=${r.accountName}`),r?.orgId&&a.push("-c",`orgId=${r.orgId}`),r?.rootId&&a.push("-c",`rootId=${r.rootId}`),r?.managementAccountId&&a.push("-c",`managementAccountId=${r.managementAccountId}`),r?.ipamPoolId&&a.push("-c",`ipamPoolId=${r.ipamPoolId}`),r?.fjallOrgId&&a.push("-c",`fjallOrgId=${r.fjallOrgId}`),r?.fjallOidcConfigured&&a.push("-c",`fjallOidcConfigured=${r.fjallOidcConfigured}`),r?.fjallAccountGlobalsConfigured&&a.push("-c",`fjallAccountGlobalsConfigured=${r.fjallAccountGlobalsConfigured}`),r?.fjallAccountTrailState&&a.push("-c",`fjallAccountTrailState=${r.fjallAccountTrailState}`),r?.orgConfig&&a.push("-c",`orgConfig=${r.orgConfig}`),r?.fjallAdoptBackupVault&&a.push("-c","fjallAdoptBackupVault=true"),r?.resolvedSecretArns&&a.push("-c",`fjallResolvedSecretArns=${r.resolvedSecretArns}`),a}buildParameterArgs(r){if(r===void 0)return[];const a=Object.entries(r);if(a.length===0)return[];const n=[];for(const[e,l]of a){if(!/^[A-Za-z][A-Za-z0-9]*$/.test(e))throw new Error(`Invalid CloudFormation parameter name "${e}": must match /^[A-Za-z][A-Za-z0-9]*$/ (alphanumeric, leading letter, no separators).`);if(l==="")throw new Error(`CloudFormation parameter "${e}" has an empty value.`);if(/[,\n\r]/.test(l))throw new Error(`CloudFormation parameter "${e}" value contains "," or newline \u2014 cdk's --parameters splits on "," so the deploy would silently fragment.`);n.push("--parameters",`${e}=${l}`)}return n}injectCascadeCredentials(r,a){a&&(r.AWS_ACCESS_KEY_ID=a.accessKeyId,r.AWS_SECRET_ACCESS_KEY=a.secretAccessKey,delete r.AWS_SESSION_TOKEN,a.sessionToken&&(r.AWS_SESSION_TOKEN=a.sessionToken))}buildCdkEnv(r){const a={...u(process.env),CI:"true",FORCE_COLOR:"0",CDK_DISABLE_VERSION_CHECK:"1"};return r?.context?.region&&(a.AWS_REGION=r.context.region,a.AWS_DEFAULT_REGION=r.context.region,a.CDK_DEFAULT_REGION=r.context.region),r?.context?.accountId&&(a.CDK_DEFAULT_ACCOUNT=r.context.accountId),this.injectCascadeCredentials(a,r?.credentials),a}}export{s as CdkArgumentBuilder};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{getApplicationStackName as l,getOrganisationStackName as i,isApplicationStack as u}from"../../types/operations.js";const
|
|
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 +1 @@
|
|
|
1
|
-
import{DEFAULT_REGION as
|
|
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 +1 @@
|
|
|
1
|
-
import{APPLICATION_STACKS as n,APPLICATION_DEPLOY_ORDER as
|
|
1
|
+
import{APPLICATION_STACKS as n,APPLICATION_DEPLOY_ORDER as l,getApplicationStepName as t,getApplicationStepId as a}from"../types/index.js";import{STEP_IDS as e,STEP_NAMES as s,INFRA_STEP_NAME as d}from"../types/index.js";const O={Network:e.NETWORK_DESTROY,Storage:e.STORAGE_DESTROY,Messaging:e.MESSAGING_DESTROY,Database:e.DATABASE_DESTROY,Compute:e.COMPUTE_DESTROY,Cdn:e.CDN_DESTROY};function C(A){return O[A]??`${A.toLowerCase()}-destroy`}class p{static DEPLOYMENT_STEPS=new Map([["application-deploy",[{id:e.AUTH,name:s.AUTH},{id:e.DETECT_CONFIG,name:s.PREPARE_DEPLOY},{id:e.TARGET_READINESS,name:s.TARGET_READINESS,conditions:{requiresInfra:!0}},{id:e.BOOTSTRAP,name:s.BOOTSTRAP,conditions:{requiresInfra:!0}},{id:a(n.NETWORK,"deploy"),name:t(n.NETWORK,"deploy"),conditions:{requiresInfra:!0,requiresNetwork:!0}},{id:a(n.STORAGE,"deploy"),name:t(n.STORAGE,"deploy"),conditions:{requiresInfra:!0,requiresStorage:!0}},{id:a(n.MESSAGING,"deploy"),name:t(n.MESSAGING,"deploy"),conditions:{requiresInfra:!0,requiresMessaging:!0}},{id:a(n.DATABASE,"deploy"),name:t(n.DATABASE,"deploy"),conditions:{requiresInfra:!0,requiresDatabase:!0}},{id:e.DOCKER_OPERATIONS,name:"Docker operations",conditions:{requiresDocker:!0,requiresCompute:!0,skipForInfraOnly:!0}},{id:e.TAG_ECR_IMAGES,name:"Tagging container images",conditions:{requiresImageTagging:!0,requiresCompute:!0,skipForInfraOnly:!0}},{id:a(n.COMPUTE,"deploy"),name:t(n.COMPUTE,"deploy"),conditions:{requiresInfra:!0,requiresCompute:!0}},{id:a(n.CDN,"deploy"),name:t(n.CDN,"deploy"),conditions:{requiresInfra:!0,requiresCdn:!0}}]],["application-destroy",[{id:e.AUTH,name:s.AUTH},{id:e.DETECT_CONFIG,name:s.CHECK_INFRA_STATE},{id:a(n.CDN,"destroy"),name:t(n.CDN,"destroy"),conditions:{requiresCdn:!0}},{id:a(n.COMPUTE,"destroy"),name:t(n.COMPUTE,"destroy")},{id:a(n.DATABASE,"destroy"),name:t(n.DATABASE,"destroy")},{id:a(n.MESSAGING,"destroy"),name:t(n.MESSAGING,"destroy"),conditions:{requiresMessaging:!0}},{id:a(n.STORAGE,"destroy"),name:t(n.STORAGE,"destroy"),conditions:{requiresStorage:!0}},{id:a(n.NETWORK,"destroy"),name:t(n.NETWORK,"destroy")}]],["organisation-deploy",[{id:e.AUTH,name:s.AUTH},{id:e.ORG_SETUP,name:s.ORG_SETUP},{id:e.PREPARE_ENVIRONMENT,name:d.PREPARE},{id:e.ORG_DEPLOY,name:s.ORG_DEPLOY,conditions:{requiresOrgChanges:!0}},{id:e.CASCADE_PLATFORM,name:s.CASCADE_PLATFORM,conditions:{requiresPlatformAccount:!0}},{id:e.CASCADE_ACCOUNTS,name:s.CASCADE_ACCOUNTS,conditions:{requiresMemberAccounts:!0}}]],["organisation-destroy",[{id:e.AUTH,name:s.AUTH},{id:e.CFN_CHECK,name:s.CHECK_INFRA_STATE},{id:e.CASCADE_ACCOUNTS,name:"Destroying account infrastructure",conditions:{requiresMemberAccounts:!0}},{id:e.CASCADE_PLATFORM,name:"Destroying platform infrastructure",conditions:{requiresPlatformAccount:!0}},{id:e.DESTROY,name:"Destroying organisation infrastructure"}]],["platform-deploy",[{id:e.AUTH,name:s.AUTH},{id:e.CONNECT,name:d.CONNECT},{id:e.PREPARE_ENVIRONMENT,name:d.PREPARE},{id:e.DEPLOY,name:d.DEPLOY},{id:e.MONITORING,name:d.MONITORING}]],["platform-destroy",[{id:e.AUTH,name:s.AUTH},{id:e.CFN_CHECK,name:"Checking infrastructure state"},{id:e.ORG_DESTROY,name:"Destroying platform infrastructure"}]],["account-deploy",[{id:e.AUTH,name:s.AUTH},{id:e.CONNECT,name:d.CONNECT},{id:e.PREPARE_ENVIRONMENT,name:d.PREPARE},{id:e.DEPLOY,name:d.DEPLOY},{id:e.MONITORING,name:d.MONITORING}]],["account-destroy",[{id:e.AUTH,name:s.AUTH},{id:e.CFN_CHECK,name:"Checking infrastructure state"},{id:e.ORG_DESTROY,name:"Destroying account infrastructure"}]]]);static getSteps(r){if(r.deploymentType==="application"&&r.operation==="deploy"&&r.deployOnly)return[{id:e.AUTH,name:s.AUTH},{id:e.DOCKER_OPERATIONS,name:"Build and push Docker container"},{id:a(n.COMPUTE,"deploy"),name:t(n.COMPUTE,"deploy")}];const o=`${r.deploymentType}-${r.operation}`;return(this.DEPLOYMENT_STEPS.get(o)??[]).filter(i=>{if(r.deploymentType==="application"){const T=r.builderName==="opennext";if(i.conditions?.requiresInfra&&r.deployOnly)return!1;if(r.resources){const E=r.resources;if(i.conditions?.requiresNetwork&&!E.hasNetwork||i.conditions?.requiresCompute&&!E.hasCompute||i.conditions?.requiresDatabase&&!E.hasDatabase||i.conditions?.requiresStorage&&!E.hasStorage||i.conditions?.requiresMessaging&&!E.hasMessaging||i.conditions?.requiresCdn&&!E.hasCdn)return!1}else if(r.operation==="deploy"&&!T&&(i.conditions?.requiresNetwork||i.conditions?.requiresCompute||i.conditions?.requiresDatabase||i.conditions?.requiresStorage||i.conditions?.requiresMessaging||i.conditions?.requiresCdn))return!1;if(i.conditions?.requiresDocker||i.id===e.DOCKER_OPERATIONS)return T||r.infraOnly||r.resources&&!r.resources.hasCompute?!1:r.deployOnly?r.hasDockerfile===!0:r.hasDockerfile===!0||r.hasDockerfile===!1&&!r.isManagedAccount;if(i.conditions?.requiresImageTagging)return r.infraOnly||T||r.resources&&!r.resources.hasCompute?!1:r.hasDockerfile===!1}return!(i.conditions?.requiresMemberAccounts&&!r.hasMemberAccounts||i.conditions?.requiresPlatformAccount&&!r.hasPlatformAccount||i.conditions?.requiresOrgChanges&&r.hasOrgChanges!==!0||i.conditions?.requiresPlatformChanges&&r.hasPlatformChanges!==!0||i.conditions?.requiresAccountChanges&&r.hasAccountChanges!==!0||i.conditions?.requiresDomainConfiguration&&!r.hasDomainConfiguration||i.conditions?.requiresDomainChanges&&r.hasDomainChanges!==!0)}).map(i=>i.id===e.DOCKER_OPERATIONS?{...i,name:r.hasDockerfile?"Building and pushing Docker image":"Initialising container repository"}:i)}static getStepNames(r){return this.getSteps(r).map(o=>o.name)}static getStepIndex(r,o){return this.getSteps(o).findIndex(c=>c.id===r)}static getStepById(r,o="application"){const u=`${o}-deploy`,i=(this.DEPLOYMENT_STEPS.get(u)||[]).find(f=>f.id===r);if(i)return i;const T=`${o}-destroy`;return(this.DEPLOYMENT_STEPS.get(T)||[]).find(f=>f.id===r)}static isStepIncluded(r,o){return this.getSteps(o).some(c=>c.id===r)}static createContext(r,o,u){return{deploymentType:r,operation:o,deployOnly:u?.deployOnly??!1,infraOnly:u?.infraOnly??!1,isManagedAccount:u?.isManagedAccount??!1,hasDockerfile:u?.hasDockerfile??!1,pattern:u?.pattern??null,builderName:u?.builderName,resources:u?.resources}}static getDeploymentTypes(){return["application","organisation","platform","account"]}static getStackNames(r){switch(r){case"application":return[...l];case"organisation":return["Organisation"];case"platform":return["Platform"];case"account":return["Account"];default:return[]}}static getInfraStepIds(){return[e.CFN_CHECK,e.TARGET_READINESS,e.BOOTSTRAP,e.DIFF,e.NETWORK,e.STORAGE,e.MESSAGING,e.DATABASE,e.COMPUTE,e.CDN]}static getDockerStepIds(){return[e.ECR_INIT,e.DOCKER_DEPLOY]}}export{p as StepRegistry,C as getDestroyStepId};
|