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 "bytes"
7 "context"
8 "encoding/json"
9 "errors"
10 "os"
11 "path/filepath"
12 "strings"
13 "testing"
14 "time"
15
16 "github.com/carlosframework/platform/internal/cgroup"
17 "github.com/carlosframework/platform/internal/hostbox"
18 "github.com/carlosframework/platform/internal/hostclaim"
19 execlease "github.com/carlosframework/platform/internal/hostexec"
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 "github.com/carlosframework/platform/internal/registry"
25 "golang.org/x/sys/unix"
26)
27
28// Fix round 1, I1: root reads the agent-written journal the way it opens
29// every agent-writable path: a FIFO at the path answers at once rather
30// than holding root under the host guard, a symlink is refused, a file
31// past the cap is refused before it is decoded, and a cancelled context
32// reads nothing.
33func TestRootReaderOpensTheJournalBeneathTheDataRoot(t *testing.T) {
34 ctx := context.Background()
35 claim := &hostclaim.Claim{Host: "shop.example", Holder: &hostclaim.Holder{Incarnation: "inc", Stamp: 9, Session: 7, Generation: "P1", WriterBox: "dst"},
36 Op: hostclaim.Op{Kind: hostclaim.KindRelocating, ID: "op1", Phase: hostclaim.PhaseRelocatingProbing, Exec: "dst", Journal: map[string]string{"install_stamp": "9", "prepared": "P1", "probe_attempts.dst": "1"}}}
37 o := hostclaim.Observed{Host: "shop.example", Claim: claim}
38 attempt := hostclaim.WakeRetryAttempt{ID: "a", InvocationID: "b", Session: 7, Box: "dst"}
39 good := func(t *testing.T, dd string) relocationAttemptJournal {
40 t.Helper()
41 j, _ := readRelocationAttempts(dd, "shop.example", "op1")
42 j.Sessions["7"] = 1
43 j.Starts["1"] = relocationStart{Session: 7, Generation: "P1", Stamp: 9, State: probeReserved, Attempt: attempt}
44 return j
45 }
46 t.Run("a FIFO", func(t *testing.T) {
47 dd := t.TempDir()
48 path, _ := relocationAttemptsPath(dd, "op1")
49 if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
50 t.Fatal(err)
51 }
52 if err := unix.Mkfifo(path, 0o600); err != nil {
53 t.Fatal(err)
54 }
55 done := make(chan error, 1)
56 go func() {
57 _, err := (relocationAttemptReader{DataDir: dd}).ReadAttempt(ctx, o)
58 done <- err
59 }()
60 select {
61 case err := <-done:
62 if !errors.Is(err, hostbox.ErrChanged) {
63 t.Fatalf("a FIFO: %v", err)
64 }
65 case <-time.After(5 * time.Second):
66 t.Fatal("root blocked on a FIFO at the journal's path")
67 }
68 })
69 t.Run("a symlink", func(t *testing.T) {
70 dd := t.TempDir()
71 elsewhere := t.TempDir()
72 if err := good(t, elsewhere).write(elsewhere); err != nil {
73 t.Fatal(err)
74 }
75 real, _ := relocationAttemptsPath(elsewhere, "op1")
76 path, _ := relocationAttemptsPath(dd, "op1")
77 if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
78 t.Fatal(err)
79 }
80 if err := os.Symlink(real, path); err != nil {
81 t.Fatal(err)
82 }
83 if _, err := (relocationAttemptReader{DataDir: dd}).ReadAttempt(ctx, o); !errors.Is(err, hostbox.ErrChanged) {
84 t.Fatalf("a symlink to a sound journal was followed: %v", err)
85 }
86 // The same journal in place reads.
87 if err := os.Remove(path); err != nil {
88 t.Fatal(err)
89 }
90 if err := good(t, dd).write(dd); err != nil {
91 t.Fatal(err)
92 }
93 if got, err := (relocationAttemptReader{DataDir: dd}).ReadAttempt(ctx, o); err != nil || got != attempt {
94 t.Fatalf("the journal in place: %+v %v", got, err)
95 }
96 })
97 t.Run("a symlinked directory", func(t *testing.T) {
98 dd := t.TempDir()
99 elsewhere := t.TempDir()
100 if err := good(t, elsewhere).write(elsewhere); err != nil {
101 t.Fatal(err)
102 }
103 if err := os.MkdirAll(filepath.Join(dd, "relocate"), 0o700); err != nil {
104 t.Fatal(err)
105 }
106 if err := os.Symlink(filepath.Join(elsewhere, "relocate", "ops"), filepath.Join(dd, "relocate", "ops")); err != nil {
107 t.Fatal(err)
108 }
109 if _, err := (relocationAttemptReader{DataDir: dd}).ReadAttempt(ctx, o); !errors.Is(err, hostbox.ErrChanged) {
110 t.Fatalf("a symlinked ops directory was followed: %v", err)
111 }
112 })
113 t.Run("past the cap", func(t *testing.T) {
114 dd := t.TempDir()
115 j := good(t, dd)
116 raw, _ := json.Marshal(j)
117 raw = append(raw, bytes.Repeat([]byte(" "), relocationAttemptsLimit+1-len(raw))...)
118 path, _ := relocationAttemptsPath(dd, "op1")
119 if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
120 t.Fatal(err)
121 }
122 if err := os.WriteFile(path, raw, 0o600); err != nil {
123 t.Fatal(err)
124 }
125 if _, err := (relocationAttemptReader{DataDir: dd}).ReadAttempt(ctx, o); !errors.Is(err, hostbox.ErrChanged) {
126 t.Fatalf("a journal past the cap: %v", err)
127 }
128 // Trailing whitespace within the cap decodes.
129 if err := os.WriteFile(path, raw[:relocationAttemptsLimit], 0o600); err != nil {
130 t.Fatal(err)
131 }
132 if got, err := (relocationAttemptReader{DataDir: dd}).ReadAttempt(ctx, o); err != nil || got != attempt {
133 t.Fatalf("a journal at the cap: %+v %v", got, err)
134 }
135 })
136 t.Run("a cancelled context", func(t *testing.T) {
137 dd := t.TempDir()
138 if err := good(t, dd).write(dd); err != nil {
139 t.Fatal(err)
140 }
141 gone, cancel := context.WithCancel(ctx)
142 cancel()
143 if _, err := (relocationAttemptReader{DataDir: dd}).ReadAttempt(gone, o); !errors.Is(err, context.Canceled) {
144 t.Fatalf("a cancelled read: %v", err)
145 }
146 })
147}
148
149// Fix round 1, M4: root's authorize, with the relocation reader installed,
150// over a journal relocationProbeAttempts.Reserve actually wrote for P's
151// opening. The reservation the agent presents binds end to end: the
152// attempt id as the role reservation, the invocation id, the launch-input
153// digest, the peer's boot and controller. Another invocation, a journal
154// rewritten under the agent, and a journal replaced by a FIFO are refused.
155func TestRootAuthorizesAReservationTheRelocationJournalHolds(t *testing.T) {
156 ctx := t.Context()
157 dataDir := t.TempDir()
158 f := newROFixture(t, roOptions{})
159 f.inputs.Launch = func(context.Context, registry.Route) (ClaimServingLaunch, error) {
160 return ClaimServingLaunch{Identity: hostwrite.Identity{Config: "sha256:config", Release: "sha256:release"}, Executable: "/opt/carlos/versions/v1/app", ExecutableSHA256: strings.Repeat("b", 64), Version: "v1",
161 Args: []string{"--socket", filepath.Join(provision.InstanceRunDir(roHost), "app.sock"), "--db", provision.NestedDBPath(dataDir, roHost)}, Env: []string{"CARLOS_ADMIN_TOKEN=test"}}, nil
162 }
163 in, err := f.inputs.Select(ctx, f.route, f.read(), roProbe)
164 if err != nil {
165 t.Fatal(err)
166 }
167 admitted := f.admit(f.read(), in)
168 h := admitted.Claim.Holder
169 enrollment := strings.Repeat("a", 32)
170 boot, err := execlease.BootID()
171 if err != nil {
172 t.Fatal(err)
173 }
174 start, err := claimConstructionProcessStart()
175 if err != nil {
176 t.Fatal(err)
177 }
178 peer := StopPeer{UID: 1001, PID: os.Getpid(), ProcessStart: start, BootID: boot, ControllerID: "controller-1"}
179 target := hostguard.FenceTarget{Region: "ie", Host: roHost, Incarnation: h.Incarnation, Stamp: h.Stamp, Session: h.Session, Op: roOp, Phase: hostclaim.PhaseRelocatingProbing, Box: "dst", Enrollment: enrollment, BootID: peer.BootID, ControllerID: peer.ControllerID, ClaimETag: admitted.ETag, DirectoryRevision: "7"}
180
181 // The agent's side: the journal Reserve writes for the admitted session.
182 attempts := &relocationProbeAttempts{Claims: f.claims, DataDir: dataDir, Box: "dst", NewID: f.inputs.NewID, Now: f.inputs.Now}
183 s := OperationStart{Admitted: admitted, Policy: roProbe, Inputs: in, Owner: target}
184 if err := attempts.Reserve(ctx, s, 0); err != nil {
185 t.Fatal(err)
186 }
187 a := attempts.CurrentAttempt(s)
188 if a.ID == "" {
189 t.Fatal("no reservation")
190 }
191
192 // Root's side: its own providers, the reader over the same data root.
193 exe := hostbox.LaunchFile{Device: 5, Inode: 6}
194 kernel := &launchKernel{path: in.Executable, want: exe, digest: in.ExecutableSHA256}
195 root := &ClaimLaunchRoot{ValidateExecutable: func(string, string, string) error { return nil }, Local: &rev2Local{}, Clock: &rev2Clock{wall: time.Now(), boot: 100}, DataRoot: dataDir, ExecLockDir: t.TempDir(),
196 Attempts: map[string]claimLaunchAttemptReader{hostclaim.KindRelocating: relocationAttemptReader{DataDir: dataDir}},
197 Guard: hostguard.Guard{DataDir: dataDir, LockDir: t.TempDir(), Claims: hostguard.StoreReader{Store: f.claims}},
198 Actor: func(context.Context) (hostbox.Actor, error) {
199 return hostbox.Actor{Box: "dst", Enrollment: enrollment, Revision: 7, Binding: "7", Region: "ie", Eligible: true}, nil
200 },
201 Kernel: kernel}
202 configureLaunchParticipation(t, &launchFixture{root: root, peer: peer, target: target, claims: &launchClaims{obs: admitted}})
203 spec := &hostbox.LaunchSpec{Identity: in.Identity, Executable: in.Executable, Digest: in.ExecutableSHA256, Args: in.Args, Env: in.Env}
204 reservation := hostbox.LaunchReservation{ID: a.ID + ":main", Role: "main", InvocationID: a.InvocationID, Inputs: a.LaunchInputs, Target: target, Group: hostbox.LaunchGroup{RelativePath: instancectl.ClaimUnitControlGroup(roHost)}, Executable: exe}
205 q := hostbox.RoleLaunchRequest{V: hostbox.RoleLaunchV, Op: hostbox.RoleLaunchStart, Spec: spec, Reservation: &reservation, Executable: in.Executable, MemoryMax: cgroup.TierDefault.MemoryMax}
206 if err := root.authorize(ctx, peer, q, admitted.Claim); err != nil {
207 t.Fatalf("the reservation the journal holds: %v", err)
208 }
209 other := reservation
210 other.InvocationID = "another"
211 q.Reservation = &other
212 if err := root.authorize(ctx, peer, q, admitted.Claim); !errors.Is(err, hostbox.ErrChanged) {
213 t.Fatalf("another invocation: %v", err)
214 }
215 q.Reservation = &reservation
216 // The agent rewrites its journal: root's reservation no longer matches.
217 j, err := readRelocationAttempts(dataDir, roHost, roOp)
218 if err != nil {
219 t.Fatal(err)
220 }
221 st := j.Starts["1"]
222 st.Attempt.InvocationID = "rewritten"
223 j.Starts["1"] = st
224 if err := j.write(dataDir); err != nil {
225 t.Fatal(err)
226 }
227 if err := root.authorize(ctx, peer, q, admitted.Claim); !errors.Is(err, hostbox.ErrChanged) {
228 t.Fatalf("a rewritten journal: %v", err)
229 }
230 // A FIFO in the journal's place refuses at once.
231 path, _ := relocationAttemptsPath(dataDir, roOp)
232 if err := os.Remove(path); err != nil {
233 t.Fatal(err)
234 }
235 if err := unix.Mkfifo(path, 0o600); err != nil {
236 t.Fatal(err)
237 }
238 done := make(chan error, 1)
239 go func() { done <- root.authorize(ctx, peer, q, admitted.Claim) }()
240 select {
241 case err := <-done:
242 if !errors.Is(err, hostbox.ErrChanged) {
243 t.Fatalf("a FIFO: %v", err)
244 }
245 case <-time.After(5 * time.Second):
246 t.Fatal("root blocked on a FIFO in the journal's place")
247 }
248}
249