| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/json" |
| 6 | "os" |
| 7 | "os/exec" |
| 8 | "path/filepath" |
| 9 | "testing" |
| 10 | |
| 11 | "github.com/carlosframework/platform/internal/hostbox" |
| 12 | "github.com/carlosframework/platform/internal/hostguard" |
| 13 | "github.com/carlosframework/platform/internal/release" |
| 14 | ) |
| 15 | |
| 16 | func plan14RebindProcessRoot(t *testing.T, dir string) (ClaimRootHTTP, *rollbackDisk, recoveryRebindRequest) { |
| 17 | root, disk := plan14InventoryProcessRoot(t, dir) |
| 18 | root.InventoryActor = func(context.Context) (hostbox.Actor, error) { |
| 19 | return hostbox.Actor{Box: "box-1", Enrollment: "enrollment-1", Revision: 7, Binding: "7"}, nil |
| 20 | } |
| 21 | raw, err := os.ReadFile(filepath.Join(dir, "request.json")) |
| 22 | if err != nil { |
| 23 | t.Fatal(err) |
| 24 | } |
| 25 | var q recoveryRebindRequest |
| 26 | if err = json.Unmarshal(raw, &q); err != nil { |
| 27 | t.Fatal(err) |
| 28 | } |
| 29 | return root, disk, q |
| 30 | } |
| 31 | func TestPlan14RecoveryRebindProcessCrash(t *testing.T) { |
| 32 | t.Parallel() |
| 33 | if phase := os.Getenv("PLAN14_REBIND_CRASH"); phase != "" { |
| 34 | root, disk, q := plan14RebindProcessRoot(t, os.Getenv("PLAN14_REBIND_DIR")) |
| 35 | writes := 0 |
| 36 | disk.beforeWrite = func(string, []byte) { |
| 37 | if phase == "before intent" && writes == 0 || phase == "database" && writes == 1 { |
| 38 | os.Exit(73) |
| 39 | } |
| 40 | } |
| 41 | disk.afterWrite = func(string, []byte) { |
| 42 | writes++ |
| 43 | if phase == "intent" && writes == 1 || phase == "commit" && writes == 2 { |
| 44 | os.Exit(73) |
| 45 | } |
| 46 | } |
| 47 | err := root.rebindRecoveryInventory(t.Context(), q) |
| 48 | t.Fatal("crash not reached", phase, err) |
| 49 | } |
| 50 | for _, phase := range []string{"before intent", "intent", "database", "commit"} { |
| 51 | t.Run(phase, func(t *testing.T) { |
| 52 | f, original, disk, q := plan14RecoveryRebind(t) |
| 53 | dir := t.TempDir() |
| 54 | for _, name := range []string{"root", "data", "opt", "units", "locks", "child"} { |
| 55 | if err := os.Mkdir(filepath.Join(dir, name), 0700); err != nil { |
| 56 | t.Fatal(err) |
| 57 | } |
| 58 | } |
| 59 | store := release.NewDirStore(filepath.Join(dir, "deployment")) |
| 60 | keys, err := f.claims.List(t.Context(), "") |
| 61 | if err != nil { |
| 62 | t.Fatal(err) |
| 63 | } |
| 64 | for _, key := range keys { |
| 65 | body, _, e := f.claims.Get(t.Context(), key) |
| 66 | if e != nil { |
| 67 | t.Fatal(e) |
| 68 | } |
| 69 | if _, e = store.Put(t.Context(), key, body, ""); e != nil { |
| 70 | t.Fatal(e) |
| 71 | } |
| 72 | } |
| 73 | raw, err := disk.Read(q.Next.Host + ".json") |
| 74 | if err != nil { |
| 75 | t.Fatal(err) |
| 76 | } |
| 77 | if err = os.WriteFile(filepath.Join(dir, "root", q.Next.Host+".json"), raw, 0600); err != nil { |
| 78 | t.Fatal(err) |
| 79 | } |
| 80 | raw, _ = json.Marshal(q) |
| 81 | if err = os.WriteFile(filepath.Join(dir, "request.json"), raw, 0600); err != nil { |
| 82 | t.Fatal(err) |
| 83 | } |
| 84 | root, _, _ := plan14RebindProcessRoot(t, dir) |
| 85 | source, _, err := original.Reconcile.Inventory.Snapshot(t.Context(), q.Next.Host, q.Prior) |
| 86 | if err != nil { |
| 87 | t.Fatal(err) |
| 88 | } |
| 89 | if err = root.Reconcile.Inventory.WriteStopInventory(t.Context(), source); err != nil { |
| 90 | t.Fatal(err) |
| 91 | } |
| 92 | cmd := exec.CommandContext(t.Context(), os.Args[0], "-test.run=^TestPlan14RecoveryRebindProcessCrash$") |
| 93 | cmd.Dir = filepath.Join(dir, "child") |
| 94 | cmd.Env = append(os.Environ(), "PLAN14_REBIND_CRASH="+phase, "PLAN14_REBIND_DIR="+dir) |
| 95 | output, err := cmd.CombinedOutput() |
| 96 | exit, ok := err.(*exec.ExitError) |
| 97 | if !ok || exit.ExitCode() != 73 { |
| 98 | t.Fatalf("crash %v: %s", err, output) |
| 99 | } |
| 100 | resumed, _, _ := plan14RebindProcessRoot(t, dir) |
| 101 | if err = resumed.recoverRootInventory(t.Context()); err != nil { |
| 102 | t.Fatal("startup recovery", err) |
| 103 | } |
| 104 | if err = resumed.rebindRecoveryInventory(t.Context(), q); err != nil { |
| 105 | t.Fatal("replay", err) |
| 106 | } |
| 107 | view, err := resumed.Stop.Coordinator.Guard.Root.View(t.Context(), q.Next.Host, hostguard.Captured{Incarnation: q.Next.Incarnation, Stamp: q.Next.Stamp}) |
| 108 | if err != nil || view.Installation.Stamp != q.Next.Stamp || len(view.Retired) != 1 || view.Retired[0] != q.Prior { |
| 109 | t.Fatal(view, err) |
| 110 | } |
| 111 | _, rev, err := resumed.Reconcile.Inventory.Snapshot(t.Context(), q.Next.Host, view.Captured) |
| 112 | if err != nil || rev != 1 { |
| 113 | t.Fatal(rev, err) |
| 114 | } |
| 115 | }) |
| 116 | } |
| 117 | } |
| 118 | |