From fc48e5f4a35adb39c8140ea031c2da8a102824f7 Mon Sep 17 00:00:00 2001 From: NightStar Date: Tue, 28 Jul 2026 15:59:08 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20provider=20interface=20+=20registry=20?= =?UTF-8?q?=E2=80=94=20factory=20pattern=20for=20multi-cloud?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/provider/provider.go | 132 +++++++++++++++++++++++++++++ internal/provider/registry.go | 41 +++++++++ internal/provider/registry_test.go | 57 +++++++++++++ 3 files changed, 230 insertions(+) create mode 100644 internal/provider/provider.go create mode 100644 internal/provider/registry.go create mode 100644 internal/provider/registry_test.go diff --git a/internal/provider/provider.go b/internal/provider/provider.go new file mode 100644 index 0000000..8b623ea --- /dev/null +++ b/internal/provider/provider.go @@ -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)) +} diff --git a/internal/provider/registry.go b/internal/provider/registry.go new file mode 100644 index 0000000..fa8fd36 --- /dev/null +++ b/internal/provider/registry.go @@ -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 +} diff --git a/internal/provider/registry_test.go b/internal/provider/registry_test.go new file mode 100644 index 0000000..1542eb9 --- /dev/null +++ b/internal/provider/registry_test.go @@ -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) + } +}