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