mirror of
https://github.com/tiennm99/traefik-cloudflare-dns.git
synced 2026-10-11 03:13:52 +00:00
Create a record for each Host(...) in running containers' Traefik router labels, tagged with a configurable comment and optional tags, and delete owned records once their host has been down for DELETE_AFTER.
138 lines
3.8 KiB
Go
138 lines
3.8 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const cloudflareAPI = "https://api.cloudflare.com/client/v4"
|
|
|
|
// Record is a Cloudflare DNS record, limited to the fields this tool uses.
|
|
type Record struct {
|
|
ID string `json:"id,omitempty"`
|
|
Type string `json:"type"`
|
|
Name string `json:"name"`
|
|
Content string `json:"content"`
|
|
TTL int `json:"ttl,omitempty"`
|
|
Proxied bool `json:"proxied"`
|
|
Comment string `json:"comment,omitempty"`
|
|
Tags []string `json:"tags,omitempty"`
|
|
}
|
|
|
|
// CloudflareClient manages DNS records in one zone with a scoped API token.
|
|
type CloudflareClient struct {
|
|
base string
|
|
token string
|
|
zoneID string
|
|
http *http.Client
|
|
}
|
|
|
|
// NewCloudflareClient returns a client for zoneID.
|
|
func NewCloudflareClient(token, zoneID string) *CloudflareClient {
|
|
return &CloudflareClient{
|
|
base: cloudflareAPI,
|
|
token: token,
|
|
zoneID: zoneID,
|
|
http: &http.Client{Timeout: 30 * time.Second},
|
|
}
|
|
}
|
|
|
|
type cfResponse struct {
|
|
Success bool `json:"success"`
|
|
Errors []struct {
|
|
Code int `json:"code"`
|
|
Message string `json:"message"`
|
|
} `json:"errors"`
|
|
Result json.RawMessage `json:"result"`
|
|
ResultInfo struct {
|
|
Page int `json:"page"`
|
|
TotalPages int `json:"total_pages"`
|
|
} `json:"result_info"`
|
|
}
|
|
|
|
func (c *CloudflareClient) do(ctx context.Context, method, path string, body any) (*cfResponse, error) {
|
|
var reader io.Reader
|
|
if body != nil {
|
|
b, err := json.Marshal(body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
reader = bytes.NewReader(b)
|
|
}
|
|
req, err := http.NewRequestWithContext(ctx, method, c.base+path, reader)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+c.token)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, err := c.http.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
var out cfResponse
|
|
if err := json.NewDecoder(io.LimitReader(resp.Body, 32<<20)).Decode(&out); err != nil {
|
|
return nil, fmt.Errorf("HTTP %d: undecodable response: %w", resp.StatusCode, err)
|
|
}
|
|
if !out.Success {
|
|
var msgs []string
|
|
for _, e := range out.Errors {
|
|
msgs = append(msgs, fmt.Sprintf("%d %s", e.Code, e.Message))
|
|
}
|
|
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, strings.Join(msgs, "; "))
|
|
}
|
|
return &out, nil
|
|
}
|
|
|
|
// ListRecords returns every A, AAAA and CNAME record in the zone.
|
|
func (c *CloudflareClient) ListRecords(ctx context.Context) ([]Record, error) {
|
|
var all []Record
|
|
for page := 1; ; page++ {
|
|
q := url.Values{"per_page": {"100"}, "page": {fmt.Sprint(page)}}
|
|
out, err := c.do(ctx, http.MethodGet, "/zones/"+c.zoneID+"/dns_records?"+q.Encode(), nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list records: %w", err)
|
|
}
|
|
var records []Record
|
|
if err := json.Unmarshal(out.Result, &records); err != nil {
|
|
return nil, fmt.Errorf("list records: %w", err)
|
|
}
|
|
for _, r := range records {
|
|
if r.Type == "A" || r.Type == "AAAA" || r.Type == "CNAME" {
|
|
all = append(all, r)
|
|
}
|
|
}
|
|
if out.ResultInfo.TotalPages <= page {
|
|
return all, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// CreateRecord creates r and returns the stored record.
|
|
func (c *CloudflareClient) CreateRecord(ctx context.Context, r Record) (Record, error) {
|
|
out, err := c.do(ctx, http.MethodPost, "/zones/"+c.zoneID+"/dns_records", r)
|
|
if err != nil {
|
|
return Record{}, fmt.Errorf("create %s: %w", r.Name, err)
|
|
}
|
|
var created Record
|
|
if err := json.Unmarshal(out.Result, &created); err != nil {
|
|
return Record{}, fmt.Errorf("create %s: %w", r.Name, err)
|
|
}
|
|
return created, nil
|
|
}
|
|
|
|
// DeleteRecord deletes the record with id.
|
|
func (c *CloudflareClient) DeleteRecord(ctx context.Context, id string) error {
|
|
if _, err := c.do(ctx, http.MethodDelete, "/zones/"+c.zoneID+"/dns_records/"+id, nil); err != nil {
|
|
return fmt.Errorf("delete %s: %w", id, err)
|
|
}
|
|
return nil
|
|
}
|