feat: provider interface + registry — factory pattern for multi-cloud

This commit is contained in:
2026-07-28 15:59:08 +08:00
parent 8c901099c3
commit fc48e5f4a3
3 changed files with 230 additions and 0 deletions
+132
View File
@@ -0,0 +1,132 @@
package provider
import (
"context"
"fmt"
"time"
)
type ProviderConfig struct {
APIKey string
AccessKeyID string
AccessKeySecret string
Endpoint string
}
type Provider interface {
Name() string
DisplayName() string
ValidateAuth(ctx context.Context) error
Server() ServerService
Domain() DomainService
Storage() StorageService
CDN() CDNService
Billing() BillingService
}
type ServerService interface {
List(ctx context.Context) ([]Server, error)
Get(ctx context.Context, id string) (*Server, error)
Start(ctx context.Context, id string) error
Stop(ctx context.Context, id string) error
Reboot(ctx context.Context, id string) error
Reinstall(ctx context.Context, id, osTemplate string) error
ResetPassword(ctx context.Context, id string) error
GetVNC(ctx context.Context, id string) (string, error)
Upgrade(ctx context.Context, id, plan string) error
}
type DomainService interface {
List(ctx context.Context) ([]Domain, error)
DNSList(ctx context.Context, domain string) ([]DNSRecord, error)
DNSAdd(ctx context.Context, domain string, record DNSRecord) error
DNSDelete(ctx context.Context, domain string, recordID string) error
}
type StorageService interface {
List(ctx context.Context) ([]StorageInstance, error)
BucketList(ctx context.Context, instanceID string) ([]Bucket, error)
BucketCreate(ctx context.Context, instanceID, name string) error
}
type CDNService interface {
List(ctx context.Context) ([]CDNInstance, error)
}
type BillingService interface {
Orders(ctx context.Context) ([]Order, error)
}
type Server struct {
ID string `json:"id" table:"ID"`
Name string `json:"name" table:"NAME"`
Status string `json:"status" table:"STATUS"`
IP string `json:"ip" table:"IP"`
CPU int `json:"cpu" table:"CPU"`
Memory int `json:"memory" table:"MEM"`
Disk int `json:"disk" table:"DISK"`
OS string `json:"os" table:"OS"`
Region string `json:"region" table:"REGION"`
ExpireAt time.Time `json:"expires" table:"EXPIRES"`
Raw any `json:"-"`
}
type Domain struct {
ID string `json:"id" table:"ID"`
Name string `json:"name" table:"DOMAIN"`
Status string `json:"status" table:"STATUS"`
ExpireAt time.Time `json:"expires" table:"EXPIRES"`
Raw any `json:"-"`
}
type DNSRecord struct {
ID string `json:"id" table:"ID"`
Type string `json:"type" table:"TYPE"`
Name string `json:"name" table:"NAME"`
Value string `json:"value" table:"VALUE"`
TTL int `json:"ttl" table:"TTL"`
}
type StorageInstance struct {
ID string `json:"id" table:"ID"`
Name string `json:"name" table:"NAME"`
Status string `json:"status" table:"STATUS"`
Raw any `json:"-"`
}
type Bucket struct {
ID string `json:"id" table:"ID"`
Name string `json:"name" table:"NAME"`
Region string `json:"region" table:"REGION"`
}
type CDNInstance struct {
ID string `json:"id" table:"ID"`
Domain string `json:"domain" table:"DOMAIN"`
Status string `json:"status" table:"STATUS"`
Raw any `json:"-"`
}
type Order struct {
ID string `json:"id" table:"ID"`
Product string `json:"product" table:"PRODUCT"`
Amount float64 `json:"amount" table:"AMOUNT"`
Status string `json:"status" table:"STATUS"`
Created time.Time `json:"created" table:"CREATED"`
Raw any `json:"-"`
}
type Error struct {
Code int
Message string
Detail string
Raw []byte
}
func (e *Error) Error() string {
return e.Message
}
func (e *Error) Debug() string {
return fmt.Sprintf("%s\n%s", e.Detail, string(e.Raw))
}
+41
View File
@@ -0,0 +1,41 @@
package provider
import (
"fmt"
"sort"
"sync"
)
type Factory func(cfg ProviderConfig) (Provider, error)
var (
registry = map[string]Factory{}
registryMu sync.RWMutex
)
func Register(name string, factory Factory) {
registryMu.Lock()
defer registryMu.Unlock()
registry[name] = factory
}
func New(name string, cfg ProviderConfig) (Provider, error) {
registryMu.RLock()
factory, ok := registry[name]
registryMu.RUnlock()
if !ok {
return nil, fmt.Errorf("unknown provider: %s (run 'ry provider add %s' first)", name, name)
}
return factory(cfg)
}
func List() []string {
registryMu.RLock()
defer registryMu.RUnlock()
names := make([]string, 0, len(registry))
for name := range registry {
names = append(names, name)
}
sort.Strings(names)
return names
}
+57
View File
@@ -0,0 +1,57 @@
package provider
import (
"context"
"testing"
)
type mockProvider struct {
name string
}
func (m *mockProvider) Name() string { return m.name }
func (m *mockProvider) DisplayName() string { return "Mock" }
func (m *mockProvider) ValidateAuth(ctx context.Context) error { return nil }
func (m *mockProvider) Server() ServerService { return nil }
func (m *mockProvider) Domain() DomainService { return nil }
func (m *mockProvider) Storage() StorageService { return nil }
func (m *mockProvider) CDN() CDNService { return nil }
func (m *mockProvider) Billing() BillingService { return nil }
func TestRegistryRegisterAndNew(t *testing.T) {
Register("mock", func(cfg ProviderConfig) (Provider, error) {
return &mockProvider{name: "mock"}, nil
})
p, err := New("mock", ProviderConfig{})
if err != nil {
t.Fatalf("New() error: %v", err)
}
if p.Name() != "mock" {
t.Errorf("Name() = %q, want %q", p.Name(), "mock")
}
}
func TestRegistryUnknown(t *testing.T) {
_, err := New("unknown", ProviderConfig{})
if err == nil {
t.Error("expected error for unknown provider")
}
}
func TestRegistryList(t *testing.T) {
Register("list-test", func(cfg ProviderConfig) (Provider, error) {
return &mockProvider{name: "list-test"}, nil
})
names := List()
found := false
for _, n := range names {
if n == "list-test" {
found = true
break
}
}
if !found {
t.Errorf("List() should contain 'list-test', got %v", names)
}
}