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 "errors"
8 "net"
9 "net/http"
10 "os"
11 "os/exec"
12 "path/filepath"
13 "strings"
14 "testing"
15
16 "github.com/carlosframework/platform/internal/activator"
17 "github.com/carlosframework/platform/internal/cgroup"
18 "github.com/carlosframework/platform/internal/hostbox"
19 "github.com/carlosframework/platform/internal/hostclaim"
20 "github.com/carlosframework/platform/internal/hostguard"
21 "github.com/carlosframework/platform/internal/hostwrite"
22 "github.com/carlosframework/platform/internal/instancectl"
23 "github.com/carlosframework/platform/internal/provision"
24 "golang.org/x/sys/unix"
25)
26
27// serveClaimRoot runs the production instance-control mux in claim mode with
28// h as its claim handler, on a real unix socket.
29func serveClaimRoot(t *testing.T, h http.Handler) string {
30 t.Helper()
31 return serveClaimRootWith(t, h, nil)
32}
33
34// serveClaimRootWithHeldRelease serves h as the claim root with the moves
35// held-release route registered to it too, as the host registers it with
36// both flags on (whole-branch fixes, item 6).
37func serveClaimRootWithHeldRelease(t *testing.T, h http.Handler) string {
38 t.Helper()
39 return serveClaimRootWith(t, h, h)
40}
41
42func serveClaimRootWith(t *testing.T, h, heldRelease http.Handler) string {
43 t.Helper()
44 dir, err := os.MkdirTemp("", "cl")
45 if err != nil {
46 t.Fatal(err)
47 }
48 t.Cleanup(func() { os.RemoveAll(dir) })
49 sock := filepath.Join(dir, "c.sock")
50 ln, err := net.Listen("unix", sock)
51 if err != nil {
52 t.Fatal(err)
53 }
54 ctx, cancel := context.WithCancel(context.Background())
55 server := &instancectl.Server{ClaimStop: h, ClaimHeldRelease: heldRelease, PeerOK: func(net.Conn) error { return nil }}
56 done := make(chan struct{})
57 go func() { defer close(done); _ = server.Serve(ctx, ln) }()
58 t.Cleanup(func() { cancel(); <-done })
59 return sock
60}
61
62func procSelfStart(t *testing.T) string {
63 t.Helper()
64 raw, err := os.ReadFile("/proc/self/stat")
65 if err != nil {
66 t.Fatal(err)
67 }
68 fields := strings.Fields(string(raw)[strings.LastIndexByte(string(raw), ')')+1:])
69 return fields[19]
70}
71
72func TestClaimRoleRootClientThroughTheClaimMux(t *testing.T) {
73 f := newLaunchFixture(t, servingOp())
74 authenticated := 0
75 f.root.Authenticate = func(_ context.Context, conn net.Conn) (StopPeer, error) {
76 if conn == nil {
77 t.Fatal("no request connection")
78 }
79 authenticated++
80 return f.peer, nil
81 }
82 sock := serveClaimRoot(t, ClaimRootWithLaunch{Launch: f.root})
83 client := NewClaimRoleRootClient(sock)
84 protocol, err := client.Capability(t.Context())
85 if err != nil || protocol != hostbox.RoleLaunchProtocol {
86 t.Fatalf("capability=%q %v", protocol, err)
87 }
88 grant, err := client.Launch(t.Context(), f.req)
89 if err != nil || grant.Check(*f.req.Reservation) != nil {
90 t.Fatalf("grant=%+v %v", grant, err)
91 }
92 if _, err := client.Launch(t.Context(), f.req); !errors.Is(err, hostbox.ErrChanged) {
93 t.Fatalf("replay over the wire: %v", err)
94 }
95 if err := client.Stop(t.Context(), hostbox.RoleLaunchRequest{V: hostbox.RoleLaunchV, Op: hostbox.RoleLaunchStop, Reservation: f.req.Reservation}); err != nil {
96 t.Fatal(err)
97 }
98 if authenticated != 4 {
99 t.Fatalf("authenticated %d of 4 requests", authenticated)
100 }
101 // An unauthenticated peer gets nothing, not even the capability.
102 f.root.Authenticate = func(context.Context, net.Conn) (StopPeer, error) { return StopPeer{}, errors.New("not the edge") }
103 if _, err := client.Capability(t.Context()); err == nil {
104 t.Fatal("unauthenticated capability")
105 }
106 // A body the protocol cannot carry is refused before authority checks.
107 f.root.Authenticate = func(context.Context, net.Conn) (StopPeer, error) { return f.peer, nil }
108 resp, err := (&http.Client{Transport: &http.Transport{DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
109 return (&net.Dialer{}).DialContext(ctx, "unix", sock)
110 }}}).Post("http://control/claim/launch", "application/json", strings.NewReader(`{"v":1,"op":"start","argv":["/bin/sh"]}`))
111 if err != nil {
112 t.Fatal(err)
113 }
114 resp.Body.Close()
115 if resp.StatusCode != http.StatusBadRequest {
116 t.Fatalf("unknown field status=%d", resp.StatusCode)
117 }
118}
119
120func TestClaimRoleLaunchEndToEndThroughTheRootProtocol(t *testing.T) {
121 f := newLaunchFixture(t, servingOp())
122 f.root.Authenticate = func(context.Context, net.Conn) (StopPeer, error) { return f.peer, nil }
123 base, err := os.MkdirTemp("", "e2e")
124 if err != nil {
125 t.Fatal(err)
126 }
127 t.Cleanup(func() { os.RemoveAll(base) })
128 exe := filepath.Join(base, "app")
129 if err := os.WriteFile(exe, []byte("build"), 0o555); err != nil {
130 t.Fatal(err)
131 }
132 var st unix.Stat_t
133 if err := unix.Stat(exe, &st); err != nil {
134 t.Fatal(err)
135 }
136 f.kernel.path, f.kernel.want = exe, hostbox.LaunchFile{Device: uint64(st.Dev), Inode: st.Ino}
137 f.kernel.observed.Executable = f.kernel.want
138 f.kernel.observed.Running = f.kernel.want
139 f.kernel.observed.ProcessStart = procSelfStart(t)
140 f.units.started.MainPID = os.Getpid()
141 sock := serveClaimRoot(t, ClaimRootWithLaunch{Launch: f.root})
142 work, err := (hostwrite.Config{}).Select(t.Context(), f.target.Host, "spawn")
143 if err != nil {
144 t.Fatal(err)
145 }
146 f.root.DataRoot, f.root.runRoot = base, filepath.Join(base, "run")
147 launchDir, runDir := provision.InstanceDir(base, f.target.Host), filepath.Join(f.root.runRoot, provision.InstanceID(f.target.Host))
148 if err := os.MkdirAll(launchDir, 0o700); err != nil {
149 t.Fatal(err)
150 }
151 launch := activator.RoleLaunch{Work: work, Target: f.target, Root: NewClaimRoleRootClient(sock), LaunchDir: launchDir, RunDir: runDir, LogSocket: filepath.Join(base, "l.sock"), MemoryMax: cgroup.TierDefault.MemoryMax, Executable: func(context.Context, *exec.Cmd) (*os.File, error) { return os.Open(exe) }}
152 attempt := hostclaim.WakeRetryAttempt{ID: "attempt-1", InvocationID: "inv-1", LaunchInputs: "inputs-1", Session: 13, Box: "box-1", Enrollment: "enrollment-1", BootID: f.peer.BootID, ControllerID: "controller-1"}
153 build := func() *exec.Cmd {
154 cmd := exec.Command("/opt/carlos/live/shop.example", "--socket", filepath.Join(runDir, "app.sock"), "--db", filepath.Join(launchDir, "app.db"))
155 cmd.Env = []string{"CARLOS_ADMIN_TOKEN=test"}
156 return cmd
157 }
158 launch.Spec = &hostbox.LaunchSpec{Identity: f.req.Spec.Identity, Executable: exe, Digest: f.kernel.digest, Args: build().Args[1:], Env: build().Env}
159 attempt.LaunchInputs, _ = launch.Spec.Hash()
160 f.claims.set(func(c *hostclaim.Claim) { a := attempt; c.Holder.WakeRetry.Attempt = &a })
161 cmd, _, settle, _, err := launch.Start(t.Context(), "main", attempt, build, func() error { return nil })
162 if err != nil {
163 t.Fatal(err)
164 }
165 if cmd.Process.Pid != os.Getpid() {
166 t.Fatalf("pid=%d", cmd.Process.Pid)
167 }
168 got, err := f.journal.UnitReservation(t.Context(), f.target, "attempt-1:main")
169 if err != nil || got.State != hostbox.UnitStarted || got.InvocationID != "inv-1" || got.Inputs != attempt.LaunchInputs {
170 t.Fatalf("root journal=%+v %v", got, err)
171 }
172 if err := settle(t.Context()); err != nil {
173 t.Fatal(err)
174 }
175 if got, err = f.journal.UnitReservation(t.Context(), f.target, "attempt-1:main"); err != nil || got.State != hostbox.UnitSettled {
176 t.Fatalf("settled=%+v %v", got, err)
177 }
178 // The same attempt cannot launch twice, even from the live controller.
179 if _, _, _, _, err := launch.Start(t.Context(), "main", attempt, build, func() error { return nil }); err == nil {
180 t.Fatal("spent attempt relaunched")
181 }
182}
183
184func TestClaimRolePreflightAdmitsOnlyTheCompleteProtocol(t *testing.T) {
185 var nilAssembly *ClaimRoleAssembly
186 for name, a := range map[string]*ClaimRoleAssembly{
187 "nil assembly": nilAssembly,
188 "no protocol": {},
189 "root refuses": {Root: fakeLaunchRoot{err: activator.ErrClaimIsolationUnavailable}},
190 "older protocol": {Root: fakeLaunchRoot{protocol: "isolated-unit-v0"}},
191 "unreachable": {Root: fakeLaunchRoot{err: errors.New("dial unix: no such file")}},
192 } {
193 if err := a.Preflight(t.Context(), ClaimLaunchInputs{}); !errors.Is(err, activator.ErrClaimIsolationUnavailable) {
194 t.Fatalf("%s: %v", name, err)
195 }
196 }
197 if err := (&ClaimRoleAssembly{Root: fakeLaunchRoot{protocol: hostbox.RoleLaunchProtocol}}).Preflight(t.Context(), ClaimLaunchInputs{}); err != nil {
198 t.Fatal(err)
199 }
200}
201
202func TestClaimRoleIsolatedPathsBindTheNestedInstance(t *testing.T) {
203 roots := map[string]string{"data": "/data"}
204 host := "shop.example"
205 got, err := claimIsolatedPaths(roots, host, ClaimLaunchInputs{DBPath: "/data/instances/" + iidOf(host) + "/shop.db", Limit: cgroup.TierDefault})
206 if err != nil {
207 t.Fatal(err)
208 }
209 if got.LaunchDir != "/data/instances/"+iidOf(host) || got.RunDir != "/run/carlos/instances/"+iidOf(host) || !strings.HasSuffix(got.LogSocket, ".run.sock") || got.MemoryMax != cgroup.TierDefault.MemoryMax {
210 t.Fatalf("paths=%+v", got)
211 }
212 for name, in := range map[string]ClaimLaunchInputs{
213 "flat database": {DBPath: "/data/shop.example.db", Limit: cgroup.TierDefault},
214 "other instance": {DBPath: "/data/instances/other/shop.db", Limit: cgroup.TierDefault},
215 "nested deeper": {DBPath: "/data/instances/" + iidOf(host) + "/x/shop.db", Limit: cgroup.TierDefault},
216 "not a tier": {DBPath: "/data/instances/" + iidOf(host) + "/shop.db", Limit: cgroup.Limit{MemoryMax: 7}},
217 "relative": {DBPath: "instances/shop.db", Limit: cgroup.TierDefault},
218 "unescaped parent": {DBPath: "/data/instances/" + iidOf(host) + "/../x/shop.db", Limit: cgroup.TierDefault},
219 } {
220 if _, err := claimIsolatedPaths(roots, host, in); !errors.Is(err, hostbox.ErrRoleLaunchShape) {
221 t.Fatalf("%s: %v", name, err)
222 }
223 }
224}
225
226func TestProductionClaimLaunchWrapsTheRootHandler(t *testing.T) {
227 journal, err := hostbox.OpenLaunchJournal(filepath.Join(t.TempDir(), "launch.db"))
228 if err != nil {
229 t.Fatal(err)
230 }
231 defer journal.Close()
232 inner := stopKernelFunc(func(context.Context, hostguard.FenceTarget, StopInventory, DrainAcknowledgement) (StopKernelEvidence, error) {
233 return StopKernelEvidence{}, nil
234 })
235 authenticate := func(context.Context, net.Conn) (StopPeer, error) { return StopPeer{}, nil }
236 root := ClaimRootHTTP{Launches: journal, Stop: ClaimStopHTTP{Authenticate: authenticate, Coordinator: ClaimStopCoordinator{Kernel: inner, Guard: hostguard.Guard{DataDir: "/data", Claims: &launchClaims{}}}}}
237 actor := func(context.Context) (hostbox.Actor, error) { return hostbox.Actor{}, nil }
238 wrapped, err := claimLaunchFor(root, actor, map[string]string{"opt": "/opt/carlos", "data": "/data"})
239 if err != nil {
240 t.Fatal(err)
241 }
242 k, ok := wrapped.Root.Stop.Coordinator.Kernel.(claimUnitStopKernel)
243 if !ok || k.Journal != journal || k.Inner == nil {
244 t.Fatalf("stop kernel not wrapped: %T", wrapped.Root.Stop.Coordinator.Kernel)
245 }
246 l := wrapped.Launch
247 units, ok := l.Units.(instancectl.ClaimUnits)
248 if !ok || units.Systemctl == nil || units.CgroupRoot != "/sys/fs/cgroup" || !strings.HasSuffix(units.ExeRoot, "/exe") || units.Room == nil {
249 t.Fatalf("units=%+v", l.Units)
250 }
251 if kernel, ok := l.Kernel.(linuxClaimLaunchKernel); !ok || kernel.OptRoot != "/opt/carlos" || kernel.CgroupRoot != "/sys/fs/cgroup" || kernel.ProcRoot != "/proc" || kernel.Owner != 0 {
252 t.Fatalf("kernel=%+v", l.Kernel)
253 }
254 if l.Journal != journal || l.Authenticate == nil || l.Actor == nil || l.Guard.DataDir != "/data" {
255 t.Fatalf("launch root=%+v", l)
256 }
257 for name, bad := range map[string]func(*ClaimRootHTTP, *map[string]string){
258 "no journal": func(r *ClaimRootHTTP, _ *map[string]string) { r.Launches = nil },
259 "no authenticate": func(r *ClaimRootHTTP, _ *map[string]string) { r.Stop.Authenticate = nil },
260 "no stop kernel": func(r *ClaimRootHTTP, _ *map[string]string) { r.Stop.Coordinator.Kernel = nil },
261 "unbound data root": func(_ *ClaimRootHTTP, m *map[string]string) { (*m)["data"] = "/elsewhere" },
262 "no opt root": func(_ *ClaimRootHTTP, m *map[string]string) { *m = map[string]string{"data": "/data"} },
263 } {
264 r := root
265 m := map[string]string{"opt": "/opt/carlos", "data": "/data"}
266 bad(&r, &m)
267 if _, err := claimLaunchFor(r, actor, m); err == nil {
268 t.Fatalf("%s accepted", name)
269 }
270 }
271 if _, err := WithProductionClaimLaunch(t.Context(), http.NotFoundHandler()); err == nil {
272 t.Fatal("wrapped a handler that is not the claim root")
273 }
274 if _, err := WithProductionClaimLaunch(t.Context(), root); err == nil {
275 t.Fatal("wrapped without the production assembly")
276 }
277}
278
279func iidOf(host string) string { return provision.InstanceID(host) }
280