# Custom Hosting Provider Guide This guide walks you through creating a new hosting provider integration for Setec Manager. By the end, you will have a provider package that auto-registers with the system and can be used through the same unified API as the built-in Hostinger provider. --- ## Prerequisites - Go 1.25+ (matching the project's `go.mod`) - Familiarity with the Go `interface` pattern and HTTP client programming - An API key or credentials for the hosting provider you are integrating - A checkout of the `setec-manager` repository --- ## Project Structure Provider implementations live under `internal/hosting//`. Each provider is its own Go package. ``` internal/hosting/ provider.go -- Provider interface + model types + registry store.go -- ProviderConfig, ProviderConfigStore config.go -- Legacy config store hostinger/ -- Built-in Hostinger provider client.go -- HTTP client, auth, retry logic dns.go -- DNS record operations myprovider/ -- Your new provider (create this) provider.go -- init() registration + interface methods client.go -- HTTP client for the provider's API dns.go -- (optional) DNS-specific logic domains.go -- (optional) Domain-specific logic vms.go -- (optional) VPS-specific logic ``` You can organize files however you like within the package; the only requirement is that the package calls `hosting.Register(...)` in an `init()` function. --- ## The Provider Interface The `Provider` interface is defined in `internal/hosting/provider.go`. Every provider must implement all methods. Methods that your provider does not support should return `ErrNotSupported`. ```go type Provider interface { // Identity Name() string DisplayName() string // Configuration Configure(config map[string]string) error TestConnection() error // DNS ListDNSRecords(domain string) ([]DNSRecord, error) CreateDNSRecord(domain string, record DNSRecord) error UpdateDNSRecords(domain string, records []DNSRecord, overwrite bool) error DeleteDNSRecord(domain string, recordName, recordType string) error ResetDNSRecords(domain string) error // Domains ListDomains() ([]Domain, error) GetDomain(domain string) (*Domain, error) CheckDomainAvailability(domains []string) ([]DomainAvailability, error) PurchaseDomain(req DomainPurchaseRequest) (*Domain, error) SetNameservers(domain string, nameservers []string) error EnableDomainLock(domain string) error DisableDomainLock(domain string) error EnablePrivacyProtection(domain string) error DisablePrivacyProtection(domain string) error // VMs / VPS ListVMs() ([]VM, error) GetVM(id string) (*VM, error) CreateVM(req VMCreateRequest) (*VM, error) ListDataCenters() ([]DataCenter, error) ListSSHKeys() ([]SSHKey, error) AddSSHKey(name, publicKey string) (*SSHKey, error) DeleteSSHKey(id string) error // Billing ListSubscriptions() ([]Subscription, error) GetCatalog() ([]CatalogItem, error) } ``` ### Method Reference #### Identity Methods | Method | Parameters | Returns | Description | |---|---|---|---| | `Name()` | - | `string` | Short machine-readable name (lowercase, no spaces). Used as the registry key and in API URLs. Example: `"hostinger"`, `"cloudflare"`. | | `DisplayName()` | - | `string` | Human-readable name shown in the UI. Example: `"Hostinger"`, `"Cloudflare"`. | #### Configuration Methods | Method | Parameters | Returns | Description | |---|---|---|---| | `Configure(config)` | `map[string]string` -- key-value config pairs. Common keys: `"api_key"`, `"api_secret"`, `"base_url"`. | `error` | Called when a user saves credentials. Store them in struct fields. Validate format but do not make API calls. | | `TestConnection()` | - | `error` | Make a lightweight API call (e.g., list domains) to verify credentials are valid. Return `nil` on success. | #### DNS Methods | Method | Parameters | Returns | Description | |---|---|---|---| | `ListDNSRecords(domain)` | `domain string` -- the FQDN | `([]DNSRecord, error)` | Return all DNS records for the zone. | | `CreateDNSRecord(domain, record)` | `domain string`, `record DNSRecord` | `error` | Add a single record without affecting existing records. | | `UpdateDNSRecords(domain, records, overwrite)` | `domain string`, `records []DNSRecord`, `overwrite bool` | `error` | Batch update. If `overwrite` is true, replace all records; otherwise merge. | | `DeleteDNSRecord(domain, recordName, recordType)` | `domain string`, `recordName string` (subdomain or `@`), `recordType string` (e.g. `"A"`) | `error` | Delete matching records. | | `ResetDNSRecords(domain)` | `domain string` | `error` | Reset the zone to provider defaults. | #### Domain Methods | Method | Parameters | Returns | Description | |---|---|---|---| | `ListDomains()` | - | `([]Domain, error)` | Return all domains on the account. | | `GetDomain(domain)` | `domain string` | `(*Domain, error)` | Return details for a single domain. | | `CheckDomainAvailability(domains)` | `domains []string` | `([]DomainAvailability, error)` | Check if domains are available for registration and return pricing. | | `PurchaseDomain(req)` | `req DomainPurchaseRequest` | `(*Domain, error)` | Register a new domain. | | `SetNameservers(domain, nameservers)` | `domain string`, `nameservers []string` | `error` | Update the authoritative nameservers. | | `EnableDomainLock(domain)` | `domain string` | `error` | Enable registrar lock (transfer protection). | | `DisableDomainLock(domain)` | `domain string` | `error` | Disable registrar lock. | | `EnablePrivacyProtection(domain)` | `domain string` | `error` | Enable WHOIS privacy. | | `DisablePrivacyProtection(domain)` | `domain string` | `error` | Disable WHOIS privacy. | #### VM / VPS Methods | Method | Parameters | Returns | Description | |---|---|---|---| | `ListVMs()` | - | `([]VM, error)` | Return all VPS instances on the account. | | `GetVM(id)` | `id string` | `(*VM, error)` | Return details for a single VM. | | `CreateVM(req)` | `req VMCreateRequest` | `(*VM, error)` | Provision a new VPS instance. | | `ListDataCenters()` | - | `([]DataCenter, error)` | Return available regions/data centers. | | `ListSSHKeys()` | - | `([]SSHKey, error)` | Return all stored SSH public keys. | | `AddSSHKey(name, publicKey)` | `name string`, `publicKey string` | `(*SSHKey, error)` | Upload a new SSH public key. | | `DeleteSSHKey(id)` | `id string` | `error` | Remove an SSH key. | #### Billing Methods | Method | Parameters | Returns | Description | |---|---|---|---| | `ListSubscriptions()` | - | `([]Subscription, error)` | Return all active subscriptions. | | `GetCatalog()` | - | `([]CatalogItem, error)` | Return purchasable products and plans. | --- ## Type Reference All model types are defined in `internal/hosting/provider.go`. ### DNSRecord | Field | Type | JSON | Description | |---|---|---|---| | `ID` | `string` | `id` | Provider-assigned identifier. May be synthesized (e.g., `name/type/priority`). Optional on create. | | `Type` | `string` | `type` | Record type: `A`, `AAAA`, `CNAME`, `MX`, `TXT`, `NS`, `SRV`, `CAA`. | | `Name` | `string` | `name` | Subdomain label or `@` for the zone apex. | | `Content` | `string` | `content` | Record value (IP address, hostname, text, etc.). | | `TTL` | `int` | `ttl` | Time-to-live in seconds. | | `Priority` | `int` | `priority` | Priority value for MX and SRV records. Zero for other types. | ### Domain | Field | Type | JSON | Description | |---|---|---|---| | `Name` | `string` | `name` | Fully qualified domain name. | | `Registrar` | `string` | `registrar` | Registrar name (optional). | | `Status` | `string` | `status` | Registration status (e.g., `"active"`, `"expired"`, `"pending"`). | | `ExpiresAt` | `time.Time` | `expires_at` | Expiration date. | | `AutoRenew` | `bool` | `auto_renew` | Whether automatic renewal is enabled. | | `Locked` | `bool` | `locked` | Whether transfer lock is enabled. | | `PrivacyProtection` | `bool` | `privacy_protection` | Whether WHOIS privacy is enabled. | | `Nameservers` | `[]string` | `nameservers` | Current authoritative nameservers. | ### DomainAvailability | Field | Type | JSON | Description | |---|---|---|---| | `Domain` | `string` | `domain` | The queried domain name. | | `Available` | `bool` | `available` | Whether the domain is available for registration. | | `Price` | `float64` | `price` | Purchase price (zero if unavailable). | | `Currency` | `string` | `currency` | Currency code (e.g., `"USD"`). | ### DomainPurchaseRequest | Field | Type | JSON | Description | |---|---|---|---| | `Domain` | `string` | `domain` | Domain to purchase. | | `Period` | `int` | `period` | Registration period in years. | | `AutoRenew` | `bool` | `auto_renew` | Enable auto-renewal. | | `Privacy` | `bool` | `privacy_protection` | Enable WHOIS privacy. | | `PaymentID` | `string` | `payment_method_id` | Payment method identifier (optional, provider-specific). | ### VM | Field | Type | JSON | Description | |---|---|---|---| | `ID` | `string` | `id` | Provider-assigned VM identifier. | | `Name` | `string` | `name` | Human-readable VM name / hostname. | | `Status` | `string` | `status` | Current state: `"running"`, `"stopped"`, `"creating"`, `"error"`. | | `Plan` | `string` | `plan` | Plan/tier identifier. | | `Region` | `string` | `region` | Data center / region identifier. | | `IPv4` | `string` | `ipv4` | Public IPv4 address (optional). | | `IPv6` | `string` | `ipv6` | Public IPv6 address (optional). | | `OS` | `string` | `os` | Operating system template name (optional). | | `CPUs` | `int` | `cpus` | Number of virtual CPUs. | | `MemoryMB` | `int` | `memory_mb` | RAM in megabytes. | | `DiskGB` | `int` | `disk_gb` | Disk size in gigabytes. | | `BandwidthGB` | `int` | `bandwidth_gb` | Monthly bandwidth allowance in gigabytes. | | `CreatedAt` | `time.Time` | `created_at` | Creation timestamp. | | `Labels` | `map[string]string` | `labels` | Arbitrary key-value labels (optional). | ### VMCreateRequest | Field | Type | JSON | Description | |---|---|---|---| | `Plan` | `string` | `plan` | Plan/tier identifier from the catalog. | | `DataCenterID` | `string` | `data_center_id` | Target data center from `ListDataCenters()`. | | `Template` | `string` | `template` | OS template identifier. | | `Password` | `string` | `password` | Root/admin password for the VM. | | `Hostname` | `string` | `hostname` | Desired hostname. | | `SSHKeyID` | `string` | `ssh_key_id` | SSH key to install (optional). | | `PaymentID` | `string` | `payment_method_id` | Payment method identifier (optional). | ### DataCenter | Field | Type | JSON | Description | |---|---|---|---| | `ID` | `string` | `id` | Unique identifier used in `VMCreateRequest`. | | `Name` | `string` | `name` | Short name (e.g., `"US East"`). | | `Location` | `string` | `location` | City or locality. | | `Country` | `string` | `country` | ISO country code. | ### SSHKey | Field | Type | JSON | Description | |---|---|---|---| | `ID` | `string` | `id` | Provider-assigned key identifier. | | `Name` | `string` | `name` | User-assigned label. | | `Fingerprint` | `string` | `fingerprint` | Key fingerprint (e.g., `"SHA256:..."`). | | `PublicKey` | `string` | `public_key` | Full public key string. | ### Subscription | Field | Type | JSON | Description | |---|---|---|---| | `ID` | `string` | `id` | Subscription identifier. | | `Name` | `string` | `name` | Product name. | | `Status` | `string` | `status` | Status: `"active"`, `"cancelled"`, `"expired"`. | | `Plan` | `string` | `plan` | Plan identifier. | | `Price` | `float64` | `price` | Recurring price. | | `Currency` | `string` | `currency` | Currency code. | | `RenewsAt` | `time.Time` | `renews_at` | Next renewal date. | | `CreatedAt` | `time.Time` | `created_at` | Subscription start date. | ### CatalogItem | Field | Type | JSON | Description | |---|---|---|---| | `ID` | `string` | `id` | Product/plan identifier. | | `Name` | `string` | `name` | Product name. | | `Category` | `string` | `category` | Category: `"vps"`, `"hosting"`, `"domain"`, etc. | | `PriceCents` | `int` | `price_cents` | Price in cents (e.g., 1199 = $11.99). | | `Currency` | `string` | `currency` | Currency code. | | `Period` | `string` | `period` | Billing period: `"monthly"`, `"yearly"`. | | `Description` | `string` | `description` | Human-readable description (optional). | ### ProviderConfig Stored in `internal/hosting/store.go`. This is the credential record persisted to disk. | Field | Type | JSON | Description | |---|---|---|---| | `Provider` | `string` | `provider` | Provider name (must match `Provider.Name()`). | | `APIKey` | `string` | `api_key` | Primary API key or bearer token. | | `APISecret` | `string` | `api_secret` | Secondary secret (optional, provider-specific). | | `Extra` | `map[string]string` | `extra` | Additional provider-specific config values. | | `Connected` | `bool` | `connected` | Whether the last `TestConnection()` succeeded. | --- ## Implementing the Interface ### Step 1: Create the Package ```bash mkdir -p internal/hosting/myprovider ``` ### Step 2: Implement the Provider Create `internal/hosting/myprovider/provider.go`: ```go package myprovider import ( "errors" "fmt" "net/http" "time" "setec-manager/internal/hosting" ) // ErrNotSupported is returned by methods this provider does not implement. var ErrNotSupported = errors.New("myprovider: operation not supported") // Provider implements hosting.Provider for the MyProvider service. type Provider struct { client *http.Client apiKey string baseURL string } // init registers this provider with the hosting registry. // This runs automatically when the package is imported. func init() { hosting.Register(&Provider{ client: &http.Client{ Timeout: 30 * time.Second, }, baseURL: "https://api.myprovider.com", }) } // ── Identity ──────────────────────────────────────────────────────── func (p *Provider) Name() string { return "myprovider" } func (p *Provider) DisplayName() string { return "My Provider" } // ── Configuration ─────────────────────────────────────────────────── func (p *Provider) Configure(config map[string]string) error { key, ok := config["api_key"] if !ok || key == "" { return fmt.Errorf("myprovider: api_key is required") } p.apiKey = key if baseURL, ok := config["base_url"]; ok && baseURL != "" { p.baseURL = baseURL } return nil } func (p *Provider) TestConnection() error { // Make a lightweight API call to verify credentials. // For example, list domains or get account info. _, err := p.ListDomains() return err } // ── DNS ───────────────────────────────────────────────────────────── func (p *Provider) ListDNSRecords(domain string) ([]hosting.DNSRecord, error) { // TODO: Implement API call to list DNS records return nil, ErrNotSupported } func (p *Provider) CreateDNSRecord(domain string, record hosting.DNSRecord) error { return ErrNotSupported } func (p *Provider) UpdateDNSRecords(domain string, records []hosting.DNSRecord, overwrite bool) error { return ErrNotSupported } func (p *Provider) DeleteDNSRecord(domain string, recordName, recordType string) error { return ErrNotSupported } func (p *Provider) ResetDNSRecords(domain string) error { return ErrNotSupported } // ── Domains ───────────────────────────────────────────────────────── func (p *Provider) ListDomains() ([]hosting.Domain, error) { return nil, ErrNotSupported } func (p *Provider) GetDomain(domain string) (*hosting.Domain, error) { return nil, ErrNotSupported } func (p *Provider) CheckDomainAvailability(domains []string) ([]hosting.DomainAvailability, error) { return nil, ErrNotSupported } func (p *Provider) PurchaseDomain(req hosting.DomainPurchaseRequest) (*hosting.Domain, error) { return nil, ErrNotSupported } func (p *Provider) SetNameservers(domain string, nameservers []string) error { return ErrNotSupported } func (p *Provider) EnableDomainLock(domain string) error { return ErrNotSupported } func (p *Provider) DisableDomainLock(domain string) error { return ErrNotSupported } func (p *Provider) EnablePrivacyProtection(domain string) error { return ErrNotSupported } func (p *Provider) DisablePrivacyProtection(domain string) error { return ErrNotSupported } // ── VMs / VPS ─────────────────────────────────────────────────────── func (p *Provider) ListVMs() ([]hosting.VM, error) { return nil, ErrNotSupported } func (p *Provider) GetVM(id string) (*hosting.VM, error) { return nil, ErrNotSupported } func (p *Provider) CreateVM(req hosting.VMCreateRequest) (*hosting.VM, error) { return nil, ErrNotSupported } func (p *Provider) ListDataCenters() ([]hosting.DataCenter, error) { return nil, ErrNotSupported } func (p *Provider) ListSSHKeys() ([]hosting.SSHKey, error) { return nil, ErrNotSupported } func (p *Provider) AddSSHKey(name, publicKey string) (*hosting.SSHKey, error) { return nil, ErrNotSupported } func (p *Provider) DeleteSSHKey(id string) error { return ErrNotSupported } // ── Billing ───────────────────────────────────────────────────────── func (p *Provider) ListSubscriptions() ([]hosting.Subscription, error) { return nil, ErrNotSupported } func (p *Provider) GetCatalog() ([]hosting.CatalogItem, error) { return nil, ErrNotSupported } ``` --- ## Registration Registration happens automatically via Go's `init()` mechanism. When the main binary imports the provider package (even as a side-effect import), the `init()` function runs and calls `hosting.Register()`. In `cmd/main.go` (or wherever the binary entry point is), add a blank import: ```go import ( // Register hosting providers _ "setec-manager/internal/hosting/hostinger" _ "setec-manager/internal/hosting/myprovider" ) ``` The `hosting.Register()` function stores the provider instance in a global `map[string]Provider` protected by a `sync.RWMutex`: ```go // From internal/hosting/provider.go func Register(p Provider) { registryMu.Lock() defer registryMu.Unlock() registry[p.Name()] = p } ``` After registration, the provider is accessible via `hosting.Get("myprovider")` and appears in `hosting.List()`. --- ## Configuration Storage When a user configures your provider (via the UI or API), the system: 1. Calls `provider.Configure(map[string]string{"api_key": "..."})` to set credentials in memory. 2. Calls `provider.TestConnection()` to verify the credentials work. 3. Saves a `ProviderConfig` to disk via `ProviderConfigStore.Save()`. The config file is written to `/.json` with `0600` permissions: ```json { "provider": "myprovider", "api_key": "sk-abc123...", "api_secret": "", "extra": { "base_url": "https://api.myprovider.com/v2" }, "connected": true } ``` On startup, `ProviderConfigStore.loadAll()` reads all JSON files from the config directory, and for each one that matches a registered provider, calls `Configure()` to restore credentials. --- ## Error Handling ### The ErrNotSupported Pattern Define a sentinel error in your provider package: ```go var ErrNotSupported = errors.New("myprovider: operation not supported") ``` Return this error from any interface method your provider does not implement. The HTTP handler layer checks for this error and returns HTTP 501 (Not Implemented) to the client. ### API Errors For errors from the upstream provider API, return a descriptive error with context: ```go return fmt.Errorf("myprovider: list domains: %w", err) ``` ### Rate Limiting If the provider has rate limits, handle them inside your client. See the Hostinger implementation in `internal/hosting/hostinger/client.go` for a reference pattern: 1. Check for HTTP 429 responses. 2. Read the `Retry-After` header. 3. Sleep and retry (up to a maximum number of retries). 4. Return a clear error if retries are exhausted. ```go if resp.StatusCode == http.StatusTooManyRequests { retryAfter := parseRetryAfter(resp.Header.Get("Retry-After")) if attempt < maxRetries { time.Sleep(retryAfter) continue } return fmt.Errorf("myprovider: rate limited after %d retries", maxRetries) } ``` --- ## Testing ### Unit Tests Create `internal/hosting/myprovider/provider_test.go`: ```go package myprovider import ( "testing" "setec-manager/internal/hosting" ) func TestProviderImplementsInterface(t *testing.T) { var _ hosting.Provider = (*Provider)(nil) } func TestName(t *testing.T) { p := &Provider{} if p.Name() != "myprovider" { t.Errorf("expected name 'myprovider', got %q", p.Name()) } } func TestConfigure(t *testing.T) { p := &Provider{} err := p.Configure(map[string]string{}) if err == nil { t.Error("expected error when api_key is missing") } err = p.Configure(map[string]string{"api_key": "test-key"}) if err != nil { t.Errorf("unexpected error: %v", err) } if p.apiKey != "test-key" { t.Errorf("expected apiKey 'test-key', got %q", p.apiKey) } } func TestUnsupportedMethodsReturnError(t *testing.T) { p := &Provider{} _, err := p.ListVMs() if err != ErrNotSupported { t.Errorf("ListVMs: expected ErrNotSupported, got %v", err) } _, err = p.GetCatalog() if err != ErrNotSupported { t.Errorf("GetCatalog: expected ErrNotSupported, got %v", err) } } ``` ### Integration Tests For integration tests against the real API, use build tags to prevent them from running in CI: ```go //go:build integration package myprovider import ( "os" "testing" ) func TestListDomainsIntegration(t *testing.T) { key := os.Getenv("MYPROVIDER_API_KEY") if key == "" { t.Skip("MYPROVIDER_API_KEY not set") } p := &Provider{} p.Configure(map[string]string{"api_key": key}) domains, err := p.ListDomains() if err != nil { t.Fatalf("ListDomains failed: %v", err) } t.Logf("Found %d domains", len(domains)) } ``` Run integration tests: ```bash go test -tags=integration ./internal/hosting/myprovider/ -v ``` ### Registration Test Verify that importing the package registers the provider: ```go package myprovider_test import ( "testing" "setec-manager/internal/hosting" _ "setec-manager/internal/hosting/myprovider" ) func TestRegistration(t *testing.T) { p, err := hosting.Get("myprovider") if err != nil { t.Fatalf("provider not registered: %v", err) } if p.DisplayName() == "" { t.Error("DisplayName is empty") } } ``` --- ## Example: Skeleton Provider (DNS Only) This is a complete, minimal provider that implements only DNS management. All other methods return `ErrNotSupported`. You can copy this file and fill in the DNS methods with real API calls. ```go package dnsonlyprovider import ( "encoding/json" "errors" "fmt" "io" "net/http" "time" "setec-manager/internal/hosting" ) var ErrNotSupported = errors.New("dnsonlyprovider: operation not supported") type Provider struct { client *http.Client apiKey string baseURL string } func init() { hosting.Register(&Provider{ client: &http.Client{Timeout: 30 * time.Second}, baseURL: "https://api.dns-only.example.com/v1", }) } func (p *Provider) Name() string { return "dnsonlyprovider" } func (p *Provider) DisplayName() string { return "DNS-Only Provider" } func (p *Provider) Configure(config map[string]string) error { key, ok := config["api_key"] if !ok || key == "" { return fmt.Errorf("dnsonlyprovider: api_key is required") } p.apiKey = key return nil } func (p *Provider) TestConnection() error { // Try listing zones as a health check. req, _ := http.NewRequest("GET", p.baseURL+"/zones", nil) req.Header.Set("Authorization", "Bearer "+p.apiKey) resp, err := p.client.Do(req) if err != nil { return fmt.Errorf("dnsonlyprovider: connection failed: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return fmt.Errorf("dnsonlyprovider: API returned %d: %s", resp.StatusCode, body) } return nil } // ── DNS (implemented) ─────────────────────────────────────────────── func (p *Provider) ListDNSRecords(domain string) ([]hosting.DNSRecord, error) { req, _ := http.NewRequest("GET", fmt.Sprintf("%s/zones/%s/records", p.baseURL, domain), nil) req.Header.Set("Authorization", "Bearer "+p.apiKey) resp, err := p.client.Do(req) if err != nil { return nil, fmt.Errorf("dnsonlyprovider: list records: %w", err) } defer resp.Body.Close() var records []hosting.DNSRecord if err := json.NewDecoder(resp.Body).Decode(&records); err != nil { return nil, fmt.Errorf("dnsonlyprovider: parse records: %w", err) } return records, nil } func (p *Provider) CreateDNSRecord(domain string, record hosting.DNSRecord) error { // Implementation: POST to /zones/{domain}/records return ErrNotSupported // replace with real implementation } func (p *Provider) UpdateDNSRecords(domain string, records []hosting.DNSRecord, overwrite bool) error { // Implementation: PUT to /zones/{domain}/records return ErrNotSupported // replace with real implementation } func (p *Provider) DeleteDNSRecord(domain string, recordName, recordType string) error { // Implementation: DELETE /zones/{domain}/records?name=...&type=... return ErrNotSupported // replace with real implementation } func (p *Provider) ResetDNSRecords(domain string) error { return ErrNotSupported } // ── Everything else: not supported ────────────────────────────────── func (p *Provider) ListDomains() ([]hosting.Domain, error) { return nil, ErrNotSupported } func (p *Provider) GetDomain(domain string) (*hosting.Domain, error) { return nil, ErrNotSupported } func (p *Provider) CheckDomainAvailability(domains []string) ([]hosting.DomainAvailability, error) { return nil, ErrNotSupported } func (p *Provider) PurchaseDomain(req hosting.DomainPurchaseRequest) (*hosting.Domain, error) { return nil, ErrNotSupported } func (p *Provider) SetNameservers(domain string, nameservers []string) error { return ErrNotSupported } func (p *Provider) EnableDomainLock(domain string) error { return ErrNotSupported } func (p *Provider) DisableDomainLock(domain string) error { return ErrNotSupported } func (p *Provider) EnablePrivacyProtection(domain string) error { return ErrNotSupported } func (p *Provider) DisablePrivacyProtection(domain string) error { return ErrNotSupported } func (p *Provider) ListVMs() ([]hosting.VM, error) { return nil, ErrNotSupported } func (p *Provider) GetVM(id string) (*hosting.VM, error) { return nil, ErrNotSupported } func (p *Provider) CreateVM(req hosting.VMCreateRequest) (*hosting.VM, error) { return nil, ErrNotSupported } func (p *Provider) ListDataCenters() ([]hosting.DataCenter, error) { return nil, ErrNotSupported } func (p *Provider) ListSSHKeys() ([]hosting.SSHKey, error) { return nil, ErrNotSupported } func (p *Provider) AddSSHKey(name, publicKey string) (*hosting.SSHKey, error) { return nil, ErrNotSupported } func (p *Provider) DeleteSSHKey(id string) error { return ErrNotSupported } func (p *Provider) ListSubscriptions() ([]hosting.Subscription, error) { return nil, ErrNotSupported } func (p *Provider) GetCatalog() ([]hosting.CatalogItem, error) { return nil, ErrNotSupported } ``` --- ## Example: Full Provider Structure For a provider that implements all capabilities, organize the code across multiple files: ``` internal/hosting/fullprovider/ provider.go -- init(), Name(), DisplayName(), Configure(), TestConnection() client.go -- HTTP client with auth, retry, rate-limit handling dns.go -- ListDNSRecords, CreateDNSRecord, UpdateDNSRecords, DeleteDNSRecord, ResetDNSRecords domains.go -- ListDomains, GetDomain, CheckDomainAvailability, PurchaseDomain, nameserver/lock/privacy methods vms.go -- ListVMs, GetVM, CreateVM, ListDataCenters ssh.go -- ListSSHKeys, AddSSHKey, DeleteSSHKey billing.go -- ListSubscriptions, GetCatalog types.go -- Provider-specific API request/response types ``` Each file focuses on a single capability area. The `client.go` file provides a shared `doRequest()` method (similar to the Hostinger client) that handles authentication headers, JSON marshaling, error parsing, and retry logic. ### Key Patterns from the Hostinger Implementation 1. **Separate API types from generic types.** Define provider-specific request/response structs (e.g., `hostingerDNSRecord`) and conversion functions (`toGenericDNSRecord`, `toHostingerDNSRecord`). 2. **Validate before mutating.** The Hostinger DNS implementation calls a `/validate` endpoint before applying updates. If your provider offers similar validation, use it. 3. **Synthesize IDs when the API does not provide them.** Hostinger does not return record IDs in zone listings, so the client synthesizes them from `name/type/priority`. 4. **Handle rate limits transparently.** The client retries on HTTP 429 with exponential back-off, capping at 60 seconds per retry and 3 retries total. This keeps rate-limit handling invisible to the caller.