Compare commits

..
1 Commits
Author SHA1 Message Date
sneak 33cd81a5ff config: a metrics username without a password stops startup (closes #268)
check / check (push) Canceled after 0s
DNSWATCHER_METRICS_USERNAME turns on /metrics, and its Basic Auth used
DNSWATCHER_METRICS_PASSWORD as it was, empty included, so the username
with an empty password got in. config.New now refuses a username
without a password with ErrNoMetricsPassword, whose message names both
variables. The README's variable table says the password is required
with the username.

Judgement call: the check is in New, after buildConfig, since it
compares two settings buildConfig has already read; buildConfig is
also at the linter's length limit.

Model: opus-5-5
2026-10-13 16:13:29 +00:00
3 changed files with 2 additions and 71 deletions
-2
View File
@@ -21,8 +21,6 @@ trial run of the finished image: https://git.eeqj.de/sneak/dnswatcher/issues/149
- 2026-10-13: `DNSWATCHER_METRICS_USERNAME` set without
`DNSWATCHER_METRICS_PASSWORD` stops startup (closes #268).
- 2026-10-13: a save syncs the state file to disk before renaming it into place,
so a crash cannot leave it empty and stop the next start (closes #264).
- 2026-10-13: a `PORT` that cannot be bound stops startup with an error naming
the address, instead of running without the HTTP server (closes #266).
- 2026-10-13: a bad `PORT`, `DNSWATCHER_TLS_EXPIRY_WARNING` or true/false
+2 -22
View File
@@ -4,7 +4,6 @@ package state
import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"maps"
@@ -254,20 +253,10 @@ func (s *State) Save() error {
return fmt.Errorf("creating data directory: %w", err)
}
// Atomic write: write to temp file, then rename. The temp file is
// synced before the rename and the directory after it, or a crash
// can leave the renamed state file empty.
// Atomic write: write to temp file, then rename
tmpPath := path + ".tmp"
//nolint:gosec // path is from trusted config
tmpFile, err := os.OpenFile(
tmpPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, filePermissions,
)
if err == nil {
_, err = tmpFile.Write(data)
err = errors.Join(err, tmpFile.Sync(), tmpFile.Close())
}
err = os.WriteFile(tmpPath, data, filePermissions)
if err != nil {
return fmt.Errorf("writing temp state file: %w", err)
}
@@ -277,15 +266,6 @@ func (s *State) Save() error {
return fmt.Errorf("renaming state file: %w", err)
}
dir, err := os.Open(filepath.Dir(path))
if err == nil {
err = errors.Join(dir.Sync(), dir.Close())
}
if err != nil {
return fmt.Errorf("syncing data directory: %w", err)
}
s.log.Debug("state saved to disk", "path", path)
return nil
-47
View File
@@ -618,53 +618,6 @@ func TestSaveAtomicWrite(t *testing.T) {
}
}
// TestSaveOverLeftoverTempFile verifies that a temp file left by a
// crash, longer than the state Save writes, leaves nothing of itself in
// the state file, and that no temp file remains after Save.
func TestSaveOverLeftoverTempFile(t *testing.T) {
t.Parallel()
dir := t.TempDir()
statePath := filepath.Join(dir, "state.json")
tmpPath := statePath + ".tmp"
err := os.WriteFile(tmpPath, []byte(strings.Repeat("x", 64*1024)), 0o600)
if err != nil {
t.Fatalf("writing leftover temp file: %v", err)
}
s := state.NewForTestWithDataDir(dir)
populateState(t, s)
err = s.Save()
if err != nil {
t.Fatalf("Save() error: %v", err)
}
//nolint:gosec // test file with known path
data, err := os.ReadFile(statePath)
if err != nil {
t.Fatalf("reading state file: %v", err)
}
var snap state.Snapshot
err = json.Unmarshal(data, &snap)
if err != nil {
t.Fatalf("state file is not complete JSON: %v", err)
}
if len(snap.Domains) != 2 {
t.Errorf("state file has %d domains, want 2", len(snap.Domains))
}
_, err = os.Stat(tmpPath)
if !os.IsNotExist(err) {
t.Error("temp file should not exist after save")
}
}
// TestLoadMissingFile verifies that loading a nonexistent file is not an error.
func TestLoadMissingFile(t *testing.T) {
t.Parallel()