package host import ( "context" "encoding/json" "errors" "os" "os/exec" "path/filepath" "testing" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/release" ) func TestPlan14GenerationRetirementBindingProcessCrash(t *testing.T) { if dir := os.Getenv("PLAN14_E21_RETIRE_DIR"); dir != "" { f, disk, rec := generationLaunchFixture(t) state := &rollbackDisk{dir: filepath.Join(dir, "fences")} disk.dir = filepath.Join(dir, "generations") for _, path := range []string{state.dir, disk.dir} { if err := os.Mkdir(path, 0700); err != nil { t.Fatal(err) } } f.root.Guard.Root = hostguard.NewRootAuthority(state) installation := hostguard.RootInstallation{Host: rec.Host, Account: rec.Account, App: rec.App, Record: provision.RecordKey(rec.Account, rec.App, rec.Host), Incarnation: f.target.Incarnation, Stamp: f.target.Stamp, Box: f.target.Box, Enrollment: f.target.Enrollment, Migration: 1, InventoryRevision: 1} if err := f.root.Guard.EstablishRoot(t.Context(), installation, func(*hostclaim.Claim) error { return nil }); err != nil { t.Fatal(err) } in, err := f.journal.Read(t.Context(), f.target) if err != nil { t.Fatal(err) } journal, err := hostbox.OpenLaunchJournal(filepath.Join(dir, "launch.db")) if err != nil { t.Fatal(err) } defer journal.Close() if err := journal.Prelaunch(t.Context(), in); err != nil { t.Fatal(err) } f.root.Journal = journal if _, err := f.root.Serve(t.Context(), f.peer, f.req); err != nil { t.Fatal(err) } r := readGenerationLaunch(t, disk) if err := f.root.stop(t.Context(), f.peer, *f.req.Reservation); err != nil { t.Fatal(err) } if err := journal.CloseWriters(t.Context(), f.target); err != nil { t.Fatal(err) } phase := os.Getenv("PLAN14_E21_RETIRE_PHASE") disk.afterWrite = func(_ string, raw []byte) { var record release.GenerationReservation if err := json.Unmarshal(raw, &record); err != nil { t.Fatal(err) } if phase == "closed" && record.Closed && record.Retirement == "" || phase == "retired" && record.Retirement != "" { os.Exit(75) } } verifier := generationRetirement{guard: f.root.Guard, journal: journal, kernel: func(context.Context, release.GenerationReservation) (string, error) { if phase == "kernel" { os.Exit(75) } return r.Binding.Digest, nil }} if err := verifier.retire(t.Context(), &release.RootGenerations{Store: disk}, r); err != nil { t.Fatal(err) } t.Fatal("crash boundary not reached") } for _, phase := range []string{"closed", "kernel", "retired"} { t.Run(phase, func(t *testing.T) { dir := t.TempDir() cmd := exec.Command(os.Args[0], "-test.run=^TestPlan14GenerationRetirementBindingProcessCrash$", "-test.count=1") cmd.Dir = dir cmd.Env = append(os.Environ(), "PLAN14_E21_RETIRE_DIR="+dir, "PLAN14_E21_RETIRE_PHASE="+phase) out, err := cmd.CombinedOutput() var exit *exec.ExitError if !errors.As(err, &exit) || exit.ExitCode() != 75 { t.Fatalf("child: %v %s", err, out) } disk := &generationDisk{rollbackDisk: &rollbackDisk{dir: filepath.Join(dir, "generations")}} r := readGenerationLaunch(t, disk) g := &release.RootGenerations{Store: disk} got, err := g.Read(r.Binding) if err != nil || !got.Closed || (got.Retirement != "") != (phase == "retired") { t.Fatalf("lost restriction: %+v %v", got, err) } if _, err := g.Reserve(r.Binding); err == nil { t.Fatal("crash reopened launch") } journal, err := hostbox.OpenLaunchJournal(filepath.Join(dir, "launch.db")) if err != nil { t.Fatal(err) } defer journal.Close() verifier := generationRetirement{guard: hostguard.Guard{DataDir: filepath.Join(dir, "data"), LockDir: t.TempDir(), Root: hostguard.NewRootAuthority(&rollbackDisk{dir: filepath.Join(dir, "fences")})}, journal: journal, kernel: func(context.Context, release.GenerationReservation) (string, error) { return "", os.ErrPermission }} if phase != "retired" { if err := verifier.retire(t.Context(), g, r); err == nil { t.Fatal("reopened without kernel evidence") } got, err = g.Read(r.Binding) if err != nil || got.Retirement != "" { t.Fatal("failed reconciliation released pin", err) } } verifier.kernel = func(context.Context, release.GenerationReservation) (string, error) { return r.Binding.Digest, nil } if err := verifier.retire(t.Context(), g, r); err != nil { t.Fatal(err) } got, err = g.Read(r.Binding) if err != nil || got.Retirement == "" { t.Fatal("verified recovery not retained", err) } }) } }