diff --git a/go.sum b/go.sum index 0da5c24..c11b335 100644 --- a/go.sum +++ b/go.sum @@ -4,12 +4,6 @@ filippo.io/age v1.3.1 h1:hbzdQOJkuaMEpRCLSN1/C5DX74RPcNCk6oqhKMXmZi0= filippo.io/age v1.3.1/go.mod h1:EZorDTYUxt836i3zdori5IJX/v2Lj6kWFU0cfh6C0D4= filippo.io/hpke v0.4.0 h1:p575VVQ6ted4pL+it6M00V/f2qTZITO0zgmdKCkd5+A= filippo.io/hpke v0.4.0/go.mod h1:EmAN849/P3qdeK+PCMkDpDm83vRHM5cDipBJ8xbQLVY= -github.com/cplieger/health v1.0.0 h1:mevbZo10XtMAzfWaYoi6UrcGooFgHJvlhIJwbNQIVoM= -github.com/cplieger/health v1.0.0/go.mod h1:INjkN9qJnR14X5AFqX6ymFaHvx51djYgk2oQb/gJ9u8= -github.com/cplieger/health v1.0.1 h1:ZzOL6vEVOncHU2i2XDVg4G4v654vFaoCuE+Wvp0mXF0= -github.com/cplieger/health v1.0.1/go.mod h1:INjkN9qJnR14X5AFqX6ymFaHvx51djYgk2oQb/gJ9u8= -github.com/cplieger/health v1.0.2 h1:tS2XtQVQL4TXEAr8u0xCtjOqJFrOtRzD8pzm/l2DbSU= -github.com/cplieger/health v1.0.2/go.mod h1:INjkN9qJnR14X5AFqX6ymFaHvx51djYgk2oQb/gJ9u8= github.com/cplieger/health v1.1.0 h1:s//JW6MZtiHLLAhJhD21wxzeqFP94dqV0KMa+zJRTWM= github.com/cplieger/health v1.1.0/go.mod h1:INjkN9qJnR14X5AFqX6ymFaHvx51djYgk2oQb/gJ9u8= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= diff --git a/health.go b/health.go deleted file mode 100644 index 1e1f723..0000000 --- a/health.go +++ /dev/null @@ -1,58 +0,0 @@ -package main - -// File-based healthcheck for distroless containers, delegating to -// github.com/cplieger/health. - -import ( - "fmt" - "os" - "path/filepath" - - "github.com/cplieger/health" -) - -// healthMarkerPath is the default marker location. -const healthMarkerPath = health.DefaultPath - -// healthMarker wraps *health.Marker to preserve the internal API used -// by main.go and tests. -type healthMarker struct { - *health.Marker - degraded bool -} - -// newHealthMarker constructs a marker and detects degraded mode. -func newHealthMarker(path string) *healthMarker { - m := health.NewMarker(path) - degraded := probeHealthDir(path) != nil - return &healthMarker{Marker: m, degraded: degraded} -} - -// runProbe delegates to health.RunProbe (calls os.Exit). -func runProbe(path string) { - health.RunProbe(path) -} - -// probeCheck delegates to health.ProbeCheck (testable, no os.Exit). -func probeCheck(path string) int { - return health.ProbeCheck(path) -} - -// probeHealthDir verifies the marker's parent directory is writable by -// creating and deleting a temp file. -func probeHealthDir(path string) error { - dir := filepath.Dir(path) - f, err := os.CreateTemp(dir, ".health-probe-*") - if err != nil { - return err - } - name := f.Name() - if closeErr := f.Close(); closeErr != nil { - _ = os.Remove(name) - return fmt.Errorf("close probe: %w", closeErr) - } - if rmErr := os.Remove(name); rmErr != nil { - return fmt.Errorf("remove probe: %w", rmErr) - } - return nil -} diff --git a/health_test.go b/health_test.go deleted file mode 100644 index 1348045..0000000 --- a/health_test.go +++ /dev/null @@ -1,189 +0,0 @@ -package main - -import ( - "os" - "path/filepath" - "testing" - - "pgregory.net/rapid" -) - -// TestHealthMarker_SetCreatesAndRemoves covers the happy path: a writable -// dir, Set(true) creates the marker, Set(false) removes it. -func TestHealthMarker_SetCreatesAndRemoves(t *testing.T) { - path := filepath.Join(t.TempDir(), ".healthy") - m := newHealthMarker(path) - - m.Set(true) - if _, err := os.Stat(path); err != nil { - t.Fatalf("marker should exist after Set(true): %v", err) - } - - m.Set(false) - if _, err := os.Stat(path); !os.IsNotExist(err) { - t.Fatalf("marker should not exist after Set(false): %v", err) - } -} - -// TestHealthMarker_Cleanup confirms Cleanup removes the marker and is -// safe to call when the marker already does not exist. -func TestHealthMarker_Cleanup(t *testing.T) { - path := filepath.Join(t.TempDir(), ".healthy") - m := newHealthMarker(path) - - m.Set(true) - m.Cleanup() - if _, err := os.Stat(path); !os.IsNotExist(err) { - t.Fatalf("marker should be gone after Cleanup: %v", err) - } - - // Second cleanup must not error. - m.Cleanup() -} - -// TestHealthMarker_DegradedMode verifies that when the marker directory -// is not writable, the marker enters degraded mode: Set and Cleanup are -// no-ops and no file is ever created. -func TestHealthMarker_DegradedMode(t *testing.T) { - // Create a read-only directory to simulate a compose misconfiguration. - dir := filepath.Join(t.TempDir(), "ro") - if err := os.Mkdir(dir, 0o500); err != nil { - t.Fatalf("mkdir ro: %v", err) - } - - path := filepath.Join(dir, ".healthy") - m := newHealthMarker(path) - - if !m.degraded { - // Some environments (root, permissive filesystems like Windows - // or containers) allow writes through 0500; skip rather than - // fail in those cases. - t.Skip("test environment bypasses directory mode; skipping") - } - - m.Set(true) - if _, err := os.Stat(path); !os.IsNotExist(err) { - t.Fatalf("degraded marker should never create file: %v", err) - } - m.Cleanup() // must not panic -} - -// TestHealthMarker_Idempotent ensures repeated Set(true) and Set(false) -// calls are safe and converge to the expected file state. -func TestHealthMarker_Idempotent(t *testing.T) { - path := filepath.Join(t.TempDir(), ".healthy") - m := newHealthMarker(path) - - for range 3 { - m.Set(true) - } - if _, err := os.Stat(path); err != nil { - t.Fatalf("marker should exist after repeated Set(true): %v", err) - } - - for range 3 { - m.Set(false) - } - if _, err := os.Stat(path); !os.IsNotExist(err) { - t.Fatalf("marker should not exist after repeated Set(false): %v", err) - } -} - -// TestHealthMarker_Property exercises arbitrary Set sequences and asserts -// that the file state always matches the last Set argument. -func TestHealthMarker_Property(t *testing.T) { - dir := t.TempDir() - rapid.Check(t, func(rt *rapid.T) { - // A fresh subdir per iteration so markers from earlier iterations - // don't leak into later ones. - nonce := rapid.StringMatching(`[a-z0-9]{8}`).Draw(rt, "nonce") - subdir := filepath.Join(dir, nonce) - if err := os.Mkdir(subdir, 0o755); err != nil { - rt.Fatalf("mkdir subdir: %v", err) - } - path := filepath.Join(subdir, ".healthy") - m := newHealthMarker(path) - - calls := rapid.SliceOfN(rapid.Bool(), 1, 30).Draw(rt, "calls") - for _, ok := range calls { - m.Set(ok) - } - last := calls[len(calls)-1] - - _, err := os.Stat(path) - exists := err == nil - if exists != last { - rt.Fatalf("after Set(%v): exists=%v, want %v", - last, exists, last) - } - }) -} - -// TestProbeHealthDir_Writable confirms the probe succeeds on a normal -// writable temp dir and leaves no artifact behind. -func TestProbeHealthDir_Writable(t *testing.T) { - dir := t.TempDir() - if err := probeHealthDir(filepath.Join(dir, ".healthy")); err != nil { - t.Fatalf("probeHealthDir on writable dir: %v", err) - } - - entries, err := os.ReadDir(dir) - if err != nil { - t.Fatalf("readdir: %v", err) - } - if len(entries) != 0 { - t.Fatalf("probe left artifacts behind: %v", entries) - } -} - -// TestProbeHealthDir_NonExistent confirms a missing parent directory is -// reported as an error rather than masked. -func TestProbeHealthDir_NonExistent(t *testing.T) { - err := probeHealthDir(filepath.Join(t.TempDir(), "nope", ".healthy")) - if err == nil { - t.Fatal("expected error for non-existent parent dir") - } -} - -// --- Unit tests: probeCheck --- - -func TestProbeCheck_healthy_when_marker_exists(t *testing.T) { - dir := t.TempDir() - path := filepath.Join(dir, ".healthy") - if err := os.WriteFile(path, nil, 0o644); err != nil { - t.Fatalf("create marker: %v", err) - } - - got := probeCheck(path) - if got != 0 { - t.Errorf("probeCheck(marker exists) = %d, want 0", got) - } -} - -func TestProbeCheck_unhealthy_when_marker_absent_and_dir_writable(t *testing.T) { - dir := t.TempDir() - path := filepath.Join(dir, ".healthy") - - got := probeCheck(path) - if got != 1 { - t.Errorf("probeCheck(marker absent, writable dir) = %d, want 1", got) - } -} - -func TestProbeCheck_degraded_when_marker_absent_and_dir_not_writable(t *testing.T) { - dir := filepath.Join(t.TempDir(), "ro") - if err := os.Mkdir(dir, 0o500); err != nil { - t.Fatalf("mkdir: %v", err) - } - path := filepath.Join(dir, ".healthy") - - // If the OS ignores directory mode (root, Windows), skip. - if probeHealthDir(path) == nil { - t.Skip("test environment bypasses directory mode; skipping") - } - - got := probeCheck(path) - if got != 0 { - t.Errorf("probeCheck(degraded) = %d, want 0 (degraded reports healthy)", got) - } -} diff --git a/main.go b/main.go index 25e68cf..2f823a2 100644 --- a/main.go +++ b/main.go @@ -9,12 +9,13 @@ import ( "syscall" "filippo.io/age" + "github.com/cplieger/health" ) func main() { // CLI health probe for Docker healthcheck (distroless has no curl/wget). if len(os.Args) > 1 && os.Args[1] == modeHealth { - runProbe(healthMarkerPath) + health.RunProbe(health.DefaultPath) } cfg, err := parseConfig() @@ -62,7 +63,7 @@ func runServer(repoRoot string, identity age.Identity) int { ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() - marker := newHealthMarker(healthMarkerPath) + marker := health.NewMarker(health.DefaultPath) marker.Set(false) result, err := decryptAll(ctx, repoRoot, identity)