carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package hostclaim
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "os"
9 "reflect"
10 "strings"
11 "testing"
12
13 "github.com/carlosframework/platform/internal/release"
14)
15
16func plan15Enable(t *testing.T) {
17 t.Helper()
18 t.Setenv("CARLOS_HOST_CLAIM", "1")
19 t.Setenv("CARLOS_HOST_CLAIM_ORIGINS", "1")
20}
21func plan15Admit(o Observed, generation, region string) func(*Claim) error {
22 h := o.Claim.Holder
23 return AdmitWriter(Admission{ObservedOpID: o.Claim.Op.ID, ObservedPhase: o.Claim.Op.Phase,
24 ObservedSession: h.Session, ObservedGeneration: h.Generation, Box: o.Claim.Op.Exec,
25 Mode: "cold", NewGeneration: generation, Region: region, PredecessorRegion: region, PredecessorCertified: true})
26}
27func plan15Proof(t *testing.T, s release.Store, o Observed, generation, region string, want bool) {
28 t.Helper()
29 h := o.Claim.Holder
30 got, err := AdmittedOrigin(t.Context(), s, o, h.Incarnation, h.Location, region, generation)
31 if err != nil || got != want {
32 t.Fatalf("proof %s/%s=%v,%v want %v", region, generation, got, err, want)
33 }
34}
35func TestPlan15PruningMigrationAndScope(t *testing.T) {
36 t.Setenv("CARLOS_HOST_CLAIM", "")
37 t.Setenv("CARLOS_HOST_CLAIM_ORIGINS", "")
38 s := release.NewDirStore(t.TempDir())
39 o := servingClaim(t, t.Context(), s)
40 // A legacy claim can have more admissions than the covered retention window.
41 for i := 0; i < 12; i++ {
42 o = updateGenerationTest(t, t.Context(), s, o, plan15Admit(o, fmt.Sprintf("old%d", i), "ie"))
43 o = updateGenerationTest(t, t.Context(), s, o, func(c *Claim) error {
44 for i := range c.Holder.Admissions {
45 c.Holder.Admissions[i].Covered = true
46 }
47 return nil
48 })
49 }
50 if len(o.Claim.Holder.Admissions) != 8 {
51 t.Fatal("fixture did not prune")
52 }
53 plan15Enable(t)
54 // The chain starts empty: before the first recorded admission, even a
55 // retained (and possibly injected) admission proves nothing.
56 plan15Proof(t, s, o, "old0", "ie", false)
57 plan15Proof(t, s, o, "old4", "ie", false)
58 plan15Proof(t, s, o, "old11", "ie", false)
59 o = updateGenerationTest(t, t.Context(), s, o, plan15Admit(o, "first", "ie"))
60 for i := 0; i < 24; i++ {
61 o = updateGenerationTest(t, t.Context(), s, o, func(c *Claim) error {
62 for i := range c.Holder.Admissions {
63 c.Holder.Admissions[i].Covered = true
64 }
65 return nil
66 })
67 o = updateGenerationTest(t, t.Context(), s, o, plan15Admit(o, fmt.Sprintf("new%d", i), "ie"))
68 }
69 for _, g := range []string{"first", "new0", "new23"} {
70 plan15Proof(t, s, o, g, "ie", true)
71 }
72 plan15Proof(t, s, o, "old0", "ie", false)
73 plan15Proof(t, s, o, "old4", "ie", false)
74 plan15Proof(t, s, o, "old11", "ie", false)
75 plan15Proof(t, s, o, "never", "ie", false)
76 plan15Proof(t, s, o, "first", "uk", false)
77 h := o.Claim.Holder
78 for _, q := range []struct{ inc, loc string }{{strings.Repeat("b", 64), "instances/" + o.Host}, {h.Incarnation, "instances/" + o.Host}} {
79 got, err := AdmittedOrigin(t.Context(), s, o, q.inc, q.loc, "ie", "first")
80 if got || err != nil {
81 t.Fatalf("wrong scope %v %v", got, err)
82 }
83 }
84 // The old incarnation's history survives holder removal and the retirement cap.
85 old := *h
86 o = updateGenerationTest(t, t.Context(), s, o, func(c *Claim) error {
87 c.Holder = nil
88 c.Op = Op{}
89 c.Obligations = nil
90 c.RetiredIncarnations = nil
91 return nil
92 })
93 got, err := AdmittedOrigin(t.Context(), s, o, old.Incarnation, old.Location, "ie", "first")
94 if !got || err != nil {
95 t.Fatalf("retired origin %v %v", got, err)
96 }
97}
98
99type plan15Store struct {
100 release.Store
101 failNode, failClaim, conflict, uncertain bool
102 puts, gets int
103}
104
105func (s *plan15Store) Get(ctx context.Context, key string) ([]byte, string, error) {
106 s.gets++
107 return s.Store.Get(ctx, key)
108}
109func (s *plan15Store) Put(ctx context.Context, key string, b []byte, tag string) (string, error) {
110 s.puts++
111 if strings.HasPrefix(key, "control/host-origins/") && s.failNode {
112 return "", os.ErrPermission
113 }
114 if strings.HasPrefix(key, "control/hosts/") {
115 if s.failClaim {
116 return "", os.ErrPermission
117 }
118 if s.conflict {
119 return "", release.ErrCASConflict
120 }
121 if s.uncertain {
122 _, err := s.Store.Put(ctx, key, b, tag)
123 if err != nil {
124 return "", err
125 }
126 return "", os.ErrDeadlineExceeded
127 }
128 }
129 return s.Store.Put(ctx, key, b, tag)
130}
131func TestPlan15AtomicityAndRetry(t *testing.T) {
132 for _, fault := range []string{"node", "claim", "conflict", "uncertain"} {
133 t.Run(fault, func(t *testing.T) {
134 plan15Enable(t)
135 raw := release.NewDirStore(t.TempDir())
136 o := servingClaim(t, t.Context(), raw)
137 s := &plan15Store{Store: raw, failNode: fault == "node", failClaim: fault == "claim", conflict: fault == "conflict", uncertain: fault == "uncertain"}
138 if _, err := Update(t.Context(), s, o, plan15Admit(o, "candidate", "ie")); err == nil {
139 t.Fatal("fault ignored")
140 }
141 fresh, err := Read(t.Context(), raw, o.Host)
142 if err != nil {
143 t.Fatal(err)
144 }
145 committed := fault == "uncertain"
146 if (fresh.Claim.Holder.Generation == "candidate") != committed || (fresh.Claim.Origins != nil) != committed {
147 t.Fatal("split commit")
148 }
149 plan15Proof(t, raw, fresh, "candidate", "ie", committed)
150 if !committed {
151 fresh = updateGenerationTest(t, t.Context(), raw, fresh, plan15Admit(fresh, "candidate", "ie"))
152 plan15Proof(t, raw, fresh, "candidate", "ie", true)
153 }
154 if _, err := Update(t.Context(), raw, o, plan15Admit(o, "stale", "ie")); !errors.Is(err, ErrConflict) {
155 t.Fatalf("stale: %v", err)
156 }
157 plan15Proof(t, raw, fresh, "stale", "ie", false)
158 })
159 }
160}
161func TestPlan15NoSelfAuthentication(t *testing.T) {
162 plan15Enable(t)
163 raw := release.NewDirStore(t.TempDir())
164 o := servingClaim(t, t.Context(), raw)
165 o = updateGenerationTest(t, t.Context(), raw, o, plan15Admit(o, "real", "ie"))
166 h := o.Claim.Holder
167 fake := originNode{V: 1, Host: o.Host, Count: o.Claim.Origins.Count + 1, Previous: o.Claim.Origins.Digest, Origin: originTuple{h.Incarnation, h.Location, "ie", "forged"}}
168 body, _ := json.Marshal(fake)
169 digest := originDigest(body)
170 if _, err := raw.Put(t.Context(), originKey(o.Host, digest), body, ""); err != nil {
171 t.Fatal(err)
172 }
173 plan15Proof(t, raw, o, "forged", "ie", false)
174 // Even a caller who knows the ETag cannot substitute a new head in the reader.
175 tampered := o
176 clone := *o.Claim
177 tampered.Claim = &clone
178 clone.Origins = &OriginHead{digest, fake.Count}
179 if ok, err := AdmittedOrigin(t.Context(), raw, tampered, h.Incarnation, h.Location, "ie", "forged"); ok || !errors.Is(err, ErrConflict) {
180 t.Fatalf("edited observation: %v %v", ok, err)
181 }
182 for _, change := range []func(*Claim){func(c *Claim) { c.Origins = &OriginHead{digest, fake.Count} }, func(c *Claim) { c.Origins = nil }} {
183 if _, err := Update(t.Context(), raw, o, func(c *Claim) error { change(c); return nil }); !errors.Is(err, ErrWrongState) {
184 t.Fatalf("head bypass: %v", err)
185 }
186 }
187 // Only the observed admissions change; the store and ETag are current.
188 fresh, err := Read(t.Context(), raw, o.Host)
189 if err != nil {
190 t.Fatal(err)
191 }
192 fresh.Claim.Holder.Admissions = append(fresh.Claim.Holder.Admissions, AdmissionRecord{Region: "ie", Generation: "invented", Mode: "cold"})
193 if _, err := Update(t.Context(), raw, fresh, plan15Admit(fresh, "new", "ie")); !errors.Is(err, ErrConflict) {
194 t.Fatalf("forged migration: %v", err)
195 }
196 // Missing and corrupt reachable nodes are errors, never an affirmative answer.
197 key := originKey(o.Host, o.Claim.Origins.Digest)
198 b, tag, err := raw.Get(t.Context(), key)
199 if err != nil {
200 t.Fatal(err)
201 }
202 if _, err = raw.Put(t.Context(), key, append(b, ' '), tag); err != nil {
203 t.Fatal(err)
204 }
205 if ok, err := AdmittedOrigin(t.Context(), raw, o, h.Incarnation, h.Location, "ie", "real"); ok || err == nil {
206 t.Fatal("corruption accepted")
207 }
208 if err = raw.Delete(t.Context(), key); err != nil {
209 t.Fatal(err)
210 }
211 if ok, err := AdmittedOrigin(t.Context(), raw, o, h.Incarnation, h.Location, "ie", "real"); ok || err == nil {
212 t.Fatal("missing node accepted")
213 }
214}
215func TestPlan15FlagOffAndWarm(t *testing.T) {
216 for _, flags := range [][2]string{{"", ""}, {"", "1"}, {"1", ""}} {
217 t.Run(fmt.Sprint(flags), func(t *testing.T) {
218 t.Setenv("CARLOS_HOST_CLAIM", flags[0])
219 t.Setenv("CARLOS_HOST_CLAIM_ORIGINS", flags[1])
220 raw := release.NewDirStore(t.TempDir())
221 o := servingClaim(t, t.Context(), raw)
222 s := &plan15Store{Store: raw}
223 var err error
224 o, err = Update(t.Context(), s, o, plan15Admit(o, "legacy", "ie"))
225 if err != nil {
226 t.Fatal(err)
227 }
228 if o.Claim.Origins != nil || s.puts != 1 || s.gets != 0 {
229 t.Fatal("flag-off wrote provenance")
230 }
231 b, _ := json.Marshal(o.Claim)
232 if strings.Contains(string(b), "admitted_origins") {
233 t.Fatal("flag-off wire changed")
234 }
235 })
236 }
237 plan15Enable(t)
238 raw := release.NewDirStore(t.TempDir())
239 o := servingClaim(t, t.Context(), raw)
240 o = updateGenerationTest(t, t.Context(), raw, o, plan15Admit(o, "same", "ie"))
241 before := *o.Claim.Origins
242 o = updateGenerationTest(t, t.Context(), raw, o, AdmitWriter(Admission{ObservedSession: o.Claim.Holder.Session, ObservedGeneration: "same", Box: o.Claim.Op.Exec, Mode: "warm", Region: "ie", PredecessorCertified: true}))
243 if !reflect.DeepEqual(before, *o.Claim.Origins) {
244 t.Fatal("warm duplicated origin")
245 }
246 plan15Proof(t, raw, o, "same", "ie", true)
247 t.Setenv("CARLOS_HOST_CLAIM_ORIGINS", "")
248 if _, err := Update(t.Context(), raw, o, plan15Admit(o, "gap", "ie")); !errors.Is(err, ErrWrongState) {
249 t.Fatalf("disabled writer made gap: %v", err)
250 }
251}
252
253func TestPlan15LimitsAndMalformedHistory(t *testing.T) {
254 plan15Enable(t)
255 s := release.NewDirStore(t.TempDir())
256 o := servingClaim(t, t.Context(), s)
257 for _, gen := range []string{"../escape", RootGeneration, strings.Repeat("x", 257)} {
258 if _, err := Update(t.Context(), s, o, plan15Admit(o, gen, "ie")); err == nil {
259 t.Fatalf("bad generation %q", gen)
260 }
261 }
262 o = updateGenerationTest(t, t.Context(), s, o, plan15Admit(o, "first", "ie"))
263 h := o.Claim.Holder
264 // A synthetic near-limit history tests refusal without writing a million nodes.
265 node := originNode{V: 1, Host: o.Host, Count: originCountLimit, Previous: strings.Repeat("a", 64), Origin: originTuple{h.Incarnation, h.Location, "ie", "first"}}
266 b, _ := json.Marshal(node)
267 head := OriginHead{originDigest(b), originCountLimit}
268 if _, err := s.Put(t.Context(), originKey(o.Host, head.Digest), b, ""); err != nil {
269 t.Fatal(err)
270 }
271 o.Claim.Origins = &head
272 key, _ := Key(o.Host)
273 b, _ = json.Marshal(o.Claim)
274 tag, err := s.Put(t.Context(), key, b, o.ETag)
275 if err != nil {
276 t.Fatal(err)
277 }
278 o.ETag = tag
279 if _, err := Update(t.Context(), s, o, plan15Admit(o, "over-limit", "ie")); !errors.Is(err, ErrWrongState) {
280 t.Fatalf("limit: %v", err)
281 }
282 ctx, cancel := context.WithCancel(t.Context())
283 cancel()
284 if ok, err := AdmittedOrigin(ctx, s, o, h.Incarnation, h.Location, "ie", "absent"); ok || err == nil {
285 t.Fatal("ignored cancellation")
286 }
287 node.Count--
288 b, _ = json.Marshal(node)
289 // Same digest-shaped head with a mismatched count cannot prove membership.
290 if _, err := s.Put(t.Context(), originKey(o.Host, originDigest(b)), b, ""); err != nil {
291 t.Fatal(err)
292 }
293 o.Claim.Origins = &OriginHead{originDigest(b), originCountLimit}
294 b, _ = json.Marshal(o.Claim)
295 o.ETag, err = s.Put(t.Context(), key, b, o.ETag)
296 if err != nil {
297 t.Fatal(err)
298 }
299 if ok, err := AdmittedOrigin(t.Context(), s, o, h.Incarnation, h.Location, "ie", "first"); ok || err == nil {
300 t.Fatal("count mismatch accepted")
301 }
302 for _, head := range []*OriginHead{{"bad", 1}, {strings.Repeat("a", 64), 0}, {strings.Repeat("a", 64), originCountLimit + 1}} {
303 o.Claim.Origins = head
304 if err := o.Claim.Validate(); err == nil {
305 t.Fatal("bad head parsed")
306 }
307 }
308}
309