No One Can Stop Me Now
This commit is contained in:
1660
services/setec-manager/docs/api-reference.md
Normal file
1660
services/setec-manager/docs/api-reference.md
Normal file
File diff suppressed because it is too large
Load Diff
790
services/setec-manager/docs/custom-provider-guide.md
Normal file
790
services/setec-manager/docs/custom-provider-guide.md
Normal file
@@ -0,0 +1,790 @@
|
||||
# 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/<provider_name>/`. 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 `<config_dir>/<provider_name>.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.
|
||||
859
services/setec-manager/docs/hosting-providers.md
Normal file
859
services/setec-manager/docs/hosting-providers.md
Normal file
@@ -0,0 +1,859 @@
|
||||
# Hosting Provider Integration System
|
||||
|
||||
## Overview
|
||||
|
||||
Setec Manager includes a pluggable hosting provider architecture that lets you manage DNS records, domains, VPS instances, SSH keys, and billing subscriptions through a unified interface. The system is built around a Go `Provider` interface defined in `internal/hosting/provider.go`. Each hosting provider (e.g., Hostinger) implements this interface and auto-registers itself at import time via an `init()` function.
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
internal/hosting/
|
||||
provider.go -- Provider interface, model types, global registry
|
||||
store.go -- ProviderConfig type, ProviderConfigStore (disk persistence)
|
||||
config.go -- Legacy config store (being superseded by store.go)
|
||||
hostinger/
|
||||
client.go -- Hostinger HTTP client with retry/rate-limit handling
|
||||
dns.go -- Hostinger DNS implementation
|
||||
```
|
||||
|
||||
The registry is a process-global `map[string]Provider` guarded by a `sync.RWMutex`. Providers call `hosting.Register(&Provider{})` inside their package `init()` function. The main binary imports the provider package (e.g., `_ "setec-manager/internal/hosting/hostinger"`) to trigger registration.
|
||||
|
||||
Provider credentials are stored as individual JSON files in a protected directory (`0700` directory, `0600` files) managed by `ProviderConfigStore`. Each file is named `<provider>.json` and contains the `ProviderConfig` struct:
|
||||
|
||||
```json
|
||||
{
|
||||
"provider": "hostinger",
|
||||
"api_key": "Bearer ...",
|
||||
"api_secret": "",
|
||||
"extra": {},
|
||||
"connected": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Supported Providers
|
||||
|
||||
### Hostinger (Built-in)
|
||||
|
||||
| Capability | Supported | Notes |
|
||||
|---|---|---|
|
||||
| DNS Management | Yes | Full CRUD, validation before writes, zone reset |
|
||||
| Domain Management | Yes | List, lookup, availability check, purchase, nameservers, lock, privacy |
|
||||
| VPS Management | Yes | List, create, get details, data center listing |
|
||||
| SSH Key Management | Yes | Add, list, delete |
|
||||
| Billing | Yes | Subscriptions and catalog |
|
||||
|
||||
The Hostinger provider communicates with `https://developers.hostinger.com` using a Bearer token. It includes automatic retry with back-off on HTTP 429 (rate limit) responses, up to 3 retries per request.
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
### Via the UI
|
||||
|
||||
1. Navigate to the Hosting Providers section in the Setec Manager dashboard.
|
||||
2. Select "Hostinger" from the provider list.
|
||||
3. Enter your API token (obtained from hPanel -- see [Hostinger Setup Guide](hostinger-setup.md)).
|
||||
4. Click "Test Connection" to verify the token is valid.
|
||||
5. Click "Save" to persist the configuration.
|
||||
|
||||
### Via Config Files
|
||||
|
||||
Provider configurations are stored as JSON files in the config directory (typically `/opt/setec-manager/data/hosting/`).
|
||||
|
||||
Create or edit the file directly:
|
||||
|
||||
```bash
|
||||
mkdir -p /opt/setec-manager/data/hosting
|
||||
cat > /opt/setec-manager/data/hosting/hostinger.json << 'EOF'
|
||||
{
|
||||
"provider": "hostinger",
|
||||
"api_key": "YOUR_BEARER_TOKEN_HERE",
|
||||
"api_secret": "",
|
||||
"extra": {},
|
||||
"connected": true
|
||||
}
|
||||
EOF
|
||||
chmod 600 /opt/setec-manager/data/hosting/hostinger.json
|
||||
```
|
||||
|
||||
### Via API
|
||||
|
||||
```bash
|
||||
curl -X POST https://your-server:9090/api/hosting/providers/hostinger/configure \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"api_key": "YOUR_HOSTINGER_API_TOKEN"
|
||||
}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Reference
|
||||
|
||||
All hosting endpoints require authentication via JWT (cookie or `Authorization: Bearer` header). The base URL is `https://your-server:9090`.
|
||||
|
||||
### Provider Management
|
||||
|
||||
#### List Providers
|
||||
|
||||
```
|
||||
GET /api/hosting/providers
|
||||
```
|
||||
|
||||
Returns all registered hosting providers and their connection status.
|
||||
|
||||
```bash
|
||||
curl -s https://your-server:9090/api/hosting/providers \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "hostinger",
|
||||
"display_name": "Hostinger",
|
||||
"connected": true
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### Configure Provider
|
||||
|
||||
```
|
||||
POST /api/hosting/providers/{provider}/configure
|
||||
```
|
||||
|
||||
Sets the API credentials for a provider.
|
||||
|
||||
```bash
|
||||
curl -X POST https://your-server:9090/api/hosting/providers/hostinger/configure \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"api_key": "YOUR_API_TOKEN"
|
||||
}'
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"status": "configured"
|
||||
}
|
||||
```
|
||||
|
||||
#### Test Connection
|
||||
|
||||
```
|
||||
POST /api/hosting/providers/{provider}/test
|
||||
```
|
||||
|
||||
Verifies that the saved credentials are valid by making a test API call.
|
||||
|
||||
```bash
|
||||
curl -X POST https://your-server:9090/api/hosting/providers/hostinger/test \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"message": "Connection successful"
|
||||
}
|
||||
```
|
||||
|
||||
#### Remove Provider Configuration
|
||||
|
||||
```
|
||||
DELETE /api/hosting/providers/{provider}
|
||||
```
|
||||
|
||||
Deletes saved credentials for a provider.
|
||||
|
||||
```bash
|
||||
curl -X DELETE https://your-server:9090/api/hosting/providers/hostinger \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"status": "deleted"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## DNS Management
|
||||
|
||||
### List DNS Records
|
||||
|
||||
```
|
||||
GET /api/hosting/providers/{provider}/dns/{domain}
|
||||
```
|
||||
|
||||
Returns all DNS records for the specified domain.
|
||||
|
||||
```bash
|
||||
curl -s https://your-server:9090/api/hosting/providers/hostinger/dns/example.com \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "@/A/0",
|
||||
"type": "A",
|
||||
"name": "@",
|
||||
"content": "93.184.216.34",
|
||||
"ttl": 14400,
|
||||
"priority": 0
|
||||
},
|
||||
{
|
||||
"id": "www/CNAME/0",
|
||||
"type": "CNAME",
|
||||
"name": "www",
|
||||
"content": "example.com",
|
||||
"ttl": 14400,
|
||||
"priority": 0
|
||||
},
|
||||
{
|
||||
"id": "@/MX/10",
|
||||
"type": "MX",
|
||||
"name": "@",
|
||||
"content": "mail.example.com",
|
||||
"ttl": 14400,
|
||||
"priority": 10
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Create DNS Record
|
||||
|
||||
```
|
||||
POST /api/hosting/providers/{provider}/dns/{domain}
|
||||
```
|
||||
|
||||
Adds a new DNS record without overwriting existing records.
|
||||
|
||||
```bash
|
||||
curl -X POST https://your-server:9090/api/hosting/providers/hostinger/dns/example.com \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"type": "A",
|
||||
"name": "api",
|
||||
"content": "93.184.216.35",
|
||||
"ttl": 3600
|
||||
}'
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"status": "created"
|
||||
}
|
||||
```
|
||||
|
||||
### Update DNS Records (Batch)
|
||||
|
||||
```
|
||||
PUT /api/hosting/providers/{provider}/dns/{domain}
|
||||
```
|
||||
|
||||
Updates DNS records for a domain. If `overwrite` is `true`, all existing records are replaced; otherwise the records are merged.
|
||||
|
||||
The Hostinger provider validates records against the API before applying changes.
|
||||
|
||||
```bash
|
||||
curl -X PUT https://your-server:9090/api/hosting/providers/hostinger/dns/example.com \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"records": [
|
||||
{
|
||||
"type": "A",
|
||||
"name": "@",
|
||||
"content": "93.184.216.34",
|
||||
"ttl": 14400
|
||||
},
|
||||
{
|
||||
"type": "CNAME",
|
||||
"name": "www",
|
||||
"content": "example.com",
|
||||
"ttl": 14400
|
||||
}
|
||||
],
|
||||
"overwrite": false
|
||||
}'
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"status": "updated"
|
||||
}
|
||||
```
|
||||
|
||||
### Delete DNS Record
|
||||
|
||||
```
|
||||
DELETE /api/hosting/providers/{provider}/dns/{domain}?name={name}&type={type}
|
||||
```
|
||||
|
||||
Removes DNS records matching the given name and type.
|
||||
|
||||
```bash
|
||||
curl -X DELETE "https://your-server:9090/api/hosting/providers/hostinger/dns/example.com?name=api&type=A" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"status": "deleted"
|
||||
}
|
||||
```
|
||||
|
||||
### Reset DNS Zone
|
||||
|
||||
```
|
||||
POST /api/hosting/providers/{provider}/dns/{domain}/reset
|
||||
```
|
||||
|
||||
Resets the domain's DNS zone to the provider's default records. This is destructive and removes all custom records.
|
||||
|
||||
```bash
|
||||
curl -X POST https://your-server:9090/api/hosting/providers/hostinger/dns/example.com/reset \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"status": "reset"
|
||||
}
|
||||
```
|
||||
|
||||
### Supported DNS Record Types
|
||||
|
||||
| Type | Description | Priority Field |
|
||||
|---|---|---|
|
||||
| A | IPv4 address | No |
|
||||
| AAAA | IPv6 address | No |
|
||||
| CNAME | Canonical name / alias | No |
|
||||
| MX | Mail exchange | Yes |
|
||||
| TXT | Text record (SPF, DKIM, etc.) | No |
|
||||
| NS | Name server | No |
|
||||
| SRV | Service record | Yes |
|
||||
| CAA | Certificate Authority Authorization | No |
|
||||
|
||||
---
|
||||
|
||||
## Domain Management
|
||||
|
||||
### List Domains
|
||||
|
||||
```
|
||||
GET /api/hosting/providers/{provider}/domains
|
||||
```
|
||||
|
||||
```bash
|
||||
curl -s https://your-server:9090/api/hosting/providers/hostinger/domains \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "example.com",
|
||||
"registrar": "Hostinger",
|
||||
"status": "active",
|
||||
"expires_at": "2027-03-15T00:00:00Z",
|
||||
"auto_renew": true,
|
||||
"locked": true,
|
||||
"privacy_protection": true,
|
||||
"nameservers": ["ns1.dns-parking.com", "ns2.dns-parking.com"]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Get Domain Details
|
||||
|
||||
```
|
||||
GET /api/hosting/providers/{provider}/domains/{domain}
|
||||
```
|
||||
|
||||
```bash
|
||||
curl -s https://your-server:9090/api/hosting/providers/hostinger/domains/example.com \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"name": "example.com",
|
||||
"registrar": "Hostinger",
|
||||
"status": "active",
|
||||
"expires_at": "2027-03-15T00:00:00Z",
|
||||
"auto_renew": true,
|
||||
"locked": true,
|
||||
"privacy_protection": true,
|
||||
"nameservers": ["ns1.dns-parking.com", "ns2.dns-parking.com"]
|
||||
}
|
||||
```
|
||||
|
||||
### Check Domain Availability
|
||||
|
||||
```
|
||||
POST /api/hosting/providers/{provider}/domains/check
|
||||
```
|
||||
|
||||
```bash
|
||||
curl -X POST https://your-server:9090/api/hosting/providers/hostinger/domains/check \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"domains": ["cool-project.com", "cool-project.io", "cool-project.dev"]
|
||||
}'
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"domain": "cool-project.com",
|
||||
"available": true,
|
||||
"price": 9.99,
|
||||
"currency": "USD"
|
||||
},
|
||||
{
|
||||
"domain": "cool-project.io",
|
||||
"available": false
|
||||
},
|
||||
{
|
||||
"domain": "cool-project.dev",
|
||||
"available": true,
|
||||
"price": 14.99,
|
||||
"currency": "USD"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Purchase Domain
|
||||
|
||||
```
|
||||
POST /api/hosting/providers/{provider}/domains/purchase
|
||||
```
|
||||
|
||||
```bash
|
||||
curl -X POST https://your-server:9090/api/hosting/providers/hostinger/domains/purchase \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"domain": "cool-project.com",
|
||||
"period": 1,
|
||||
"auto_renew": true,
|
||||
"privacy_protection": true,
|
||||
"payment_method_id": "pm_abc123"
|
||||
}'
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"name": "cool-project.com",
|
||||
"status": "active",
|
||||
"expires_at": "2027-03-11T00:00:00Z",
|
||||
"auto_renew": true,
|
||||
"locked": false,
|
||||
"privacy_protection": true,
|
||||
"nameservers": ["ns1.dns-parking.com", "ns2.dns-parking.com"]
|
||||
}
|
||||
```
|
||||
|
||||
### Set Nameservers
|
||||
|
||||
```
|
||||
PUT /api/hosting/providers/{provider}/domains/{domain}/nameservers
|
||||
```
|
||||
|
||||
```bash
|
||||
curl -X PUT https://your-server:9090/api/hosting/providers/hostinger/domains/example.com/nameservers \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"nameservers": ["ns1.cloudflare.com", "ns2.cloudflare.com"]
|
||||
}'
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"status": "updated"
|
||||
}
|
||||
```
|
||||
|
||||
### Enable Domain Lock
|
||||
|
||||
```
|
||||
POST /api/hosting/providers/{provider}/domains/{domain}/lock
|
||||
```
|
||||
|
||||
Prevents unauthorized domain transfers.
|
||||
|
||||
```bash
|
||||
curl -X POST https://your-server:9090/api/hosting/providers/hostinger/domains/example.com/lock \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"status": "locked"
|
||||
}
|
||||
```
|
||||
|
||||
### Disable Domain Lock
|
||||
|
||||
```
|
||||
DELETE /api/hosting/providers/{provider}/domains/{domain}/lock
|
||||
```
|
||||
|
||||
```bash
|
||||
curl -X DELETE https://your-server:9090/api/hosting/providers/hostinger/domains/example.com/lock \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"status": "unlocked"
|
||||
}
|
||||
```
|
||||
|
||||
### Enable Privacy Protection
|
||||
|
||||
```
|
||||
POST /api/hosting/providers/{provider}/domains/{domain}/privacy
|
||||
```
|
||||
|
||||
Enables WHOIS privacy protection to hide registrant details.
|
||||
|
||||
```bash
|
||||
curl -X POST https://your-server:9090/api/hosting/providers/hostinger/domains/example.com/privacy \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"status": "enabled"
|
||||
}
|
||||
```
|
||||
|
||||
### Disable Privacy Protection
|
||||
|
||||
```
|
||||
DELETE /api/hosting/providers/{provider}/domains/{domain}/privacy
|
||||
```
|
||||
|
||||
```bash
|
||||
curl -X DELETE https://your-server:9090/api/hosting/providers/hostinger/domains/example.com/privacy \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"status": "disabled"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## VPS Management
|
||||
|
||||
### List Virtual Machines
|
||||
|
||||
```
|
||||
GET /api/hosting/providers/{provider}/vms
|
||||
```
|
||||
|
||||
```bash
|
||||
curl -s https://your-server:9090/api/hosting/providers/hostinger/vms \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "vm-abc123",
|
||||
"name": "production-1",
|
||||
"status": "running",
|
||||
"plan": "kvm-2",
|
||||
"region": "us-east-1",
|
||||
"ipv4": "93.184.216.34",
|
||||
"ipv6": "2606:2800:220:1:248:1893:25c8:1946",
|
||||
"os": "Ubuntu 22.04",
|
||||
"cpus": 2,
|
||||
"memory_mb": 4096,
|
||||
"disk_gb": 80,
|
||||
"bandwidth_gb": 4000,
|
||||
"created_at": "2025-01-15T10:30:00Z",
|
||||
"labels": {
|
||||
"env": "production"
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Get VM Details
|
||||
|
||||
```
|
||||
GET /api/hosting/providers/{provider}/vms/{id}
|
||||
```
|
||||
|
||||
```bash
|
||||
curl -s https://your-server:9090/api/hosting/providers/hostinger/vms/vm-abc123 \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
**Response:** Same shape as a single item from the list response.
|
||||
|
||||
### Create VM
|
||||
|
||||
```
|
||||
POST /api/hosting/providers/{provider}/vms
|
||||
```
|
||||
|
||||
```bash
|
||||
curl -X POST https://your-server:9090/api/hosting/providers/hostinger/vms \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"plan": "kvm-2",
|
||||
"data_center_id": "us-east-1",
|
||||
"template": "ubuntu-22.04",
|
||||
"password": "SecurePassword123!",
|
||||
"hostname": "web-server-2",
|
||||
"ssh_key_id": "key-abc123",
|
||||
"payment_method_id": "pm_abc123"
|
||||
}'
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"id": "vm-def456",
|
||||
"name": "web-server-2",
|
||||
"status": "creating",
|
||||
"plan": "kvm-2",
|
||||
"region": "us-east-1",
|
||||
"os": "Ubuntu 22.04",
|
||||
"cpus": 2,
|
||||
"memory_mb": 4096,
|
||||
"disk_gb": 80,
|
||||
"bandwidth_gb": 4000,
|
||||
"created_at": "2026-03-11T14:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### List Data Centers
|
||||
|
||||
```
|
||||
GET /api/hosting/providers/{provider}/datacenters
|
||||
```
|
||||
|
||||
Returns available regions/data centers for VM creation.
|
||||
|
||||
```bash
|
||||
curl -s https://your-server:9090/api/hosting/providers/hostinger/datacenters \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "us-east-1",
|
||||
"name": "US East",
|
||||
"location": "New York",
|
||||
"country": "US"
|
||||
},
|
||||
{
|
||||
"id": "eu-west-1",
|
||||
"name": "EU West",
|
||||
"location": "Amsterdam",
|
||||
"country": "NL"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SSH Key Management
|
||||
|
||||
### List SSH Keys
|
||||
|
||||
```
|
||||
GET /api/hosting/providers/{provider}/ssh-keys
|
||||
```
|
||||
|
||||
```bash
|
||||
curl -s https://your-server:9090/api/hosting/providers/hostinger/ssh-keys \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "key-abc123",
|
||||
"name": "deploy-key",
|
||||
"fingerprint": "SHA256:abcd1234...",
|
||||
"public_key": "ssh-ed25519 AAAAC3Nz..."
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Add SSH Key
|
||||
|
||||
```
|
||||
POST /api/hosting/providers/{provider}/ssh-keys
|
||||
```
|
||||
|
||||
```bash
|
||||
curl -X POST https://your-server:9090/api/hosting/providers/hostinger/ssh-keys \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "new-deploy-key",
|
||||
"public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... user@host"
|
||||
}'
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"id": "key-def456",
|
||||
"name": "new-deploy-key",
|
||||
"fingerprint": "SHA256:efgh5678...",
|
||||
"public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI..."
|
||||
}
|
||||
```
|
||||
|
||||
### Delete SSH Key
|
||||
|
||||
```
|
||||
DELETE /api/hosting/providers/{provider}/ssh-keys/{id}
|
||||
```
|
||||
|
||||
```bash
|
||||
curl -X DELETE https://your-server:9090/api/hosting/providers/hostinger/ssh-keys/key-def456 \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"status": "deleted"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Billing
|
||||
|
||||
### List Subscriptions
|
||||
|
||||
```
|
||||
GET /api/hosting/providers/{provider}/subscriptions
|
||||
```
|
||||
|
||||
```bash
|
||||
curl -s https://your-server:9090/api/hosting/providers/hostinger/subscriptions \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "sub-abc123",
|
||||
"name": "Premium Web Hosting",
|
||||
"status": "active",
|
||||
"plan": "premium-hosting-48m",
|
||||
"price": 2.99,
|
||||
"currency": "USD",
|
||||
"renews_at": "2027-03-15T00:00:00Z",
|
||||
"created_at": "2023-03-15T00:00:00Z"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Get Product Catalog
|
||||
|
||||
```
|
||||
GET /api/hosting/providers/{provider}/catalog
|
||||
```
|
||||
|
||||
```bash
|
||||
curl -s https://your-server:9090/api/hosting/providers/hostinger/catalog \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "kvm-2",
|
||||
"name": "KVM 2",
|
||||
"category": "vps",
|
||||
"price_cents": 1199,
|
||||
"currency": "USD",
|
||||
"period": "monthly",
|
||||
"description": "2 vCPU, 4 GB RAM, 80 GB SSD"
|
||||
},
|
||||
{
|
||||
"id": "premium-hosting-12m",
|
||||
"name": "Premium Web Hosting",
|
||||
"category": "hosting",
|
||||
"price_cents": 299,
|
||||
"currency": "USD",
|
||||
"period": "monthly",
|
||||
"description": "100 websites, 100 GB SSD, free SSL"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Responses
|
||||
|
||||
All endpoints return errors in a consistent format:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "description of what went wrong"
|
||||
}
|
||||
```
|
||||
|
||||
| HTTP Status | Meaning |
|
||||
|---|---|
|
||||
| 400 | Bad request (invalid parameters) |
|
||||
| 401 | Authentication required or token invalid |
|
||||
| 404 | Provider or resource not found |
|
||||
| 409 | Conflict (e.g., duplicate resource) |
|
||||
| 429 | Rate limited by the upstream provider |
|
||||
| 500 | Internal server error |
|
||||
| 501 | Provider does not support this operation (`ErrNotSupported`) |
|
||||
|
||||
When a provider does not implement a particular capability, the endpoint returns HTTP 501 with an `ErrNotSupported` error message. This allows partial implementations where a provider only supports DNS management, for example.
|
||||
365
services/setec-manager/docs/hostinger-setup.md
Normal file
365
services/setec-manager/docs/hostinger-setup.md
Normal file
@@ -0,0 +1,365 @@
|
||||
# Hostinger Setup Guide
|
||||
|
||||
This guide covers configuring the Hostinger hosting provider integration in Setec Manager.
|
||||
|
||||
---
|
||||
|
||||
## Getting Your API Token
|
||||
|
||||
Hostinger provides API access through bearer tokens generated in the hPanel control panel.
|
||||
|
||||
### Step-by-Step
|
||||
|
||||
1. **Log in to hPanel.** Go to [https://hpanel.hostinger.com](https://hpanel.hostinger.com) and sign in with your Hostinger account.
|
||||
|
||||
2. **Navigate to your profile.** Click your profile icon or name in the top-right corner of the dashboard.
|
||||
|
||||
3. **Open Account Settings.** Select "Account Settings" or "Profile" from the dropdown menu.
|
||||
|
||||
4. **Go to the API section.** Look for the "API" or "API Tokens" tab. This may be under "Account" > "API" depending on your hPanel version.
|
||||
|
||||
5. **Generate a new token.** Click "Create API Token" or "Generate Token."
|
||||
- Give the token a descriptive name (e.g., `setec-manager`).
|
||||
- Select the permissions/scopes you need. For full Setec Manager integration, grant:
|
||||
- DNS management (read/write)
|
||||
- Domain management (read/write)
|
||||
- VPS management (read/write)
|
||||
- Billing (read)
|
||||
- Set an expiration if desired (recommended: no expiration for server-to-server use, but rotate periodically).
|
||||
|
||||
6. **Copy the token.** The token is shown only once. Copy it immediately and store it securely. It will look like a long alphanumeric string.
|
||||
|
||||
**Important:** Treat this token like a password. Anyone with the token has API access to your Hostinger account.
|
||||
|
||||
---
|
||||
|
||||
## Configuring in Setec Manager
|
||||
|
||||
### Via the Web UI
|
||||
|
||||
1. Log in to your Setec Manager dashboard at `https://your-server:9090`.
|
||||
2. Navigate to the Hosting Providers section.
|
||||
3. Click "Hostinger" from the provider list.
|
||||
4. Paste your API token into the "API Key" field.
|
||||
5. Click "Test Connection" -- you should see a success message confirming the token is valid.
|
||||
6. Click "Save Configuration" to persist the credentials.
|
||||
|
||||
### Via the API
|
||||
|
||||
```bash
|
||||
# Set your Setec Manager JWT token
|
||||
export TOKEN="your-setec-manager-jwt"
|
||||
|
||||
# Configure the Hostinger provider
|
||||
curl -X POST https://your-server:9090/api/hosting/providers/hostinger/configure \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"api_key": "YOUR_HOSTINGER_BEARER_TOKEN"
|
||||
}'
|
||||
|
||||
# Verify the connection
|
||||
curl -X POST https://your-server:9090/api/hosting/providers/hostinger/test \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
### Via Config File
|
||||
|
||||
Create the config file directly on the server:
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /opt/setec-manager/data/hosting
|
||||
sudo tee /opt/setec-manager/data/hosting/hostinger.json > /dev/null << 'EOF'
|
||||
{
|
||||
"provider": "hostinger",
|
||||
"api_key": "YOUR_HOSTINGER_BEARER_TOKEN",
|
||||
"api_secret": "",
|
||||
"extra": {},
|
||||
"connected": true
|
||||
}
|
||||
EOF
|
||||
sudo chmod 600 /opt/setec-manager/data/hosting/hostinger.json
|
||||
```
|
||||
|
||||
Restart Setec Manager for the config to be loaded:
|
||||
|
||||
```bash
|
||||
sudo systemctl restart setec-manager
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Available Features
|
||||
|
||||
The Hostinger provider supports all major integration capabilities:
|
||||
|
||||
| Feature | Status | Notes |
|
||||
|---|---|---|
|
||||
| DNS Record Listing | Supported | Lists all records in a zone |
|
||||
| DNS Record Creation | Supported | Adds records without overwriting |
|
||||
| DNS Record Update (Batch) | Supported | Validates before applying; supports overwrite mode |
|
||||
| DNS Record Deletion | Supported | Filter by name and/or type |
|
||||
| DNS Zone Reset | Supported | Resets to Hostinger default records |
|
||||
| Domain Listing | Supported | All domains on the account |
|
||||
| Domain Details | Supported | Full WHOIS and registration info |
|
||||
| Domain Availability Check | Supported | Batch check with pricing |
|
||||
| Domain Purchase | Supported | Requires valid payment method |
|
||||
| Nameserver Management | Supported | Update authoritative nameservers |
|
||||
| Domain Lock | Supported | Enable/disable transfer lock |
|
||||
| Privacy Protection | Supported | Enable/disable WHOIS privacy |
|
||||
| VPS Listing | Supported | All VPS instances |
|
||||
| VPS Details | Supported | Full specs, IP, status |
|
||||
| VPS Creation | Supported | Requires plan, template, data center |
|
||||
| Data Center Listing | Supported | Available regions for VM creation |
|
||||
| SSH Key Management | Supported | Add, list, delete public keys |
|
||||
| Subscription Listing | Supported | Active billing subscriptions |
|
||||
| Product Catalog | Supported | Available plans and pricing |
|
||||
|
||||
---
|
||||
|
||||
## Rate Limits
|
||||
|
||||
The Hostinger API enforces rate limiting on all endpoints. The Setec Manager integration handles rate limits automatically:
|
||||
|
||||
- **Detection:** HTTP 429 (Too Many Requests) responses are detected.
|
||||
- **Retry-After header:** The client reads the `Retry-After` header to determine how long to wait.
|
||||
- **Automatic retry:** Up to 3 retries are attempted with the specified back-off.
|
||||
- **Back-off cap:** Individual retry delays are capped at 60 seconds.
|
||||
- **Failure:** If all retries are exhausted, the error is returned to the caller.
|
||||
|
||||
### Best Practices
|
||||
|
||||
- Avoid rapid-fire bulk operations. Space out batch DNS updates.
|
||||
- Use the batch `UpdateDNSRecords` endpoint with multiple records in one call instead of creating records one at a time.
|
||||
- Cache domain and VM listings on the client side when possible.
|
||||
- If you see frequent 429 errors in logs, reduce the frequency of polling operations.
|
||||
|
||||
---
|
||||
|
||||
## DNS Record Management
|
||||
|
||||
### Hostinger API Endpoints Used
|
||||
|
||||
| Operation | Hostinger API Path |
|
||||
|---|---|
|
||||
| List records | `GET /api/dns/v1/zones/{domain}` |
|
||||
| Update records | `PUT /api/dns/v1/zones/{domain}` |
|
||||
| Validate records | `POST /api/dns/v1/zones/{domain}/validate` |
|
||||
| Delete records | `DELETE /api/dns/v1/zones/{domain}` |
|
||||
| Reset zone | `POST /api/dns/v1/zones/{domain}/reset` |
|
||||
|
||||
### Supported Record Types
|
||||
|
||||
| Type | Example Content | Priority | Notes |
|
||||
|---|---|---|---|
|
||||
| A | `93.184.216.34` | No | IPv4 address |
|
||||
| AAAA | `2606:2800:220:1::` | No | IPv6 address |
|
||||
| CNAME | `example.com` | No | Must be a hostname, not an IP |
|
||||
| MX | `mail.example.com` | Yes | Priority determines delivery order (lower = higher priority) |
|
||||
| TXT | `v=spf1 include:...` | No | Used for SPF, DKIM, domain verification |
|
||||
| NS | `ns1.example.com` | No | Nameserver delegation |
|
||||
| SRV | `sip.example.com` | Yes | Service location records |
|
||||
| CAA | `letsencrypt.org` | No | Certificate authority authorization |
|
||||
|
||||
### Record ID Synthesis
|
||||
|
||||
Hostinger does not return unique record IDs in zone listings. Setec Manager synthesizes an ID from `name/type/priority` for each record. For example, an MX record for the root domain with priority 10 gets the ID `@/MX/10`. This ID is used internally for tracking but should not be passed back to the Hostinger API.
|
||||
|
||||
### Validation Before Write
|
||||
|
||||
The Hostinger provider validates DNS records before applying changes. When you call `UpdateDNSRecords`, the system:
|
||||
|
||||
1. Converts generic `DNSRecord` structs to Hostinger-specific format.
|
||||
2. Sends the records to the `/validate` endpoint.
|
||||
3. If validation passes, sends the actual update to the zone endpoint.
|
||||
4. If validation fails, returns the validation error without modifying the zone.
|
||||
|
||||
This prevents malformed records from corrupting your DNS zone.
|
||||
|
||||
---
|
||||
|
||||
## Domain Management
|
||||
|
||||
### Purchasing Domains
|
||||
|
||||
Before purchasing a domain:
|
||||
|
||||
1. Check availability using the availability check endpoint.
|
||||
2. Note the price and currency in the response.
|
||||
3. Ensure you have a valid payment method configured in your Hostinger account.
|
||||
4. Submit the purchase request with the `payment_method_id` from your Hostinger account.
|
||||
|
||||
```bash
|
||||
# Check availability
|
||||
curl -X POST https://your-server:9090/api/hosting/providers/hostinger/domains/check \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"domains": ["my-new-site.com"]}'
|
||||
|
||||
# Purchase (if available)
|
||||
curl -X POST https://your-server:9090/api/hosting/providers/hostinger/domains/purchase \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"domain": "my-new-site.com",
|
||||
"period": 1,
|
||||
"auto_renew": true,
|
||||
"privacy_protection": true
|
||||
}'
|
||||
```
|
||||
|
||||
### Domain Transfers
|
||||
|
||||
Domain transfers are initiated outside of Setec Manager through the Hostinger hPanel. Once a domain is transferred to your Hostinger account, it will appear in `ListDomains` and can be managed through Setec Manager.
|
||||
|
||||
### WHOIS Privacy
|
||||
|
||||
Hostinger offers WHOIS privacy protection (also called "Domain Privacy Protection") that replaces your personal contact information in WHOIS records with proxy information. Enable it to keep your registrant details private:
|
||||
|
||||
```bash
|
||||
curl -X POST https://your-server:9090/api/hosting/providers/hostinger/domains/example.com/privacy \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## VPS Management
|
||||
|
||||
### Creating a VM
|
||||
|
||||
To create a VPS instance, you need three pieces of information:
|
||||
|
||||
1. **Plan ID** -- Get from the catalog endpoint (`GET /api/hosting/providers/hostinger/catalog`).
|
||||
2. **Data Center ID** -- Get from the data centers endpoint (`GET /api/hosting/providers/hostinger/datacenters`).
|
||||
3. **Template** -- The OS template name (e.g., `"ubuntu-22.04"`, `"debian-12"`, `"centos-9"`).
|
||||
|
||||
```bash
|
||||
# List available plans
|
||||
curl -s https://your-server:9090/api/hosting/providers/hostinger/catalog \
|
||||
-H "Authorization: Bearer $TOKEN" | jq '.[] | select(.category == "vps")'
|
||||
|
||||
# List data centers
|
||||
curl -s https://your-server:9090/api/hosting/providers/hostinger/datacenters \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
|
||||
# Create the VM
|
||||
curl -X POST https://your-server:9090/api/hosting/providers/hostinger/vms \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"plan": "kvm-2",
|
||||
"data_center_id": "us-east-1",
|
||||
"template": "ubuntu-22.04",
|
||||
"password": "YourSecurePassword!",
|
||||
"hostname": "app-server",
|
||||
"ssh_key_id": "key-abc123"
|
||||
}'
|
||||
```
|
||||
|
||||
### Docker Support
|
||||
|
||||
Hostinger VPS instances support Docker out of the box on Linux templates. After creating a VM:
|
||||
|
||||
1. SSH into the new VM.
|
||||
2. Install Docker using the standard installation method for your chosen OS.
|
||||
3. Alternatively, select a Docker-optimized template if available in your Hostinger account.
|
||||
|
||||
### VM Status Values
|
||||
|
||||
| Status | Description |
|
||||
|---|---|
|
||||
| `running` | VM is powered on and operational |
|
||||
| `stopped` | VM is powered off |
|
||||
| `creating` | VM is being provisioned (may take a few minutes) |
|
||||
| `error` | VM encountered an error during provisioning |
|
||||
| `suspended` | VM is suspended (usually billing-related) |
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Errors
|
||||
|
||||
#### "hostinger API error 401: Unauthorized"
|
||||
|
||||
**Cause:** The API token is invalid, expired, or revoked.
|
||||
|
||||
**Fix:**
|
||||
1. Log in to hPanel and verify the token exists and is not expired.
|
||||
2. Generate a new token if needed.
|
||||
3. Update the configuration in Setec Manager.
|
||||
|
||||
#### "hostinger API error 403: Forbidden"
|
||||
|
||||
**Cause:** The API token does not have the required permissions/scopes.
|
||||
|
||||
**Fix:**
|
||||
1. Check the token's permissions in hPanel.
|
||||
2. Ensure the token has read/write access for the feature you are trying to use (DNS, domains, VPS, billing).
|
||||
3. Generate a new token with the correct scopes if needed.
|
||||
|
||||
#### "hostinger API error 429: rate limited"
|
||||
|
||||
**Cause:** Too many API requests in a short period.
|
||||
|
||||
**Fix:**
|
||||
- The client retries automatically up to 3 times. If you still see this error, you are making requests too frequently.
|
||||
- Space out bulk operations.
|
||||
- Use batch endpoints (e.g., `UpdateDNSRecords` with multiple records) instead of individual calls.
|
||||
|
||||
#### "hostinger API error 404: Not Found"
|
||||
|
||||
**Cause:** The domain, VM, or resource does not exist in your Hostinger account.
|
||||
|
||||
**Fix:**
|
||||
- Verify the domain is registered with Hostinger (not just DNS-hosted).
|
||||
- Check that the VM ID is correct.
|
||||
- Ensure the domain's DNS zone is active in Hostinger.
|
||||
|
||||
#### "validate DNS records: hostinger API error 422"
|
||||
|
||||
**Cause:** One or more DNS records failed validation.
|
||||
|
||||
**Fix:**
|
||||
- Check record types are valid (A, AAAA, CNAME, MX, TXT, NS, SRV, CAA).
|
||||
- Verify content format matches the record type (e.g., A records must be valid IPv4 addresses).
|
||||
- Ensure TTL is a positive integer.
|
||||
- MX and SRV records require a priority value.
|
||||
- CNAME records cannot coexist with other record types at the same name.
|
||||
|
||||
#### "connection failed" or "execute request" errors
|
||||
|
||||
**Cause:** Network connectivity issue between Setec Manager and `developers.hostinger.com`.
|
||||
|
||||
**Fix:**
|
||||
- Verify the server has outbound HTTPS access.
|
||||
- Check DNS resolution: `dig developers.hostinger.com`.
|
||||
- Check if a firewall is blocking outbound port 443.
|
||||
- Verify the server's system clock is accurate (TLS certificate validation requires correct time).
|
||||
|
||||
#### "hosting provider 'hostinger' not registered"
|
||||
|
||||
**Cause:** The Hostinger provider package was not imported in the binary.
|
||||
|
||||
**Fix:**
|
||||
- Ensure `cmd/main.go` includes the blank import: `_ "setec-manager/internal/hosting/hostinger"`.
|
||||
- Rebuild and restart Setec Manager.
|
||||
|
||||
### Checking Logs
|
||||
|
||||
Setec Manager logs hosting provider operations to the configured log file (default: `/var/log/setec-manager.log`). Look for lines containing `hostinger` or `hosting`:
|
||||
|
||||
```bash
|
||||
grep -i hostinger /var/log/setec-manager.log | tail -20
|
||||
```
|
||||
|
||||
### Testing Connectivity Manually
|
||||
|
||||
You can test the Hostinger API directly from the server to rule out Setec Manager issues:
|
||||
|
||||
```bash
|
||||
curl -s -H "Authorization: Bearer YOUR_HOSTINGER_TOKEN" \
|
||||
https://developers.hostinger.com/api/dns/v1/zones/your-domain.com
|
||||
```
|
||||
|
||||
If this succeeds but Setec Manager fails, the issue is in the Setec Manager configuration. If this also fails, the issue is with the token or network connectivity.
|
||||
Reference in New Issue
Block a user