carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "bytes"
5 "context"
6 "crypto/ed25519"
7 "crypto/sha256"
8 "encoding/hex"
9 "encoding/json"
10 "errors"
11 "os"
12 "path/filepath"
13 "reflect"
14 "strings"
15 "testing"
16
17 "github.com/carlosframework/platform/internal/hostfloor"
18 "github.com/carlosframework/platform/internal/release"
19)
20
21// channelFixture is a fresh box (applyFixture with no binary on it) whose
22// stable channel names v1.4.0, whose identity is published, and whose
23// deployment has a host floor.
24type channelFixture struct {
25 *applyFixture
26 in channelInstall
27 body []byte
28 fetches int
29 owned []string
30}
31
32func newChannelFixture(t *testing.T, floor string) *channelFixture {
33 t.Helper()
34 f := &channelFixture{applyFixture: newApplyFixture(t, "stable", "bootstrap")}
35 if err := os.Remove(systemBinaryPath(f.a.optDir)); err != nil {
36 t.Fatal(err)
37 }
38 f.body = []byte("#!/bin/sh\necho carlos v1.4.0\n")
39 f.publish(t, "v1.4.0", f.body)
40 if floor != "" {
41 if err := hostfloor.Publish(context.Background(), f.store, release.RootSigner(f.priv), "bucket", hostfloor.Floor{Min: 1, MinRelease: floor}); err != nil {
42 t.Fatal(err)
43 }
44 }
45 f.in = channelInstall{a: f.a, bucket: "bucket", fetchTo: func(ctx context.Context, s release.Store, key, dst, sha string) error {
46 f.fetches++
47 return release.FetchToFile(ctx, s, key, dst, systemArtifactLimit, sha)
48 }, own: func(path string) error {
49 f.owned = append(f.owned, path)
50 return nil
51 }}
52 return f
53}
54
55// publish promotes version on stable with body as its linux-arm64
56// artifact and publishes its release identity.
57func (f *channelFixture) publish(t *testing.T, version string, body []byte) {
58 t.Helper()
59 ctx := context.Background()
60 sum := sha256.Sum256(body)
61 rel := release.SystemRelease{Version: version, ReleaseTag: version, MovesProtocol: 1, Artifacts: map[string]release.SystemArtifact{
62 "linux-arm64": {Key: release.SystemArtifactKey(version, "linux-arm64"), SHA256: hex.EncodeToString(sum[:])}}}
63 if _, err := f.store.Put(ctx, rel.Artifacts["linux-arm64"].Key, body, ""); err != nil {
64 t.Fatal(err)
65 }
66 if _, err := release.PromoteSystem(ctx, f.store, release.RootSigner(f.priv), "router", "stable", rel, release.SystemPromoteOptions{}); err != nil {
67 t.Fatal(err)
68 }
69 wire, _, err := f.store.Get(ctx, release.SystemPointerKey("router", "stable"))
70 if err != nil {
71 t.Fatal(err)
72 }
73 if err := release.PublishSystemIdentity(ctx, f.store, f.pub, wire); err != nil {
74 t.Fatal(err)
75 }
76}
77
78func (f *channelFixture) slots(t *testing.T) (cur, prev []byte) {
79 t.Helper()
80 cur, _ = os.ReadFile(systemBinaryPath(f.a.optDir))
81 prev, _ = os.ReadFile(systemPrevPath(f.a.optDir))
82 return cur, prev
83}
84
85func TestInstallChannelPutsTheChannelReleaseInBothSlots(t *testing.T) {
86 f := newChannelFixture(t, "v1.2.0")
87 got, err := f.in.run(context.Background())
88 if err != nil {
89 t.Fatal(err)
90 }
91 if got.Version != "v1.4.0" || got.Tag != "v1.4.0" || got.Already {
92 t.Fatalf("installed %+v", got)
93 }
94 cur, prev := f.slots(t)
95 if !bytes.Equal(cur, f.body) || !bytes.Equal(prev, f.body) {
96 t.Fatalf("slots hold %q and %q, not the channel's artifact", cur, prev)
97 }
98 // The identity guard-arm will be given is the channel release's.
99 held, err := os.ReadFile(channelIdentityPath(f.a.optDir))
100 if err != nil {
101 t.Fatal(err)
102 }
103 p, err := release.VerifySystemIdentity(f.pub, held)
104 if err != nil || p.Current.Version != "v1.4.0" {
105 t.Fatalf("identity %v %v", p, err)
106 }
107 // An update later finds the binary on disk known good, and has seen
108 // this promote.
109 applied, err := readSystemApplied(f.a.optDir)
110 if err != nil || applied == nil || applied.Version != "v1.4.0" || applied.Channel != "stable" {
111 t.Fatalf("applied %+v %v", applied, err)
112 }
113 seen, err := readSystemSeen(f.a.optDir)
114 if err != nil || seen["stable"].IsZero() {
115 t.Fatalf("seen %v %v", seen, err)
116 }
117 // Nothing is left in staging or the agent's cache.
118 if _, err := os.Stat(f.a.stagingDir()); !os.IsNotExist(err) {
119 t.Fatalf("staging left: %v", err)
120 }
121 if _, err := os.Stat(filepath.Dir(systemCachePath(f.a.dataDir, "v1.4.0"))); !os.IsNotExist(err) {
122 t.Fatalf("cache left: %v", err)
123 }
124 // The candidate was asked its version from staging before it was put
125 // in place.
126 if !f.called("run carlos version") {
127 t.Fatalf("calls %v", f.calls)
128 }
129 // R2, S2: every directory on the cache's path is handed to the
130 // service user, and nothing else, so the agent's own update can
131 // download the next release beside them and ask for it.
132 if want := cachePathDirs(f.a.dataDir, "v1.4.0"); !reflect.DeepEqual(f.owned, want) {
133 t.Fatalf("handed to the service user: %v, want %v", f.owned, want)
134 }
135 // Run again: nothing to do, nothing fetched.
136 again, err := f.in.run(context.Background())
137 if err != nil || !again.Already || f.fetches != 1 {
138 t.Fatalf("second run %+v %v, %d fetches", again, err, f.fetches)
139 }
140 if line := installedLine(again); line != "v1.4.0 (v1.4.0) from the stable channel is already installed" {
141 t.Fatalf("line %q", line)
142 }
143 // The agent, its next release promoted, downloads it and asks for it
144 // in the directories install-channel left.
145 leg := &systemLeg{dataDir: f.a.dataDir, fetchTo: func(ctx context.Context, key, dst, sha string) error {
146 return release.FetchToFile(ctx, f.store, key, dst, systemArtifactLimit, sha)
147 }}
148 next := []byte("#!/bin/sh\necho carlos v1.5.0\n")
149 f.publish(t, "v1.5.0", next)
150 sum := sha256.Sum256(next)
151 pending := &systemPending{version: "v1.5.0", artifact: release.SystemArtifact{Key: release.SystemArtifactKey("v1.5.0", "linux-arm64"), SHA256: hex.EncodeToString(sum[:])}}
152 if err := leg.fetch(context.Background(), pending); err != nil {
153 t.Fatalf("the agent's next download: %v", err)
154 }
155 if err := leg.writeRequest(pending, f.now); err != nil {
156 t.Fatalf("the agent's next request: %v", err)
157 }
158}
159
160// cachePathDirs are the directories install-channel hands the service
161// user for version: system, system/router and the version's own.
162func cachePathDirs(dataDir, version string) []string {
163 return []string{filepath.Join(dataDir, "system"), filepath.Join(dataDir, "system", "router"), filepath.Join(dataDir, "system", "router", version)}
164}
165
166// S2: a run whose ownership handoff fails installs nothing; the next run
167// reconciles every directory on the path, including the ones the failed
168// run created, not only the ones it creates itself.
169func TestInstallChannelReconcilesOwnershipOnARetry(t *testing.T) {
170 f := newChannelFixture(t, "v1.2.0")
171 own := f.in.own
172 f.in.own = func(string) error { return errors.New("chown: operation not permitted") }
173 if _, err := f.in.run(context.Background()); err == nil {
174 t.Fatal("a failed handoff installed")
175 }
176 if cur, prev := f.slots(t); cur != nil || prev != nil || f.fetches != 0 {
177 t.Fatalf("a failed handoff wrote %q %q after %d fetches", cur, prev, f.fetches)
178 }
179 // The failed run created system, as root, and never handed it over.
180 if _, err := os.Stat(filepath.Join(f.a.dataDir, "system")); err != nil {
181 t.Fatalf("the failed run's parent: %v", err)
182 }
183 f.in.own = own
184 if _, err := f.in.run(context.Background()); err != nil {
185 t.Fatal(err)
186 }
187 if want := cachePathDirs(f.a.dataDir, "v1.4.0"); !reflect.DeepEqual(f.owned, want) {
188 t.Fatalf("the retry handed %v, want %v", f.owned, want)
189 }
190}
191
192// S2: a symlinked parent is refused before anything is fetched, and never
193// followed; the real handoff opens each directory without following one.
194func TestInstallChannelRefusesASymlinkedParent(t *testing.T) {
195 f := newChannelFixture(t, "v1.2.0")
196 elsewhere := t.TempDir()
197 if err := os.Symlink(elsewhere, filepath.Join(f.a.dataDir, "system")); err != nil {
198 t.Fatal(err)
199 }
200 if _, err := f.in.run(context.Background()); err == nil || f.fetches != 0 || len(f.owned) != 0 {
201 t.Fatalf("symlinked parent: %v, %d fetches, owned %v", err, f.fetches, f.owned)
202 }
203 if ents, _ := os.ReadDir(elsewhere); len(ents) != 0 {
204 t.Fatalf("followed the symlink: %v", ents)
205 }
206 if err := ownDir(filepath.Join(f.a.dataDir, "system"), os.Getuid(), os.Getgid()); err == nil {
207 t.Fatal("the handoff followed a symlink")
208 }
209 if err := ownDir(elsewhere, os.Getuid(), os.Getgid()); err != nil {
210 t.Fatalf("the handoff of a real directory: %v", err)
211 }
212}
213
214func TestInstallChannelRefusesBelowTheFloorAndWithoutOne(t *testing.T) {
215 for _, c := range []struct{ name, floor, want string }{
216 {"below the floor", "v2.0.0", "below the host floor v2.0.0"},
217 {"no floor", "", "no host floor is published"},
218 } {
219 t.Run(c.name, func(t *testing.T) {
220 f := newChannelFixture(t, c.floor)
221 _, err := f.in.run(context.Background())
222 if err == nil || !strings.Contains(err.Error(), c.want) {
223 t.Fatalf("err %v", err)
224 }
225 cur, prev := f.slots(t)
226 if cur != nil || prev != nil || f.fetches != 0 {
227 t.Fatalf("refused install wrote %q %q after %d fetches", cur, prev, f.fetches)
228 }
229 })
230 }
231}
232
233func TestInstallChannelRefusesAnUnreadableStoreAndABadArtifact(t *testing.T) {
234 f := newChannelFixture(t, "v1.2.0")
235 f.a.openStore = func(context.Context) (release.Store, ed25519.PublicKey, error) {
236 return nil, nil, errors.New("no route to bucket")
237 }
238 if _, err := f.in.run(context.Background()); err == nil || !strings.Contains(err.Error(), "could not be read") {
239 t.Fatalf("unreachable store: %v", err)
240 }
241 g := newChannelFixture(t, "v1.2.0")
242 // The object no longer matches the signed sha256.
243 _, etag, _ := g.store.Get(context.Background(), release.SystemArtifactKey("v1.4.0", "linux-arm64"))
244 if _, err := g.store.Put(context.Background(), release.SystemArtifactKey("v1.4.0", "linux-arm64"), []byte("#!/bin/sh\necho carlos v0.0.1\n"), etag); err != nil {
245 t.Fatal(err)
246 }
247 if _, err := g.in.run(context.Background()); err == nil || !strings.Contains(err.Error(), "could not be fetched") {
248 t.Fatalf("swapped artifact: %v", err)
249 }
250 if cur, prev := g.slots(t); cur != nil || prev != nil {
251 t.Fatalf("swapped artifact installed %q %q", cur, prev)
252 }
253}
254
255func TestInstallChannelLeavesAnInstalledBoxToUpdates(t *testing.T) {
256 // An armed guard.
257 f := newChannelFixture(t, "v1.2.0")
258 ctx := context.Background()
259 raw, _, _ := f.store.Get(ctx, hostfloor.Key)
260 auth, _, _ := f.store.Get(ctx, hostfloor.AuthKey)
261 policy, _ := json.Marshal(hostfloor.Policy{Schema: 1, DeploymentBucket: "bucket", Mode: "armed", Floor: raw, Authorization: auth})
262 if err := os.MkdirAll(systemGuardDir(f.a.optDir), 0o755); err != nil {
263 t.Fatal(err)
264 }
265 if err := os.WriteFile(filepath.Join(systemGuardDir(f.a.optDir), "policy.json"), policy, 0o644); err != nil {
266 t.Fatal(err)
267 }
268 if _, err := f.in.run(ctx); err == nil || !strings.Contains(err.Error(), "guard is armed") {
269 t.Fatalf("armed guard: %v", err)
270 }
271 // A box an earlier install or update put another release on.
272 g := newChannelFixture(t, "v1.2.0")
273 if _, err := g.in.run(ctx); err != nil {
274 t.Fatal(err)
275 }
276 g.publish(t, "v1.5.0", []byte("#!/bin/sh\necho carlos v1.5.0\n"))
277 if _, err := g.in.run(ctx); err == nil || !strings.Contains(err.Error(), "already runs v1.4.0") {
278 t.Fatalf("installed box: %v", err)
279 }
280 if cur, _ := g.slots(t); !bytes.Equal(cur, g.body) {
281 t.Fatalf("installed box changed to %q", cur)
282 }
283}
284
285// A refused run removes nothing through a symlinked parent: the cleanup of
286// the version directory is made only after the directories are made, and
287// goes by descriptor, so root cannot be walked out of /data.
288func TestInstallChannelRemovesNothingThroughASymlink(t *testing.T) {
289 f := newChannelFixture(t, "v1.2.0")
290 elsewhere := t.TempDir()
291 keep := filepath.Join(elsewhere, "router", "v1.4.0", "keep")
292 if err := os.MkdirAll(filepath.Dir(keep), 0o755); err != nil {
293 t.Fatal(err)
294 }
295 if err := os.WriteFile(keep, []byte("mine"), 0o644); err != nil {
296 t.Fatal(err)
297 }
298 if err := os.Symlink(elsewhere, filepath.Join(f.a.dataDir, "system")); err != nil {
299 t.Fatal(err)
300 }
301 if _, err := f.in.run(context.Background()); err == nil {
302 t.Fatal("a symlinked parent installed")
303 }
304 if b, err := os.ReadFile(keep); err != nil || string(b) != "mine" {
305 t.Fatalf("a refused run removed what the symlink led to: %q %v", b, err)
306 }
307 // The removal itself, called directly: a symlinked parent, and a
308 // version directory that is itself a symlink, are neither entered nor
309 // followed.
310 if err := removeCacheEntry(f.a.dataDir, "v1.4.0"); err == nil {
311 t.Fatal("the removal went through a symlinked parent")
312 }
313 if b, err := os.ReadFile(keep); err != nil || string(b) != "mine" {
314 t.Fatalf("removal followed a symlinked parent: %q %v", b, err)
315 }
316 g := newChannelFixture(t, "v1.2.0")
317 router := filepath.Join(g.a.dataDir, "system", "router")
318 if err := os.MkdirAll(router, 0o755); err != nil {
319 t.Fatal(err)
320 }
321 other := t.TempDir()
322 if err := os.WriteFile(filepath.Join(other, "keep"), []byte("mine"), 0o644); err != nil {
323 t.Fatal(err)
324 }
325 if err := os.Symlink(other, filepath.Join(router, "v1.4.0")); err != nil {
326 t.Fatal(err)
327 }
328 if err := removeCacheEntry(g.a.dataDir, "v1.4.0"); err == nil {
329 t.Fatal("the removal entered a symlinked version directory")
330 }
331 if b, err := os.ReadFile(filepath.Join(other, "keep")); err != nil || string(b) != "mine" {
332 t.Fatalf("removal followed the version directory's symlink: %q %v", b, err)
333 }
334 // And a real entry, with a nested directory and a symlink inside it, is
335 // removed whole, its parents left.
336 h := newChannelFixture(t, "v1.2.0")
337 entry := filepath.Join(h.a.dataDir, "system", "router", "v1.4.0")
338 if err := os.MkdirAll(filepath.Join(entry, "sub"), 0o755); err != nil {
339 t.Fatal(err)
340 }
341 if err := os.WriteFile(filepath.Join(entry, "sub", "carlos"), []byte("x"), 0o755); err != nil {
342 t.Fatal(err)
343 }
344 if err := os.Symlink(other, filepath.Join(entry, "link")); err != nil {
345 t.Fatal(err)
346 }
347 if err := removeCacheEntry(h.a.dataDir, "v1.4.0"); err != nil {
348 t.Fatal(err)
349 }
350 if _, err := os.Stat(entry); !os.IsNotExist(err) {
351 t.Fatalf("entry left: %v", err)
352 }
353 if _, err := os.Stat(filepath.Join(h.a.dataDir, "system", "router")); err != nil {
354 t.Fatalf("a parent was removed: %v", err)
355 }
356 if b, err := os.ReadFile(filepath.Join(other, "keep")); err != nil || string(b) != "mine" {
357 t.Fatalf("removal followed a symlink inside the entry: %q %v", b, err)
358 }
359 if err := removeCacheEntry(h.a.dataDir, "v1.4.0"); err != nil {
360 t.Fatalf("an entry already gone: %v", err)
361 }
362}
363
364// A box an update has managed, at the channel's head, has applied.json and no
365// channel identity: its releases come through carlos system update, and its
366// carlos.prev is not overwritten.
367func TestInstallChannelRefusesAnUpdatedBoxAtTheChannelHead(t *testing.T) {
368 f := newChannelFixture(t, "v1.2.0")
369 if err := os.WriteFile(systemBinaryPath(f.a.optDir), f.body, 0o755); err != nil {
370 t.Fatal(err)
371 }
372 prev := []byte("#!/bin/sh\necho carlos v1.3.0\n")
373 if err := os.WriteFile(systemPrevPath(f.a.optDir), prev, 0o755); err != nil {
374 t.Fatal(err)
375 }
376 if err := writeJSONDurable(f.a.file("applied.json"), systemApplied{Version: "v1.4.0", Channel: "stable", At: f.now}, 0o644); err != nil {
377 t.Fatal(err)
378 }
379 _, err := f.in.run(context.Background())
380 if err == nil || err.Error() != "this box already runs v1.4.0, so carlos system update installs its releases" {
381 t.Fatalf("updated box: %v", err)
382 }
383 if cur, p := f.slots(t); !bytes.Equal(cur, f.body) || !bytes.Equal(p, prev) || f.fetches != 0 {
384 t.Fatalf("an updated box was changed: %q %q after %d fetches", cur, p, f.fetches)
385 }
386}
387
388// A binary this command did not put in a slot, with no applied.json, is
389// refused and left where it is; a run cut off part way, which leaves only the
390// artifact behind, is completed.
391func TestInstallChannelRefusesAHandRolledBoxAndCompletesACutOffRun(t *testing.T) {
392 for _, slot := range []func(string) string{systemBinaryPath, systemPrevPath} {
393 f := newChannelFixture(t, "v1.2.0")
394 path := slot(f.a.optDir)
395 if err := os.WriteFile(path, []byte("#!/bin/sh\necho hand rolled\n"), 0o755); err != nil {
396 t.Fatal(err)
397 }
398 _, err := f.in.run(context.Background())
399 if err == nil || err.Error() != path+" holds a build this command did not install, so nothing was installed" {
400 t.Fatalf("hand-rolled %s: %v", path, err)
401 }
402 if b, _ := os.ReadFile(path); string(b) != "#!/bin/sh\necho hand rolled\n" || f.fetches != 0 {
403 t.Fatalf("a hand-rolled binary was replaced: %q after %d fetches", b, f.fetches)
404 }
405 if _, err := os.Stat(systemCachePath(f.a.dataDir, "v1.4.0")); !os.IsNotExist(err) {
406 t.Fatalf("a refused run left a cache entry: %v", err)
407 }
408 }
409 // Cut off after carlos.prev, or after both slots: no applied.json, and
410 // only the artifact on disk.
411 for name, slots := range map[string][]func(string) string{
412 "prev only": {systemPrevPath},
413 "both slots": {systemPrevPath, systemBinaryPath},
414 } {
415 t.Run(name, func(t *testing.T) {
416 f := newChannelFixture(t, "v1.2.0")
417 for _, s := range slots {
418 if err := os.WriteFile(s(f.a.optDir), f.body, 0o755); err != nil {
419 t.Fatal(err)
420 }
421 }
422 got, err := f.in.run(context.Background())
423 if err != nil || got.Already {
424 t.Fatalf("retry %+v %v", got, err)
425 }
426 if cur, prev := f.slots(t); !bytes.Equal(cur, f.body) || !bytes.Equal(prev, f.body) {
427 t.Fatalf("slots hold %q %q", cur, prev)
428 }
429 if applied, err := readSystemApplied(f.a.optDir); err != nil || applied == nil || applied.Version != "v1.4.0" {
430 t.Fatalf("applied %+v %v", applied, err)
431 }
432 if _, err := os.Stat(channelIdentityPath(f.a.optDir)); err != nil {
433 t.Fatal(err)
434 }
435 })
436 }
437}
438
439// A slot that is a symlink is refused, even one to a file that holds the
440// artifact, and neither the link nor its target is touched.
441func TestInstallChannelRefusesASymlinkedSlot(t *testing.T) {
442 for _, slot := range []func(string) string{systemBinaryPath, systemPrevPath} {
443 f := newChannelFixture(t, "v1.2.0")
444 target := filepath.Join(t.TempDir(), "elsewhere")
445 if err := os.WriteFile(target, f.body, 0o755); err != nil {
446 t.Fatal(err)
447 }
448 path := slot(f.a.optDir)
449 if err := os.Symlink(target, path); err != nil {
450 t.Fatal(err)
451 }
452 _, err := f.in.run(context.Background())
453 if err == nil || err.Error() != path+" holds a build this command did not install, so nothing was installed" || f.fetches != 0 {
454 t.Fatalf("symlinked %s: %v after %d fetches", path, err, f.fetches)
455 }
456 if l, err := os.Readlink(path); err != nil || l != target {
457 t.Fatalf("the link was changed: %q %v", l, err)
458 }
459 if b, _ := os.ReadFile(target); !bytes.Equal(b, f.body) {
460 t.Fatalf("the link's target was changed: %q", b)
461 }
462 // A slot that cannot be read at all is an error, not a pass.
463 if err := os.Remove(path); err != nil {
464 t.Fatal(err)
465 }
466 if err := os.Mkdir(path, 0o755); err != nil {
467 t.Fatal(err)
468 }
469 if _, err := f.in.run(context.Background()); err == nil || !strings.Contains(err.Error(), "holds a build this command did not install") {
470 t.Fatalf("a directory in a slot: %v", err)
471 }
472 }
473}
474