//go:build unix package hostguard import ( "context" "encoding/json" "errors" "os" "os/exec" "path/filepath" "testing" "github.com/carlosframework/platform/internal/hostclaim" ) type artifactsCrashStore struct { *rootTestStore phase string } func (s *artifactsCrashStore) Write(name string, raw []byte) error { if err := s.rootTestStore.Write(name, raw); err != nil { return err } var r rootRecord if json.Unmarshal(raw, &r) != nil { return ErrMalformedRecord } if s.phase == "intent" && r.PendingInstallation != nil { os.Exit(77) } if s.phase == "sync" && r.PendingInstallation == nil { return os.ErrPermission } return nil } func TestPlan14InventoryArtifactsCrash(t *testing.T) { if dir := os.Getenv("PLAN14_E37_ARTIFACT_CHILD"); dir != "" { f, target, _ := fenceFixture(t) f.obs.Claim.Holder.Account, f.obs.Claim.Holder.App = "bab", "app" store := &artifactsCrashStore{rootTestStore: &rootTestStore{dir: dir}, phase: os.Getenv("PLAN14_E37_PHASE")} f.g.Root = NewRootAuthority(store) r, err := f.g.Root.read(target.Host) if err != nil { t.Fatal(err) } in := r.installation in.InventoryRevision++ err = f.g.CommitRootInventoryArtifacts(t.Context(), in, []byte("request"), allow, nil, func(RootInstallation) error { if err := os.WriteFile(filepath.Join(dir, "effect.lock"), []byte("committed"), 0600); err != nil { return err } if store.phase == "commit" { os.Exit(77) } return nil }, func(prior RootInstallation) (InstallationArtifacts, error) { a := prior.Artifacts a.Timers = "backup" return a, nil }) if store.phase == "sync" && errors.Is(err, os.ErrPermission) { os.Exit(77) } t.Fatal("crash boundary not reached", err) } for _, phase := range []string{"intent", "commit", "sync"} { t.Run(phase, func(t *testing.T) { f, target, captured := fenceFixture(t) dir := t.TempDir() store := &rootTestStore{dir: dir} f.g.Root = NewRootAuthority(store) f.obs.Claim.Holder.Account, f.obs.Claim.Holder.App = "bab", "app" old := RootInstallation{Artifacts: InstallationArtifacts{Schema: 1, StateDirectory: true}, Host: target.Host, Incarnation: captured.Incarnation, Stamp: captured.Stamp, Account: "bab", App: "app", Record: "record", Box: target.Box, Enrollment: target.Enrollment, Migration: 1, InventoryRevision: 1} if err := f.g.EstablishRoot(t.Context(), old, allow); err != nil { t.Fatal(err) } binary, err := os.Executable() if err != nil { t.Fatal(err) } cmd := exec.CommandContext(t.Context(), binary, "-test.run=^TestPlan14InventoryArtifactsCrash$", "-test.count=1") cmd.Dir = dir cmd.Env = append(os.Environ(), "PLAN14_E37_ARTIFACT_CHILD="+dir, "PLAN14_E37_PHASE="+phase) output, err := cmd.CombinedOutput() var exit *exec.ExitError if !errors.As(err, &exit) || exit.ExitCode() != 77 { t.Fatalf("child: %v %s", err, output) } f.g.Root = NewRootAuthority(store) r, err := f.g.Root.read(target.Host) if err != nil { t.Fatal(err) } want := InstallationArtifacts{Schema: 1, StateDirectory: true, Timers: "backup"} if phase != "sync" { if r.pendingInstallation == nil || r.pendingInstallation.Artifacts != want { t.Fatal("intent lost requirements", r, err) } if _, err := f.g.Root.GenerationInstallations(t.Context()); err == nil { t.Fatal("pending inventory certified") } } else { if r.installation.Artifacts != want { t.Fatal("visible commit lost requirements") } failing := NewRootAuthority(&artifactsCrashStore{rootTestStore: store, phase: "sync"}) for i := 0; i < 2; i++ { if _, err := failing.GenerationInstallations(t.Context()); !errors.Is(err, os.ErrPermission) { t.Fatal("unsynced requirements certified", err) } } } next := old next.InventoryRevision++ if err := f.g.CommitRootInventoryArtifacts(t.Context(), next, []byte("request"), allow, nil, func(RootInstallation) error { return nil }, func(RootInstallation) (InstallationArtifacts, error) { return want, nil }); err != nil { t.Fatal("resume", err) } views, err := f.g.Root.GenerationInstallations(t.Context()) if err != nil || len(views) != 1 || views[0].Installation.Artifacts != want { t.Fatal("reopen lost requirements", views, err) } f.obs.Claim.Holder.Stamp++ f.obs.Claim.Holder.Session++ next.Stamp++ next.InventoryRevision = 1 next.Artifacts = InstallationArtifacts{} if err := f.g.RebindRootInventory(t.Context(), captured, 2, next, []byte("rebind"), func(*hostclaim.Claim) error { return nil }, func() error { return nil }); err != nil { t.Fatal(err) } views, err = f.g.Root.GenerationInstallations(context.Background()) if err != nil || len(views) != 1 || views[0].Installation.Artifacts != want || len(views[0].Retired) != 1 { t.Fatal("rebind erased requirements", views, err) } }) } }