package host import ( "context" "encoding/json" "os" "os/exec" "path/filepath" "testing" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/release" ) func plan14RebindProcessRoot(t *testing.T, dir string) (ClaimRootHTTP, *rollbackDisk, recoveryRebindRequest) { root, disk := plan14InventoryProcessRoot(t, dir) root.InventoryActor = func(context.Context) (hostbox.Actor, error) { return hostbox.Actor{Box: "box-1", Enrollment: "enrollment-1", Revision: 7, Binding: "7"}, nil } raw, err := os.ReadFile(filepath.Join(dir, "request.json")) if err != nil { t.Fatal(err) } var q recoveryRebindRequest if err = json.Unmarshal(raw, &q); err != nil { t.Fatal(err) } return root, disk, q } func TestPlan14RecoveryRebindProcessCrash(t *testing.T) { t.Parallel() if phase := os.Getenv("PLAN14_REBIND_CRASH"); phase != "" { root, disk, q := plan14RebindProcessRoot(t, os.Getenv("PLAN14_REBIND_DIR")) writes := 0 disk.beforeWrite = func(string, []byte) { if phase == "before intent" && writes == 0 || phase == "database" && writes == 1 { os.Exit(73) } } disk.afterWrite = func(string, []byte) { writes++ if phase == "intent" && writes == 1 || phase == "commit" && writes == 2 { os.Exit(73) } } err := root.rebindRecoveryInventory(t.Context(), q) t.Fatal("crash not reached", phase, err) } for _, phase := range []string{"before intent", "intent", "database", "commit"} { t.Run(phase, func(t *testing.T) { f, original, disk, q := plan14RecoveryRebind(t) dir := t.TempDir() for _, name := range []string{"root", "data", "opt", "units", "locks", "child"} { if err := os.Mkdir(filepath.Join(dir, name), 0700); err != nil { t.Fatal(err) } } store := release.NewDirStore(filepath.Join(dir, "deployment")) keys, err := f.claims.List(t.Context(), "") if err != nil { t.Fatal(err) } for _, key := range keys { body, _, e := f.claims.Get(t.Context(), key) if e != nil { t.Fatal(e) } if _, e = store.Put(t.Context(), key, body, ""); e != nil { t.Fatal(e) } } raw, err := disk.Read(q.Next.Host + ".json") if err != nil { t.Fatal(err) } if err = os.WriteFile(filepath.Join(dir, "root", q.Next.Host+".json"), raw, 0600); err != nil { t.Fatal(err) } raw, _ = json.Marshal(q) if err = os.WriteFile(filepath.Join(dir, "request.json"), raw, 0600); err != nil { t.Fatal(err) } root, _, _ := plan14RebindProcessRoot(t, dir) source, _, err := original.Reconcile.Inventory.Snapshot(t.Context(), q.Next.Host, q.Prior) if err != nil { t.Fatal(err) } if err = root.Reconcile.Inventory.WriteStopInventory(t.Context(), source); err != nil { t.Fatal(err) } cmd := exec.CommandContext(t.Context(), os.Args[0], "-test.run=^TestPlan14RecoveryRebindProcessCrash$") cmd.Dir = filepath.Join(dir, "child") cmd.Env = append(os.Environ(), "PLAN14_REBIND_CRASH="+phase, "PLAN14_REBIND_DIR="+dir) output, err := cmd.CombinedOutput() exit, ok := err.(*exec.ExitError) if !ok || exit.ExitCode() != 73 { t.Fatalf("crash %v: %s", err, output) } resumed, _, _ := plan14RebindProcessRoot(t, dir) if err = resumed.recoverRootInventory(t.Context()); err != nil { t.Fatal("startup recovery", err) } if err = resumed.rebindRecoveryInventory(t.Context(), q); err != nil { t.Fatal("replay", err) } view, err := resumed.Stop.Coordinator.Guard.Root.View(t.Context(), q.Next.Host, hostguard.Captured{Incarnation: q.Next.Incarnation, Stamp: q.Next.Stamp}) if err != nil || view.Installation.Stamp != q.Next.Stamp || len(view.Retired) != 1 || view.Retired[0] != q.Prior { t.Fatal(view, err) } _, rev, err := resumed.Reconcile.Inventory.Snapshot(t.Context(), q.Next.Host, view.Captured) if err != nil || rev != 1 { t.Fatal(rev, err) } }) } }