Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab02a8663a |
+2
-66
@@ -10,20 +10,14 @@ run:
|
|||||||
|
|
||||||
linters:
|
linters:
|
||||||
default: all
|
default: all
|
||||||
enable:
|
|
||||||
# Successor to the deprecated gomodguard. Named explicitly, rather than
|
|
||||||
# left to `default: all`, because it carries the module policy below.
|
|
||||||
- gomodguard_v2
|
|
||||||
disable:
|
disable:
|
||||||
# Genuinely incompatible with project patterns
|
# Genuinely incompatible with project patterns
|
||||||
- exhaustruct # Requires all struct fields
|
- exhaustruct # Requires all struct fields
|
||||||
|
- depguard # Dependency allow/block lists
|
||||||
- godot # Requires comments to end with periods
|
- godot # Requires comments to end with periods
|
||||||
|
- wsl # Deprecated, replaced by wsl_v5
|
||||||
- wrapcheck # Too verbose for internal packages
|
- wrapcheck # Too verbose for internal packages
|
||||||
- varnamelen # Short names like db, id are idiomatic Go
|
- varnamelen # Short names like db, id are idiomatic Go
|
||||||
# Deprecated: the warning is attached to the old name, so it is
|
|
||||||
# silenced by disabling that name, not by enabling the successor.
|
|
||||||
- wsl # Deprecated, replaced by wsl_v5
|
|
||||||
- gomodguard # Deprecated, replaced by gomodguard_v2
|
|
||||||
settings:
|
settings:
|
||||||
lll:
|
lll:
|
||||||
line-length: 88
|
line-length: 88
|
||||||
@@ -34,64 +28,6 @@ linters:
|
|||||||
max-complexity: 15
|
max-complexity: 15
|
||||||
dupl:
|
dupl:
|
||||||
threshold: 100
|
threshold: 100
|
||||||
depguard:
|
|
||||||
# Test-support code must not be compiled into the shipped binary. A
|
|
||||||
# test-support package exists to hand a test privileges the program
|
|
||||||
# itself must never have, so a file that is not a test must not import
|
|
||||||
# one. Test files, and the files inside a package whose directory name
|
|
||||||
# ends in `test`, are where that code belongs, and are exempt.
|
|
||||||
#
|
|
||||||
# The deny list below is the one part of this file a repository is
|
|
||||||
# expected to extend, and the only part it may. depguard matches an
|
|
||||||
# import path against a list of prefixes, so it cannot be told "any path
|
|
||||||
# whose last segment ends in test"; a repository's own test-support
|
|
||||||
# packages have to be named here one at a time, by full import path,
|
|
||||||
# under a module path that differs from repository to repository. Add
|
|
||||||
# them; change nothing else.
|
|
||||||
rules:
|
|
||||||
test-support:
|
|
||||||
list-mode: lax
|
|
||||||
files:
|
|
||||||
- "$all"
|
|
||||||
- "!$test"
|
|
||||||
- "!**/*test/**"
|
|
||||||
deny:
|
|
||||||
- pkg: net/http/httptest
|
|
||||||
desc: >-
|
|
||||||
Test-support code belongs in test files and in packages whose
|
|
||||||
directory name ends in test, not in the shipped binary.
|
|
||||||
# Only decisions already recorded in the Go package defaults are
|
|
||||||
# listed here. Every entry matches the module path exactly.
|
|
||||||
gomodguard_v2:
|
|
||||||
blocked:
|
|
||||||
- module: github.com/rs/zerolog
|
|
||||||
recommendations:
|
|
||||||
- log/slog
|
|
||||||
reason: "Structured logging is stdlib log/slog."
|
|
||||||
# One entry per pre-fork module path, because the later releases
|
|
||||||
# are separate paths. A prefix match would be shorter but would
|
|
||||||
# also reach github.com/go-redis/redismock, the test double for
|
|
||||||
# the successor these entries recommend.
|
|
||||||
- module: github.com/go-redis/redis
|
|
||||||
recommendations:
|
|
||||||
- github.com/redis/go-redis/v9
|
|
||||||
reason: "Pre-fork module; use the maintained go-redis v9."
|
|
||||||
- module: github.com/go-redis/redis/v7
|
|
||||||
recommendations:
|
|
||||||
- github.com/redis/go-redis/v9
|
|
||||||
reason: "Pre-fork module; use the maintained go-redis v9."
|
|
||||||
- module: github.com/go-redis/redis/v8
|
|
||||||
recommendations:
|
|
||||||
- github.com/redis/go-redis/v9
|
|
||||||
reason: "Pre-fork module; use the maintained go-redis v9."
|
|
||||||
- module: github.com/sergi/go-diff
|
|
||||||
recommendations:
|
|
||||||
- github.com/aymanbagabas/go-udiff
|
|
||||||
reason: "No unified diff output; use go-udiff."
|
|
||||||
- module: github.com/hexops/gotextdiff
|
|
||||||
recommendations:
|
|
||||||
- github.com/aymanbagabas/go-udiff
|
|
||||||
reason: "Unmaintained fork; use go-udiff."
|
|
||||||
|
|
||||||
issues:
|
issues:
|
||||||
max-issues-per-linter: 0
|
max-issues-per-linter: 0
|
||||||
|
|||||||
+10
-8
@@ -41,15 +41,18 @@ RUN make build
|
|||||||
# alpine 3.21, 2026-02-28
|
# alpine 3.21, 2026-02-28
|
||||||
FROM alpine@sha256:c3f8e73fdb79deaebaa2037150150191b9dcbfba68b4a46d70103204c53f4709
|
FROM alpine@sha256:c3f8e73fdb79deaebaa2037150150191b9dcbfba68b4a46d70103204c53f4709
|
||||||
|
|
||||||
RUN apk add --no-cache ca-certificates tzdata su-exec
|
RUN apk add --no-cache ca-certificates tzdata
|
||||||
|
|
||||||
COPY --from=builder /src/bin/dnswatcher /usr/local/bin/dnswatcher
|
COPY --from=builder /src/bin/dnswatcher /usr/local/bin/dnswatcher
|
||||||
COPY deploy/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
|
||||||
|
|
||||||
# dnswatcher runs as this unprivileged user. The entrypoint creates the
|
# Run as an unprivileged user that owns the data directory. A fresh named
|
||||||
# data directory and gives it to this user on every start.
|
# volume inherits this ownership; a bind-mounted host directory must be
|
||||||
|
# owned by uid 10001 (see "Running under upaas" in README.md), or startup
|
||||||
|
# fails.
|
||||||
RUN addgroup -S -g 10001 dnswatcher \
|
RUN addgroup -S -g 10001 dnswatcher \
|
||||||
&& adduser -S -G dnswatcher -u 10001 dnswatcher
|
&& adduser -S -G dnswatcher -u 10001 dnswatcher \
|
||||||
|
&& mkdir -p /var/lib/dnswatcher \
|
||||||
|
&& chown dnswatcher:dnswatcher /var/lib/dnswatcher
|
||||||
|
|
||||||
ENV DNSWATCHER_DATA_DIR=/var/lib/dnswatcher
|
ENV DNSWATCHER_DATA_DIR=/var/lib/dnswatcher
|
||||||
|
|
||||||
@@ -59,8 +62,7 @@ ENV DNSWATCHER_DATA_DIR=/var/lib/dnswatcher
|
|||||||
# data directory, or the binary's directory, the working directory.
|
# data directory, or the binary's directory, the working directory.
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
|
|
||||||
# No USER: the entrypoint must start as root to set up the data
|
USER dnswatcher
|
||||||
# directory; it then runs dnswatcher as the dnswatcher user.
|
|
||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
@@ -70,4 +72,4 @@ EXPOSE 8080
|
|||||||
HEALTHCHECK --interval=10s --timeout=5s --start-period=10s --retries=3 \
|
HEALTHCHECK --interval=10s --timeout=5s --start-period=10s --retries=3 \
|
||||||
CMD wget -q -O /dev/null "http://127.0.0.1:${PORT:-8080}/.well-known/healthcheck" || exit 1
|
CMD wget -q -O /dev/null "http://127.0.0.1:${PORT:-8080}/.well-known/healthcheck" || exit 1
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
|
ENTRYPOINT ["/usr/local/bin/dnswatcher"]
|
||||||
|
|||||||
@@ -278,8 +278,6 @@ internal/
|
|||||||
tlscheck/tlscheck.go TLS certificate inspector
|
tlscheck/tlscheck.go TLS certificate inspector
|
||||||
notify/notify.go Notification service (Slack, Mattermost, ntfy)
|
notify/notify.go Notification service (Slack, Mattermost, ntfy)
|
||||||
watcher/watcher.go Main monitoring orchestrator and scheduler
|
watcher/watcher.go Main monitoring orchestrator and scheduler
|
||||||
livedns/livedns.go Retry and concurrency limit for tests
|
|
||||||
against live DNS (imported only by tests)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Design Principles
|
### Design Principles
|
||||||
@@ -533,7 +531,17 @@ repository's `Dockerfile` and runs it. The app needs:
|
|||||||
- **Branch:** `prod`. `prod` is cut from `main`, and merging a `main` to
|
- **Branch:** `prod`. `prod` is cut from `main`, and merging a `main` to
|
||||||
`prod` pull request is a deploy.
|
`prod` pull request is a deploy.
|
||||||
- **Volume:** one host directory mounted at `/var/lib/dnswatcher`, where
|
- **Volume:** one host directory mounted at `/var/lib/dnswatcher`, where
|
||||||
the state file lives.
|
the state file lives. upaas bind-mounts the host path it is given and
|
||||||
|
does not create it. The container runs as uid 10001 and does not start
|
||||||
|
unless it can write there. Create the directory before the first
|
||||||
|
deploy:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mkdir -p /path/to/data
|
||||||
|
chown 10001:10001 /path/to/data
|
||||||
|
chmod 700 /path/to/data
|
||||||
|
```
|
||||||
|
|
||||||
- **Network and port:** the dashboard is unauthenticated and shows every
|
- **Network and port:** the dashboard is unauthenticated and shows every
|
||||||
watched name and recent alert, and upaas publishes every mapped port on
|
watched name and recent alert, and upaas publishes every mapped port on
|
||||||
all interfaces of the host
|
all interfaces of the host
|
||||||
|
|||||||
+3
-4
@@ -4,10 +4,9 @@
|
|||||||
|
|
||||||
DNS is never mocked in this project, not in tests and not anywhere
|
DNS is never mocked in this project, not in tests and not anywhere
|
||||||
else; see the README section "No DNS mocking. Ever." Every test that
|
else; see the README section "No DNS mocking. Ever." Every test that
|
||||||
looks something up in DNS **MUST** query live DNS servers, never a
|
involves DNS **MUST** use live queries against real DNS servers: the
|
||||||
stand-in. Logic that works on record data, such as comparing or
|
resolver's tests, and the tests of code that uses the resolver, such
|
||||||
formatting records, may be tested on that data directly with no
|
as the watcher.
|
||||||
lookup.
|
|
||||||
|
|
||||||
### Rationale
|
### Rationale
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,11 @@
|
|||||||
|
|
||||||
# Status
|
# Status
|
||||||
|
|
||||||
pre-1.0. No git tags.
|
pre-1.0. No git tags. Core resolver work in flight on feature/resolver
|
||||||
|
(dirty: internal/resolver/resolver_test.go). Local checkout has diverged
|
||||||
|
from origin: origin/main is 8 commits ahead (watcher orchestrator,
|
||||||
|
unified TARGETS) and origin/feature/resolver already contains the full
|
||||||
|
iterative resolver implementation with hermetic mocked tests.
|
||||||
|
|
||||||
# Next Step
|
# Next Step
|
||||||
|
|
||||||
@@ -19,24 +23,13 @@ Rationale, Design, TODO, License, Author) if any are still missing.
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
- 2026-09-29: the image sets up its own data directory. Its entrypoint,
|
- 2026-09-29: nothing stands in for DNS any more. The watcher tests use the
|
||||||
`deploy/docker-entrypoint.sh`, starts as root, creates the data directory if
|
real resolver against live DNS and test changes by preparing the saved
|
||||||
needed, gives it and everything in it to the `dnswatcher` user with mode 700
|
state a check starts from; the resolver timeout test queries an address
|
||||||
on the directory, then runs dnswatcher as that user with `su-exec`. A
|
that never answers, and `NewFromLoggerWithClient`, used only by its
|
||||||
bind-mounted host directory no longer has to be chowned first (closes #166).
|
stand-in client, is gone. The live-DNS retry and concurrency limit moved
|
||||||
- 2026-09-29: `.golangci.yml` re-fetched unchanged from `sneak/prompts`. It
|
to `internal/livedns`, which both test packages use. `TESTING.md` states
|
||||||
replaces the deprecated `gomodguard` with `gomodguard_v2`, so `make lint` no
|
the README's rule (closes #159).
|
||||||
longer warns about it, and turns on `depguard` with the org `test-support`
|
|
||||||
rule, which rejects `net/http/httptest` except in test files and in files
|
|
||||||
under a directory whose name ends in `test`. This repo had no `deny` entries
|
|
||||||
of its own to carry forward (closes #123).
|
|
||||||
- 2026-09-29: nothing stands in for DNS any more. Watcher tests that look
|
|
||||||
something up in DNS use the real resolver against live DNS servers and test
|
|
||||||
record and nameserver changes by preparing the saved state a check starts
|
|
||||||
from; the resolver timeout test queries an address that never answers, and
|
|
||||||
`NewFromLoggerWithClient`, used only by its stand-in client, is gone. The
|
|
||||||
live-DNS retry and concurrency limit moved to `internal/livedns`, which both
|
|
||||||
test packages use. `TESTING.md` states the README's rule (closes #159).
|
|
||||||
- 2026-09-28: the inconsistency alert is sent once, on the check where two
|
- 2026-09-28: the inconsistency alert is sent once, on the check where two
|
||||||
nameservers start to disagree or where a nameserver that disagrees first
|
nameservers start to disagree or where a nameserver that disagrees first
|
||||||
appears, instead of on every check while they disagree, and not again after
|
appears, instead of on every check while they disagree, and not again after
|
||||||
@@ -275,5 +268,4 @@ Infrastructure notes (from untracked TODO.md):
|
|||||||
- Module path sneak.berlin/go/dnswatcher differs from the git.eeqj.de
|
- Module path sneak.berlin/go/dnswatcher differs from the git.eeqj.de
|
||||||
remote intentionally; do not "fix" it
|
remote intentionally; do not "fix" it
|
||||||
- Dependencies: github.com/miekg/dns, golang.org/x/net/publicsuffix
|
- Dependencies: github.com/miekg/dns, golang.org/x/net/publicsuffix
|
||||||
- DNS is never mocked; tests that look something up in DNS query live DNS
|
- Tests use live DNS and never mock it (README, "No DNS mocking. Ever.")
|
||||||
servers (README, "No DNS mocking. Ever.")
|
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# deploy/docker-entrypoint.sh: the Docker image's ENTRYPOINT. It runs as
|
|
||||||
# root only to give the data directory to the dnswatcher user: a host
|
|
||||||
# directory bind-mounted there keeps its host owner, often root, and may
|
|
||||||
# hold a state file left by another uid, which dnswatcher could neither
|
|
||||||
# read nor replace. dnswatcher itself always runs as the dnswatcher user.
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
main() {
|
|
||||||
dir="${DNSWATCHER_DATA_DIR:-/var/lib/dnswatcher}"
|
|
||||||
mkdir -p "$dir"
|
|
||||||
chown -R dnswatcher:dnswatcher "$dir"
|
|
||||||
chmod 700 "$dir"
|
|
||||||
exec su-exec dnswatcher /usr/local/bin/dnswatcher "$@"
|
|
||||||
}
|
|
||||||
|
|
||||||
main "$@"
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// Package livedns runs the live DNS operations of tests. Tests that
|
// Package livedns runs the live DNS operations of tests. Every test in
|
||||||
// look something up in DNS query live DNS servers, never a stand-in —
|
// this project that needs DNS resolves against the real, live DNS —
|
||||||
// see TESTING.md. Nothing here mocks, fakes, stubs, records or replays
|
// see TESTING.md. Nothing here mocks, fakes, stubs, records or replays
|
||||||
// DNS, and nothing here skips a test: it only changes *how* the live
|
// DNS, and nothing here skips a test: it only changes *how* the live
|
||||||
// queries are issued, so that a single dropped UDP packet or one slow
|
// queries are issued, so that a single dropped UDP packet or one slow
|
||||||
@@ -16,12 +16,10 @@
|
|||||||
// operations are in flight at once in one test binary.
|
// operations are in flight at once in one test binary.
|
||||||
//
|
//
|
||||||
// 2. Retry with exponential backoff. Each live operation gets several
|
// 2. Retry with exponential backoff. Each live operation gets several
|
||||||
// attempts with its own timeout. An attempt is retried when it
|
// attempts with its own timeout. The retry condition is strictly
|
||||||
// obtained nothing to check, never because of what the test
|
// transport-level — "did a nameserver answer at all" — never the
|
||||||
// asserts about the result, so a wrong result still fails on the
|
// assertion the test is making. Code that answers incorrectly
|
||||||
// first attempt. A fault in the code under test that leaves
|
// still fails on the first attempt.
|
||||||
// nothing to check looks the same as live DNS not answering, and
|
|
||||||
// fails only after the last attempt.
|
|
||||||
package livedns
|
package livedns
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -115,7 +113,7 @@ func Retry(
|
|||||||
}
|
}
|
||||||
|
|
||||||
t.Fatalf(
|
t.Fatalf(
|
||||||
"%s: all %d live attempts failed: %v",
|
"%s: no answer after %d live attempts: %v",
|
||||||
what, attempts, last,
|
what, attempts, last,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,12 +17,11 @@ import (
|
|||||||
// Live DNS test support
|
// Live DNS test support
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
//
|
//
|
||||||
// Tests that look something up in DNS query live DNS servers, never a
|
// Every test in this package resolves against the real, live DNS —
|
||||||
// stand-in; logic that works on record data may be tested on that
|
// see TESTING.md. Each live operation below goes through
|
||||||
// data with no lookup (see TESTING.md). Each live operation below goes
|
// livedns.Retry, which bounds how many resolutions are in flight at
|
||||||
// through livedns.Retry, which bounds how many resolutions are in
|
// once and retries an operation that got no answer (see package
|
||||||
// flight at once and retries an operation that got no answer (see
|
// livedns).
|
||||||
// package livedns).
|
|
||||||
//
|
//
|
||||||
// Where an assertion spans several independent nameservers, a quorum
|
// Where an assertion spans several independent nameservers, a quorum
|
||||||
// is enough: a strict majority answering as expected. A server that
|
// is enough: a strict majority answering as expected. A server that
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ func newTestResolver(t *testing.T) *resolver.Resolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// findOneNSForDomain picks one authoritative nameserver to aim a
|
// findOneNSForDomain picks one authoritative nameserver to aim a
|
||||||
// test at. Quorum handling lives in livedns_test.go, and the live-DNS
|
// test at. Live-DNS retry, concurrency and quorum handling live in
|
||||||
// retry and concurrency limit in package livedns.
|
// livedns_test.go.
|
||||||
func findOneNSForDomain(
|
func findOneNSForDomain(
|
||||||
t *testing.T,
|
t *testing.T,
|
||||||
r *resolver.Resolver,
|
r *resolver.Resolver,
|
||||||
@@ -528,10 +528,8 @@ func TestQueryNameserverIP_Timeout(t *testing.T) {
|
|||||||
r := newTestResolver(t)
|
r := newTestResolver(t)
|
||||||
|
|
||||||
// Nothing answers at 192.0.2.1, a documentation address. The
|
// Nothing answers at 192.0.2.1, a documentation address. The
|
||||||
// resolver tries each query twice, and the first try gives up
|
// deadline must outlast one query timeout (two seconds): a query
|
||||||
// after two seconds. A deadline that ends during the first try
|
// cut short by the deadline itself is not reported as a timeout.
|
||||||
// makes the status vary from run to run between nodata and
|
|
||||||
// timeout, so the deadline must outlast the first try.
|
|
||||||
ctx, cancel := context.WithTimeout(
|
ctx, cancel := context.WithTimeout(
|
||||||
context.Background(), 3*time.Second,
|
context.Background(), 3*time.Second,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -19,10 +19,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// The watcher looks these names up in live DNS with the real resolver,
|
// The watcher looks these names up in live DNS with the real resolver,
|
||||||
// so tests assert on what the watcher does with the answers, never on
|
// so tests assert on notifications and saved state, never on the
|
||||||
// the records these zones publish. testHost's nameservers and addresses
|
// records these zones publish. testHost's addresses stay the same from
|
||||||
// stay the same from one check to the next, which the tests that check
|
// one check to the next, which the tests that check it twice rely on.
|
||||||
// it twice rely on.
|
|
||||||
const (
|
const (
|
||||||
testDomain = "google.com"
|
testDomain = "google.com"
|
||||||
testHost = "cloudflare.com"
|
testHost = "cloudflare.com"
|
||||||
@@ -39,8 +38,7 @@ const (
|
|||||||
|
|
||||||
// --- Stand-ins for the port checker, TLS checker and notifier ---
|
// --- Stand-ins for the port checker, TLS checker and notifier ---
|
||||||
//
|
//
|
||||||
// DNS has none: the watchers built here use the real resolver (see
|
// DNS has none: the watcher uses the real resolver (see TESTING.md).
|
||||||
// TESTING.md).
|
|
||||||
|
|
||||||
// mockPortChecker reports every port open until closed is set.
|
// mockPortChecker reports every port open until closed is set.
|
||||||
type mockPortChecker struct {
|
type mockPortChecker struct {
|
||||||
@@ -173,10 +171,11 @@ func defaultTestConfig(t *testing.T) *config.Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkOnce runs the watcher's checks once and returns an error when a
|
// checkOnce runs the watcher's checks once and returns
|
||||||
// configured name has no hostname state saved by this check, or that
|
// livedns.ErrNoAnswer when live DNS did not answer for a configured
|
||||||
// state holds no address. Either live DNS gave no answer for the name,
|
// name. The watcher saves a name's hostname state only when all of the
|
||||||
// or the watcher saved no fresh result for it.
|
// name's lookups succeed, so live DNS answered for a name when this
|
||||||
|
// check saved its hostname state and that state holds an address.
|
||||||
func checkOnce(
|
func checkOnce(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
w *watcher.Watcher,
|
w *watcher.Watcher,
|
||||||
@@ -192,53 +191,53 @@ func checkOnce(
|
|||||||
hs, ok := deps.state.GetHostnameState(name)
|
hs, ok := deps.state.GetHostnameState(name)
|
||||||
if !ok || hs.LastChecked.Before(started) ||
|
if !ok || hs.LastChecked.Before(started) ||
|
||||||
len(addresses(hs)) == 0 {
|
len(addresses(hs)) == 0 {
|
||||||
return fmt.Errorf(
|
return fmt.Errorf("%w: %s", livedns.ErrNoAnswer, name)
|
||||||
"%s: %w, or the watcher saved no fresh "+
|
|
||||||
"result for it",
|
|
||||||
name, livedns.ErrNoAnswer,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// runChecks builds a watcher, lets prepare set up the saved state and
|
// runFirstCheck builds a watcher, lets prepare set up the saved state
|
||||||
// stand-ins it starts from, and runs its checks once against live DNS.
|
// and stand-ins it starts from, and runs its checks once against live
|
||||||
// If change is not nil, change then alters the saved state or stand-ins
|
// DNS. When live DNS does not answer, the watcher is thrown away and
|
||||||
// and the checks run a second time. When either check finds no fresh
|
// built again, so a failed attempt leaves nothing behind in the state
|
||||||
// address for a name (see checkOnce), the watcher is thrown away and
|
// or the notifications.
|
||||||
// all of this runs again on a new one, so a failed attempt leaves
|
func runFirstCheck(
|
||||||
// nothing behind in the saved state, the stand-ins or the notifications.
|
|
||||||
func runChecks(
|
|
||||||
t *testing.T,
|
t *testing.T,
|
||||||
cfg *config.Config,
|
cfg *config.Config,
|
||||||
prepare, change func(deps *testDeps),
|
prepare func(deps *testDeps),
|
||||||
) *testDeps {
|
) (*watcher.Watcher, *testDeps) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
var deps *testDeps
|
var (
|
||||||
|
w *watcher.Watcher
|
||||||
livedns.Retry(t, "watcher checks", func(ctx context.Context) error {
|
deps *testDeps
|
||||||
var w *watcher.Watcher
|
)
|
||||||
|
|
||||||
|
livedns.Retry(t, "first check", func(ctx context.Context) error {
|
||||||
w, deps = newTestWatcher(t, cfg)
|
w, deps = newTestWatcher(t, cfg)
|
||||||
|
|
||||||
if prepare != nil {
|
if prepare != nil {
|
||||||
prepare(deps)
|
prepare(deps)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := checkOnce(ctx, w, deps)
|
|
||||||
if err != nil || change == nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
change(deps)
|
|
||||||
|
|
||||||
return checkOnce(ctx, w, deps)
|
return checkOnce(ctx, w, deps)
|
||||||
})
|
})
|
||||||
|
|
||||||
return deps
|
return w, deps
|
||||||
|
}
|
||||||
|
|
||||||
|
// runCheck runs the watcher's checks once more against live DNS,
|
||||||
|
// repeating them while live DNS does not answer. A failed lookup keeps
|
||||||
|
// the name's saved records, so a repeat compares against the same
|
||||||
|
// saved state.
|
||||||
|
func runCheck(t *testing.T, w *watcher.Watcher, deps *testDeps) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
livedns.Retry(t, "check", func(ctx context.Context) error {
|
||||||
|
return checkOnce(ctx, w, deps)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// addresses returns the A and AAAA values saved for a hostname.
|
// addresses returns the A and AAAA values saved for a hostname.
|
||||||
@@ -296,7 +295,7 @@ func TestFirstRunBaseline(t *testing.T) {
|
|||||||
cfg.Domains = []string{testDomain}
|
cfg.Domains = []string{testDomain}
|
||||||
cfg.Hostnames = []string{testHost}
|
cfg.Hostnames = []string{testHost}
|
||||||
|
|
||||||
deps := runChecks(t, cfg, nil, nil)
|
_, deps := runFirstCheck(t, cfg, nil)
|
||||||
|
|
||||||
assertNoNotifications(t, deps)
|
assertNoNotifications(t, deps)
|
||||||
assertStatePopulated(t, deps)
|
assertStatePopulated(t, deps)
|
||||||
@@ -348,7 +347,7 @@ func TestDomainPortAndTLSChecks(t *testing.T) {
|
|||||||
cfg := defaultTestConfig(t)
|
cfg := defaultTestConfig(t)
|
||||||
cfg.Domains = []string{testDomain}
|
cfg.Domains = []string{testDomain}
|
||||||
|
|
||||||
deps := runChecks(t, cfg, nil, nil)
|
_, deps := runFirstCheck(t, cfg, nil)
|
||||||
|
|
||||||
snap := deps.state.GetSnapshot()
|
snap := deps.state.GetSnapshot()
|
||||||
|
|
||||||
@@ -388,11 +387,11 @@ func TestNSChangeDetection(t *testing.T) {
|
|||||||
cfg.Domains = []string{testDomain}
|
cfg.Domains = []string{testDomain}
|
||||||
|
|
||||||
// The saved state lists nameservers that live DNS does not.
|
// The saved state lists nameservers that live DNS does not.
|
||||||
deps := runChecks(t, cfg, func(deps *testDeps) {
|
_, deps := runFirstCheck(t, cfg, func(deps *testDeps) {
|
||||||
deps.state.SetDomainState(testDomain, &state.DomainState{
|
deps.state.SetDomainState(testDomain, &state.DomainState{
|
||||||
Nameservers: []string{oldNS1, oldNS2},
|
Nameservers: []string{oldNS1, oldNS2},
|
||||||
})
|
})
|
||||||
}, nil)
|
})
|
||||||
|
|
||||||
assertNotified(t, deps, "NS Change: "+testDomain, "warning")
|
assertNotified(t, deps, "NS Change: "+testDomain, "warning")
|
||||||
|
|
||||||
@@ -408,16 +407,17 @@ func TestRecordChangeDetection(t *testing.T) {
|
|||||||
cfg := defaultTestConfig(t)
|
cfg := defaultTestConfig(t)
|
||||||
cfg.Hostnames = []string{testHost}
|
cfg.Hostnames = []string{testHost}
|
||||||
|
|
||||||
// Between the checks, save for every nameserver an address live DNS
|
w, deps := runFirstCheck(t, cfg, nil)
|
||||||
// never returns.
|
|
||||||
deps := runChecks(t, cfg, nil, func(deps *testDeps) {
|
|
||||||
hs, _ := deps.state.GetHostnameState(testHost)
|
|
||||||
for _, nsState := range hs.RecordsByNameserver {
|
|
||||||
nsState.Records = map[string][]string{"A": {oldIP}}
|
|
||||||
}
|
|
||||||
|
|
||||||
deps.state.SetHostnameState(testHost, hs)
|
// Save, for every nameserver, an address live DNS never returns.
|
||||||
})
|
hs, _ := deps.state.GetHostnameState(testHost)
|
||||||
|
for _, nsState := range hs.RecordsByNameserver {
|
||||||
|
nsState.Records = map[string][]string{"A": {oldIP}}
|
||||||
|
}
|
||||||
|
|
||||||
|
deps.state.SetHostnameState(testHost, hs)
|
||||||
|
|
||||||
|
runCheck(t, w, deps)
|
||||||
|
|
||||||
assertNotified(t, deps, "Record Change: "+testHost, "warning")
|
assertNotified(t, deps, "Record Change: "+testHost, "warning")
|
||||||
}
|
}
|
||||||
@@ -428,12 +428,13 @@ func TestPortStateChange(t *testing.T) {
|
|||||||
cfg := defaultTestConfig(t)
|
cfg := defaultTestConfig(t)
|
||||||
cfg.Hostnames = []string{testHost}
|
cfg.Hostnames = []string{testHost}
|
||||||
|
|
||||||
// Between the checks, every port closes.
|
w, deps := runFirstCheck(t, cfg, nil)
|
||||||
deps := runChecks(t, cfg, nil, func(deps *testDeps) {
|
|
||||||
deps.portChecker.mu.Lock()
|
deps.portChecker.mu.Lock()
|
||||||
deps.portChecker.closed = true
|
deps.portChecker.closed = true
|
||||||
deps.portChecker.mu.Unlock()
|
deps.portChecker.mu.Unlock()
|
||||||
})
|
|
||||||
|
runCheck(t, w, deps)
|
||||||
|
|
||||||
hs, _ := deps.state.GetHostnameState(testHost)
|
hs, _ := deps.state.GetHostnameState(testHost)
|
||||||
assertNotified(
|
assertNotified(
|
||||||
@@ -453,7 +454,7 @@ func TestTLSExpiryWarning(t *testing.T) {
|
|||||||
cfg := defaultTestConfig(t)
|
cfg := defaultTestConfig(t)
|
||||||
cfg.Hostnames = []string{testHost}
|
cfg.Hostnames = []string{testHost}
|
||||||
|
|
||||||
deps := runChecks(t, cfg, expiresInThreeDays, nil)
|
_, deps := runFirstCheck(t, cfg, expiresInThreeDays)
|
||||||
|
|
||||||
assertNotified(t, deps, "TLS Expiry Warning: "+testHost, "warning")
|
assertNotified(t, deps, "TLS Expiry Warning: "+testHost, "warning")
|
||||||
}
|
}
|
||||||
@@ -465,20 +466,19 @@ func TestTLSExpiryWarningDedup(t *testing.T) {
|
|||||||
cfg.Hostnames = []string{testHost}
|
cfg.Hostnames = []string{testHost}
|
||||||
cfg.TLSInterval = 24 * time.Hour
|
cfg.TLSInterval = 24 * time.Hour
|
||||||
|
|
||||||
|
w, deps := runFirstCheck(t, cfg, expiresInThreeDays)
|
||||||
|
|
||||||
title := "TLS Expiry Warning: " + testHost
|
title := "TLS Expiry Warning: " + testHost
|
||||||
|
|
||||||
// The second check comes within the TLS interval of the first,
|
warnings := countNotifications(deps, title)
|
||||||
// so it must not warn again.
|
|
||||||
var warnings int
|
|
||||||
|
|
||||||
deps := runChecks(t, cfg, expiresInThreeDays, func(deps *testDeps) {
|
|
||||||
warnings = countNotifications(deps, title)
|
|
||||||
})
|
|
||||||
|
|
||||||
if warnings == 0 {
|
if warnings == 0 {
|
||||||
t.Fatal("expected expiry warnings from the first check")
|
t.Fatal("expected expiry warnings from the first check")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The second check comes within the TLS interval of the first,
|
||||||
|
// so it must not warn again.
|
||||||
|
runCheck(t, w, deps)
|
||||||
|
|
||||||
got := countNotifications(deps, title)
|
got := countNotifications(deps, title)
|
||||||
if got != warnings {
|
if got != warnings {
|
||||||
t.Errorf(
|
t.Errorf(
|
||||||
@@ -525,7 +525,7 @@ func TestDNSRunsBeforePortAndTLSChecks(t *testing.T) {
|
|||||||
cfg.Hostnames = []string{testHost}
|
cfg.Hostnames = []string{testHost}
|
||||||
|
|
||||||
// The saved state says the last check found testHost at oldIP.
|
// The saved state says the last check found testHost at oldIP.
|
||||||
deps := runChecks(t, cfg, func(deps *testDeps) {
|
_, deps := runFirstCheck(t, cfg, func(deps *testDeps) {
|
||||||
deps.state.SetHostnameState(testHost, &state.HostnameState{
|
deps.state.SetHostnameState(testHost, &state.HostnameState{
|
||||||
RecordsByNameserver: map[string]*state.NameserverRecordState{
|
RecordsByNameserver: map[string]*state.NameserverRecordState{
|
||||||
oldNS1: {
|
oldNS1: {
|
||||||
@@ -534,7 +534,7 @@ func TestDNSRunsBeforePortAndTLSChecks(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}, nil)
|
})
|
||||||
|
|
||||||
snap := deps.state.GetSnapshot()
|
snap := deps.state.GetSnapshot()
|
||||||
|
|
||||||
@@ -665,21 +665,23 @@ func TestNSFailureAndRecovery(t *testing.T) {
|
|||||||
cfg := defaultTestConfig(t)
|
cfg := defaultTestConfig(t)
|
||||||
cfg.Hostnames = []string{testHost}
|
cfg.Hostnames = []string{testHost}
|
||||||
|
|
||||||
// Between the checks, save every nameserver the first check found
|
w, deps := runFirstCheck(t, cfg, nil)
|
||||||
// as failed, and add, as answering, one that live DNS does not list.
|
|
||||||
deps := runChecks(t, cfg, nil, func(deps *testDeps) {
|
|
||||||
hs, _ := deps.state.GetHostnameState(testHost)
|
|
||||||
for _, nsState := range hs.RecordsByNameserver {
|
|
||||||
nsState.Status = "error"
|
|
||||||
}
|
|
||||||
|
|
||||||
hs.RecordsByNameserver[oldNS1] = &state.NameserverRecordState{
|
// Save every nameserver the first check found as failed, and add
|
||||||
Records: map[string][]string{"A": {oldIP}},
|
// one that live DNS does not list as having answered.
|
||||||
Status: "ok",
|
hs, _ := deps.state.GetHostnameState(testHost)
|
||||||
}
|
for _, nsState := range hs.RecordsByNameserver {
|
||||||
|
nsState.Status = "error"
|
||||||
|
}
|
||||||
|
|
||||||
deps.state.SetHostnameState(testHost, hs)
|
hs.RecordsByNameserver[oldNS1] = &state.NameserverRecordState{
|
||||||
})
|
Records: map[string][]string{"A": {oldIP}},
|
||||||
|
Status: "ok",
|
||||||
|
}
|
||||||
|
|
||||||
|
deps.state.SetHostnameState(testHost, hs)
|
||||||
|
|
||||||
|
runCheck(t, w, deps)
|
||||||
|
|
||||||
assertNotified(t, deps, "NS Failure: "+testHost, "error")
|
assertNotified(t, deps, "NS Failure: "+testHost, "error")
|
||||||
assertNotified(t, deps, "NS Recovery: "+testHost, "success")
|
assertNotified(t, deps, "NS Recovery: "+testHost, "success")
|
||||||
|
|||||||
Reference in New Issue
Block a user