readysite / pkg / platform / providers / gcp / server.go
4.2 KB
server.go
package gcp

import (
	"fmt"
	"strings"

	computepb "cloud.google.com/go/compute/apiv1/computepb"
	"github.com/readysite/readysite/pkg/platform"
	"google.golang.org/api/iterator"
	"google.golang.org/protobuf/proto"
)

// CreateServer creates a GCP Compute Engine instance
func (b *backend) CreateServer(opts platform.ServerOptions) (*platform.Server, error) {
	// Translate region and size
	zone, ok := regions[opts.Region]
	if !ok {
		return nil, fmt.Errorf("%w: %s", platform.ErrUnsupportedRegion, opts.Region)
	}
	machineType, ok := sizes[opts.Size]
	if !ok {
		return nil, fmt.Errorf("%w: %s", platform.ErrUnsupportedSize, opts.Size)
	}

	// Build metadata for SSH key
	var metadata *computepb.Metadata
	if opts.SSHKey != "" {
		metadata = &computepb.Metadata{
			Items: []*computepb.Items{
				{
					Key:   proto.String("ssh-keys"),
					Value: proto.String("root:" + opts.SSHKey),
				},
			},
		}
	}

	// Build labels from tags
	labels := make(map[string]string)
	for _, t := range opts.Tags {
		parts := strings.SplitN(t, "=", 2)
		if len(parts) == 2 {
			labels[parts[0]] = parts[1]
		}
	}

	req := &computepb.InsertInstanceRequest{
		Project: b.project,
		Zone:    zone,
		InstanceResource: &computepb.Instance{
			Name:        proto.String(opts.Name),
			MachineType: proto.String(fmt.Sprintf("zones/%s/machineTypes/%s", zone, machineType)),
			Disks: []*computepb.AttachedDisk{
				{
					AutoDelete: proto.Bool(true),
					Boot:       proto.Bool(true),
					InitializeParams: &computepb.AttachedDiskInitializeParams{
						SourceImage: proto.String(opts.Image),
						DiskSizeGb:  proto.Int64(20),
					},
				},
			},
			NetworkInterfaces: []*computepb.NetworkInterface{
				{
					AccessConfigs: []*computepb.AccessConfig{
						{
							Name: proto.String("External NAT"),
							Type: proto.String("ONE_TO_ONE_NAT"),
						},
					},
				},
			},
			Metadata: metadata,
			Labels:   labels,
		},
	}

	op, err := b.instances.Insert(b.ctx, req)
	if err != nil {
		return nil, fmt.Errorf("insert instance: %w", err)
	}

	// Wait for operation to complete
	if err := op.Wait(b.ctx); err != nil {
		return nil, fmt.Errorf("wait for instance: %w", err)
	}

	// Get the created instance
	return b.GetServer(opts.Name)
}

// GetServer retrieves a GCP instance by name
func (b *backend) GetServer(name string) (*platform.Server, error) {
	// We need to search across all zones - use aggregated list
	req := &computepb.AggregatedListInstancesRequest{
		Project: b.project,
		Filter:  proto.String(fmt.Sprintf("name=%s", name)),
	}

	it := b.instances.AggregatedList(b.ctx, req)
	for {
		pair, err := it.Next()
		if err == iterator.Done {
			break
		}
		if err != nil {
			return nil, fmt.Errorf("list instances: %w", err)
		}

		for _, instance := range pair.Value.Instances {
			if instance.GetName() == name {
				return b.instanceToServer(instance), nil
			}
		}
	}

	return nil, platform.ErrNotFound
}

// DeleteServer terminates a GCP instance
func (b *backend) DeleteServer(id string) error {
	// ID format: "zone/instance-name"
	parts := strings.SplitN(id, "/", 2)
	if len(parts) != 2 {
		return fmt.Errorf("invalid instance ID format (expected zone/name)")
	}

	req := &computepb.DeleteInstanceRequest{
		Project:  b.project,
		Zone:     parts[0],
		Instance: parts[1],
	}

	op, err := b.instances.Delete(b.ctx, req)
	if err != nil {
		return fmt.Errorf("delete instance: %w", err)
	}

	return op.Wait(b.ctx)
}

// Helper methods

func (b *backend) instanceToServer(i *computepb.Instance) *platform.Server {
	ip := ""
	for _, iface := range i.NetworkInterfaces {
		for _, config := range iface.AccessConfigs {
			if config.NatIP != nil {
				ip = *config.NatIP
				break
			}
		}
	}

	zone := extractZone(i.GetZone())
	status := strings.ToLower(i.GetStatus())

	return &platform.Server{
		ID:     fmt.Sprintf("%s/%s", zone, i.GetName()),
		Name:   i.GetName(),
		IP:     ip,
		Size:   extractMachineType(i.GetMachineType()),
		Region: zone,
		Status: status,
	}
}

func extractZone(zoneURL string) string {
	// URL format: .../zones/ZONE
	parts := strings.Split(zoneURL, "/")
	if len(parts) > 0 {
		return parts[len(parts)-1]
	}
	return ""
}

func extractMachineType(mtURL string) string {
	// URL format: .../machineTypes/TYPE
	parts := strings.Split(mtURL, "/")
	if len(parts) > 0 {
		return parts[len(parts)-1]
	}
	return ""
}
← Back