feat: add posibility to use token file in hetzner config

This commit is contained in:
2025-04-25 21:24:59 +02:00
parent 12fbd33dd1
commit 4819f92569
6 changed files with 189 additions and 83 deletions

View File

@@ -6,8 +6,10 @@ import (
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
"git.cloonar.com/cloonar/updns/internal/config"
pvd "git.cloonar.com/cloonar/updns/internal/provider"
)
@@ -33,8 +35,8 @@ type record struct {
Name string `json:"name"`
Value string `json:"value"`
Type string `json:"type"`
ZoneID string `json:"zone_id"`
TTL int `json:"ttl"`
ZoneID string `json:"zone_id"`
TTL int `json:"ttl"`
}
type recordsResponse struct {
@@ -42,12 +44,47 @@ type recordsResponse struct {
}
// NewProvider creates a Hetzner DNS provider using the official API.
func NewProvider(token string) pvd.Provider {
return &provider{token: token, client: http.DefaultClient, apiBaseURL: defaultAPIBase}
func NewProvider(cfg config.HetznerConfig) (pvd.Provider, error) {
var token string
hasToken := cfg.APIToken != ""
hasTokenFile := cfg.APITokenFile != ""
if hasToken && hasTokenFile {
return nil, fmt.Errorf("hetzner config: provide api_token or api_token_file, not both")
}
if !hasToken && !hasTokenFile {
return nil, fmt.Errorf("hetzner config: api_token or api_token_file must be provided")
}
if hasTokenFile {
tokenBytes, err := os.ReadFile(cfg.APITokenFile)
if err != nil {
return nil, fmt.Errorf("reading hetzner token file %q: %w", cfg.APITokenFile, err)
}
token = strings.TrimSpace(string(tokenBytes))
} else {
token = cfg.APIToken
}
if token == "" {
// This case might happen if the file exists but is empty
return nil, fmt.Errorf("hetzner api token is empty")
}
return &provider{token: token, client: http.DefaultClient, apiBaseURL: defaultAPIBase}, nil
}
// NewProviderWithURL creates a Hetzner provider with a custom API base URL (for testing).
// Note: This testing helper still requires a direct token string.
func NewProviderWithURL(token, apiBaseURL string) pvd.Provider {
// Basic validation for the test helper
if token == "" {
panic("NewProviderWithURL requires a non-empty token for testing")
}
if apiBaseURL == "" {
panic("NewProviderWithURL requires a non-empty apiBaseURL for testing")
}
return &provider{token: token, client: http.DefaultClient, apiBaseURL: apiBaseURL}
}
@@ -64,7 +101,7 @@ func (p *provider) UpdateRecord(ctx context.Context, domain, ip string) error {
} else {
zoneName = strings.Join(parts[len(parts)-2:], ".")
}
subdomain := strings.Join(parts[:len(parts)-2], ".")
subdomain := strings.Join(parts[:len(parts)-2], ".")
// Fetch zone ID
zonesURL := fmt.Sprintf("%s/zones?name=%s", p.apiBaseURL, zoneName)
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, zonesURL, nil)
@@ -111,57 +148,57 @@ func (p *provider) UpdateRecord(ctx context.Context, domain, ip string) error {
}
if recID == "" {
// return fmt.Errorf("record %s not found", domain)
// Create new record
// Cut the last 2 parts of the domain name
createURL := fmt.Sprintf("%s/records", p.apiBaseURL)
body := record{
Name: subdomain,
Type: "A",
Value: ip,
TTL: 60,
ZoneID: zoneID,
}
buf := &bytes.Buffer{}
if err := json.NewEncoder(buf).Encode(body); err != nil {
return fmt.Errorf("encode create body: %w", err)
}
req, _ = http.NewRequestWithContext(ctx, http.MethodPost, createURL, buf)
req.Header.Set("Auth-API-Token", p.token)
req.Header.Set("Content-Type", "application/json")
resp, err = p.client.Do(req)
if err != nil {
return fmt.Errorf("create record: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("create API status: %s", resp.Status)
}
// Create new record
// Cut the last 2 parts of the domain name
createURL := fmt.Sprintf("%s/records", p.apiBaseURL)
body := record{
Name: subdomain,
Type: "A",
Value: ip,
TTL: 60,
ZoneID: zoneID,
}
buf := &bytes.Buffer{}
if err := json.NewEncoder(buf).Encode(body); err != nil {
return fmt.Errorf("encode create body: %w", err)
}
req, _ = http.NewRequestWithContext(ctx, http.MethodPost, createURL, buf)
req.Header.Set("Auth-API-Token", p.token)
req.Header.Set("Content-Type", "application/json")
resp, err = p.client.Do(req)
if err != nil {
return fmt.Errorf("create record: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("create API status: %s", resp.Status)
}
} else {
// Update record value
updateURL := fmt.Sprintf("%s/records/%s", p.apiBaseURL, recID)
body := record{
Name: subdomain,
Type: "A",
Value: ip,
TTL: 60,
ZoneID: zoneID,
}
buf := &bytes.Buffer{}
if err := json.NewEncoder(buf).Encode(body); err != nil {
return fmt.Errorf("encode update body: %w", err)
}
req, _ = http.NewRequestWithContext(ctx, http.MethodPut, updateURL, buf)
req.Header.Set("Auth-API-Token", p.token)
req.Header.Set("Content-Type", "application/json")
resp, err = p.client.Do(req)
if err != nil {
return fmt.Errorf("update record: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("update API status: %s", resp.Status)
}
}
// Update record value
updateURL := fmt.Sprintf("%s/records/%s", p.apiBaseURL, recID)
body := record{
Name: subdomain,
Type: "A",
Value: ip,
TTL: 60,
ZoneID: zoneID,
}
buf := &bytes.Buffer{}
if err := json.NewEncoder(buf).Encode(body); err != nil {
return fmt.Errorf("encode update body: %w", err)
}
req, _ = http.NewRequestWithContext(ctx, http.MethodPut, updateURL, buf)
req.Header.Set("Auth-API-Token", p.token)
req.Header.Set("Content-Type", "application/json")
resp, err = p.client.Do(req)
if err != nil {
return fmt.Errorf("update record: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("update API status: %s", resp.Status)
}
}
return nil
}