@fjall/deploy-core 0.89.2

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 (154) hide show
  1. package/LICENSE +21 -0
  2. package/dist/src/aws/AwsProvider.d.ts +39 -0
  3. package/dist/src/aws/AwsProvider.js +1 -0
  4. package/dist/src/aws/SimpleAwsProvider.d.ts +22 -0
  5. package/dist/src/aws/SimpleAwsProvider.js +73 -0
  6. package/dist/src/aws/index.d.ts +4 -0
  7. package/dist/src/aws/index.js +3 -0
  8. package/dist/src/aws/organisations/accounts.d.ts +21 -0
  9. package/dist/src/aws/organisations/accounts.js +99 -0
  10. package/dist/src/aws/organisations/backup.d.ts +12 -0
  11. package/dist/src/aws/organisations/backup.js +28 -0
  12. package/dist/src/aws/organisations/costAllocation.d.ts +12 -0
  13. package/dist/src/aws/organisations/costAllocation.js +26 -0
  14. package/dist/src/aws/organisations/identityCentre.d.ts +8 -0
  15. package/dist/src/aws/organisations/identityCentre.js +19 -0
  16. package/dist/src/aws/organisations/index.d.ts +16 -0
  17. package/dist/src/aws/organisations/index.js +12 -0
  18. package/dist/src/aws/organisations/ipam.d.ts +7 -0
  19. package/dist/src/aws/organisations/ipam.js +18 -0
  20. package/dist/src/aws/organisations/organisation.d.ts +12 -0
  21. package/dist/src/aws/organisations/organisation.js +94 -0
  22. package/dist/src/aws/organisations/organisationalUnits.d.ts +19 -0
  23. package/dist/src/aws/organisations/organisationalUnits.js +125 -0
  24. package/dist/src/aws/organisations/policies.d.ts +7 -0
  25. package/dist/src/aws/organisations/policies.js +36 -0
  26. package/dist/src/aws/organisations/ram.d.ts +7 -0
  27. package/dist/src/aws/organisations/ram.js +15 -0
  28. package/dist/src/aws/organisations/serviceAccess.d.ts +7 -0
  29. package/dist/src/aws/organisations/serviceAccess.js +38 -0
  30. package/dist/src/aws/organisations/trustedAccess.d.ts +7 -0
  31. package/dist/src/aws/organisations/trustedAccess.js +15 -0
  32. package/dist/src/aws/organisations/types.d.ts +29 -0
  33. package/dist/src/aws/organisations/types.js +16 -0
  34. package/dist/src/aws/utils/CloudFormationFailureAnalyser.d.ts +32 -0
  35. package/dist/src/aws/utils/CloudFormationFailureAnalyser.js +228 -0
  36. package/dist/src/aws/utils/cloudformationEvents.d.ts +98 -0
  37. package/dist/src/aws/utils/cloudformationEvents.js +596 -0
  38. package/dist/src/aws/utils/errors.d.ts +26 -0
  39. package/dist/src/aws/utils/errors.js +59 -0
  40. package/dist/src/aws/utils/regions.d.ts +1 -0
  41. package/dist/src/aws/utils/regions.js +1 -0
  42. package/dist/src/aws/utils/stackStatus.d.ts +23 -0
  43. package/dist/src/aws/utils/stackStatus.js +90 -0
  44. package/dist/src/index.d.ts +35 -0
  45. package/dist/src/index.js +45 -0
  46. package/dist/src/orchestration/applicationDeploy.d.ts +11 -0
  47. package/dist/src/orchestration/applicationDeploy.js +327 -0
  48. package/dist/src/orchestration/contextHelpers.d.ts +9 -0
  49. package/dist/src/orchestration/contextHelpers.js +14 -0
  50. package/dist/src/orchestration/deploy.d.ts +10 -0
  51. package/dist/src/orchestration/deploy.js +42 -0
  52. package/dist/src/orchestration/detectionPipeline.d.ts +23 -0
  53. package/dist/src/orchestration/detectionPipeline.js +65 -0
  54. package/dist/src/orchestration/dockerInterface.d.ts +56 -0
  55. package/dist/src/orchestration/dockerInterface.js +1 -0
  56. package/dist/src/orchestration/domainInterface.d.ts +37 -0
  57. package/dist/src/orchestration/domainInterface.js +1 -0
  58. package/dist/src/orchestration/index.d.ts +8 -0
  59. package/dist/src/orchestration/index.js +3 -0
  60. package/dist/src/orchestration/organisationDeploy.d.ts +16 -0
  61. package/dist/src/orchestration/organisationDeploy.js +382 -0
  62. package/dist/src/orchestration/organisationSetup.d.ts +42 -0
  63. package/dist/src/orchestration/organisationSetup.js +227 -0
  64. package/dist/src/orchestration/resolveOperation.d.ts +10 -0
  65. package/dist/src/orchestration/resolveOperation.js +53 -0
  66. package/dist/src/orchestration/serviceFactory.d.ts +15 -0
  67. package/dist/src/orchestration/serviceFactory.js +16 -0
  68. package/dist/src/services/application/ApplicationStackService.d.ts +93 -0
  69. package/dist/src/services/application/ApplicationStackService.js +436 -0
  70. package/dist/src/services/application/index.d.ts +1 -0
  71. package/dist/src/services/application/index.js +1 -0
  72. package/dist/src/services/infrastructure/CdkArgumentBuilder.d.ts +12 -0
  73. package/dist/src/services/infrastructure/CdkArgumentBuilder.js +67 -0
  74. package/dist/src/services/infrastructure/CdkCommandRunner.d.ts +30 -0
  75. package/dist/src/services/infrastructure/CdkCommandRunner.js +241 -0
  76. package/dist/src/services/infrastructure/CdkErrorFormatter.d.ts +4 -0
  77. package/dist/src/services/infrastructure/CdkErrorFormatter.js +194 -0
  78. package/dist/src/services/infrastructure/CdkEventMonitoring.d.ts +19 -0
  79. package/dist/src/services/infrastructure/CdkEventMonitoring.js +41 -0
  80. package/dist/src/services/infrastructure/CdkOutputAnalyser.d.ts +43 -0
  81. package/dist/src/services/infrastructure/CdkOutputAnalyser.js +125 -0
  82. package/dist/src/services/infrastructure/CdkOutputParser.d.ts +8 -0
  83. package/dist/src/services/infrastructure/CdkOutputParser.js +33 -0
  84. package/dist/src/services/infrastructure/CdkProcessManager.d.ts +20 -0
  85. package/dist/src/services/infrastructure/CdkProcessManager.js +244 -0
  86. package/dist/src/services/infrastructure/CdkService.d.ts +71 -0
  87. package/dist/src/services/infrastructure/CdkService.js +254 -0
  88. package/dist/src/services/infrastructure/CloudFormationService.d.ts +79 -0
  89. package/dist/src/services/infrastructure/CloudFormationService.js +249 -0
  90. package/dist/src/services/infrastructure/index.d.ts +8 -0
  91. package/dist/src/services/infrastructure/index.js +7 -0
  92. package/dist/src/services/supporting/CdkContextBuilder.d.ts +49 -0
  93. package/dist/src/services/supporting/CdkContextBuilder.js +44 -0
  94. package/dist/src/services/supporting/TemplateHashService.d.ts +67 -0
  95. package/dist/src/services/supporting/TemplateHashService.js +152 -0
  96. package/dist/src/services/supporting/helpers.d.ts +46 -0
  97. package/dist/src/services/supporting/helpers.js +81 -0
  98. package/dist/src/services/supporting/index.d.ts +3 -0
  99. package/dist/src/services/supporting/index.js +3 -0
  100. package/dist/src/types/FjallState.d.ts +50 -0
  101. package/dist/src/types/FjallState.js +118 -0
  102. package/dist/src/types/ProgressEvent.d.ts +35 -0
  103. package/dist/src/types/ProgressEvent.js +48 -0
  104. package/dist/src/types/apiClient.d.ts +34 -0
  105. package/dist/src/types/apiClient.js +1 -0
  106. package/dist/src/types/application/ApplicationServiceTypes.d.ts +56 -0
  107. package/dist/src/types/application/ApplicationServiceTypes.js +30 -0
  108. package/dist/src/types/application/index.d.ts +1 -0
  109. package/dist/src/types/application/index.js +1 -0
  110. package/dist/src/types/callbacks.d.ts +36 -0
  111. package/dist/src/types/callbacks.js +1 -0
  112. package/dist/src/types/constants.d.ts +6 -0
  113. package/dist/src/types/constants.js +6 -0
  114. package/dist/src/types/credentials.d.ts +30 -0
  115. package/dist/src/types/credentials.js +1 -0
  116. package/dist/src/types/deployment/DeploymentServiceTypes.d.ts +23 -0
  117. package/dist/src/types/deployment/DeploymentServiceTypes.js +1 -0
  118. package/dist/src/types/deployment/DeploymentTypes.d.ts +29 -0
  119. package/dist/src/types/deployment/DeploymentTypes.js +1 -0
  120. package/dist/src/types/deployment/cloudformation.d.ts +14 -0
  121. package/dist/src/types/deployment/cloudformation.js +1 -0
  122. package/dist/src/types/deployment/index.d.ts +5 -0
  123. package/dist/src/types/deployment/index.js +1 -0
  124. package/dist/src/types/deployment/parallel.d.ts +46 -0
  125. package/dist/src/types/deployment/parallel.js +10 -0
  126. package/dist/src/types/errors/CdkError.d.ts +14 -0
  127. package/dist/src/types/errors/CdkError.js +20 -0
  128. package/dist/src/types/errors/ServiceError.d.ts +86 -0
  129. package/dist/src/types/errors/ServiceError.js +119 -0
  130. package/dist/src/types/events.d.ts +40 -0
  131. package/dist/src/types/events.js +5 -0
  132. package/dist/src/types/index.d.ts +20 -0
  133. package/dist/src/types/index.js +9 -0
  134. package/dist/src/types/operations.d.ts +193 -0
  135. package/dist/src/types/operations.js +285 -0
  136. package/dist/src/types/orgConfig.d.ts +28 -0
  137. package/dist/src/types/orgConfig.js +11 -0
  138. package/dist/src/types/params.d.ts +74 -0
  139. package/dist/src/types/params.js +1 -0
  140. package/dist/src/types/patternDetection.d.ts +43 -0
  141. package/dist/src/types/patternDetection.js +92 -0
  142. package/dist/src/types/validation.d.ts +12 -0
  143. package/dist/src/types/validation.js +1 -0
  144. package/dist/src/util/fsHelpers.d.ts +4 -0
  145. package/dist/src/util/fsHelpers.js +16 -0
  146. package/dist/src/util/index.d.ts +3 -0
  147. package/dist/src/util/index.js +3 -0
  148. package/dist/src/util/securityHelpers.d.ts +31 -0
  149. package/dist/src/util/securityHelpers.js +124 -0
  150. package/dist/src/util/singleton.d.ts +2 -0
  151. package/dist/src/util/singleton.js +9 -0
  152. package/dist/src/util/sleep.d.ts +4 -0
  153. package/dist/src/util/sleep.js +4 -0
  154. package/package.json +42 -0
@@ -0,0 +1,65 @@
1
+ import { join } from "path";
2
+ import { success, failure } from "@fjall/generator";
3
+ import { logger } from "@fjall/util";
4
+ import { detectPattern, deriveResourcesFromManifestStacks } from "../types/patternDetection.js";
5
+ import { emitProgress, PROGRESS_MESSAGES } from "../services/supporting/helpers.js";
6
+ /**
7
+ * Pre-deployment analysis pipeline.
8
+ *
9
+ * Detects the application pattern, synthesises infrastructure, computes
10
+ * template hashes, and determines which stacks have changed.
11
+ */
12
+ export async function runDetectionPipeline(operation, services, context, callbacks) {
13
+ // 1. Detect application pattern
14
+ const { pattern, hasDatabase } = detectPattern(operation.path);
15
+ callbacks.onLog?.(`Pattern detected: ${pattern ?? "standard"}`, "info");
16
+ // 2. Synthesise infrastructure
17
+ emitProgress(callbacks, PROGRESS_MESSAGES.SYNTH);
18
+ const synthResult = await services.cdkService.runCdkSynth(context, (chunk) => callbacks.onCdkOutput?.(chunk, "synth"));
19
+ if (!synthResult.success) {
20
+ return failure(new Error(`CDK synthesis failed: ${synthResult.error}`));
21
+ }
22
+ // 3. Compute template hashes
23
+ emitProgress(callbacks, PROGRESS_MESSAGES.HASH);
24
+ const cdkOutPath = join(operation.path, "cdk.out");
25
+ const hashResult = await services.hashService.getTemplateHashes(cdkOutPath);
26
+ if (!hashResult.success) {
27
+ return failure(new Error(`Template hash computation failed: ${hashResult.error.message}`));
28
+ }
29
+ const currentHashes = hashResult.data;
30
+ // 4. Compare with stored state
31
+ const comparisonResult = await services.hashService.compareWithState(currentHashes, operation.path);
32
+ if (!comparisonResult.success) {
33
+ return failure(new Error(`Hash comparison failed: ${comparisonResult.error.message}`));
34
+ }
35
+ const comparison = comparisonResult.data;
36
+ // 5. Stale hash detection: unchanged templates whose stacks don't exist in CFN
37
+ const stackChanges = new Map(comparison.stackChanges);
38
+ for (const [stackName, hasChanged] of comparison.stackChanges) {
39
+ if (!hasChanged) {
40
+ const exists = await services.cfnService.stackExists(stackName);
41
+ if (!exists) {
42
+ logger.debug("detectionPipeline", "Stale hash detected — stack missing in CFN", {
43
+ stackName
44
+ });
45
+ stackChanges.set(stackName, true);
46
+ }
47
+ }
48
+ }
49
+ // 6. Derive resource flags from manifest stacks
50
+ const stackNames = Array.from(currentHashes.keys());
51
+ const resources = deriveResourcesFromManifestStacks(stackNames);
52
+ // 7. Detect Dockerfile (Compute stack presence implies Docker)
53
+ const hasDockerfile = resources.hasCompute;
54
+ const hasDifferences = Array.from(stackChanges.values()).some(Boolean);
55
+ callbacks.onLog?.(`Detection complete: ${comparison.changedCount} changed, ${comparison.unchangedCount} unchanged`, "info");
56
+ return success({
57
+ pattern,
58
+ hasDatabase,
59
+ hasDifferences,
60
+ stackChanges,
61
+ currentHashes,
62
+ resources,
63
+ hasDockerfile
64
+ });
65
+ }
@@ -0,0 +1,56 @@
1
+ import type { Result } from "@fjall/generator";
2
+ export interface DockerServiceConfig {
3
+ name: string;
4
+ dockerfilePath: string;
5
+ dockerTarget?: string;
6
+ }
7
+ export interface DockerBuildParams {
8
+ appName: string;
9
+ appPath: string;
10
+ region: string;
11
+ accountId: string;
12
+ imageTag?: string;
13
+ services?: DockerServiceConfig[];
14
+ platform?: string;
15
+ }
16
+ export interface DockerBuildResult {
17
+ imageUri: string;
18
+ imageTag: string;
19
+ services?: Array<{
20
+ name: string;
21
+ imageUri: string;
22
+ }>;
23
+ }
24
+ export interface ECRInitParams {
25
+ appName: string;
26
+ region: string;
27
+ accountId: string;
28
+ }
29
+ export interface ECRInitResult {
30
+ repositoryUri: string;
31
+ }
32
+ /**
33
+ * Parameters for tagging existing ECR images for ECS services.
34
+ * Used for non-Dockerfile apps where CodeBuild pushes :latest but
35
+ * ECS expects :<serviceName>-latest tags.
36
+ */
37
+ export interface TagImagesParams {
38
+ appName: string;
39
+ appPath: string;
40
+ region: string;
41
+ accountId: string;
42
+ }
43
+ export interface TagImagesResult {
44
+ taggedServices: string[];
45
+ }
46
+ /**
47
+ * Interface for Docker operations (build, push, ECR initialisation).
48
+ * CLI provides CliDockerProvider (wraps dockerode-based services).
49
+ * Worker passes undefined (Docker ops skipped until container deployments enabled).
50
+ */
51
+ export interface DockerProvider {
52
+ buildAndPush(params: DockerBuildParams): Promise<Result<DockerBuildResult>>;
53
+ initialiseECR(params: ECRInitParams): Promise<Result<ECRInitResult>>;
54
+ /** Tag existing ECR images for ECS services (non-Dockerfile apps). Optional. */
55
+ tagImages?(params: TagImagesParams): Promise<Result<TagImagesResult>>;
56
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,37 @@
1
+ import type { Result } from "@fjall/generator";
2
+ import type { DeployCallbacks } from "../types/callbacks.js";
3
+ /**
4
+ * Configuration for a single domain to deploy.
5
+ */
6
+ export interface DomainConfig {
7
+ name: string;
8
+ type: "apex" | "delegated";
9
+ }
10
+ /**
11
+ * Result of a domain deployment phase.
12
+ */
13
+ export interface DomainDeployResult {
14
+ domainsDeployed: number;
15
+ errors: string[];
16
+ }
17
+ /**
18
+ * Provider interface for domain deployment operations.
19
+ *
20
+ * Domain deployment requires CLI-specific services (zone file parsing, CDK
21
+ * synthesis in domain component directories). deploy-core defines the cascade
22
+ * structure; the caller provides the implementation.
23
+ *
24
+ * If no provider is given, the domains phase is skipped.
25
+ */
26
+ export interface DomainDeployProvider {
27
+ /**
28
+ * Get the list of configured domains for this organisation.
29
+ * Returns an empty array if no domains are configured.
30
+ */
31
+ getDomains(): DomainConfig[];
32
+ /**
33
+ * Deploy a single domain. Apex domains create hosted zones and delegation
34
+ * roles; delegated domains create NS records in the parent zone.
35
+ */
36
+ deployDomain(domainName: string, callbacks: DeployCallbacks): Promise<Result<void>>;
37
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ export { deploy } from "./deploy.js";
2
+ export { deployOrganisation } from "./organisationDeploy.js";
3
+ export type { DockerProvider, DockerServiceConfig, DockerBuildParams, DockerBuildResult, ECRInitParams, ECRInitResult, TagImagesParams, TagImagesResult } from "./dockerInterface.js";
4
+ export type { DomainDeployProvider, DomainConfig, DomainDeployResult } from "./domainInterface.js";
5
+ export type { DeployServices } from "./serviceFactory.js";
6
+ export type { DetectionResult } from "./detectionPipeline.js";
7
+ export { runOrganisationSetup } from "./organisationSetup.js";
8
+ export type { OrgSetupPhase, OrgSetupCallbacks, OrgSetupConfig, OrgSetupResult } from "./organisationSetup.js";
@@ -0,0 +1,3 @@
1
+ export { deploy } from "./deploy.js";
2
+ export { deployOrganisation } from "./organisationDeploy.js";
3
+ export { runOrganisationSetup } from "./organisationSetup.js";
@@ -0,0 +1,16 @@
1
+ import { type Result } from "@fjall/generator";
2
+ import type { DeployParams, DeployResult } from "../types/params.js";
3
+ import type { OrganisationOperation } from "../types/operations.js";
4
+ import type { DeployServices } from "./serviceFactory.js";
5
+ /**
6
+ * Organisation deployment orchestration.
7
+ *
8
+ * Handles three target types:
9
+ * - organisation: deploy org infra + cascade to platform + all accounts
10
+ * - platform: deploy platform stack only
11
+ * - account: deploy single account stack
12
+ *
13
+ * Auth and org setup (creating AWS accounts/OUs) are the caller's
14
+ * responsibility. deploy-core receives credentials and deploys.
15
+ */
16
+ export declare function deployOrganisation(params: DeployParams, services: DeployServices, operation: OrganisationOperation): Promise<Result<DeployResult>>;
@@ -0,0 +1,382 @@
1
+ import { success, failure } from "@fjall/generator";
2
+ import { logger } from "@fjall/util";
3
+ import { ORGANISATION_TYPES, getOrganisationStackName } from "../types/operations.js";
4
+ 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";
8
+ /**
9
+ * Organisation deployment orchestration.
10
+ *
11
+ * Handles three target types:
12
+ * - organisation: deploy org infra + cascade to platform + all accounts
13
+ * - platform: deploy platform stack only
14
+ * - account: deploy single account stack
15
+ *
16
+ * Auth and org setup (creating AWS accounts/OUs) are the caller's
17
+ * responsibility. deploy-core receives credentials and deploys.
18
+ */
19
+ export async function deployOrganisation(params, services, operation) {
20
+ const startTime = Date.now();
21
+ switch (operation.type) {
22
+ case ORGANISATION_TYPES.ORGANISATION:
23
+ return deployOrgWithCascade(params, services, operation, startTime);
24
+ case ORGANISATION_TYPES.PLATFORM:
25
+ return deploySingleComponent(params, services, operation, "platform", startTime);
26
+ case ORGANISATION_TYPES.ACCOUNT:
27
+ return deploySingleComponent(params, services, operation, "account", startTime);
28
+ default: {
29
+ const _exhaustive = operation.type;
30
+ return failure(new Error(`Unsupported organisation type: ${String(_exhaustive)}`));
31
+ }
32
+ }
33
+ }
34
+ /**
35
+ * Build a deployment context for an organisation component.
36
+ */
37
+ function buildOrgContext(params, services, operation, deployType, accountName) {
38
+ return CdkContextBuilder.buildDeploymentContext({
39
+ deployType,
40
+ target: operation.target,
41
+ path: operation.path,
42
+ region: services.awsProvider.getRegion(),
43
+ accountName,
44
+ callerIdentity: {
45
+ Account: services.awsProvider.getAccountId() ?? "",
46
+ Arn: "",
47
+ UserId: ""
48
+ },
49
+ ...buildParamsContext(params)
50
+ }, {
51
+ verbose: params.options?.verbose,
52
+ infraOnly: params.options?.infraOnly
53
+ }, params.orgConfig);
54
+ }
55
+ /**
56
+ * Deploy a single organisation component (platform or account).
57
+ */
58
+ async function deploySingleComponent(params, services, operation, deployType, startTime) {
59
+ const { callbacks } = params;
60
+ const context = buildOrgContext(params, services, operation, deployType, deployType === "account" ? operation.target : undefined);
61
+ // Synth
62
+ callbacks.onLog?.(`Synthesising ${deployType} infrastructure…`, "info");
63
+ const synthResult = await services.cdkService.runCdkSynth(context, (chunk) => callbacks.onCdkOutput?.(chunk, "synth"));
64
+ if (!synthResult.success) {
65
+ const error = new Error(`CDK synthesis failed: ${synthResult.error}`);
66
+ callbacks.onError?.(error);
67
+ return failure(error);
68
+ }
69
+ // Bootstrap
70
+ callbacks.onCDKBootstrap?.("bootstrapping");
71
+ const bootstrapResult = await services.cdkService.runCdkBootstrap(context, (chunk) => callbacks.onOutput?.(chunk));
72
+ if (!bootstrapResult.success) {
73
+ callbacks.onCDKBootstrap?.("failed");
74
+ const error = new Error(`Bootstrap failed: ${bootstrapResult.error}`);
75
+ callbacks.onError?.(error);
76
+ return failure(error);
77
+ }
78
+ callbacks.onCDKBootstrap?.("complete");
79
+ // Deploy
80
+ const stackName = getOrganisationStackName(operation.type);
81
+ const stepId = `${deployType}-deploy`;
82
+ const stepName = `Deploying ${deployType} infrastructure`;
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);
85
+ if (!deployResult.success) {
86
+ callbacks.onStepComplete?.(stepId, stepName, "error", 0, 1);
87
+ const error = new Error(deployResult.error);
88
+ callbacks.onError?.(error);
89
+ return failure(error);
90
+ }
91
+ callbacks.onStepComplete?.(stepId, stepName, "completed", 0, 1);
92
+ return success({
93
+ target: operation.target,
94
+ deploymentType: "organisation",
95
+ durationMs: Date.now() - startTime
96
+ });
97
+ }
98
+ /**
99
+ * Full organisation deployment with cascade to platform + member accounts.
100
+ */
101
+ async function deployOrgWithCascade(params, services, operation, startTime) {
102
+ const { callbacks, options } = params;
103
+ const providerAccounts = params.orgConfig?.providerAccounts ?? [];
104
+ const context = buildOrgContext(params, services, operation, "organisation");
105
+ // Synth
106
+ callbacks.onLog?.("Synthesising organisation infrastructure…", "info");
107
+ const synthResult = await services.cdkService.runCdkSynth(context, (chunk) => callbacks.onCdkOutput?.(chunk, "synth"));
108
+ if (!synthResult.success) {
109
+ const error = new Error(`CDK synthesis failed: ${synthResult.error}`);
110
+ callbacks.onError?.(error);
111
+ return failure(error);
112
+ }
113
+ // Bootstrap org account
114
+ callbacks.onCDKBootstrap?.("bootstrapping");
115
+ const bootstrapResult = await services.cdkService.runCdkBootstrap(context, (chunk) => callbacks.onOutput?.(chunk));
116
+ if (!bootstrapResult.success) {
117
+ callbacks.onCDKBootstrap?.("failed");
118
+ const error = new Error(`Bootstrap failed: ${bootstrapResult.error}`);
119
+ callbacks.onError?.(error);
120
+ return failure(error);
121
+ }
122
+ callbacks.onCDKBootstrap?.("complete");
123
+ // Deploy org infrastructure
124
+ const orgStepId = "organisation-deploy";
125
+ const orgStepName = "Deploying organisation infrastructure";
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);
130
+ const orgStackName = getOrganisationStackName(ORGANISATION_TYPES.ORGANISATION);
131
+ const orgResult = await services.cdkService.runCdkDeploy(context, orgStackName, (chunk) => callbacks.onOutput?.(chunk), (event) => callbacks.onResourceProgress?.(event), services.awsProvider);
132
+ if (!orgResult.success) {
133
+ callbacks.onStepComplete?.(orgStepId, orgStepName, "error", 0, totalSteps);
134
+ const error = new Error(orgResult.error);
135
+ callbacks.onError?.(error);
136
+ return failure(error);
137
+ }
138
+ callbacks.onStepComplete?.(orgStepId, orgStepName, "completed", 0, totalSteps);
139
+ // Cascade to platform + domains + member accounts
140
+ if (cascadeEnabled && providerAccounts.length > 0) {
141
+ callbacks.onCascadeStart?.();
142
+ const cascadeErrors = [];
143
+ let accountsDeployed = 0;
144
+ let platformDeployed = false;
145
+ let domainsDeployed = false;
146
+ // Phase 1: Deploy platform account
147
+ const platformAccount = providerAccounts.find((acc) => acc.environment === "platform");
148
+ if (platformAccount) {
149
+ callbacks.onCascadePhaseStart?.("platform");
150
+ const platformResult = await deployCascadeAccount(params, services, operation, platformAccount, "platform", callbacks);
151
+ if (platformResult.success) {
152
+ platformDeployed = true;
153
+ }
154
+ else {
155
+ cascadeErrors.push({
156
+ accountId: platformAccount.id,
157
+ error: platformResult.error.message
158
+ });
159
+ }
160
+ }
161
+ // Phase 1.5: Read Platform stack outputs for IPAM pool IDs
162
+ let ipamPoolIds = new Map();
163
+ if (platformDeployed && platformAccount) {
164
+ ipamPoolIds = await readPlatformIpamPoolIds(services, platformAccount, callbacks);
165
+ }
166
+ // Phase 2: Deploy domains (apex sequential, delegated parallel)
167
+ if (params.domainProvider) {
168
+ const domainResult = await deployDomains(params.domainProvider, callbacks);
169
+ domainsDeployed = domainResult.domainsDeployed > 0;
170
+ for (const err of domainResult.errors) {
171
+ cascadeErrors.push({ accountId: "domains", error: err });
172
+ }
173
+ }
174
+ // Phase 3: Deploy member accounts in parallel
175
+ const memberAccounts = providerAccounts.filter((acc) => acc.environment !== "root" && acc.environment !== "platform");
176
+ if (memberAccounts.length > 0) {
177
+ callbacks.onCascadePhaseStart?.("accounts");
178
+ const region = services.awsProvider.getRegion();
179
+ const memberResults = await Promise.all(memberAccounts.map((account) => {
180
+ const regionSuffix = region.replace(/-/g, "");
181
+ const ipamPoolId = ipamPoolIds.get(`${account.id}-${regionSuffix}`);
182
+ return deployCascadeAccount(params, services, operation, account, "account", callbacks, ipamPoolId);
183
+ }));
184
+ for (let j = 0; j < memberResults.length; j++) {
185
+ const result = memberResults[j];
186
+ const account = memberAccounts[j];
187
+ if (result.success) {
188
+ accountsDeployed++;
189
+ }
190
+ else {
191
+ cascadeErrors.push({
192
+ accountId: account.id,
193
+ error: result.error.message
194
+ });
195
+ }
196
+ }
197
+ }
198
+ callbacks.onCascadeComplete?.({
199
+ platformDeployed,
200
+ domainsDeployed,
201
+ accountsDeployed,
202
+ accountsFailed: cascadeErrors.length,
203
+ errors: cascadeErrors
204
+ });
205
+ if (cascadeErrors.length > 0) {
206
+ const errorSummary = cascadeErrors
207
+ .map((e) => ` ${e.accountId}: ${e.error}`)
208
+ .join("\n");
209
+ callbacks.onLog?.(`Cascade completed with ${cascadeErrors.length} failure(s):\n${errorSummary}`, "warn");
210
+ }
211
+ }
212
+ return success({
213
+ target: operation.target,
214
+ deploymentType: "organisation",
215
+ durationMs: Date.now() - startTime
216
+ });
217
+ }
218
+ /**
219
+ * Deploy a single cascade account (platform or member).
220
+ * Assumes the target account's role, sets env credentials, and deploys.
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
260
+ });
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
+ }
@@ -0,0 +1,42 @@
1
+ import { type Result } from "@fjall/generator";
2
+ import type { AwsProvider } from "../aws/AwsProvider.js";
3
+ export type OrgSetupPhase = "create-organisation" | "enable-policies" | "enable-service-access" | "enable-ram-sharing" | "activate-trusted-access" | "enable-ipam" | "configure-backup" | "create-accounts" | "create-organisational-units" | "place-accounts" | "activate-cost-tags" | "check-identity-centre";
4
+ export interface OrgSetupCallbacks {
5
+ onPhaseStart?(phase: OrgSetupPhase): void;
6
+ onPhaseComplete?(phase: OrgSetupPhase, result: "completed" | "skipped" | "error"): void;
7
+ onProgress?(message: string): void;
8
+ onError?(phase: OrgSetupPhase, error: Error): void;
9
+ }
10
+ export interface OrgSetupConfig {
11
+ accounts: Array<{
12
+ name: string;
13
+ email: string;
14
+ }>;
15
+ platformAccountId: string;
16
+ organisationalUnits: string[];
17
+ accountPlacements?: Record<string, string>;
18
+ costAllocationTags?: string[];
19
+ skipIdentityCentre?: boolean;
20
+ }
21
+ export interface OrgSetupResult {
22
+ organisationId: string;
23
+ createdAccounts: Array<{
24
+ name: string;
25
+ accountId: string;
26
+ }>;
27
+ identityCentreStatus?: string;
28
+ phasesCompleted: OrgSetupPhase[];
29
+ phasesSkipped: OrgSetupPhase[];
30
+ errors: Array<{
31
+ phase: OrgSetupPhase;
32
+ error: string;
33
+ }>;
34
+ }
35
+ /**
36
+ * Orchestrate the full AWS Organisation setup sequence.
37
+ *
38
+ * Runs 12 phases sequentially. Non-fatal phase failures are recorded
39
+ * and execution continues. The only fatal failure is phase 1
40
+ * (create-organisation) since all subsequent phases depend on the org ID.
41
+ */
42
+ export declare function runOrganisationSetup(awsProvider: AwsProvider, config: OrgSetupConfig, callbacks?: OrgSetupCallbacks): Promise<Result<OrgSetupResult>>;