| 1 | //go:build unix |
| 2 | |
| 3 | package hostguard |
| 4 | |
| 5 | import ( |
| 6 | "context" |
| 7 | "encoding/json" |
| 8 | "errors" |
| 9 | "os" |
| 10 | "os/exec" |
| 11 | "path/filepath" |
| 12 | "testing" |
| 13 | |
| 14 | "github.com/carlosframework/platform/internal/hostclaim" |
| 15 | ) |
| 16 | |
| 17 | type artifactsCrashStore struct { |
| 18 | *rootTestStore |
| 19 | phase string |
| 20 | } |
| 21 | |
| 22 | func (s *artifactsCrashStore) Write(name string, raw []byte) error { |
| 23 | if err := s.rootTestStore.Write(name, raw); err != nil { |
| 24 | return err |
| 25 | } |
| 26 | var r rootRecord |
| 27 | if json.Unmarshal(raw, &r) != nil { |
| 28 | return ErrMalformedRecord |
| 29 | } |
| 30 | if s.phase == "intent" && r.PendingInstallation != nil { |
| 31 | os.Exit(77) |
| 32 | } |
| 33 | if s.phase == "sync" && r.PendingInstallation == nil { |
| 34 | return os.ErrPermission |
| 35 | } |
| 36 | return nil |
| 37 | } |
| 38 | |
| 39 | func TestPlan14InventoryArtifactsCrash(t *testing.T) { |
| 40 | if dir := os.Getenv("PLAN14_E37_ARTIFACT_CHILD"); dir != "" { |
| 41 | f, target, _ := fenceFixture(t) |
| 42 | f.obs.Claim.Holder.Account, f.obs.Claim.Holder.App = "bab", "app" |
| 43 | store := &artifactsCrashStore{rootTestStore: &rootTestStore{dir: dir}, phase: os.Getenv("PLAN14_E37_PHASE")} |
| 44 | f.g.Root = NewRootAuthority(store) |
| 45 | r, err := f.g.Root.read(target.Host) |
| 46 | if err != nil { |
| 47 | t.Fatal(err) |
| 48 | } |
| 49 | in := r.installation |
| 50 | in.InventoryRevision++ |
| 51 | err = f.g.CommitRootInventoryArtifacts(t.Context(), in, []byte("request"), allow, nil, func(RootInstallation) error { |
| 52 | if err := os.WriteFile(filepath.Join(dir, "effect.lock"), []byte("committed"), 0600); err != nil { |
| 53 | return err |
| 54 | } |
| 55 | if store.phase == "commit" { |
| 56 | os.Exit(77) |
| 57 | } |
| 58 | return nil |
| 59 | }, func(prior RootInstallation) (InstallationArtifacts, error) { |
| 60 | a := prior.Artifacts |
| 61 | a.Timers = "backup" |
| 62 | return a, nil |
| 63 | }) |
| 64 | if store.phase == "sync" && errors.Is(err, os.ErrPermission) { |
| 65 | os.Exit(77) |
| 66 | } |
| 67 | t.Fatal("crash boundary not reached", err) |
| 68 | } |
| 69 | for _, phase := range []string{"intent", "commit", "sync"} { |
| 70 | t.Run(phase, func(t *testing.T) { |
| 71 | f, target, captured := fenceFixture(t) |
| 72 | dir := t.TempDir() |
| 73 | store := &rootTestStore{dir: dir} |
| 74 | f.g.Root = NewRootAuthority(store) |
| 75 | f.obs.Claim.Holder.Account, f.obs.Claim.Holder.App = "bab", "app" |
| 76 | 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} |
| 77 | if err := f.g.EstablishRoot(t.Context(), old, allow); err != nil { |
| 78 | t.Fatal(err) |
| 79 | } |
| 80 | binary, err := os.Executable() |
| 81 | if err != nil { |
| 82 | t.Fatal(err) |
| 83 | } |
| 84 | cmd := exec.CommandContext(t.Context(), binary, "-test.run=^TestPlan14InventoryArtifactsCrash$", "-test.count=1") |
| 85 | cmd.Dir = dir |
| 86 | cmd.Env = append(os.Environ(), "PLAN14_E37_ARTIFACT_CHILD="+dir, "PLAN14_E37_PHASE="+phase) |
| 87 | output, err := cmd.CombinedOutput() |
| 88 | var exit *exec.ExitError |
| 89 | if !errors.As(err, &exit) || exit.ExitCode() != 77 { |
| 90 | t.Fatalf("child: %v %s", err, output) |
| 91 | } |
| 92 | f.g.Root = NewRootAuthority(store) |
| 93 | r, err := f.g.Root.read(target.Host) |
| 94 | if err != nil { |
| 95 | t.Fatal(err) |
| 96 | } |
| 97 | want := InstallationArtifacts{Schema: 1, StateDirectory: true, Timers: "backup"} |
| 98 | if phase != "sync" { |
| 99 | if r.pendingInstallation == nil || r.pendingInstallation.Artifacts != want { |
| 100 | t.Fatal("intent lost requirements", r, err) |
| 101 | } |
| 102 | if _, err := f.g.Root.GenerationInstallations(t.Context()); err == nil { |
| 103 | t.Fatal("pending inventory certified") |
| 104 | } |
| 105 | } else { |
| 106 | if r.installation.Artifacts != want { |
| 107 | t.Fatal("visible commit lost requirements") |
| 108 | } |
| 109 | failing := NewRootAuthority(&artifactsCrashStore{rootTestStore: store, phase: "sync"}) |
| 110 | for i := 0; i < 2; i++ { |
| 111 | if _, err := failing.GenerationInstallations(t.Context()); !errors.Is(err, os.ErrPermission) { |
| 112 | t.Fatal("unsynced requirements certified", err) |
| 113 | } |
| 114 | } |
| 115 | } |
| 116 | next := old |
| 117 | next.InventoryRevision++ |
| 118 | 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 { |
| 119 | t.Fatal("resume", err) |
| 120 | } |
| 121 | views, err := f.g.Root.GenerationInstallations(t.Context()) |
| 122 | if err != nil || len(views) != 1 || views[0].Installation.Artifacts != want { |
| 123 | t.Fatal("reopen lost requirements", views, err) |
| 124 | } |
| 125 | f.obs.Claim.Holder.Stamp++ |
| 126 | f.obs.Claim.Holder.Session++ |
| 127 | next.Stamp++ |
| 128 | next.InventoryRevision = 1 |
| 129 | next.Artifacts = InstallationArtifacts{} |
| 130 | if err := f.g.RebindRootInventory(t.Context(), captured, 2, next, []byte("rebind"), func(*hostclaim.Claim) error { return nil }, func() error { return nil }); err != nil { |
| 131 | t.Fatal(err) |
| 132 | } |
| 133 | views, err = f.g.Root.GenerationInstallations(context.Background()) |
| 134 | if err != nil || len(views) != 1 || views[0].Installation.Artifacts != want || len(views[0].Retired) != 1 { |
| 135 | t.Fatal("rebind erased requirements", views, err) |
| 136 | } |
| 137 | }) |
| 138 | } |
| 139 | } |
| 140 | |