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 "encoding/json"
7 "errors"
8 "os"
9 "path/filepath"
10 "testing"
11
12 "github.com/carlosframework/platform/internal/hostbox"
13 "github.com/carlosframework/platform/internal/hostclaim"
14 "github.com/carlosframework/platform/internal/hostfiles"
15 "github.com/carlosframework/platform/internal/lease/leasetest"
16 "github.com/carlosframework/platform/internal/provision"
17 "github.com/carlosframework/platform/internal/registry"
18 "github.com/carlosframework/platform/internal/release"
19)
20
21type filesSeedFixture struct {
22 t *testing.T
23 claims release.Store
24 data *leasetest.VersionedBucket
25 files *movesFiles
26 route registry.Route
27 observed hostclaim.Observed
28 actor hostbox.Actor
29}
30
31func newFilesSeedFixture(t *testing.T) *filesSeedFixture {
32 t.Helper()
33 t.Setenv("CARLOS_HOST_CLAIM_ORIGINS", "1")
34 t.Setenv("CARLOS_HOST_CLAIM", "1")
35 claims := release.NewDirStore(t.TempDir())
36 o, err := hostclaim.Read(t.Context(), claims, filesHost)
37 if err != nil {
38 t.Fatal(err)
39 }
40 nh := hostclaim.NewHolder{Family: "instance", Key: "apps/bab/jam/instances/" + filesHost + ".json", Account: "bab", App: "jam", Host: filesHost, Region: "ie", CreatedAt: filesCreated}
41 o, err = hostclaim.Update(t.Context(), claims, o, hostclaim.Adopt(nh, "box-1", nil, nil, "adopt", func() string { return "adopt" }))
42 if err != nil {
43 t.Fatal(err)
44 }
45 data := leasetest.NewVersionedBucket()
46 data.AddBucket("data")
47 dir := t.TempDir()
48 rt := registry.Route{Host: filesHost, Kind: "instance", Account: "bab", App: "jam", Hibernate: true, DBPath: provision.NestedDBPath(dir, filesHost), Socket: provision.NestedSocket(filesHost)}
49 env := filepath.Dir(rt.DBPath)
50 if err := os.MkdirAll(filepath.Join(env, filesHost), 0700); err != nil {
51 t.Fatal(err)
52 }
53 if err := os.WriteFile(filepath.Join(env, filesHost+".env"), []byte("KEY=value\n"), 0600); err != nil {
54 t.Fatal(err)
55 }
56 f := &movesFiles{Files: casBucketStore{b: data, bucket: "data"}, Data: data, Bucket: "data", Box: "box-1", Region: "ie", HomeRegion: "ie", DataDir: dir, EnvFile: claimEnvFileFor(dir, testClaimRoutes{rows: map[string]registry.Route{filesHost: rt}}), Proof: ClaimOriginProof{Claims: claims, HomeRegion: "ie", Region: "ie"}}
57 return &filesSeedFixture{t: t, claims: claims, data: data, files: f, route: rt, observed: o, actor: hostbox.Actor{Box: "box-1", Region: "ie", HomeRegion: "ie", Bucket: "data", Enrollment: "enrolled", Revision: 1, Binding: "1", Eligible: true}}
58}
59func (f *filesSeedFixture) admit(gen string) {
60 f.t.Helper()
61 o := f.observed
62 a := hostclaim.Admission{ObservedSession: o.Claim.Holder.Session, ObservedGeneration: o.Claim.Holder.Generation, Box: f.actor.Box, Region: f.actor.Region, PredecessorRegion: f.actor.Region, Mode: "cold", NewGeneration: gen}
63 var err error
64 f.observed, err = hostclaim.Update(f.t.Context(), f.claims, o, hostclaim.AdmitWriter(a))
65 if err != nil {
66 f.t.Fatal(err)
67 }
68}
69func (f *filesSeedFixture) inspect() (filesSeedSelection, error) {
70 return inspectFilesSeed(f.t.Context(), f.claims, f.files, f.observed, f.actor, &f.route)
71}
72
73func TestFilesSeedClassificationRealOrigins(t *testing.T) {
74 f := newFilesSeedFixture(t)
75 for _, gen := range []string{"", "first", "retry"} {
76 if gen != "" {
77 f.admit(gen)
78 }
79 before := f.observed.ETag
80 got, err := f.inspect()
81 if err != nil || got.State != "eligible" {
82 t.Fatal(gen, got, err)
83 }
84 fresh, err := hostclaim.Read(t.Context(), f.claims, filesHost)
85 if err != nil || fresh.ETag != before {
86 t.Fatal("classifier wrote claim", err)
87 }
88 }
89 if f.observed.Claim.Origins == nil {
90 t.Fatal("admissions did not record real origins")
91 }
92}
93
94func TestFilesSeedClassificationRefusals(t *testing.T) {
95 for _, fault := range []string{"missing-files", "flat", "symlink", "replica", "sidecar", "backing", "seed", "unit", "tunnel", "no-row", "stale", "foreign", "chain-gap", "foreign-predecessor", "cycle"} {
96 t.Run(fault, func(t *testing.T) {
97 f := newFilesSeedFixture(t)
98 f.admit("first")
99 f.admit("second")
100 var row *registry.Route = &f.route
101 switch fault {
102 case "missing-files":
103 if err := os.Remove(filepath.Join(filepath.Dir(f.route.DBPath), filesHost)); err != nil {
104 t.Fatal(err)
105 }
106 case "flat":
107 f.route.DBPath = filepath.Join(f.files.DataDir, filesHost+".db")
108 case "symlink":
109 path := filepath.Join(filepath.Dir(f.route.DBPath), filesHost)
110 if err := os.Remove(path); err != nil {
111 t.Fatal(err)
112 }
113 if err := os.Symlink(t.TempDir(), path); err != nil {
114 t.Fatal(err)
115 }
116 case "replica":
117 f.route.Replica = true
118 case "sidecar":
119 f.route.Kind = "sidecar"
120 case "backing":
121 f.route.Backing = "foreign"
122 case "seed":
123 f.route.Seed = "seed"
124 case "unit":
125 f.route.Unit = "unit"
126 case "tunnel":
127 f.route.Tunnel = "tunnel"
128 case "no-row":
129 row = nil
130 case "stale":
131 old := f.observed
132 f.admit("third")
133 f.observed = old
134 default:
135 o, err := hostclaim.Update(t.Context(), f.claims, f.observed, func(c *hostclaim.Claim) error {
136 switch fault {
137 case "foreign":
138 c.Op.Exec = "other"
139 case "chain-gap":
140 c.Holder.Admissions = c.Holder.Admissions[1:]
141 case "foreign-predecessor":
142 c.Holder.Admissions[0].Box = "other"
143 case "cycle":
144 c.Holder.Admissions[0].Predecessor = "second"
145 }
146 return nil
147 })
148 if err != nil {
149 t.Fatal(err)
150 }
151 f.observed = o
152 }
153 got, err := inspectFilesSeed(t.Context(), f.claims, f.files, f.observed, f.actor, row)
154 if err == nil && got.State == "eligible" {
155 t.Fatal("eligible", fault)
156 }
157 })
158 }
159}
160
161func TestFilesSeedExistingManifestNeverSeededPast(t *testing.T) {
162 f := newFilesSeedFixture(t)
163 f.admit("first")
164 f.admit("second")
165 prefix := movesFilesPrefix(f.observed.Claim.Holder.Location, "first")
166 p, err := hostfiles.NewFromCopy(f.files.Files, prefix, emptyManifest(), nil)
167 if err != nil {
168 t.Fatal(err)
169 }
170 p.SetOrigins([]string{prefix})
171 if _, err = p.Commit(t.Context()); err != nil {
172 t.Fatal(err)
173 }
174 got, err := f.inspect()
175 if err != nil || got.State != "established" {
176 t.Fatal(got, err)
177 }
178 // An unreadable older manifest still refuses after a newer valid one.
179 f.data.Put("data", movesFilesPrefix(f.observed.Claim.Holder.Location, "")+"manifest.json", []byte("broken"))
180 if _, err = f.inspect(); err == nil {
181 t.Fatal("seeded past malformed older manifest")
182 }
183}
184
185func TestFilesSeedManifestCapIncludesTombstonesAndSkips(t *testing.T) {
186 for _, entry := range []hostfiles.Entry{{Deleted: true, Seq: 1}, {Skipped: "too-large", Seq: 1}} {
187 f := newFilesSeedFixture(t)
188 f.admit("first")
189 prefix := movesFilesPrefix(f.observed.Claim.Holder.Location, "first")
190 m := emptyManifest()
191 m.Seq = 1
192 // Valid path components, beyond the cumulative path cap.
193 path := "a"
194 for len(path) < 5000 {
195 path += "/a"
196 }
197 m.Entries = map[string]hostfiles.Entry{path: entry}
198 body, _ := json.Marshal(m)
199 f.data.Put("data", prefix+"manifest.json", body)
200 if _, err := f.inspect(); !errors.Is(err, hostfiles.ErrTooDeep) {
201 t.Fatal("cap", err)
202 }
203 }
204}
205
206func TestFilesSeedCandidatesUseClaimsWithoutRegistry(t *testing.T) {
207 f := newFilesSeedFixture(t)
208 got, err := filesSeedCandidates(t.Context(), f.claims, f.actor.Box, "")
209 if err != nil || len(got) != 1 || got[0].ETag != f.observed.ETag {
210 t.Fatal(got, err)
211 }
212 got, err = filesSeedCandidates(t.Context(), f.claims, "other-box", "")
213 if err != nil || len(got) != 0 {
214 t.Fatal(got, err)
215 }
216 got, err = filesSeedCandidates(t.Context(), f.claims, "other-box", filesHost)
217 if err != nil || len(got) != 1 {
218 t.Fatal(got, err)
219 }
220 if _, err = filesSeedCandidates(t.Context(), f.claims, f.actor.Box, "../bad"); err == nil {
221 t.Fatal("accepted path selector")
222 }
223 if _, err = f.claims.Put(t.Context(), "control/hosts/bad-key", []byte("{}"), ""); err != nil {
224 t.Fatal(err)
225 }
226 if _, err = filesSeedCandidates(t.Context(), f.claims, f.actor.Box, ""); err == nil {
227 t.Fatal("ignored malformed census")
228 }
229}
230