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 && rootrunner
2
3package host
4
5import (
6 "context"
7 "io"
8 "net"
9 "net/http"
10 "os"
11 "os/exec"
12 "path/filepath"
13 "strings"
14 "testing"
15 "time"
16
17 "github.com/carlosframework/platform/internal/hostclaim"
18 "github.com/carlosframework/platform/internal/lease/leasetest"
19 "github.com/carlosframework/platform/internal/provision"
20)
21
22func seedTwoHostNames() []string {
23 if os.Getenv("FILES_SEED_ISOLATION_MUTATION") == "1" {
24 return []string{"seed-negative-a.bab.oncarlos.com", "seed-negative-b.bab.oncarlos.com"}
25 }
26 return []string{"seed-a.bab.oncarlos.com", "seed-b.bab.oncarlos.com"}
27}
28
29func TestRootFilesSeedTwoHostIsolation(t *testing.T) {
30 runServingParkPeerNamespace(t, false, false, "seed-two-host")
31}
32
33type seedLiveHost struct {
34 p *servingPreparation
35 executor *filesSeedExecutor
36 endpoint string
37 files *movesFiles
38 witness filesSeedLiveWitness
39 stop func()
40 observed hostclaim.Observed
41}
42
43func rootFilesSeedTwoHost(t *testing.T, binary string) {
44 witness := rootFilesSeedIsolatedWitness(t, binary, true)
45 hosts := make([]*seedLiveHost, 0, 2)
46 for i, host := range seedTwoHostNames() {
47 h := &seedLiveHost{witness: witness}
48 capture := filesSeedLiveWitness{host: host, shared: i > 0, seed: func(t *testing.T, p *servingPreparation, e *filesSeedExecutor) {
49 h.p, h.executor, h.endpoint = p, e, os.Getenv("CARLOS_S3_ENDPOINT")
50 if err := os.WriteFile(filepath.Join(filepath.Dir(p.f.db), host, "payload"), []byte("real seeded bytes:"+host), 0600); err != nil {
51 t.Fatal(err)
52 }
53 }}
54 runFilesSeedPreparationWithLive(t, "real-seed", "same-controller", []bool{true}, []filesSeedLiveWitness{capture})
55 hosts = append(hosts, h)
56 }
57 for _, h := range hosts {
58 t.Setenv("CARLOS_S3_ENDPOINT", h.endpoint)
59 if err := h.executor.run(t.Context(), h.p.f.f.hostName(), io.Discard); err != nil {
60 t.Fatal("seed", err)
61 }
62 f := h.p.f
63 opened, err := h.executor.source.Open(t.Context(), f.f.claims, f.f.data.(*leasetest.VersionedBucket), f.actor)
64 if err != nil {
65 t.Fatal(err)
66 }
67 h.files = opened.(*movesFiles)
68 h.wake(t)
69 }
70 for _, h := range hosts {
71 h.probe(t)
72 }
73 // Retire each real launched generation while the other tenant stays live.
74 for i, h := range hosts {
75 other := hosts[1-i]
76 other.probe(t)
77 h.stop()
78 previous := h.observed.Claim.Holder.Generation
79 h.wake(t)
80 if h.observed.Claim.Holder.Generation == previous {
81 t.Fatal("cold wake reused generation")
82 }
83 other.probe(t)
84 h.probe(t)
85 }
86 for _, h := range hosts {
87 h.stop()
88 }
89 if strings.HasPrefix(t.Name(), "TestRootRebootFilesSeedTwoHost") && strings.HasSuffix(t.Name(), "Phase1") {
90 saveFilesSeedReboot(t, binary, hosts)
91 }
92}
93
94func (h *seedLiveHost) wake(t *testing.T) {
95 t.Helper()
96 t.Setenv("CARLOS_S3_ENDPOINT", h.endpoint)
97 f := h.p.f
98 f.f.now = f.f.now.Add(time.Minute)
99 p := newServingPreparation(t, f, "boot", "controller", func(w *ClaimWakeAssembly) {
100 w.Inputs.(*claimServingOwners).Sources.Files = h.files
101 w.ServingPrerequisites = servingFilesPrerequisites(f.f.claims, h.files)
102 h.witness.configure(w)
103 })
104 h.witness.prepare(p)
105 admitted, start := p.prepare(t)
106 if err := start.Restore(t.Context()); err != nil {
107 t.Fatal("restore", err)
108 }
109 if err := start.Reserve(t.Context(), 0); err != nil {
110 t.Fatal("reserve", err)
111 }
112 if err := start.Materialize(t.Context()); err != nil {
113 t.Fatal("materialize", err)
114 }
115 stop := h.witness.start(t, p, admitted, start)
116 h.observed = admitted
117 h.stop = func() {
118 stop()
119 t.Setenv("CARLOS_S3_ENDPOINT", h.endpoint)
120 if err := p.runtime.FailedAdmission(t.Context(), admitted, hostclaim.ErrWrongState); err != nil {
121 t.Fatal("release", err)
122 }
123 }
124}
125
126func (h *seedLiveHost) probe(t *testing.T) {
127 t.Helper()
128 host := h.p.f.f.hostName()
129 transport := &http.Transport{DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
130 return (&net.Dialer{}).DialContext(ctx, "unix", provision.NestedSocket(host))
131 }}
132 defer transport.CloseIdleConnections()
133 response, err := (&http.Client{Transport: transport, Timeout: time.Second}).Get("http://tenant/")
134 if err != nil {
135 t.Fatal("sibling unavailable", host, err)
136 }
137 defer response.Body.Close()
138 body, err := io.ReadAll(response.Body)
139 if err != nil || response.StatusCode != 200 || string(body) != "real seeded bytes:"+host {
140 t.Fatal("sibling changed", host, string(body), err)
141 }
142}
143
144func TestRootFilesSeedIsolationNegativeControl(t *testing.T) {
145 servingRootRunner(t)
146 binary, err := os.Executable()
147 if err != nil {
148 t.Fatal(err)
149 }
150 cmd := exec.CommandContext(t.Context(), binary, "-test.run=^TestRootFilesSeedTwoHostIsolation$", "-test.v", "-test.timeout=2m")
151 cmd.Env = append(os.Environ(), "FILES_SEED_ISOLATION_MUTATION=1")
152 output, err := cmd.CombinedOutput()
153 if err == nil || !strings.Contains(string(output), "isolation mutation detected: /tmp/exposed-root") {
154 t.Fatalf("isolation mutant not caught: %v\n%s", err, output)
155 }
156 t.Log("root-only mutation caught: exposed private-root file was read by the tenant")
157}
158