No One Can Stop Me Now
This commit is contained in:
107
services/setec-manager/internal/hosting/config.go
Normal file
107
services/setec-manager/internal/hosting/config.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package hosting
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ProviderConfigStore manages saved provider configurations on disk.
|
||||
// Each provider's config is stored as a separate JSON file with restrictive
|
||||
// permissions (0600) since the files contain API keys.
|
||||
type ProviderConfigStore struct {
|
||||
configDir string
|
||||
}
|
||||
|
||||
// NewConfigStore creates a new store rooted at configDir. The directory is
|
||||
// created on first write if it does not already exist.
|
||||
func NewConfigStore(configDir string) *ProviderConfigStore {
|
||||
return &ProviderConfigStore{configDir: configDir}
|
||||
}
|
||||
|
||||
// configPath returns the file path for a provider's config file.
|
||||
func (s *ProviderConfigStore) configPath(providerName string) string {
|
||||
return filepath.Join(s.configDir, providerName+".json")
|
||||
}
|
||||
|
||||
// ensureDir creates the config directory if it does not exist.
|
||||
func (s *ProviderConfigStore) ensureDir() error {
|
||||
return os.MkdirAll(s.configDir, 0700)
|
||||
}
|
||||
|
||||
// Save writes a provider configuration to disk. It overwrites any existing
|
||||
// config for the same provider.
|
||||
func (s *ProviderConfigStore) Save(providerName string, cfg ProviderConfig) error {
|
||||
if providerName == "" {
|
||||
return fmt.Errorf("hosting: provider name must not be empty")
|
||||
}
|
||||
if err := s.ensureDir(); err != nil {
|
||||
return fmt.Errorf("hosting: create config dir: %w", err)
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(cfg, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("hosting: marshal config for %s: %w", providerName, err)
|
||||
}
|
||||
|
||||
path := s.configPath(providerName)
|
||||
if err := os.WriteFile(path, data, 0600); err != nil {
|
||||
return fmt.Errorf("hosting: write config for %s: %w", providerName, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Load reads a provider configuration from disk.
|
||||
func (s *ProviderConfigStore) Load(providerName string) (*ProviderConfig, error) {
|
||||
path := s.configPath(providerName)
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("hosting: no config found for provider %q", providerName)
|
||||
}
|
||||
return nil, fmt.Errorf("hosting: read config for %s: %w", providerName, err)
|
||||
}
|
||||
|
||||
var cfg ProviderConfig
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("hosting: parse config for %s: %w", providerName, err)
|
||||
}
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
// Delete removes a provider's saved configuration.
|
||||
func (s *ProviderConfigStore) Delete(providerName string) error {
|
||||
path := s.configPath(providerName)
|
||||
if err := os.Remove(path); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil // already gone
|
||||
}
|
||||
return fmt.Errorf("hosting: delete config for %s: %w", providerName, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListConfigured returns the names of all providers that have saved configs.
|
||||
func (s *ProviderConfigStore) ListConfigured() ([]string, error) {
|
||||
entries, err := os.ReadDir(s.configDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil // no directory means no configs
|
||||
}
|
||||
return nil, fmt.Errorf("hosting: list configs: %w", err)
|
||||
}
|
||||
|
||||
var names []string
|
||||
for _, e := range entries {
|
||||
if e.IsDir() {
|
||||
continue
|
||||
}
|
||||
name := e.Name()
|
||||
if strings.HasSuffix(name, ".json") {
|
||||
names = append(names, strings.TrimSuffix(name, ".json"))
|
||||
}
|
||||
}
|
||||
return names, nil
|
||||
}
|
||||
127
services/setec-manager/internal/hosting/hostinger/billing.go
Normal file
127
services/setec-manager/internal/hosting/hostinger/billing.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package hostinger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"setec-manager/internal/hosting"
|
||||
)
|
||||
|
||||
// hostingerSubscription is the Hostinger API representation of a subscription.
|
||||
type hostingerSubscription struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
RenewalDate string `json:"renewal_date"`
|
||||
Price struct {
|
||||
Amount float64 `json:"amount"`
|
||||
Currency string `json:"currency"`
|
||||
} `json:"price"`
|
||||
}
|
||||
|
||||
// hostingerCatalogItem is the Hostinger API representation of a catalog item.
|
||||
type hostingerCatalogItem struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Category string `json:"category"`
|
||||
Price float64 `json:"price"`
|
||||
Currency string `json:"currency"`
|
||||
Features map[string]string `json:"features,omitempty"`
|
||||
}
|
||||
|
||||
// hostingerPaymentMethod is the Hostinger API representation of a payment method.
|
||||
type hostingerPaymentMethod struct {
|
||||
ID int `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Last4 string `json:"last4"`
|
||||
ExpMonth int `json:"exp_month"`
|
||||
ExpYear int `json:"exp_year"`
|
||||
Default bool `json:"default"`
|
||||
}
|
||||
|
||||
// PaymentMethod is the exported type for payment method information.
|
||||
type PaymentMethod struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Last4 string `json:"last4"`
|
||||
ExpMonth int `json:"exp_month"`
|
||||
ExpYear int `json:"exp_year"`
|
||||
Default bool `json:"default"`
|
||||
}
|
||||
|
||||
// ListSubscriptions retrieves all billing subscriptions.
|
||||
func (c *Client) ListSubscriptions() ([]hosting.Subscription, error) {
|
||||
var subs []hostingerSubscription
|
||||
if err := c.doRequest(http.MethodGet, "/api/billing/v1/subscriptions", nil, &subs); err != nil {
|
||||
return nil, fmt.Errorf("list subscriptions: %w", err)
|
||||
}
|
||||
|
||||
result := make([]hosting.Subscription, 0, len(subs))
|
||||
for _, s := range subs {
|
||||
renewsAt, _ := time.Parse(time.RFC3339, s.RenewalDate)
|
||||
result = append(result, hosting.Subscription{
|
||||
ID: strconv.Itoa(s.ID),
|
||||
Name: s.Name,
|
||||
Status: s.Status,
|
||||
RenewsAt: renewsAt,
|
||||
Price: s.Price.Amount,
|
||||
Currency: s.Price.Currency,
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetCatalog retrieves the product catalog, optionally filtered by category.
|
||||
// If category is empty, all catalog items are returned.
|
||||
func (c *Client) GetCatalog(category string) ([]hosting.CatalogItem, error) {
|
||||
path := "/api/billing/v1/catalog"
|
||||
if category != "" {
|
||||
path += "?" + url.Values{"category": {category}}.Encode()
|
||||
}
|
||||
|
||||
var items []hostingerCatalogItem
|
||||
if err := c.doRequest(http.MethodGet, path, nil, &items); err != nil {
|
||||
return nil, fmt.Errorf("get catalog: %w", err)
|
||||
}
|
||||
|
||||
result := make([]hosting.CatalogItem, 0, len(items))
|
||||
for _, item := range items {
|
||||
result = append(result, hosting.CatalogItem{
|
||||
ID: item.ID,
|
||||
Name: item.Name,
|
||||
Category: item.Category,
|
||||
Price: item.Price,
|
||||
Currency: item.Currency,
|
||||
Features: item.Features,
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ListPaymentMethods retrieves all payment methods on the account.
|
||||
// This is a Hostinger-specific method not part of the generic Provider interface.
|
||||
func (c *Client) ListPaymentMethods() ([]PaymentMethod, error) {
|
||||
var methods []hostingerPaymentMethod
|
||||
if err := c.doRequest(http.MethodGet, "/api/billing/v1/payment-methods", nil, &methods); err != nil {
|
||||
return nil, fmt.Errorf("list payment methods: %w", err)
|
||||
}
|
||||
|
||||
result := make([]PaymentMethod, 0, len(methods))
|
||||
for _, m := range methods {
|
||||
result = append(result, PaymentMethod{
|
||||
ID: strconv.Itoa(m.ID),
|
||||
Type: m.Type,
|
||||
Name: m.Name,
|
||||
Last4: m.Last4,
|
||||
ExpMonth: m.ExpMonth,
|
||||
ExpYear: m.ExpYear,
|
||||
Default: m.Default,
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
172
services/setec-manager/internal/hosting/hostinger/client.go
Normal file
172
services/setec-manager/internal/hosting/hostinger/client.go
Normal file
@@ -0,0 +1,172 @@
|
||||
package hostinger
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"setec-manager/internal/hosting"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultBaseURL = "https://developers.hostinger.com"
|
||||
maxRetries = 3
|
||||
)
|
||||
|
||||
// APIError represents an error response from the Hostinger API.
|
||||
type APIError struct {
|
||||
StatusCode int `json:"-"`
|
||||
Message string `json:"error"`
|
||||
CorrelationID string `json:"correlation_id,omitempty"`
|
||||
}
|
||||
|
||||
func (e *APIError) Error() string {
|
||||
if e.CorrelationID != "" {
|
||||
return fmt.Sprintf("hostinger API error %d: %s (correlation_id: %s)", e.StatusCode, e.Message, e.CorrelationID)
|
||||
}
|
||||
return fmt.Sprintf("hostinger API error %d: %s", e.StatusCode, e.Message)
|
||||
}
|
||||
|
||||
// Client is the Hostinger API client. It implements hosting.Provider.
|
||||
type Client struct {
|
||||
apiToken string
|
||||
httpClient *http.Client
|
||||
baseURL string
|
||||
}
|
||||
|
||||
// Compile-time check that Client implements hosting.Provider.
|
||||
var _ hosting.Provider = (*Client)(nil)
|
||||
|
||||
// New creates a new Hostinger API client with the given bearer token.
|
||||
func New(token string) *Client {
|
||||
return &Client{
|
||||
apiToken: token,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
baseURL: defaultBaseURL,
|
||||
}
|
||||
}
|
||||
|
||||
// Name returns the provider identifier.
|
||||
func (c *Client) Name() string { return "hostinger" }
|
||||
|
||||
// DisplayName returns the human-readable provider name.
|
||||
func (c *Client) DisplayName() string { return "Hostinger" }
|
||||
|
||||
// Configure applies the given configuration to the client.
|
||||
func (c *Client) Configure(cfg hosting.ProviderConfig) error {
|
||||
if cfg.APIKey == "" {
|
||||
return fmt.Errorf("hostinger: API key is required")
|
||||
}
|
||||
c.apiToken = cfg.APIKey
|
||||
if cfg.BaseURL != "" {
|
||||
c.baseURL = cfg.BaseURL
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestConnection verifies the API token by making a lightweight API call.
|
||||
func (c *Client) TestConnection() error {
|
||||
_, err := c.ListDomains()
|
||||
return err
|
||||
}
|
||||
|
||||
// doRequest executes an HTTP request against the Hostinger API.
|
||||
// body may be nil for requests with no body. result may be nil if the
|
||||
// response body should be discarded.
|
||||
func (c *Client) doRequest(method, path string, body interface{}, result interface{}) error {
|
||||
url := c.baseURL + path
|
||||
|
||||
var rawBody []byte
|
||||
if body != nil {
|
||||
var err error
|
||||
rawBody, err = json.Marshal(body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal request body: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
var lastErr error
|
||||
for attempt := 0; attempt <= maxRetries; attempt++ {
|
||||
var bodyReader io.Reader
|
||||
if rawBody != nil {
|
||||
bodyReader = bytes.NewReader(rawBody)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, url, bodyReader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+c.apiToken)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
lastErr = fmt.Errorf("execute request: %w", err)
|
||||
continue
|
||||
}
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("read response body: %w", err)
|
||||
}
|
||||
|
||||
// Handle rate limiting with retry.
|
||||
if resp.StatusCode == http.StatusTooManyRequests {
|
||||
if attempt < maxRetries {
|
||||
retryAfter := parseRetryAfter(resp.Header.Get("Retry-After"))
|
||||
time.Sleep(retryAfter)
|
||||
lastErr = &APIError{StatusCode: 429, Message: "rate limited"}
|
||||
continue
|
||||
}
|
||||
return &APIError{StatusCode: 429, Message: "rate limited after retries"}
|
||||
}
|
||||
|
||||
// Handle error responses.
|
||||
if resp.StatusCode >= 400 {
|
||||
apiErr := &APIError{StatusCode: resp.StatusCode}
|
||||
if jsonErr := json.Unmarshal(respBody, apiErr); jsonErr != nil {
|
||||
apiErr.Message = string(respBody)
|
||||
}
|
||||
return apiErr
|
||||
}
|
||||
|
||||
// Parse successful response.
|
||||
if result != nil && len(respBody) > 0 {
|
||||
if err := json.Unmarshal(respBody, result); err != nil {
|
||||
return fmt.Errorf("unmarshal response: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return lastErr
|
||||
}
|
||||
|
||||
// parseRetryAfter parses the Retry-After header value.
|
||||
// Returns a default of 1 second if the header is missing or unparseable.
|
||||
func parseRetryAfter(value string) time.Duration {
|
||||
if value == "" {
|
||||
return time.Second
|
||||
}
|
||||
seconds, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return time.Second
|
||||
}
|
||||
if seconds <= 0 {
|
||||
return time.Second
|
||||
}
|
||||
if seconds > 60 {
|
||||
seconds = 60
|
||||
}
|
||||
return time.Duration(seconds) * time.Second
|
||||
}
|
||||
136
services/setec-manager/internal/hosting/hostinger/dns.go
Normal file
136
services/setec-manager/internal/hosting/hostinger/dns.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package hostinger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"setec-manager/internal/hosting"
|
||||
)
|
||||
|
||||
// hostingerDNSRecord is the Hostinger API representation of a DNS record.
|
||||
type hostingerDNSRecord struct {
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
TTL int `json:"ttl"`
|
||||
Priority *int `json:"priority,omitempty"`
|
||||
}
|
||||
|
||||
// hostingerDNSUpdateRequest is the request body for updating DNS records.
|
||||
type hostingerDNSUpdateRequest struct {
|
||||
Records []hostingerDNSRecord `json:"records"`
|
||||
Overwrite bool `json:"overwrite"`
|
||||
}
|
||||
|
||||
// hostingerDNSValidateRequest is the request body for validating DNS records.
|
||||
type hostingerDNSValidateRequest struct {
|
||||
Records []hostingerDNSRecord `json:"records"`
|
||||
}
|
||||
|
||||
// ListDNSRecords retrieves all DNS records for the given domain.
|
||||
func (c *Client) ListDNSRecords(domain string) ([]hosting.DNSRecord, error) {
|
||||
path := fmt.Sprintf("/api/dns/v1/zones/%s", url.PathEscape(domain))
|
||||
|
||||
var apiRecords []hostingerDNSRecord
|
||||
if err := c.doRequest(http.MethodGet, path, nil, &apiRecords); err != nil {
|
||||
return nil, fmt.Errorf("list DNS records for %s: %w", domain, err)
|
||||
}
|
||||
|
||||
records := make([]hosting.DNSRecord, 0, len(apiRecords))
|
||||
for _, r := range apiRecords {
|
||||
records = append(records, toGenericDNSRecord(r))
|
||||
}
|
||||
return records, nil
|
||||
}
|
||||
|
||||
// UpdateDNSRecords updates DNS records for the given domain.
|
||||
// If overwrite is true, existing records are replaced entirely.
|
||||
func (c *Client) UpdateDNSRecords(domain string, records []hosting.DNSRecord, overwrite bool) error {
|
||||
path := fmt.Sprintf("/api/dns/v1/zones/%s", url.PathEscape(domain))
|
||||
|
||||
hostingerRecords := make([]hostingerDNSRecord, 0, len(records))
|
||||
for _, r := range records {
|
||||
hostingerRecords = append(hostingerRecords, toHostingerDNSRecord(r))
|
||||
}
|
||||
|
||||
// Validate first.
|
||||
validatePath := fmt.Sprintf("/api/dns/v1/zones/%s/validate", url.PathEscape(domain))
|
||||
validateReq := hostingerDNSValidateRequest{Records: hostingerRecords}
|
||||
if err := c.doRequest(http.MethodPost, validatePath, validateReq, nil); err != nil {
|
||||
return fmt.Errorf("validate DNS records for %s: %w", domain, err)
|
||||
}
|
||||
|
||||
req := hostingerDNSUpdateRequest{
|
||||
Records: hostingerRecords,
|
||||
Overwrite: overwrite,
|
||||
}
|
||||
if err := c.doRequest(http.MethodPut, path, req, nil); err != nil {
|
||||
return fmt.Errorf("update DNS records for %s: %w", domain, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateDNSRecord adds a single DNS record to the domain without overwriting.
|
||||
func (c *Client) CreateDNSRecord(domain string, record hosting.DNSRecord) error {
|
||||
return c.UpdateDNSRecords(domain, []hosting.DNSRecord{record}, false)
|
||||
}
|
||||
|
||||
// DeleteDNSRecord removes DNS records matching the given filter.
|
||||
func (c *Client) DeleteDNSRecord(domain string, filter hosting.DNSRecordFilter) error {
|
||||
path := fmt.Sprintf("/api/dns/v1/zones/%s", url.PathEscape(domain))
|
||||
|
||||
params := url.Values{}
|
||||
if filter.Name != "" {
|
||||
params.Set("name", filter.Name)
|
||||
}
|
||||
if filter.Type != "" {
|
||||
params.Set("type", filter.Type)
|
||||
}
|
||||
if len(params) > 0 {
|
||||
path += "?" + params.Encode()
|
||||
}
|
||||
|
||||
if err := c.doRequest(http.MethodDelete, path, nil, nil); err != nil {
|
||||
return fmt.Errorf("delete DNS record %s/%s for %s: %w", filter.Name, filter.Type, domain, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResetDNSRecords resets the domain's DNS zone to default records.
|
||||
func (c *Client) ResetDNSRecords(domain string) error {
|
||||
path := fmt.Sprintf("/api/dns/v1/zones/%s/reset", url.PathEscape(domain))
|
||||
if err := c.doRequest(http.MethodPost, path, nil, nil); err != nil {
|
||||
return fmt.Errorf("reset DNS records for %s: %w", domain, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// toGenericDNSRecord converts a Hostinger DNS record to the generic type.
|
||||
func toGenericDNSRecord(r hostingerDNSRecord) hosting.DNSRecord {
|
||||
rec := hosting.DNSRecord{
|
||||
Type: r.Type,
|
||||
Name: r.Name,
|
||||
Content: r.Content,
|
||||
TTL: r.TTL,
|
||||
}
|
||||
if r.Priority != nil {
|
||||
rec.Priority = *r.Priority
|
||||
}
|
||||
return rec
|
||||
}
|
||||
|
||||
// toHostingerDNSRecord converts a generic DNS record to the Hostinger format.
|
||||
func toHostingerDNSRecord(r hosting.DNSRecord) hostingerDNSRecord {
|
||||
rec := hostingerDNSRecord{
|
||||
Type: r.Type,
|
||||
Name: r.Name,
|
||||
Content: r.Content,
|
||||
TTL: r.TTL,
|
||||
}
|
||||
if r.Priority != 0 {
|
||||
p := r.Priority
|
||||
rec.Priority = &p
|
||||
}
|
||||
return rec
|
||||
}
|
||||
218
services/setec-manager/internal/hosting/hostinger/domains.go
Normal file
218
services/setec-manager/internal/hosting/hostinger/domains.go
Normal file
@@ -0,0 +1,218 @@
|
||||
package hostinger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"setec-manager/internal/hosting"
|
||||
)
|
||||
|
||||
// hostingerDomain is the Hostinger API representation of a domain.
|
||||
type hostingerDomain struct {
|
||||
Domain string `json:"domain"`
|
||||
Status string `json:"status"`
|
||||
ExpirationDate string `json:"expiration_date"`
|
||||
AutoRenew bool `json:"auto_renew"`
|
||||
DomainLock bool `json:"domain_lock"`
|
||||
PrivacyProtection bool `json:"privacy_protection"`
|
||||
Nameservers []string `json:"nameservers"`
|
||||
}
|
||||
|
||||
// hostingerDomainList wraps the list response.
|
||||
type hostingerDomainList struct {
|
||||
Domains []hostingerDomain `json:"domains"`
|
||||
}
|
||||
|
||||
// hostingerAvailabilityRequest is the check-availability request body.
|
||||
type hostingerAvailabilityRequest struct {
|
||||
Domains []string `json:"domains"`
|
||||
}
|
||||
|
||||
// hostingerAvailabilityResult is a single domain availability result.
|
||||
type hostingerAvailabilityResult struct {
|
||||
Domain string `json:"domain"`
|
||||
Available bool `json:"available"`
|
||||
Price *struct {
|
||||
Amount float64 `json:"amount"`
|
||||
Currency string `json:"currency"`
|
||||
} `json:"price,omitempty"`
|
||||
}
|
||||
|
||||
// hostingerPurchaseRequest is the domain purchase request body.
|
||||
type hostingerPurchaseRequest struct {
|
||||
Domain string `json:"domain"`
|
||||
Period int `json:"period"`
|
||||
AutoRenew bool `json:"auto_renew"`
|
||||
Privacy bool `json:"privacy_protection"`
|
||||
PaymentMethodID string `json:"payment_method_id,omitempty"`
|
||||
}
|
||||
|
||||
// hostingerNameserversRequest is the body for updating nameservers.
|
||||
type hostingerNameserversRequest struct {
|
||||
Nameservers []string `json:"nameservers"`
|
||||
}
|
||||
|
||||
// ListDomains retrieves all domains in the account portfolio.
|
||||
func (c *Client) ListDomains() ([]hosting.Domain, error) {
|
||||
var list hostingerDomainList
|
||||
if err := c.doRequest(http.MethodGet, "/api/domains/v1/portfolio", nil, &list); err != nil {
|
||||
return nil, fmt.Errorf("list domains: %w", err)
|
||||
}
|
||||
|
||||
domains := make([]hosting.Domain, 0, len(list.Domains))
|
||||
for _, d := range list.Domains {
|
||||
domains = append(domains, toSummaryDomain(d))
|
||||
}
|
||||
return domains, nil
|
||||
}
|
||||
|
||||
// GetDomain retrieves details for a specific domain.
|
||||
func (c *Client) GetDomain(domain string) (*hosting.DomainDetail, error) {
|
||||
path := fmt.Sprintf("/api/domains/v1/portfolio/%s", url.PathEscape(domain))
|
||||
|
||||
var d hostingerDomain
|
||||
if err := c.doRequest(http.MethodGet, path, nil, &d); err != nil {
|
||||
return nil, fmt.Errorf("get domain %s: %w", domain, err)
|
||||
}
|
||||
|
||||
result := toDetailDomain(d)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// CheckDomainAvailability checks whether the given domain is available for
|
||||
// registration across the specified TLDs. If tlds is empty, the domain string
|
||||
// is checked as-is.
|
||||
func (c *Client) CheckDomainAvailability(domain string, tlds []string) ([]hosting.DomainAvailability, error) {
|
||||
// Build the list of fully qualified domain names to check.
|
||||
var domains []string
|
||||
if len(tlds) == 0 {
|
||||
domains = []string{domain}
|
||||
} else {
|
||||
for _, tld := range tlds {
|
||||
tld = strings.TrimPrefix(tld, ".")
|
||||
domains = append(domains, domain+"."+tld)
|
||||
}
|
||||
}
|
||||
|
||||
req := hostingerAvailabilityRequest{Domains: domains}
|
||||
|
||||
var results []hostingerAvailabilityResult
|
||||
if err := c.doRequest(http.MethodPost, "/api/domains/v1/availability", req, &results); err != nil {
|
||||
return nil, fmt.Errorf("check domain availability: %w", err)
|
||||
}
|
||||
|
||||
avail := make([]hosting.DomainAvailability, 0, len(results))
|
||||
for _, r := range results {
|
||||
da := hosting.DomainAvailability{
|
||||
Domain: r.Domain,
|
||||
Available: r.Available,
|
||||
}
|
||||
// Extract TLD from the domain name.
|
||||
if idx := strings.Index(r.Domain, "."); idx >= 0 {
|
||||
da.TLD = r.Domain[idx+1:]
|
||||
}
|
||||
if r.Price != nil {
|
||||
da.Price = r.Price.Amount
|
||||
da.Currency = r.Price.Currency
|
||||
}
|
||||
avail = append(avail, da)
|
||||
}
|
||||
return avail, nil
|
||||
}
|
||||
|
||||
// PurchaseDomain registers a new domain.
|
||||
func (c *Client) PurchaseDomain(req hosting.DomainPurchaseRequest) (*hosting.OrderResult, error) {
|
||||
body := hostingerPurchaseRequest{
|
||||
Domain: req.Domain,
|
||||
Period: req.Years,
|
||||
AutoRenew: req.AutoRenew,
|
||||
Privacy: req.Privacy,
|
||||
PaymentMethodID: req.PaymentMethod,
|
||||
}
|
||||
|
||||
var d hostingerDomain
|
||||
if err := c.doRequest(http.MethodPost, "/api/domains/v1/portfolio", body, &d); err != nil {
|
||||
return nil, fmt.Errorf("purchase domain %s: %w", req.Domain, err)
|
||||
}
|
||||
|
||||
return &hosting.OrderResult{
|
||||
OrderID: d.Domain,
|
||||
Status: "completed",
|
||||
Message: fmt.Sprintf("domain %s registered", d.Domain),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SetNameservers updates the nameservers for a domain.
|
||||
func (c *Client) SetNameservers(domain string, nameservers []string) error {
|
||||
path := fmt.Sprintf("/api/domains/v1/portfolio/%s/nameservers", url.PathEscape(domain))
|
||||
body := hostingerNameserversRequest{Nameservers: nameservers}
|
||||
|
||||
if err := c.doRequest(http.MethodPut, path, body, nil); err != nil {
|
||||
return fmt.Errorf("set nameservers for %s: %w", domain, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnableDomainLock enables the registrar lock for a domain.
|
||||
func (c *Client) EnableDomainLock(domain string) error {
|
||||
path := fmt.Sprintf("/api/domains/v1/portfolio/%s/domain-lock", url.PathEscape(domain))
|
||||
if err := c.doRequest(http.MethodPut, path, nil, nil); err != nil {
|
||||
return fmt.Errorf("enable domain lock for %s: %w", domain, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DisableDomainLock disables the registrar lock for a domain.
|
||||
func (c *Client) DisableDomainLock(domain string) error {
|
||||
path := fmt.Sprintf("/api/domains/v1/portfolio/%s/domain-lock", url.PathEscape(domain))
|
||||
if err := c.doRequest(http.MethodDelete, path, nil, nil); err != nil {
|
||||
return fmt.Errorf("disable domain lock for %s: %w", domain, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnablePrivacyProtection enables WHOIS privacy protection for a domain.
|
||||
func (c *Client) EnablePrivacyProtection(domain string) error {
|
||||
path := fmt.Sprintf("/api/domains/v1/portfolio/%s/privacy-protection", url.PathEscape(domain))
|
||||
if err := c.doRequest(http.MethodPut, path, nil, nil); err != nil {
|
||||
return fmt.Errorf("enable privacy protection for %s: %w", domain, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DisablePrivacyProtection disables WHOIS privacy protection for a domain.
|
||||
func (c *Client) DisablePrivacyProtection(domain string) error {
|
||||
path := fmt.Sprintf("/api/domains/v1/portfolio/%s/privacy-protection", url.PathEscape(domain))
|
||||
if err := c.doRequest(http.MethodDelete, path, nil, nil); err != nil {
|
||||
return fmt.Errorf("disable privacy protection for %s: %w", domain, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// toSummaryDomain converts a Hostinger domain to the summary Domain type.
|
||||
func toSummaryDomain(d hostingerDomain) hosting.Domain {
|
||||
expires, _ := time.Parse(time.RFC3339, d.ExpirationDate)
|
||||
return hosting.Domain{
|
||||
Name: d.Domain,
|
||||
Status: d.Status,
|
||||
ExpiresAt: expires,
|
||||
}
|
||||
}
|
||||
|
||||
// toDetailDomain converts a Hostinger domain to the full DomainDetail type.
|
||||
func toDetailDomain(d hostingerDomain) hosting.DomainDetail {
|
||||
expires, _ := time.Parse(time.RFC3339, d.ExpirationDate)
|
||||
return hosting.DomainDetail{
|
||||
Name: d.Domain,
|
||||
Status: d.Status,
|
||||
Registrar: "hostinger",
|
||||
ExpiresAt: expires,
|
||||
AutoRenew: d.AutoRenew,
|
||||
Locked: d.DomainLock,
|
||||
PrivacyProtection: d.PrivacyProtection,
|
||||
Nameservers: d.Nameservers,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package hostinger
|
||||
|
||||
import "setec-manager/internal/hosting"
|
||||
|
||||
func init() {
|
||||
hosting.Register(New(""))
|
||||
}
|
||||
219
services/setec-manager/internal/hosting/hostinger/vps.go
Normal file
219
services/setec-manager/internal/hosting/hostinger/vps.go
Normal file
@@ -0,0 +1,219 @@
|
||||
package hostinger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"setec-manager/internal/hosting"
|
||||
)
|
||||
|
||||
// hostingerVM is the Hostinger API representation of a virtual machine.
|
||||
type hostingerVM struct {
|
||||
ID int `json:"id"`
|
||||
Hostname string `json:"hostname"`
|
||||
Status string `json:"status"`
|
||||
Plan string `json:"plan"`
|
||||
DataCenter string `json:"data_center"`
|
||||
IPv4 string `json:"ipv4"`
|
||||
IPv6 string `json:"ipv6"`
|
||||
OS string `json:"os"`
|
||||
CPUs int `json:"cpus"`
|
||||
RAMMB int `json:"ram_mb"`
|
||||
DiskGB int `json:"disk_gb"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// hostingerDataCenter is the Hostinger API representation of a data center.
|
||||
type hostingerDataCenter struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Location string `json:"location"`
|
||||
Country string `json:"country"`
|
||||
}
|
||||
|
||||
// hostingerSSHKey is the Hostinger API representation of an SSH key.
|
||||
type hostingerSSHKey struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
PublicKey string `json:"public_key"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// hostingerCreateVMRequest is the request body for creating a VM.
|
||||
type hostingerCreateVMRequest struct {
|
||||
Hostname string `json:"hostname"`
|
||||
Plan string `json:"plan"`
|
||||
DataCenterID int `json:"data_center_id"`
|
||||
OS string `json:"template"`
|
||||
Password string `json:"password,omitempty"`
|
||||
SSHKeyID *int `json:"ssh_key_id,omitempty"`
|
||||
}
|
||||
|
||||
// hostingerCreateVMResponse is the response from the VM creation endpoint.
|
||||
type hostingerCreateVMResponse struct {
|
||||
OrderID string `json:"order_id"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// hostingerAddSSHKeyRequest is the request body for adding an SSH key.
|
||||
type hostingerAddSSHKeyRequest struct {
|
||||
Name string `json:"name"`
|
||||
PublicKey string `json:"public_key"`
|
||||
}
|
||||
|
||||
// ListVMs retrieves all virtual machines in the account.
|
||||
func (c *Client) ListVMs() ([]hosting.VirtualMachine, error) {
|
||||
var vms []hostingerVM
|
||||
if err := c.doRequest(http.MethodGet, "/api/vps/v1/virtual-machines", nil, &vms); err != nil {
|
||||
return nil, fmt.Errorf("list VMs: %w", err)
|
||||
}
|
||||
|
||||
result := make([]hosting.VirtualMachine, 0, len(vms))
|
||||
for _, vm := range vms {
|
||||
result = append(result, toGenericVM(vm))
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetVM retrieves a specific virtual machine by ID.
|
||||
func (c *Client) GetVM(id string) (*hosting.VirtualMachine, error) {
|
||||
path := fmt.Sprintf("/api/vps/v1/virtual-machines/%s", url.PathEscape(id))
|
||||
|
||||
var vm hostingerVM
|
||||
if err := c.doRequest(http.MethodGet, path, nil, &vm); err != nil {
|
||||
return nil, fmt.Errorf("get VM %s: %w", id, err)
|
||||
}
|
||||
|
||||
result := toGenericVM(vm)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// CreateVM provisions a new virtual machine.
|
||||
func (c *Client) CreateVM(req hosting.VMCreateRequest) (*hosting.OrderResult, error) {
|
||||
body := hostingerCreateVMRequest{
|
||||
Hostname: req.Hostname,
|
||||
Plan: req.Plan,
|
||||
OS: req.OS,
|
||||
Password: req.Password,
|
||||
}
|
||||
|
||||
// Parse data center ID from string to int for the Hostinger API.
|
||||
dcID, err := strconv.Atoi(req.DataCenterID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid data center ID %q: must be numeric", req.DataCenterID)
|
||||
}
|
||||
body.DataCenterID = dcID
|
||||
|
||||
// Parse SSH key ID if provided.
|
||||
if req.SSHKeyID != "" {
|
||||
keyID, err := strconv.Atoi(req.SSHKeyID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid SSH key ID %q: must be numeric", req.SSHKeyID)
|
||||
}
|
||||
body.SSHKeyID = &keyID
|
||||
}
|
||||
|
||||
var resp hostingerCreateVMResponse
|
||||
if err := c.doRequest(http.MethodPost, "/api/vps/v1/virtual-machines", body, &resp); err != nil {
|
||||
return nil, fmt.Errorf("create VM: %w", err)
|
||||
}
|
||||
|
||||
return &hosting.OrderResult{
|
||||
OrderID: resp.OrderID,
|
||||
Status: resp.Status,
|
||||
Message: resp.Message,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListDataCenters retrieves all available data centers.
|
||||
func (c *Client) ListDataCenters() ([]hosting.DataCenter, error) {
|
||||
var dcs []hostingerDataCenter
|
||||
if err := c.doRequest(http.MethodGet, "/api/vps/v1/data-centers", nil, &dcs); err != nil {
|
||||
return nil, fmt.Errorf("list data centers: %w", err)
|
||||
}
|
||||
|
||||
result := make([]hosting.DataCenter, 0, len(dcs))
|
||||
for _, dc := range dcs {
|
||||
result = append(result, hosting.DataCenter{
|
||||
ID: strconv.Itoa(dc.ID),
|
||||
Name: dc.Name,
|
||||
Location: dc.Location,
|
||||
Country: dc.Country,
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ListSSHKeys retrieves all SSH keys in the account.
|
||||
func (c *Client) ListSSHKeys() ([]hosting.SSHKey, error) {
|
||||
var keys []hostingerSSHKey
|
||||
if err := c.doRequest(http.MethodGet, "/api/vps/v1/public-keys", nil, &keys); err != nil {
|
||||
return nil, fmt.Errorf("list SSH keys: %w", err)
|
||||
}
|
||||
|
||||
result := make([]hosting.SSHKey, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
created, _ := time.Parse(time.RFC3339, k.CreatedAt)
|
||||
result = append(result, hosting.SSHKey{
|
||||
ID: strconv.Itoa(k.ID),
|
||||
Name: k.Name,
|
||||
PublicKey: k.PublicKey,
|
||||
CreatedAt: created,
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// AddSSHKey uploads a new SSH public key.
|
||||
func (c *Client) AddSSHKey(name, publicKey string) (*hosting.SSHKey, error) {
|
||||
body := hostingerAddSSHKeyRequest{
|
||||
Name: name,
|
||||
PublicKey: publicKey,
|
||||
}
|
||||
|
||||
var key hostingerSSHKey
|
||||
if err := c.doRequest(http.MethodPost, "/api/vps/v1/public-keys", body, &key); err != nil {
|
||||
return nil, fmt.Errorf("add SSH key: %w", err)
|
||||
}
|
||||
|
||||
created, _ := time.Parse(time.RFC3339, key.CreatedAt)
|
||||
return &hosting.SSHKey{
|
||||
ID: strconv.Itoa(key.ID),
|
||||
Name: key.Name,
|
||||
PublicKey: key.PublicKey,
|
||||
CreatedAt: created,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DeleteSSHKey removes an SSH key by ID.
|
||||
func (c *Client) DeleteSSHKey(id string) error {
|
||||
path := fmt.Sprintf("/api/vps/v1/public-keys/%s", url.PathEscape(id))
|
||||
if err := c.doRequest(http.MethodDelete, path, nil, nil); err != nil {
|
||||
return fmt.Errorf("delete SSH key %s: %w", id, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// toGenericVM converts a Hostinger VM to the generic VirtualMachine type.
|
||||
func toGenericVM(vm hostingerVM) hosting.VirtualMachine {
|
||||
created, _ := time.Parse(time.RFC3339, vm.CreatedAt)
|
||||
|
||||
return hosting.VirtualMachine{
|
||||
ID: strconv.Itoa(vm.ID),
|
||||
Hostname: vm.Hostname,
|
||||
IPAddress: vm.IPv4,
|
||||
IPv6: vm.IPv6,
|
||||
Status: vm.Status,
|
||||
Plan: vm.Plan,
|
||||
DataCenter: vm.DataCenter,
|
||||
OS: vm.OS,
|
||||
CPUs: vm.CPUs,
|
||||
RAMBytes: int64(vm.RAMMB) * 1024 * 1024,
|
||||
DiskBytes: int64(vm.DiskGB) * 1024 * 1024 * 1024,
|
||||
CreatedAt: created,
|
||||
}
|
||||
}
|
||||
287
services/setec-manager/internal/hosting/provider.go
Normal file
287
services/setec-manager/internal/hosting/provider.go
Normal file
@@ -0,0 +1,287 @@
|
||||
package hosting
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ErrNotSupported is returned when a provider does not support a given operation.
|
||||
var ErrNotSupported = errors.New("operation not supported by this provider")
|
||||
|
||||
// Provider is the interface all hosting service integrations must implement.
|
||||
// Not all providers support all features -- methods should return ErrNotSupported
|
||||
// for unsupported operations.
|
||||
type Provider interface {
|
||||
// Name returns the provider identifier (e.g. "hostinger", "digitalocean").
|
||||
Name() string
|
||||
|
||||
// DisplayName returns a human-readable provider name.
|
||||
DisplayName() string
|
||||
|
||||
// --- Authentication ---
|
||||
|
||||
// Configure applies the given configuration to the provider.
|
||||
Configure(cfg ProviderConfig) error
|
||||
|
||||
// TestConnection verifies that the provider credentials are valid.
|
||||
TestConnection() error
|
||||
|
||||
// --- DNS Management ---
|
||||
|
||||
// ListDNSRecords returns all DNS records for a domain.
|
||||
ListDNSRecords(domain string) ([]DNSRecord, error)
|
||||
|
||||
// CreateDNSRecord adds a single DNS record to a domain.
|
||||
CreateDNSRecord(domain string, record DNSRecord) error
|
||||
|
||||
// UpdateDNSRecords replaces DNS records for a domain. If overwrite is true,
|
||||
// all existing records are removed first.
|
||||
UpdateDNSRecords(domain string, records []DNSRecord, overwrite bool) error
|
||||
|
||||
// DeleteDNSRecord removes DNS records matching the filter.
|
||||
DeleteDNSRecord(domain string, filter DNSRecordFilter) error
|
||||
|
||||
// ResetDNSRecords restores the default DNS records for a domain.
|
||||
ResetDNSRecords(domain string) error
|
||||
|
||||
// --- Domain Management ---
|
||||
|
||||
// ListDomains returns all domains on the account.
|
||||
ListDomains() ([]Domain, error)
|
||||
|
||||
// GetDomain returns detailed information about a single domain.
|
||||
GetDomain(domain string) (*DomainDetail, error)
|
||||
|
||||
// CheckDomainAvailability checks registration availability across TLDs.
|
||||
CheckDomainAvailability(domain string, tlds []string) ([]DomainAvailability, error)
|
||||
|
||||
// PurchaseDomain registers a new domain.
|
||||
PurchaseDomain(req DomainPurchaseRequest) (*OrderResult, error)
|
||||
|
||||
// SetNameservers configures the nameservers for a domain.
|
||||
SetNameservers(domain string, nameservers []string) error
|
||||
|
||||
// EnableDomainLock enables the registrar lock on a domain.
|
||||
EnableDomainLock(domain string) error
|
||||
|
||||
// DisableDomainLock disables the registrar lock on a domain.
|
||||
DisableDomainLock(domain string) error
|
||||
|
||||
// EnablePrivacyProtection enables WHOIS privacy for a domain.
|
||||
EnablePrivacyProtection(domain string) error
|
||||
|
||||
// DisablePrivacyProtection disables WHOIS privacy for a domain.
|
||||
DisablePrivacyProtection(domain string) error
|
||||
|
||||
// --- VPS Management ---
|
||||
|
||||
// ListVMs returns all virtual machines on the account.
|
||||
ListVMs() ([]VirtualMachine, error)
|
||||
|
||||
// GetVM returns details for a single virtual machine.
|
||||
GetVM(id string) (*VirtualMachine, error)
|
||||
|
||||
// CreateVM provisions a new virtual machine.
|
||||
CreateVM(req VMCreateRequest) (*OrderResult, error)
|
||||
|
||||
// ListDataCenters returns available data center locations.
|
||||
ListDataCenters() ([]DataCenter, error)
|
||||
|
||||
// --- SSH Keys ---
|
||||
|
||||
// ListSSHKeys returns all SSH keys on the account.
|
||||
ListSSHKeys() ([]SSHKey, error)
|
||||
|
||||
// AddSSHKey uploads a new SSH public key.
|
||||
AddSSHKey(name, publicKey string) (*SSHKey, error)
|
||||
|
||||
// DeleteSSHKey removes an SSH key by ID.
|
||||
DeleteSSHKey(id string) error
|
||||
|
||||
// --- Billing ---
|
||||
|
||||
// ListSubscriptions returns all active subscriptions.
|
||||
ListSubscriptions() ([]Subscription, error)
|
||||
|
||||
// GetCatalog returns available products in a category.
|
||||
GetCatalog(category string) ([]CatalogItem, error)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Model types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// ProviderConfig holds the credentials and settings needed to connect to a
|
||||
// hosting provider.
|
||||
type ProviderConfig struct {
|
||||
APIKey string `json:"api_key"`
|
||||
APISecret string `json:"api_secret,omitempty"`
|
||||
BaseURL string `json:"base_url,omitempty"`
|
||||
Extra map[string]string `json:"extra,omitempty"`
|
||||
}
|
||||
|
||||
// DNSRecord represents a single DNS record.
|
||||
type DNSRecord struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"` // A, AAAA, CNAME, MX, TXT, etc.
|
||||
Content string `json:"content"`
|
||||
TTL int `json:"ttl,omitempty"` // seconds; 0 means provider default
|
||||
Priority int `json:"priority,omitempty"` // used by MX, SRV
|
||||
}
|
||||
|
||||
// DNSRecordFilter identifies DNS records to match for deletion or lookup.
|
||||
type DNSRecordFilter struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
// Domain is a summary of a domain on the account.
|
||||
type Domain struct {
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
}
|
||||
|
||||
// DomainDetail contains full information about a domain registration.
|
||||
type DomainDetail struct {
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
Registrar string `json:"registrar"`
|
||||
RegisteredAt time.Time `json:"registered_at"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
AutoRenew bool `json:"auto_renew"`
|
||||
Locked bool `json:"locked"`
|
||||
PrivacyProtection bool `json:"privacy_protection"`
|
||||
Nameservers []string `json:"nameservers"`
|
||||
}
|
||||
|
||||
// DomainAvailability reports whether a domain + TLD combination can be
|
||||
// registered and its price.
|
||||
type DomainAvailability struct {
|
||||
Domain string `json:"domain"`
|
||||
TLD string `json:"tld"`
|
||||
Available bool `json:"available"`
|
||||
Price float64 `json:"price"` // in the provider's default currency
|
||||
Currency string `json:"currency"`
|
||||
}
|
||||
|
||||
// DomainPurchaseRequest contains everything needed to register a domain.
|
||||
type DomainPurchaseRequest struct {
|
||||
Domain string `json:"domain"`
|
||||
Years int `json:"years"`
|
||||
AutoRenew bool `json:"auto_renew"`
|
||||
Privacy bool `json:"privacy"`
|
||||
PaymentMethod string `json:"payment_method,omitempty"`
|
||||
}
|
||||
|
||||
// OrderResult is returned after a purchase or provisioning request.
|
||||
type OrderResult struct {
|
||||
OrderID string `json:"order_id"`
|
||||
Status string `json:"status"` // e.g. "pending", "completed", "failed"
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// VirtualMachine represents a VPS instance.
|
||||
type VirtualMachine struct {
|
||||
ID string `json:"id"`
|
||||
Hostname string `json:"hostname"`
|
||||
IPAddress string `json:"ip_address"`
|
||||
IPv6 string `json:"ipv6,omitempty"`
|
||||
Status string `json:"status"` // running, stopped, provisioning, etc.
|
||||
Plan string `json:"plan"`
|
||||
DataCenter string `json:"data_center"`
|
||||
OS string `json:"os"`
|
||||
CPUs int `json:"cpus"`
|
||||
RAMBytes int64 `json:"ram_bytes"`
|
||||
DiskBytes int64 `json:"disk_bytes"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// VMCreateRequest contains everything needed to provision a new VPS.
|
||||
type VMCreateRequest struct {
|
||||
Hostname string `json:"hostname"`
|
||||
Plan string `json:"plan"`
|
||||
DataCenterID string `json:"data_center_id"`
|
||||
OS string `json:"os"`
|
||||
SSHKeyID string `json:"ssh_key_id,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
}
|
||||
|
||||
// DataCenter represents a physical hosting location.
|
||||
type DataCenter struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Location string `json:"location"` // city or region
|
||||
Country string `json:"country"`
|
||||
}
|
||||
|
||||
// SSHKey is a stored SSH public key.
|
||||
type SSHKey struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
PublicKey string `json:"public_key"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// Subscription represents a billing subscription.
|
||||
type Subscription struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"` // active, cancelled, expired
|
||||
RenewsAt time.Time `json:"renews_at"`
|
||||
Price float64 `json:"price"`
|
||||
Currency string `json:"currency"`
|
||||
}
|
||||
|
||||
// CatalogItem is a purchasable product or plan.
|
||||
type CatalogItem struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Category string `json:"category"`
|
||||
Price float64 `json:"price"`
|
||||
Currency string `json:"currency"`
|
||||
Features map[string]string `json:"features,omitempty"`
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Provider registry
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
var (
|
||||
mu sync.RWMutex
|
||||
providers = map[string]Provider{}
|
||||
)
|
||||
|
||||
// Register adds a provider to the global registry. It panics if a provider
|
||||
// with the same name is already registered.
|
||||
func Register(p Provider) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
name := p.Name()
|
||||
if _, exists := providers[name]; exists {
|
||||
panic("hosting: provider already registered: " + name)
|
||||
}
|
||||
providers[name] = p
|
||||
}
|
||||
|
||||
// Get returns a registered provider by name.
|
||||
func Get(name string) (Provider, bool) {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
p, ok := providers[name]
|
||||
return p, ok
|
||||
}
|
||||
|
||||
// List returns the names of all registered providers, sorted alphabetically.
|
||||
func List() []string {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
names := make([]string, 0, len(providers))
|
||||
for name := range providers {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
return names
|
||||
}
|
||||
Reference in New Issue
Block a user