Abort the seed and probe scripts on the first failed command
Guard the staged size as a string so an unmeasurable file fails closed.
This commit is contained in:
@@ -39,12 +39,60 @@ func inFlightZone(t *testing.T) (dir, path string) {
|
||||
}
|
||||
|
||||
func runSeedScript(t *testing.T, sh, path string, plan SeedPlan, content, stdin string) error {
|
||||
t.Helper()
|
||||
return runSeedScriptWithPath(t, sh, path, plan, content, stdin, "")
|
||||
}
|
||||
|
||||
func runSeedScriptWithPath(t *testing.T, sh, path string, plan SeedPlan, content, stdin, pathEnv string) error {
|
||||
t.Helper()
|
||||
cmd := exec.Command(sh, "-c", seedScript(path, plan, len(content)))
|
||||
cmd.Stdin = strings.NewReader(stdin)
|
||||
if pathEnv != "" {
|
||||
cmd.Env = append(os.Environ(), "PATH="+pathEnv)
|
||||
}
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// shimPath puts a stand-in for tool at the front of a PATH, so the generated
|
||||
// script meets a failing command where a real image would meet a working one.
|
||||
func shimPath(t *testing.T, tool, body string) string {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(dir, tool), []byte("#!/bin/sh\n"+body+"\n"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return dir + string(os.PathListSeparator) + os.Getenv("PATH")
|
||||
}
|
||||
|
||||
func realTool(t *testing.T, tool string) string {
|
||||
t.Helper()
|
||||
p, err := exec.LookPath(tool)
|
||||
if err != nil {
|
||||
t.Skipf("need %s: %v", tool, err)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// assertZoneUntouched checks the in-flight layout survived a failed run whole:
|
||||
// live serial 1 still at path, journal still there, nothing quarantined and no
|
||||
// staging file left behind.
|
||||
func assertZoneUntouched(t *testing.T, dir, path string) {
|
||||
t.Helper()
|
||||
found := siblings(t, dir)
|
||||
serial, ok := ParseZoneSerial(found[filepath.Base(path)])
|
||||
if !ok || serial != 1 {
|
||||
t.Errorf("zone file was replaced by a failed run: serial = (%d,%v)", serial, ok)
|
||||
}
|
||||
if _, ok := found[filepath.Base(JournalPath(path))]; !ok {
|
||||
t.Error("the journal was lost by a failed run")
|
||||
}
|
||||
for name := range found {
|
||||
if strings.Contains(name, quarantineMarker) {
|
||||
t.Errorf("a failed run must not leave a quarantined file: %s", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func probeState(t *testing.T, sh, path string) ZoneDiskState {
|
||||
t.Helper()
|
||||
out, err := exec.Command(sh, "-c", zoneStateProbe(path)).Output()
|
||||
@@ -166,3 +214,66 @@ func keys(m map[string]string) []string {
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// A rename that fails leaves live data where it is, so the install must not
|
||||
// happen: the skeleton beside a higher-serial journal is the production failure
|
||||
// this seed exists to avoid.
|
||||
func TestSeedScriptFailedQuarantineAbortsInstall(t *testing.T) {
|
||||
sh := requireShell(t, "wc", "tr", "mv", "rm", "mkdir", "dirname")
|
||||
dir, path := inFlightZone(t)
|
||||
pathEnv := shimPath(t, "mv", `case "$*" in *`+quarantineMarker+`*) exit 1;; esac
|
||||
exec `+realTool(t, "mv")+` "$@"`)
|
||||
|
||||
plan := PlanSeed(probeState(t, sh, path))
|
||||
content := renderSeedZone("example.com", "10.0.0.1", plan.Serial)
|
||||
if err := runSeedScriptWithPath(t, sh, path, plan, content, content, pathEnv); err == nil {
|
||||
t.Fatal("a failed quarantine must fail the seed")
|
||||
}
|
||||
assertZoneUntouched(t, dir, path)
|
||||
}
|
||||
|
||||
// The size guard has to fail closed: an image without wc cannot measure the
|
||||
// staged file, and an unverified file must never be installed.
|
||||
func TestSeedScriptUnmeasurableStagingAbortsInstall(t *testing.T) {
|
||||
sh := requireShell(t, "wc", "tr", "mv", "rm", "mkdir", "dirname")
|
||||
dir, path := inFlightZone(t)
|
||||
pathEnv := shimPath(t, "wc", "exit 127")
|
||||
|
||||
plan := PlanSeed(probeState(t, sh, path))
|
||||
content := renderSeedZone("example.com", "10.0.0.1", plan.Serial)
|
||||
if err := runSeedScriptWithPath(t, sh, path, plan, content, content, pathEnv); err == nil {
|
||||
t.Fatal("an unmeasurable staging file must fail the seed")
|
||||
}
|
||||
assertZoneUntouched(t, dir, path)
|
||||
if _, ok := siblings(t, dir)[filepath.Base(path)+seedTempSuffix]; ok {
|
||||
t.Error("the staging file should have been cleaned up")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSeedScriptFailedMkdirAbortsInstall(t *testing.T) {
|
||||
sh := requireShell(t, "wc", "tr", "mv", "rm", "mkdir", "dirname")
|
||||
dir, path := inFlightZone(t)
|
||||
pathEnv := shimPath(t, "mkdir", "exit 1")
|
||||
|
||||
plan := PlanSeed(probeState(t, sh, path))
|
||||
content := renderSeedZone("example.com", "10.0.0.1", plan.Serial)
|
||||
if err := runSeedScriptWithPath(t, sh, path, plan, content, content, pathEnv); err == nil {
|
||||
t.Fatal("a failed mkdir must fail the seed")
|
||||
}
|
||||
assertZoneUntouched(t, dir, path)
|
||||
}
|
||||
|
||||
// The probe decides whether there is anything to preserve, so a tool it cannot
|
||||
// run must be an error rather than a state that reads as "nothing readable".
|
||||
func TestZoneStateProbeFailedToolIsAnError(t *testing.T) {
|
||||
sh := requireShell(t, "head", "od", "tr")
|
||||
_, path := inFlightZone(t)
|
||||
pathEnv := shimPath(t, "tr", "exit 127")
|
||||
|
||||
cmd := exec.Command(sh, "-c", zoneStateProbe(path))
|
||||
cmd.Env = append(os.Environ(), "PATH="+pathEnv)
|
||||
out, err := cmd.Output()
|
||||
if err == nil {
|
||||
t.Fatalf("probe reported success without reading the journal header: %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user