readysite / pkg / platform / providers / gcp / volume.go
4.0 KB
volume.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"
)

// CreateVolume creates a GCP persistent disk
func (b *backend) CreateVolume(name string, sizeGB int, region platform.Region) (*platform.Volume, error) {
	zone, ok := regions[region]
	if !ok {
		return nil, fmt.Errorf("%w: %s", platform.ErrUnsupportedRegion, region)
	}

	req := &computepb.InsertDiskRequest{
		Project: b.project,
		Zone:    zone,
		DiskResource: &computepb.Disk{
			Name:   proto.String(name),
			SizeGb: proto.Int64(int64(sizeGB)),
			Type:   proto.String(fmt.Sprintf("zones/%s/diskTypes/pd-ssd", zone)),
		},
	}

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

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

	return &platform.Volume{
		ID:     fmt.Sprintf("%s/%s", zone, name),
		Name:   name,
		Size:   sizeGB,
		Region: zone,
	}, nil
}

// GetVolume retrieves a GCP persistent disk by name
func (b *backend) GetVolume(name string) (*platform.Volume, error) {
	// Search across all zones
	req := &computepb.AggregatedListDisksRequest{
		Project: b.project,
		Filter:  proto.String(fmt.Sprintf("name=%s", name)),
	}

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

		for _, disk := range pair.Value.Disks {
			if disk.GetName() == name {
				serverID := ""
				if len(disk.Users) > 0 {
					// Users contains full URL to instance
					serverID = disk.Users[0]
				}
				zone := extractZone(disk.GetZone())
				return &platform.Volume{
					ID:       fmt.Sprintf("%s/%s", zone, name),
					Name:     name,
					Size:     int(disk.GetSizeGb()),
					Region:   zone,
					ServerID: serverID,
				}, nil
			}
		}
	}

	return nil, platform.ErrNotFound
}

// AttachVolume attaches a disk to an instance
func (b *backend) AttachVolume(volumeID, serverID string) error {
	// volumeID format: "zone/disk-name"
	volParts := strings.SplitN(volumeID, "/", 2)
	if len(volParts) != 2 {
		return fmt.Errorf("invalid volume ID format")
	}

	// serverID format: "zone/instance-name"
	serverParts := strings.SplitN(serverID, "/", 2)
	if len(serverParts) != 2 {
		return fmt.Errorf("invalid server ID format")
	}

	req := &computepb.AttachDiskInstanceRequest{
		Project:  b.project,
		Zone:     serverParts[0],
		Instance: serverParts[1],
		AttachedDiskResource: &computepb.AttachedDisk{
			Source: proto.String(fmt.Sprintf("projects/%s/zones/%s/disks/%s", b.project, volParts[0], volParts[1])),
		},
	}

	op, err := b.instances.AttachDisk(b.ctx, req)
	if err != nil {
		return fmt.Errorf("attach disk: %w", err)
	}

	return op.Wait(b.ctx)
}

// DetachVolume detaches a disk from an instance
func (b *backend) DetachVolume(volumeID string) error {
	// First get the volume to find attached instance
	parts := strings.SplitN(volumeID, "/", 2)
	if len(parts) != 2 {
		return fmt.Errorf("invalid volume ID format")
	}

	getReq := &computepb.GetDiskRequest{
		Project: b.project,
		Zone:    parts[0],
		Disk:    parts[1],
	}

	disk, err := b.disks.Get(b.ctx, getReq)
	if err != nil {
		return fmt.Errorf("get disk: %w", err)
	}

	if len(disk.Users) == 0 {
		return nil // Already detached
	}

	// Extract instance info from user URL
	instanceURL := disk.Users[0]
	// URL format: .../zones/ZONE/instances/NAME
	instanceParts := strings.Split(instanceURL, "/")
	if len(instanceParts) < 4 {
		return fmt.Errorf("invalid instance URL format")
	}
	zone := instanceParts[len(instanceParts)-3]
	instanceName := instanceParts[len(instanceParts)-1]

	detachReq := &computepb.DetachDiskInstanceRequest{
		Project:    b.project,
		Zone:       zone,
		Instance:   instanceName,
		DeviceName: parts[1],
	}

	op, err := b.instances.DetachDisk(b.ctx, detachReq)
	if err != nil {
		return fmt.Errorf("detach disk: %w", err)
	}

	return op.Wait(b.ctx)
}
← Back