Files
felhom-controller/controller/internal/dockerexec/dockerexec.go
T
admin 80e6ad8c47
gates / gates (push) Successful in 26s
controller v0.267.0: tests off DooPlex's Docker, cut-off copies refused, two pages true
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
2026-09-23 20:25:28 +02:00

100 lines
3.8 KiB
Go

// 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()
}