| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/json" |
| 6 | "errors" |
| 7 | "os" |
| 8 | "os/exec" |
| 9 | "path/filepath" |
| 10 | "testing" |
| 11 | |
| 12 | "github.com/carlosframework/platform/internal/hostbox" |
| 13 | "github.com/carlosframework/platform/internal/hostclaim" |
| 14 | "github.com/carlosframework/platform/internal/hostguard" |
| 15 | "github.com/carlosframework/platform/internal/provision" |
| 16 | "github.com/carlosframework/platform/internal/release" |
| 17 | ) |
| 18 | |
| 19 | func TestPlan14GenerationRetirementBindingProcessCrash(t *testing.T) { |
| 20 | if dir := os.Getenv("PLAN14_E21_RETIRE_DIR"); dir != "" { |
| 21 | f, disk, rec := generationLaunchFixture(t) |
| 22 | state := &rollbackDisk{dir: filepath.Join(dir, "fences")} |
| 23 | disk.dir = filepath.Join(dir, "generations") |
| 24 | for _, path := range []string{state.dir, disk.dir} { |
| 25 | if err := os.Mkdir(path, 0700); err != nil { |
| 26 | t.Fatal(err) |
| 27 | } |
| 28 | } |
| 29 | f.root.Guard.Root = hostguard.NewRootAuthority(state) |
| 30 | 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} |
| 31 | if err := f.root.Guard.EstablishRoot(t.Context(), installation, func(*hostclaim.Claim) error { return nil }); err != nil { |
| 32 | t.Fatal(err) |
| 33 | } |
| 34 | in, err := f.journal.Read(t.Context(), f.target) |
| 35 | if err != nil { |
| 36 | t.Fatal(err) |
| 37 | } |
| 38 | journal, err := hostbox.OpenLaunchJournal(filepath.Join(dir, "launch.db")) |
| 39 | if err != nil { |
| 40 | t.Fatal(err) |
| 41 | } |
| 42 | defer journal.Close() |
| 43 | if err := journal.Prelaunch(t.Context(), in); err != nil { |
| 44 | t.Fatal(err) |
| 45 | } |
| 46 | f.root.Journal = journal |
| 47 | if _, err := f.root.Serve(t.Context(), f.peer, f.req); err != nil { |
| 48 | t.Fatal(err) |
| 49 | } |
| 50 | r := readGenerationLaunch(t, disk) |
| 51 | if err := f.root.stop(t.Context(), f.peer, *f.req.Reservation); err != nil { |
| 52 | t.Fatal(err) |
| 53 | } |
| 54 | if err := journal.CloseWriters(t.Context(), f.target); err != nil { |
| 55 | t.Fatal(err) |
| 56 | } |
| 57 | phase := os.Getenv("PLAN14_E21_RETIRE_PHASE") |
| 58 | disk.afterWrite = func(_ string, raw []byte) { |
| 59 | var record release.GenerationReservation |
| 60 | if err := json.Unmarshal(raw, &record); err != nil { |
| 61 | t.Fatal(err) |
| 62 | } |
| 63 | if phase == "closed" && record.Closed && record.Retirement == "" || phase == "retired" && record.Retirement != "" { |
| 64 | os.Exit(75) |
| 65 | } |
| 66 | } |
| 67 | verifier := generationRetirement{guard: f.root.Guard, journal: journal, kernel: func(context.Context, release.GenerationReservation) (string, error) { |
| 68 | if phase == "kernel" { |
| 69 | os.Exit(75) |
| 70 | } |
| 71 | return r.Binding.Digest, nil |
| 72 | }} |
| 73 | if err := verifier.retire(t.Context(), &release.RootGenerations{Store: disk}, r); err != nil { |
| 74 | t.Fatal(err) |
| 75 | } |
| 76 | t.Fatal("crash boundary not reached") |
| 77 | } |
| 78 | for _, phase := range []string{"closed", "kernel", "retired"} { |
| 79 | t.Run(phase, func(t *testing.T) { |
| 80 | dir := t.TempDir() |
| 81 | cmd := exec.Command(os.Args[0], "-test.run=^TestPlan14GenerationRetirementBindingProcessCrash$", "-test.count=1") |
| 82 | cmd.Dir = dir |
| 83 | cmd.Env = append(os.Environ(), "PLAN14_E21_RETIRE_DIR="+dir, "PLAN14_E21_RETIRE_PHASE="+phase) |
| 84 | out, err := cmd.CombinedOutput() |
| 85 | var exit *exec.ExitError |
| 86 | if !errors.As(err, &exit) || exit.ExitCode() != 75 { |
| 87 | t.Fatalf("child: %v %s", err, out) |
| 88 | } |
| 89 | disk := &generationDisk{rollbackDisk: &rollbackDisk{dir: filepath.Join(dir, "generations")}} |
| 90 | r := readGenerationLaunch(t, disk) |
| 91 | g := &release.RootGenerations{Store: disk} |
| 92 | got, err := g.Read(r.Binding) |
| 93 | if err != nil || !got.Closed || (got.Retirement != "") != (phase == "retired") { |
| 94 | t.Fatalf("lost restriction: %+v %v", got, err) |
| 95 | } |
| 96 | if _, err := g.Reserve(r.Binding); err == nil { |
| 97 | t.Fatal("crash reopened launch") |
| 98 | } |
| 99 | journal, err := hostbox.OpenLaunchJournal(filepath.Join(dir, "launch.db")) |
| 100 | if err != nil { |
| 101 | t.Fatal(err) |
| 102 | } |
| 103 | defer journal.Close() |
| 104 | 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 }} |
| 105 | if phase != "retired" { |
| 106 | if err := verifier.retire(t.Context(), g, r); err == nil { |
| 107 | t.Fatal("reopened without kernel evidence") |
| 108 | } |
| 109 | got, err = g.Read(r.Binding) |
| 110 | if err != nil || got.Retirement != "" { |
| 111 | t.Fatal("failed reconciliation released pin", err) |
| 112 | } |
| 113 | } |
| 114 | verifier.kernel = func(context.Context, release.GenerationReservation) (string, error) { return r.Binding.Digest, nil } |
| 115 | if err := verifier.retire(t.Context(), g, r); err != nil { |
| 116 | t.Fatal(err) |
| 117 | } |
| 118 | got, err = g.Read(r.Binding) |
| 119 | if err != nil || got.Retirement == "" { |
| 120 | t.Fatal("verified recovery not retained", err) |
| 121 | } |
| 122 | }) |
| 123 | } |
| 124 | } |
| 125 | |