carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package registry
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "path/filepath"
8 "testing"
9)
10
11// oldShape returns raw JSON with the four rollout fields deleted from the
12// route object, as a binary from before they existed would have written it.
13// route is the object holding them: the document itself, or the value under
14// the given key.
15func oldShape(t *testing.T, raw []byte, key string) []byte {
16 t.Helper()
17 var doc map[string]json.RawMessage
18 if err := json.Unmarshal(raw, &doc); err != nil {
19 t.Fatal(err)
20 }
21 target := doc
22 if key != "" {
23 target = map[string]json.RawMessage{}
24 if err := json.Unmarshal(doc[key], &target); err != nil {
25 t.Fatal(err)
26 }
27 }
28 for _, f := range []string{"GoodVersion", "GoodDigest", "GoodDemand", "ServingVersion"} {
29 if _, ok := target[f]; !ok {
30 t.Fatalf("fixture has no %s to delete", f)
31 }
32 delete(target, f)
33 }
34 if key != "" {
35 b, err := json.Marshal(target)
36 if err != nil {
37 t.Fatal(err)
38 }
39 doc[key] = b
40 }
41 out, err := json.Marshal(doc)
42 if err != nil {
43 t.Fatal(err)
44 }
45 return out
46}
47
48// A cold handover retired by a binary without the rollout fields must still
49// restore after the upgrade, at both comparison sites (the retirement archive
50// and the saved handover body), while a route whose fields differ stays
51// refused: the comparison is an ownership proof and is only canonicalised,
52// never loosened.
53func TestRestoreClaimRowsAcceptsPreUpgradeBytes(t *testing.T) {
54 t.Setenv("CARLOS_HOST_CLAIM", "1")
55 ctx := context.Background()
56 oldOwner := ClaimOwnership{"inc", 7, "installation-7", "route-7"}
57 newOwner := ClaimOwnership{"inc", 8, "installation-8", "route-8"}
58
59 // setup retires a route holding good (and whatever the route carries)
60 // and returns the registry plus the old and next rows.
61 setup := func(t *testing.T, good string) (*Registry, ColdRows, ColdRows) {
62 r, err := Open(filepath.Join(t.TempDir(), "registry.db"))
63 if err != nil {
64 t.Fatal(err)
65 }
66 t.Cleanup(func() { r.Close() })
67 route := Route{Host: "shop.example", App: "app", ClaimOwnership: oldOwner}
68 if err = r.Upsert(route); err != nil {
69 t.Fatal(err)
70 }
71 if good != "" {
72 if err = r.SetGoodBuild(route.Host, GoodBuild{}, GoodBuild{Version: good, Digest: "aa", Demand: "swap"}); err != nil {
73 t.Fatal(err)
74 }
75 }
76 current, err := r.Get(route.Host)
77 if err != nil || current == nil {
78 t.Fatalf("Get: %v", err)
79 }
80 old := ColdRows{Route: *current}
81 next := ColdRows{Route: *current}
82 next.Route.ClaimOwnership = newOwner
83 targets := []ClaimRow{{Kind: "route", Host: route.Host, Ownership: oldOwner}}
84 if err = r.RetireClaimRows(ctx, "ret", targets, func([]ClaimRowArchive) error { return nil }); err != nil {
85 t.Fatal(err)
86 }
87 return r, old, next
88 }
89 // rewriteArchive replaces the stored retirement archive's route contents
90 // with the pre-upgrade shape.
91 rewriteArchive := func(t *testing.T, r *Registry) {
92 var raw []byte
93 if err := r.sql.QueryRow(`SELECT archive FROM claim_row_retirements WHERE journal='ret'`).Scan(&raw); err != nil {
94 t.Fatal(err)
95 }
96 var a ClaimRowArchive
97 if err := json.Unmarshal(raw, &a); err != nil {
98 t.Fatal(err)
99 }
100 a.Contents = oldShape(t, a.Contents, "")
101 raw, err := json.Marshal(a)
102 if err != nil {
103 t.Fatal(err)
104 }
105 if _, err = r.sql.Exec(`UPDATE claim_row_retirements SET archive=? WHERE journal='ret'`, raw); err != nil {
106 t.Fatal(err)
107 }
108 }
109
110 t.Run("archive site accepts the old shape", func(t *testing.T) {
111 r, old, next := setup(t, "")
112 rewriteArchive(t, r)
113 if err := r.RestoreClaimRows(ctx, "h1", "ret", old, next); err != nil {
114 t.Fatalf("pre-upgrade archive refused: %v", err)
115 }
116 })
117 t.Run("archive site refuses a route whose rollout fields differ", func(t *testing.T) {
118 r, old, next := setup(t, "")
119 rewriteArchive(t, r)
120 old.Route.GoodVersion, next.Route.GoodVersion = "v9", "v9"
121 if err := r.RestoreClaimRows(ctx, "h1", "ret", old, next); !errors.Is(err, ErrClaimOwnership) {
122 t.Fatalf("err = %v, want ErrClaimOwnership", err)
123 }
124 })
125 t.Run("archive site still compares current-shape bytes exactly", func(t *testing.T) {
126 r, old, next := setup(t, "v1")
127 if err := r.RestoreClaimRows(ctx, "h1", "ret", old, next); err != nil {
128 t.Fatalf("current-shape archive refused: %v", err)
129 }
130 r2, old2, next2 := setup(t, "v1")
131 old2.Route.GoodVersion, next2.Route.GoodVersion = "v2", "v2"
132 if err := r2.RestoreClaimRows(ctx, "h1", "ret", old2, next2); !errors.Is(err, ErrClaimOwnership) {
133 t.Fatalf("err = %v, want ErrClaimOwnership", err)
134 }
135 })
136 t.Run("replay site accepts the old shape", func(t *testing.T) {
137 r, old, next := setup(t, "")
138 if err := r.RestoreClaimRows(ctx, "h1", "ret", old, next); err != nil {
139 t.Fatal(err)
140 }
141 var body []byte
142 if err := r.sql.QueryRow(`SELECT body FROM cold_handovers WHERE id='h1'`).Scan(&body); err != nil {
143 t.Fatal(err)
144 }
145 if _, err := r.sql.Exec(`UPDATE cold_handovers SET body=? WHERE id='h1'`, oldShape(t, body, "Route")); err != nil {
146 t.Fatal(err)
147 }
148 if err := r.RestoreClaimRows(ctx, "h1", "ret", old, next); err != nil {
149 t.Fatalf("pre-upgrade handover body refused on replay: %v", err)
150 }
151 })
152 t.Run("replay site refuses a route whose rollout fields differ", func(t *testing.T) {
153 r, old, next := setup(t, "")
154 if err := r.RestoreClaimRows(ctx, "h1", "ret", old, next); err != nil {
155 t.Fatal(err)
156 }
157 var body []byte
158 if err := r.sql.QueryRow(`SELECT body FROM cold_handovers WHERE id='h1'`).Scan(&body); err != nil {
159 t.Fatal(err)
160 }
161 if _, err := r.sql.Exec(`UPDATE cold_handovers SET body=? WHERE id='h1'`, oldShape(t, body, "Route")); err != nil {
162 t.Fatal(err)
163 }
164 old.Route.GoodVersion, next.Route.GoodVersion = "v9", "v9"
165 if err := r.RestoreClaimRows(ctx, "h1", "ret", old, next); !errors.Is(err, ErrClaimOwnership) {
166 t.Fatalf("err = %v, want ErrClaimOwnership", err)
167 }
168 })
169 t.Run("replay site fails closed on an undecodable body", func(t *testing.T) {
170 r, old, next := setup(t, "")
171 if err := r.RestoreClaimRows(ctx, "h1", "ret", old, next); err != nil {
172 t.Fatal(err)
173 }
174 if _, err := r.sql.Exec(`UPDATE cold_handovers SET body=? WHERE id='h1'`, []byte("not json")); err != nil {
175 t.Fatal(err)
176 }
177 if err := r.RestoreClaimRows(ctx, "h1", "ret", old, next); !errors.Is(err, ErrClaimOwnership) {
178 t.Fatalf("err = %v, want ErrClaimOwnership", err)
179 }
180 })
181}
182