carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1// Package dnsrec writes DURABLE DNS records — the DKIM CNAMEs, MAIL FROM MX
2// and SPF/DMARC TXT records a sending identity needs (tenant-email spec §7).
3//
4// It is deliberately NOT internal/edge/dns.go. That package's dnsProvider
5// publishes a transient ACME challenge TXT and hands back a cleanup closure;
6// its whole contract is "present this, then remove it". These records outlive
7// the process that wrote them, are upserted rather than presented, and are
8// deleted only on teardown. Putting them behind that interface would mean a
9// caller holding a cleanup closure for a record that must never be cleaned up.
10package dnsrec
11
12import (
13 "context"
14 "sync"
15)
16
17const (
18 TypeCNAME = "CNAME"
19 TypeMX = "MX"
20 TypeTXT = "TXT"
21)
22
23// Record is one resource record set. Values is a set, not a single string,
24// because a TXT or MX name can legitimately carry several.
25type Record struct {
26 Name string
27 Type string
28 Values []string
29 TTL int64
30}
31
32// Key identifies the record SET — name and type, never the values. DNS
33// replaces a whole set at a time, and identifying by value would make
34// EnsureRecord a create (which collides with a stranded record from a
35// crashed earlier attempt) instead of an upsert (which repairs it).
36func (r Record) Key() string { return r.Name + "|" + r.Type }
37
38// Writer is the backend seam. Both methods are idempotent: an EnsureRecord
39// over an existing set replaces it, and deleting an absent set succeeds.
40// Every walk that drives one must be re-runnable after a half-failure.
41type Writer interface {
42 EnsureRecord(ctx context.Context, r Record) error
43 DeleteRecord(ctx context.Context, r Record) error
44}
45
46// MemWriter is the in-memory fake: no AWS, no network, and a one-shot
47// failure injector so a test can drive a walk through a half-failure at an
48// exact step.
49type MemWriter struct {
50 mu sync.Mutex
51
52 // Records maps Record.Key() -> the stored set.
53 Records map[string]Record
54 // FailNext, keyed by method name, is consumed one-shot: the next call
55 // to that method fails, then the entry is gone. Consumption happens
56 // BEFORE any state change, so a failed call leaves state untouched.
57 FailNext map[string]error
58}
59
60func NewMemWriter() *MemWriter {
61 return &MemWriter{Records: map[string]Record{}, FailNext: map[string]error{}}
62}
63
64func (w *MemWriter) takeFailure(method string) error {
65 if err, ok := w.FailNext[method]; ok {
66 delete(w.FailNext, method)
67 return err
68 }
69 return nil
70}
71
72func (w *MemWriter) EnsureRecord(_ context.Context, r Record) error {
73 w.mu.Lock()
74 defer w.mu.Unlock()
75 if err := w.takeFailure("EnsureRecord"); err != nil {
76 return err
77 }
78 w.Records[r.Key()] = r
79 return nil
80}
81
82func (w *MemWriter) DeleteRecord(_ context.Context, r Record) error {
83 w.mu.Lock()
84 defer w.mu.Unlock()
85 if err := w.takeFailure("DeleteRecord"); err != nil {
86 return err
87 }
88 delete(w.Records, r.Key())
89 return nil
90}
91