@stacksjs/cloud 0.58.48 → 0.58.49
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/package.json +3 -2
- package/src/cloud/ai.ts +94 -0
- package/src/cloud/aws-sdk-layer/nodejs/package-lock.json +386 -0
- package/src/cloud/aws-sdk-layer/nodejs/package.json +15 -0
- package/src/cloud/cache.ts +0 -0
- package/src/cloud/cdn.ts +374 -0
- package/src/cloud/cli.ts +45 -0
- package/src/cloud/compute.ts +80 -0
- package/src/cloud/dashboard.ts +85 -0
- package/src/cloud/database.ts +0 -0
- package/src/cloud/deployment.ts +41 -0
- package/src/cloud/dns.ts +34 -0
- package/src/cloud/docs.ts +51 -0
- package/src/cloud/email.ts +339 -0
- package/src/cloud/file-system.ts +35 -0
- package/src/cloud/index.ts +99 -0
- package/src/cloud/jump-box.ts +48 -0
- package/src/cloud/lambda/ask/index.js +35 -0
- package/src/cloud/lambda/cli-setup/index.js +67 -0
- package/src/cloud/lambda/summarize/index.js +35 -0
- package/src/cloud/network.ts +52 -0
- package/src/cloud/package/README.md +59 -0
- package/src/cloud/package/package.json +63 -0
- package/src/cloud/permissions.ts +33 -0
- package/src/cloud/queue.ts +0 -0
- package/src/cloud/redirects.ts +45 -0
- package/src/cloud/router-layer/nodejs/package.json +15 -0
- package/src/cloud/search-engine.ts +108 -0
- package/src/cloud/security.ts +259 -0
- package/src/cloud/stacksjs-router-0.58.48.tgz +0 -0
- package/src/cloud/storage.ts +168 -0
- package/src/edge/origin-request.ts +49 -0
- package/src/helpers.ts +597 -0
- package/src/index.ts +3 -0
- package/src/runtime/README.md +116 -0
- package/src/runtime/bootstrap +3 -0
- package/src/runtime/example/lambda.ts +36 -0
- package/src/runtime/runtime.ts +830 -0
- package/src/runtime/scripts/build-layer.ts +104 -0
- package/src/runtime/scripts/publish-layer.ts +110 -0
- package/src/runtime/server.ts +39 -0
- package/src/types.ts +28 -0
package/src/helpers.ts
ADDED
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
import { CloudFormation } from '@aws-sdk/client-cloudformation'
|
|
2
|
+
import type { DescribeLogGroupsCommandOutput } from '@aws-sdk/client-cloudwatch-logs'
|
|
3
|
+
import { CloudWatchLogsClient, DeleteLogGroupCommand, DescribeLogGroupsCommand } from '@aws-sdk/client-cloudwatch-logs'
|
|
4
|
+
import { EC2, _InstanceType as InstanceType } from '@aws-sdk/client-ec2'
|
|
5
|
+
import { DescribeFileSystemsCommand, EFSClient } from '@aws-sdk/client-efs'
|
|
6
|
+
import { IAM } from '@aws-sdk/client-iam'
|
|
7
|
+
import { SSM } from '@aws-sdk/client-ssm'
|
|
8
|
+
import { Lambda } from '@aws-sdk/client-lambda'
|
|
9
|
+
import type { CountryCode } from '@aws-sdk/client-route-53-domains'
|
|
10
|
+
import { ContactType, Route53Domains } from '@aws-sdk/client-route-53-domains'
|
|
11
|
+
import { ListBucketsCommand, S3 } from '@aws-sdk/client-s3'
|
|
12
|
+
import { config } from '@stacksjs/config'
|
|
13
|
+
import { err, handleError, ok } from '@stacksjs/error-handling'
|
|
14
|
+
import { log } from '@stacksjs/logging'
|
|
15
|
+
import { path as p } from '@stacksjs/path'
|
|
16
|
+
import { rimraf } from '@stacksjs/utils'
|
|
17
|
+
import { slug } from '@stacksjs/strings'
|
|
18
|
+
|
|
19
|
+
const appEnv = config.app.env === 'local' ? 'dev' : config.app.env
|
|
20
|
+
const cloudName = `stacks-cloud-${appEnv}`
|
|
21
|
+
|
|
22
|
+
export { InstanceType }
|
|
23
|
+
|
|
24
|
+
export async function getSecurityGroupId(securityGroupName: string) {
|
|
25
|
+
const ec2 = new EC2({ region: 'us-east-1' })
|
|
26
|
+
const { SecurityGroups } = await ec2.describeSecurityGroups({
|
|
27
|
+
Filters: [{ Name: 'group-name', Values: [securityGroupName] }],
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
if (!SecurityGroups)
|
|
31
|
+
return err(`Security group ${securityGroupName} not found`)
|
|
32
|
+
|
|
33
|
+
if (SecurityGroups[0])
|
|
34
|
+
return ok(SecurityGroups[0].GroupId)
|
|
35
|
+
|
|
36
|
+
return err(`Security group ${securityGroupName} not found`)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface PurchaseOptions {
|
|
40
|
+
domain: string
|
|
41
|
+
years: number
|
|
42
|
+
privacy: boolean
|
|
43
|
+
autoRenew: boolean
|
|
44
|
+
adminFirstName: string
|
|
45
|
+
adminLastName: string
|
|
46
|
+
adminOrganization: string
|
|
47
|
+
adminAddressLine1: string
|
|
48
|
+
adminAddressLine2: string
|
|
49
|
+
adminCity: string
|
|
50
|
+
adminState: string
|
|
51
|
+
adminCountry: CountryCode
|
|
52
|
+
adminZip: string
|
|
53
|
+
adminPhone: string
|
|
54
|
+
adminEmail: string
|
|
55
|
+
techFirstName: string
|
|
56
|
+
techLastName: string
|
|
57
|
+
techOrganization: string
|
|
58
|
+
techAddressLine1: string
|
|
59
|
+
techAddressLine2: string
|
|
60
|
+
techCity: string
|
|
61
|
+
techState: string
|
|
62
|
+
techCountry: CountryCode
|
|
63
|
+
techZip: string
|
|
64
|
+
techPhone: string
|
|
65
|
+
techEmail: string
|
|
66
|
+
registrantFirstName: string
|
|
67
|
+
registrantLastName: string
|
|
68
|
+
registrantOrganization: string
|
|
69
|
+
registrantAddressLine1: string
|
|
70
|
+
registrantAddressLine2: string
|
|
71
|
+
registrantCity: string
|
|
72
|
+
registrantState: string
|
|
73
|
+
registrantCountry: CountryCode
|
|
74
|
+
registrantZip: string
|
|
75
|
+
registrantPhone: string
|
|
76
|
+
registrantEmail: string
|
|
77
|
+
privacyAdmin: boolean
|
|
78
|
+
privacyTech: boolean
|
|
79
|
+
privacyRegistrant: boolean
|
|
80
|
+
contactType: ContactType
|
|
81
|
+
verbose: boolean
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function purchaseDomain(domain: string, options: PurchaseOptions) {
|
|
85
|
+
const route53domains = new Route53Domains({ region: 'us-east-1' })
|
|
86
|
+
const contactType = options.contactType.toUpperCase() as ContactType
|
|
87
|
+
|
|
88
|
+
const params = {
|
|
89
|
+
DomainName: domain,
|
|
90
|
+
DurationInYears: options.years || 1,
|
|
91
|
+
AutoRenew: options.autoRenew || true,
|
|
92
|
+
AdminContact: {
|
|
93
|
+
FirstName: options.adminFirstName,
|
|
94
|
+
LastName: options.adminLastName,
|
|
95
|
+
ContactType: contactType || ContactType.PERSON,
|
|
96
|
+
OrganizationName: options.adminOrganization,
|
|
97
|
+
AddressLine1: options.adminAddressLine1,
|
|
98
|
+
AddressLine2: options.adminAddressLine2,
|
|
99
|
+
City: options.adminCity,
|
|
100
|
+
State: options.adminState,
|
|
101
|
+
CountryCode: options.adminCountry,
|
|
102
|
+
ZipCode: options.adminZip.toString(),
|
|
103
|
+
PhoneNumber: options.adminPhone.toString().includes('+') ? options.adminPhone.toString() : `+${options.adminPhone.toString()}`,
|
|
104
|
+
Email: options.adminEmail,
|
|
105
|
+
},
|
|
106
|
+
RegistrantContact: {
|
|
107
|
+
FirstName: options.registrantFirstName,
|
|
108
|
+
LastName: options.registrantLastName,
|
|
109
|
+
ContactType: contactType || ContactType.PERSON,
|
|
110
|
+
OrganizationName: options.registrantOrganization,
|
|
111
|
+
AddressLine1: options.registrantAddressLine1,
|
|
112
|
+
AddressLine2: options.registrantAddressLine2,
|
|
113
|
+
City: options.registrantCity,
|
|
114
|
+
State: options.registrantState,
|
|
115
|
+
CountryCode: options.registrantCountry,
|
|
116
|
+
ZipCode: options.registrantZip.toString(),
|
|
117
|
+
PhoneNumber: options.registrantPhone.toString().includes('+') ? options.registrantPhone.toString() : `+${options.registrantPhone.toString()}`,
|
|
118
|
+
Email: options.registrantEmail,
|
|
119
|
+
},
|
|
120
|
+
TechContact: {
|
|
121
|
+
FirstName: options.techFirstName,
|
|
122
|
+
LastName: options.techLastName,
|
|
123
|
+
ContactType: contactType || ContactType.PERSON,
|
|
124
|
+
OrganizationName: options.techOrganization,
|
|
125
|
+
AddressLine1: options.techAddressLine1,
|
|
126
|
+
AddressLine2: options.techAddressLine2,
|
|
127
|
+
City: options.techCity,
|
|
128
|
+
State: options.techState,
|
|
129
|
+
CountryCode: options.techCountry,
|
|
130
|
+
ZipCode: options.techZip.toString(),
|
|
131
|
+
PhoneNumber: options.techPhone.toString().includes('+') ? options.techPhone.toString() : `+${options.techPhone.toString()}`,
|
|
132
|
+
Email: options.techEmail,
|
|
133
|
+
},
|
|
134
|
+
PrivacyProtectAdminContact: options.privacyAdmin || options.privacy || true,
|
|
135
|
+
PrivacyProtectRegistrantContact: options.privacyRegistrant || options.privacy || true,
|
|
136
|
+
PrivacyProtectTechContact: options.privacyTech || options.privacy || true,
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
return ok(route53domains.registerDomain(params))
|
|
141
|
+
}
|
|
142
|
+
catch (error: any) {
|
|
143
|
+
return err(error)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function getJumpBoxInstanceId(name?: string) {
|
|
148
|
+
if (!name)
|
|
149
|
+
name = `${cloudName}/JumpBox`
|
|
150
|
+
|
|
151
|
+
const ec2 = new EC2({ region: 'us-east-1' })
|
|
152
|
+
const data = await ec2.describeInstances({
|
|
153
|
+
Filters: [
|
|
154
|
+
{
|
|
155
|
+
Name: 'tag:Name',
|
|
156
|
+
Values: [name],
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
if (data.Reservations && data.Reservations[0] && data.Reservations[0].Instances && data.Reservations[0].Instances[0])
|
|
162
|
+
return data.Reservations[0].Instances[0].InstanceId
|
|
163
|
+
|
|
164
|
+
return undefined
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function deleteEc2Instance(id: string, stackName?: string) {
|
|
168
|
+
if (!stackName)
|
|
169
|
+
stackName = cloudName
|
|
170
|
+
|
|
171
|
+
if (!id)
|
|
172
|
+
return err(`Instance ${id} not found`)
|
|
173
|
+
|
|
174
|
+
const ec2 = new EC2({ region: 'us-east-1' })
|
|
175
|
+
await ec2.terminateInstances({ InstanceIds: [id] })
|
|
176
|
+
|
|
177
|
+
return ok(`Instance ${id} is being terminated`)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export async function deleteJumpBox(stackName?: string) {
|
|
181
|
+
if (!stackName)
|
|
182
|
+
stackName = cloudName
|
|
183
|
+
|
|
184
|
+
const jumpBoxId = await getJumpBoxInstanceId()
|
|
185
|
+
|
|
186
|
+
if (!jumpBoxId)
|
|
187
|
+
return err('Jump-box not found')
|
|
188
|
+
|
|
189
|
+
return await deleteEc2Instance(jumpBoxId, stackName)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export async function deleteIamUsers() {
|
|
193
|
+
const iam = new IAM({ region: 'us-east-1' })
|
|
194
|
+
const data = await iam.listUsers({})
|
|
195
|
+
const teamName = slug(config.team.name)
|
|
196
|
+
const users = data.Users?.filter((user) => {
|
|
197
|
+
const userNameLower = user.UserName?.toLowerCase()
|
|
198
|
+
return userNameLower !== 'stacks' && userNameLower !== teamName.toLowerCase() && userNameLower?.includes(teamName.toLowerCase())
|
|
199
|
+
}) || []
|
|
200
|
+
|
|
201
|
+
if (!users || users.length === 0)
|
|
202
|
+
return ok(`No Stacks IAM users found for team ${teamName}`)
|
|
203
|
+
|
|
204
|
+
const promises = users.map(async (user) => {
|
|
205
|
+
const userName = user.UserName || ''
|
|
206
|
+
log.info(`Deleting IAM user: ${userName}`)
|
|
207
|
+
|
|
208
|
+
// Get the list of policies attached to the user
|
|
209
|
+
const policies = await iam.listAttachedUserPolicies({ UserName: userName })
|
|
210
|
+
|
|
211
|
+
// Detach each policy
|
|
212
|
+
await Promise.all(policies.AttachedPolicies?.map(policy =>
|
|
213
|
+
iam.detachUserPolicy({ UserName: userName, PolicyArn: policy.PolicyArn || '' }),
|
|
214
|
+
) || [])
|
|
215
|
+
|
|
216
|
+
// Get the list of access keys for the user
|
|
217
|
+
const accessKeys = await iam.listAccessKeys({ UserName: userName })
|
|
218
|
+
|
|
219
|
+
// Delete each access key
|
|
220
|
+
await Promise.all(accessKeys.AccessKeyMetadata?.map(key =>
|
|
221
|
+
iam.deleteAccessKey({ UserName: userName, AccessKeyId: key.AccessKeyId || '' }),
|
|
222
|
+
) || [])
|
|
223
|
+
|
|
224
|
+
// Now delete the user
|
|
225
|
+
return iam.deleteUser({ UserName: userName })
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
await Promise.all(promises).catch((error: Error) => {
|
|
229
|
+
console.error(`Error deleting user: ${error}`)
|
|
230
|
+
return err(handleError('Error deleting Stacks IAM users', error))
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
return ok(`Stacks IAM users deleted for team ${teamName}`)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export async function deleteStacksBuckets() {
|
|
237
|
+
try {
|
|
238
|
+
const s3 = new S3({ region: 'us-east-1' })
|
|
239
|
+
const data = await s3.listBuckets({})
|
|
240
|
+
const stacksBuckets = data.Buckets?.filter(bucket => bucket.Name?.includes('stacks'))
|
|
241
|
+
|
|
242
|
+
if (!stacksBuckets)
|
|
243
|
+
return err('No stacks buckets found')
|
|
244
|
+
|
|
245
|
+
const promises = stacksBuckets.map(async (bucket) => {
|
|
246
|
+
const bucketName = bucket.Name || ''
|
|
247
|
+
|
|
248
|
+
// Delete the bucket
|
|
249
|
+
log.info(`Deleting bucket ${bucketName}...`)
|
|
250
|
+
|
|
251
|
+
// List all objects in the bucket
|
|
252
|
+
const objects = await s3.listObjectsV2({ Bucket: bucketName })
|
|
253
|
+
log.info(`Finished listing bucket ${bucketName} objects`)
|
|
254
|
+
// Delete all objects
|
|
255
|
+
if (objects.Contents) {
|
|
256
|
+
log.info('Deleting bucket objects...')
|
|
257
|
+
await Promise.all(objects.Contents.map(object =>
|
|
258
|
+
s3.deleteObject({ Bucket: bucketName, Key: object.Key || '' }).catch(error => handleError(error)),
|
|
259
|
+
))
|
|
260
|
+
log.info(`Finished deleting objects from bucket ${bucketName}`)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
log.info(`Deleting bucket ${bucketName} versions...`)
|
|
264
|
+
try {
|
|
265
|
+
const versions = await s3.listObjectVersions({ Bucket: bucketName })
|
|
266
|
+
|
|
267
|
+
if (versions.Versions) {
|
|
268
|
+
await Promise.all(versions.Versions.map(version =>
|
|
269
|
+
s3.deleteObject({ Bucket: bucketName, Key: version.Key || '', VersionId: version.VersionId }),
|
|
270
|
+
)).catch(error => handleError(error))
|
|
271
|
+
log.info(`Finished deleting versions from bucket ${bucketName}`)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Delete all delete markers
|
|
275
|
+
log.info(`Deleting bucket ${bucketName} delete markers...`)
|
|
276
|
+
if (versions.DeleteMarkers) {
|
|
277
|
+
await Promise.all(versions.DeleteMarkers.map(marker =>
|
|
278
|
+
s3.deleteObject({ Bucket: bucketName, Key: marker.Key || '', VersionId: marker.VersionId }),
|
|
279
|
+
)).catch(error => handleError(error))
|
|
280
|
+
log.info(`Finished deleting delete markers from bucket ${bucketName}`)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// If the bucket has uncompleted multipart uploads, you need to abort them
|
|
284
|
+
const uploads = await s3.listMultipartUploads({ Bucket: bucketName })
|
|
285
|
+
if (uploads.Uploads) {
|
|
286
|
+
log.info('Aborting bucket multipart uploads...')
|
|
287
|
+
await Promise.all(uploads.Uploads.map(upload =>
|
|
288
|
+
s3.abortMultipartUpload({ Bucket: bucketName, Key: upload.Key || '', UploadId: upload.UploadId }),
|
|
289
|
+
)).catch(error => handleError(error))
|
|
290
|
+
log.info(`Finished aborting multipart uploads from bucket ${bucketName}`)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
await s3.deleteBucket({ Bucket: bucketName }).catch(error => handleError(error))
|
|
294
|
+
log.info(`Bucket ${bucketName} deleted`)
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
log.info(`Error listing bucket ${bucketName} versions`, error)
|
|
298
|
+
}
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
await Promise.all(promises).catch((error: Error) => {
|
|
302
|
+
return err(handleError('Error deleting stacks buckets', error))
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
return ok('Stacks buckets deleted')
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
return err(handleError('Error deleting stacks buckets', error as Error))
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export async function deleteStacksFunctions() {
|
|
313
|
+
const lambda = new Lambda({ region: 'us-east-1' })
|
|
314
|
+
const data = await lambda.listFunctions({})
|
|
315
|
+
const stacksFunctions = data.Functions?.filter(func => func.FunctionName?.includes('stacks')) || []
|
|
316
|
+
|
|
317
|
+
if (!stacksFunctions || stacksFunctions.length === 0)
|
|
318
|
+
return ok('No stacks functions found')
|
|
319
|
+
|
|
320
|
+
const promises = stacksFunctions.map(func => lambda.deleteFunction({ FunctionName: func.FunctionName || '' }))
|
|
321
|
+
|
|
322
|
+
await Promise.all(promises).catch((error: Error) => {
|
|
323
|
+
if (error.message.includes('it is a replicated function')) {
|
|
324
|
+
log.info('Function is replicated, skipping...')
|
|
325
|
+
return ok('CloudFront is still deleting the some functions. Try again later.')
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return err(handleError('Error deleting stacks functions', error))
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
return ok('Stacks functions deleted')
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export async function deleteLogGroups() {
|
|
335
|
+
try {
|
|
336
|
+
const client = new CloudWatchLogsClient({ region: 'us-east-1' })
|
|
337
|
+
|
|
338
|
+
const logGroups: DescribeLogGroupsCommandOutput = await client.send(new DescribeLogGroupsCommand({}))
|
|
339
|
+
|
|
340
|
+
if (!logGroups?.logGroups)
|
|
341
|
+
return err('No log groups found')
|
|
342
|
+
|
|
343
|
+
for (const group of logGroups.logGroups) {
|
|
344
|
+
if (group.logGroupName?.includes('stacks'))
|
|
345
|
+
|
|
346
|
+
await client.send(new DeleteLogGroupCommand({ logGroupName: group.logGroupName }))
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return ok('Log groups deleted')
|
|
350
|
+
}
|
|
351
|
+
catch (error) {
|
|
352
|
+
return err(handleError('Error deleting log groups', error as Error))
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export async function deleteParameterStore() {
|
|
357
|
+
const ssm = new SSM({ region: 'us-east-1' })
|
|
358
|
+
const data = await ssm.describeParameters({})
|
|
359
|
+
|
|
360
|
+
if (!data.Parameters)
|
|
361
|
+
return ok('No parameters found')
|
|
362
|
+
|
|
363
|
+
const stacksParameters = data.Parameters.filter(param => param.Name?.includes('stacks')) || []
|
|
364
|
+
|
|
365
|
+
if (!stacksParameters || stacksParameters.length === 0)
|
|
366
|
+
return ok('No stacks parameters found')
|
|
367
|
+
|
|
368
|
+
const promises = stacksParameters.map(param => ssm.deleteParameter({ Name: param.Name || '' }))
|
|
369
|
+
|
|
370
|
+
await Promise.all(promises).catch((error: Error) => {
|
|
371
|
+
return err(handleError('Error deleting parameter store', error))
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
return ok('Parameter store deleted')
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export async function deleteCdkRemnants() {
|
|
378
|
+
try {
|
|
379
|
+
return ok(await rimraf([
|
|
380
|
+
p.cloudPath('cdk.out/'),
|
|
381
|
+
p.cloudPath('cdk.context.json'),
|
|
382
|
+
]))
|
|
383
|
+
}
|
|
384
|
+
catch (error) {
|
|
385
|
+
return err(handleError('Error deleting CDK remnants', error as Error))
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export async function hasBeenDeployed() {
|
|
390
|
+
const s3 = new S3({ region: 'us-east-1' })
|
|
391
|
+
|
|
392
|
+
try {
|
|
393
|
+
const response = await s3.send(new ListBucketsCommand({}))
|
|
394
|
+
return ok(response.Buckets?.some(bucket => bucket.Name?.includes(config.app.name?.toLocaleLowerCase() || 'stacks')) || false)
|
|
395
|
+
}
|
|
396
|
+
catch (error) {
|
|
397
|
+
return err(handleError('Error checking if the app has been deployed', error as Error))
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export async function getJumpBoxInstanceProfileName() {
|
|
402
|
+
const iam = new IAM({ region: 'us-east-1' })
|
|
403
|
+
const data = await iam.listInstanceProfiles({})
|
|
404
|
+
const instanceProfile = data.InstanceProfiles?.find(profile => profile.InstanceProfileName?.includes('JumpBox'))
|
|
405
|
+
|
|
406
|
+
if (!instanceProfile)
|
|
407
|
+
return err('Jump-box IAM instance profile not found')
|
|
408
|
+
|
|
409
|
+
return ok(instanceProfile?.InstanceProfileName)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export async function addJumpBox(stackName?: string) {
|
|
413
|
+
if (!stackName)
|
|
414
|
+
stackName = cloudName
|
|
415
|
+
|
|
416
|
+
if (await getJumpBoxInstanceId())
|
|
417
|
+
return err('The jump–box you are trying to add already exists. Please remove it & wait until it finished terminating.')
|
|
418
|
+
|
|
419
|
+
const ec2 = new EC2({ region: 'us-east-1' })
|
|
420
|
+
|
|
421
|
+
const r = await getJumpBoxSecurityGroupName()
|
|
422
|
+
|
|
423
|
+
if (r.isErr())
|
|
424
|
+
return err(r.error)
|
|
425
|
+
|
|
426
|
+
if (!r.value)
|
|
427
|
+
return err('Security group not found when adding jump-box')
|
|
428
|
+
|
|
429
|
+
const result = await getSecurityGroupId(r.value)
|
|
430
|
+
let sgId: string | undefined
|
|
431
|
+
|
|
432
|
+
if (result.isErr())
|
|
433
|
+
return err(result.error)
|
|
434
|
+
else
|
|
435
|
+
sgId = result.value
|
|
436
|
+
|
|
437
|
+
if (!sgId)
|
|
438
|
+
return err('Security group not found when adding jump-box')
|
|
439
|
+
|
|
440
|
+
const client = new EFSClient({ region: 'us-east-1' })
|
|
441
|
+
const command = new DescribeFileSystemsCommand({})
|
|
442
|
+
const data = await client.send(command)
|
|
443
|
+
|
|
444
|
+
const fileSystemName = `stacks-${config.app.env}-efs`
|
|
445
|
+
const fileSystem = data.FileSystems?.find(fs => fs.Name === fileSystemName)
|
|
446
|
+
const fileSystemId = fileSystem?.FileSystemId
|
|
447
|
+
|
|
448
|
+
if (!fileSystem || !fileSystemId)
|
|
449
|
+
return err(`EFS file system ${fileSystemName} not found`)
|
|
450
|
+
|
|
451
|
+
const userDataScript = `
|
|
452
|
+
#!/bin/bash
|
|
453
|
+
yum update -y
|
|
454
|
+
yum install -y amazon-efs-utils
|
|
455
|
+
yum install -y git
|
|
456
|
+
yum install -y https://s3.us-east-1.amazonaws.com/amazon-ssm-us-east-1/latest/linux_amd64/amazon-ssm-agent.rpm
|
|
457
|
+
mkdir /mnt/efs
|
|
458
|
+
mount -t efs ${fileSystemId}:/ /mnt/efs
|
|
459
|
+
git clone https://github.com/stacksjs/stacks.git /mnt/efs
|
|
460
|
+
`
|
|
461
|
+
|
|
462
|
+
const base64UserData = btoa(userDataScript)
|
|
463
|
+
const res = await getJumpBoxInstanceProfileName()
|
|
464
|
+
|
|
465
|
+
if (res.isErr())
|
|
466
|
+
return err(res.error)
|
|
467
|
+
|
|
468
|
+
const jumpBoxInstanceProfileName: string | undefined = res.value
|
|
469
|
+
if (!jumpBoxInstanceProfileName)
|
|
470
|
+
return err('Jump-box IAM instance profile not found')
|
|
471
|
+
|
|
472
|
+
const instance = await ec2.runInstances({
|
|
473
|
+
ImageId: 'ami-03a6eaae9938c858c', // Amazon Linux 2023 AMI
|
|
474
|
+
// ImageId: new ec2.AmazonLinuxImage(),
|
|
475
|
+
InstanceType: InstanceType.t2_micro,
|
|
476
|
+
MaxCount: 1,
|
|
477
|
+
MinCount: 1,
|
|
478
|
+
SecurityGroupIds: [sgId],
|
|
479
|
+
SubnetId: 'subnet-004c5f196358b00f0',
|
|
480
|
+
TagSpecifications: [
|
|
481
|
+
{
|
|
482
|
+
ResourceType: 'instance',
|
|
483
|
+
Tags: [
|
|
484
|
+
{
|
|
485
|
+
Key: 'Name',
|
|
486
|
+
Value: `${cloudName}-jump-box`,
|
|
487
|
+
},
|
|
488
|
+
],
|
|
489
|
+
},
|
|
490
|
+
],
|
|
491
|
+
UserData: base64UserData,
|
|
492
|
+
IamInstanceProfile: {
|
|
493
|
+
Name: jumpBoxInstanceProfileName,
|
|
494
|
+
},
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
return instance.Instances && instance.Instances[0]
|
|
498
|
+
? ok(`Jump-box created with id ${instance.Instances[0].InstanceId}`)
|
|
499
|
+
: err('Jump-box creation failed')
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
export async function getJumpBoxSecurityGroupName() {
|
|
503
|
+
const jumpBoxId = await getJumpBoxInstanceId()
|
|
504
|
+
|
|
505
|
+
if (!jumpBoxId)
|
|
506
|
+
return err('Jump-box not found')
|
|
507
|
+
|
|
508
|
+
const ec2 = new EC2({ region: 'us-east-1' })
|
|
509
|
+
const data = await ec2.describeInstances({ InstanceIds: [jumpBoxId] })
|
|
510
|
+
|
|
511
|
+
if (data.Reservations && data.Reservations[0] && data.Reservations[0].Instances && data.Reservations[0].Instances[0]) {
|
|
512
|
+
const instance = data.Reservations[0].Instances[0]
|
|
513
|
+
const securityGroups = instance.SecurityGroups
|
|
514
|
+
|
|
515
|
+
if (securityGroups && securityGroups[0])
|
|
516
|
+
return ok(securityGroups[0].GroupName)
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return err('Security group not found')
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
export async function getSecurityGroupFromInstanceId(instanceId: string) {
|
|
523
|
+
const ec2 = new EC2({ region: 'us-east-1' })
|
|
524
|
+
const data = await ec2.describeInstances({ InstanceIds: [instanceId] })
|
|
525
|
+
|
|
526
|
+
if (data.Reservations && data.Reservations[0] && data.Reservations[0].Instances && data.Reservations[0].Instances[0]) {
|
|
527
|
+
const instance = data.Reservations[0].Instances[0]
|
|
528
|
+
const securityGroups = instance.SecurityGroups
|
|
529
|
+
|
|
530
|
+
if (securityGroups && securityGroups[0])
|
|
531
|
+
return securityGroups[0].GroupId // Returns the ID of the first security group
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return undefined
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
export async function isFirstDeployment() {
|
|
538
|
+
const stackName = cloudName
|
|
539
|
+
const cloudFormation = new CloudFormation()
|
|
540
|
+
const data = await cloudFormation.listStacks({ StackStatusFilter: ['CREATE_COMPLETE', 'UPDATE_COMPLETE'] })
|
|
541
|
+
const isStacksCloudPresent = data.StackSummaries?.some(stack => stack.StackName === stackName)
|
|
542
|
+
|
|
543
|
+
return !isStacksCloudPresent
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
export async function isFailedState() {
|
|
547
|
+
const cloudFormation = new CloudFormation()
|
|
548
|
+
const data = await cloudFormation.listStacks({ StackStatusFilter: ['CREATE_FAILED', 'UPDATE_FAILED', 'ROLLBACK_COMPLETE', 'UPDATE_ROLLBACK_COMPLETE'] })
|
|
549
|
+
const isStacksCloudPresent = data.StackSummaries?.some(stack => stack.StackName === cloudName)
|
|
550
|
+
|
|
551
|
+
return !isStacksCloudPresent
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
export async function getOrCreateTimestamp(): Promise<string> {
|
|
555
|
+
const parameterName = `/stacks/timestamp`
|
|
556
|
+
const ssm = new SSM({ region: 'us-east-1' })
|
|
557
|
+
|
|
558
|
+
try {
|
|
559
|
+
const response = await ssm.getParameter({ Name: parameterName })
|
|
560
|
+
const timestamp = response.Parameter ? response.Parameter.Value : undefined
|
|
561
|
+
|
|
562
|
+
if (!timestamp)
|
|
563
|
+
throw new Error('Timestamp parameter not found')
|
|
564
|
+
|
|
565
|
+
return timestamp
|
|
566
|
+
}
|
|
567
|
+
catch (error: any) {
|
|
568
|
+
const timestamp = new Date().getTime().toString()
|
|
569
|
+
log.debug(`Creating timestamp parameter ${parameterName} with value ${timestamp}`)
|
|
570
|
+
await ssm.putParameter({
|
|
571
|
+
Name: parameterName,
|
|
572
|
+
Value: timestamp,
|
|
573
|
+
Type: 'String',
|
|
574
|
+
})
|
|
575
|
+
|
|
576
|
+
return timestamp
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// function isProductionEnv(env: string) {
|
|
581
|
+
// return env === 'production' || env === 'prod'
|
|
582
|
+
// }
|
|
583
|
+
|
|
584
|
+
// export async function getExistingBucketNameByPrefix(prefix: string): Promise<string | undefined | null> {
|
|
585
|
+
// const s3 = new S3({ region: 'us-east-1' })
|
|
586
|
+
|
|
587
|
+
// try {
|
|
588
|
+
// const response = await s3.send(new ListBucketsCommand({}))
|
|
589
|
+
// const bucket = response.Buckets?.find(bucket => bucket.Name?.startsWith(prefix))
|
|
590
|
+
|
|
591
|
+
// return bucket ? bucket.Name : null
|
|
592
|
+
// }
|
|
593
|
+
// catch (error) {
|
|
594
|
+
// console.error('Error fetching buckets', error)
|
|
595
|
+
// return `${prefix}-${timestamp}`
|
|
596
|
+
// }
|
|
597
|
+
// }
|
package/src/index.ts
ADDED