summaryrefslogtreecommitdiff
path: root/provider
diff options
context:
space:
mode:
authorMel <mel@rnrd.eu>2026-08-20 06:18:05 +0200
committerMel <mel@rnrd.eu>2026-08-20 06:18:05 +0200
commitb52c860a7b1d3f57abc442bb8c1257f228d49536 (patch)
tree524865405ba3b972d684ded62d7ab67547bb9040 /provider
downloadddnnss-b52c860a7b1d3f57abc442bb8c1257f228d49536.tar.zst
ddnnss-b52c860a7b1d3f57abc442bb8c1257f228d49536.zip
ddnnss
Signed-off-by: Mel <mel@rnrd.eu>
Diffstat (limited to 'provider')
-rw-r--r--provider/dummy.go32
-rw-r--r--provider/provider.go104
2 files changed, 136 insertions, 0 deletions
diff --git a/provider/dummy.go b/provider/dummy.go
new file mode 100644
index 0000000..bec5907
--- /dev/null
+++ b/provider/dummy.go
@@ -0,0 +1,32 @@
+package provider
+
+import (
+ "log/slog"
+ "net/netip"
+)
+
+// a stand-in for a real provider.
+// once asked to ensure a record is set,
+// it will tell you what was requested,
+// and not take any action.
+// very useful for debugging!
+type dummy struct {
+ log *slog.Logger
+}
+
+var _ Provider = (*dummy)(nil)
+
+func newDummy(log *slog.Logger) (*dummy, error) {
+ return &dummy{log: log.With(slog.String("provider", "dummy"))}, nil
+}
+
+func (d *dummy) Ensure(zone, name string, rtype RecordType, addr netip.Addr) (string, bool, error) {
+ d.log.Info(
+ "asked to ensure record, but nothing happened",
+ slog.String("zone", zone),
+ slog.String("name", name),
+ slog.String("record", string(rtype)),
+ slog.String("addr", addr.String()),
+ )
+ return "", true, nil
+}
diff --git a/provider/provider.go b/provider/provider.go
new file mode 100644
index 0000000..ee31ba8
--- /dev/null
+++ b/provider/provider.go
@@ -0,0 +1,104 @@
+package provider
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "log/slog"
+ "net/http"
+ "net/netip"
+ "os"
+ "strings"
+ "time"
+)
+
+type Kind string
+
+const (
+ // only the generic dummy testing provider for now
+ KindDummy Kind = "dummy"
+)
+
+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, cfg Config) (Provider, error) {
+ switch cfg.Kind {
+ case KindDummy:
+ return newDummy(log)
+ 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
+}