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