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)
}