@fjall/deploy-core 0.94.1 → 0.95.0
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/dist/.minified +1 -1
- package/dist/src/aws/organisations/accounts.js +1 -99
- package/dist/src/aws/organisations/backup.js +1 -30
- package/dist/src/aws/organisations/costAllocation.js +1 -28
- package/dist/src/aws/organisations/delegatedAdmin.js +3 -43
- package/dist/src/aws/organisations/identityCentre.js +1 -23
- package/dist/src/aws/organisations/ipam.js +1 -20
- package/dist/src/aws/organisations/organisation.js +1 -103
- package/dist/src/aws/organisations/organisationalUnits.js +1 -239
- package/dist/src/aws/organisations/policies.js +1 -37
- package/dist/src/aws/organisations/ram.js +1 -19
- package/dist/src/aws/organisations/serviceAccess.js +1 -44
- package/dist/src/aws/organisations/trustedAccess.js +1 -19
- package/dist/src/aws/utils/regions.js +1 -1
- package/dist/src/index.js +1 -65
- package/dist/src/orchestration/__tests__/cascadeTestHelpers.js +1 -78
- package/dist/src/orchestration/activeDeploymentGuard.js +5 -39
- package/dist/src/orchestration/applicationDeploy.js +1 -149
- package/dist/src/orchestration/applicationDeployHelpers.js +4 -223
- package/dist/src/orchestration/applicationDestroy.js +1 -131
- package/dist/src/orchestration/builders/dockerBuilder.js +1 -98
- package/dist/src/orchestration/builders/openNextBuilder.js +1 -144
- package/dist/src/orchestration/cascadeHelpers.js +1 -160
- package/dist/src/orchestration/contextHelpers.js +1 -107
- package/dist/src/orchestration/deploy.js +1 -42
- package/dist/src/orchestration/destroy.js +1 -67
- package/dist/src/orchestration/detectionPipeline.js +1 -84
- package/dist/src/orchestration/dockerBuildHelper.js +1 -49
- package/dist/src/orchestration/dockerInterface.js +0 -1
- package/dist/src/orchestration/domainInterface.js +0 -1
- package/dist/src/orchestration/openNextBuild.js +3 -243
- package/dist/src/orchestration/organisationDeploy.js +3 -284
- package/dist/src/orchestration/organisationDestroy.js +3 -189
- package/dist/src/orchestration/organisationSetup.js +1 -247
- package/dist/src/orchestration/resolveOperation.js +1 -123
- package/dist/src/orchestration/welcomeImageHelper.js +1 -64
- package/dist/src/services/application/ApplicationStackService.js +1 -218
- package/dist/src/services/application/applicationStackHelpers.js +4 -248
- package/dist/src/services/infrastructure/CdkCommandRunner.js +2 -244
- package/dist/src/services/infrastructure/CdkOutputAnalyser.js +1 -125
- package/dist/src/services/infrastructure/CdkProcessManager.js +3 -278
- package/dist/src/services/infrastructure/CdkService.js +3 -213
- package/dist/src/services/infrastructure/CloudFormationService.js +1 -248
- package/dist/src/services/infrastructure/ICdkProcessManager.js +0 -1
- package/dist/src/services/supporting/CdkContextBuilder.js +1 -44
- package/dist/src/services/supporting/TemplateHashService.js +1 -152
- package/dist/src/steps/stepRegistry.js +1 -505
- package/dist/src/types/apiClient.js +0 -1
- package/dist/src/types/detection.js +0 -1
- package/dist/src/types/frameworkBuilder.js +0 -8
- package/dist/src/types/params.js +0 -1
- package/dist/src/types/patternDetection.js +1 -88
- package/dist/src/types/stepDefinitions.js +1 -98
- package/package.json +4 -4
|
@@ -1,244 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import { success, failure } from "@fjall/generator";
|
|
4
|
-
import { CdkError } from "../../types/errors/CdkError.js";
|
|
5
|
-
import { DEFAULT_REGION } from "../../aws/utils/regions.js";
|
|
6
|
-
import { isCdkError, formatInfrastructureError } from "./CdkErrorFormatter.js";
|
|
7
|
-
import { hasCdkDifferences, parseDiffOutput } from "./CdkOutputParser.js";
|
|
8
|
-
import { DEFAULT_DEPLOY_TIMEOUT_MS } from "./CdkEventMonitoring.js";
|
|
9
|
-
import { analyseBootstrapError } from "./CdkOutputAnalyser.js";
|
|
10
|
-
/** Synth is CPU-bound template generation — 5 minutes is generous */
|
|
11
|
-
export const SYNTH_TIMEOUT_MS = 300_000;
|
|
12
|
-
/** Bootstrap creates a single stack with a handful of resources */
|
|
13
|
-
export const BOOTSTRAP_TIMEOUT_MS = 180_000;
|
|
14
|
-
/** Import maps existing resources into CDK state — slower than deploy due to resource lookup */
|
|
15
|
-
export const IMPORT_TIMEOUT_MS = 1_200_000;
|
|
16
|
-
/** CDK flags used across multiple commands */
|
|
17
|
-
const CDK_FLAGS = Object.freeze({
|
|
18
|
-
APP: "--app",
|
|
19
|
-
CI: "--ci",
|
|
20
|
-
REQUIRE_APPROVAL: "--require-approval",
|
|
21
|
-
APPROVAL_NEVER: "never",
|
|
22
|
-
VERBOSE: "--verbose",
|
|
23
|
-
NO_LOOKUPS: "--no-lookups",
|
|
24
|
-
ALL: "--all"
|
|
25
|
-
});
|
|
26
|
-
/**
|
|
27
|
-
* Runs low-level CDK CLI commands (synth, deploy, destroy, diff, bootstrap, import).
|
|
28
|
-
* Delegates process management to CdkProcessManager (or any ICdkProcessManager).
|
|
29
|
-
*/
|
|
30
|
-
export class CdkCommandRunner {
|
|
31
|
-
processManager;
|
|
32
|
-
constructor(processManager) {
|
|
33
|
-
this.processManager = processManager;
|
|
34
|
-
}
|
|
35
|
-
dispose() {
|
|
36
|
-
this.processManager.dispose();
|
|
37
|
-
}
|
|
38
|
-
async checkDifferences(path, stackName, options) {
|
|
39
|
-
const args = ["diff"];
|
|
40
|
-
if (stackName) {
|
|
41
|
-
args.push(stackName);
|
|
42
|
-
}
|
|
43
|
-
else {
|
|
44
|
-
args.push(CDK_FLAGS.ALL);
|
|
45
|
-
}
|
|
46
|
-
// Add no-colour flag for cleaner parsing
|
|
47
|
-
args.push("--no-color");
|
|
48
|
-
if (options?.noLookups) {
|
|
49
|
-
args.push(CDK_FLAGS.NO_LOOKUPS);
|
|
50
|
-
}
|
|
51
|
-
const result = await this.processManager.runCdkCommand(path, args, {
|
|
52
|
-
...options,
|
|
53
|
-
// Diff command returns non-zero if differences exist
|
|
54
|
-
ignoreExitCode: true,
|
|
55
|
-
// Diff output includes stderr (CDK writes errors there)
|
|
56
|
-
combineOutput: true
|
|
57
|
-
});
|
|
58
|
-
if (!result.success) {
|
|
59
|
-
return failure(new CdkError(formatInfrastructureError(result.error, path), "diff_failed", undefined, undefined, result.error, undefined, false));
|
|
60
|
-
}
|
|
61
|
-
const output = result.data.output || "";
|
|
62
|
-
// Check if the output indicates an error rather than a successful diff
|
|
63
|
-
if (isCdkError(output)) {
|
|
64
|
-
return failure(new CdkError(formatInfrastructureError(output, path), "diff_failed", undefined, undefined, output, undefined, false));
|
|
65
|
-
}
|
|
66
|
-
const hasDiffs = hasCdkDifferences(output);
|
|
67
|
-
const details = parseDiffOutput(output);
|
|
68
|
-
return success({
|
|
69
|
-
hasDifferences: hasDiffs,
|
|
70
|
-
output,
|
|
71
|
-
details
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
async deploy(path, stackName, options) {
|
|
75
|
-
const args = ["deploy"];
|
|
76
|
-
if (options?.appDir) {
|
|
77
|
-
args.push(CDK_FLAGS.APP, options.appDir);
|
|
78
|
-
}
|
|
79
|
-
else if (options?.useCdkOut) {
|
|
80
|
-
args.push(CDK_FLAGS.APP, "cdk.out");
|
|
81
|
-
}
|
|
82
|
-
if (stackName) {
|
|
83
|
-
args.push(stackName);
|
|
84
|
-
}
|
|
85
|
-
else {
|
|
86
|
-
args.push(CDK_FLAGS.ALL);
|
|
87
|
-
}
|
|
88
|
-
// Never require approval in programmatic mode
|
|
89
|
-
args.push(CDK_FLAGS.REQUIRE_APPROVAL, CDK_FLAGS.APPROVAL_NEVER);
|
|
90
|
-
args.push(CDK_FLAGS.CI);
|
|
91
|
-
// Disable version reporting and metadata for cleaner output
|
|
92
|
-
args.push("--no-version-reporting");
|
|
93
|
-
args.push("--no-path-metadata");
|
|
94
|
-
args.push("--no-asset-metadata");
|
|
95
|
-
if (options?.verbose) {
|
|
96
|
-
args.push(CDK_FLAGS.VERBOSE);
|
|
97
|
-
}
|
|
98
|
-
else {
|
|
99
|
-
args.push("--progress", "events");
|
|
100
|
-
}
|
|
101
|
-
const deployOptions = {
|
|
102
|
-
...options,
|
|
103
|
-
timeout: options?.timeout || DEFAULT_DEPLOY_TIMEOUT_MS
|
|
104
|
-
};
|
|
105
|
-
if (options?.passThroughCDK) {
|
|
106
|
-
return this.processManager.runCdkCommandPassthrough(path, args, deployOptions);
|
|
107
|
-
}
|
|
108
|
-
return this.processManager.runCdkCommand(path, args, deployOptions);
|
|
109
|
-
}
|
|
110
|
-
async destroy(path, stackName, options) {
|
|
111
|
-
const args = ["destroy"];
|
|
112
|
-
if (options?.appDir) {
|
|
113
|
-
args.push(CDK_FLAGS.APP, options.appDir);
|
|
114
|
-
}
|
|
115
|
-
else if (options?.useCdkOut) {
|
|
116
|
-
args.push(CDK_FLAGS.APP, "cdk.out");
|
|
117
|
-
}
|
|
118
|
-
if (stackName) {
|
|
119
|
-
args.push(stackName);
|
|
120
|
-
}
|
|
121
|
-
else {
|
|
122
|
-
args.push(CDK_FLAGS.ALL);
|
|
123
|
-
}
|
|
124
|
-
args.push("--force");
|
|
125
|
-
if (options?.verbose) {
|
|
126
|
-
args.push(CDK_FLAGS.VERBOSE);
|
|
127
|
-
}
|
|
128
|
-
const destroyOptions = {
|
|
129
|
-
...options,
|
|
130
|
-
timeout: options?.timeout || DEFAULT_DEPLOY_TIMEOUT_MS
|
|
131
|
-
};
|
|
132
|
-
return this.processManager.runCdkCommand(path, args, destroyOptions);
|
|
133
|
-
}
|
|
134
|
-
/**
|
|
135
|
-
* Import existing AWS resources into CDK stack
|
|
136
|
-
*
|
|
137
|
-
* NOTE: This method is fully functional but currently unreachable as import
|
|
138
|
-
* functionality is temporarily disabled at the AST layer (astCodeModification.ts).
|
|
139
|
-
* When import is re-enabled, this method will work without changes.
|
|
140
|
-
*/
|
|
141
|
-
async runImport(path, resourceMappingFile, options) {
|
|
142
|
-
const args = ["import"];
|
|
143
|
-
if (options?.stacks && options.stacks.length > 0) {
|
|
144
|
-
args.push(...options.stacks);
|
|
145
|
-
}
|
|
146
|
-
if (resourceMappingFile) {
|
|
147
|
-
args.push("--resource-mapping", resourceMappingFile);
|
|
148
|
-
}
|
|
149
|
-
args.push(CDK_FLAGS.REQUIRE_APPROVAL, CDK_FLAGS.APPROVAL_NEVER);
|
|
150
|
-
args.push(CDK_FLAGS.CI);
|
|
151
|
-
if (options?.noLookups) {
|
|
152
|
-
args.push(CDK_FLAGS.NO_LOOKUPS);
|
|
153
|
-
}
|
|
154
|
-
if (options?.verbose) {
|
|
155
|
-
args.push(CDK_FLAGS.VERBOSE);
|
|
156
|
-
args.push("--trace");
|
|
157
|
-
args.push("--debug");
|
|
158
|
-
}
|
|
159
|
-
const importOptions = {
|
|
160
|
-
...options,
|
|
161
|
-
timeout: options?.timeout || IMPORT_TIMEOUT_MS
|
|
162
|
-
};
|
|
163
|
-
if (options?.passThroughCDK) {
|
|
164
|
-
return this.processManager.runCdkCommandPassthrough(path, args, importOptions);
|
|
165
|
-
}
|
|
166
|
-
return this.processManager.runCdkCommand(path, args, importOptions);
|
|
167
|
-
}
|
|
168
|
-
async synth(path, options) {
|
|
169
|
-
options?.outputCallback?.("Synthesising CloudFormation template...\n");
|
|
170
|
-
const lookupArgs = options?.noLookups
|
|
171
|
-
? [CDK_FLAGS.NO_LOOKUPS]
|
|
172
|
-
: [];
|
|
173
|
-
const outputArgs = options?.outputDir
|
|
174
|
-
? ["--output", options.outputDir]
|
|
175
|
-
: [];
|
|
176
|
-
const region = options?.context?.region || DEFAULT_REGION;
|
|
177
|
-
const result = await this.processManager.runCdkCommand(path, ["synth", ...lookupArgs, ...outputArgs, CDK_FLAGS.CI, "--quiet"], {
|
|
178
|
-
...options,
|
|
179
|
-
context: { ...options?.context, region },
|
|
180
|
-
timeout: options?.timeout || SYNTH_TIMEOUT_MS
|
|
181
|
-
});
|
|
182
|
-
if (!result.success) {
|
|
183
|
-
const formatted = result.error
|
|
184
|
-
? formatInfrastructureError(result.error, path)
|
|
185
|
-
: "Failed to synthesise CloudFormation template";
|
|
186
|
-
return failure(formatted);
|
|
187
|
-
}
|
|
188
|
-
return result;
|
|
189
|
-
}
|
|
190
|
-
async bootstrap(accountId, region, options) {
|
|
191
|
-
const bootstrapPath = tmpdir();
|
|
192
|
-
logger.debug("CdkService", "Starting CDK bootstrap", {
|
|
193
|
-
accountId,
|
|
194
|
-
region,
|
|
195
|
-
bootstrapPath,
|
|
196
|
-
target: `aws://${accountId}/${region}`
|
|
197
|
-
});
|
|
198
|
-
const result = await this.processManager.runCdkCommand(bootstrapPath, [
|
|
199
|
-
"bootstrap",
|
|
200
|
-
`aws://${accountId}/${region}`,
|
|
201
|
-
"--cloudformation-execution-policies",
|
|
202
|
-
"arn:aws:iam::aws:policy/AdministratorAccess",
|
|
203
|
-
"-c",
|
|
204
|
-
`accountId=${accountId}`,
|
|
205
|
-
CDK_FLAGS.REQUIRE_APPROVAL,
|
|
206
|
-
CDK_FLAGS.APPROVAL_NEVER,
|
|
207
|
-
CDK_FLAGS.CI,
|
|
208
|
-
"--quiet",
|
|
209
|
-
"--force"
|
|
210
|
-
], {
|
|
211
|
-
...options,
|
|
212
|
-
timeout: options?.timeout || BOOTSTRAP_TIMEOUT_MS,
|
|
213
|
-
context: { region, accountId },
|
|
214
|
-
credentials: options?.credentials,
|
|
215
|
-
skipProjectCheck: true,
|
|
216
|
-
extraEnv: {
|
|
217
|
-
TERM: "dumb",
|
|
218
|
-
CDK_DISABLE_NOTICES: "true",
|
|
219
|
-
CDK_DISABLE_PROGRESS_BAR: "true"
|
|
220
|
-
}
|
|
221
|
-
});
|
|
222
|
-
logger.debug("CdkService", result.success
|
|
223
|
-
? "Bootstrap completed successfully"
|
|
224
|
-
: "Bootstrap exited with non-zero code", {
|
|
225
|
-
accountId,
|
|
226
|
-
region,
|
|
227
|
-
exitCode: result.success ? result.data.exitCode : undefined,
|
|
228
|
-
output: result.success
|
|
229
|
-
? result.data.output?.trim() || "(empty)"
|
|
230
|
-
: "(empty)",
|
|
231
|
-
error: result.success ? "(empty)" : result.error.trim() || "(empty)"
|
|
232
|
-
});
|
|
233
|
-
if (!result.success) {
|
|
234
|
-
if (result.error.includes("already bootstrapped")) {
|
|
235
|
-
return success({
|
|
236
|
-
output: "Environment is already bootstrapped",
|
|
237
|
-
exitCode: 0
|
|
238
|
-
});
|
|
239
|
-
}
|
|
240
|
-
return failure(analyseBootstrapError(result.error));
|
|
241
|
-
}
|
|
242
|
-
return success({ output: "AWS environment bootstrapped", exitCode: 0 });
|
|
243
|
-
}
|
|
244
|
-
}
|
|
1
|
+
import{tmpdir as C}from"os";import{logger as d}from"@fjall/util/logger";import{success as p,failure as n}from"@fjall/generator";import{CdkError as m}from"../../types/errors/CdkError.js";import{DEFAULT_REGION as l}from"../../aws/utils/regions.js";import{isCdkError as E,formatInfrastructureError as c}from"./CdkErrorFormatter.js";import{hasCdkDifferences as O,parseDiffOutput as P}from"./CdkOutputParser.js";import{DEFAULT_DEPLOY_TIMEOUT_MS as f}from"./CdkEventMonitoring.js";import{analyseBootstrapError as A}from"./CdkOutputAnalyser.js";const _=3e5,R=18e4,g=12e5,u=Object.freeze({APP:"--app",CI:"--ci",REQUIRE_APPROVAL:"--require-approval",APPROVAL_NEVER:"never",VERBOSE:"--verbose",NO_LOOKUPS:"--no-lookups",ALL:"--all"});class V{processManager;constructor(o){this.processManager=o}dispose(){this.processManager.dispose()}async checkDifferences(o,s,r){const e=["diff"];s?e.push(s):e.push(u.ALL),e.push("--no-color"),r?.noLookups&&e.push(u.NO_LOOKUPS);const t=await this.processManager.runCdkCommand(o,e,{...r,ignoreExitCode:!0,combineOutput:!0});if(!t.success)return n(new m(c(t.error,o),"diff_failed",void 0,void 0,t.error,void 0,!1));const a=t.data.output||"";if(E(a))return n(new m(c(a,o),"diff_failed",void 0,void 0,a,void 0,!1));const i=O(a),h=P(a);return p({hasDifferences:i,output:a,details:h})}async deploy(o,s,r){const e=["deploy"];r?.appDir?e.push(u.APP,r.appDir):r?.useCdkOut&&e.push(u.APP,"cdk.out"),s?e.push(s):e.push(u.ALL),e.push(u.REQUIRE_APPROVAL,u.APPROVAL_NEVER),e.push(u.CI),e.push("--no-version-reporting"),e.push("--no-path-metadata"),e.push("--no-asset-metadata"),r?.verbose?e.push(u.VERBOSE):e.push("--progress","events");const t={...r,timeout:r?.timeout||f};return r?.passThroughCDK?this.processManager.runCdkCommandPassthrough(o,e,t):this.processManager.runCdkCommand(o,e,t)}async destroy(o,s,r){const e=["destroy"];r?.appDir?e.push(u.APP,r.appDir):r?.useCdkOut&&e.push(u.APP,"cdk.out"),s?e.push(s):e.push(u.ALL),e.push("--force"),r?.verbose&&e.push(u.VERBOSE);const t={...r,timeout:r?.timeout||f};return this.processManager.runCdkCommand(o,e,t)}async runImport(o,s,r){const e=["import"];r?.stacks&&r.stacks.length>0&&e.push(...r.stacks),s&&e.push("--resource-mapping",s),e.push(u.REQUIRE_APPROVAL,u.APPROVAL_NEVER),e.push(u.CI),r?.noLookups&&e.push(u.NO_LOOKUPS),r?.verbose&&(e.push(u.VERBOSE),e.push("--trace"),e.push("--debug"));const t={...r,timeout:r?.timeout||g};return r?.passThroughCDK?this.processManager.runCdkCommandPassthrough(o,e,t):this.processManager.runCdkCommand(o,e,t)}async synth(o,s){s?.outputCallback?.(`Synthesising CloudFormation template...
|
|
2
|
+
`);const r=s?.noLookups?[u.NO_LOOKUPS]:[],e=s?.outputDir?["--output",s.outputDir]:[],t=s?.context?.region||l,a=await this.processManager.runCdkCommand(o,["synth",...r,...e,u.CI,"--quiet"],{...s,context:{...s?.context,region:t},timeout:s?.timeout||_});if(!a.success){const i=a.error?c(a.error,o):"Failed to synthesise CloudFormation template";return n(i)}return a}async bootstrap(o,s,r){const e=C();d.debug("CdkService","Starting CDK bootstrap",{accountId:o,region:s,bootstrapPath:e,target:`aws://${o}/${s}`});const t=await this.processManager.runCdkCommand(e,["bootstrap",`aws://${o}/${s}`,"--cloudformation-execution-policies","arn:aws:iam::aws:policy/AdministratorAccess","-c",`accountId=${o}`,u.REQUIRE_APPROVAL,u.APPROVAL_NEVER,u.CI,"--quiet","--force"],{...r,timeout:r?.timeout||R,context:{region:s,accountId:o},credentials:r?.credentials,skipProjectCheck:!0,extraEnv:{TERM:"dumb",CDK_DISABLE_NOTICES:"true",CDK_DISABLE_PROGRESS_BAR:"true"}});return d.debug("CdkService",t.success?"Bootstrap completed successfully":"Bootstrap exited with non-zero code",{accountId:o,region:s,exitCode:t.success?t.data.exitCode:void 0,output:t.success&&t.data.output?.trim()||"(empty)",error:t.success?"(empty)":t.error.trim()||"(empty)"}),t.success?p({output:"AWS environment bootstrapped",exitCode:0}):t.error.includes("already bootstrapped")?p({output:"Environment is already bootstrapped",exitCode:0}):n(A(t.error))}}export{R as BOOTSTRAP_TIMEOUT_MS,V as CdkCommandRunner,g as IMPORT_TIMEOUT_MS,_ as SYNTH_TIMEOUT_MS};
|
|
@@ -1,125 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { STACK_NOT_FOUND_PATTERN, CDK_NO_STACKS_MATCH } from "../../aws/utils/cloudformationEvents.js";
|
|
3
|
-
import { success, failure } from "@fjall/generator";
|
|
4
|
-
import { formatInfrastructureError } from "./CdkErrorFormatter.js";
|
|
5
|
-
import { startStackMonitoring } from "./CdkEventMonitoring.js";
|
|
6
|
-
/**
|
|
7
|
-
* Analyse CDK deploy output and classify the result as success/skip/failure.
|
|
8
|
-
*/
|
|
9
|
-
export function analyseDeployOutput(cdkOutput, result, stackName) {
|
|
10
|
-
const stackSpecificNoResourcesPattern = new RegExp(`${stackName}[:\\s|]*.*?(?:stack has no resources|skipping deployment)`, "i");
|
|
11
|
-
const hasEmptyStack = stackSpecificNoResourcesPattern.test(cdkOutput);
|
|
12
|
-
const hasNoChanges = cdkOutput.includes("no changes") ||
|
|
13
|
-
cdkOutput.includes("Stack is up to date") ||
|
|
14
|
-
cdkOutput.includes("No changes to deploy");
|
|
15
|
-
const noStacksMatch = cdkOutput.includes(CDK_NO_STACKS_MATCH) ||
|
|
16
|
-
(!result.success && result.error.includes(CDK_NO_STACKS_MATCH));
|
|
17
|
-
if (result.success) {
|
|
18
|
-
return success({
|
|
19
|
-
message: hasEmptyStack
|
|
20
|
-
? "Stack has no resources, skipped"
|
|
21
|
-
: hasNoChanges
|
|
22
|
-
? "Infrastructure already up to date"
|
|
23
|
-
: "Deployment successful",
|
|
24
|
-
status: hasNoChanges ? "NO_CHANGES" : undefined,
|
|
25
|
-
details: { output: cdkOutput, skipped: hasEmptyStack }
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
if (noStacksMatch) {
|
|
29
|
-
return success({
|
|
30
|
-
message: "Stack not defined, skipped",
|
|
31
|
-
status: "SKIPPED",
|
|
32
|
-
details: { output: cdkOutput, skipped: true }
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
let errorMessage = result.error || "Deployment failed";
|
|
36
|
-
if (cdkOutput.includes("failed:")) {
|
|
37
|
-
const errorMatch = cdkOutput.match(/failed: .*?: (.*?)(?:\n|$)/);
|
|
38
|
-
if (errorMatch) {
|
|
39
|
-
errorMessage = errorMatch[1];
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
logger.error("CdkService", "CDK deployment returned failure", {
|
|
43
|
-
errorMessage
|
|
44
|
-
});
|
|
45
|
-
return failure(errorMessage);
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* Classify a bootstrap error into a user-friendly message.
|
|
49
|
-
*/
|
|
50
|
-
export function analyseBootstrapError(errorInfo) {
|
|
51
|
-
if (errorInfo.includes("Access Denied") ||
|
|
52
|
-
errorInfo.includes("AccessDenied")) {
|
|
53
|
-
return "Access denied. Check your AWS credentials and permissions";
|
|
54
|
-
}
|
|
55
|
-
if (errorInfo.includes("ExpiredToken")) {
|
|
56
|
-
return "AWS credentials have expired. Please refresh your credentials";
|
|
57
|
-
}
|
|
58
|
-
if (errorInfo.includes("ENOTFOUND") || errorInfo.includes("ECONNREFUSED")) {
|
|
59
|
-
return "Network error. Please check your internet connection and try again";
|
|
60
|
-
}
|
|
61
|
-
if (errorInfo.includes("timeout")) {
|
|
62
|
-
return "Bootstrap operation timed out";
|
|
63
|
-
}
|
|
64
|
-
return formatInfrastructureError(errorInfo);
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* Analyse CDK destroy result and classify it.
|
|
68
|
-
*/
|
|
69
|
-
export function analyseDestroyResult(result) {
|
|
70
|
-
if (!result.success) {
|
|
71
|
-
const stackDoesNotExist = result.error.includes(STACK_NOT_FOUND_PATTERN) ||
|
|
72
|
-
result.error.includes(CDK_NO_STACKS_MATCH);
|
|
73
|
-
if (stackDoesNotExist) {
|
|
74
|
-
return success({
|
|
75
|
-
message: "Stack already deleted or does not exist",
|
|
76
|
-
details: { skipped: true }
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
return failure(result.error || "Destroy failed");
|
|
80
|
-
}
|
|
81
|
-
return success({ message: "Stack destroyed successfully" });
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Try to detect the actual CloudFormation stack name from a CDK output chunk.
|
|
85
|
-
* CDK outputs the stack name in several formats during deployment.
|
|
86
|
-
*/
|
|
87
|
-
export function detectStackNameFromChunk(chunk) {
|
|
88
|
-
// Pattern 1: "Deploying stack <name>"
|
|
89
|
-
const deployMatch = chunk.match(/Deploying stack\s+([^\s]+)/);
|
|
90
|
-
if (deployMatch) {
|
|
91
|
-
return { detected: true, stackName: deployMatch[1] };
|
|
92
|
-
}
|
|
93
|
-
// Pattern 2: "<stack-name> | <number>/<number> |"
|
|
94
|
-
const progressMatch = chunk.match(/^([^\s|]+)\s*\|\s*\d+\/\d+/m);
|
|
95
|
-
if (progressMatch) {
|
|
96
|
-
return { detected: true, stackName: progressMatch[1] };
|
|
97
|
-
}
|
|
98
|
-
// Pattern 3: "Stack ARN:" line contains the stack name
|
|
99
|
-
const arnMatch = chunk.match(/arn:aws:cloudformation:[^:]+:[^:]+:stack\/([^/]+)\//);
|
|
100
|
-
if (arnMatch) {
|
|
101
|
-
return { detected: true, stackName: arnMatch[1] };
|
|
102
|
-
}
|
|
103
|
-
return { detected: false };
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Create an enhanced output callback that detects the actual stack name from CDK output
|
|
107
|
-
* and starts CloudFormation monitoring once detected.
|
|
108
|
-
*/
|
|
109
|
-
export function createEnhancedOutputCallback(state, originalOutputCallback, cfnMonitor, onResourceProgress) {
|
|
110
|
-
return (chunk) => {
|
|
111
|
-
state.cdkOutput += chunk;
|
|
112
|
-
originalOutputCallback?.(chunk);
|
|
113
|
-
if (!state.stackDetected) {
|
|
114
|
-
const detection = detectStackNameFromChunk(chunk);
|
|
115
|
-
if (detection.detected && detection.stackName) {
|
|
116
|
-
state.actualStackName = detection.stackName;
|
|
117
|
-
state.stackDetected = true;
|
|
118
|
-
}
|
|
119
|
-
// Start monitoring once we detect the actual stack name
|
|
120
|
-
if (state.stackDetected && cfnMonitor && !state.monitoringPromise) {
|
|
121
|
-
state.monitoringPromise = startStackMonitoring(cfnMonitor, state.actualStackName, onResourceProgress);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
};
|
|
125
|
-
}
|
|
1
|
+
import{logger as m}from"@fjall/util/logger";import{STACK_NOT_FOUND_PATTERN as p,CDK_NO_STACKS_MATCH as i}from"../../aws/utils/cloudformationEvents.js";import{success as n,failure as u}from"@fjall/generator";import{formatInfrastructureError as f}from"./CdkErrorFormatter.js";import{startStackMonitoring as k}from"./CdkEventMonitoring.js";function E(e,t,s){const r=new RegExp(`${s}[:\\s|]*.*?(?:stack has no resources|skipping deployment)`,"i").test(e),c=e.includes("no changes")||e.includes("Stack is up to date")||e.includes("No changes to deploy"),l=e.includes(i)||!t.success&&t.error.includes(i);if(t.success)return n({message:r?"Stack has no resources, skipped":c?"Infrastructure already up to date":"Deployment successful",status:c?"NO_CHANGES":void 0,details:{output:e,skipped:r}});if(l)return n({message:"Stack not defined, skipped",status:"SKIPPED",details:{output:e,skipped:!0}});let o=t.error||"Deployment failed";if(e.includes("failed:")){const d=e.match(/failed: .*?: (.*?)(?:\n|$)/);d&&(o=d[1])}return m.error("CdkService","CDK deployment returned failure",{errorMessage:o}),u(o)}function C(e){return e.includes("Access Denied")||e.includes("AccessDenied")?"Access denied. Check your AWS credentials and permissions":e.includes("ExpiredToken")?"AWS credentials have expired. Please refresh your credentials":e.includes("ENOTFOUND")||e.includes("ECONNREFUSED")?"Network error. Please check your internet connection and try again":e.includes("timeout")?"Bootstrap operation timed out":f(e)}function x(e){return e.success?n({message:"Stack destroyed successfully"}):e.error.includes(p)||e.error.includes(i)?n({message:"Stack already deleted or does not exist",details:{skipped:!0}}):u(e.error||"Destroy failed")}function N(e){const t=e.match(/Deploying stack\s+([^\s]+)/);if(t)return{detected:!0,stackName:t[1]};const s=e.match(/^([^\s|]+)\s*\|\s*\d+\/\d+/m);if(s)return{detected:!0,stackName:s[1]};const a=e.match(/arn:aws:cloudformation:[^:]+:[^:]+:stack\/([^/]+)\//);return a?{detected:!0,stackName:a[1]}:{detected:!1}}function A(e,t,s,a){return r=>{if(e.cdkOutput+=r,t?.(r),!e.stackDetected){const c=N(r);c.detected&&c.stackName&&(e.actualStackName=c.stackName,e.stackDetected=!0),e.stackDetected&&s&&!e.monitoringPromise&&(e.monitoringPromise=k(s,e.actualStackName,a))}}}export{C as analyseBootstrapError,E as analyseDeployOutput,x as analyseDestroyResult,A as createEnhancedOutputCallback,N as detectStackNameFromChunk};
|
|
@@ -1,278 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import { createRequire } from "module";
|
|
5
|
-
import { logger } from "@fjall/util/logger";
|
|
6
|
-
import { filterDangerousEnvVars, maskSensitiveOutput } 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 (err) {
|
|
21
|
-
logger.debug("CdkService", "Failed to resolve aws-cdk binary, falling back to npx", { error: err instanceof Error ? err.message : String(err) });
|
|
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
|
-
exitHandler;
|
|
33
|
-
sigintHandler;
|
|
34
|
-
sigtermHandler;
|
|
35
|
-
constructor(argBuilder) {
|
|
36
|
-
this.argBuilder = argBuilder;
|
|
37
|
-
// Register cleanup handlers (stored for deregistration)
|
|
38
|
-
this.exitHandler = () => this.cleanup();
|
|
39
|
-
this.sigintHandler = () => {
|
|
40
|
-
this.cleanup();
|
|
41
|
-
process.exit(130);
|
|
42
|
-
};
|
|
43
|
-
this.sigtermHandler = () => {
|
|
44
|
-
this.cleanup();
|
|
45
|
-
process.exit(143);
|
|
46
|
-
};
|
|
47
|
-
process.on("exit", this.exitHandler);
|
|
48
|
-
process.on("SIGINT", this.sigintHandler);
|
|
49
|
-
process.on("SIGTERM", this.sigtermHandler);
|
|
50
|
-
}
|
|
51
|
-
forceKillProcess(child) {
|
|
52
|
-
child.stdout?.destroy();
|
|
53
|
-
child.stderr?.destroy();
|
|
54
|
-
child.kill("SIGTERM");
|
|
55
|
-
const killTimer = setTimeout(() => {
|
|
56
|
-
if (child.exitCode === null) {
|
|
57
|
-
child.kill("SIGKILL");
|
|
58
|
-
}
|
|
59
|
-
}, SIGKILL_GRACE_PERIOD_MS);
|
|
60
|
-
killTimer.unref();
|
|
61
|
-
child.once("exit", () => clearTimeout(killTimer));
|
|
62
|
-
}
|
|
63
|
-
cleanup() {
|
|
64
|
-
for (const [_name, child] of this.runningProcesses) {
|
|
65
|
-
if (!child.killed) {
|
|
66
|
-
child.stdout?.destroy();
|
|
67
|
-
child.stderr?.destroy();
|
|
68
|
-
child.kill("SIGTERM");
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
this.runningProcesses.clear();
|
|
72
|
-
}
|
|
73
|
-
dispose() {
|
|
74
|
-
this.cleanup();
|
|
75
|
-
process.removeListener("exit", this.exitHandler);
|
|
76
|
-
process.removeListener("SIGINT", this.sigintHandler);
|
|
77
|
-
process.removeListener("SIGTERM", this.sigtermHandler);
|
|
78
|
-
}
|
|
79
|
-
async runCdkCommandPassthrough(workingDir, args, options) {
|
|
80
|
-
return new Promise((resolve) => {
|
|
81
|
-
if (!existsSync(workingDir)) {
|
|
82
|
-
resolve(failure(`Directory not found: ${workingDir}`));
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
// Check for cdk.json in the working directory
|
|
86
|
-
const cdkJsonPath = join(workingDir, "cdk.json");
|
|
87
|
-
if (!existsSync(cdkJsonPath)) {
|
|
88
|
-
resolve(failure(`No CDK project found in ${workingDir}`));
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
const contextArgs = this.argBuilder.buildContextArgs(options?.context);
|
|
92
|
-
const fullArgs = [...cdkBin.prefixArgs, ...args, ...contextArgs];
|
|
93
|
-
const env = this.argBuilder.buildCdkEnv(options);
|
|
94
|
-
// Run CDK with stdio: 'inherit' for direct terminal output
|
|
95
|
-
const child = spawn(cdkBin.command, fullArgs, {
|
|
96
|
-
cwd: workingDir,
|
|
97
|
-
env,
|
|
98
|
-
stdio: "inherit", // Pass through to terminal directly
|
|
99
|
-
shell: false,
|
|
100
|
-
detached: false
|
|
101
|
-
});
|
|
102
|
-
if (!child.pid) {
|
|
103
|
-
child.on("error", (err) => {
|
|
104
|
-
logger.debug("CdkProcess", "Spawn error on failed child process", {
|
|
105
|
-
error: err.message
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
resolve(failure(`Failed to spawn CDK process - no PID. cwd=${workingDir}, args=${fullArgs.join(" ")}`));
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
// Track process for cleanup
|
|
112
|
-
const processId = `cdk-passthrough-${++this.processCounter}`;
|
|
113
|
-
this.runningProcesses.set(processId, child);
|
|
114
|
-
let processKilled = false;
|
|
115
|
-
let settled = false;
|
|
116
|
-
// Timeout to prevent indefinite hangs (default 30 minutes, matching deploy)
|
|
117
|
-
const timeoutMs = options?.timeout ?? 30 * 60 * 1000;
|
|
118
|
-
const timeoutHandle = setTimeout(() => {
|
|
119
|
-
if (!child.killed) {
|
|
120
|
-
processKilled = true;
|
|
121
|
-
this.forceKillProcess(child);
|
|
122
|
-
}
|
|
123
|
-
}, timeoutMs);
|
|
124
|
-
child.on("close", (code) => {
|
|
125
|
-
clearTimeout(timeoutHandle);
|
|
126
|
-
this.runningProcesses.delete(processId);
|
|
127
|
-
if (settled)
|
|
128
|
-
return;
|
|
129
|
-
settled = true;
|
|
130
|
-
if (processKilled) {
|
|
131
|
-
resolve(failure("CDK command timed out"));
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
if (code === 0 || options?.ignoreExitCode) {
|
|
135
|
-
resolve(success({ exitCode: code || 0 }));
|
|
136
|
-
}
|
|
137
|
-
else {
|
|
138
|
-
resolve(failure(`CDK command failed with exit code ${code}`));
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
child.on("error", (err) => {
|
|
142
|
-
clearTimeout(timeoutHandle);
|
|
143
|
-
this.runningProcesses.delete(processId);
|
|
144
|
-
if (settled || processKilled)
|
|
145
|
-
return;
|
|
146
|
-
settled = true;
|
|
147
|
-
resolve(failure(`Failed to run CDK command: ${err.message}`));
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
async runCdkCommand(workingDir, args, options) {
|
|
152
|
-
return new Promise((resolve) => {
|
|
153
|
-
if (!existsSync(workingDir)) {
|
|
154
|
-
resolve(failure(`Directory not found: ${workingDir}`));
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
// Check for cdk.json unless caller explicitly skips (e.g., bootstrap runs from tmpdir)
|
|
158
|
-
if (!options?.skipProjectCheck) {
|
|
159
|
-
const cdkJsonPath = join(workingDir, "cdk.json");
|
|
160
|
-
if (!existsSync(cdkJsonPath)) {
|
|
161
|
-
resolve(failure(`No CDK project found in ${workingDir}`));
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
let output = "";
|
|
166
|
-
let errorOutput = "";
|
|
167
|
-
let processKilled = false;
|
|
168
|
-
const contextArgs = this.argBuilder.buildContextArgs(options?.context);
|
|
169
|
-
const fullArgs = [...cdkBin.prefixArgs, ...args, ...contextArgs];
|
|
170
|
-
const env = {
|
|
171
|
-
...this.argBuilder.buildCdkEnv(options),
|
|
172
|
-
NO_COLOR: "1",
|
|
173
|
-
...(options?.extraEnv ? filterDangerousEnvVars(options.extraEnv) : {})
|
|
174
|
-
};
|
|
175
|
-
// Try to spawn the CDK process
|
|
176
|
-
logger.debug("CdkService", "Spawning CDK process", {
|
|
177
|
-
command: `${cdkBin.command} ${fullArgs.join(" ")}`,
|
|
178
|
-
workingDir
|
|
179
|
-
});
|
|
180
|
-
const child = spawn(cdkBin.command, fullArgs, {
|
|
181
|
-
cwd: workingDir,
|
|
182
|
-
env,
|
|
183
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
184
|
-
shell: false,
|
|
185
|
-
detached: false
|
|
186
|
-
});
|
|
187
|
-
// Check if spawn was successful
|
|
188
|
-
if (!child.pid) {
|
|
189
|
-
const spawnError = `Failed to spawn CDK process - no PID. cwd=${workingDir}, args=${fullArgs.join(" ")}`;
|
|
190
|
-
// CRITICAL: Attach error handler to prevent uncaught exception
|
|
191
|
-
// Node.js emits an async 'error' event on the child process even after spawn fails
|
|
192
|
-
// If we don't handle it, it becomes an uncaught exception and crashes the app
|
|
193
|
-
child.on("error", (err) => {
|
|
194
|
-
logger.debug("CdkProcess", "Deferred spawn error on failed child process", {
|
|
195
|
-
error: err.message
|
|
196
|
-
});
|
|
197
|
-
});
|
|
198
|
-
resolve(failure(spawnError));
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
// Track process for cleanup
|
|
202
|
-
const processId = `cdk-${++this.processCounter}`;
|
|
203
|
-
this.runningProcesses.set(processId, child);
|
|
204
|
-
let settled = false;
|
|
205
|
-
const timeoutMs = options?.timeout ?? 30 * 60 * 1000;
|
|
206
|
-
const timeoutHandle = setTimeout(() => {
|
|
207
|
-
if (!child.killed) {
|
|
208
|
-
processKilled = true;
|
|
209
|
-
this.forceKillProcess(child);
|
|
210
|
-
}
|
|
211
|
-
}, timeoutMs);
|
|
212
|
-
child.stdout?.on("data", (data) => {
|
|
213
|
-
const chunk = data.toString();
|
|
214
|
-
output += chunk;
|
|
215
|
-
if (options?.outputCallback) {
|
|
216
|
-
options.outputCallback(maskSensitiveOutput(chunk));
|
|
217
|
-
}
|
|
218
|
-
options?.cdkOutputLogger?.writeCdkOutput("stdout", maskSensitiveOutput(chunk));
|
|
219
|
-
});
|
|
220
|
-
child.stderr?.on("data", (data) => {
|
|
221
|
-
const chunk = data.toString();
|
|
222
|
-
options?.cdkOutputLogger?.writeCdkOutput("stderr", maskSensitiveOutput(chunk));
|
|
223
|
-
// Filter out non-critical warnings from the in-memory error buffer
|
|
224
|
-
if (!chunk.includes("deprecated") &&
|
|
225
|
-
!chunk.includes("npm WARN") &&
|
|
226
|
-
!chunk.includes("ENOENT")) {
|
|
227
|
-
errorOutput += chunk;
|
|
228
|
-
}
|
|
229
|
-
// Always send to callback for visibility
|
|
230
|
-
if (options?.errorCallback) {
|
|
231
|
-
options.errorCallback(maskSensitiveOutput(chunk));
|
|
232
|
-
}
|
|
233
|
-
});
|
|
234
|
-
child.on("error", (error) => {
|
|
235
|
-
clearTimeout(timeoutHandle);
|
|
236
|
-
this.runningProcesses.delete(processId);
|
|
237
|
-
if (settled || processKilled)
|
|
238
|
-
return;
|
|
239
|
-
settled = true;
|
|
240
|
-
resolve(failure(getErrorMessage(error)));
|
|
241
|
-
});
|
|
242
|
-
child.on("close", (code) => {
|
|
243
|
-
clearTimeout(timeoutHandle);
|
|
244
|
-
this.runningProcesses.delete(processId);
|
|
245
|
-
if (settled)
|
|
246
|
-
return;
|
|
247
|
-
settled = true;
|
|
248
|
-
if (processKilled) {
|
|
249
|
-
resolve(failure("CDK command timed out"));
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
const exitedClean = code === 0 || (options?.ignoreExitCode === true && code === 1);
|
|
253
|
-
// For diff command, we need to include stderr in output since CDK outputs errors there
|
|
254
|
-
const combinedOutput = output + (errorOutput ? `\n${errorOutput}` : "");
|
|
255
|
-
if (exitedClean) {
|
|
256
|
-
const rawOutput = options?.combineOutput ? combinedOutput : output;
|
|
257
|
-
resolve(success({
|
|
258
|
-
output: maskSensitiveOutput(rawOutput),
|
|
259
|
-
exitCode: code || 0
|
|
260
|
-
}));
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
// Parse error output for meaningful messages (match on raw before masking)
|
|
264
|
-
let errorMessage = errorOutput;
|
|
265
|
-
if (output) {
|
|
266
|
-
const errorMatch = output.match(/❌.*?Error:(.*)$/m);
|
|
267
|
-
if (errorMatch) {
|
|
268
|
-
errorMessage = errorMatch[1]?.trim() ?? "";
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
// Include stdout in the error string so callers can pattern-match on combined output
|
|
272
|
-
const errorText = errorMessage || `CDK command failed with exit code ${code}`;
|
|
273
|
-
const fullError = output ? `${errorText}\n${output}` : errorText;
|
|
274
|
-
resolve(failure(maskSensitiveOutput(fullError)));
|
|
275
|
-
});
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
}
|
|
1
|
+
import{spawn as K}from"child_process";import{existsSync as E}from"fs";import{join as T}from"path";import{createRequire as A}from"module";import{logger as $}from"@fjall/util/logger";import{filterDangerousEnvVars as M,maskSensitiveOutput as g}from"@fjall/util";import{success as H,failure as i}from"@fjall/generator";import{getErrorMessage as w}from"@fjall/util";function L(){try{const e=A(import.meta.url).resolve("aws-cdk/bin/cdk");return{command:process.execPath,prefixArgs:[e]}}catch(h){return $.debug("CdkService","Failed to resolve aws-cdk binary, falling back to npx",{error:h instanceof Error?h.message:String(h)}),{command:"npx",prefixArgs:["cdk"]}}}const x=L(),j=5e3;class v{runningProcesses=new Map;processCounter=0;argBuilder;exitHandler;sigintHandler;sigtermHandler;constructor(e){this.argBuilder=e,this.exitHandler=()=>this.cleanup(),this.sigintHandler=()=>{this.cleanup(),process.exit(130)},this.sigtermHandler=()=>{this.cleanup(),process.exit(143)},process.on("exit",this.exitHandler),process.on("SIGINT",this.sigintHandler),process.on("SIGTERM",this.sigtermHandler)}forceKillProcess(e){e.stdout?.destroy(),e.stderr?.destroy(),e.kill("SIGTERM");const c=setTimeout(()=>{e.exitCode===null&&e.kill("SIGKILL")},j);c.unref(),e.once("exit",()=>clearTimeout(c))}cleanup(){for(const[e,c]of this.runningProcesses)c.killed||(c.stdout?.destroy(),c.stderr?.destroy(),c.kill("SIGTERM"));this.runningProcesses.clear()}dispose(){this.cleanup(),process.removeListener("exit",this.exitHandler),process.removeListener("SIGINT",this.sigintHandler),process.removeListener("SIGTERM",this.sigtermHandler)}async runCdkCommandPassthrough(e,c,r){return new Promise(t=>{if(!E(e)){t(i(`Directory not found: ${e}`));return}const a=T(e,"cdk.json");if(!E(a)){t(i(`No CDK project found in ${e}`));return}const m=this.argBuilder.buildContextArgs(r?.context),f=[...x.prefixArgs,...c,...m],S=this.argBuilder.buildCdkEnv(r),d=K(x.command,f,{cwd:e,env:S,stdio:"inherit",shell:!1,detached:!1});if(!d.pid){d.on("error",u=>{$.debug("CdkProcess","Spawn error on failed child process",{error:u.message})}),t(i(`Failed to spawn CDK process - no PID. cwd=${e}, args=${f.join(" ")}`));return}const C=`cdk-passthrough-${++this.processCounter}`;this.runningProcesses.set(C,d);let o=!1,l=!1;const p=r?.timeout??30*60*1e3,k=setTimeout(()=>{d.killed||(o=!0,this.forceKillProcess(d))},p);d.on("close",u=>{if(clearTimeout(k),this.runningProcesses.delete(C),!l){if(l=!0,o){t(i("CDK command timed out"));return}u===0||r?.ignoreExitCode?t(H({exitCode:u||0})):t(i(`CDK command failed with exit code ${u}`))}}),d.on("error",u=>{clearTimeout(k),this.runningProcesses.delete(C),!(l||o)&&(l=!0,t(i(`Failed to run CDK command: ${u.message}`)))})})}async runCdkCommand(e,c,r){return new Promise(t=>{if(!E(e)){t(i(`Directory not found: ${e}`));return}if(!r?.skipProjectCheck){const s=T(e,"cdk.json");if(!E(s)){t(i(`No CDK project found in ${e}`));return}}let a="",m="",f=!1;const S=this.argBuilder.buildContextArgs(r?.context),d=[...x.prefixArgs,...c,...S],C={...this.argBuilder.buildCdkEnv(r),NO_COLOR:"1",...r?.extraEnv?M(r.extraEnv):{}};$.debug("CdkService","Spawning CDK process",{command:`${x.command} ${d.join(" ")}`,workingDir:e});const o=K(x.command,d,{cwd:e,env:C,stdio:["ignore","pipe","pipe"],shell:!1,detached:!1});if(!o.pid){const s=`Failed to spawn CDK process - no PID. cwd=${e}, args=${d.join(" ")}`;o.on("error",n=>{$.debug("CdkProcess","Deferred spawn error on failed child process",{error:n.message})}),t(i(s));return}const l=`cdk-${++this.processCounter}`;this.runningProcesses.set(l,o);let p=!1;const k=r?.timeout??30*60*1e3,u=setTimeout(()=>{o.killed||(f=!0,this.forceKillProcess(o))},k);o.stdout?.on("data",s=>{const n=s.toString();a+=n,r?.outputCallback&&r.outputCallback(g(n)),r?.cdkOutputLogger?.writeCdkOutput("stdout",g(n))}),o.stderr?.on("data",s=>{const n=s.toString();r?.cdkOutputLogger?.writeCdkOutput("stderr",g(n)),!n.includes("deprecated")&&!n.includes("npm WARN")&&!n.includes("ENOENT")&&(m+=n),r?.errorCallback&&r.errorCallback(g(n))}),o.on("error",s=>{clearTimeout(u),this.runningProcesses.delete(l),!(p||f)&&(p=!0,t(i(w(s))))}),o.on("close",s=>{if(clearTimeout(u),this.runningProcesses.delete(l),p)return;if(p=!0,f){t(i("CDK command timed out"));return}const n=s===0||r?.ignoreExitCode===!0&&s===1,O=a+(m?`
|
|
2
|
+
${m}`:"");if(n){const P=r?.combineOutput?O:a;t(H({output:g(P),exitCode:s||0}));return}let b=m;if(a){const P=a.match(/❌.*?Error:(.*)$/m);P&&(b=P[1]?.trim()??"")}const I=b||`CDK command failed with exit code ${s}`,y=a?`${I}
|
|
3
|
+
${a}`:I;t(i(g(y)))})})}}export{v as CdkProcessManager};
|