@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,213 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import { success, failure } from "@fjall/generator";
|
|
5
|
-
import { DEFAULT_REGION } from "../../aws/utils/regions.js";
|
|
6
|
-
import { getErrorMessage, maskSensitiveOutput } from "@fjall/util";
|
|
7
|
-
import { CdkEventMonitor, startStackMonitoring } from "./CdkEventMonitoring.js";
|
|
8
|
-
import { analyseDeployOutput, analyseDestroyResult, createEnhancedOutputCallback } from "./CdkOutputAnalyser.js";
|
|
9
|
-
import { CdkCommandRunner } from "./CdkCommandRunner.js";
|
|
10
|
-
import { CdkArgumentBuilder } from "./CdkArgumentBuilder.js";
|
|
11
|
-
import { CdkProcessManager } from "./CdkProcessManager.js";
|
|
12
|
-
import { wrapWithConstructMapEnrichment } from "./constructMapEnrichment.js";
|
|
13
|
-
import { STACK_DETECTION_FALLBACK_MS, resolveStackName, getFallbackStackName, buildDeploymentCdkContext } from "./cdkServiceHelpers.js";
|
|
14
|
-
export class CdkService {
|
|
15
|
-
commandRunner;
|
|
16
|
-
eventMonitor;
|
|
17
|
-
constructor(options) {
|
|
18
|
-
const processManager = options?.processManager ??
|
|
19
|
-
new CdkProcessManager(new CdkArgumentBuilder());
|
|
20
|
-
this.commandRunner = new CdkCommandRunner(processManager);
|
|
21
|
-
this.eventMonitor = new CdkEventMonitor({
|
|
22
|
-
eventLogWriterFactory: options?.eventLogWriterFactory
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
dispose() {
|
|
26
|
-
this.commandRunner.dispose();
|
|
27
|
-
}
|
|
28
|
-
// Delegate low-level commands to CdkCommandRunner
|
|
29
|
-
async checkDifferences(path, stackName, options) {
|
|
30
|
-
return this.commandRunner.checkDifferences(path, stackName, options);
|
|
31
|
-
}
|
|
32
|
-
async deploy(path, stackName, options) {
|
|
33
|
-
return this.commandRunner.deploy(path, stackName, options);
|
|
34
|
-
}
|
|
35
|
-
async destroy(path, stackName, options) {
|
|
36
|
-
return this.commandRunner.destroy(path, stackName, options);
|
|
37
|
-
}
|
|
38
|
-
async runImport(path, resourceMappingFile, options) {
|
|
39
|
-
return this.commandRunner.runImport(path, resourceMappingFile, options);
|
|
40
|
-
}
|
|
41
|
-
async synth(path, options) {
|
|
42
|
-
return this.commandRunner.synth(path, options);
|
|
43
|
-
}
|
|
44
|
-
async bootstrap(accountId, region, options) {
|
|
45
|
-
return this.commandRunner.bootstrap(accountId, region, options);
|
|
46
|
-
}
|
|
47
|
-
async runCdkSynth(context, onOutput) {
|
|
48
|
-
const accountId = context.callerIdentity?.Account;
|
|
49
|
-
try {
|
|
50
|
-
const result = await this.synth(context.path, {
|
|
51
|
-
outputCallback: onOutput,
|
|
52
|
-
context: buildDeploymentCdkContext(context, accountId, context.region || DEFAULT_REGION)
|
|
53
|
-
});
|
|
54
|
-
if (!result.success) {
|
|
55
|
-
return failure(result.error || "Failed to synthesise CloudFormation template");
|
|
56
|
-
}
|
|
57
|
-
return success({
|
|
58
|
-
message: "CloudFormation template synthesised",
|
|
59
|
-
details: result.data.output
|
|
60
|
-
? { synthesisTime: result.data.output }
|
|
61
|
-
: undefined
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
catch (error) {
|
|
65
|
-
return failure(`CDK synth failed: ${getErrorMessage(error)}`);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
async runCdkBootstrap(context, onOutput, credentials) {
|
|
69
|
-
const accountId = context.callerIdentity?.Account;
|
|
70
|
-
const region = context.region || DEFAULT_REGION;
|
|
71
|
-
try {
|
|
72
|
-
if (!accountId) {
|
|
73
|
-
return failure("No AWS account ID available");
|
|
74
|
-
}
|
|
75
|
-
const nodeModulesPath = join(context.path, "node_modules");
|
|
76
|
-
if (!existsSync(nodeModulesPath)) {
|
|
77
|
-
return failure(`Dependencies not installed. Please run 'npm install' in ${context.path} before deploying.`);
|
|
78
|
-
}
|
|
79
|
-
const result = await this.bootstrap(accountId, region, {
|
|
80
|
-
outputCallback: onOutput,
|
|
81
|
-
credentials
|
|
82
|
-
});
|
|
83
|
-
if (!result.success) {
|
|
84
|
-
return failure(result.error || "Failed to bootstrap AWS environment");
|
|
85
|
-
}
|
|
86
|
-
return success({ message: "AWS environment bootstrapped" });
|
|
87
|
-
}
|
|
88
|
-
catch (error) {
|
|
89
|
-
return failure(`CDK bootstrap failed: ${getErrorMessage(error)}`);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
async runCdkDiff(context, onOutput) {
|
|
93
|
-
const accountId = context.callerIdentity?.Account;
|
|
94
|
-
try {
|
|
95
|
-
const result = await this.checkDifferences(context.path, undefined, {
|
|
96
|
-
verbose: context.options?.verbose,
|
|
97
|
-
outputCallback: onOutput,
|
|
98
|
-
context: buildDeploymentCdkContext(context, accountId, context.region || DEFAULT_REGION)
|
|
99
|
-
});
|
|
100
|
-
if (!result.success) {
|
|
101
|
-
return failure(`CDK diff failed: ${result.error.message}`);
|
|
102
|
-
}
|
|
103
|
-
return success({
|
|
104
|
-
message: "Diff check complete",
|
|
105
|
-
details: {
|
|
106
|
-
hasDifferences: result.data.hasDifferences,
|
|
107
|
-
details: result.data.details
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
catch (error) {
|
|
112
|
-
return failure(`CDK diff failed: ${getErrorMessage(error)}`);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
async runCdkDeploy(context, stackPattern, onOutput, onResourceProgress, aws, credentials) {
|
|
116
|
-
const accountId = context.callerIdentity?.Account;
|
|
117
|
-
const region = context.region || DEFAULT_REGION;
|
|
118
|
-
if (!accountId) {
|
|
119
|
-
return failure("AWS account ID not available. Please ensure AWS credentials are properly configured.");
|
|
120
|
-
}
|
|
121
|
-
if (!aws) {
|
|
122
|
-
return failure("AwsProvider is required for deployment monitoring.");
|
|
123
|
-
}
|
|
124
|
-
// Read construct map from fjall-manifest.json (written during synth)
|
|
125
|
-
const enrichedOnResourceProgress = wrapWithConstructMapEnrichment(context.path, onResourceProgress);
|
|
126
|
-
let cfnMonitor = null;
|
|
127
|
-
let fallbackMonitoringTimeout;
|
|
128
|
-
try {
|
|
129
|
-
const targetStackName = resolveStackName(stackPattern, context) ??
|
|
130
|
-
getFallbackStackName(context);
|
|
131
|
-
cfnMonitor = await this.eventMonitor.createEventMonitor("deploy", targetStackName, region, context, aws);
|
|
132
|
-
if (onOutput) {
|
|
133
|
-
onOutput(maskSensitiveOutput(`Starting CloudFormation deployment of ${targetStackName}...\n`));
|
|
134
|
-
onOutput(`Monitoring CloudFormation events (CDK process running in background)...\n`);
|
|
135
|
-
}
|
|
136
|
-
const state = {
|
|
137
|
-
cdkOutput: "",
|
|
138
|
-
actualStackName: targetStackName,
|
|
139
|
-
stackDetected: false,
|
|
140
|
-
monitoringPromise: null
|
|
141
|
-
};
|
|
142
|
-
const enhancedOutputCallback = createEnhancedOutputCallback(state, onOutput, cfnMonitor, enrichedOnResourceProgress);
|
|
143
|
-
// Fall back to predicted stack name if actual name not detected in time
|
|
144
|
-
fallbackMonitoringTimeout = setTimeout(() => {
|
|
145
|
-
if (!state.stackDetected && cfnMonitor && !state.monitoringPromise) {
|
|
146
|
-
logger.debug("CdkService", "Fallback monitoring STARTING", {
|
|
147
|
-
targetStackName,
|
|
148
|
-
stackDetected: state.stackDetected,
|
|
149
|
-
hasOnResourceProgress: !!onResourceProgress
|
|
150
|
-
});
|
|
151
|
-
state.monitoringPromise = startStackMonitoring(cfnMonitor, targetStackName, enrichedOnResourceProgress);
|
|
152
|
-
}
|
|
153
|
-
}, STACK_DETECTION_FALLBACK_MS);
|
|
154
|
-
const result = await this.deploy(context.path, targetStackName, {
|
|
155
|
-
verbose: context.options?.verbose,
|
|
156
|
-
outputCallback: enhancedOutputCallback,
|
|
157
|
-
useCdkOut: true,
|
|
158
|
-
cdkOutputLogger: cfnMonitor?.getEventLogger() ?? undefined,
|
|
159
|
-
context: buildDeploymentCdkContext(context, accountId, region),
|
|
160
|
-
credentials
|
|
161
|
-
});
|
|
162
|
-
return analyseDeployOutput(state.cdkOutput, result, state.actualStackName);
|
|
163
|
-
}
|
|
164
|
-
catch (error) {
|
|
165
|
-
const errorMsg = `CDK deploy failed: ${getErrorMessage(error)}`;
|
|
166
|
-
logger.error("CdkService", "CDK deployment exception", {
|
|
167
|
-
error: errorMsg
|
|
168
|
-
});
|
|
169
|
-
return failure(errorMsg);
|
|
170
|
-
}
|
|
171
|
-
finally {
|
|
172
|
-
clearTimeout(fallbackMonitoringTimeout);
|
|
173
|
-
if (cfnMonitor) {
|
|
174
|
-
cfnMonitor.stopMonitoring();
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
async runCdkDestroy(context, stackPattern, onOutput, onResourceProgress, aws, useCdkOut, credentials) {
|
|
179
|
-
const accountId = context.callerIdentity?.Account;
|
|
180
|
-
const region = context.region || DEFAULT_REGION;
|
|
181
|
-
let cfnMonitor = null;
|
|
182
|
-
try {
|
|
183
|
-
const targetStackName = resolveStackName(stackPattern, context);
|
|
184
|
-
if (accountId && targetStackName && aws) {
|
|
185
|
-
cfnMonitor = await this.eventMonitor.createEventMonitor("destroy", targetStackName, region, context, aws);
|
|
186
|
-
cfnMonitor.startMonitoring(targetStackName, (event) => {
|
|
187
|
-
onResourceProgress?.(event);
|
|
188
|
-
}, (_success, _message) => {
|
|
189
|
-
// Stack destroy completed — no console output per architecture
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
const result = await this.destroy(context.path, stackPattern, {
|
|
193
|
-
verbose: context.options?.verbose,
|
|
194
|
-
outputCallback: onOutput,
|
|
195
|
-
useCdkOut,
|
|
196
|
-
cdkOutputLogger: cfnMonitor?.getEventLogger() ?? undefined,
|
|
197
|
-
context: buildDeploymentCdkContext(context, accountId, region, {
|
|
198
|
-
includeImageVersion: false
|
|
199
|
-
}),
|
|
200
|
-
credentials
|
|
201
|
-
});
|
|
202
|
-
return analyseDestroyResult(result);
|
|
203
|
-
}
|
|
204
|
-
catch (error) {
|
|
205
|
-
return failure(`CDK destroy failed: ${getErrorMessage(error)}`);
|
|
206
|
-
}
|
|
207
|
-
finally {
|
|
208
|
-
if (cfnMonitor) {
|
|
209
|
-
cfnMonitor.stopMonitoring();
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
1
|
+
import{existsSync as v}from"fs";import{join as M}from"path";import{logger as C}from"@fjall/util/logger";import{success as h,failure as s}from"@fjall/generator";import{DEFAULT_REGION as f}from"../../aws/utils/regions.js";import{getErrorMessage as g,maskSensitiveOutput as S}from"@fjall/util";import{CdkEventMonitor as I,startStackMonitoring as A}from"./CdkEventMonitoring.js";import{analyseDeployOutput as R,analyseDestroyResult as E,createEnhancedOutputCallback as F}from"./CdkOutputAnalyser.js";import{CdkCommandRunner as w}from"./CdkCommandRunner.js";import{CdkArgumentBuilder as K}from"./CdkArgumentBuilder.js";import{CdkProcessManager as N}from"./CdkProcessManager.js";import{wrapWithConstructMapEnrichment as O}from"./constructMapEnrichment.js";import{STACK_DETECTION_FALLBACK_MS as T,resolveStackName as D,getFallbackStackName as L,buildDeploymentCdkContext as y}from"./cdkServiceHelpers.js";class X{commandRunner;eventMonitor;constructor(e){const t=e?.processManager??new N(new K);this.commandRunner=new w(t),this.eventMonitor=new I({eventLogWriterFactory:e?.eventLogWriterFactory})}dispose(){this.commandRunner.dispose()}async checkDifferences(e,t,r){return this.commandRunner.checkDifferences(e,t,r)}async deploy(e,t,r){return this.commandRunner.deploy(e,t,r)}async destroy(e,t,r){return this.commandRunner.destroy(e,t,r)}async runImport(e,t,r){return this.commandRunner.runImport(e,t,r)}async synth(e,t){return this.commandRunner.synth(e,t)}async bootstrap(e,t,r){return this.commandRunner.bootstrap(e,t,r)}async runCdkSynth(e,t){const r=e.callerIdentity?.Account;try{const n=await this.synth(e.path,{outputCallback:t,context:y(e,r,e.region||f)});return n.success?h({message:"CloudFormation template synthesised",details:n.data.output?{synthesisTime:n.data.output}:void 0}):s(n.error||"Failed to synthesise CloudFormation template")}catch(n){return s(`CDK synth failed: ${g(n)}`)}}async runCdkBootstrap(e,t,r){const n=e.callerIdentity?.Account,u=e.region||f;try{if(!n)return s("No AWS account ID available");const d=M(e.path,"node_modules");if(!v(d))return s(`Dependencies not installed. Please run 'npm install' in ${e.path} before deploying.`);const l=await this.bootstrap(n,u,{outputCallback:t,credentials:r});return l.success?h({message:"AWS environment bootstrapped"}):s(l.error||"Failed to bootstrap AWS environment")}catch(d){return s(`CDK bootstrap failed: ${g(d)}`)}}async runCdkDiff(e,t){const r=e.callerIdentity?.Account;try{const n=await this.checkDifferences(e.path,void 0,{verbose:e.options?.verbose,outputCallback:t,context:y(e,r,e.region||f)});return n.success?h({message:"Diff check complete",details:{hasDifferences:n.data.hasDifferences,details:n.data.details}}):s(`CDK diff failed: ${n.error.message}`)}catch(n){return s(`CDK diff failed: ${g(n)}`)}}async runCdkDeploy(e,t,r,n,u,d){const l=e.callerIdentity?.Account,m=e.region||f;if(!l)return s("AWS account ID not available. Please ensure AWS credentials are properly configured.");if(!u)return s("AwsProvider is required for deployment monitoring.");const p=O(e.path,n);let o=null,c;try{const i=D(t,e)??L(e);o=await this.eventMonitor.createEventMonitor("deploy",i,m,e,u),r&&(r(S(`Starting CloudFormation deployment of ${i}...
|
|
2
|
+
`)),r(`Monitoring CloudFormation events (CDK process running in background)...
|
|
3
|
+
`));const a={cdkOutput:"",actualStackName:i,stackDetected:!1,monitoringPromise:null},k=F(a,r,o,p);c=setTimeout(()=>{!a.stackDetected&&o&&!a.monitoringPromise&&(C.debug("CdkService","Fallback monitoring STARTING",{targetStackName:i,stackDetected:a.stackDetected,hasOnResourceProgress:!!n}),a.monitoringPromise=A(o,i,p))},T);const b=await this.deploy(e.path,i,{verbose:e.options?.verbose,outputCallback:k,useCdkOut:!0,cdkOutputLogger:o?.getEventLogger()??void 0,context:y(e,l,m),credentials:d});return R(a.cdkOutput,b,a.actualStackName)}catch(i){const a=`CDK deploy failed: ${g(i)}`;return C.error("CdkService","CDK deployment exception",{error:a}),s(a)}finally{clearTimeout(c),o&&o.stopMonitoring()}}async runCdkDestroy(e,t,r,n,u,d,l){const m=e.callerIdentity?.Account,p=e.region||f;let o=null;try{const c=D(t,e);m&&c&&u&&(o=await this.eventMonitor.createEventMonitor("destroy",c,p,e,u),o.startMonitoring(c,a=>{n?.(a)},(a,k)=>{}));const i=await this.destroy(e.path,t,{verbose:e.options?.verbose,outputCallback:r,useCdkOut:d,cdkOutputLogger:o?.getEventLogger()??void 0,context:y(e,m,p,{includeImageVersion:!1}),credentials:l});return E(i)}catch(c){return s(`CDK destroy failed: ${g(c)}`)}finally{o&&o.stopMonitoring()}}}export{X as CdkService};
|
|
@@ -1,248 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { stackStatusMap } from "../../aws/utils/stackStatus.js";
|
|
3
|
-
import { success, failure } from "@fjall/generator";
|
|
4
|
-
import { BaseServiceError } from "../../types/errors/ServiceError.js";
|
|
5
|
-
import { logger } from "@fjall/util/logger";
|
|
6
|
-
import { getErrorMessage, sleep } from "@fjall/util";
|
|
7
|
-
import { STACK_NOT_FOUND_PATTERN } from "@fjall/util/aws";
|
|
8
|
-
import { extractErrorName } from "../../aws/organisations/types.js";
|
|
9
|
-
/**
|
|
10
|
-
* CloudFormation-specific error
|
|
11
|
-
*/
|
|
12
|
-
export class CloudFormationError extends BaseServiceError {
|
|
13
|
-
errorType;
|
|
14
|
-
stackName;
|
|
15
|
-
stackStatus;
|
|
16
|
-
constructor(message, errorType, stackName, stackStatus, details, recoverable = false) {
|
|
17
|
-
super(`CFN_${errorType.toUpperCase()}`, message, details, recoverable);
|
|
18
|
-
this.errorType = errorType;
|
|
19
|
-
this.stackName = stackName;
|
|
20
|
-
this.stackStatus = stackStatus;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* CloudFormation service using the AwsProvider interface.
|
|
25
|
-
*
|
|
26
|
-
* Unlike the CLI version which lazily initialises AwsContext,
|
|
27
|
-
* deploy-core receives a pre-configured AwsProvider at construction.
|
|
28
|
-
*/
|
|
29
|
-
export class CloudFormationService {
|
|
30
|
-
aws;
|
|
31
|
-
constructor(aws) {
|
|
32
|
-
this.aws = aws;
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Classify an AWS SDK error into a typed CloudFormationError.
|
|
36
|
-
*/
|
|
37
|
-
classifyAwsError(error, fallbackMessage, stackName) {
|
|
38
|
-
const errorName = extractErrorName(error);
|
|
39
|
-
const errorMessage = getErrorMessage(error);
|
|
40
|
-
if (errorName === "CredentialsError" || errorName === "UnauthorizedError") {
|
|
41
|
-
return new CloudFormationError(`AWS credentials error: ${errorMessage}`, "auth_error", stackName, undefined, error, false);
|
|
42
|
-
}
|
|
43
|
-
if (errorName === "Throttling" ||
|
|
44
|
-
errorName === "TooManyRequestsException") {
|
|
45
|
-
return new CloudFormationError(`AWS rate limit exceeded: ${errorMessage}`, "throttled", stackName, undefined, error, true);
|
|
46
|
-
}
|
|
47
|
-
if (errorName === "NetworkingError" || errorName === "ENOTFOUND") {
|
|
48
|
-
return new CloudFormationError(`Network error: ${errorMessage}`, "network_error", stackName, undefined, error, true);
|
|
49
|
-
}
|
|
50
|
-
return new CloudFormationError(`${fallbackMessage}: ${errorMessage}`, "unknown", stackName, undefined, error);
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Get stack outputs for services that need them
|
|
54
|
-
*/
|
|
55
|
-
async getStackOutputs(stackName, callbacks) {
|
|
56
|
-
callbacks?.onStackCheck?.(stackName);
|
|
57
|
-
const client = this.aws.getClient(CloudFormationClient);
|
|
58
|
-
const command = new DescribeStacksCommand({ StackName: stackName });
|
|
59
|
-
try {
|
|
60
|
-
const response = await client.send(command);
|
|
61
|
-
const stack = response.Stacks?.[0];
|
|
62
|
-
if (!stack?.Outputs) {
|
|
63
|
-
return success([]);
|
|
64
|
-
}
|
|
65
|
-
const outputs = stack.Outputs.map((output) => ({
|
|
66
|
-
OutputKey: output.OutputKey,
|
|
67
|
-
OutputValue: output.OutputValue,
|
|
68
|
-
ExportName: output.ExportName
|
|
69
|
-
}));
|
|
70
|
-
callbacks?.onOutputsRetrieved?.(stackName, outputs.length);
|
|
71
|
-
return success(outputs);
|
|
72
|
-
}
|
|
73
|
-
catch (error) {
|
|
74
|
-
if (error instanceof Error &&
|
|
75
|
-
error.name === "ValidationError" &&
|
|
76
|
-
error.message?.includes(STACK_NOT_FOUND_PATTERN)) {
|
|
77
|
-
callbacks?.onStackNotFound?.(stackName);
|
|
78
|
-
return success([]);
|
|
79
|
-
}
|
|
80
|
-
return failure(new CloudFormationError(`Failed to get outputs for stack ${stackName}: ${getErrorMessage(error)}`, "unknown", stackName, undefined, error));
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Get stack status directly
|
|
85
|
-
*/
|
|
86
|
-
async getStackStatus(stackName, callbacks) {
|
|
87
|
-
callbacks?.onStackCheck?.(stackName);
|
|
88
|
-
const client = this.aws.getClient(CloudFormationClient);
|
|
89
|
-
const command = new DescribeStacksCommand({ StackName: stackName });
|
|
90
|
-
try {
|
|
91
|
-
const response = await client.send(command);
|
|
92
|
-
const stack = response.Stacks?.[0];
|
|
93
|
-
if (!stack) {
|
|
94
|
-
return success({
|
|
95
|
-
status: "DOES_NOT_EXIST",
|
|
96
|
-
safeToRedeploy: "Yes",
|
|
97
|
-
description: "Stack does not exist yet"
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
const status = stack.StackStatus || "UNKNOWN";
|
|
101
|
-
const statusInfo = stackStatusMap[status] || stackStatusMap["UNKNOWN"];
|
|
102
|
-
callbacks?.onStackFound?.(stackName, status);
|
|
103
|
-
return success({
|
|
104
|
-
status,
|
|
105
|
-
safeToRedeploy: statusInfo.safeToRedeploy,
|
|
106
|
-
description: statusInfo.description,
|
|
107
|
-
statusReason: stack.StackStatusReason
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
catch (error) {
|
|
111
|
-
if (error instanceof Error &&
|
|
112
|
-
error.name === "ValidationError" &&
|
|
113
|
-
error.message?.includes(STACK_NOT_FOUND_PATTERN)) {
|
|
114
|
-
return success({
|
|
115
|
-
status: "DOES_NOT_EXIST",
|
|
116
|
-
safeToRedeploy: "Yes",
|
|
117
|
-
description: "Stack does not exist yet"
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
return failure(this.classifyAwsError(error, `Failed to get stack status for ${stackName}`, stackName));
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
/**
|
|
124
|
-
* Internal helper to list all CloudFormation exports with pagination.
|
|
125
|
-
*/
|
|
126
|
-
async listAllExports(earlyExit) {
|
|
127
|
-
const client = this.aws.getClient(CloudFormationClient);
|
|
128
|
-
const allExports = [];
|
|
129
|
-
try {
|
|
130
|
-
let nextToken;
|
|
131
|
-
do {
|
|
132
|
-
const command = new ListExportsCommand({ NextToken: nextToken });
|
|
133
|
-
const response = await client.send(command);
|
|
134
|
-
const pageExports = response.Exports || [];
|
|
135
|
-
for (const exp of pageExports) {
|
|
136
|
-
if (exp.Name && exp.Value) {
|
|
137
|
-
allExports.push({ Name: exp.Name, Value: exp.Value });
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
if (earlyExit?.(pageExports)) {
|
|
141
|
-
break;
|
|
142
|
-
}
|
|
143
|
-
nextToken = response.NextToken;
|
|
144
|
-
} while (nextToken);
|
|
145
|
-
return success(allExports);
|
|
146
|
-
}
|
|
147
|
-
catch (error) {
|
|
148
|
-
return failure(new CloudFormationError(`Failed to list exports: ${getErrorMessage(error)}`, "unknown", undefined, undefined, error, false));
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
/**
|
|
152
|
-
* Look up specific CloudFormation exports by exact name.
|
|
153
|
-
*/
|
|
154
|
-
async getExportsByNames(names) {
|
|
155
|
-
if (names.length === 0) {
|
|
156
|
-
return success(new Map());
|
|
157
|
-
}
|
|
158
|
-
const nameSet = new Set(names);
|
|
159
|
-
const found = new Map();
|
|
160
|
-
const result = await this.listAllExports((pageExports) => {
|
|
161
|
-
for (const exp of pageExports) {
|
|
162
|
-
if (exp.Name && nameSet.has(exp.Name) && exp.Value) {
|
|
163
|
-
found.set(exp.Name, exp.Value);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
return found.size >= nameSet.size;
|
|
167
|
-
});
|
|
168
|
-
if (!result.success) {
|
|
169
|
-
return failure(result.error);
|
|
170
|
-
}
|
|
171
|
-
return success(found);
|
|
172
|
-
}
|
|
173
|
-
/**
|
|
174
|
-
* List all CloudFormation exports
|
|
175
|
-
*/
|
|
176
|
-
async listExports(callbacks) {
|
|
177
|
-
const result = await this.listAllExports();
|
|
178
|
-
if (result.success) {
|
|
179
|
-
callbacks?.onExportsRetrieved?.(result.data.length);
|
|
180
|
-
}
|
|
181
|
-
return result;
|
|
182
|
-
}
|
|
183
|
-
/**
|
|
184
|
-
* Delete a CloudFormation stack directly via the API.
|
|
185
|
-
* Unlike CDK destroy, this works on stacks in DELETE_FAILED state.
|
|
186
|
-
*/
|
|
187
|
-
async deleteStack(stackName) {
|
|
188
|
-
const client = this.aws.getClient(CloudFormationClient);
|
|
189
|
-
try {
|
|
190
|
-
await client.send(new DeleteStackCommand({ StackName: stackName }));
|
|
191
|
-
return success(undefined);
|
|
192
|
-
}
|
|
193
|
-
catch (error) {
|
|
194
|
-
return failure(this.classifyAwsError(error, `Failed to delete stack ${stackName}`, stackName));
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
/**
|
|
198
|
-
* Check whether a CloudFormation stack exists and is in a deployable state.
|
|
199
|
-
*/
|
|
200
|
-
async stackExists(stackName, client) {
|
|
201
|
-
const cfnClient = client ?? this.aws.getClient(CloudFormationClient);
|
|
202
|
-
try {
|
|
203
|
-
const response = await cfnClient.send(new DescribeStacksCommand({ StackName: stackName }));
|
|
204
|
-
const status = response.Stacks?.[0]?.StackStatus;
|
|
205
|
-
return !!status && status !== "REVIEW_IN_PROGRESS";
|
|
206
|
-
}
|
|
207
|
-
catch (error) {
|
|
208
|
-
if (error instanceof Error &&
|
|
209
|
-
error.message?.includes(STACK_NOT_FOUND_PATTERN)) {
|
|
210
|
-
return false;
|
|
211
|
-
}
|
|
212
|
-
logger.debug("CloudFormationService", "Error checking stack existence, assuming exists", {
|
|
213
|
-
stackName,
|
|
214
|
-
error: getErrorMessage(error)
|
|
215
|
-
});
|
|
216
|
-
return true;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
/**
|
|
220
|
-
* Poll stack status until deletion completes, fails, or times out.
|
|
221
|
-
*/
|
|
222
|
-
async waitForDeleteComplete(stackName, options) {
|
|
223
|
-
const timeoutMs = options?.timeoutMs ?? 10 * 60 * 1000;
|
|
224
|
-
const pollIntervalMs = options?.pollIntervalMs ?? 5000;
|
|
225
|
-
const start = Date.now();
|
|
226
|
-
while (Date.now() - start < timeoutMs) {
|
|
227
|
-
const statusResult = await this.getStackStatus(stackName);
|
|
228
|
-
if (!statusResult.success) {
|
|
229
|
-
if (statusResult.error.recoverable) {
|
|
230
|
-
options?.onProgress?.(`Transient error polling stack ${stackName}, retrying: ${statusResult.error.message}`);
|
|
231
|
-
await sleep(pollIntervalMs);
|
|
232
|
-
continue;
|
|
233
|
-
}
|
|
234
|
-
return failure(statusResult.error);
|
|
235
|
-
}
|
|
236
|
-
const status = statusResult.data?.status;
|
|
237
|
-
if (status === "DELETE_COMPLETE" || status === "DOES_NOT_EXIST") {
|
|
238
|
-
return success(undefined);
|
|
239
|
-
}
|
|
240
|
-
if (status === "DELETE_FAILED") {
|
|
241
|
-
return failure(new CloudFormationError(`Stack ${stackName} deletion failed: ${statusResult.data?.statusReason || "unknown reason"}`, "stack_failed", stackName, status, undefined, false));
|
|
242
|
-
}
|
|
243
|
-
options?.onProgress?.(`Stack ${stackName} status: ${status ?? "unknown"}`);
|
|
244
|
-
await sleep(pollIntervalMs);
|
|
245
|
-
}
|
|
246
|
-
return failure(new CloudFormationError(`Timed out waiting for stack ${stackName} deletion after ${Math.round(timeoutMs / 1000)}s`, "timeout", stackName, undefined, undefined, true));
|
|
247
|
-
}
|
|
248
|
-
}
|
|
1
|
+
import{CloudFormationClient as d,DeleteStackCommand as g,DescribeStacksCommand as p,ListExportsCommand as h}from"@aws-sdk/client-cloudformation";import{stackStatusMap as E}from"../../aws/utils/stackStatus.js";import{success as u,failure as c}from"@fjall/generator";import{BaseServiceError as k}from"../../types/errors/ServiceError.js";import{logger as x}from"@fjall/util/logger";import{getErrorMessage as f,sleep as S}from"@fjall/util";import{STACK_NOT_FOUND_PATTERN as w}from"@fjall/util/aws";import{extractErrorName as y}from"../../aws/organisations/types.js";class l extends k{errorType;stackName;stackStatus;constructor(t,r,n,e,s,o=!1){super(`CFN_${r.toUpperCase()}`,t,s,o),this.errorType=r,this.stackName=n,this.stackStatus=e}}class D{aws;constructor(t){this.aws=t}classifyAwsError(t,r,n){const e=y(t),s=f(t);return e==="CredentialsError"||e==="UnauthorizedError"?new l(`AWS credentials error: ${s}`,"auth_error",n,void 0,t,!1):e==="Throttling"||e==="TooManyRequestsException"?new l(`AWS rate limit exceeded: ${s}`,"throttled",n,void 0,t,!0):e==="NetworkingError"||e==="ENOTFOUND"?new l(`Network error: ${s}`,"network_error",n,void 0,t,!0):new l(`${r}: ${s}`,"unknown",n,void 0,t)}async getStackOutputs(t,r){r?.onStackCheck?.(t);const n=this.aws.getClient(d),e=new p({StackName:t});try{const o=(await n.send(e)).Stacks?.[0];if(!o?.Outputs)return u([]);const a=o.Outputs.map(i=>({OutputKey:i.OutputKey,OutputValue:i.OutputValue,ExportName:i.ExportName}));return r?.onOutputsRetrieved?.(t,a.length),u(a)}catch(s){return s instanceof Error&&s.name==="ValidationError"&&s.message?.includes(w)?(r?.onStackNotFound?.(t),u([])):c(new l(`Failed to get outputs for stack ${t}: ${f(s)}`,"unknown",t,void 0,s))}}async getStackStatus(t,r){r?.onStackCheck?.(t);const n=this.aws.getClient(d),e=new p({StackName:t});try{const o=(await n.send(e)).Stacks?.[0];if(!o)return u({status:"DOES_NOT_EXIST",safeToRedeploy:"Yes",description:"Stack does not exist yet"});const a=o.StackStatus||"UNKNOWN",i=E[a]||E.UNKNOWN;return r?.onStackFound?.(t,a),u({status:a,safeToRedeploy:i.safeToRedeploy,description:i.description,statusReason:o.StackStatusReason})}catch(s){return s instanceof Error&&s.name==="ValidationError"&&s.message?.includes(w)?u({status:"DOES_NOT_EXIST",safeToRedeploy:"Yes",description:"Stack does not exist yet"}):c(this.classifyAwsError(s,`Failed to get stack status for ${t}`,t))}}async listAllExports(t){const r=this.aws.getClient(d),n=[];try{let e;do{const s=new h({NextToken:e}),o=await r.send(s),a=o.Exports||[];for(const i of a)i.Name&&i.Value&&n.push({Name:i.Name,Value:i.Value});if(t?.(a))break;e=o.NextToken}while(e);return u(n)}catch(e){return c(new l(`Failed to list exports: ${f(e)}`,"unknown",void 0,void 0,e,!1))}}async getExportsByNames(t){if(t.length===0)return u(new Map);const r=new Set(t),n=new Map,e=await this.listAllExports(s=>{for(const o of s)o.Name&&r.has(o.Name)&&o.Value&&n.set(o.Name,o.Value);return n.size>=r.size});return e.success?u(n):c(e.error)}async listExports(t){const r=await this.listAllExports();return r.success&&t?.onExportsRetrieved?.(r.data.length),r}async deleteStack(t){const r=this.aws.getClient(d);try{return await r.send(new g({StackName:t})),u(void 0)}catch(n){return c(this.classifyAwsError(n,`Failed to delete stack ${t}`,t))}}async stackExists(t,r){const n=r??this.aws.getClient(d);try{const s=(await n.send(new p({StackName:t}))).Stacks?.[0]?.StackStatus;return!!s&&s!=="REVIEW_IN_PROGRESS"}catch(e){return e instanceof Error&&e.message?.includes(w)?!1:(x.debug("CloudFormationService","Error checking stack existence, assuming exists",{stackName:t,error:f(e)}),!0)}}async waitForDeleteComplete(t,r){const n=r?.timeoutMs??6e5,e=r?.pollIntervalMs??5e3,s=Date.now();for(;Date.now()-s<n;){const o=await this.getStackStatus(t);if(!o.success){if(o.error.recoverable){r?.onProgress?.(`Transient error polling stack ${t}, retrying: ${o.error.message}`),await S(e);continue}return c(o.error)}const a=o.data?.status;if(a==="DELETE_COMPLETE"||a==="DOES_NOT_EXIST")return u(void 0);if(a==="DELETE_FAILED")return c(new l(`Stack ${t} deletion failed: ${o.data?.statusReason||"unknown reason"}`,"stack_failed",t,a,void 0,!1));r?.onProgress?.(`Stack ${t} status: ${a??"unknown"}`),await S(e)}return c(new l(`Timed out waiting for stack ${t} deletion after ${Math.round(n/1e3)}s`,"timeout",t,void 0,void 0,!0))}}export{l as CloudFormationError,D as CloudFormationService};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,44 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
/**
|
|
3
|
-
* Builder for creating standardised CDK deployment contexts.
|
|
4
|
-
* Ensures all required fields are present with proper defaults.
|
|
5
|
-
*
|
|
6
|
-
* Unlike the CLI version, this uses OrgConfig (injected) rather than
|
|
7
|
-
* Config.loadConfig() (file-based) for region resolution.
|
|
8
|
-
*/
|
|
9
|
-
export class CdkContextBuilder {
|
|
10
|
-
/**
|
|
11
|
-
* Build a standardised deployment context for CDK operations.
|
|
12
|
-
* Ensures all required fields are present with proper defaults.
|
|
13
|
-
*/
|
|
14
|
-
static buildDeploymentContext(input, options, orgConfig) {
|
|
15
|
-
return {
|
|
16
|
-
deployType: input.deployType,
|
|
17
|
-
target: input.target,
|
|
18
|
-
path: input.path,
|
|
19
|
-
options: options,
|
|
20
|
-
stackOutputs: input.stackOutputs || {},
|
|
21
|
-
callerIdentity: input.callerIdentity,
|
|
22
|
-
region: input.region || orgConfig?.primaryRegion || DEFAULT_REGION,
|
|
23
|
-
isManagedAccount: input.isManagedAccount,
|
|
24
|
-
accountName: input.accountName,
|
|
25
|
-
logPath: input.logPath,
|
|
26
|
-
imageVersion: input.imageVersion,
|
|
27
|
-
orgId: input.orgId,
|
|
28
|
-
rootId: input.rootId,
|
|
29
|
-
managementAccountId: input.managementAccountId,
|
|
30
|
-
ipamPoolId: input.ipamPoolId,
|
|
31
|
-
fjallOrgId: input.fjallOrgId,
|
|
32
|
-
orgConfig: input.orgConfig
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Build context from an existing context with updated options
|
|
37
|
-
*/
|
|
38
|
-
static updateContext(existingContext, updates) {
|
|
39
|
-
return {
|
|
40
|
-
...existingContext,
|
|
41
|
-
...updates
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
}
|
|
1
|
+
import{DEFAULT_REGION as t}from"@fjall/generator";class d{static buildDeploymentContext(o,e,a){return{deployType:o.deployType,target:o.target,path:o.path,options:e,stackOutputs:o.stackOutputs||{},callerIdentity:o.callerIdentity,region:o.region||a?.primaryRegion||t,isManagedAccount:o.isManagedAccount,accountName:o.accountName,logPath:o.logPath,imageVersion:o.imageVersion,orgId:o.orgId,rootId:o.rootId,managementAccountId:o.managementAccountId,ipamPoolId:o.ipamPoolId,fjallOrgId:o.fjallOrgId,orgConfig:o.orgConfig}}static updateContext(o,e){return{...o,...e}}}export{d as CdkContextBuilder};
|