@stacksjs/cloud 0.66.0 → 0.67.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/src/helpers.ts DELETED
@@ -1,800 +0,0 @@
1
- import type { DescribeLogGroupsCommandOutput } from '@aws-sdk/client-cloudwatch-logs'
2
- import type { CountryCode, RegisterDomainCommandOutput } from '@aws-sdk/client-route-53-domains'
3
- import { CloudFormation } from '@aws-sdk/client-cloudformation'
4
- import { CloudWatchLogsClient, DeleteLogGroupCommand, DescribeLogGroupsCommand } from '@aws-sdk/client-cloudwatch-logs'
5
- import {
6
- DeleteNetworkInterfaceCommand,
7
- DeleteSubnetCommand,
8
- DeleteVpcCommand,
9
- DescribeNetworkInterfacesCommand,
10
- DescribeRegionsCommand,
11
- DescribeSubnetsCommand,
12
- DescribeVpcsCommand,
13
- DetachNetworkInterfaceCommand,
14
- EC2,
15
- EC2Client,
16
- _InstanceType as InstanceType,
17
- TerminateInstancesCommand,
18
- } from '@aws-sdk/client-ec2'
19
- import { DescribeFileSystemsCommand, EFSClient } from '@aws-sdk/client-efs'
20
- import { IAM } from '@aws-sdk/client-iam'
21
- import { Lambda } from '@aws-sdk/client-lambda'
22
- import { ContactType, Route53Domains } from '@aws-sdk/client-route-53-domains'
23
- import { ListBucketsCommand, S3 } from '@aws-sdk/client-s3'
24
- import { SSM } from '@aws-sdk/client-ssm'
25
- import { config } from '@stacksjs/config'
26
- import { err, handleError, ok, type Result } from '@stacksjs/error-handling'
27
- import { log } from '@stacksjs/logging'
28
- import { path as p } from '@stacksjs/path'
29
- import { slug } from '@stacksjs/strings'
30
-
31
- const appEnv = config.app.env === 'local' ? 'dev' : config.app.env
32
- const cloudName = `stacks-cloud-${appEnv}`
33
-
34
- export { InstanceType }
35
-
36
- export async function getSecurityGroupId(securityGroupName: string): Promise<Result<string | undefined, string>> {
37
- const ec2 = new EC2({ region: 'us-east-1' })
38
- const { SecurityGroups } = await ec2.describeSecurityGroups({
39
- Filters: [{ Name: 'group-name', Values: [securityGroupName] }],
40
- })
41
-
42
- if (!SecurityGroups)
43
- return err(`Security group ${securityGroupName} not found`)
44
-
45
- if (SecurityGroups[0])
46
- return ok(SecurityGroups[0].GroupId)
47
-
48
- return err(`Security group ${securityGroupName} not found`)
49
- }
50
-
51
- export interface PurchaseOptions {
52
- domain: string
53
- years: number
54
- privacy: boolean
55
- autoRenew: boolean
56
- adminFirstName: string
57
- adminLastName: string
58
- adminOrganization: string
59
- adminAddressLine1: string
60
- adminAddressLine2: string
61
- adminCity: string
62
- adminState: string
63
- adminCountry: CountryCode
64
- adminZip: string
65
- adminPhone: string
66
- adminEmail: string
67
- techFirstName: string
68
- techLastName: string
69
- techOrganization: string
70
- techAddressLine1: string
71
- techAddressLine2: string
72
- techCity: string
73
- techState: string
74
- techCountry: CountryCode
75
- techZip: string
76
- techPhone: string
77
- techEmail: string
78
- registrantFirstName: string
79
- registrantLastName: string
80
- registrantOrganization: string
81
- registrantAddressLine1: string
82
- registrantAddressLine2: string
83
- registrantCity: string
84
- registrantState: string
85
- registrantCountry: CountryCode
86
- registrantZip: string
87
- registrantPhone: string
88
- registrantEmail: string
89
- privacyAdmin: boolean
90
- privacyTech: boolean
91
- privacyRegistrant: boolean
92
- contactType: ContactType
93
- verbose: boolean
94
- }
95
-
96
- export function purchaseDomain(
97
- domain: string,
98
- options: PurchaseOptions,
99
- ): Result<Promise<RegisterDomainCommandOutput>, Error> {
100
- const route53domains = new Route53Domains({ region: 'us-east-1' })
101
- const contactType = options.contactType.toUpperCase() as ContactType
102
-
103
- const params = {
104
- DomainName: domain,
105
- DurationInYears: options.years || 1,
106
- AutoRenew: options.autoRenew || true,
107
- AdminContact: {
108
- FirstName: options.adminFirstName,
109
- LastName: options.adminLastName,
110
- ContactType: contactType || ContactType.PERSON,
111
- OrganizationName: options.adminOrganization,
112
- AddressLine1: options.adminAddressLine1,
113
- AddressLine2: options.adminAddressLine2,
114
- City: options.adminCity,
115
- State: options.adminState,
116
- CountryCode: options.adminCountry,
117
- ZipCode: options.adminZip.toString(),
118
- PhoneNumber: options.adminPhone.toString().includes('+')
119
- ? options.adminPhone.toString()
120
- : `+${options.adminPhone.toString()}`,
121
- Email: options.adminEmail,
122
- },
123
- RegistrantContact: {
124
- FirstName: options.registrantFirstName,
125
- LastName: options.registrantLastName,
126
- ContactType: contactType || ContactType.PERSON,
127
- OrganizationName: options.registrantOrganization,
128
- AddressLine1: options.registrantAddressLine1,
129
- AddressLine2: options.registrantAddressLine2,
130
- City: options.registrantCity,
131
- State: options.registrantState,
132
- CountryCode: options.registrantCountry,
133
- ZipCode: options.registrantZip.toString(),
134
- PhoneNumber: options.registrantPhone.toString().includes('+')
135
- ? options.registrantPhone.toString()
136
- : `+${options.registrantPhone.toString()}`,
137
- Email: options.registrantEmail,
138
- },
139
- TechContact: {
140
- FirstName: options.techFirstName,
141
- LastName: options.techLastName,
142
- ContactType: contactType || ContactType.PERSON,
143
- OrganizationName: options.techOrganization,
144
- AddressLine1: options.techAddressLine1,
145
- AddressLine2: options.techAddressLine2,
146
- City: options.techCity,
147
- State: options.techState,
148
- CountryCode: options.techCountry,
149
- ZipCode: options.techZip.toString(),
150
- PhoneNumber: options.techPhone.toString().includes('+')
151
- ? options.techPhone.toString()
152
- : `+${options.techPhone.toString()}`,
153
- Email: options.techEmail,
154
- },
155
- PrivacyProtectAdminContact: options.privacyAdmin || options.privacy || true,
156
- PrivacyProtectRegistrantContact: options.privacyRegistrant || options.privacy || true,
157
- PrivacyProtectTechContact: options.privacyTech || options.privacy || true,
158
- }
159
-
160
- try {
161
- return ok(route53domains.registerDomain(params))
162
- }
163
- catch (error: any) {
164
- return err(error)
165
- }
166
- }
167
-
168
- export async function getJumpBoxInstanceId(name?: string): Promise<string | undefined> {
169
- if (!name)
170
- name = `${cloudName}/JumpBox`
171
-
172
- const ec2 = new EC2({ region: 'us-east-1' })
173
- const data = await ec2.describeInstances({
174
- Filters: [
175
- {
176
- Name: 'tag:Name',
177
- Values: [name],
178
- },
179
- ],
180
- })
181
-
182
- if (data.Reservations?.[0]?.Instances?.[0])
183
- return data.Reservations[0].Instances[0].InstanceId
184
-
185
- return undefined
186
- }
187
-
188
- export async function deleteEc2Instance(id: string, stackName?: string): Promise<Result<string, string>> {
189
- if (!stackName)
190
- stackName = cloudName
191
-
192
- if (!id)
193
- return err(`Instance ${id} not found`)
194
-
195
- const ec2 = new EC2({ region: 'us-east-1' })
196
- await ec2.terminateInstances({ InstanceIds: [id] })
197
-
198
- return ok(`Instance ${id} is being terminated`)
199
- }
200
-
201
- export async function deleteJumpBox(stackName?: string): Promise<Result<string, string>> {
202
- if (!stackName)
203
- stackName = cloudName
204
-
205
- const jumpBoxId = await getJumpBoxInstanceId()
206
-
207
- if (!jumpBoxId)
208
- return err('Jump-box not found')
209
-
210
- log.info(`Deleting jump-box ${jumpBoxId}...`)
211
-
212
- return await deleteEc2Instance(jumpBoxId, stackName)
213
- }
214
-
215
- export async function deleteIamUsers(): Promise<Result<string, string>> {
216
- const iam = new IAM({ region: 'us-east-1' })
217
- const data = await iam.listUsers({})
218
- const teamName = slug(config.team.name)
219
- const users
220
- = data.Users?.filter((user) => {
221
- const userNameLower = user.UserName?.toLowerCase()
222
- return (
223
- userNameLower !== 'stacks'
224
- && userNameLower !== teamName.toLowerCase()
225
- && userNameLower?.includes(teamName.toLowerCase())
226
- )
227
- }) || []
228
-
229
- if (!users || users.length === 0)
230
- return ok(`No Stacks IAM users found for team ${teamName}`)
231
-
232
- const promises = users.map(async (user) => {
233
- const userName = user.UserName || ''
234
-
235
- log.info(`Deleting IAM user: ${userName}`)
236
-
237
- // Get the list of policies attached to the user
238
- const policies = await iam.listAttachedUserPolicies({ UserName: userName })
239
-
240
- // Detach each policy
241
- await Promise.all(
242
- policies.AttachedPolicies?.map(policy =>
243
- iam.detachUserPolicy({
244
- UserName: userName,
245
- PolicyArn: policy.PolicyArn || '',
246
- }),
247
- ) || [],
248
- )
249
-
250
- // Get the list of access keys for the user
251
- const accessKeys = await iam.listAccessKeys({ UserName: userName })
252
-
253
- // Delete each access key
254
- await Promise.all(
255
- accessKeys.AccessKeyMetadata?.map(key =>
256
- iam.deleteAccessKey({
257
- UserName: userName,
258
- AccessKeyId: key.AccessKeyId || '',
259
- }),
260
- ) || [],
261
- )
262
-
263
- // Now delete the user
264
- return iam.deleteUser({ UserName: userName })
265
- })
266
-
267
- await Promise.all(promises).catch((error: Error) => {
268
- console.error(error)
269
- return err(handleError('Error deleting Stacks IAM users'))
270
- })
271
-
272
- return ok(`Stacks IAM users deleted for team ${teamName}`)
273
- }
274
-
275
- export async function deleteStacksBuckets(): Promise<Result<string, string | Error>> {
276
- try {
277
- const s3 = new S3({ region: 'us-east-1' })
278
- const data = await s3.listBuckets({})
279
- const stacksBuckets = data.Buckets?.filter(bucket => bucket.Name?.includes('stacks'))
280
-
281
- if (!stacksBuckets)
282
- return err('No stacks buckets found')
283
-
284
- const promises = stacksBuckets.map(async (bucket) => {
285
- const bucketName = bucket.Name || ''
286
-
287
- // Delete the bucket
288
- log.info(`Deleting bucket ${bucketName}...`)
289
-
290
- // List all objects in the bucket
291
- const objects = await s3.listObjectsV2({ Bucket: bucketName })
292
- log.info(`Finished listing bucket ${bucketName} objects`)
293
-
294
- // Delete all objects
295
- if (objects.Contents) {
296
- log.info('Deleting bucket objects...')
297
-
298
- await Promise.all(
299
- objects.Contents.map(object =>
300
- s3.deleteObject({ Bucket: bucketName, Key: object.Key || '' }).catch(error => handleError(error)),
301
- ),
302
- )
303
-
304
- log.info(`Finished deleting objects from bucket ${bucketName}`)
305
- }
306
-
307
- log.info(`Deleting bucket ${bucketName} versions...`)
308
- try {
309
- const versions = await s3.listObjectVersions({ Bucket: bucketName })
310
-
311
- if (versions.Versions) {
312
- await Promise.all(
313
- versions.Versions.map(version =>
314
- s3.deleteObject({
315
- Bucket: bucketName,
316
- Key: version.Key || '',
317
- VersionId: version.VersionId,
318
- }),
319
- ),
320
- ).catch(error => handleError(error))
321
- log.info(`Finished deleting versions from bucket ${bucketName}`)
322
- }
323
-
324
- // Delete all delete markers
325
- log.info(`Deleting bucket ${bucketName} delete markers...`)
326
-
327
- if (versions.DeleteMarkers) {
328
- await Promise.all(
329
- versions.DeleteMarkers.map(marker =>
330
- s3.deleteObject({
331
- Bucket: bucketName,
332
- Key: marker.Key || '',
333
- VersionId: marker.VersionId,
334
- }),
335
- ),
336
- ).catch(error => handleError(error))
337
-
338
- log.info(`Finished deleting delete markers from bucket ${bucketName}`)
339
- }
340
-
341
- // If the bucket has uncompleted multipart uploads, you need to abort them
342
- const uploads = await s3.listMultipartUploads({ Bucket: bucketName })
343
- if (uploads.Uploads) {
344
- log.info('Aborting bucket multipart uploads...')
345
-
346
- await Promise.all(
347
- uploads.Uploads.map(upload =>
348
- s3.abortMultipartUpload({
349
- Bucket: bucketName,
350
- Key: upload.Key || '',
351
- UploadId: upload.UploadId,
352
- }),
353
- ),
354
- ).catch(error => handleError(error))
355
-
356
- log.info(`Finished aborting multipart uploads from bucket ${bucketName}`)
357
- }
358
-
359
- await s3.deleteBucket({ Bucket: bucketName }).catch(error => handleError(error))
360
-
361
- log.info(`Bucket ${bucketName} deleted`)
362
- }
363
- catch (error) {
364
- log.info(`Error listing bucket ${bucketName} versions`, error)
365
- }
366
- })
367
-
368
- await Promise.all(promises).catch((error: Error) => {
369
- console.error(error)
370
- return err(handleError('Error deleting stacks buckets'))
371
- })
372
-
373
- return ok('Stacks buckets deleted')
374
- }
375
- catch (error) {
376
- return err(handleError('Error deleting stacks buckets', error))
377
- }
378
- }
379
-
380
- export async function deleteStacksFunctions(): Promise<Result<string, string>> {
381
- const lambda = new Lambda({ region: 'us-east-1' })
382
- const data = await lambda.listFunctions({})
383
- const stacksFunctions = data.Functions?.filter(func => func.FunctionName?.includes('stacks')) || []
384
-
385
- if (!stacksFunctions || stacksFunctions.length === 0)
386
- return ok('No stacks functions found')
387
-
388
- const promises = stacksFunctions.map(func => lambda.deleteFunction({ FunctionName: func.FunctionName || '' }))
389
-
390
- await Promise.all(promises).catch((error: Error) => {
391
- if (error.message.includes('it is a replicated function')) {
392
- log.info('Function is replicated, skipping...')
393
-
394
- return ok('CloudFront is still deleting the some functions. Try again later.')
395
- }
396
-
397
- return err(handleError('Error deleting stacks functions', error))
398
- })
399
-
400
- return ok('Stacks functions deleted')
401
- }
402
-
403
- export async function deleteLogGroups(): Promise<Result<string, Error>> {
404
- try {
405
- const ec2Client = new EC2Client({ region: 'us-east-1' })
406
- const { Regions } = await ec2Client.send(new DescribeRegionsCommand({}))
407
- const regions = Regions?.map(region => region.RegionName) || []
408
-
409
- for (const region of regions) {
410
- const client = new CloudWatchLogsClient({ region })
411
- const logGroups: DescribeLogGroupsCommandOutput = await client.send(new DescribeLogGroupsCommand({}))
412
-
413
- if (logGroups?.logGroups) {
414
- for (const group of logGroups.logGroups) {
415
- const appName = config.app.name?.toLocaleLowerCase() || 'stacks'
416
- if (group.logGroupName?.includes(appName))
417
- await client.send(new DeleteLogGroupCommand({ logGroupName: group.logGroupName }))
418
- }
419
- }
420
- }
421
-
422
- return ok('Log groups deleted in all regions')
423
- }
424
- catch (error) {
425
- return err(handleError('Error deleting log groups', error))
426
- }
427
- }
428
-
429
- export async function deleteParameterStore(): Promise<Result<string, string>> {
430
- const ssm = new SSM({ region: 'us-east-1' })
431
- const data = await ssm.describeParameters({})
432
-
433
- if (!data.Parameters)
434
- return ok('No parameters found')
435
-
436
- const appName = config.app.name?.toLocaleLowerCase() || 'stacks'
437
- const stacksParameters = data.Parameters.filter(param => param.Name?.includes(appName)) || []
438
-
439
- if (!stacksParameters || stacksParameters.length === 0)
440
- return ok('No stacks parameters found')
441
-
442
- const promises = stacksParameters.map(param => ssm.deleteParameter({ Name: param.Name || '' }))
443
-
444
- await Promise.all(promises).catch((error: Error) => {
445
- return err(handleError('Error deleting parameter store', error))
446
- })
447
-
448
- return ok('Parameter store deleted')
449
- }
450
-
451
- export async function deleteVpcs(): Promise<Result<string, Error>> {
452
- const ec2Client = new EC2Client({ region: 'us-east-1' })
453
- const vpcNamePattern = config.app.name ? `${config.app.name.toLowerCase()}-` : 'stacks-'
454
-
455
- try {
456
- // Describe all VPCs
457
- const describeVpcsCommand = new DescribeVpcsCommand({})
458
- const { Vpcs } = await ec2Client.send(describeVpcsCommand)
459
-
460
- if (!Vpcs || Vpcs.length === 0) {
461
- return ok('No VPCs found')
462
- }
463
-
464
- // Filter VPCs based on the name pattern
465
- const vpcsToDel = Vpcs.filter(vpc => vpc.Tags?.some(tag => tag.Key === 'Name' && tag.Value === vpcNamePattern))
466
-
467
- if (vpcsToDel.length === 0) {
468
- return ok(`No VPCs found matching the pattern: ${vpcNamePattern}`)
469
- }
470
-
471
- // Delete each matching VPC
472
- for (const vpc of vpcsToDel) {
473
- if (vpc.VpcId) {
474
- const deleteVpcCommand = new DeleteVpcCommand({ VpcId: vpc.VpcId })
475
- await ec2Client.send(deleteVpcCommand)
476
- log.info(`Deleted VPC: ${vpc.VpcId} (${vpcNamePattern})`)
477
- }
478
- }
479
-
480
- return ok(`Deleted ${vpcsToDel.length} VPCs matching the pattern: ${vpcNamePattern}`)
481
- }
482
- catch (error) {
483
- return err(handleError(`Error deleting VPCs: ${error}`))
484
- }
485
- }
486
-
487
- export async function deleteCdkRemnants(): Promise<Result<string, Error>> {
488
- try {
489
- await Bun.$`rm -rf ${p.cloudPath('cdk.out/')} ${p.cloudPath('cdk.context.json')}`.text()
490
- return ok('CDK remnants deleted')
491
- }
492
- catch (error) {
493
- return err(handleError('Error deleting CDK remnants', error))
494
- }
495
- }
496
-
497
- export async function deleteSubnets(): Promise<Result<string, Error>> {
498
- const ec2Client = new EC2Client({ region: 'us-east-1' })
499
- const subnetNamePattern = config.app.name ? `${config.app.name.toLowerCase()}-` : 'stacks-'
500
-
501
- try {
502
- // Describe all subnets
503
- const describeSubnetsCommand = new DescribeSubnetsCommand({})
504
- const { Subnets } = await ec2Client.send(describeSubnetsCommand)
505
-
506
- if (!Subnets || Subnets.length === 0) {
507
- return ok('No subnets found')
508
- }
509
-
510
- // Filter subnets based on the name pattern
511
- const subnetsToDel = Subnets.filter(subnet =>
512
- subnet.Tags?.some(tag => tag.Key === 'Name' && tag.Value?.startsWith(subnetNamePattern)),
513
- )
514
-
515
- if (subnetsToDel.length === 0) {
516
- return ok(`No subnets found matching the pattern: ${subnetNamePattern}`)
517
- }
518
-
519
- // Delete dependencies and subnets
520
- for (const subnet of subnetsToDel) {
521
- if (subnet.SubnetId) {
522
- // Describe network interfaces in the subnet
523
- const describeNIsCommand = new DescribeNetworkInterfacesCommand({
524
- Filters: [{ Name: 'subnet-id', Values: [subnet.SubnetId] }],
525
- })
526
- const { NetworkInterfaces } = await ec2Client.send(describeNIsCommand)
527
-
528
- // Delete network interfaces
529
- for (const ni of NetworkInterfaces || []) {
530
- if (ni.NetworkInterfaceId) {
531
- // If the network interface is attached to an instance, terminate the instance
532
- if (ni.Attachment?.InstanceId) {
533
- const terminateInstanceCommand = new TerminateInstancesCommand({
534
- InstanceIds: [ni.Attachment.InstanceId],
535
- })
536
- await ec2Client.send(terminateInstanceCommand)
537
- log.info(`Terminated instance: ${ni.Attachment.InstanceId}`)
538
-
539
- // Wait for the instance to terminate
540
- await new Promise(resolve => setTimeout(resolve, 60000)) // Wait for 60 seconds
541
- }
542
-
543
- // Detach the network interface if it's attached
544
- if (ni.Attachment?.AttachmentId) {
545
- const detachCommand = new DetachNetworkInterfaceCommand({
546
- AttachmentId: ni.Attachment.AttachmentId,
547
- Force: true,
548
- })
549
- await ec2Client.send(detachCommand)
550
- log.info(`Detached network interface: ${ni.NetworkInterfaceId}`)
551
-
552
- // Wait for the detachment to complete
553
- await new Promise(resolve => setTimeout(resolve, 10000)) // Wait for 10 seconds
554
- }
555
-
556
- // Delete the network interface
557
- const deleteNICommand = new DeleteNetworkInterfaceCommand({
558
- NetworkInterfaceId: ni.NetworkInterfaceId,
559
- })
560
- await ec2Client.send(deleteNICommand)
561
- log.info(`Deleted network interface: ${ni.NetworkInterfaceId}`)
562
- }
563
- }
564
-
565
- // Delete the subnet
566
- const deleteSubnetCommand = new DeleteSubnetCommand({ SubnetId: subnet.SubnetId })
567
- await ec2Client.send(deleteSubnetCommand)
568
- log.info(`Deleted subnet: ${subnet.SubnetId} (${subnet.Tags?.find(tag => tag.Key === 'Name')?.Value})`)
569
- }
570
- }
571
-
572
- return ok(`Deleted ${subnetsToDel.length} subnets matching the pattern: ${subnetNamePattern}`)
573
- }
574
- catch (error) {
575
- return err(handleError(`Error deleting subnets: ${error}`))
576
- }
577
- }
578
-
579
- export async function hasBeenDeployed(): Promise<Result<boolean, Error>> {
580
- const s3 = new S3({ region: 'us-east-1' })
581
-
582
- try {
583
- const response = await s3.send(new ListBucketsCommand({}))
584
-
585
- return ok(
586
- response.Buckets?.some(bucket => bucket.Name?.includes(config.app.name?.toLocaleLowerCase() || 'stacks'))
587
- || false,
588
- )
589
- }
590
- catch (error) {
591
- console.error(error)
592
- return err(handleError('Error checking if the app has been deployed'))
593
- }
594
- }
595
-
596
- export async function getJumpBoxInstanceProfileName(): Promise<Result<string | undefined, string>> {
597
- const iam = new IAM({ region: 'us-east-1' })
598
- const data = await iam.listInstanceProfiles({})
599
- const instanceProfile = data.InstanceProfiles?.find(profile => profile.InstanceProfileName?.includes('JumpBox'))
600
-
601
- if (!instanceProfile)
602
- return err('Jump-box IAM instance profile not found')
603
-
604
- return ok(instanceProfile?.InstanceProfileName)
605
- }
606
-
607
- export async function addJumpBox(stackName?: string): Promise<Result<string, string>> {
608
- if (!stackName)
609
- stackName = cloudName
610
-
611
- if (await getJumpBoxInstanceId()) {
612
- return err(
613
- 'The jump–box you are trying to add already exists. Please remove it & wait until it finished terminating.',
614
- )
615
- }
616
-
617
- const ec2 = new EC2({ region: 'us-east-1' })
618
- const r = await getJumpBoxSecurityGroupName()
619
-
620
- if (r.isErr())
621
- return err(r.error)
622
- if (!r.value)
623
- return err('Security group not found when adding jump-box')
624
-
625
- const result = await getSecurityGroupId(r.value)
626
- if (result.isErr())
627
- return err(result.error)
628
- const sgId = result.value
629
-
630
- if (!sgId)
631
- return err('Security group not found when adding jump-box')
632
-
633
- const client = new EFSClient({ region: 'us-east-1' })
634
- const command = new DescribeFileSystemsCommand({})
635
- const data = await client.send(command)
636
- const fileSystemName = `stacks-${config.app.env}-efs`
637
- const fileSystem = data.FileSystems?.find(fs => fs.Name === fileSystemName)
638
- const fileSystemId = fileSystem?.FileSystemId
639
-
640
- if (!fileSystem || !fileSystemId)
641
- return err(`EFS file system ${fileSystemName} not found`)
642
-
643
- const userDataScript = `
644
- #!/bin/bash
645
- yum update -y
646
- yum install -y amazon-efs-utils
647
- yum install -y git
648
- yum install -y https://s3.us-east-1.amazonaws.com/amazon-ssm-us-east-1/latest/linux_amd64/amazon-ssm-agent.rpm
649
- mkdir /mnt/efs
650
- mount -t efs ${fileSystemId}:/ /mnt/efs
651
- git clone https://github.com/stacksjs/stacks.git /mnt/efs
652
- `
653
-
654
- const base64UserData = btoa(userDataScript)
655
- const res = await getJumpBoxInstanceProfileName()
656
-
657
- if (res.isErr())
658
- return err(res.error)
659
-
660
- const jumpBoxInstanceProfileName: string | undefined = res.value
661
- if (!jumpBoxInstanceProfileName)
662
- return err('Jump-box IAM instance profile not found')
663
-
664
- const instance = await ec2.runInstances({
665
- ImageId: 'ami-03a6eaae9938c858c', // Amazon Linux 2023 AMI
666
- // ImageId: new ec2.AmazonLinuxImage(),
667
- InstanceType: InstanceType.t2_micro,
668
- MaxCount: 1,
669
- MinCount: 1,
670
- SecurityGroupIds: [sgId],
671
- SubnetId: 'subnet-004c5f196358b00f0',
672
- TagSpecifications: [
673
- {
674
- ResourceType: 'instance',
675
- Tags: [
676
- {
677
- Key: 'Name',
678
- Value: `${cloudName}-jump-box`,
679
- },
680
- ],
681
- },
682
- ],
683
- UserData: base64UserData,
684
- IamInstanceProfile: {
685
- Name: jumpBoxInstanceProfileName,
686
- },
687
- })
688
-
689
- return instance.Instances?.[0]
690
- ? ok(`Jump-box created with id ${instance.Instances[0].InstanceId}`)
691
- : err('Jump-box creation failed')
692
- }
693
-
694
- export async function getJumpBoxSecurityGroupName(): Promise<Result<string | undefined, string>> {
695
- const jumpBoxId = await getJumpBoxInstanceId()
696
-
697
- if (!jumpBoxId)
698
- return err('Jump-box not found')
699
-
700
- const ec2 = new EC2({ region: 'us-east-1' })
701
- const data = await ec2.describeInstances({ InstanceIds: [jumpBoxId] })
702
-
703
- if (data.Reservations?.[0]?.Instances?.[0]) {
704
- const instance = data.Reservations[0].Instances[0]
705
- const securityGroups = instance.SecurityGroups
706
-
707
- if (securityGroups?.[0])
708
- return ok(securityGroups[0].GroupName)
709
- }
710
-
711
- return err('Security group not found')
712
- }
713
-
714
- export async function getSecurityGroupFromInstanceId(instanceId: string): Promise<string | undefined> {
715
- const ec2 = new EC2({ region: 'us-east-1' })
716
- const data = await ec2.describeInstances({ InstanceIds: [instanceId] })
717
-
718
- if (data.Reservations?.[0]?.Instances?.[0]) {
719
- const instance = data.Reservations[0].Instances[0]
720
- const securityGroups = instance.SecurityGroups
721
-
722
- if (securityGroups?.[0])
723
- return securityGroups[0].GroupId // Returns the ID of the first security group
724
- }
725
-
726
- return undefined
727
- }
728
-
729
- export async function isFirstDeployment(): Promise<boolean> {
730
- const stackName = cloudName
731
- const cloudFormation = new CloudFormation()
732
- const data = await cloudFormation.listStacks({
733
- StackStatusFilter: ['CREATE_COMPLETE', 'UPDATE_COMPLETE'],
734
- })
735
- const isStacksCloudPresent = data.StackSummaries?.some(stack => stack.StackName === stackName)
736
-
737
- return !isStacksCloudPresent
738
- }
739
-
740
- export async function isFailedState(): Promise<boolean> {
741
- const cloudFormation = new CloudFormation()
742
- const data = await cloudFormation.listStacks({
743
- StackStatusFilter: ['CREATE_FAILED', 'UPDATE_FAILED', 'ROLLBACK_COMPLETE', 'UPDATE_ROLLBACK_COMPLETE'],
744
- })
745
- const isStacksCloudPresent = data.StackSummaries?.some(stack => stack.StackName === cloudName)
746
-
747
- return !isStacksCloudPresent
748
- }
749
-
750
- export async function getOrCreateTimestamp(): Promise<string> {
751
- const parameterName = `/stacks/timestamp`
752
- const ssm = new SSM({ region: 'us-east-1' })
753
-
754
- try {
755
- const response = await ssm.getParameter({ Name: parameterName })
756
- const timestamp = response.Parameter ? response.Parameter.Value : undefined
757
-
758
- if (!timestamp)
759
- throw new Error('Timestamp parameter not found')
760
-
761
- return timestamp
762
- }
763
- catch (error: any) {
764
- const timestamp = new Date().getTime().toString()
765
- log.debug(`Creating timestamp parameter ${parameterName} with value ${timestamp}`, error)
766
-
767
- await ssm.putParameter({
768
- Name: parameterName,
769
- Value: timestamp,
770
- Type: 'String',
771
- })
772
-
773
- return timestamp
774
- }
775
- }
776
-
777
- // get the CloudFront distribution ID of the current stack
778
- export async function getCloudFrontDistributionId(): Promise<string> {
779
- return ''
780
- // return await runCommand(`aws cloudfront list-distributions --query "DistributionList.Items[?Origins.Items[0].DomainName=='${config.app.url}'].Id"`)
781
- }
782
-
783
- // function isProductionEnv(env: string) {
784
- // return env === 'production' || env === 'prod'
785
- // }
786
-
787
- // export async function getExistingBucketNameByPrefix(prefix: string): Promise<string | undefined | null> {
788
- // const s3 = new S3({ region: 'us-east-1' })
789
-
790
- // try {
791
- // const response = await s3.send(new ListBucketsCommand({}))
792
- // const bucket = response.Buckets?.find(bucket => bucket.Name?.startsWith(prefix))
793
-
794
- // return bucket ? bucket.Name : null
795
- // }
796
- // catch (error) {
797
- // console.error('Error fetching buckets', error)
798
- // return `${prefix}-${timestamp}`
799
- // }
800
- // }