diff options
Diffstat (limited to 'provider')
| -rw-r--r-- | provider/dummy.go | 32 | ||||
| -rw-r--r-- | provider/provider.go | 104 |
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 +} |
