@stacksjs/ts-cloud 0.1.1
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.md +21 -0
- package/README.md +321 -0
- package/bin/cli.ts +133 -0
- package/bin/commands/analytics.ts +328 -0
- package/bin/commands/api.ts +379 -0
- package/bin/commands/assets.ts +221 -0
- package/bin/commands/audit.ts +501 -0
- package/bin/commands/backup.ts +682 -0
- package/bin/commands/cache.ts +294 -0
- package/bin/commands/cdn.ts +281 -0
- package/bin/commands/config.ts +202 -0
- package/bin/commands/container.ts +105 -0
- package/bin/commands/cost.ts +208 -0
- package/bin/commands/database.ts +401 -0
- package/bin/commands/deploy.ts +674 -0
- package/bin/commands/domain.ts +397 -0
- package/bin/commands/email.ts +423 -0
- package/bin/commands/environment.ts +285 -0
- package/bin/commands/events.ts +424 -0
- package/bin/commands/firewall.ts +145 -0
- package/bin/commands/function.ts +116 -0
- package/bin/commands/generate.ts +280 -0
- package/bin/commands/git.ts +139 -0
- package/bin/commands/iam.ts +464 -0
- package/bin/commands/index.ts +48 -0
- package/bin/commands/init.ts +120 -0
- package/bin/commands/logs.ts +148 -0
- package/bin/commands/network.ts +579 -0
- package/bin/commands/notify.ts +489 -0
- package/bin/commands/queue.ts +407 -0
- package/bin/commands/scheduler.ts +370 -0
- package/bin/commands/secrets.ts +54 -0
- package/bin/commands/server.ts +629 -0
- package/bin/commands/shared.ts +97 -0
- package/bin/commands/ssl.ts +138 -0
- package/bin/commands/stack.ts +325 -0
- package/bin/commands/status.ts +385 -0
- package/bin/commands/storage.ts +450 -0
- package/bin/commands/team.ts +96 -0
- package/bin/commands/tunnel.ts +489 -0
- package/bin/commands/utils.ts +202 -0
- package/build.ts +15 -0
- package/cloud +2 -0
- package/package.json +99 -0
- package/src/aws/acm.ts +768 -0
- package/src/aws/application-autoscaling.ts +845 -0
- package/src/aws/bedrock.ts +4074 -0
- package/src/aws/client.ts +878 -0
- package/src/aws/cloudformation.ts +896 -0
- package/src/aws/cloudfront.ts +1531 -0
- package/src/aws/cloudwatch-logs.ts +154 -0
- package/src/aws/comprehend.ts +839 -0
- package/src/aws/connect.ts +1056 -0
- package/src/aws/deploy-imap.ts +384 -0
- package/src/aws/dynamodb.ts +340 -0
- package/src/aws/ec2.ts +1385 -0
- package/src/aws/ecr.ts +621 -0
- package/src/aws/ecs.ts +615 -0
- package/src/aws/elasticache.ts +301 -0
- package/src/aws/elbv2.ts +942 -0
- package/src/aws/email.ts +928 -0
- package/src/aws/eventbridge.ts +248 -0
- package/src/aws/iam.ts +1689 -0
- package/src/aws/imap-server.ts +2100 -0
- package/src/aws/index.ts +213 -0
- package/src/aws/kendra.ts +1097 -0
- package/src/aws/lambda.ts +786 -0
- package/src/aws/opensearch.ts +158 -0
- package/src/aws/personalize.ts +977 -0
- package/src/aws/polly.ts +559 -0
- package/src/aws/rds.ts +888 -0
- package/src/aws/rekognition.ts +846 -0
- package/src/aws/route53-domains.ts +359 -0
- package/src/aws/route53.ts +1046 -0
- package/src/aws/s3.ts +2318 -0
- package/src/aws/scheduler.ts +571 -0
- package/src/aws/secrets-manager.ts +769 -0
- package/src/aws/ses.ts +1081 -0
- package/src/aws/setup-phone.ts +104 -0
- package/src/aws/setup-sms.ts +580 -0
- package/src/aws/sms.ts +1735 -0
- package/src/aws/smtp-server.ts +531 -0
- package/src/aws/sns.ts +758 -0
- package/src/aws/sqs.ts +382 -0
- package/src/aws/ssm.ts +807 -0
- package/src/aws/sts.ts +92 -0
- package/src/aws/support.ts +391 -0
- package/src/aws/test-imap.ts +86 -0
- package/src/aws/textract.ts +780 -0
- package/src/aws/transcribe.ts +108 -0
- package/src/aws/translate.ts +641 -0
- package/src/aws/voice.ts +1379 -0
- package/src/config.ts +35 -0
- package/src/deploy/index.ts +7 -0
- package/src/deploy/static-site-external-dns.ts +906 -0
- package/src/deploy/static-site.ts +1125 -0
- package/src/dns/godaddy.ts +412 -0
- package/src/dns/index.ts +183 -0
- package/src/dns/porkbun.ts +362 -0
- package/src/dns/route53-adapter.ts +414 -0
- package/src/dns/types.ts +114 -0
- package/src/dns/validator.ts +369 -0
- package/src/generators/index.ts +5 -0
- package/src/generators/infrastructure.ts +1660 -0
- package/src/index.ts +163 -0
- package/src/push/apns.ts +452 -0
- package/src/push/fcm.ts +506 -0
- package/src/push/index.ts +58 -0
- package/src/ssl/acme-client.ts +478 -0
- package/src/ssl/index.ts +7 -0
- package/src/ssl/letsencrypt.ts +747 -0
- package/src/types.ts +2 -0
- package/src/utils/cli.ts +398 -0
- package/src/validation/index.ts +5 -0
- package/src/validation/template.ts +405 -0
- package/test/index.test.ts +128 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,896 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AWS CloudFormation Operations
|
|
3
|
+
* Direct API calls without AWS CLI dependency
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { AWSClient, buildQueryParams } from './client'
|
|
7
|
+
|
|
8
|
+
export interface StackParameter {
|
|
9
|
+
ParameterKey: string
|
|
10
|
+
ParameterValue: string
|
|
11
|
+
UsePreviousValue?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface StackTag {
|
|
15
|
+
Key: string
|
|
16
|
+
Value: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface CreateStackOptions {
|
|
20
|
+
stackName: string
|
|
21
|
+
templateBody?: string
|
|
22
|
+
templateUrl?: string
|
|
23
|
+
parameters?: StackParameter[]
|
|
24
|
+
capabilities?: string[]
|
|
25
|
+
roleArn?: string
|
|
26
|
+
tags?: StackTag[]
|
|
27
|
+
timeoutInMinutes?: number
|
|
28
|
+
onFailure?: 'DO_NOTHING' | 'ROLLBACK' | 'DELETE'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface UpdateStackOptions {
|
|
32
|
+
stackName: string
|
|
33
|
+
templateBody?: string
|
|
34
|
+
templateUrl?: string
|
|
35
|
+
parameters?: StackParameter[]
|
|
36
|
+
capabilities?: string[]
|
|
37
|
+
roleArn?: string
|
|
38
|
+
tags?: StackTag[]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface DescribeStacksOptions {
|
|
42
|
+
stackName?: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface StackEvent {
|
|
46
|
+
Timestamp: string
|
|
47
|
+
ResourceType: string
|
|
48
|
+
LogicalResourceId: string
|
|
49
|
+
ResourceStatus: string
|
|
50
|
+
ResourceStatusReason?: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface Stack {
|
|
54
|
+
StackId: string
|
|
55
|
+
StackName: string
|
|
56
|
+
StackStatus: string
|
|
57
|
+
StackStatusReason?: string
|
|
58
|
+
CreationTime: string
|
|
59
|
+
LastUpdatedTime?: string
|
|
60
|
+
Parameters?: StackParameter[]
|
|
61
|
+
Outputs?: Array<{
|
|
62
|
+
OutputKey: string
|
|
63
|
+
OutputValue: string
|
|
64
|
+
Description?: string
|
|
65
|
+
ExportName?: string
|
|
66
|
+
}>
|
|
67
|
+
Tags?: StackTag[]
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* CloudFormation stack management using direct API calls
|
|
72
|
+
*/
|
|
73
|
+
export class CloudFormationClient {
|
|
74
|
+
private client: AWSClient
|
|
75
|
+
private region: string
|
|
76
|
+
|
|
77
|
+
constructor(region: string = 'us-east-1', profile?: string) {
|
|
78
|
+
this.region = region
|
|
79
|
+
this.client = new AWSClient()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Create a new CloudFormation stack
|
|
84
|
+
*/
|
|
85
|
+
async createStack(options: CreateStackOptions): Promise<{ StackId: string }> {
|
|
86
|
+
const params: Record<string, any> = {
|
|
87
|
+
Action: 'CreateStack',
|
|
88
|
+
StackName: options.stackName,
|
|
89
|
+
Version: '2010-05-15',
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (options.templateBody) {
|
|
93
|
+
params.TemplateBody = options.templateBody
|
|
94
|
+
}
|
|
95
|
+
else if (options.templateUrl) {
|
|
96
|
+
params.TemplateURL = options.templateUrl
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
throw new Error('Either templateBody or templateUrl must be provided')
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (options.parameters) {
|
|
103
|
+
options.parameters.forEach((param, index) => {
|
|
104
|
+
params[`Parameters.member.${index + 1}.ParameterKey`] = param.ParameterKey
|
|
105
|
+
params[`Parameters.member.${index + 1}.ParameterValue`] = param.ParameterValue
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (options.capabilities) {
|
|
110
|
+
options.capabilities.forEach((cap, index) => {
|
|
111
|
+
params[`Capabilities.member.${index + 1}`] = cap
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (options.roleArn) {
|
|
116
|
+
params.RoleARN = options.roleArn
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (options.tags) {
|
|
120
|
+
options.tags.forEach((tag, index) => {
|
|
121
|
+
params[`Tags.member.${index + 1}.Key`] = tag.Key
|
|
122
|
+
params[`Tags.member.${index + 1}.Value`] = tag.Value
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (options.timeoutInMinutes) {
|
|
127
|
+
params.TimeoutInMinutes = options.timeoutInMinutes
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (options.onFailure) {
|
|
131
|
+
params.OnFailure = options.onFailure
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const result = await this.client.request({
|
|
135
|
+
service: 'cloudformation',
|
|
136
|
+
region: this.region,
|
|
137
|
+
method: 'POST',
|
|
138
|
+
path: '/',
|
|
139
|
+
body: new URLSearchParams(params).toString(),
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
return { StackId: result.StackId || result.CreateStackResult?.StackId }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Update an existing CloudFormation stack
|
|
147
|
+
*/
|
|
148
|
+
async updateStack(options: UpdateStackOptions): Promise<{ StackId: string }> {
|
|
149
|
+
const params: Record<string, any> = {
|
|
150
|
+
Action: 'UpdateStack',
|
|
151
|
+
StackName: options.stackName,
|
|
152
|
+
Version: '2010-05-15',
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (options.templateBody) {
|
|
156
|
+
params.TemplateBody = options.templateBody
|
|
157
|
+
}
|
|
158
|
+
else if (options.templateUrl) {
|
|
159
|
+
params.TemplateURL = options.templateUrl
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (options.parameters) {
|
|
163
|
+
options.parameters.forEach((param, index) => {
|
|
164
|
+
params[`Parameters.member.${index + 1}.ParameterKey`] = param.ParameterKey
|
|
165
|
+
if (param.UsePreviousValue) {
|
|
166
|
+
params[`Parameters.member.${index + 1}.UsePreviousValue`] = 'true'
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
params[`Parameters.member.${index + 1}.ParameterValue`] = param.ParameterValue
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (options.capabilities) {
|
|
175
|
+
options.capabilities.forEach((cap, index) => {
|
|
176
|
+
params[`Capabilities.member.${index + 1}`] = cap
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (options.roleArn) {
|
|
181
|
+
params.RoleARN = options.roleArn
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (options.tags) {
|
|
185
|
+
options.tags.forEach((tag, index) => {
|
|
186
|
+
params[`Tags.member.${index + 1}.Key`] = tag.Key
|
|
187
|
+
params[`Tags.member.${index + 1}.Value`] = tag.Value
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const result = await this.client.request({
|
|
192
|
+
service: 'cloudformation',
|
|
193
|
+
region: this.region,
|
|
194
|
+
method: 'POST',
|
|
195
|
+
path: '/',
|
|
196
|
+
body: new URLSearchParams(params).toString(),
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
return { StackId: result.StackId || result.UpdateStackResult?.StackId }
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Delete a CloudFormation stack
|
|
204
|
+
*/
|
|
205
|
+
async deleteStack(stackName: string, roleArn?: string, retainResources?: string[]): Promise<void> {
|
|
206
|
+
const params: Record<string, any> = {
|
|
207
|
+
Action: 'DeleteStack',
|
|
208
|
+
StackName: stackName,
|
|
209
|
+
Version: '2010-05-15',
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (roleArn) {
|
|
213
|
+
params.RoleARN = roleArn
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Add retained resources if provided
|
|
217
|
+
if (retainResources && retainResources.length > 0) {
|
|
218
|
+
retainResources.forEach((resource, index) => {
|
|
219
|
+
params[`RetainResources.member.${index + 1}`] = resource
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
await this.client.request({
|
|
224
|
+
service: 'cloudformation',
|
|
225
|
+
region: this.region,
|
|
226
|
+
method: 'POST',
|
|
227
|
+
path: '/',
|
|
228
|
+
body: new URLSearchParams(params).toString(),
|
|
229
|
+
})
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Describe CloudFormation stacks
|
|
234
|
+
*/
|
|
235
|
+
async describeStacks(options: DescribeStacksOptions = {}): Promise<{ Stacks: Stack[] }> {
|
|
236
|
+
const params: Record<string, any> = {
|
|
237
|
+
Action: 'DescribeStacks',
|
|
238
|
+
Version: '2010-05-15',
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (options.stackName) {
|
|
242
|
+
params.StackName = options.stackName
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const result = await this.client.request({
|
|
246
|
+
service: 'cloudformation',
|
|
247
|
+
region: this.region,
|
|
248
|
+
method: 'POST',
|
|
249
|
+
path: '/',
|
|
250
|
+
body: new URLSearchParams(params).toString(),
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
// Parse the response
|
|
254
|
+
const stacks = this.parseStacksResponse(result)
|
|
255
|
+
return { Stacks: stacks }
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Get stack events
|
|
260
|
+
*/
|
|
261
|
+
async describeStackEvents(stackName: string): Promise<{ StackEvents: StackEvent[] }> {
|
|
262
|
+
const params: Record<string, any> = {
|
|
263
|
+
Action: 'DescribeStackEvents',
|
|
264
|
+
StackName: stackName,
|
|
265
|
+
Version: '2010-05-15',
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const result = await this.client.request({
|
|
269
|
+
service: 'cloudformation',
|
|
270
|
+
region: this.region,
|
|
271
|
+
method: 'POST',
|
|
272
|
+
path: '/',
|
|
273
|
+
body: new URLSearchParams(params).toString(),
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
return { StackEvents: this.parseStackEvents(result) }
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* List stack resources
|
|
281
|
+
*/
|
|
282
|
+
async listStackResources(stackName: string): Promise<{ StackResourceSummaries: any[] }> {
|
|
283
|
+
const params: Record<string, any> = {
|
|
284
|
+
Action: 'ListStackResources',
|
|
285
|
+
StackName: stackName,
|
|
286
|
+
Version: '2010-05-15',
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const result = await this.client.request({
|
|
290
|
+
service: 'cloudformation',
|
|
291
|
+
region: this.region,
|
|
292
|
+
method: 'POST',
|
|
293
|
+
path: '/',
|
|
294
|
+
body: new URLSearchParams(params).toString(),
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
// Parse resources from XML response - handle single member (object) or multiple members (array)
|
|
298
|
+
const member = result?.ListStackResourcesResult?.StackResourceSummaries?.member
|
|
299
|
+
let resources: any[] = []
|
|
300
|
+
|
|
301
|
+
if (member) {
|
|
302
|
+
resources = Array.isArray(member) ? member : [member]
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return { StackResourceSummaries: resources }
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Wait for stack to reach a specific status
|
|
310
|
+
*/
|
|
311
|
+
async waitForStack(stackName: string, waitType: 'stack-create-complete' | 'stack-update-complete' | 'stack-delete-complete'): Promise<void> {
|
|
312
|
+
const targetStatuses = {
|
|
313
|
+
'stack-create-complete': ['CREATE_COMPLETE'],
|
|
314
|
+
'stack-update-complete': ['UPDATE_COMPLETE'],
|
|
315
|
+
'stack-delete-complete': ['DELETE_COMPLETE'],
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const failureStatuses = [
|
|
319
|
+
'CREATE_FAILED',
|
|
320
|
+
'ROLLBACK_FAILED',
|
|
321
|
+
'ROLLBACK_COMPLETE',
|
|
322
|
+
'UPDATE_ROLLBACK_FAILED',
|
|
323
|
+
'UPDATE_ROLLBACK_COMPLETE',
|
|
324
|
+
]
|
|
325
|
+
|
|
326
|
+
const targets = targetStatuses[waitType]
|
|
327
|
+
const maxAttempts = 360 // 30 minutes (DNS records can take 10-30 minutes)
|
|
328
|
+
let attempts = 0
|
|
329
|
+
|
|
330
|
+
while (attempts < maxAttempts) {
|
|
331
|
+
try {
|
|
332
|
+
const result = await this.describeStacks({ stackName })
|
|
333
|
+
|
|
334
|
+
if (result.Stacks.length === 0) {
|
|
335
|
+
if (waitType === 'stack-delete-complete') {
|
|
336
|
+
return // Stack deleted successfully
|
|
337
|
+
}
|
|
338
|
+
// For create/update operations, stack might not be visible yet - retry
|
|
339
|
+
if (attempts % 10 === 0) {
|
|
340
|
+
console.log(`[waitForStack] Attempt ${attempts}: Stack not visible yet`)
|
|
341
|
+
}
|
|
342
|
+
await new Promise(resolve => setTimeout(resolve, 2000))
|
|
343
|
+
attempts++
|
|
344
|
+
continue
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const stack = result.Stacks[0]
|
|
348
|
+
|
|
349
|
+
if (attempts % 10 === 0) {
|
|
350
|
+
console.log(`[waitForStack] Attempt ${attempts}: Status = ${stack.StackStatus}${stack.StackStatusReason ? ` (${stack.StackStatusReason})` : ''}`)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (targets.includes(stack.StackStatus)) {
|
|
354
|
+
return // Target status reached
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// If stack is being deleted but we're waiting for create/update, something went wrong
|
|
358
|
+
if ((waitType === 'stack-create-complete' || waitType === 'stack-update-complete') &&
|
|
359
|
+
(stack.StackStatus === 'DELETE_IN_PROGRESS' || stack.StackStatus === 'DELETE_COMPLETE')) {
|
|
360
|
+
console.log(`[waitForStack] Stack is being deleted (creation/update failed)`)
|
|
361
|
+
// Try to get stack events to understand the failure
|
|
362
|
+
let failedEventReason = ''
|
|
363
|
+
try {
|
|
364
|
+
const eventsResult = await this.describeStackEvents(stackName)
|
|
365
|
+
console.log('[waitForStack] Stack events (most recent first):')
|
|
366
|
+
for (const event of eventsResult.StackEvents.slice(0, 15)) {
|
|
367
|
+
if (event.ResourceStatus.includes('FAILED') || event.ResourceStatusReason) {
|
|
368
|
+
console.log(` ${event.LogicalResourceId}: ${event.ResourceStatus} - ${event.ResourceStatusReason || 'No reason provided'}`)
|
|
369
|
+
// Capture the first failed event reason for the error message
|
|
370
|
+
if (event.ResourceStatus.includes('FAILED') && event.ResourceStatusReason && !failedEventReason) {
|
|
371
|
+
failedEventReason = event.ResourceStatusReason
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
// Ignore errors fetching events
|
|
378
|
+
}
|
|
379
|
+
// Include the detailed failure reason in the error message
|
|
380
|
+
const errorReason = failedEventReason || stack.StackStatusReason || 'Check CloudFormation console for details.'
|
|
381
|
+
throw new Error(`Stack creation/update failed - stack is being deleted. Reason: ${errorReason}`)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Handle DELETE_FAILED specifically - might need to retain resources
|
|
385
|
+
if (stack.StackStatus === 'DELETE_FAILED' && waitType === 'stack-delete-complete') {
|
|
386
|
+
const error: any = new Error(`Stack deletion failed - may have resources that need to be retained`)
|
|
387
|
+
error.code = 'DELETE_FAILED'
|
|
388
|
+
error.stackStatus = stack.StackStatus
|
|
389
|
+
error.statusReason = stack.StackStatusReason
|
|
390
|
+
throw error
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (failureStatuses.includes(stack.StackStatus)) {
|
|
394
|
+
throw new Error(`Stack reached failure status: ${stack.StackStatus}`)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Wait 5 seconds before next attempt
|
|
398
|
+
await new Promise(resolve => setTimeout(resolve, 5000))
|
|
399
|
+
attempts++
|
|
400
|
+
}
|
|
401
|
+
catch (error: any) {
|
|
402
|
+
if (waitType === 'stack-delete-complete' && error.message?.includes('does not exist')) {
|
|
403
|
+
return // Stack deleted
|
|
404
|
+
}
|
|
405
|
+
// For create operations, stack might not be visible yet - retry
|
|
406
|
+
if (waitType === 'stack-create-complete' && error.message?.includes('does not exist')) {
|
|
407
|
+
if (attempts % 10 === 0) {
|
|
408
|
+
console.log(`[waitForStack] Attempt ${attempts}: Stack does not exist (error), retrying...`)
|
|
409
|
+
}
|
|
410
|
+
await new Promise(resolve => setTimeout(resolve, 2000))
|
|
411
|
+
attempts++
|
|
412
|
+
continue
|
|
413
|
+
}
|
|
414
|
+
console.log(`[waitForStack] Unexpected error: ${error.message}`)
|
|
415
|
+
throw error
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
console.log(`[waitForStack] Timeout after ${attempts} attempts`)
|
|
420
|
+
throw new Error(`Timeout waiting for stack to reach ${waitType}`)
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Validate CloudFormation template
|
|
425
|
+
*/
|
|
426
|
+
async validateTemplate(templateBody: string): Promise<any> {
|
|
427
|
+
const params: Record<string, any> = {
|
|
428
|
+
Action: 'ValidateTemplate',
|
|
429
|
+
TemplateBody: templateBody,
|
|
430
|
+
Version: '2010-05-15',
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return await this.client.request({
|
|
434
|
+
service: 'cloudformation',
|
|
435
|
+
region: this.region,
|
|
436
|
+
method: 'POST',
|
|
437
|
+
path: '/',
|
|
438
|
+
body: new URLSearchParams(params).toString(),
|
|
439
|
+
})
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* List all stacks
|
|
444
|
+
*/
|
|
445
|
+
async listStacks(statusFilter?: string[]): Promise<{ StackSummaries: Array<{
|
|
446
|
+
StackId: string
|
|
447
|
+
StackName: string
|
|
448
|
+
TemplateDescription?: string
|
|
449
|
+
CreationTime: string
|
|
450
|
+
LastUpdatedTime?: string
|
|
451
|
+
DeletionTime?: string
|
|
452
|
+
StackStatus: string
|
|
453
|
+
}> }> {
|
|
454
|
+
const params: Record<string, any> = {
|
|
455
|
+
Action: 'ListStacks',
|
|
456
|
+
Version: '2010-05-15',
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (statusFilter) {
|
|
460
|
+
statusFilter.forEach((status, index) => {
|
|
461
|
+
params[`StackStatusFilter.member.${index + 1}`] = status
|
|
462
|
+
})
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const result = await this.client.request({
|
|
466
|
+
service: 'cloudformation',
|
|
467
|
+
region: this.region,
|
|
468
|
+
method: 'POST',
|
|
469
|
+
path: '/',
|
|
470
|
+
body: new URLSearchParams(params).toString(),
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
return { StackSummaries: [] } // TODO: Parse response
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Create change set (for preview before updating)
|
|
479
|
+
*/
|
|
480
|
+
async createChangeSet(options: {
|
|
481
|
+
stackName: string
|
|
482
|
+
changeSetName: string
|
|
483
|
+
templateBody?: string
|
|
484
|
+
templateUrl?: string
|
|
485
|
+
parameters?: StackParameter[]
|
|
486
|
+
capabilities?: string[]
|
|
487
|
+
changeSetType?: 'CREATE' | 'UPDATE'
|
|
488
|
+
}): Promise<{ Id: string, StackId: string }> {
|
|
489
|
+
const params: Record<string, any> = {
|
|
490
|
+
Action: 'CreateChangeSet',
|
|
491
|
+
StackName: options.stackName,
|
|
492
|
+
ChangeSetName: options.changeSetName,
|
|
493
|
+
Version: '2010-05-15',
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (options.templateBody) {
|
|
497
|
+
params.TemplateBody = options.templateBody
|
|
498
|
+
}
|
|
499
|
+
else if (options.templateUrl) {
|
|
500
|
+
params.TemplateURL = options.templateUrl
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (options.parameters) {
|
|
504
|
+
options.parameters.forEach((param, index) => {
|
|
505
|
+
params[`Parameters.member.${index + 1}.ParameterKey`] = param.ParameterKey
|
|
506
|
+
params[`Parameters.member.${index + 1}.ParameterValue`] = param.ParameterValue
|
|
507
|
+
})
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (options.capabilities) {
|
|
511
|
+
options.capabilities.forEach((cap, index) => {
|
|
512
|
+
params[`Capabilities.member.${index + 1}`] = cap
|
|
513
|
+
})
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (options.changeSetType) {
|
|
517
|
+
params.ChangeSetType = options.changeSetType
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const result = await this.client.request({
|
|
521
|
+
service: 'cloudformation',
|
|
522
|
+
region: this.region,
|
|
523
|
+
method: 'POST',
|
|
524
|
+
path: '/',
|
|
525
|
+
body: new URLSearchParams(params).toString(),
|
|
526
|
+
})
|
|
527
|
+
|
|
528
|
+
return { Id: result.Id, StackId: result.StackId }
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Describe change set
|
|
533
|
+
*/
|
|
534
|
+
async describeChangeSet(stackName: string, changeSetName: string): Promise<any> {
|
|
535
|
+
const params: Record<string, any> = {
|
|
536
|
+
Action: 'DescribeChangeSet',
|
|
537
|
+
StackName: stackName,
|
|
538
|
+
ChangeSetName: changeSetName,
|
|
539
|
+
Version: '2010-05-15',
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
return await this.client.request({
|
|
543
|
+
service: 'cloudformation',
|
|
544
|
+
region: this.region,
|
|
545
|
+
method: 'POST',
|
|
546
|
+
path: '/',
|
|
547
|
+
body: new URLSearchParams(params).toString(),
|
|
548
|
+
})
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Execute change set
|
|
553
|
+
*/
|
|
554
|
+
async executeChangeSet(stackName: string, changeSetName: string): Promise<void> {
|
|
555
|
+
const params: Record<string, any> = {
|
|
556
|
+
Action: 'ExecuteChangeSet',
|
|
557
|
+
StackName: stackName,
|
|
558
|
+
ChangeSetName: changeSetName,
|
|
559
|
+
Version: '2010-05-15',
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
await this.client.request({
|
|
563
|
+
service: 'cloudformation',
|
|
564
|
+
region: this.region,
|
|
565
|
+
method: 'POST',
|
|
566
|
+
path: '/',
|
|
567
|
+
body: new URLSearchParams(params).toString(),
|
|
568
|
+
})
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Delete change set
|
|
573
|
+
*/
|
|
574
|
+
async deleteChangeSet(stackName: string, changeSetName: string): Promise<void> {
|
|
575
|
+
const params: Record<string, any> = {
|
|
576
|
+
Action: 'DeleteChangeSet',
|
|
577
|
+
StackName: stackName,
|
|
578
|
+
ChangeSetName: changeSetName,
|
|
579
|
+
Version: '2010-05-15',
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
await this.client.request({
|
|
583
|
+
service: 'cloudformation',
|
|
584
|
+
region: this.region,
|
|
585
|
+
method: 'POST',
|
|
586
|
+
path: '/',
|
|
587
|
+
body: new URLSearchParams(params).toString(),
|
|
588
|
+
})
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Get stack outputs as key-value pairs
|
|
593
|
+
*/
|
|
594
|
+
async getStackOutputs(stackName: string): Promise<Record<string, string>> {
|
|
595
|
+
const result = await this.describeStacks({ stackName })
|
|
596
|
+
|
|
597
|
+
if (!result.Stacks || result.Stacks.length === 0) {
|
|
598
|
+
throw new Error(`Stack ${stackName} not found`)
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const stack = result.Stacks[0]
|
|
602
|
+
const outputs: Record<string, string> = {}
|
|
603
|
+
|
|
604
|
+
if (stack.Outputs) {
|
|
605
|
+
for (const output of stack.Outputs) {
|
|
606
|
+
outputs[output.OutputKey] = output.OutputValue
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return outputs
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Get stack template
|
|
615
|
+
*/
|
|
616
|
+
async getTemplate(stackName: string): Promise<{ TemplateBody: string }> {
|
|
617
|
+
const params: Record<string, any> = {
|
|
618
|
+
Action: 'GetTemplate',
|
|
619
|
+
StackName: stackName,
|
|
620
|
+
Version: '2010-05-15',
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const result = await this.client.request({
|
|
624
|
+
service: 'cloudformation',
|
|
625
|
+
region: this.region,
|
|
626
|
+
method: 'POST',
|
|
627
|
+
path: '/',
|
|
628
|
+
body: new URLSearchParams(params).toString(),
|
|
629
|
+
})
|
|
630
|
+
|
|
631
|
+
// Handle the GetTemplateResult wrapper
|
|
632
|
+
const templateBody = result?.GetTemplateResult?.TemplateBody || result?.TemplateBody || ''
|
|
633
|
+
return { TemplateBody: templateBody }
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Parse stacks response
|
|
638
|
+
*/
|
|
639
|
+
private parseStacksResponse(result: any): Stack[] {
|
|
640
|
+
const stacks: Stack[] = []
|
|
641
|
+
|
|
642
|
+
// Handle different response structures from XML parsing
|
|
643
|
+
// The XML response is: DescribeStacksResponse > DescribeStacksResult > Stacks > member
|
|
644
|
+
let stackData = result?.DescribeStacksResult?.Stacks?.member
|
|
645
|
+
|| result?.Stacks?.member
|
|
646
|
+
|| result?.Stacks
|
|
647
|
+
|| result
|
|
648
|
+
|
|
649
|
+
// If it's a single stack, wrap in array
|
|
650
|
+
if (stackData && !Array.isArray(stackData)) {
|
|
651
|
+
stackData = [stackData]
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (Array.isArray(stackData)) {
|
|
655
|
+
for (const s of stackData) {
|
|
656
|
+
if (s.StackId || s.StackName) {
|
|
657
|
+
// Parse outputs
|
|
658
|
+
let outputs: Array<{ OutputKey: string, OutputValue: string, Description?: string, ExportName?: string }> | undefined
|
|
659
|
+
if (s.Outputs?.member) {
|
|
660
|
+
const outputData = Array.isArray(s.Outputs.member) ? s.Outputs.member : [s.Outputs.member]
|
|
661
|
+
outputs = outputData.map((o: any) => ({
|
|
662
|
+
OutputKey: o.OutputKey,
|
|
663
|
+
OutputValue: o.OutputValue,
|
|
664
|
+
Description: o.Description,
|
|
665
|
+
ExportName: o.ExportName,
|
|
666
|
+
}))
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
stacks.push({
|
|
670
|
+
StackId: s.StackId,
|
|
671
|
+
StackName: s.StackName,
|
|
672
|
+
StackStatus: s.StackStatus,
|
|
673
|
+
CreationTime: s.CreationTime,
|
|
674
|
+
LastUpdatedTime: s.LastUpdatedTime,
|
|
675
|
+
StackStatusReason: s.StackStatusReason,
|
|
676
|
+
Outputs: outputs,
|
|
677
|
+
})
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
// Fallback for flat response
|
|
682
|
+
else if (result.StackId) {
|
|
683
|
+
stacks.push({
|
|
684
|
+
StackId: result.StackId,
|
|
685
|
+
StackName: result.StackName,
|
|
686
|
+
StackStatus: result.StackStatus,
|
|
687
|
+
CreationTime: result.CreationTime,
|
|
688
|
+
LastUpdatedTime: result.LastUpdatedTime,
|
|
689
|
+
})
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
return stacks
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Parse stack events response
|
|
697
|
+
*/
|
|
698
|
+
private parseStackEvents(result: any): StackEvent[] {
|
|
699
|
+
const events: StackEvent[] = []
|
|
700
|
+
|
|
701
|
+
// Handle XML response structure: DescribeStackEventsResult > StackEvents > member
|
|
702
|
+
let eventData = result?.DescribeStackEventsResult?.StackEvents?.member
|
|
703
|
+
|| result?.StackEvents?.member
|
|
704
|
+
|| result?.StackEvents
|
|
705
|
+
|| []
|
|
706
|
+
|
|
707
|
+
// If single event, wrap in array
|
|
708
|
+
if (eventData && !Array.isArray(eventData)) {
|
|
709
|
+
eventData = [eventData]
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
for (const e of eventData) {
|
|
713
|
+
if (e.LogicalResourceId) {
|
|
714
|
+
events.push({
|
|
715
|
+
Timestamp: e.Timestamp,
|
|
716
|
+
ResourceType: e.ResourceType,
|
|
717
|
+
LogicalResourceId: e.LogicalResourceId,
|
|
718
|
+
ResourceStatus: e.ResourceStatus,
|
|
719
|
+
ResourceStatusReason: e.ResourceStatusReason,
|
|
720
|
+
})
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
return events
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Wait for stack with real-time progress output (CDK-style)
|
|
729
|
+
*/
|
|
730
|
+
async waitForStackWithProgress(
|
|
731
|
+
stackName: string,
|
|
732
|
+
waitType: 'stack-create-complete' | 'stack-update-complete' | 'stack-delete-complete',
|
|
733
|
+
onProgress?: (event: {
|
|
734
|
+
resourceId: string
|
|
735
|
+
resourceType: string
|
|
736
|
+
status: string
|
|
737
|
+
reason?: string
|
|
738
|
+
timestamp: string
|
|
739
|
+
}) => void,
|
|
740
|
+
): Promise<void> {
|
|
741
|
+
const targetStatuses = {
|
|
742
|
+
'stack-create-complete': ['CREATE_COMPLETE'],
|
|
743
|
+
'stack-update-complete': ['UPDATE_COMPLETE'],
|
|
744
|
+
'stack-delete-complete': ['DELETE_COMPLETE'],
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const failureStatuses = [
|
|
748
|
+
'CREATE_FAILED',
|
|
749
|
+
'ROLLBACK_FAILED',
|
|
750
|
+
'ROLLBACK_COMPLETE',
|
|
751
|
+
'UPDATE_ROLLBACK_FAILED',
|
|
752
|
+
'UPDATE_ROLLBACK_COMPLETE',
|
|
753
|
+
]
|
|
754
|
+
|
|
755
|
+
const targets = targetStatuses[waitType]
|
|
756
|
+
const maxAttempts = 360 // 30 minutes (DNS records can take 10-30 minutes)
|
|
757
|
+
let attempts = 0
|
|
758
|
+
const seenEvents = new Set<string>()
|
|
759
|
+
|
|
760
|
+
while (attempts < maxAttempts) {
|
|
761
|
+
try {
|
|
762
|
+
// Get stack events for progress
|
|
763
|
+
if (onProgress) {
|
|
764
|
+
try {
|
|
765
|
+
const eventsResult = await this.describeStackEvents(stackName)
|
|
766
|
+
// Events are returned newest first, reverse for chronological order
|
|
767
|
+
const events = [...(eventsResult.StackEvents || [])].reverse()
|
|
768
|
+
|
|
769
|
+
for (const event of events) {
|
|
770
|
+
// Create unique key for this event
|
|
771
|
+
const eventKey = `${event.LogicalResourceId}-${event.ResourceStatus}-${event.Timestamp}`
|
|
772
|
+
|
|
773
|
+
if (!seenEvents.has(eventKey)) {
|
|
774
|
+
seenEvents.add(eventKey)
|
|
775
|
+
onProgress({
|
|
776
|
+
resourceId: event.LogicalResourceId,
|
|
777
|
+
resourceType: event.ResourceType,
|
|
778
|
+
status: event.ResourceStatus,
|
|
779
|
+
reason: event.ResourceStatusReason,
|
|
780
|
+
timestamp: event.Timestamp,
|
|
781
|
+
})
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
catch {
|
|
786
|
+
// Events might not be available yet, continue
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// Check stack status
|
|
791
|
+
const result = await this.describeStacks({ stackName })
|
|
792
|
+
|
|
793
|
+
if (result.Stacks.length === 0) {
|
|
794
|
+
if (waitType === 'stack-delete-complete') {
|
|
795
|
+
return // Stack deleted successfully
|
|
796
|
+
}
|
|
797
|
+
await new Promise(resolve => setTimeout(resolve, 2000))
|
|
798
|
+
attempts++
|
|
799
|
+
continue
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
const stack = result.Stacks[0]
|
|
803
|
+
|
|
804
|
+
if (targets.includes(stack.StackStatus)) {
|
|
805
|
+
return // Target status reached
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
if (stack.StackStatus === 'DELETE_FAILED' && waitType === 'stack-delete-complete') {
|
|
809
|
+
const error: any = new Error(`Stack deletion failed - may have resources that need to be retained`)
|
|
810
|
+
error.code = 'DELETE_FAILED'
|
|
811
|
+
error.stackStatus = stack.StackStatus
|
|
812
|
+
error.statusReason = stack.StackStatusReason
|
|
813
|
+
throw error
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
if (failureStatuses.includes(stack.StackStatus)) {
|
|
817
|
+
throw new Error(`Stack reached failure status: ${stack.StackStatus}`)
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
await new Promise(resolve => setTimeout(resolve, 3000))
|
|
821
|
+
attempts++
|
|
822
|
+
}
|
|
823
|
+
catch (error: any) {
|
|
824
|
+
if (waitType === 'stack-delete-complete' && error.message?.includes('does not exist')) {
|
|
825
|
+
return
|
|
826
|
+
}
|
|
827
|
+
if (waitType === 'stack-create-complete' && error.message?.includes('does not exist')) {
|
|
828
|
+
await new Promise(resolve => setTimeout(resolve, 2000))
|
|
829
|
+
attempts++
|
|
830
|
+
continue
|
|
831
|
+
}
|
|
832
|
+
throw error
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
throw new Error(`Timeout waiting for stack to reach ${waitType}`)
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* Wait for stack operation to complete (create, update, or delete)
|
|
841
|
+
* Returns a result object with success status
|
|
842
|
+
*/
|
|
843
|
+
async waitForStackComplete(
|
|
844
|
+
stackName: string,
|
|
845
|
+
maxAttempts: number = 120,
|
|
846
|
+
delayMs: number = 5000,
|
|
847
|
+
): Promise<{ success: boolean; status: string; reason?: string }> {
|
|
848
|
+
const successStatuses = [
|
|
849
|
+
'CREATE_COMPLETE',
|
|
850
|
+
'UPDATE_COMPLETE',
|
|
851
|
+
'DELETE_COMPLETE',
|
|
852
|
+
]
|
|
853
|
+
const failureStatuses = [
|
|
854
|
+
'CREATE_FAILED',
|
|
855
|
+
'UPDATE_FAILED',
|
|
856
|
+
'DELETE_FAILED',
|
|
857
|
+
'ROLLBACK_COMPLETE',
|
|
858
|
+
'ROLLBACK_FAILED',
|
|
859
|
+
'UPDATE_ROLLBACK_COMPLETE',
|
|
860
|
+
'UPDATE_ROLLBACK_FAILED',
|
|
861
|
+
]
|
|
862
|
+
|
|
863
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
864
|
+
try {
|
|
865
|
+
const result = await this.describeStacks({ stackName })
|
|
866
|
+
|
|
867
|
+
if (result.Stacks.length === 0) {
|
|
868
|
+
// Stack was deleted
|
|
869
|
+
return { success: true, status: 'DELETE_COMPLETE' }
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
const stack = result.Stacks[0]
|
|
873
|
+
const status = stack.StackStatus
|
|
874
|
+
|
|
875
|
+
if (successStatuses.includes(status)) {
|
|
876
|
+
return { success: true, status }
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
if (failureStatuses.includes(status)) {
|
|
880
|
+
return { success: false, status, reason: stack.StackStatusReason }
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// Still in progress, wait and retry
|
|
884
|
+
await new Promise(resolve => setTimeout(resolve, delayMs))
|
|
885
|
+
}
|
|
886
|
+
catch (error: any) {
|
|
887
|
+
if (error.message?.includes('does not exist')) {
|
|
888
|
+
return { success: true, status: 'DELETE_COMPLETE' }
|
|
889
|
+
}
|
|
890
|
+
throw error
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
return { success: false, status: 'TIMEOUT', reason: 'Timeout waiting for stack operation' }
|
|
895
|
+
}
|
|
896
|
+
}
|