diff --git a/client/src/app/shared/components/snapshot-confirmation-dialog/snapshot-confirmation-dialog.component.ts b/client/src/app/shared/components/snapshot-confirmation-dialog/snapshot-confirmation-dialog.component.ts index 07c11b8..a6ba801 100644 --- a/client/src/app/shared/components/snapshot-confirmation-dialog/snapshot-confirmation-dialog.component.ts +++ b/client/src/app/shared/components/snapshot-confirmation-dialog/snapshot-confirmation-dialog.component.ts @@ -30,10 +30,11 @@ export class SnapshotConfirmationDialogComponent { id: this.data.instanceElement.deploymentId, instanceId: this.data.instanceElement.ec2InstanceId, hostname: this.data.instanceElement.hostname, - region: Region.AP_SOUTHEAST_3, + region: this.data.instanceElement.availabilityZone, ami: this.data.instanceElement.ami, serverSize: this.data.instanceElement.serverSize, lifecycle: this.data.instanceElement.lifecycle, + timeToExpire: this.data.instanceElement.timeToExpire, }; this.apiService .captureInstanceSnapshopt(apiPayload) diff --git a/client/src/app/shared/model/deployment-request.ts b/client/src/app/shared/model/deployment-request.ts index 09fc15e..adfdf05 100644 --- a/client/src/app/shared/model/deployment-request.ts +++ b/client/src/app/shared/model/deployment-request.ts @@ -10,4 +10,5 @@ export class DeploymentApiRequest { lifecycle!: Lifecycle; ttlValue?: number; ttlUnit?: string; + timeToExpire?: number; } diff --git a/ecr-scripts/instance.tf b/ecr-scripts/instance.tf index e91d0ef..3a501c8 100644 --- a/ecr-scripts/instance.tf +++ b/ecr-scripts/instance.tf @@ -52,7 +52,7 @@ resource "aws_spot_instance_request" "my_deployed_spot_instances" { vpc_security_group_ids = local.use_custom_security_group ? [var.security_group_id] : null user_data = data.aws_s3_object.user_data.body tags = { - Name = "${each.value.hostname}.${data.aws_route53_zone.hosted_zone.name}" + Name = each.value.hostname Hostname = replace(each.value.hostname, "/.${data.aws_route53_zone.hosted_zone.name}/", "") DeploymentID = each.value.id TimeToExpire = each.value.timeToExpire diff --git a/server/db/db.go b/server/db/db.go index 0853d66..8865e85 100644 --- a/server/db/db.go +++ b/server/db/db.go @@ -122,20 +122,21 @@ func UpdateRecord(id string, updateData models.DynamoDBData) error { return ErrHostnameExists } + // there was a field for region, removed for now as region is static update := expression.Set( expression.Name("ami"), expression.Value(updateData.Ami), ).Set( expression.Name("serverSize"), expression.Value(updateData.ServerSize), ).Set( expression.Name("hostname"), expression.Value(updateData.Hostname), - ).Set( - expression.Name("region"), expression.Value(updateData.Region), ).Set( expression.Name("creationUser"), expression.Value(updateData.CreationUser), ).Set( expression.Name("lifecycle"), expression.Value(updateData.Lifecycle), ).Set( expression.Name("timeToExpire"), expression.Value(updateData.TimeToExpire), + ).Set( + expression.Name("snapShot"), expression.Value(updateData.SnapShot), ) // Build the update expression. diff --git a/server/handler.go b/server/handler.go index 556d06c..540176c 100644 --- a/server/handler.go +++ b/server/handler.go @@ -8,6 +8,7 @@ import ( "log" "net/http" "os" + "strconv" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-sdk-go-v2/aws" @@ -255,6 +256,13 @@ func GetAWSData(c *gin.Context) { // add region env config.Region = regionEnv + // get list of snapshot AMIs, and add to the AMI available + config.Ami, err = instance.GetAvailableAmis(config.Ami) + if err != nil { + log.Printf("Failed to get list of AMIs: %v", err) + return + } + c.JSON(http.StatusOK, config) } @@ -403,11 +411,16 @@ func CaptureInstanceSnapshot(c *gin.Context) { log.Println("update request for id:", id) var snapshotID string - if snapshotID, err = instance.CaptureInstanceSnapshot(req.InstanceID); err != nil { + if snapshotID, err = instance.CaptureInstanceImage(req.InstanceID); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } + timeToLive, err := strconv.ParseInt(req.TimeToExpire, 10, 64) + if err != nil { + log.Printf("Failed to parse ttl with error %v", err) + } + // Convert request to DynamoDBData struct data := models.DynamoDBData{ ID: id, @@ -419,6 +432,7 @@ func CaptureInstanceSnapshot(c *gin.Context) { Lifecycle: req.Lifecycle, SnapShot: snapshotID, ContentDeployment: req.ContentDeployment, + TimeToExpire: timeToLive, } // Update the DynamoDB row to include the captured snapshot ID diff --git a/server/instance/instance.go b/server/instance/instance.go index ec04cd9..e3b5c74 100644 --- a/server/instance/instance.go +++ b/server/instance/instance.go @@ -2,7 +2,9 @@ package instance import ( "context" + "fmt" "log" + "time" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" @@ -47,47 +49,37 @@ func GetDeployedInstances() ([]models.DeploymentResponse, error) { for _, reservation := range output.Reservations { for _, instance := range reservation.Instances { - // Retrieve volume IDs from the instance - var volumeIDs []string - for _, blockDevice := range instance.BlockDeviceMappings { - volumeIDs = append(volumeIDs, aws.ToString(blockDevice.Ebs.VolumeId)) + // Get image based on the instance + filter := []types.Filter{ + { + Name: aws.String("source-instance-id"), + Values: []string{*instance.InstanceId}, + }, + { + Name: aws.String("is-public"), + Values: []string{"false"}, + }, } - // Check for snapshots and get the latest snapshot ID for the volumes - snapshotID := "None" - var latestSnapshot *types.Snapshot - for _, volumeID := range volumeIDs { - snapshotInput := &ec2.DescribeSnapshotsInput{ - Filters: []types.Filter{ - { - Name: aws.String("volume-id"), - Values: []string{volumeID}, - }, - }, - } - snapshots, err := ec2Client.DescribeSnapshots(context.Background(), snapshotInput) - if err != nil { - log.Printf("Error retrieving snapshots for volume %s: %v", volumeID, err) - continue - } - for i := range snapshots.Snapshots { - snapshot := snapshots.Snapshots[i] - if latestSnapshot == nil || snapshot.StartTime.After(*latestSnapshot.StartTime) { - latestSnapshot = &snapshots.Snapshots[i] - } - } - + imageResult, err := getImage(filter) + if err != nil { + log.Printf("failed to resolve image for instance %s: %v", *instance.InstanceId, err) + return nil, err } - if latestSnapshot != nil { - snapshotID = aws.ToString(latestSnapshot.SnapshotId) + var imageID string + if len(imageResult.Images) == 0 { + imageID = "none" + } else { + imageID = *imageResult.Images[0].ImageId } + deployment := models.DeploymentResponse{ InstanceID: aws.ToString(instance.InstanceId), DeploymentID: getInstanceTagValue("DeploymentID", instance.Tags), Hostname: getInstanceTagValue("Name", instance.Tags), TimeToExpire: getInstanceTagValue("TimeToExpire", instance.Tags), - SnapshotID: snapshotID, + SnapshotID: imageID, Ami: aws.ToString(instance.ImageId), ServerSize: string(instance.InstanceType), AvailabilityZone: aws.ToString(instance.Placement.AvailabilityZone), @@ -149,28 +141,136 @@ func getLifecycle(lifecycle types.InstanceLifecycleType) string { return string(lifecycle) } -func CaptureInstanceSnapshot(instanceID string) (string, error) { - describeInstanceInput := &ec2.DescribeInstancesInput{ - InstanceIds: []string{instanceID}, +func CaptureInstanceImage(instanceID string) (string, error) { + // check if an image for that instance already exists + filter := []types.Filter{ + { + Name: aws.String("source-instance-id"), + Values: []string{instanceID}, + }, + { + Name: aws.String("is-public"), + Values: []string{"false"}, + }, } - instanceResult, err := ec2Client.DescribeInstances(context.Background(), describeInstanceInput) + + imageResult, err := getImage(filter) if err != nil { - log.Printf("failed to describe instance %s: %v", instanceID, err) + log.Printf("failed to resolve image for instance %s: %v", instanceID, err) return "", err } - // snapshot the first volume - volumeID := instanceResult.Reservations[0].Instances[0].BlockDeviceMappings[0].Ebs.VolumeId + // if it exists deregister it + if len(imageResult.Images) == 0 { + log.Printf("No images returned for deregistering") + } else { + describeDeregisterImage := &ec2.DeregisterImageInput{ + ImageId: imageResult.Images[0].ImageId, + } + log.Printf("The id of the image is %s, deregistering...", *imageResult.Images[0].ImageId) + _, err := ec2Client.DeregisterImage(context.Background(), describeDeregisterImage) + if err != nil { + log.Printf("failed to deregister image for instance %s: %v", instanceID, err) + return "", err + } + } + + // get tags of the instance + describeInstanceTags := &ec2.DescribeTagsInput{ + Filters: []types.Filter{ + { + Name: aws.String("resource-id"), + Values: []string{instanceID}, + }, + }, + } + + tagsResult, err := ec2Client.DescribeTags(context.Background(), describeInstanceTags) + if err != nil { + log.Printf("failed to describe tags for instance %s: %v", instanceID, err) + return "", err + } + + instanceName := "None" + for _, tags := range tagsResult.Tags { + if *tags.Key == "Name" { + instanceName = *tags.Value + } + } + + // get current time + now := time.Now() + date := now.Format(time.DateOnly) + time := fmt.Sprintf("%d%d%d", now.Hour(), now.Minute(), now.Second()) + formattedName := instanceName + "_" + date + "_" + time - snapshotInput := &ec2.CreateSnapshotInput{ - VolumeId: volumeID, + // snapshot the instance + imageInput := &ec2.CreateImageInput{ + InstanceId: aws.String(instanceID), + Name: aws.String(formattedName), + TagSpecifications: []types.TagSpecification{ + { + ResourceType: types.ResourceType("image"), + Tags: []types.Tag{ + { + Key: aws.String("DeployedBy"), + Value: aws.String("turbo-deploy"), + }, + }, + }, + }, } - result, err := ec2Client.CreateSnapshot(context.Background(), snapshotInput) + result, err := ec2Client.CreateImage(context.Background(), imageInput) if err != nil { - log.Printf("failed to create snapshot for instance %s: %v", instanceID, err) + log.Printf("failed to create image for instance %s: %v", instanceID, err) return "", err } - log.Printf("Snapshot for instance %s created successfully: %s", instanceID, aws.ToString(result.SnapshotId)) - return aws.ToString(result.SnapshotId), nil + log.Printf("Image for instance %s created successfully: %s", instanceID, aws.ToString(result.ImageId)) + return aws.ToString(result.ImageId), nil +} + +func GetAvailableAmis(amilist []string) ([]string, error) { + // check if an image for that instance already exists + filter := []types.Filter{ + { + Name: aws.String("is-public"), + Values: []string{"false"}, + }, + { + Name: aws.String("tag:DeployedBy"), + Values: []string{"turbo-deploy"}, + }, + } + + imageResult, err := getImage(filter) + if err != nil { + log.Printf("failed to retrieve images: %v", err) + } + + // if it exists grab it + if len(imageResult.Images) == 0 { + log.Printf("No images returned for extra listing") + } else { + for i := range imageResult.Images { + image := imageResult.Images[i] + log.Printf("Image %s found, appending to the list...", *image.ImageId) + amilist = append(amilist, *image.ImageId) + } + } + + return amilist, nil +} + +func getImage(filter []types.Filter) (*ec2.DescribeImagesOutput, error) { + describeInstanceImage := &ec2.DescribeImagesInput{ + Filters: filter, + } + + imageResult, err := ec2Client.DescribeImages(context.Background(), describeInstanceImage) + if err != nil { + return nil, err + } + + return imageResult, nil }