@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.
- package/LICENSE +21 -0
- package/dist/src/aws/AwsProvider.d.ts +39 -0
- package/dist/src/aws/AwsProvider.js +1 -0
- package/dist/src/aws/SimpleAwsProvider.d.ts +22 -0
- package/dist/src/aws/SimpleAwsProvider.js +73 -0
- package/dist/src/aws/index.d.ts +4 -0
- package/dist/src/aws/index.js +3 -0
- package/dist/src/aws/organisations/accounts.d.ts +21 -0
- package/dist/src/aws/organisations/accounts.js +99 -0
- package/dist/src/aws/organisations/backup.d.ts +12 -0
- package/dist/src/aws/organisations/backup.js +28 -0
- package/dist/src/aws/organisations/costAllocation.d.ts +12 -0
- package/dist/src/aws/organisations/costAllocation.js +26 -0
- package/dist/src/aws/organisations/identityCentre.d.ts +8 -0
- package/dist/src/aws/organisations/identityCentre.js +19 -0
- package/dist/src/aws/organisations/index.d.ts +16 -0
- package/dist/src/aws/organisations/index.js +12 -0
- package/dist/src/aws/organisations/ipam.d.ts +7 -0
- package/dist/src/aws/organisations/ipam.js +18 -0
- package/dist/src/aws/organisations/organisation.d.ts +12 -0
- package/dist/src/aws/organisations/organisation.js +94 -0
- package/dist/src/aws/organisations/organisationalUnits.d.ts +19 -0
- package/dist/src/aws/organisations/organisationalUnits.js +125 -0
- package/dist/src/aws/organisations/policies.d.ts +7 -0
- package/dist/src/aws/organisations/policies.js +36 -0
- package/dist/src/aws/organisations/ram.d.ts +7 -0
- package/dist/src/aws/organisations/ram.js +15 -0
- package/dist/src/aws/organisations/serviceAccess.d.ts +7 -0
- package/dist/src/aws/organisations/serviceAccess.js +38 -0
- package/dist/src/aws/organisations/trustedAccess.d.ts +7 -0
- package/dist/src/aws/organisations/trustedAccess.js +15 -0
- package/dist/src/aws/organisations/types.d.ts +29 -0
- package/dist/src/aws/organisations/types.js +16 -0
- package/dist/src/aws/utils/CloudFormationFailureAnalyser.d.ts +32 -0
- package/dist/src/aws/utils/CloudFormationFailureAnalyser.js +228 -0
- package/dist/src/aws/utils/cloudformationEvents.d.ts +98 -0
- package/dist/src/aws/utils/cloudformationEvents.js +596 -0
- package/dist/src/aws/utils/errors.d.ts +26 -0
- package/dist/src/aws/utils/errors.js +59 -0
- package/dist/src/aws/utils/regions.d.ts +1 -0
- package/dist/src/aws/utils/regions.js +1 -0
- package/dist/src/aws/utils/stackStatus.d.ts +23 -0
- package/dist/src/aws/utils/stackStatus.js +90 -0
- package/dist/src/index.d.ts +35 -0
- package/dist/src/index.js +45 -0
- package/dist/src/orchestration/applicationDeploy.d.ts +11 -0
- package/dist/src/orchestration/applicationDeploy.js +327 -0
- package/dist/src/orchestration/contextHelpers.d.ts +9 -0
- package/dist/src/orchestration/contextHelpers.js +14 -0
- package/dist/src/orchestration/deploy.d.ts +10 -0
- package/dist/src/orchestration/deploy.js +42 -0
- package/dist/src/orchestration/detectionPipeline.d.ts +23 -0
- package/dist/src/orchestration/detectionPipeline.js +65 -0
- package/dist/src/orchestration/dockerInterface.d.ts +56 -0
- package/dist/src/orchestration/dockerInterface.js +1 -0
- package/dist/src/orchestration/domainInterface.d.ts +37 -0
- package/dist/src/orchestration/domainInterface.js +1 -0
- package/dist/src/orchestration/index.d.ts +8 -0
- package/dist/src/orchestration/index.js +3 -0
- package/dist/src/orchestration/organisationDeploy.d.ts +16 -0
- package/dist/src/orchestration/organisationDeploy.js +382 -0
- package/dist/src/orchestration/organisationSetup.d.ts +42 -0
- package/dist/src/orchestration/organisationSetup.js +227 -0
- package/dist/src/orchestration/resolveOperation.d.ts +10 -0
- package/dist/src/orchestration/resolveOperation.js +53 -0
- package/dist/src/orchestration/serviceFactory.d.ts +15 -0
- package/dist/src/orchestration/serviceFactory.js +16 -0
- package/dist/src/services/application/ApplicationStackService.d.ts +93 -0
- package/dist/src/services/application/ApplicationStackService.js +436 -0
- package/dist/src/services/application/index.d.ts +1 -0
- package/dist/src/services/application/index.js +1 -0
- package/dist/src/services/infrastructure/CdkArgumentBuilder.d.ts +12 -0
- package/dist/src/services/infrastructure/CdkArgumentBuilder.js +67 -0
- package/dist/src/services/infrastructure/CdkCommandRunner.d.ts +30 -0
- package/dist/src/services/infrastructure/CdkCommandRunner.js +241 -0
- package/dist/src/services/infrastructure/CdkErrorFormatter.d.ts +4 -0
- package/dist/src/services/infrastructure/CdkErrorFormatter.js +194 -0
- package/dist/src/services/infrastructure/CdkEventMonitoring.d.ts +19 -0
- package/dist/src/services/infrastructure/CdkEventMonitoring.js +41 -0
- package/dist/src/services/infrastructure/CdkOutputAnalyser.d.ts +43 -0
- package/dist/src/services/infrastructure/CdkOutputAnalyser.js +125 -0
- package/dist/src/services/infrastructure/CdkOutputParser.d.ts +8 -0
- package/dist/src/services/infrastructure/CdkOutputParser.js +33 -0
- package/dist/src/services/infrastructure/CdkProcessManager.d.ts +20 -0
- package/dist/src/services/infrastructure/CdkProcessManager.js +244 -0
- package/dist/src/services/infrastructure/CdkService.d.ts +71 -0
- package/dist/src/services/infrastructure/CdkService.js +254 -0
- package/dist/src/services/infrastructure/CloudFormationService.d.ts +79 -0
- package/dist/src/services/infrastructure/CloudFormationService.js +249 -0
- package/dist/src/services/infrastructure/index.d.ts +8 -0
- package/dist/src/services/infrastructure/index.js +7 -0
- package/dist/src/services/supporting/CdkContextBuilder.d.ts +49 -0
- package/dist/src/services/supporting/CdkContextBuilder.js +44 -0
- package/dist/src/services/supporting/TemplateHashService.d.ts +67 -0
- package/dist/src/services/supporting/TemplateHashService.js +152 -0
- package/dist/src/services/supporting/helpers.d.ts +46 -0
- package/dist/src/services/supporting/helpers.js +81 -0
- package/dist/src/services/supporting/index.d.ts +3 -0
- package/dist/src/services/supporting/index.js +3 -0
- package/dist/src/types/FjallState.d.ts +50 -0
- package/dist/src/types/FjallState.js +118 -0
- package/dist/src/types/ProgressEvent.d.ts +35 -0
- package/dist/src/types/ProgressEvent.js +48 -0
- package/dist/src/types/apiClient.d.ts +34 -0
- package/dist/src/types/apiClient.js +1 -0
- package/dist/src/types/application/ApplicationServiceTypes.d.ts +56 -0
- package/dist/src/types/application/ApplicationServiceTypes.js +30 -0
- package/dist/src/types/application/index.d.ts +1 -0
- package/dist/src/types/application/index.js +1 -0
- package/dist/src/types/callbacks.d.ts +36 -0
- package/dist/src/types/callbacks.js +1 -0
- package/dist/src/types/constants.d.ts +6 -0
- package/dist/src/types/constants.js +6 -0
- package/dist/src/types/credentials.d.ts +30 -0
- package/dist/src/types/credentials.js +1 -0
- package/dist/src/types/deployment/DeploymentServiceTypes.d.ts +23 -0
- package/dist/src/types/deployment/DeploymentServiceTypes.js +1 -0
- package/dist/src/types/deployment/DeploymentTypes.d.ts +29 -0
- package/dist/src/types/deployment/DeploymentTypes.js +1 -0
- package/dist/src/types/deployment/cloudformation.d.ts +14 -0
- package/dist/src/types/deployment/cloudformation.js +1 -0
- package/dist/src/types/deployment/index.d.ts +5 -0
- package/dist/src/types/deployment/index.js +1 -0
- package/dist/src/types/deployment/parallel.d.ts +46 -0
- package/dist/src/types/deployment/parallel.js +10 -0
- package/dist/src/types/errors/CdkError.d.ts +14 -0
- package/dist/src/types/errors/CdkError.js +20 -0
- package/dist/src/types/errors/ServiceError.d.ts +86 -0
- package/dist/src/types/errors/ServiceError.js +119 -0
- package/dist/src/types/events.d.ts +40 -0
- package/dist/src/types/events.js +5 -0
- package/dist/src/types/index.d.ts +20 -0
- package/dist/src/types/index.js +9 -0
- package/dist/src/types/operations.d.ts +193 -0
- package/dist/src/types/operations.js +285 -0
- package/dist/src/types/orgConfig.d.ts +28 -0
- package/dist/src/types/orgConfig.js +11 -0
- package/dist/src/types/params.d.ts +74 -0
- package/dist/src/types/params.js +1 -0
- package/dist/src/types/patternDetection.d.ts +43 -0
- package/dist/src/types/patternDetection.js +92 -0
- package/dist/src/types/validation.d.ts +12 -0
- package/dist/src/types/validation.js +1 -0
- package/dist/src/util/fsHelpers.d.ts +4 -0
- package/dist/src/util/fsHelpers.js +16 -0
- package/dist/src/util/index.d.ts +3 -0
- package/dist/src/util/index.js +3 -0
- package/dist/src/util/securityHelpers.d.ts +31 -0
- package/dist/src/util/securityHelpers.js +124 -0
- package/dist/src/util/singleton.d.ts +2 -0
- package/dist/src/util/singleton.js +9 -0
- package/dist/src/util/sleep.d.ts +4 -0
- package/dist/src/util/sleep.js +4 -0
- 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
|
+
}
|