server.go
package aws
import (
"crypto/md5"
"encoding/hex"
"fmt"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ec2"
"github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/readysite/readysite/pkg/platform"
)
// CreateServer creates an EC2 instance
func (b *backend) CreateServer(opts platform.ServerOptions) (*platform.Server, error) {
// Translate size
instanceType, ok := sizes[opts.Size]
if !ok {
return nil, fmt.Errorf("%w: %s", platform.ErrUnsupportedSize, opts.Size)
}
// Handle SSH key
var keyName *string
if opts.SSHKey != "" {
name, err := b.getOrCreateSSHKey(opts.SSHKey)
if err != nil {
return nil, fmt.Errorf("ssh key: %w", err)
}
keyName = &name
}
// Build tags
tags := []types.Tag{
{Key: aws.String("Name"), Value: aws.String(opts.Name)},
}
for _, t := range opts.Tags {
parts := splitTag(t)
if len(parts) == 2 {
tags = append(tags, types.Tag{Key: aws.String(parts[0]), Value: aws.String(parts[1])})
}
}
input := &ec2.RunInstancesInput{
ImageId: aws.String(opts.Image),
InstanceType: types.InstanceType(instanceType),
MinCount: aws.Int32(1),
MaxCount: aws.Int32(1),
KeyName: keyName,
TagSpecifications: []types.TagSpecification{
{
ResourceType: types.ResourceTypeInstance,
Tags: tags,
},
},
}
result, err := b.ec2.RunInstances(b.ctx, input)
if err != nil {
return nil, fmt.Errorf("run instances: %w", err)
}
if len(result.Instances) == 0 {
return nil, fmt.Errorf("no instances created")
}
instanceID := *result.Instances[0].InstanceId
// Wait for instance to be running
server, err := b.waitForInstance(instanceID)
if err != nil {
return nil, err
}
return server, nil
}
// GetServer retrieves an EC2 instance by name
func (b *backend) GetServer(name string) (*platform.Server, error) {
input := &ec2.DescribeInstancesInput{
Filters: []types.Filter{
{Name: aws.String("tag:Name"), Values: []string{name}},
{Name: aws.String("instance-state-name"), Values: []string{"pending", "running", "stopping", "stopped"}},
},
}
result, err := b.ec2.DescribeInstances(b.ctx, input)
if err != nil {
return nil, fmt.Errorf("describe instances: %w", err)
}
for _, reservation := range result.Reservations {
for _, instance := range reservation.Instances {
return b.instanceToServer(&instance), nil
}
}
return nil, platform.ErrNotFound
}
// DeleteServer terminates an EC2 instance
func (b *backend) DeleteServer(id string) error {
input := &ec2.TerminateInstancesInput{
InstanceIds: []string{id},
}
_, err := b.ec2.TerminateInstances(b.ctx, input)
if err != nil {
return fmt.Errorf("terminate instance: %w", err)
}
return nil
}
// Helper methods
func (b *backend) waitForInstance(instanceID string) (*platform.Server, error) {
timeout := time.After(5 * time.Minute)
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-timeout:
return nil, platform.ErrTimeout
case <-ticker.C:
input := &ec2.DescribeInstancesInput{
InstanceIds: []string{instanceID},
}
result, err := b.ec2.DescribeInstances(b.ctx, input)
if err != nil {
continue
}
for _, reservation := range result.Reservations {
for _, instance := range reservation.Instances {
if instance.State.Name == types.InstanceStateNameRunning {
return b.instanceToServer(&instance), nil
}
}
}
}
}
}
func (b *backend) instanceToServer(i *types.Instance) *platform.Server {
name := ""
for _, tag := range i.Tags {
if *tag.Key == "Name" {
name = *tag.Value
break
}
}
ip := ""
if i.PublicIpAddress != nil {
ip = *i.PublicIpAddress
}
region := ""
if i.Placement != nil && i.Placement.AvailabilityZone != nil {
region = *i.Placement.AvailabilityZone
}
return &platform.Server{
ID: *i.InstanceId,
Name: name,
IP: ip,
Size: string(i.InstanceType),
Region: region,
Status: string(i.State.Name),
}
}
func (b *backend) getOrCreateSSHKey(publicKey string) (string, error) {
// Generate a deterministic name from the key
hash := md5.Sum([]byte(publicKey))
keyName := "readysite-" + hex.EncodeToString(hash[:8])
// Check if key exists
describeInput := &ec2.DescribeKeyPairsInput{
KeyNames: []string{keyName},
}
_, err := b.ec2.DescribeKeyPairs(b.ctx, describeInput)
if err == nil {
return keyName, nil
}
// Import key
importInput := &ec2.ImportKeyPairInput{
KeyName: aws.String(keyName),
PublicKeyMaterial: []byte(publicKey),
}
_, err = b.ec2.ImportKeyPair(b.ctx, importInput)
if err != nil {
return "", fmt.Errorf("import key pair: %w", err)
}
return keyName, nil
}
func splitTag(t string) []string {
for i := 0; i < len(t); i++ {
if t[i] == '=' {
return []string{t[:i], t[i+1:]}
}
}
return nil
}