feat: fix hetzner api provide example-config.yaml instead of config.yaml which is used for testing

This commit is contained in:
2025-04-25 21:15:01 +02:00
parent adae58b7bc
commit 12fbd33dd1
3 changed files with 81 additions and 28 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
config.yaml

17
example-config.yaml Normal file
View File

@@ -0,0 +1,17 @@
server:
bind_address: ":9090"
tls:
enabled: false
cert_file: "cert.pem"
key_file: "key.pem"
upstream:
provider: hetzner
hetzner:
api_token: "YOUR_HETZNER_API_TOKEN"
clients:
client1:
secret: "s3cr3t123"
exact:
- "home.example.com"
wildcard:
- "example.net"

View File

@@ -11,7 +11,7 @@ import (
pvd "git.cloonar.com/cloonar/updns/internal/provider" pvd "git.cloonar.com/cloonar/updns/internal/provider"
) )
const defaultAPIBase = "https://dns.hetzner.com" const defaultAPIBase = "https://dns.hetzner.com/api/v1"
type provider struct { type provider struct {
token string token string
@@ -29,10 +29,12 @@ type zonesResponse struct {
} }
type record struct { type record struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Value string `json:"value"` Value string `json:"value"`
Type string `json:"type"` Type string `json:"type"`
ZoneID string `json:"zone_id"`
TTL int `json:"ttl"`
} }
type recordsResponse struct { type recordsResponse struct {
@@ -62,10 +64,11 @@ func (p *provider) UpdateRecord(ctx context.Context, domain, ip string) error {
} else { } else {
zoneName = strings.Join(parts[len(parts)-2:], ".") zoneName = strings.Join(parts[len(parts)-2:], ".")
} }
subdomain := strings.Join(parts[:len(parts)-2], ".")
// Fetch zone ID // Fetch zone ID
zonesURL := fmt.Sprintf("%s/zones?name=%s", p.apiBaseURL, zoneName) zonesURL := fmt.Sprintf("%s/zones?name=%s", p.apiBaseURL, zoneName)
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, zonesURL, nil) req, _ := http.NewRequestWithContext(ctx, http.MethodGet, zonesURL, nil)
req.Header.Set("Authorization", "Bearer "+p.token) req.Header.Set("Auth-API-Token", p.token)
resp, err := p.client.Do(req) resp, err := p.client.Do(req)
if err != nil { if err != nil {
return fmt.Errorf("fetch zones: %w", err) return fmt.Errorf("fetch zones: %w", err)
@@ -86,7 +89,7 @@ func (p *provider) UpdateRecord(ctx context.Context, domain, ip string) error {
// Fetch records in zone // Fetch records in zone
recsURL := fmt.Sprintf("%s/records?zone_id=%s", p.apiBaseURL, zoneID) recsURL := fmt.Sprintf("%s/records?zone_id=%s", p.apiBaseURL, zoneID)
req, _ = http.NewRequestWithContext(ctx, http.MethodGet, recsURL, nil) req, _ = http.NewRequestWithContext(ctx, http.MethodGet, recsURL, nil)
req.Header.Set("Authorization", "Bearer "+p.token) req.Header.Set("Auth-API-Token", p.token)
resp, err = p.client.Do(req) resp, err = p.client.Do(req)
if err != nil { if err != nil {
return fmt.Errorf("fetch records: %w", err) return fmt.Errorf("fetch records: %w", err)
@@ -101,32 +104,64 @@ func (p *provider) UpdateRecord(ctx context.Context, domain, ip string) error {
} }
var recID string var recID string
for _, rec := range rr.Records { for _, rec := range rr.Records {
if rec.Name == domain { if rec.Name == subdomain {
recID = rec.ID recID = rec.ID
break break
} }
} }
if recID == "" { if recID == "" {
return fmt.Errorf("record %s not found", domain) // 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)
}
} 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 := map[string]string{"value": ip}
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("Authorization", "Bearer "+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 return nil
} }