controller v0.267.0: tests off DooPlex's Docker, cut-off copies refused, two pages true
gates / gates (push) Successful in 26s
gates / gates (push) Successful in 26s
R-650: internal/dockerexec — every docker exec routed through it; under go test a real docker is refused (opt-in FELHOM_TEST_REAL_DOCKER=1; a stub under the temp dir is allowed). api/stacks/web tests run under a silent stub (TestMain). TestR650_NoBareDockerExec pins it repo-wide. R-640: a dump without its engine's completion marker is refused before the first mutation (unit + off-site restore) and again before any load. R-499: the Tier-2 page's system-disk sentence has four true branches. R-518: the backup button states the measured ~8 min stop. R-626: measured on 9202, not reproduced. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
// Package dockerexec is the ONE way the controller builds a `docker` / `docker compose` /
|
||||
// `docker-compose` process. It exists for R-650: the controller's unit tests run on DooPlex, the
|
||||
// build host, which is production Docker (Gitea, the registry, k3s). A test that fell through to a
|
||||
// real `docker run … tar` created a volume there, and one ran a real `docker compose down`.
|
||||
//
|
||||
// Under `go test` a docker command is REFUSED — its Start/Run/Output returns an error naming the
|
||||
// command — unless one of two things holds:
|
||||
//
|
||||
// - FELHOM_TEST_REAL_DOCKER=1 is set (a deliberate, per-run opt-in); or
|
||||
// - the executable resolves under os.TempDir() — a stub a test wrote into t.TempDir() and put on
|
||||
// PATH, which is a seam, not Docker.
|
||||
//
|
||||
// Outside `go test` (the real controller) nothing changes: Command is exec.Command.
|
||||
// Pinned by TestR650_* in this package and by the repo-wide TestR650_NoBareDockerExec sweep.
|
||||
package dockerexec
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// OptInEnv is the environment variable that lets a test reach the real docker on purpose.
|
||||
const OptInEnv = "FELHOM_TEST_REAL_DOCKER"
|
||||
|
||||
// underTest is a variable so this package's own tests can model the production binary.
|
||||
var underTest = testing.Testing
|
||||
|
||||
// IsDocker reports whether name is a docker binary (by base name).
|
||||
func IsDocker(name string) bool {
|
||||
b := filepath.Base(name)
|
||||
return b == "docker" || b == "docker-compose"
|
||||
}
|
||||
|
||||
// refusal returns the error a refused command carries, or nil when the command may run.
|
||||
func refusal(name string, args []string) error {
|
||||
if !IsDocker(name) || !underTest() || os.Getenv(OptInEnv) == "1" {
|
||||
return nil
|
||||
}
|
||||
if p, err := exec.LookPath(name); err == nil {
|
||||
if abs, aerr := filepath.Abs(p); aerr == nil {
|
||||
tmp := filepath.Clean(os.TempDir()) + string(os.PathSeparator)
|
||||
if strings.HasPrefix(abs, tmp) {
|
||||
return nil // a test's own stub on PATH
|
||||
}
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("R-650: refused to run the real %q under go test (this host may be production Docker); "+
|
||||
"use a seam or a stub on PATH, or set %s=1 deliberately",
|
||||
strings.TrimSpace(name+" "+strings.Join(args, " ")), OptInEnv)
|
||||
}
|
||||
|
||||
// Command is exec.Command for a docker binary, refused under go test (see package doc).
|
||||
// A non-docker name passes through unchanged, so a generic runner may call it for any command.
|
||||
func Command(name string, args ...string) *exec.Cmd {
|
||||
cmd := exec.Command(name, args...)
|
||||
if err := refusal(name, args); err != nil {
|
||||
cmd.Err = err
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
// CommandContext is exec.CommandContext with the same guard.
|
||||
func CommandContext(ctx context.Context, name string, args ...string) *exec.Cmd {
|
||||
cmd := exec.CommandContext(ctx, name, args...)
|
||||
if err := refusal(name, args); err != nil {
|
||||
cmd.Err = err
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Runner is what *testing.M offers; named so this file does not need a test-only type.
|
||||
type Runner interface{ Run() int }
|
||||
|
||||
// RunWithStub is for a package's TestMain: it puts a `docker` and a `docker-compose` that print
|
||||
// nothing and exit 0 at the front of PATH for the whole test binary, then runs the tests. It is the
|
||||
// package-wide seam for fixtures that build a real stacks.Manager (whose NewManager/ScanStacks run
|
||||
// `docker compose version` and `docker ps`). A test that needs a specific answer still writes its
|
||||
// own stub; a test that needs the real docker sets OptInEnv. (R-650)
|
||||
func RunWithStub(m Runner) int {
|
||||
dir, err := os.MkdirTemp("", "r650-docker-stub-")
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "R-650 stub:", err)
|
||||
return 1
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
for _, n := range []string{"docker", "docker-compose"} {
|
||||
if err := os.WriteFile(filepath.Join(dir, n), []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "R-650 stub:", err)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
os.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
return m.Run()
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package dockerexec
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestR650_RealDockerIsRefusedUnderGoTest is the decoy: a harmless-looking `docker ps` with the
|
||||
// real PATH must NOT run. The consequence asserted is that Run returns the refusal, naming the
|
||||
// command — and that nothing was started (ProcessState stays nil).
|
||||
func TestR650_RealDockerIsRefusedUnderGoTest(t *testing.T) {
|
||||
t.Setenv(OptInEnv, "")
|
||||
for _, name := range []string{"docker", "docker-compose", "/usr/bin/docker"} {
|
||||
cmd := Command(name, "ps", "-a")
|
||||
err := cmd.Run()
|
||||
if err == nil || !strings.Contains(err.Error(), "R-650") || !strings.Contains(err.Error(), "ps -a") {
|
||||
t.Fatalf("%s: want an R-650 refusal naming the command, got %v", name, err)
|
||||
}
|
||||
if cmd.ProcessState != nil {
|
||||
t.Fatalf("%s: a process was started", name)
|
||||
}
|
||||
c2 := CommandContext(context.Background(), name, "volume", "create", "r650-decoy")
|
||||
if _, err := c2.CombinedOutput(); err == nil || !strings.Contains(err.Error(), "volume create r650-decoy") {
|
||||
t.Fatalf("%s: CommandContext not refused: %v", name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestR650_StubOnPathIsAllowed — a test's own fake in t.TempDir() is a seam, not Docker.
|
||||
func TestR650_StubOnPathIsAllowed(t *testing.T) {
|
||||
bin := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(bin, "docker"), []byte("#!/bin/sh\necho stub:$*\n"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv("PATH", bin)
|
||||
out, err := Command("docker", "ps").CombinedOutput()
|
||||
if err != nil || strings.TrimSpace(string(out)) != "stub:ps" {
|
||||
t.Fatalf("stub should run: out=%q err=%v", out, err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestR650_OptInAndProductionAndNonDockerPassThrough — the guard refuses nothing else.
|
||||
func TestR650_OptInAndProductionAndNonDockerPassThrough(t *testing.T) {
|
||||
t.Setenv(OptInEnv, "1")
|
||||
if err := refusal("docker", []string{"ps"}); err != nil {
|
||||
t.Fatalf("opt-in must allow: %v", err)
|
||||
}
|
||||
t.Setenv(OptInEnv, "")
|
||||
if err := refusal("du", []string{"-sb", "/"}); err != nil {
|
||||
t.Fatalf("a non-docker command must pass: %v", err)
|
||||
}
|
||||
old := underTest
|
||||
underTest = func() bool { return false }
|
||||
defer func() { underTest = old }()
|
||||
if err := refusal("docker", []string{"ps"}); err != nil {
|
||||
t.Fatalf("the production binary must never refuse: %v", err)
|
||||
}
|
||||
if Command("docker", "ps").Err != nil {
|
||||
t.Fatal("production Command carried an error")
|
||||
}
|
||||
}
|
||||
|
||||
// TestR650_NoBareDockerExec pins the invariant the package doc states: every production path that
|
||||
// builds a docker process goes through this package. A new `exec.Command("docker", …)` in
|
||||
// non-test code fails here, naming the file and line.
|
||||
func TestR650_NoBareDockerExec(t *testing.T) {
|
||||
root := filepath.Join("..", "..")
|
||||
bare := regexp.MustCompile(`\bexec\.Command(Context)?\(([^,()]+, )?"docker`)
|
||||
var hits []string
|
||||
n := 0
|
||||
err := filepath.Walk(root, func(p string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.IsDir() || !strings.HasSuffix(p, ".go") || strings.HasSuffix(p, "_test.go") {
|
||||
return nil
|
||||
}
|
||||
b, err := os.ReadFile(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n++
|
||||
for i, line := range strings.Split(string(b), "\n") {
|
||||
if bare.MatchString(line) {
|
||||
hits = append(hits, p+":"+strconv.Itoa(i+1)+": "+strings.TrimSpace(line))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n < 100 {
|
||||
t.Fatalf("scope: only %d Go files walked — the sweep is not looking at the tree", n)
|
||||
}
|
||||
if len(hits) > 0 {
|
||||
t.Fatalf("bare docker exec outside dockerexec (R-650):\n%s", strings.Join(hits, "\n"))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user