//go:build linux && rootrunner package host import ( "os" "os/exec" "path/filepath" "strconv" "testing" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/rootstate" ) func TestPlan14GenerationKernelRetirementRootRunner(t *testing.T) { dir := os.Getenv("PLAN14_E25_RETIRE_KERNEL_CHILD") if dir == "" { dir = generationRootRunnerDir(t) cmd := exec.CommandContext(t.Context(), "unshare", "--mount", "--pid", "--fork", "--mount-proc", os.Args[0], "-test.run=^TestPlan14GenerationKernelRetirementRootRunner$", "-test.count=1") cmd.Dir = dir cmd.Env = append(os.Environ(), "PLAN14_E25_RETIRE_KERNEL_CHILD="+dir) if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("isolated root retirement: %v %s", err, out) } return } t.Setenv("CARLOS_OPT_DIR", dir) path := release.VersionPath(dir, "bab", "v1", "shop") if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { t.Fatal(err) } if err := os.WriteFile(path, []byte("retained generation"), 0644); err != nil { t.Fatal(err) } identity := func(path string) rootstate.FileIdentity { t.Helper() file, err := os.Open(path) if err != nil { t.Fatal(err) } defer file.Close() id, err := rootstate.Identity(file) if err != nil { t.Fatal(err) } return id } k := generationKernel{proc: "/proc"} boot, err := k.boot() if err != nil { t.Fatal(err) } controller := exec.CommandContext(t.Context(), "/bin/sleep", "60") controller.Dir = dir if err := controller.Start(); err != nil { t.Fatal(err) } joined := false t.Cleanup(func() { if !joined { _ = controller.Process.Kill() _ = controller.Wait() } }) raw, err := generationRead(filepath.Join("/proc", strconv.Itoa(controller.Process.Pid), "stat")) if err != nil { t.Fatal(err) } start, err := generationStart(raw) if err != nil { t.Fatal(err) } r := release.GenerationReservation{Binding: release.GenerationBinding{ Binding: release.HistoryBinding{Host: filepath.Base(dir) + ".example", Account: "bab", App: "shop"}, Target: release.GenerationOwner{BootID: boot}, Version: "v1", Path: path, Tree: filepath.Dir(path), Executable: identity(path), TreeIdentity: identity(filepath.Dir(path)), ControllerPID: controller.Process.Pid, ControllerStart: start, }} if _, err := productionGenerationAbsence(t.Context(), r); err == nil { t.Fatal("live controller released") } if err := controller.Process.Kill(); err != nil { t.Fatal(err) } _ = controller.Wait() joined = true if digest, err := productionGenerationAbsence(t.Context(), r); err != nil || len(digest) != 64 { t.Fatalf("joined controller: %s %v", digest, err) } file, err := os.Open(path) if err != nil { t.Fatal(err) } if _, err := productionGenerationAbsence(t.Context(), r); err == nil { file.Close() t.Fatal("controller join erased an open reference") } file.Close() if err := os.Rename(filepath.Dir(path), filepath.Dir(path)+".old"); err != nil { t.Fatal(err) } if err := os.Mkdir(filepath.Dir(path), 0755); err != nil { t.Fatal(err) } if err := os.WriteFile(path, []byte("replacement"), 0644); err != nil { t.Fatal(err) } if _, err := productionGenerationAbsence(t.Context(), r); err == nil { t.Fatal("replaced staged tree discharged the original identity") } }