@fjall/deploy-core 0.89.4 → 0.89.6
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/LICENSE +50 -21
- package/README.md +25 -0
- package/dist/.minified +1 -0
- package/dist/src/__test-utils__/awsMockHelpers.d.ts +20 -0
- package/dist/src/__test-utils__/awsMockHelpers.js +1 -0
- package/dist/src/__test-utils__/index.d.ts +1 -0
- package/dist/src/__test-utils__/index.js +1 -0
- package/dist/src/aws/AwsProvider.d.ts +2 -2
- package/dist/src/aws/AwsProvider.js +0 -1
- package/dist/src/aws/SimpleAwsProvider.d.ts +2 -3
- package/dist/src/aws/SimpleAwsProvider.js +1 -73
- package/dist/src/aws/index.d.ts +4 -2
- package/dist/src/aws/index.js +1 -3
- package/dist/src/aws/organisations/accounts.js +10 -10
- package/dist/src/aws/organisations/backup.js +4 -2
- package/dist/src/aws/organisations/costAllocation.js +4 -2
- package/dist/src/aws/organisations/delegatedAdmin.d.ts +9 -0
- package/dist/src/aws/organisations/delegatedAdmin.js +43 -0
- package/dist/src/aws/organisations/identityCentre.d.ts +1 -1
- package/dist/src/aws/organisations/identityCentre.js +6 -2
- package/dist/src/aws/organisations/index.d.ts +4 -3
- package/dist/src/aws/organisations/index.js +1 -12
- package/dist/src/aws/organisations/ipam.js +4 -2
- package/dist/src/aws/organisations/organisation.js +27 -18
- package/dist/src/aws/organisations/organisationalUnits.d.ts +26 -6
- package/dist/src/aws/organisations/organisationalUnits.js +149 -35
- package/dist/src/aws/organisations/policies.js +4 -3
- package/dist/src/aws/organisations/ram.js +6 -2
- package/dist/src/aws/organisations/serviceAccess.js +12 -6
- package/dist/src/aws/organisations/trustedAccess.js +6 -2
- package/dist/src/aws/organisations/types.d.ts +23 -1
- package/dist/src/aws/organisations/types.js +1 -16
- package/dist/src/aws/utils/__tests__/cloudformationTestHelpers.d.ts +6 -0
- package/dist/src/aws/utils/__tests__/cloudformationTestHelpers.js +1 -0
- package/dist/src/aws/utils/cloudformationEventHelpers.d.ts +48 -0
- package/dist/src/aws/utils/cloudformationEventHelpers.js +1 -0
- package/dist/src/aws/utils/cloudformationEventTypes.d.ts +45 -0
- package/dist/src/aws/utils/cloudformationEventTypes.js +1 -0
- package/dist/src/aws/utils/cloudformationEvents.d.ts +8 -54
- package/dist/src/aws/utils/cloudformationEvents.js +1 -596
- package/dist/src/aws/utils/index.d.ts +5 -0
- package/dist/src/aws/utils/index.js +1 -0
- package/dist/src/aws/utils/stackStatus.js +1 -90
- package/dist/src/events/index.d.ts +13 -0
- package/dist/src/events/index.js +1 -0
- package/dist/src/index.d.ts +34 -17
- package/dist/src/index.js +41 -21
- package/dist/src/orchestration/__tests__/cascadeTestHelpers.d.ts +12 -0
- package/dist/src/orchestration/__tests__/cascadeTestHelpers.js +78 -0
- package/dist/src/orchestration/activeDeploymentGuard.d.ts +10 -0
- package/dist/src/orchestration/activeDeploymentGuard.js +39 -0
- package/dist/src/orchestration/applicationDeploy.js +46 -224
- package/dist/src/orchestration/applicationDeployHelpers.d.ts +39 -0
- package/dist/src/orchestration/applicationDeployHelpers.js +223 -0
- package/dist/src/orchestration/applicationDestroy.d.ts +14 -0
- package/dist/src/orchestration/applicationDestroy.js +131 -0
- package/dist/src/orchestration/builders/dockerBuilder.d.ts +17 -0
- package/dist/src/orchestration/builders/dockerBuilder.js +98 -0
- package/dist/src/orchestration/builders/frameworkRegistry.d.ts +23 -0
- package/dist/src/orchestration/builders/frameworkRegistry.js +1 -0
- package/dist/src/orchestration/builders/index.d.ts +4 -0
- package/dist/src/orchestration/builders/index.js +1 -0
- package/dist/src/orchestration/builders/openNextBuilder.d.ts +21 -0
- package/dist/src/orchestration/builders/openNextBuilder.js +144 -0
- package/dist/src/orchestration/cascadeDestroyHelpers.d.ts +30 -0
- package/dist/src/orchestration/cascadeDestroyHelpers.js +1 -0
- package/dist/src/orchestration/cascadeHelpers.d.ts +46 -0
- package/dist/src/orchestration/cascadeHelpers.js +160 -0
- package/dist/src/orchestration/contextHelpers.d.ts +47 -2
- package/dist/src/orchestration/contextHelpers.js +94 -1
- package/dist/src/orchestration/destroy.d.ts +13 -0
- package/dist/src/orchestration/destroy.js +67 -0
- package/dist/src/orchestration/detectionPipeline.d.ts +2 -11
- package/dist/src/orchestration/detectionPipeline.js +29 -10
- package/dist/src/orchestration/dockerBuildHelper.d.ts +10 -0
- package/dist/src/orchestration/dockerBuildHelper.js +49 -0
- package/dist/src/orchestration/dockerInterface.d.ts +4 -2
- package/dist/src/orchestration/index.d.ts +8 -1
- package/dist/src/orchestration/index.js +1 -3
- package/dist/src/orchestration/manifestSecretParser.d.ts +11 -0
- package/dist/src/orchestration/manifestSecretParser.js +1 -0
- package/dist/src/orchestration/openNextBuild.d.ts +28 -0
- package/dist/src/orchestration/openNextBuild.js +243 -0
- package/dist/src/orchestration/organisationDeploy.js +130 -228
- package/dist/src/orchestration/organisationDestroy.d.ts +24 -0
- package/dist/src/orchestration/organisationDestroy.js +189 -0
- package/dist/src/orchestration/organisationSetup.d.ts +6 -4
- package/dist/src/orchestration/organisationSetup.js +28 -8
- package/dist/src/orchestration/resolveOperation.js +68 -6
- package/dist/src/orchestration/serviceFactory.d.ts +4 -0
- package/dist/src/orchestration/serviceFactory.js +1 -16
- package/dist/src/orchestration/spawnHelpers.d.ts +47 -0
- package/dist/src/orchestration/spawnHelpers.js +1 -0
- package/dist/src/orchestration/stackCleanup.d.ts +39 -0
- package/dist/src/orchestration/stackCleanup.js +1 -0
- package/dist/src/orchestration/welcomeImageHelper.d.ts +15 -0
- package/dist/src/orchestration/welcomeImageHelper.js +64 -0
- package/dist/src/services/application/ApplicationStackService.d.ts +21 -30
- package/dist/src/services/application/ApplicationStackService.js +16 -234
- package/dist/src/services/application/applicationStackHelpers.d.ts +46 -0
- package/dist/src/services/application/applicationStackHelpers.js +248 -0
- package/dist/src/services/application/index.d.ts +1 -0
- package/dist/src/services/application/index.js +1 -1
- package/dist/src/services/index.d.ts +6 -0
- package/dist/src/services/index.js +1 -0
- package/dist/src/services/infrastructure/CdkArgumentBuilder.js +1 -67
- package/dist/src/services/infrastructure/CdkCommandRunner.d.ts +10 -2
- package/dist/src/services/infrastructure/CdkCommandRunner.js +18 -15
- package/dist/src/services/infrastructure/CdkErrorFormatter.js +16 -194
- package/dist/src/services/infrastructure/CdkEventMonitoring.js +1 -41
- package/dist/src/services/infrastructure/CdkOutputAnalyser.js +1 -1
- package/dist/src/services/infrastructure/CdkOutputParser.js +2 -33
- package/dist/src/services/infrastructure/CdkProcessManager.d.ts +5 -0
- package/dist/src/services/infrastructure/CdkProcessManager.js +81 -47
- package/dist/src/services/infrastructure/CdkService.d.ts +7 -52
- package/dist/src/services/infrastructure/CdkService.js +41 -82
- package/dist/src/services/infrastructure/CdkServiceTypes.d.ts +50 -0
- package/dist/src/services/infrastructure/CdkServiceTypes.js +0 -0
- package/dist/src/services/infrastructure/CloudFormationService.js +9 -10
- package/dist/src/services/infrastructure/ICdkProcessManager.d.ts +27 -0
- package/dist/src/services/infrastructure/ICdkProcessManager.js +1 -0
- package/dist/src/services/infrastructure/__tests__/cloudFormationTestHelpers.d.ts +9 -0
- package/dist/src/services/infrastructure/__tests__/cloudFormationTestHelpers.js +1 -0
- package/dist/src/services/infrastructure/cdkServiceHelpers.d.ts +9 -0
- package/dist/src/services/infrastructure/cdkServiceHelpers.js +1 -0
- package/dist/src/services/infrastructure/constructMapEnrichment.d.ts +7 -0
- package/dist/src/services/infrastructure/constructMapEnrichment.js +1 -0
- package/dist/src/services/infrastructure/index.d.ts +3 -1
- package/dist/src/services/infrastructure/index.js +1 -7
- package/dist/src/services/supporting/TemplateHashService.js +1 -1
- package/dist/src/services/supporting/helpers.js +1 -81
- package/dist/src/services/supporting/index.js +1 -3
- package/dist/src/steps/index.d.ts +1 -0
- package/dist/src/steps/index.js +1 -0
- package/dist/src/steps/stepRegistry.d.ts +71 -0
- package/dist/src/steps/stepRegistry.js +505 -0
- package/dist/src/types/FjallState.js +1 -118
- package/dist/src/types/ProgressEvent.js +1 -48
- package/dist/src/types/application/ApplicationServiceTypes.js +1 -30
- package/dist/src/types/application/index.js +1 -1
- package/dist/src/types/callbacks.d.ts +76 -4
- package/dist/src/types/callbacks.js +0 -1
- package/dist/src/types/constants.d.ts +2 -0
- package/dist/src/types/constants.js +1 -6
- package/dist/src/types/credentials.d.ts +1 -1
- package/dist/src/types/credentials.js +0 -1
- package/dist/src/types/deployment/DeploymentServiceTypes.d.ts +5 -2
- package/dist/src/types/deployment/DeploymentServiceTypes.js +1 -1
- package/dist/src/types/deployment/DeploymentTypes.d.ts +1 -0
- package/dist/src/types/deployment/DeploymentTypes.js +0 -1
- package/dist/src/types/deployment/cloudformation.js +0 -1
- package/dist/src/types/deployment/index.d.ts +3 -1
- package/dist/src/types/deployment/index.js +1 -1
- package/dist/src/types/deployment/parallel.js +1 -10
- package/dist/src/types/deploymentEventSchema.d.ts +158 -0
- package/dist/src/types/deploymentEventSchema.js +1 -0
- package/dist/src/types/detection.d.ts +22 -0
- package/dist/src/types/detection.js +1 -0
- package/dist/src/types/entitlements.d.ts +31 -0
- package/dist/src/types/entitlements.js +0 -0
- package/dist/src/types/errors/CdkError.js +1 -20
- package/dist/src/types/errors/ServiceError.d.ts +2 -1
- package/dist/src/types/errors/ServiceError.js +1 -119
- package/dist/src/types/errors/index.d.ts +2 -0
- package/dist/src/types/errors/index.js +1 -0
- package/dist/src/types/events.d.ts +3 -9
- package/dist/src/types/events.js +0 -5
- package/dist/src/types/frameworkBuilder.d.ts +96 -0
- package/dist/src/types/frameworkBuilder.js +8 -0
- package/dist/src/types/index.d.ts +19 -4
- package/dist/src/types/index.js +1 -9
- package/dist/src/types/operations.d.ts +3 -2
- package/dist/src/types/operations.js +1 -285
- package/dist/src/types/orgConfig.d.ts +2 -10
- package/dist/src/types/orgConfig.js +0 -11
- package/dist/src/types/params.d.ts +61 -0
- package/dist/src/types/patternDetection.d.ts +14 -16
- package/dist/src/types/patternDetection.js +14 -18
- package/dist/src/types/patternTypes.d.ts +19 -0
- package/dist/src/types/patternTypes.js +1 -0
- package/dist/src/types/stepDefinitions.d.ts +163 -0
- package/dist/src/types/stepDefinitions.js +98 -0
- package/dist/src/types/validation.js +0 -1
- package/dist/src/util/dockerfileDetection.d.ts +5 -0
- package/dist/src/util/dockerfileDetection.js +1 -0
- package/dist/src/util/index.d.ts +4 -3
- package/dist/src/util/index.js +1 -3
- package/dist/src/util/sequencedCallbacks.d.ts +44 -0
- package/dist/src/util/sequencedCallbacks.js +1 -0
- package/package.json +49 -8
- package/dist/src/aws/utils/CloudFormationFailureAnalyser.d.ts +0 -32
- package/dist/src/aws/utils/CloudFormationFailureAnalyser.js +0 -228
- package/dist/src/aws/utils/errors.d.ts +0 -26
- package/dist/src/aws/utils/errors.js +0 -59
- package/dist/src/util/fsHelpers.d.ts +0 -4
- package/dist/src/util/fsHelpers.js +0 -16
- package/dist/src/util/securityHelpers.d.ts +0 -31
- package/dist/src/util/securityHelpers.js +0 -124
- package/dist/src/util/singleton.d.ts +0 -2
- package/dist/src/util/singleton.js +0 -9
- package/dist/src/util/sleep.d.ts +0 -4
- package/dist/src/util/sleep.js +0 -4
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { success, failure } from "@fjall/generator";
|
|
2
|
-
import { logger } from "@fjall/util";
|
|
3
2
|
import { ORGANISATION_TYPES, getOrganisationStackName } from "../types/operations.js";
|
|
4
3
|
import { CdkContextBuilder } from "../services/supporting/CdkContextBuilder.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
4
|
+
import { stubCallerIdentity } from "../types/deployment/index.js";
|
|
5
|
+
import { buildParamsContext, collectStackOutputs, synthOrFail, bootstrapOrFail, forwardOutput, forwardResourceProgress } from "./contextHelpers.js";
|
|
6
|
+
import { partitionAccounts, deployCascadeAccount, readPlatformIpamPoolIds, deployDomains } from "./cascadeHelpers.js";
|
|
7
|
+
import { maskSensitiveOutput } from "@fjall/util";
|
|
8
|
+
import { INFRA_STEP_NAME, STEP_IDS } from "../types/stepDefinitions.js";
|
|
8
9
|
/**
|
|
9
10
|
* Organisation deployment orchestration.
|
|
10
11
|
*
|
|
@@ -41,57 +42,81 @@ function buildOrgContext(params, services, operation, deployType, accountName) {
|
|
|
41
42
|
path: operation.path,
|
|
42
43
|
region: services.awsProvider.getRegion(),
|
|
43
44
|
accountName,
|
|
44
|
-
callerIdentity:
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
callerIdentity: stubCallerIdentity(services.awsProvider.getAccountId()),
|
|
46
|
+
...buildParamsContext({
|
|
47
|
+
orgConfig: params.orgConfig,
|
|
48
|
+
identity: params.identity,
|
|
49
|
+
skipOidc: params.options?.skipOidc
|
|
50
|
+
})
|
|
50
51
|
}, {
|
|
51
52
|
verbose: params.options?.verbose,
|
|
52
53
|
infraOnly: params.options?.infraOnly
|
|
53
54
|
}, params.orgConfig);
|
|
54
55
|
}
|
|
56
|
+
const INFRA_STEPS = {
|
|
57
|
+
CONNECT: { id: STEP_IDS.CONNECT, name: INFRA_STEP_NAME.CONNECT },
|
|
58
|
+
PREPARE: { id: STEP_IDS.PREPARE_ENVIRONMENT, name: INFRA_STEP_NAME.PREPARE },
|
|
59
|
+
DEPLOY: { id: STEP_IDS.DEPLOY, name: INFRA_STEP_NAME.DEPLOY },
|
|
60
|
+
MONITORING: { id: STEP_IDS.MONITORING, name: INFRA_STEP_NAME.MONITORING },
|
|
61
|
+
ORG_DEPLOY: {
|
|
62
|
+
id: STEP_IDS.ORG_DEPLOY,
|
|
63
|
+
name: "Deploying organisation infrastructure"
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
const INFRA_STEP_TOTAL = 4;
|
|
55
67
|
/**
|
|
56
68
|
* Deploy a single organisation component (platform or account).
|
|
69
|
+
*
|
|
70
|
+
* Emits 4 named steps from INFRASTRUCTURE_STEP_NAMES: Connect securely →
|
|
71
|
+
* Prepare environment → Deploy infrastructure → Enable monitoring.
|
|
57
72
|
*/
|
|
58
73
|
async function deploySingleComponent(params, services, operation, deployType, startTime) {
|
|
59
74
|
const { callbacks } = params;
|
|
75
|
+
// Step 1: Connect securely — already seeded by the webapp trigger.
|
|
76
|
+
// Complete it now: credentials are available by the time deploy-core runs.
|
|
77
|
+
callbacks.onStepComplete?.(INFRA_STEPS.CONNECT.id, INFRA_STEPS.CONNECT.name, "completed", 0, INFRA_STEP_TOTAL);
|
|
78
|
+
// Step 2: Prepare environment (synth + bootstrap)
|
|
79
|
+
callbacks.onStepStart?.(INFRA_STEPS.PREPARE.id, INFRA_STEPS.PREPARE.name, 1, INFRA_STEP_TOTAL);
|
|
60
80
|
const context = buildOrgContext(params, services, operation, deployType, deployType === "account" ? operation.target : undefined);
|
|
61
81
|
// Synth
|
|
62
82
|
callbacks.onLog?.(`Synthesising ${deployType} infrastructure…`, "info");
|
|
63
|
-
const synthResult = await services
|
|
83
|
+
const synthResult = await synthOrFail(services, context, callbacks, "CDK synthesis failed");
|
|
64
84
|
if (!synthResult.success) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return failure(error);
|
|
85
|
+
callbacks.onStepComplete?.(INFRA_STEPS.PREPARE.id, INFRA_STEPS.PREPARE.name, "error", 1, INFRA_STEP_TOTAL);
|
|
86
|
+
return synthResult;
|
|
68
87
|
}
|
|
69
88
|
// Bootstrap
|
|
70
|
-
callbacks
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const error = new Error(`Bootstrap failed: ${bootstrapResult.error}`);
|
|
75
|
-
callbacks.onError?.(error);
|
|
76
|
-
return failure(error);
|
|
89
|
+
const bsResult = await bootstrapOrFail(services, context, callbacks);
|
|
90
|
+
if (!bsResult.success) {
|
|
91
|
+
callbacks.onStepComplete?.(INFRA_STEPS.PREPARE.id, INFRA_STEPS.PREPARE.name, "error", 1, INFRA_STEP_TOTAL);
|
|
92
|
+
return bsResult;
|
|
77
93
|
}
|
|
78
|
-
callbacks.
|
|
79
|
-
// Deploy
|
|
94
|
+
callbacks.onStepComplete?.(INFRA_STEPS.PREPARE.id, INFRA_STEPS.PREPARE.name, "completed", 1, INFRA_STEP_TOTAL);
|
|
95
|
+
// Step 3: Deploy infrastructure
|
|
80
96
|
const stackName = getOrganisationStackName(operation.type);
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
callbacks.onStepStart?.(stepId, stepName, 0, 1);
|
|
84
|
-
const deployResult = await services.cdkService.runCdkDeploy(context, stackName, (chunk) => callbacks.onOutput?.(chunk), (event) => callbacks.onResourceProgress?.(event), services.awsProvider);
|
|
97
|
+
callbacks.onStepStart?.(INFRA_STEPS.DEPLOY.id, INFRA_STEPS.DEPLOY.name, 2, INFRA_STEP_TOTAL);
|
|
98
|
+
const deployResult = await services.cdkService.runCdkDeploy(context, stackName, forwardOutput(callbacks), forwardResourceProgress(callbacks), services.awsProvider);
|
|
85
99
|
if (!deployResult.success) {
|
|
86
|
-
callbacks.onStepComplete?.(
|
|
87
|
-
const error = new Error(deployResult.error);
|
|
100
|
+
callbacks.onStepComplete?.(INFRA_STEPS.DEPLOY.id, INFRA_STEPS.DEPLOY.name, "error", 2, INFRA_STEP_TOTAL);
|
|
101
|
+
const error = new Error(maskSensitiveOutput(deployResult.error));
|
|
88
102
|
callbacks.onError?.(error);
|
|
89
103
|
return failure(error);
|
|
90
104
|
}
|
|
91
|
-
callbacks.onStepComplete?.(
|
|
105
|
+
callbacks.onStepComplete?.(INFRA_STEPS.DEPLOY.id, INFRA_STEPS.DEPLOY.name, "completed", 2, INFRA_STEP_TOTAL);
|
|
106
|
+
// Capture CloudFormation outputs (OIDC role ARN, etc.)
|
|
107
|
+
const outputsResult = await services.cfnService.getStackOutputs(stackName);
|
|
108
|
+
if (!outputsResult.success) {
|
|
109
|
+
callbacks.onLog?.("Failed to read stack outputs (non-critical)", "debug");
|
|
110
|
+
}
|
|
111
|
+
const outputs = collectStackOutputs(outputsResult);
|
|
112
|
+
// Step 4: Enable monitoring — CloudTrail + alarms are part of the
|
|
113
|
+
// Account stack. Signal post-deploy readiness.
|
|
114
|
+
callbacks.onStepStart?.(INFRA_STEPS.MONITORING.id, INFRA_STEPS.MONITORING.name, 3, INFRA_STEP_TOTAL);
|
|
115
|
+
callbacks.onStepComplete?.(INFRA_STEPS.MONITORING.id, INFRA_STEPS.MONITORING.name, "completed", 3, INFRA_STEP_TOTAL);
|
|
92
116
|
return success({
|
|
93
117
|
target: operation.target,
|
|
94
118
|
deploymentType: "organisation",
|
|
119
|
+
outputs,
|
|
95
120
|
durationMs: Date.now() - startTime
|
|
96
121
|
});
|
|
97
122
|
}
|
|
@@ -102,54 +127,67 @@ async function deployOrgWithCascade(params, services, operation, startTime) {
|
|
|
102
127
|
const { callbacks, options } = params;
|
|
103
128
|
const providerAccounts = params.orgConfig?.providerAccounts ?? [];
|
|
104
129
|
const context = buildOrgContext(params, services, operation, "organisation");
|
|
130
|
+
// Compute step counts upfront so pre-deploy failures can reference them
|
|
131
|
+
const cascadeEnabled = options?.cascade !== false;
|
|
132
|
+
const cascadeAccountCount = cascadeEnabled ? providerAccounts.length : 0;
|
|
133
|
+
// Steps: prepare (synth+bootstrap) + org-deploy + cascade accounts
|
|
134
|
+
const totalSteps = 2 + cascadeAccountCount;
|
|
135
|
+
const { id: prepareStepId, name: prepareStepName } = INFRA_STEPS.PREPARE;
|
|
136
|
+
// Step 0: Prepare (synth + bootstrap)
|
|
137
|
+
callbacks.onStepStart?.(prepareStepId, prepareStepName, 0, totalSteps);
|
|
105
138
|
// Synth
|
|
106
139
|
callbacks.onLog?.("Synthesising organisation infrastructure…", "info");
|
|
107
|
-
const synthResult = await services
|
|
140
|
+
const synthResult = await synthOrFail(services, context, callbacks, "CDK synthesis failed");
|
|
108
141
|
if (!synthResult.success) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
return failure(error);
|
|
142
|
+
callbacks.onStepComplete?.(prepareStepId, prepareStepName, "error", 0, totalSteps);
|
|
143
|
+
return synthResult;
|
|
112
144
|
}
|
|
113
145
|
// Bootstrap org account
|
|
114
|
-
callbacks
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const error = new Error(`Bootstrap failed: ${bootstrapResult.error}`);
|
|
119
|
-
callbacks.onError?.(error);
|
|
120
|
-
return failure(error);
|
|
146
|
+
const bsResult = await bootstrapOrFail(services, context, callbacks);
|
|
147
|
+
if (!bsResult.success) {
|
|
148
|
+
callbacks.onStepComplete?.(prepareStepId, prepareStepName, "error", 0, totalSteps);
|
|
149
|
+
return bsResult;
|
|
121
150
|
}
|
|
122
|
-
callbacks.
|
|
123
|
-
// Deploy org infrastructure
|
|
124
|
-
const orgStepId =
|
|
125
|
-
|
|
126
|
-
const cascadeEnabled = options?.cascade !== false;
|
|
127
|
-
const cascadeAccountCount = cascadeEnabled ? providerAccounts.length : 0;
|
|
128
|
-
const totalSteps = 1 + cascadeAccountCount;
|
|
129
|
-
callbacks.onStepStart?.(orgStepId, orgStepName, 0, totalSteps);
|
|
151
|
+
callbacks.onStepComplete?.(prepareStepId, prepareStepName, "completed", 0, totalSteps);
|
|
152
|
+
// Step 1: Deploy org infrastructure
|
|
153
|
+
const { id: orgStepId, name: orgStepName } = INFRA_STEPS.ORG_DEPLOY;
|
|
154
|
+
callbacks.onStepStart?.(orgStepId, orgStepName, 1, totalSteps);
|
|
130
155
|
const orgStackName = getOrganisationStackName(ORGANISATION_TYPES.ORGANISATION);
|
|
131
|
-
const orgResult = await services.cdkService.runCdkDeploy(context, orgStackName, (
|
|
156
|
+
const orgResult = await services.cdkService.runCdkDeploy(context, orgStackName, forwardOutput(callbacks), forwardResourceProgress(callbacks), services.awsProvider);
|
|
132
157
|
if (!orgResult.success) {
|
|
133
|
-
callbacks.onStepComplete?.(orgStepId, orgStepName, "error",
|
|
134
|
-
const error = new Error(orgResult.error);
|
|
158
|
+
callbacks.onStepComplete?.(orgStepId, orgStepName, "error", 1, totalSteps);
|
|
159
|
+
const error = new Error(maskSensitiveOutput(orgResult.error));
|
|
135
160
|
callbacks.onError?.(error);
|
|
136
161
|
return failure(error);
|
|
137
162
|
}
|
|
138
|
-
|
|
163
|
+
// Capture org root stack outputs (OIDC role ARN, etc.)
|
|
164
|
+
const orgOutputsResult = await services.cfnService.getStackOutputs(orgStackName);
|
|
165
|
+
if (!orgOutputsResult.success) {
|
|
166
|
+
callbacks.onLog?.("Failed to read org stack outputs (non-critical)", "debug");
|
|
167
|
+
}
|
|
168
|
+
const orgOutputs = collectStackOutputs(orgOutputsResult);
|
|
169
|
+
callbacks.onStepComplete?.(orgStepId, orgStepName, "completed", 1, totalSteps);
|
|
139
170
|
// Cascade to platform + domains + member accounts
|
|
171
|
+
const cascadeErrors = [];
|
|
172
|
+
const allCascadeOutputs = [];
|
|
140
173
|
if (cascadeEnabled && providerAccounts.length > 0) {
|
|
141
174
|
callbacks.onCascadeStart?.();
|
|
142
|
-
const cascadeErrors = [];
|
|
143
175
|
let accountsDeployed = 0;
|
|
144
176
|
let platformDeployed = false;
|
|
145
177
|
let domainsDeployed = false;
|
|
146
178
|
// Phase 1: Deploy platform account
|
|
147
|
-
const platformAccount = providerAccounts
|
|
179
|
+
const { platformAccount, memberAccounts } = partitionAccounts(providerAccounts);
|
|
148
180
|
if (platformAccount) {
|
|
149
181
|
callbacks.onCascadePhaseStart?.("platform");
|
|
150
182
|
const platformResult = await deployCascadeAccount(params, services, operation, platformAccount, "platform", callbacks);
|
|
151
183
|
if (platformResult.success) {
|
|
152
184
|
platformDeployed = true;
|
|
185
|
+
if (platformResult.data.outputs) {
|
|
186
|
+
allCascadeOutputs.push({
|
|
187
|
+
accountId: platformAccount.id,
|
|
188
|
+
outputs: platformResult.data.outputs
|
|
189
|
+
});
|
|
190
|
+
}
|
|
153
191
|
}
|
|
154
192
|
else {
|
|
155
193
|
cascadeErrors.push({
|
|
@@ -157,6 +195,7 @@ async function deployOrgWithCascade(params, services, operation, startTime) {
|
|
|
157
195
|
error: platformResult.error.message
|
|
158
196
|
});
|
|
159
197
|
}
|
|
198
|
+
callbacks.onCascadePhaseComplete?.("platform");
|
|
160
199
|
}
|
|
161
200
|
// Phase 1.5: Read Platform stack outputs for IPAM pool IDs
|
|
162
201
|
let ipamPoolIds = new Map();
|
|
@@ -168,24 +207,43 @@ async function deployOrgWithCascade(params, services, operation, startTime) {
|
|
|
168
207
|
const domainResult = await deployDomains(params.domainProvider, callbacks);
|
|
169
208
|
domainsDeployed = domainResult.domainsDeployed > 0;
|
|
170
209
|
for (const err of domainResult.errors) {
|
|
171
|
-
cascadeErrors.push({
|
|
210
|
+
cascadeErrors.push({
|
|
211
|
+
accountId: "domains",
|
|
212
|
+
error: maskSensitiveOutput(err)
|
|
213
|
+
});
|
|
172
214
|
}
|
|
173
215
|
}
|
|
174
216
|
// Phase 3: Deploy member accounts in parallel
|
|
175
|
-
const memberAccounts = providerAccounts.filter((acc) => acc.environment !== "root" && acc.environment !== "platform");
|
|
176
217
|
if (memberAccounts.length > 0) {
|
|
177
218
|
callbacks.onCascadePhaseStart?.("accounts");
|
|
178
219
|
const region = services.awsProvider.getRegion();
|
|
179
|
-
const
|
|
220
|
+
const memberSettled = await Promise.allSettled(memberAccounts.map((account) => {
|
|
180
221
|
const regionSuffix = region.replace(/-/g, "");
|
|
181
222
|
const ipamPoolId = ipamPoolIds.get(`${account.id}-${regionSuffix}`);
|
|
182
223
|
return deployCascadeAccount(params, services, operation, account, "account", callbacks, ipamPoolId);
|
|
183
224
|
}));
|
|
184
|
-
|
|
185
|
-
const result = memberResults[j];
|
|
225
|
+
memberSettled.forEach((settled, j) => {
|
|
186
226
|
const account = memberAccounts[j];
|
|
227
|
+
if (!account)
|
|
228
|
+
return;
|
|
229
|
+
if (settled.status === "rejected") {
|
|
230
|
+
cascadeErrors.push({
|
|
231
|
+
accountId: account.id,
|
|
232
|
+
error: maskSensitiveOutput(settled.reason instanceof Error
|
|
233
|
+
? settled.reason.message
|
|
234
|
+
: String(settled.reason))
|
|
235
|
+
});
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const result = settled.value;
|
|
187
239
|
if (result.success) {
|
|
188
240
|
accountsDeployed++;
|
|
241
|
+
if (result.data.outputs) {
|
|
242
|
+
allCascadeOutputs.push({
|
|
243
|
+
accountId: account.id,
|
|
244
|
+
outputs: result.data.outputs
|
|
245
|
+
});
|
|
246
|
+
}
|
|
189
247
|
}
|
|
190
248
|
else {
|
|
191
249
|
cascadeErrors.push({
|
|
@@ -193,7 +251,8 @@ async function deployOrgWithCascade(params, services, operation, startTime) {
|
|
|
193
251
|
error: result.error.message
|
|
194
252
|
});
|
|
195
253
|
}
|
|
196
|
-
}
|
|
254
|
+
});
|
|
255
|
+
callbacks.onCascadePhaseComplete?.("accounts");
|
|
197
256
|
}
|
|
198
257
|
callbacks.onCascadeComplete?.({
|
|
199
258
|
platformDeployed,
|
|
@@ -206,177 +265,20 @@ async function deployOrgWithCascade(params, services, operation, startTime) {
|
|
|
206
265
|
const errorSummary = cascadeErrors
|
|
207
266
|
.map((e) => ` ${e.accountId}: ${e.error}`)
|
|
208
267
|
.join("\n");
|
|
209
|
-
callbacks.onLog?.(`Cascade completed with ${cascadeErrors.length} failure(s):\n${errorSummary}
|
|
268
|
+
callbacks.onLog?.(maskSensitiveOutput(`Cascade completed with ${cascadeErrors.length} failure(s):\n${errorSummary}`), "warn");
|
|
210
269
|
}
|
|
211
270
|
}
|
|
271
|
+
const warnings = cascadeErrors.length > 0
|
|
272
|
+
? cascadeErrors.map((e) => maskSensitiveOutput(`${e.accountId}: ${e.error}`))
|
|
273
|
+
: undefined;
|
|
212
274
|
return success({
|
|
213
275
|
target: operation.target,
|
|
214
276
|
deploymentType: "organisation",
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
*/
|
|
222
|
-
async function deployCascadeAccount(params, services, operation, account, deployType, callbacks, ipamPoolId) {
|
|
223
|
-
const operationKey = `${account.name}-${account.id}`;
|
|
224
|
-
const region = services.awsProvider.getRegion();
|
|
225
|
-
callbacks.onCascadeAccountStart?.(operationKey, account.id, region);
|
|
226
|
-
// Assume role in target account
|
|
227
|
-
if (!services.awsProvider.assumeRole) {
|
|
228
|
-
const error = new Error(`Cannot cascade to account ${account.name}: AwsProvider does not support assumeRole`);
|
|
229
|
-
callbacks.onCascadeAccountComplete?.(operationKey, false, error.message, region);
|
|
230
|
-
return failure(error);
|
|
231
|
-
}
|
|
232
|
-
const roleArn = `arn:aws:iam::${account.id}:role/fjall-deployment-role`;
|
|
233
|
-
let assumedCredentials;
|
|
234
|
-
try {
|
|
235
|
-
assumedCredentials = await services.awsProvider.assumeRole(roleArn, `fjall-cascade-${account.name}`);
|
|
236
|
-
}
|
|
237
|
-
catch (err) {
|
|
238
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
239
|
-
callbacks.onCascadeAccountComplete?.(operationKey, false, message, region);
|
|
240
|
-
return failure(new Error(`Failed to assume role for ${account.name}: ${message}`));
|
|
241
|
-
}
|
|
242
|
-
// Build context for this account (includes IPAM pool ID if available)
|
|
243
|
-
const accountContext = CdkContextBuilder.buildDeploymentContext({
|
|
244
|
-
deployType,
|
|
245
|
-
target: operation.target,
|
|
246
|
-
path: operation.path,
|
|
247
|
-
region,
|
|
248
|
-
accountName: account.name,
|
|
249
|
-
callerIdentity: { Account: account.id, Arn: "", UserId: "" },
|
|
250
|
-
ipamPoolId,
|
|
251
|
-
...buildParamsContext(params)
|
|
252
|
-
}, { verbose: params.options?.verbose }, params.orgConfig);
|
|
253
|
-
// Create account-scoped provider for CDK monitoring
|
|
254
|
-
const accountProvider = new SimpleAwsProvider({
|
|
255
|
-
accessKeyId: assumedCredentials.accessKeyId,
|
|
256
|
-
secretAccessKey: assumedCredentials.secretAccessKey,
|
|
257
|
-
sessionToken: assumedCredentials.sessionToken,
|
|
258
|
-
region,
|
|
259
|
-
accountId: account.id
|
|
277
|
+
outputs: orgOutputs,
|
|
278
|
+
...(allCascadeOutputs.length > 0
|
|
279
|
+
? { cascadeOutputs: allCascadeOutputs }
|
|
280
|
+
: {}),
|
|
281
|
+
durationMs: Date.now() - startTime,
|
|
282
|
+
warnings
|
|
260
283
|
});
|
|
261
|
-
// Export account credentials for CDK subprocess
|
|
262
|
-
accountProvider.exportToEnv();
|
|
263
|
-
// Bootstrap the target account
|
|
264
|
-
callbacks.onCascadeAccountPhaseChange?.(operationKey, "synth", region);
|
|
265
|
-
const bootstrapResult = await services.cdkService.runCdkBootstrap(accountContext, (chunk) => callbacks.onOutput?.(chunk));
|
|
266
|
-
if (!bootstrapResult.success) {
|
|
267
|
-
services.awsProvider.exportToEnv();
|
|
268
|
-
callbacks.onCascadeAccountComplete?.(operationKey, false, `Bootstrap failed: ${bootstrapResult.error}`, region);
|
|
269
|
-
return failure(new Error(`Bootstrap failed for ${account.name}: ${bootstrapResult.error}`));
|
|
270
|
-
}
|
|
271
|
-
// Deploy the account stack
|
|
272
|
-
callbacks.onCascadeAccountPhaseChange?.(operationKey, "deploy", region);
|
|
273
|
-
const stackName = getOrganisationStackName(deployType === "platform"
|
|
274
|
-
? ORGANISATION_TYPES.PLATFORM
|
|
275
|
-
: ORGANISATION_TYPES.ACCOUNT);
|
|
276
|
-
const deployResult = await services.cdkService.runCdkDeploy(accountContext, stackName, (chunk) => callbacks.onOutput?.(chunk), (event) => callbacks.onCascadeAccountResourceProgress?.(operationKey, event, region), accountProvider);
|
|
277
|
-
// Restore parent credentials
|
|
278
|
-
services.awsProvider.exportToEnv();
|
|
279
|
-
if (!deployResult.success) {
|
|
280
|
-
callbacks.onCascadeAccountComplete?.(operationKey, false, deployResult.error, region);
|
|
281
|
-
return failure(new Error(deployResult.error));
|
|
282
|
-
}
|
|
283
|
-
callbacks.onCascadeAccountComplete?.(operationKey, true, undefined, region);
|
|
284
|
-
return success(undefined);
|
|
285
|
-
}
|
|
286
|
-
/**
|
|
287
|
-
* Read Platform stack outputs to extract IPAM pool IDs for member accounts.
|
|
288
|
-
* Output keys follow the pattern `IpamPoolId{12-digit-accountId}{regionSuffix}`.
|
|
289
|
-
* Returns a map keyed by `{accountId}-{regionSuffix}` → pool ID.
|
|
290
|
-
* Non-fatal: returns empty map on any error.
|
|
291
|
-
*/
|
|
292
|
-
async function readPlatformIpamPoolIds(services, platformAccount, callbacks) {
|
|
293
|
-
const poolIds = new Map();
|
|
294
|
-
// Assume role in platform account to read its stack outputs
|
|
295
|
-
if (!services.awsProvider.assumeRole) {
|
|
296
|
-
logger.debug("organisationDeploy", "Cannot read Platform outputs: assumeRole not available");
|
|
297
|
-
return poolIds;
|
|
298
|
-
}
|
|
299
|
-
const region = services.awsProvider.getRegion();
|
|
300
|
-
const roleArn = `arn:aws:iam::${platformAccount.id}:role/fjall-deployment-role`;
|
|
301
|
-
let assumedCredentials;
|
|
302
|
-
try {
|
|
303
|
-
assumedCredentials = await services.awsProvider.assumeRole(roleArn, `fjall-ipam-read-${platformAccount.name}`);
|
|
304
|
-
}
|
|
305
|
-
catch (err) {
|
|
306
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
307
|
-
logger.debug("organisationDeploy", `Cannot read Platform outputs: AssumeRole failed: ${message}`);
|
|
308
|
-
return poolIds;
|
|
309
|
-
}
|
|
310
|
-
// Create a temporary provider scoped to the platform account
|
|
311
|
-
const platformProvider = new SimpleAwsProvider({
|
|
312
|
-
accessKeyId: assumedCredentials.accessKeyId,
|
|
313
|
-
secretAccessKey: assumedCredentials.secretAccessKey,
|
|
314
|
-
sessionToken: assumedCredentials.sessionToken,
|
|
315
|
-
region,
|
|
316
|
-
accountId: platformAccount.id
|
|
317
|
-
});
|
|
318
|
-
const platformCfn = new CloudFormationService(platformProvider);
|
|
319
|
-
const outputsResult = await platformCfn.getStackOutputs("Platform");
|
|
320
|
-
if (!outputsResult.success) {
|
|
321
|
-
logger.debug("organisationDeploy", `Failed to read Platform stack outputs: ${outputsResult.error.message}`);
|
|
322
|
-
return poolIds;
|
|
323
|
-
}
|
|
324
|
-
const ipamPattern = /^IpamPoolId(\d{12})(\w+)$/;
|
|
325
|
-
for (const output of outputsResult.data) {
|
|
326
|
-
const match = output.OutputKey?.match(ipamPattern);
|
|
327
|
-
if (match && output.OutputValue) {
|
|
328
|
-
const key = `${match[1]}-${match[2]}`;
|
|
329
|
-
poolIds.set(key, output.OutputValue);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
if (poolIds.size > 0) {
|
|
333
|
-
callbacks.onLog?.(`Read ${poolIds.size} IPAM pool ID(s) from Platform stack`, "info");
|
|
334
|
-
}
|
|
335
|
-
return poolIds;
|
|
336
|
-
}
|
|
337
|
-
/**
|
|
338
|
-
* Deploy configured domains: apex domains sequentially, then delegated
|
|
339
|
-
* domains in parallel. Delegates to the caller-provided DomainDeployProvider.
|
|
340
|
-
*/
|
|
341
|
-
async function deployDomains(provider, callbacks) {
|
|
342
|
-
const domains = provider.getDomains();
|
|
343
|
-
if (domains.length === 0) {
|
|
344
|
-
return { domainsDeployed: 0, errors: [] };
|
|
345
|
-
}
|
|
346
|
-
callbacks.onCascadePhaseStart?.("domains");
|
|
347
|
-
const apexDomains = domains.filter((d) => d.type === "apex");
|
|
348
|
-
const delegatedDomains = domains.filter((d) => d.type === "delegated");
|
|
349
|
-
let domainsDeployed = 0;
|
|
350
|
-
const errors = [];
|
|
351
|
-
// Phase A: Apex domains (sequential — delegation roles must exist first)
|
|
352
|
-
for (const domain of apexDomains) {
|
|
353
|
-
const result = await provider.deployDomain(domain.name, callbacks);
|
|
354
|
-
if (result.success) {
|
|
355
|
-
domainsDeployed++;
|
|
356
|
-
}
|
|
357
|
-
else {
|
|
358
|
-
errors.push(`${domain.name}: ${result.error.message}`);
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
// Phase B: Delegated subdomains (parallel — independent of each other)
|
|
362
|
-
if (delegatedDomains.length > 0) {
|
|
363
|
-
const subdomainResults = await Promise.allSettled(delegatedDomains.map(async (domain) => {
|
|
364
|
-
const result = await provider.deployDomain(domain.name, callbacks);
|
|
365
|
-
if (result.success) {
|
|
366
|
-
domainsDeployed++;
|
|
367
|
-
}
|
|
368
|
-
else {
|
|
369
|
-
errors.push(`${domain.name}: ${result.error.message}`);
|
|
370
|
-
}
|
|
371
|
-
}));
|
|
372
|
-
for (const result of subdomainResults) {
|
|
373
|
-
if (result.status === "rejected") {
|
|
374
|
-
const message = result.reason instanceof Error
|
|
375
|
-
? result.reason.message
|
|
376
|
-
: String(result.reason);
|
|
377
|
-
errors.push(`Subdomain deploy error: ${message}`);
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
return { domainsDeployed, errors };
|
|
382
284
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Organisation destroy orchestration.
|
|
3
|
+
*
|
|
4
|
+
* Receives pre-authenticated credentials and destroys all organisation
|
|
5
|
+
* infrastructure in cascade order:
|
|
6
|
+
* 1. Member accounts across ALL regions in parallel
|
|
7
|
+
* 2. Platform account in primary region
|
|
8
|
+
* 3. Organisation root stack
|
|
9
|
+
*
|
|
10
|
+
* Auth, verification, and interactive prompts are the caller's job
|
|
11
|
+
* (per engine/consumer boundary).
|
|
12
|
+
*/
|
|
13
|
+
import { type Result } from "@fjall/generator";
|
|
14
|
+
import type { DestroyParams, DestroyResult } from "../types/params.js";
|
|
15
|
+
import type { OrganisationOperation } from "../types/operations.js";
|
|
16
|
+
import type { DeployServices } from "./serviceFactory.js";
|
|
17
|
+
/**
|
|
18
|
+
* Destroy organisation infrastructure with cascade.
|
|
19
|
+
*
|
|
20
|
+
* The cascade ordering is: members (parallel, multi-region) -> platform -> org stack.
|
|
21
|
+
* If any member or platform destruction fails, the org stack is NOT destroyed
|
|
22
|
+
* (to avoid orphaning resources).
|
|
23
|
+
*/
|
|
24
|
+
export declare function destroyOrganisation(params: DestroyParams, services: DeployServices, operation: OrganisationOperation): Promise<Result<DestroyResult>>;
|