Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec3c422afd |
@@ -21,12 +21,6 @@ trial run of the finished image: https://git.eeqj.de/sneak/dnswatcher/issues/149
|
|||||||
|
|
||||||
- 2026-10-02: the dashboard and `/api/v1/status` show why a nameserver query or
|
- 2026-10-02: the dashboard and `/api/v1/status` show why a nameserver query or
|
||||||
a certificate check failed, which only the state file showed (closes #225).
|
a certificate check failed, which only the state file showed (closes #225).
|
||||||
- 2026-10-02: a name's CNAME is stored once per nameserver, not once per record
|
|
||||||
type asked for; a state file with repeats loads each value once (closes #220).
|
|
||||||
- 2026-10-02: a DNS lookup that shutdown cuts short logs no error; one that
|
|
||||||
fails otherwise, or runs out of time, still does (closes #229).
|
|
||||||
- 2026-10-02: Record Change and Inconsistency notifications list only the record
|
|
||||||
types that differ, each with its values as plain text (closes #219).
|
|
||||||
- 2026-10-02: the startup notification no longer says every notification
|
- 2026-10-02: the startup notification no longer says every notification
|
||||||
endpoint works; it says it is a test sent to each of them (closes #230).
|
endpoint works; it says it is a test sent to each of them (closes #230).
|
||||||
- 2026-10-02: a Mattermost webhook that answers an HTTP error is logged as
|
- 2026-10-02: a Mattermost webhook that answers an HTTP error is logged as
|
||||||
|
|||||||
@@ -11,13 +11,6 @@ func ExtractRecordValue(rr dns.RR) string {
|
|||||||
return extractRecordValue(rr)
|
return extractRecordValue(rr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CollectAnswerRecords exports collectAnswerRecords for testing.
|
|
||||||
func CollectAnswerRecords(msg *dns.Msg, resp *NameserverResponse) {
|
|
||||||
var state queryState
|
|
||||||
|
|
||||||
collectAnswerRecords(msg, resp, &state)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UsableReply exports usableReply for testing.
|
// UsableReply exports usableReply for testing.
|
||||||
func UsableReply(resp *dns.Msg, zone string, name string) bool {
|
func UsableReply(resp *dns.Msg, zone string, name string) bool {
|
||||||
return usableReply(resp, zone, name)
|
return usableReply(resp, zone, name)
|
||||||
|
|||||||
@@ -714,10 +714,6 @@ func (r *Resolver) querySingleType(
|
|||||||
collectAnswerRecords(msg, resp, state)
|
collectAnswerRecords(msg, resp, state)
|
||||||
}
|
}
|
||||||
|
|
||||||
// collectAnswerRecords adds the records in msg's answer to resp, each
|
|
||||||
// value once per record type. For a name with a CNAME, a nameserver
|
|
||||||
// answers a query of any type with that CNAME, so the same value comes
|
|
||||||
// in the answer to every type asked for.
|
|
||||||
func collectAnswerRecords(
|
func collectAnswerRecords(
|
||||||
msg *dns.Msg,
|
msg *dns.Msg,
|
||||||
resp *NameserverResponse,
|
resp *NameserverResponse,
|
||||||
@@ -730,12 +726,9 @@ func collectAnswerRecords(
|
|||||||
}
|
}
|
||||||
|
|
||||||
typeName := dns.TypeToString[rr.Header().Rrtype]
|
typeName := dns.TypeToString[rr.Header().Rrtype]
|
||||||
if !slices.Contains(resp.Records[typeName], val) {
|
|
||||||
resp.Records[typeName] = append(
|
resp.Records[typeName] = append(
|
||||||
resp.Records[typeName], val,
|
resp.Records[typeName], val,
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
state.hasRecords = true
|
state.hasRecords = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -238,38 +238,6 @@ func TestExtractRecordValue_LetterCase(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestCollectAnswerRecords_CNAMEOnce collects the answers a nameserver
|
|
||||||
// gives for a name with a CNAME, one for each record type a check asks
|
|
||||||
// for. Each answer holds the CNAME, which must be stored once.
|
|
||||||
func TestCollectAnswerRecords_CNAMEOnce(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
cname := &dns.CNAME{
|
|
||||||
Hdr: dns.RR_Header{
|
|
||||||
Name: "git.eeqj.de.", Rrtype: dns.TypeCNAME, Class: dns.ClassINET,
|
|
||||||
},
|
|
||||||
Target: "fsn1app1.datavi.be.",
|
|
||||||
}
|
|
||||||
|
|
||||||
resp := &resolver.NameserverResponse{Records: map[string][]string{}}
|
|
||||||
|
|
||||||
for _, qtype := range []uint16{
|
|
||||||
dns.TypeA, dns.TypeAAAA, dns.TypeCNAME, dns.TypeMX,
|
|
||||||
dns.TypeTXT, dns.TypeSRV, dns.TypeCAA, dns.TypeNS,
|
|
||||||
} {
|
|
||||||
msg := new(dns.Msg)
|
|
||||||
msg.SetQuestion("git.eeqj.de.", qtype)
|
|
||||||
msg.Answer = []dns.RR{cname}
|
|
||||||
|
|
||||||
resolver.CollectAnswerRecords(msg, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t,
|
|
||||||
map[string][]string{"CNAME": {"fsn1app1.datavi.be."}},
|
|
||||||
resp.Records,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestShuffled shuffles the root servers with many seeds. Every order
|
// TestShuffled shuffles the root servers with many seeds. Every order
|
||||||
// must hold each root server once, so each is tried before a
|
// must hold each root server once, so each is tried before a
|
||||||
// resolution fails; each root server must come first for some seed, so
|
// resolution fails; each root server must come first for some seed, so
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -202,19 +201,6 @@ func (s *State) Load() error {
|
|||||||
return fmt.Errorf("parsing state file: %w", err)
|
return fmt.Errorf("parsing state file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// A state file saved before each record value was stored once can
|
|
||||||
// hold a hostname's CNAME once for every record type asked for.
|
|
||||||
// Each value is kept once, so the first check does not see a
|
|
||||||
// record change.
|
|
||||||
for _, hs := range snapshot.Hostnames {
|
|
||||||
for _, ns := range hs.RecordsByNameserver {
|
|
||||||
for recordType, values := range ns.Records {
|
|
||||||
slices.Sort(values)
|
|
||||||
ns.Records[recordType] = slices.Compact(values)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
s.snapshot = &snapshot
|
s.snapshot = &snapshot
|
||||||
s.log.Info("loaded state from disk", "path", path)
|
s.log.Info("loaded state from disk", "path", path)
|
||||||
|
|
||||||
|
|||||||
@@ -277,59 +277,6 @@ func TestLoadStateFromBeforeCNAMEAddresses(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestLoadStateWithRepeatedValues loads a state file saved when a
|
|
||||||
// hostname's CNAME was stored once for every record type asked for.
|
|
||||||
// Each value must load once, and every different value must load.
|
|
||||||
func TestLoadStateWithRepeatedValues(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
dir := t.TempDir()
|
|
||||||
|
|
||||||
data := []byte(`{
|
|
||||||
"version": 1,
|
|
||||||
"hostnames": {
|
|
||||||
"www.example.com": {
|
|
||||||
"recordsByNameserver": {
|
|
||||||
"ns1.example.com.": {
|
|
||||||
"records": {
|
|
||||||
"A": ["192.0.2.2", "192.0.2.1", "192.0.2.2", "192.0.2.1"],
|
|
||||||
"CNAME": ["a.example.net.", "a.example.net.", "a.example.net."]
|
|
||||||
},
|
|
||||||
"status": "ok"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`)
|
|
||||||
|
|
||||||
err := os.WriteFile(filepath.Join(dir, "state.json"), data, 0o600)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("writing state file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
s := state.NewForTestWithDataDir(dir)
|
|
||||||
|
|
||||||
err = s.Load()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Load() error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
hs, ok := s.GetHostnameState(testHostname)
|
|
||||||
if !ok {
|
|
||||||
t.Fatal("missing hostname " + testHostname)
|
|
||||||
}
|
|
||||||
|
|
||||||
want := map[string][]string{
|
|
||||||
"A": {"192.0.2.1", "192.0.2.2"},
|
|
||||||
"CNAME": {"a.example.net."},
|
|
||||||
}
|
|
||||||
|
|
||||||
got := hs.RecordsByNameserver[testNS1].Records
|
|
||||||
if !reflect.DeepEqual(got, want) {
|
|
||||||
t.Errorf("records: got %v, want %v", got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestSaveLoadRoundTrip_Hostnames verifies hostname data survives a save/load cycle.
|
// TestSaveLoadRoundTrip_Hostnames verifies hostname data survives a save/load cycle.
|
||||||
func TestSaveLoadRoundTrip_Hostnames(t *testing.T) {
|
func TestSaveLoadRoundTrip_Hostnames(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
package watcher_test
|
package watcher_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"sneak.berlin/go/dnswatcher/internal/portcheck"
|
"sneak.berlin/go/dnswatcher/internal/portcheck"
|
||||||
"sneak.berlin/go/dnswatcher/internal/resolver"
|
"sneak.berlin/go/dnswatcher/internal/resolver"
|
||||||
@@ -82,72 +79,3 @@ func TestCancelledCheckSavesNothing(t *testing.T) {
|
|||||||
t.Errorf("sent %v, want no notifications", notifications)
|
t.Errorf("sent %v, want no notifications", notifications)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// newLoggingWatcher returns a watcher for a domain and a hostname, with
|
|
||||||
// the real resolver, that writes what it logs at warning level or above
|
|
||||||
// into the returned buffer.
|
|
||||||
func newLoggingWatcher(t *testing.T) (*watcher.Watcher, *bytes.Buffer) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
cfg := defaultTestConfig(t)
|
|
||||||
cfg.Domains = []string{testSmallDomain}
|
|
||||||
cfg.Hostnames = []string{host}
|
|
||||||
|
|
||||||
w, _ := newTestWatcher(t, cfg)
|
|
||||||
|
|
||||||
logs := &bytes.Buffer{}
|
|
||||||
w.SetLogger(slog.New(slog.NewJSONHandler(
|
|
||||||
logs, &slog.HandlerOptions{Level: slog.LevelWarn},
|
|
||||||
)))
|
|
||||||
|
|
||||||
return w, logs
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestLookupCutShortIsNotLogged checks a domain and a hostname, looks
|
|
||||||
// up a nameserver's addresses and follows a CNAME, with the context
|
|
||||||
// cancelled, as shutdown leaves it. The real resolver fails each lookup
|
|
||||||
// without sending a query. Shutdown cutting a lookup short is not a
|
|
||||||
// failure, so nothing may be logged at warning level or above.
|
|
||||||
func TestLookupCutShortIsNotLogged(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
w, logs := newLoggingWatcher(t)
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(t.Context())
|
|
||||||
cancel()
|
|
||||||
|
|
||||||
w.RunOnce(ctx)
|
|
||||||
w.ResolveNameserverAddresses(ctx, []string{nsA}, nil)
|
|
||||||
w.ResolveCNAMEAddresses(ctx, host, cnameState(), nil)
|
|
||||||
|
|
||||||
if logs.Len() > 0 {
|
|
||||||
t.Errorf("logged at warning level or above:\n%s", logs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestLookupOutOfTimeIsLoggedAsError does what
|
|
||||||
// TestLookupCutShortIsNotLogged does, with the context's deadline passed
|
|
||||||
// instead. A lookup that ran out of time did fail, so the domain's NS
|
|
||||||
// lookup, the hostname's lookup, the nameserver's address lookup and the
|
|
||||||
// CNAME's are each logged as an error.
|
|
||||||
func TestLookupOutOfTimeIsLoggedAsError(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
w, logs := newLoggingWatcher(t)
|
|
||||||
|
|
||||||
ctx, cancel := context.WithDeadline(t.Context(), time.Now())
|
|
||||||
t.Cleanup(cancel)
|
|
||||||
|
|
||||||
w.RunOnce(ctx)
|
|
||||||
w.ResolveNameserverAddresses(ctx, []string{nsA}, nil)
|
|
||||||
w.ResolveCNAMEAddresses(ctx, host, cnameState(), nil)
|
|
||||||
|
|
||||||
const want = 4
|
|
||||||
|
|
||||||
lines := strings.Count(logs.String(), "\n")
|
|
||||||
errorLines := strings.Count(logs.String(), `"level":"ERROR"`)
|
|
||||||
|
|
||||||
if lines != want || errorLines != want {
|
|
||||||
t.Errorf("logged:\n%s\nwant %d lines, each at error level", logs, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -31,12 +31,6 @@ func NewForTest(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetLogger replaces the watcher's logger, so a test can read what it
|
|
||||||
// logs.
|
|
||||||
func (w *Watcher) SetLogger(log *slog.Logger) {
|
|
||||||
w.log = log
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewlyDisagreeingPairs exports newlyDisagreeingPairs for testing.
|
// NewlyDisagreeingPairs exports newlyDisagreeingPairs for testing.
|
||||||
func NewlyDisagreeingPairs(
|
func NewlyDisagreeingPairs(
|
||||||
prev, current *state.HostnameState,
|
prev, current *state.HostnameState,
|
||||||
|
|||||||
@@ -183,58 +183,3 @@ func TestInconsistencyAlert(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestFirstCheckAfterRepeatedValuesLoaded saves a state file holding a
|
|
||||||
// hostname's CNAME once for every record type asked for, as checks did
|
|
||||||
// before each value was stored once, and two addresses each repeated,
|
|
||||||
// and loads it. A check that then finds each value once at each
|
|
||||||
// nameserver must notify nothing.
|
|
||||||
func TestFirstCheckAfterRepeatedValuesLoaded(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
const (
|
|
||||||
cnameType = "CNAME"
|
|
||||||
cname = "c.example.net."
|
|
||||||
)
|
|
||||||
|
|
||||||
cfg := defaultTestConfig(t)
|
|
||||||
repeated := map[string][]string{
|
|
||||||
"A": {ip2, ip1, ip2, ip1},
|
|
||||||
cnameType: {cname, cname, cname, cname, cname, cname, cname, cname},
|
|
||||||
}
|
|
||||||
once := map[string][]string{"A": {ip1, ip2}, cnameType: {cname}}
|
|
||||||
|
|
||||||
saved := newTestDeps(t, cfg).state
|
|
||||||
saved.SetHostnameState(host, hostnameState(map[string]map[string][]string{
|
|
||||||
nsA: repeated, nsB: repeated,
|
|
||||||
}))
|
|
||||||
|
|
||||||
err := saved.Save()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("saving the state file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
deps := newTestDeps(t, cfg)
|
|
||||||
|
|
||||||
err = deps.state.Load()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("loading the state file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
prev, ok := deps.state.GetHostnameState(host)
|
|
||||||
if !ok {
|
|
||||||
t.Fatal("the state file has no state for " + host)
|
|
||||||
}
|
|
||||||
|
|
||||||
current := hostnameState(map[string]map[string][]string{
|
|
||||||
nsA: once, nsB: once,
|
|
||||||
})
|
|
||||||
|
|
||||||
// The hostname change detection uses only the notifier.
|
|
||||||
w := watcher.NewForTest(nil, nil, nil, nil, nil, deps.notifier)
|
|
||||||
w.DetectHostnameChanges(t.Context(), host, prev, current)
|
|
||||||
|
|
||||||
if got := deps.notifier.getNotifications(); len(got) != 0 {
|
|
||||||
t.Errorf("sent %v, want no notification", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
package watcher_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"sneak.berlin/go/dnswatcher/internal/watcher"
|
|
||||||
)
|
|
||||||
|
|
||||||
// When one nameserver's A record changes and its TXT record does not,
|
|
||||||
// the record change and the inconsistency it starts name the A record
|
|
||||||
// alone, with its values written as plain text.
|
|
||||||
func TestChangeMessagesNameTheChangedType(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
// A nameserver's records: this A address and the same TXT record.
|
|
||||||
records := func(address string) map[string][]string {
|
|
||||||
return map[string][]string{
|
|
||||||
"A": {address},
|
|
||||||
"TXT": {"v=spf1 -all"},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
before := hostnameState(map[string]map[string][]string{
|
|
||||||
nsA: records(ip1),
|
|
||||||
nsB: records(ip1),
|
|
||||||
})
|
|
||||||
after := hostnameState(map[string]map[string][]string{
|
|
||||||
nsA: records(ip1),
|
|
||||||
nsB: records(ip2),
|
|
||||||
})
|
|
||||||
|
|
||||||
// The hostname change detection uses only the notifier.
|
|
||||||
notifier := &mockNotifier{}
|
|
||||||
w := watcher.NewForTest(nil, nil, nil, nil, nil, notifier)
|
|
||||||
|
|
||||||
w.DetectHostnameChanges(t.Context(), host, before, after)
|
|
||||||
|
|
||||||
want := map[string]string{
|
|
||||||
"Record Change: " + host: `Hostname: www.example.net
|
|
||||||
Nameserver: b.ns.example.net.
|
|
||||||
Type: A
|
|
||||||
Old: 192.0.2.1
|
|
||||||
New: 192.0.2.2`,
|
|
||||||
"Inconsistency: " + host: `Hostname: www.example.net
|
|
||||||
Type: A
|
|
||||||
a.ns.example.net.: 192.0.2.1
|
|
||||||
b.ns.example.net.: 192.0.2.2`,
|
|
||||||
}
|
|
||||||
|
|
||||||
notifications := notifier.getNotifications()
|
|
||||||
if len(notifications) != len(want) {
|
|
||||||
t.Fatalf(
|
|
||||||
"sent %d notifications, want %d: %v",
|
|
||||||
len(notifications), len(want), notifications,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, n := range notifications {
|
|
||||||
if n.Message != want[n.Title] {
|
|
||||||
t.Errorf(
|
|
||||||
"%s message:\n%s\nwant:\n%s",
|
|
||||||
n.Title, n.Message, want[n.Title],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@ package watcher
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"slices"
|
"slices"
|
||||||
@@ -214,29 +213,13 @@ func (w *Watcher) runDNSChecks(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// logFailedLookup logs a failed DNS lookup at error level, unless ctx
|
|
||||||
// was cancelled: shutdown cancels it, and a lookup it cut short did not
|
|
||||||
// fail. A lookup that ran out of time did fail, so it is logged.
|
|
||||||
func (w *Watcher) logFailedLookup(
|
|
||||||
ctx context.Context,
|
|
||||||
msg string,
|
|
||||||
args ...any,
|
|
||||||
) {
|
|
||||||
if errors.Is(ctx.Err(), context.Canceled) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.log.Error(msg, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Watcher) checkDomain(
|
func (w *Watcher) checkDomain(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
domain string,
|
domain string,
|
||||||
) {
|
) {
|
||||||
nameservers, err := w.resolver.LookupNS(ctx, domain)
|
nameservers, err := w.resolver.LookupNS(ctx, domain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.logFailedLookup(
|
w.log.Error(
|
||||||
ctx,
|
|
||||||
"failed to lookup NS",
|
"failed to lookup NS",
|
||||||
"domain", domain,
|
"domain", domain,
|
||||||
"error", err,
|
"error", err,
|
||||||
@@ -337,8 +320,7 @@ func (w *Watcher) resolveNameserverAddresses(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
w.logFailedLookup(
|
w.log.Error(
|
||||||
ctx,
|
|
||||||
"no addresses found for nameserver",
|
"no addresses found for nameserver",
|
||||||
"nameserver", ns,
|
"nameserver", ns,
|
||||||
"error", err,
|
"error", err,
|
||||||
@@ -390,8 +372,7 @@ func (w *Watcher) checkHostname(
|
|||||||
) {
|
) {
|
||||||
results, err := w.resolver.LookupAllRecords(ctx, hostname)
|
results, err := w.resolver.LookupAllRecords(ctx, hostname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.logFailedLookup(
|
w.log.Error(
|
||||||
ctx,
|
|
||||||
"failed to lookup records",
|
"failed to lookup records",
|
||||||
"hostname", hostname,
|
"hostname", hostname,
|
||||||
"error", err,
|
"error", err,
|
||||||
@@ -474,8 +455,7 @@ func (w *Watcher) resolveCNAMEAddresses(
|
|||||||
for target := range targets {
|
for target := range targets {
|
||||||
ips, err := w.resolver.ResolveIPAddresses(ctx, target)
|
ips, err := w.resolver.ResolveIPAddresses(ctx, target)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.logFailedLookup(
|
w.log.Error(
|
||||||
ctx,
|
|
||||||
"failed to follow CNAME",
|
"failed to follow CNAME",
|
||||||
"hostname", hostname,
|
"hostname", hostname,
|
||||||
"target", target,
|
"target", target,
|
||||||
@@ -590,12 +570,10 @@ func (w *Watcher) detectRecordChanges(
|
|||||||
}
|
}
|
||||||
|
|
||||||
msg := fmt.Sprintf(
|
msg := fmt.Sprintf(
|
||||||
"Hostname: %s\nNameserver: %s\n%s",
|
"Hostname: %s\nNameserver: %s\n"+
|
||||||
|
"Old: %v\nNew: %v",
|
||||||
hostname, ns,
|
hostname, ns,
|
||||||
recordDifferences(
|
prevNS.Records, cur.Records,
|
||||||
"Old", prevNS.Records,
|
|
||||||
"New", cur.Records,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
w.notify.SendNotification(
|
w.notify.SendNotification(
|
||||||
@@ -683,12 +661,10 @@ func (w *Watcher) detectInconsistencies(
|
|||||||
ns1, ns2 := pair[0], pair[1]
|
ns1, ns2 := pair[0], pair[1]
|
||||||
|
|
||||||
msg := fmt.Sprintf(
|
msg := fmt.Sprintf(
|
||||||
"Hostname: %s\n%s",
|
"Hostname: %s\n%s: %v\n%s: %v",
|
||||||
hostname,
|
hostname,
|
||||||
recordDifferences(
|
|
||||||
ns1, current.RecordsByNameserver[ns1].Records,
|
ns1, current.RecordsByNameserver[ns1].Records,
|
||||||
ns2, current.RecordsByNameserver[ns2].Records,
|
ns2, current.RecordsByNameserver[ns2].Records,
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
w.notify.SendNotification(
|
w.notify.SendNotification(
|
||||||
@@ -1238,54 +1214,6 @@ func recordsEqual(
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// recordDifferences describes, in sorted order of type, each record
|
|
||||||
// type whose values differ between a and b: a line naming the type,
|
|
||||||
// then a line with a's values after labelA and one with b's after
|
|
||||||
// labelB. Types with the same values in both are left out.
|
|
||||||
func recordDifferences(
|
|
||||||
labelA string, a map[string][]string,
|
|
||||||
labelB string, b map[string][]string,
|
|
||||||
) string {
|
|
||||||
types := make([]string, 0, len(a)+len(b))
|
|
||||||
|
|
||||||
for recordType := range a {
|
|
||||||
types = append(types, recordType)
|
|
||||||
}
|
|
||||||
|
|
||||||
for recordType := range b {
|
|
||||||
if _, ok := a[recordType]; !ok {
|
|
||||||
types = append(types, recordType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Strings(types)
|
|
||||||
|
|
||||||
var lines []string
|
|
||||||
|
|
||||||
for _, recordType := range types {
|
|
||||||
if sliceEqual(a[recordType], b[recordType]) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
lines = append(lines,
|
|
||||||
"Type: "+recordType,
|
|
||||||
labelA+": "+joinValues(a[recordType]),
|
|
||||||
labelB+": "+joinValues(b[recordType]),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.Join(lines, "\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
// joinValues lists record values separated by commas, or says none.
|
|
||||||
func joinValues(values []string) string {
|
|
||||||
if len(values) == 0 {
|
|
||||||
return "none"
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.Join(values, ", ")
|
|
||||||
}
|
|
||||||
|
|
||||||
func sliceEqual(a, b []string) bool {
|
func sliceEqual(a, b []string) bool {
|
||||||
if len(a) != len(b) {
|
if len(a) != len(b) {
|
||||||
return false
|
return false
|
||||||
|
|||||||
Reference in New Issue
Block a user