package providers import ( "context" "encoding/base64" "os" "github.com/wangjia/pangolin/server/internal/provision" ) // vultrAdapter implements provision.CloudAdapter against the Vultr API v2. // Consumable pool: cheap, hourly-billed, reserved-IP capable. // // Credentials: PROVISION_VULTR_API_KEY (env, independent secret). // Default OS image: PROVISION_VULTR_OS_ID (defaults to a current Debian image). type vultrAdapter struct { c *httpClient osID int } func newVultrFromEnv() (provision.CloudAdapter, error) { key, err := secret("PROVISION_VULTR_API_KEY") if err != nil { return nil, err } osID := atoiDefault(os.Getenv("PROVISION_VULTR_OS_ID"), 2136) // 2136 = Debian 12 x64 return &vultrAdapter{ c: newHTTPClient("https://api.vultr.com/v2", key), osID: osID, }, nil } func (a *vultrAdapter) Kind() string { return "vultr" } func (a *vultrAdapter) SupportsElasticIP() bool { return true } // reserved IPs func (a *vultrAdapter) CreateInstance(ctx context.Context, in provision.CreateInput) (*provision.Instance, error) { body := map[string]any{ "region": in.Region, "plan": in.Plan, "os_id": a.osID, "label": in.Label, "hostname": in.Label, "user_data": base64.StdEncoding.EncodeToString([]byte(in.UserData)), "tags": in.Tags, "backups": "disabled", } if len(in.SSHKeyIDs) > 0 { body["sshkey_id"] = in.SSHKeyIDs } var resp struct { Instance struct { ID string `json:"id"` MainIP string `json:"main_ip"` Region string `json:"region"` } `json:"instance"` } if err := a.c.do(ctx, "POST", "/instances", body, &resp); err != nil { return nil, err } return &provision.Instance{ ID: resp.Instance.ID, IP: resp.Instance.MainIP, Region: resp.Instance.Region, }, nil } func (a *vultrAdapter) DestroyInstance(ctx context.Context, instanceID string) error { err := a.c.do(ctx, "DELETE", "/instances/"+instanceID, nil, nil) if err != nil && isNotFound(err) { return nil // idempotent: already gone } return err } func (a *vultrAdapter) AttachIP(ctx context.Context, instanceID string) (string, error) { // Allocate a reserved IPv4, then attach it to the instance. var created struct { ReservedIP struct { ID string `json:"id"` Subnet string `json:"subnet"` } `json:"reserved_ip"` } if err := a.c.do(ctx, "POST", "/reserved-ips", map[string]any{ "region": "", "ip_type": "v4", "label": "rotate-" + instanceID, "instance_id": instanceID, }, &created); err != nil { return "", err } return created.ReservedIP.Subnet, nil } func (a *vultrAdapter) ListRegions(ctx context.Context) ([]provision.Region, error) { var resp struct { Regions []struct { ID string `json:"id"` Country string `json:"country"` City string `json:"city"` } `json:"regions"` } if err := a.c.do(ctx, "GET", "/regions", nil, &resp); err != nil { return nil, err } out := make([]provision.Region, 0, len(resp.Regions)) for _, r := range resp.Regions { out = append(out, provision.Region{ID: r.ID, Country: r.Country, City: r.City}) } return out, nil }