carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1//go:build linux
2
3package host
4
5import (
6 "context"
7 "database/sql"
8 "errors"
9 "os"
10 "path/filepath"
11 "strings"
12 "testing"
13
14 "github.com/carlosframework/platform/internal/cgroup"
15 "github.com/carlosframework/platform/internal/hostbox"
16 "github.com/carlosframework/platform/internal/hostguard"
17 "github.com/carlosframework/platform/internal/instancectl"
18)
19
20type changingLaunchHosts struct {
21 records []hostbox.LaunchInventory
22 next []hostbox.LaunchInventory
23 calls int
24}
25
26func (j *changingLaunchHosts) HostLaunches(context.Context, string) ([]hostbox.LaunchInventory, error) {
27 j.calls++
28 if j.calls > 1 && j.next != nil {
29 return j.next, nil
30 }
31 return j.records, nil
32}
33
34func TestPlan14UnjournaledStopAbsence(t *testing.T) {
35 for _, mode := range []string{"absent", "predecessor", "journal", "changed-journal", "descriptor", "containment", "isolated", "inventory"} {
36 t.Run(mode, func(t *testing.T) {
37 f := newWritersKernelFixture(t)
38 f.dropAgentHolds(t)
39 f.target.FenceID = "fence"
40 j := &changingLaunchHosts{}
41 switch mode {
42 case "predecessor":
43 target := f.target
44 target.Stamp--
45 j.records = []hostbox.LaunchInventory{{Target: target}}
46 case "journal":
47 j.records = []hostbox.LaunchInventory{{Target: f.target}}
48 case "changed-journal":
49 j.next = []hostbox.LaunchInventory{{Target: f.target}}
50 case "descriptor":
51 f.holdDB(t, 30, 3)
52 case "containment", "isolated":
53 dir := filepath.Join(f.agentCgroup, cgroup.ExecName(claimContainmentName(f.target)))
54 if mode == "isolated" {
55 // Use the same canonical host slice as the production observer.
56 dir = filepath.Join(f.cgroupRoot, filepath.Dir(instancectl.ClaimUnitControlGroup(f.target.Host)))
57 }
58 if err := os.MkdirAll(dir, 0700); err != nil {
59 t.Fatal(err)
60 }
61 if err := os.WriteFile(filepath.Join(dir, "cgroup.events"), []byte("populated 1\n"), 0600); err != nil {
62 t.Fatal(err)
63 }
64 case "inventory":
65 f.inv.Entries[0].Inode++
66 }
67 _, err := verifyNoLaunchAt(t.Context(), f.roots(), j, f.target, f.inv)
68 wantPass := mode == "absent" || mode == "predecessor"
69 if (err == nil) != wantPass {
70 t.Fatalf("%s: %v", mode, err)
71 }
72 })
73 }
74}
75
76type unjournaledKernel struct {
77 stopKernelFunc
78 calls int
79 evidence StopKernelEvidence
80}
81
82func (k *unjournaledKernel) VerifyUnjournaled(context.Context, hostguard.FenceTarget, StopInventory) (StopKernelEvidence, error) {
83 k.calls++
84 return k.evidence, nil
85}
86func TestPlan14UnjournaledStopRequiresRootFence(t *testing.T) {
87 c, _, _ := claimStopFixture(t)
88 k := &unjournaledKernel{}
89 c.Kernel = k
90 for _, cause := range []error{sql.ErrNoRows, os.ErrPermission, hostbox.ErrTypedInventory} {
91 if _, err := c.verifyUnjournaled(t.Context(), hostguard.FenceTarget{}, StopInventory{}, cause); !errors.Is(err, cause) || k.calls != 0 {
92 t.Fatal("legacy fallback", err)
93 }
94 }
95 c.Guard.Root = hostguard.NewRootAuthority(&rollbackMemory{files: map[string][]byte{}})
96 if _, err := c.verifyUnjournaled(t.Context(), hostguard.FenceTarget{Host: "shop.example", Incarnation: "inc-1", Stamp: 11}, StopInventory{}, sql.ErrNoRows); err == nil || k.calls != 0 {
97 t.Fatal("missing root fence accepted", err)
98 }
99}
100
101func (k *unjournaledKernel) RecoverStopped(context.Context, hostguard.FenceTarget, StopInventory) (StopKernelEvidence, DrainAcknowledgement, error) {
102 return StopKernelEvidence{}, DrainAcknowledgement{}, sql.ErrNoRows
103}
104
105func TestPlan14UnjournaledStopRecoveryReceipt(t *testing.T) {
106 c, req, _ := claimStopFixture(t)
107 c.Guard.Root = hostguard.NewRootAuthority(&rollbackMemory{files: map[string][]byte{}})
108 in := hostguard.RootInstallation{Host: req.Host, Incarnation: req.Incarnation, Stamp: req.InstallationStamp, Account: "bab", App: "app", Record: "record", Box: req.Box, Enrollment: req.Enrollment, Migration: 1}
109 if err := c.Guard.Root.RetainMigrationDeny(t.Context(), in, req.WriterSession); err != nil {
110 t.Fatal(err)
111 }
112 k := &unjournaledKernel{stopKernelFunc: func(context.Context, hostguard.FenceTarget, StopInventory, DrainAcknowledgement) (StopKernelEvidence, error) {
113 return StopKernelEvidence{}, sql.ErrNoRows
114 }}
115 c.Kernel = k
116 c.Inventory = exactEvidenceInventory{c.Inventory}
117 c.Drainer = stopDrainFunc(func(context.Context, StopRequest, string) (DrainAcknowledgement, error) {
118 t.Fatal("unjournaled stop tried to close nonexistent writers")
119 return DrainAcknowledgement{}, sql.ErrNoRows
120 })
121 // Fail the evidence check after exercising the fallback, leaving the durable fence.
122 if _, err := c.FenceAndStop(t.Context(), req); !errors.Is(err, ErrClaimStopReceipt) || k.calls != 1 {
123 t.Fatal("initial fallback", err, k.calls)
124 }
125 targets, err := c.Guard.PendingStopFences(t.Context())
126 if err != nil || len(targets) != 1 {
127 t.Fatal("pending fence", err, targets)
128 }
129 c.NewID = func() string { return "recovered" }
130 k.evidence = StopKernelEvidence{ObservedBootID: "boot-2", InventorySHA256: strings.Repeat("b", 64), LocalJournalSHA256: strings.Repeat("c", 64), CgroupIdentities: []string{"absent"}}
131 if _, err = c.RecoverStopped(t.Context(), targets[0]); err != nil || k.calls != 2 {
132 t.Fatal("recovery fallback", err, k.calls)
133 }
134 if pending, err := c.Guard.PendingStopFences(t.Context()); err != nil || len(pending) != 0 {
135 t.Fatal("recovery left a pending fence", err, pending)
136 }
137 if _, err := c.Guard.FenceReceiptFor(t.Context(), targets[0]); err != nil {
138 t.Fatal("recovery receipt", err)
139 }
140}
141
142func TestPlan14StoppedAgentContainment(t *testing.T) {
143 for _, tc := range []struct{ name, raw, want string }{
144 {"running", "Id=carlos-edge.service\nControlGroup=/carlos.slice/carlos-edge.service\nSlice=carlos.slice\nActiveState=active", "/sys/fs/cgroup/carlos.slice/carlos-edge.service"},
145 {"stopped", "Id=carlos-edge.service\nControlGroup=\nSlice=carlos.slice\nActiveState=inactive", "/sys/fs/cgroup/carlos.slice/carlos-edge.service"},
146 {"failed", "Id=carlos-edge.service\nControlGroup=\nSlice=carlos.slice\nActiveState=failed", "/sys/fs/cgroup/carlos.slice/carlos-edge.service"},
147 {"nested slice", "Id=carlos-edge.service\nControlGroup=\nSlice=tenant-carlos.slice\nActiveState=inactive", "/sys/fs/cgroup/tenant.slice/tenant-carlos.slice/carlos-edge.service"},
148 {"active without group", "Id=carlos-edge.service\nControlGroup=\nSlice=carlos.slice\nActiveState=active", ""},
149 {"wrong unit", "Id=other.service\nControlGroup=\nSlice=carlos.slice\nActiveState=inactive", ""},
150 {"missing slice", "Id=carlos-edge.service\nControlGroup=\nActiveState=inactive", ""},
151 {"traversal", "Id=carlos-edge.service\nControlGroup=\nSlice=../carlos.slice\nActiveState=inactive", ""},
152 {"duplicate", "Id=carlos-edge.service\nId=carlos-edge.service\nControlGroup=\nSlice=carlos.slice\nActiveState=inactive", ""},
153 } {
154 t.Run(tc.name, func(t *testing.T) {
155 got, err := rootStopAgentGroup([]byte(tc.raw))
156 if (err == nil) != (tc.want != "") || got != tc.want {
157 t.Fatal(got, err)
158 }
159 })
160 }
161}
162
163func TestPlan14NeverIntendedStopProof(t *testing.T) {
164 target := hostguard.FenceTarget{Host: "shop.example", Incarnation: "inc", Stamp: 1}
165 for _, mode := range []string{"absent", "tombstone", "intended", "started", "legacy", "open-writers", "other-record"} {
166 t.Run(mode, func(t *testing.T) {
167 in := hostbox.LaunchInventory{Target: target, WritersClosed: true, Groups: []hostbox.LaunchGroup{{RelativePath: "agent/parent"}}}
168 r := hostbox.LaunchReservation{NeverIntended: true, State: hostbox.UnitSettled, Group: hostbox.LaunchGroup{RelativePath: "host.service"}}
169 if mode != "absent" {
170 in.Reservations = []hostbox.LaunchReservation{r}
171 }
172 switch mode {
173 case "intended":
174 in.Reservations[0].NeverIntended = false
175 case "started":
176 in.Reservations[0].PID = 1
177 case "legacy":
178 in.Reservations[0].Group.RelativePath = "role-child"
179 case "open-writers":
180 in.WritersClosed = false
181 }
182 records := []hostbox.LaunchInventory{in}
183 if mode == "other-record" {
184 other := in
185 other.WritersClosed = false
186 records = append(records, other)
187 }
188 if got := stopNeverIntended(records, target); got != (mode == "absent" || mode == "tombstone") {
189 t.Fatal("unlaunched proof", mode, got)
190 }
191 })
192 }
193}
194