@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,33 @@
1
+ export function hasCdkDifferences(output) {
2
+ if (!output)
3
+ return false;
4
+ // Prefer the summary line: "Number of stacks with differences: 2"
5
+ const summaryRegex = /Number of stacks with differences:\s*(\d+)/;
6
+ const summaryMatch = output.match(summaryRegex);
7
+ if (summaryMatch && summaryMatch[1]) {
8
+ const diffCount = parseInt(summaryMatch[1], 10);
9
+ if (!isNaN(diffCount)) {
10
+ return diffCount > 0;
11
+ }
12
+ }
13
+ // Fallback: if all stacks report "There were no differences" then no changes
14
+ const allNoDiff = output
15
+ .split("\n")
16
+ .filter((l) => l.trim().startsWith("Stack"))
17
+ .every((l) => l.includes("There were no differences"));
18
+ return !allNoDiff;
19
+ }
20
+ export function parseDiffOutput(output) {
21
+ const details = {
22
+ hasSecurityChanges: output.includes("(requires replacement)") ||
23
+ output.includes("IAM Statement Changes"),
24
+ resourceChanges: 0,
25
+ outputChanges: 0,
26
+ resources: []
27
+ };
28
+ const resourceMatches = output.match(/\[\+\]|\[-\]|\[~\]/g);
29
+ if (resourceMatches) {
30
+ details.resourceChanges = resourceMatches.length;
31
+ }
32
+ return details;
33
+ }
@@ -0,0 +1,20 @@
1
+ import { type ChildProcess } from "child_process";
2
+ import type { Result } from "@fjall/generator";
3
+ import type { CdkOptions, CdkOutput } from "./CdkService.js";
4
+ import { type CdkArgumentBuilder } from "./CdkArgumentBuilder.js";
5
+ export declare class CdkProcessManager {
6
+ private runningProcesses;
7
+ private processCounter;
8
+ private argBuilder;
9
+ constructor(argBuilder: CdkArgumentBuilder);
10
+ forceKillProcess(child: ChildProcess): void;
11
+ cleanup(): void;
12
+ runCdkCommandPassthrough(workingDir: string, args: string[], options?: CdkOptions & {
13
+ ignoreExitCode?: boolean;
14
+ }): Promise<Result<CdkOutput, string>>;
15
+ runCdkCommand(workingDir: string, args: string[], options?: CdkOptions & {
16
+ ignoreExitCode?: boolean;
17
+ skipProjectCheck?: boolean;
18
+ extraEnv?: Record<string, string>;
19
+ }): Promise<Result<CdkOutput, string>>;
20
+ }
@@ -0,0 +1,244 @@
1
+ import { spawn } from "child_process";
2
+ import { existsSync } from "fs";
3
+ import { join } from "path";
4
+ import { createRequire } from "module";
5
+ import { filterDangerousEnvVars, maskSensitiveOutput } from "../../util/securityHelpers.js";
6
+ import { logger } from "@fjall/util";
7
+ import { success, failure } from "@fjall/generator";
8
+ import { getErrorMessage } from "@fjall/util";
9
+ /**
10
+ * Resolve CDK binary path from the caller's node_modules.
11
+ * Falls back to "npx cdk" only if resolution fails (should never happen
12
+ * since aws-cdk is a direct dependency of the calling package).
13
+ */
14
+ function getCdkBinaryPath() {
15
+ try {
16
+ const require = createRequire(import.meta.url);
17
+ const cdkEntry = require.resolve("aws-cdk/bin/cdk");
18
+ return { command: process.execPath, prefixArgs: [cdkEntry] };
19
+ }
20
+ catch {
21
+ logger.debug("CdkService", "Failed to resolve aws-cdk binary, falling back to npx");
22
+ return { command: "npx", prefixArgs: ["cdk"] };
23
+ }
24
+ }
25
+ const cdkBin = getCdkBinaryPath();
26
+ /** Grace period before escalating from SIGTERM to SIGKILL on a stuck CDK process */
27
+ const SIGKILL_GRACE_PERIOD_MS = 5_000;
28
+ export class CdkProcessManager {
29
+ runningProcesses = new Map();
30
+ processCounter = 0;
31
+ argBuilder;
32
+ constructor(argBuilder) {
33
+ this.argBuilder = argBuilder;
34
+ // Register cleanup handlers
35
+ process.on("exit", () => this.cleanup());
36
+ process.on("SIGINT", () => {
37
+ this.cleanup();
38
+ process.exit(130);
39
+ });
40
+ process.on("SIGTERM", () => {
41
+ this.cleanup();
42
+ process.exit(143);
43
+ });
44
+ }
45
+ forceKillProcess(child) {
46
+ child.stdout?.destroy();
47
+ child.stderr?.destroy();
48
+ child.kill("SIGTERM");
49
+ setTimeout(() => {
50
+ if (child.exitCode === null) {
51
+ child.kill("SIGKILL");
52
+ }
53
+ }, SIGKILL_GRACE_PERIOD_MS);
54
+ }
55
+ cleanup() {
56
+ for (const [_name, child] of this.runningProcesses) {
57
+ if (!child.killed) {
58
+ child.stdout?.destroy();
59
+ child.stderr?.destroy();
60
+ child.kill("SIGTERM");
61
+ }
62
+ }
63
+ this.runningProcesses.clear();
64
+ }
65
+ async runCdkCommandPassthrough(workingDir, args, options) {
66
+ return new Promise((resolve) => {
67
+ if (!existsSync(workingDir)) {
68
+ resolve(failure(`Directory not found: ${workingDir}`));
69
+ return;
70
+ }
71
+ // Check for cdk.json in the working directory
72
+ const cdkJsonPath = join(workingDir, "cdk.json");
73
+ if (!existsSync(cdkJsonPath)) {
74
+ resolve(failure(`No CDK project found in ${workingDir}`));
75
+ return;
76
+ }
77
+ const contextArgs = this.argBuilder.buildContextArgs(options?.context);
78
+ const fullArgs = [...cdkBin.prefixArgs, ...args, ...contextArgs];
79
+ const env = this.argBuilder.buildCdkEnv(options);
80
+ // Run CDK with stdio: 'inherit' for direct terminal output
81
+ const child = spawn(cdkBin.command, fullArgs, {
82
+ cwd: workingDir,
83
+ env,
84
+ stdio: "inherit", // Pass through to terminal directly
85
+ shell: false,
86
+ detached: false
87
+ });
88
+ // Track process for cleanup
89
+ const processId = `cdk-passthrough-${Date.now()}`;
90
+ this.runningProcesses.set(processId, child);
91
+ // Timeout to prevent indefinite hangs (default 30 minutes, matching deploy)
92
+ let timeoutHandle;
93
+ if (options?.timeout) {
94
+ const timeout = options.timeout;
95
+ timeoutHandle = setTimeout(() => {
96
+ if (!child.killed) {
97
+ this.forceKillProcess(child);
98
+ this.runningProcesses.delete(processId);
99
+ resolve(failure(`CDK command timed out after ${Math.floor(timeout / 60000)} minutes`));
100
+ }
101
+ }, timeout);
102
+ }
103
+ child.on("close", (code) => {
104
+ if (timeoutHandle)
105
+ clearTimeout(timeoutHandle);
106
+ this.runningProcesses.delete(processId);
107
+ if (code === 0 || options?.ignoreExitCode) {
108
+ resolve(success({ exitCode: code || 0 }));
109
+ }
110
+ else {
111
+ resolve(failure(`CDK command failed with exit code ${code}`));
112
+ }
113
+ });
114
+ child.on("error", (err) => {
115
+ if (timeoutHandle)
116
+ clearTimeout(timeoutHandle);
117
+ this.runningProcesses.delete(processId);
118
+ resolve(failure(`Failed to run CDK command: ${err.message}`));
119
+ });
120
+ });
121
+ }
122
+ async runCdkCommand(workingDir, args, options) {
123
+ return new Promise((resolve) => {
124
+ if (!existsSync(workingDir)) {
125
+ resolve(failure(`Directory not found: ${workingDir}`));
126
+ return;
127
+ }
128
+ // Check for cdk.json unless caller explicitly skips (e.g., bootstrap runs from tmpdir)
129
+ if (!options?.skipProjectCheck) {
130
+ const cdkJsonPath = join(workingDir, "cdk.json");
131
+ if (!existsSync(cdkJsonPath)) {
132
+ resolve(failure(`No CDK project found in ${workingDir}`));
133
+ return;
134
+ }
135
+ }
136
+ let output = "";
137
+ let errorOutput = "";
138
+ let processKilled = false;
139
+ const contextArgs = this.argBuilder.buildContextArgs(options?.context);
140
+ const fullArgs = [...cdkBin.prefixArgs, ...args, ...contextArgs];
141
+ const env = {
142
+ ...this.argBuilder.buildCdkEnv(options),
143
+ NO_COLOR: "1",
144
+ ...(options?.extraEnv ? filterDangerousEnvVars(options.extraEnv) : {})
145
+ };
146
+ // Try to spawn the CDK process
147
+ logger.debug("CdkService", "Spawning CDK process", {
148
+ command: `${cdkBin.command} ${fullArgs.join(" ")}`,
149
+ workingDir
150
+ });
151
+ const child = spawn(cdkBin.command, fullArgs, {
152
+ cwd: workingDir,
153
+ env,
154
+ stdio: ["ignore", "pipe", "pipe"],
155
+ shell: false,
156
+ detached: false
157
+ });
158
+ // Check if spawn was successful
159
+ if (!child.pid) {
160
+ const spawnError = `Failed to spawn CDK process - no PID. cwd=${workingDir}, args=${fullArgs.join(" ")}`;
161
+ // CRITICAL: Attach error handler to prevent uncaught exception
162
+ // Node.js emits an async 'error' event on the child process even after spawn fails
163
+ // If we don't handle it, it becomes an uncaught exception and crashes the app
164
+ child.on("error", () => {
165
+ // Error already handled via the resolve below - this just prevents uncaught exception
166
+ });
167
+ resolve(failure(spawnError));
168
+ return;
169
+ }
170
+ // Track process for cleanup
171
+ const processId = `cdk-${++this.processCounter}`;
172
+ this.runningProcesses.set(processId, child);
173
+ let timeoutHandle;
174
+ if (options?.timeout) {
175
+ timeoutHandle = setTimeout(() => {
176
+ if (!child.killed) {
177
+ processKilled = true;
178
+ this.forceKillProcess(child);
179
+ }
180
+ }, options.timeout);
181
+ }
182
+ child.stdout?.on("data", (data) => {
183
+ const chunk = data.toString();
184
+ output += chunk;
185
+ if (options?.outputCallback) {
186
+ options.outputCallback(maskSensitiveOutput(chunk));
187
+ }
188
+ options?.cdkOutputLogger?.writeCdkOutput("stdout", maskSensitiveOutput(chunk));
189
+ });
190
+ child.stderr?.on("data", (data) => {
191
+ const chunk = data.toString();
192
+ options?.cdkOutputLogger?.writeCdkOutput("stderr", maskSensitiveOutput(chunk));
193
+ // Filter out non-critical warnings from the in-memory error buffer
194
+ if (!chunk.includes("deprecated") &&
195
+ !chunk.includes("npm WARN") &&
196
+ !chunk.includes("ENOENT")) {
197
+ errorOutput += chunk;
198
+ }
199
+ // Always send to callback for visibility
200
+ if (options?.errorCallback) {
201
+ options.errorCallback(maskSensitiveOutput(chunk));
202
+ }
203
+ });
204
+ child.on("error", (error) => {
205
+ if (timeoutHandle)
206
+ clearTimeout(timeoutHandle);
207
+ this.runningProcesses.delete(processId);
208
+ resolve(failure(getErrorMessage(error)));
209
+ });
210
+ child.on("close", (code) => {
211
+ if (timeoutHandle)
212
+ clearTimeout(timeoutHandle);
213
+ this.runningProcesses.delete(processId);
214
+ if (processKilled) {
215
+ resolve(failure("CDK command timed out"));
216
+ return;
217
+ }
218
+ const exitedClean = code === 0 || (options?.ignoreExitCode === true && code === 1);
219
+ // For diff command, we need to include stderr in output since CDK outputs errors there
220
+ const combinedOutput = output + (errorOutput ? `\n${errorOutput}` : "");
221
+ if (exitedClean) {
222
+ resolve(success({
223
+ output: args.includes("diff") ? combinedOutput : output,
224
+ exitCode: code || 0
225
+ }));
226
+ return;
227
+ }
228
+ // Parse error output for meaningful messages
229
+ let errorMessage = errorOutput;
230
+ if (output) {
231
+ // Try to extract error from CDK output
232
+ const errorMatch = output.match(/❌.*?Error:(.*)$/m);
233
+ if (errorMatch) {
234
+ errorMessage = errorMatch[1].trim();
235
+ }
236
+ }
237
+ // Include stdout in the error string so callers can pattern-match on combined output
238
+ const errorText = errorMessage || `CDK command failed with exit code ${code}`;
239
+ const fullError = output ? `${errorText}\n${output}` : errorText;
240
+ resolve(failure(fullError));
241
+ });
242
+ });
243
+ }
244
+ }
@@ -0,0 +1,71 @@
1
+ import { type ResourceEvent } from "../../aws/utils/cloudformationEvents.js";
2
+ import type { DeploymentContext } from "../../types/deployment/DeploymentTypes.js";
3
+ import type { StepOutput } from "../../types/deployment/DeploymentTypes.js";
4
+ import type { AwsProvider } from "../../aws/AwsProvider.js";
5
+ import type { Result } from "@fjall/generator";
6
+ import type { CdkError } from "../../types/errors/CdkError.js";
7
+ import type { EventLogWriterFactory } from "../../aws/utils/cloudformationEvents.js";
8
+ import { type CheckDifferencesResult } from "./CdkCommandRunner.js";
9
+ export interface CdkContext {
10
+ accountId?: string;
11
+ region?: string;
12
+ environment?: string;
13
+ stackName?: string;
14
+ managedAccount?: boolean;
15
+ accountName?: string;
16
+ imageVersion?: string;
17
+ orgId?: string;
18
+ rootId?: string;
19
+ managementAccountId?: string;
20
+ ipamPoolId?: string;
21
+ fjallOrgId?: string;
22
+ orgConfig?: string;
23
+ }
24
+ export interface CdkOptions {
25
+ verbose?: boolean;
26
+ noPrompt?: boolean;
27
+ outputCallback?: (output: string) => void;
28
+ errorCallback?: (error: string) => void;
29
+ context?: CdkContext;
30
+ timeout?: number;
31
+ passThroughCDK?: boolean;
32
+ stacks?: string[];
33
+ noLookups?: boolean;
34
+ useCdkOut?: boolean;
35
+ credentials?: {
36
+ accessKeyId: string;
37
+ secretAccessKey: string;
38
+ sessionToken?: string;
39
+ };
40
+ outputDir?: string;
41
+ appDir?: string;
42
+ cdkOutputLogger?: {
43
+ writeCdkOutput(stream: "stdout" | "stderr", chunk: string): void;
44
+ };
45
+ }
46
+ export interface CdkOutput {
47
+ output?: string;
48
+ exitCode?: number;
49
+ }
50
+ export interface CdkServiceOptions {
51
+ eventLogWriterFactory?: EventLogWriterFactory;
52
+ }
53
+ export { type CheckDifferencesResult } from "./CdkCommandRunner.js";
54
+ export declare class CdkService {
55
+ private commandRunner;
56
+ private eventMonitor;
57
+ constructor(options?: CdkServiceOptions);
58
+ checkDifferences(path: string, stackName?: string, options?: CdkOptions): Promise<Result<CheckDifferencesResult, CdkError>>;
59
+ deploy(path: string, stackName?: string, options?: CdkOptions): Promise<Result<CdkOutput, string>>;
60
+ destroy(path: string, stackName?: string, options?: CdkOptions): Promise<Result<CdkOutput, string>>;
61
+ runImport(path: string, resourceMappingFile?: string, options?: CdkOptions): Promise<Result<CdkOutput, string>>;
62
+ synth(path: string, options?: CdkOptions): Promise<Result<CdkOutput, string>>;
63
+ bootstrap(accountId: string, region: string, options?: CdkOptions): Promise<Result<CdkOutput, string>>;
64
+ private resolveStackName;
65
+ private buildDeploymentCdkContext;
66
+ runCdkSynth(context: DeploymentContext, onOutput?: (chunk: string) => void): Promise<Result<StepOutput, string>>;
67
+ runCdkBootstrap(context: DeploymentContext, onOutput?: (chunk: string) => void): Promise<Result<StepOutput, string>>;
68
+ runCdkDiff(context: DeploymentContext, onOutput?: (chunk: string) => void): Promise<Result<StepOutput, string>>;
69
+ runCdkDeploy(context: DeploymentContext, stackPattern?: string, onOutput?: (chunk: string) => void, onResourceProgress?: (event: ResourceEvent) => void, aws?: AwsProvider): Promise<Result<StepOutput, string>>;
70
+ runCdkDestroy(context: DeploymentContext, stackPattern?: string, onOutput?: (chunk: string) => void, onResourceProgress?: (event: ResourceEvent) => void, aws?: AwsProvider, useCdkOut?: boolean): Promise<Result<StepOutput, string>>;
71
+ }
@@ -0,0 +1,254 @@
1
+ import { existsSync } from "fs";
2
+ import { join } from "path";
3
+ import { logger } from "@fjall/util";
4
+ import { success, failure } from "@fjall/generator";
5
+ import { DEFAULT_REGION } from "../../aws/utils/regions.js";
6
+ import { getApplicationStackName, getOrganisationStackName, isApplicationStack } from "../../types/operations.js";
7
+ import { getErrorMessage } from "@fjall/util";
8
+ import { CdkEventMonitor, startStackMonitoring } from "./CdkEventMonitoring.js";
9
+ import { analyseDeployOutput, analyseDestroyResult, createEnhancedOutputCallback } from "./CdkOutputAnalyser.js";
10
+ import { CdkCommandRunner } from "./CdkCommandRunner.js";
11
+ /** Delay before falling back to the predicted stack name for CloudFormation monitoring */
12
+ const STACK_DETECTION_FALLBACK_MS = 5_000;
13
+ export class CdkService {
14
+ commandRunner;
15
+ eventMonitor;
16
+ constructor(options) {
17
+ this.commandRunner = new CdkCommandRunner();
18
+ this.eventMonitor = new CdkEventMonitor({
19
+ eventLogWriterFactory: options?.eventLogWriterFactory
20
+ });
21
+ }
22
+ // Delegate low-level commands to CdkCommandRunner
23
+ async checkDifferences(path, stackName, options) {
24
+ return this.commandRunner.checkDifferences(path, stackName, options);
25
+ }
26
+ async deploy(path, stackName, options) {
27
+ return this.commandRunner.deploy(path, stackName, options);
28
+ }
29
+ async destroy(path, stackName, options) {
30
+ return this.commandRunner.destroy(path, stackName, options);
31
+ }
32
+ async runImport(path, resourceMappingFile, options) {
33
+ return this.commandRunner.runImport(path, resourceMappingFile, options);
34
+ }
35
+ async synth(path, options) {
36
+ return this.commandRunner.synth(path, options);
37
+ }
38
+ async bootstrap(accountId, region, options) {
39
+ return this.commandRunner.bootstrap(accountId, region, options);
40
+ }
41
+ resolveStackName(stackPattern, context) {
42
+ if (stackPattern && !stackPattern.includes("*")) {
43
+ return stackPattern;
44
+ }
45
+ if (stackPattern) {
46
+ const stackTypeMatch = stackPattern.match(/\*?(\w+)\*?/);
47
+ if (stackTypeMatch) {
48
+ const stackType = stackTypeMatch[1];
49
+ const appName = context.target || "app";
50
+ return isApplicationStack(stackType)
51
+ ? getApplicationStackName(appName, stackType)
52
+ : `${appName}${stackType}`;
53
+ }
54
+ return stackPattern;
55
+ }
56
+ return undefined;
57
+ }
58
+ buildDeploymentCdkContext(context, accountId, region, options) {
59
+ const environment = undefined;
60
+ return {
61
+ accountId,
62
+ region,
63
+ environment,
64
+ managedAccount: context.isManagedAccount,
65
+ ...(options?.includeImageVersion !== false && {
66
+ imageVersion: context.imageVersion
67
+ }),
68
+ orgId: context.orgId,
69
+ rootId: context.rootId,
70
+ managementAccountId: context.managementAccountId,
71
+ ipamPoolId: context.ipamPoolId,
72
+ fjallOrgId: context.fjallOrgId,
73
+ orgConfig: context.orgConfig
74
+ };
75
+ }
76
+ async runCdkSynth(context, onOutput) {
77
+ const accountId = context.callerIdentity?.Account;
78
+ try {
79
+ const result = await this.synth(context.path, {
80
+ outputCallback: onOutput,
81
+ context: this.buildDeploymentCdkContext(context, accountId, context.region || DEFAULT_REGION)
82
+ });
83
+ if (!result.success) {
84
+ return failure(result.error || "Failed to synthesise CloudFormation template");
85
+ }
86
+ return success({
87
+ message: "CloudFormation template synthesised",
88
+ details: result.data.output
89
+ ? { synthesisTime: result.data.output }
90
+ : undefined
91
+ });
92
+ }
93
+ catch (error) {
94
+ return failure(`CDK synth failed: ${getErrorMessage(error)}`);
95
+ }
96
+ }
97
+ async runCdkBootstrap(context, onOutput) {
98
+ const accountId = context.callerIdentity?.Account;
99
+ const region = context.region || DEFAULT_REGION;
100
+ try {
101
+ if (!accountId) {
102
+ return failure("No AWS account ID available");
103
+ }
104
+ const nodeModulesPath = join(context.path, "node_modules");
105
+ if (!existsSync(nodeModulesPath)) {
106
+ return failure(`Dependencies not installed. Please run 'npm install' in ${context.path} before deploying.`);
107
+ }
108
+ const result = await this.bootstrap(accountId, region, {
109
+ outputCallback: onOutput
110
+ });
111
+ if (!result.success) {
112
+ return failure(result.error || "Failed to bootstrap AWS environment");
113
+ }
114
+ return success({ message: "AWS environment bootstrapped" });
115
+ }
116
+ catch (error) {
117
+ return failure(`CDK bootstrap failed: ${getErrorMessage(error)}`);
118
+ }
119
+ }
120
+ async runCdkDiff(context, onOutput) {
121
+ const accountId = context.callerIdentity?.Account;
122
+ try {
123
+ const result = await this.checkDifferences(context.path, undefined, {
124
+ verbose: context.options?.verbose,
125
+ outputCallback: onOutput,
126
+ context: this.buildDeploymentCdkContext(context, accountId, context.region || DEFAULT_REGION)
127
+ });
128
+ if (!result.success) {
129
+ return failure(`CDK diff failed: ${result.error.message}`);
130
+ }
131
+ return success({
132
+ message: "Diff check complete",
133
+ details: {
134
+ hasDifferences: result.data.hasDifferences,
135
+ details: result.data.details
136
+ }
137
+ });
138
+ }
139
+ catch (error) {
140
+ return failure(`CDK diff failed: ${getErrorMessage(error)}`);
141
+ }
142
+ }
143
+ async runCdkDeploy(context, stackPattern, onOutput, onResourceProgress, aws) {
144
+ const accountId = context.callerIdentity?.Account;
145
+ const region = context.region || DEFAULT_REGION;
146
+ if (!accountId) {
147
+ return failure("AWS account ID not available. Please ensure AWS credentials are properly configured.");
148
+ }
149
+ if (!aws) {
150
+ return failure("AwsProvider is required for deployment monitoring.");
151
+ }
152
+ let cfnMonitor = null;
153
+ let fallbackMonitoringTimeout;
154
+ try {
155
+ const targetStackName = this.resolveStackName(stackPattern, context) ??
156
+ (() => {
157
+ const deployType = context.deployType;
158
+ if (deployType === "organisation" ||
159
+ deployType === "platform" ||
160
+ deployType === "account") {
161
+ return getOrganisationStackName(deployType);
162
+ }
163
+ const appName = context.target || "app";
164
+ return `${appName}Network`;
165
+ })();
166
+ cfnMonitor = await this.eventMonitor.createEventMonitor("deploy", targetStackName, region, context, aws);
167
+ if (onOutput) {
168
+ onOutput(`Starting CloudFormation deployment of ${targetStackName}...\n`);
169
+ onOutput(`Monitoring CloudFormation events (CDK process running in background)...\n`);
170
+ }
171
+ const state = {
172
+ cdkOutput: "",
173
+ actualStackName: targetStackName,
174
+ stackDetected: false,
175
+ monitoringPromise: null
176
+ };
177
+ const enhancedOutputCallback = createEnhancedOutputCallback(state, onOutput, cfnMonitor, onResourceProgress);
178
+ // Fall back to predicted stack name if actual name not detected in time
179
+ fallbackMonitoringTimeout = setTimeout(() => {
180
+ if (!state.stackDetected && cfnMonitor && !state.monitoringPromise) {
181
+ logger.debug("CdkService", "Fallback monitoring STARTING", {
182
+ targetStackName,
183
+ stackDetected: state.stackDetected,
184
+ hasOnResourceProgress: !!onResourceProgress
185
+ });
186
+ state.monitoringPromise = startStackMonitoring(cfnMonitor, targetStackName, onResourceProgress);
187
+ }
188
+ }, STACK_DETECTION_FALLBACK_MS);
189
+ const result = await this.deploy(context.path, targetStackName, {
190
+ verbose: context.options?.verbose,
191
+ outputCallback: enhancedOutputCallback,
192
+ useCdkOut: true,
193
+ cdkOutputLogger: cfnMonitor?.getEventLogger() ?? undefined,
194
+ context: this.buildDeploymentCdkContext(context, accountId, region)
195
+ });
196
+ clearTimeout(fallbackMonitoringTimeout);
197
+ if (cfnMonitor) {
198
+ logger.debug("CdkService", "CDK process exited, stopping monitor", {
199
+ targetStackName,
200
+ stackDetected: state.stackDetected,
201
+ hadMonitoringPromise: !!state.monitoringPromise
202
+ });
203
+ cfnMonitor.stopMonitoring();
204
+ }
205
+ return analyseDeployOutput(state.cdkOutput, result, state.actualStackName);
206
+ }
207
+ catch (error) {
208
+ clearTimeout(fallbackMonitoringTimeout);
209
+ if (cfnMonitor) {
210
+ cfnMonitor.stopMonitoring();
211
+ }
212
+ const errorMsg = `CDK deploy failed: ${getErrorMessage(error)}`;
213
+ logger.error("CdkService", "CDK deployment exception", {
214
+ error: errorMsg
215
+ });
216
+ return failure(errorMsg);
217
+ }
218
+ }
219
+ async runCdkDestroy(context, stackPattern, onOutput, onResourceProgress, aws, useCdkOut) {
220
+ const accountId = context.callerIdentity?.Account;
221
+ const region = context.region || DEFAULT_REGION;
222
+ let cfnMonitor = null;
223
+ try {
224
+ const targetStackName = this.resolveStackName(stackPattern, context);
225
+ if (accountId && targetStackName && aws) {
226
+ cfnMonitor = await this.eventMonitor.createEventMonitor("destroy", targetStackName, region, context, aws);
227
+ cfnMonitor.startMonitoring(targetStackName, (event) => {
228
+ onResourceProgress?.(event);
229
+ }, (_success, _message) => {
230
+ // Stack destroy completed — no console output per architecture
231
+ });
232
+ }
233
+ const result = await this.destroy(context.path, stackPattern, {
234
+ verbose: context.options?.verbose,
235
+ outputCallback: onOutput,
236
+ useCdkOut,
237
+ cdkOutputLogger: cfnMonitor?.getEventLogger() ?? undefined,
238
+ context: this.buildDeploymentCdkContext(context, accountId, region, {
239
+ includeImageVersion: false
240
+ })
241
+ });
242
+ if (cfnMonitor) {
243
+ cfnMonitor.stopMonitoring();
244
+ }
245
+ return analyseDestroyResult(result);
246
+ }
247
+ catch (error) {
248
+ if (cfnMonitor) {
249
+ cfnMonitor.stopMonitoring();
250
+ }
251
+ return failure(`CDK destroy failed: ${getErrorMessage(error)}`);
252
+ }
253
+ }
254
+ }