1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
|
package provider
import (
"errors"
"fmt"
"io"
"log/slog"
"net/http"
"net/netip"
"os"
"strings"
"time"
)
type Kind string
const (
KindDummy Kind = "dummy"
KindBunny Kind = "bunny"
KindCloudflare Kind = "cloudflare"
)
type RecordType string
const (
TypeA RecordType = "A"
TypeAAAA RecordType = "AAAA"
)
type Config struct {
Kind Kind `json:"kind"`
// exactly one of these supplies the api key.
// both expand any $environment variables!
Key string `json:"key,omitempty"`
KeyFile string `json:"keyFile,omitempty"`
}
func (c Config) key() (string, error) {
if (c.Key == "") == (c.KeyFile == "") {
return "", errors.New("exactly one of key or keyFile must be set")
}
if c.Key != "" {
key := os.ExpandEnv(c.Key)
if key == "" {
return "", fmt.Errorf("key %q expanded to nothing", c.Key)
}
return key, nil
}
// expand those environment variables
raw, err := os.ReadFile(os.ExpandEnv(c.KeyFile))
if err != nil {
return "", err
}
return strings.TrimSpace(string(raw)), nil
}
type Provider interface {
// Ensure makes name.zone of the given type point at addr,
// creating the record when it doesn't exist yet.
// Returns the previous value (if any) and whether anything was changed.
Ensure(zone, name string, rtype RecordType, addr netip.Addr) (string, bool, error)
}
func New(log *slog.Logger, name string, cfg Config) (Provider, error) {
log = log.With(
slog.Group(
"provider",
slog.String("name", name),
slog.String("kind", string(cfg.Kind)),
),
)
switch cfg.Kind {
case KindDummy:
return newDummy(log)
case KindBunny:
return newBunny(log, cfg)
case KindCloudflare:
return newCloudflare(log, cfg)
default:
return nil, fmt.Errorf("unknown provider kind %q", cfg.Kind)
}
}
var client = http.Client{Timeout: 30 * time.Second}
// generic request sending helper, since most providers will need it.
func request(method, url string, headers map[string]string, body io.Reader) ([]byte, error) {
req, err := http.NewRequest(method, url, body)
if err != nil {
return nil, err
}
for k, v := range headers {
req.Header.Set(k, v)
}
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
raw, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if err != nil {
return nil, err
}
if resp.StatusCode < 200 || resp.StatusCode > 299 {
return nil, fmt.Errorf("%s %s: %s: %.200s", method, url, resp.Status, raw)
}
return raw, nil
}
|