carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package park
2
3import (
4 "context"
5 "errors"
6 "net/url"
7 "reflect"
8 "strings"
9 "testing"
10 "time"
11
12 "github.com/carlosframework/platform/internal/hostclaim"
13 "github.com/carlosframework/platform/internal/lease"
14)
15
16func movesFixture(t *testing.T, op string) *fixture {
17 t.Helper()
18 f := newFixture(t)
19 obs, err := hostclaim.Read(t.Context(), f.e.Claims, f.w.Host)
20 if err != nil {
21 t.Fatal(err)
22 }
23 f.ob.Park.Location = "moves/" + f.w.Host
24 if op != "" {
25 f.ob.Park.Location += "/" + op
26 }
27 _, err = hostclaim.Update(t.Context(), f.e.Claims, obs, func(c *hostclaim.Claim) error {
28 c.Op = hostclaim.Op{Kind: hostclaim.KindServing, Exec: "box"}
29 c.Holder.Generation = "live"
30 c.Obligations = []hostclaim.Obligation{f.ob}
31 return nil
32 })
33 if err != nil {
34 t.Fatal(err)
35 }
36 f.e.View = func(_ context.Context, o hostclaim.Observed) (View, error) {
37 return View{ClaimETag: o.ETag, Region: "ie", AuthoritativeRegion: "ie", MarkerKnown: true, MarkerGeneration: "unrelated", GenerationOf: func(Ref) (string, bool) { return "live", true }}, nil
38 }
39 return f
40}
41
42func TestMovesScopeParksExactPrefixAndResumes(t *testing.T) {
43 for _, op := range []string{"move-1", strings.Repeat("b", 64), ""} {
44 t.Run("op="+op, func(t *testing.T) {
45 f := movesFixture(t, op)
46 p := f.ob.Park.Location + "/"
47 expected := map[Ref]string{}
48 for _, name := range []string{"replica/0000/data", "seal.json"} {
49 for _, body := range []string{"old", "new"} {
50 id := f.b.Put("data", p+name, []byte(body))
51 expected[Ref{p + name, id}] = body
52 }
53 }
54 f.b.BareDelete("data", p+"replica/0000/data")
55 lock := Ref{p + "lock.json", f.b.Put("data", p+"lock.json", []byte("lease"))}
56 untouched := map[Ref]string{lock: "lease"}
57 keys := []string{"instances/" + f.w.Host + "/0000/data", "instances/" + f.w.Host + "/" + f.ob.Park.Incarnation + "/live/data", "moves/other.example/move-1/data", "moves/" + f.w.Host + "-other/move-1/data"}
58 if op != "" {
59 keys = append(keys, f.ob.Park.Location+"-sibling/data", "moves/"+f.w.Host+"/sibling/data")
60 }
61 for _, key := range keys {
62 untouched[Ref{key, f.b.Put("data", key, []byte("untouched"))}] = "untouched"
63 }
64 listings := 0
65 f.b.FailOn = func(action, key string) error {
66 if action == "list" && !strings.HasPrefix(key, "parked/") {
67 listings++
68 if key != p {
69 t.Errorf("listing %q, want only %q", key, p)
70 return ErrUnsafe
71 }
72 }
73 return nil
74 }
75 f.e.After = func(step string) error {
76 if step == "delete" {
77 return ErrPending
78 }
79 return nil
80 }
81 if err := f.e.Run(t.Context(), f.w); !errors.Is(err, ErrPending) {
82 t.Fatalf("interrupted retirement: %v", err)
83 }
84 f.e.After = nil
85 checkListing := f.b.FailOn
86 f.b.FailOn = func(action, key string) error {
87 if action == "copy" {
88 t.Error("manifest retry copied a source")
89 return ErrUnsafe
90 }
91 return checkListing(action, key)
92 }
93 if err := f.e.Run(t.Context(), f.w); err != nil {
94 t.Fatalf("resumed retirement: %v", err)
95 }
96 if listings != 2 || f.held.released != 1 {
97 t.Fatalf("listings=%d releases=%d", listings, f.held.released)
98 }
99 f.done(t)
100 m, err := f.e.manifest(t.Context(), f.w, f.ob)
101 if err != nil || m == nil || len(m.Archive) != len(expected) || len(m.Delete) != len(expected)+1 {
102 t.Fatalf("manifest: %+v %v", m, err)
103 }
104 for _, copied := range m.Archive {
105 body, ok := expected[ref(copied.Source)]
106 wantKey := "parked/" + f.ob.Park.Incarnation + "/" + f.w.Host + "/" + f.ob.Retirement + "/moves/" + strings.TrimPrefix(copied.Source.Key, p) + "@" + url.QueryEscape(copied.Source.VersionID)
107 if !ok || copied.Destination.Key != wantKey {
108 t.Fatalf("unexpected archive copy: %+v", copied)
109 }
110 got, err := f.b.GetVersion(t.Context(), "data", copied.Destination.Key, copied.Destination.VersionID)
111 if err != nil || string(got) != body {
112 t.Fatalf("archive body=%q, want %q: %v", got, body, err)
113 }
114 }
115 for _, source := range m.Delete {
116 if _, err := f.b.GetVersion(t.Context(), "data", source.Key, source.VersionID); !errors.Is(err, lease.ErrMirrorNotFound) {
117 t.Fatalf("source remains: %+v %v", source, err)
118 }
119 }
120 for r, body := range untouched {
121 got, err := f.b.GetVersion(t.Context(), "data", r.Key, r.VersionID)
122 if err != nil || string(got) != body {
123 t.Fatalf("unrelated version changed: %+v %q %v", r, got, err)
124 }
125 }
126 })
127 }
128}
129
130func TestMovesScopeUsesPersistedEvidenceHorizon(t *testing.T) {
131 for _, op := range []string{"move-1", ""} {
132 for _, offset := range []time.Duration{-time.Minute, 0, time.Hour} {
133 t.Run("op="+op+"/offset="+offset.String(), func(t *testing.T) {
134 f := movesFixture(t, op)
135 f.b.Put("data", f.ob.Park.Location+"/staging/data", []byte("staged"))
136 deadline := f.ob.Park.EvidenceAt.Add(SignatureAge + f.ob.Park.RequestLimit)
137 f.now = deadline.Add(offset)
138 f.e.After = func(step string) error {
139 if step == "verify" {
140 f.now = deadline.Add(2 * time.Hour)
141 }
142 return nil
143 }
144 err := f.e.Run(t.Context(), f.w)
145 if offset > 0 {
146 if err != nil {
147 t.Fatalf("delayed cleanup did not acknowledge immediately: %v", err)
148 }
149 } else {
150 if !errors.Is(err, ErrPending) {
151 t.Fatalf("early verification: %v", err)
152 }
153 obs, err := hostclaim.Read(t.Context(), f.e.Claims, f.w.Host)
154 if err != nil {
155 t.Fatal(err)
156 }
157 if len(obs.Claim.Obligations) != 1 || len(obs.Claim.QuietAfter) != 0 || f.held.released != 0 {
158 t.Fatal("acknowledged before a listing after the horizon")
159 }
160 if !reflect.DeepEqual(obs.Claim.Obligations[0].Park, f.ob.Park) {
161 t.Fatal("persisted evidence changed")
162 }
163 f.e.After = nil
164 if err := f.e.Run(t.Context(), f.w); err != nil {
165 t.Fatalf("later verification: %v", err)
166 }
167 }
168 f.done(t)
169 })
170 }
171 }
172}
173
174func TestBindMovesScope(t *testing.T) {
175 f := newFixture(t)
176 obs, err := hostclaim.Read(t.Context(), f.e.Claims, f.w.Host)
177 if err != nil {
178 t.Fatal(err)
179 }
180 _, err = hostclaim.Update(t.Context(), f.e.Claims, obs, func(c *hostclaim.Claim) error {
181 c.Obligations[0].Park = nil
182 return nil
183 })
184 if err != nil {
185 t.Fatal(err)
186 }
187 p := *f.ob.Park
188 p.Location = "moves/" + f.w.Host + "/move-1"
189 authorized := false
190 f.e.Authorize = func(_ context.Context, _ hostclaim.Observed, o hostclaim.Obligation) error {
191 authorized = reflect.DeepEqual(o.Park, &p)
192 return nil
193 }
194 if err := f.e.Bind(t.Context(), f.w, p); err != nil {
195 t.Fatal(err)
196 }
197 obs, err = hostclaim.Read(t.Context(), f.e.Claims, f.w.Host)
198 if err != nil {
199 t.Fatal(err)
200 }
201 if !authorized || !reflect.DeepEqual(obs.Claim.Obligations[0].Park, &p) {
202 t.Fatal("staging scope not authorized and persisted")
203 }
204}
205