carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package release
2
3import (
4 "context"
5 "os"
6 "path/filepath"
7 "reflect"
8 "strings"
9 "testing"
10
11 "github.com/carlosframework/platform/internal/registry"
12)
13
14func TestBoxPinsNamesLiveTargetsAndRegistryRows(t *testing.T) {
15 opt := t.TempDir()
16 for _, v := range []string{"v1", "v2"} {
17 if err := StageArtifact(opt, "bab", v, "km", []byte(v)); err != nil {
18 t.Fatal(err)
19 }
20 }
21 if err := StageArtifact(opt, "kqmv", "v3", "km", []byte("v3")); err != nil {
22 t.Fatal(err)
23 }
24 if err := PointLive(opt, "a.example.com", "bab", "v1", "km"); err != nil {
25 t.Fatal(err)
26 }
27 routes := []registry.Route{
28 {Host: "b.example.com", Kind: "instance", App: "km", Version: "v2", PrevVersion: "v1"},
29 {Host: "c.example.com", Kind: "sidecar", App: "km", Account: "kqmv", Version: "v3"},
30 {Host: "d.example.com", Kind: "instance", App: "", Version: "v9"}, // no app: names no staged path
31 }
32 pins, err := BoxPins(opt, routes)
33 if err != nil {
34 t.Fatalf("BoxPins: %v", err)
35 }
36 want := []string{VersionPath(opt, "bab", "v1", "km"), VersionPath(opt, "bab", "v2", "km"), VersionPath(opt, "kqmv", "v3", "km")}
37 if len(pins) != len(want) {
38 t.Fatalf("BoxPins = %v, want exactly %v", pins, want)
39 }
40 for _, p := range want {
41 if !pins.Has(p) {
42 t.Errorf("BoxPins is missing %s", p)
43 }
44 }
45}
46
47// A crashed PointLive leaves <host>.tmp naming a real staged tree. It is a
48// reference, as it is to LivePins.
49func TestBoxPinsKeepsATmpLeftover(t *testing.T) {
50 opt := t.TempDir()
51 if err := StageArtifact(opt, "bab", "v1", "km", []byte("v1")); err != nil {
52 t.Fatal(err)
53 }
54 if err := os.MkdirAll(filepath.Join(opt, "live"), 0o755); err != nil {
55 t.Fatal(err)
56 }
57 if err := os.Symlink(VersionPath(opt, "bab", "v1", "km"), LiveLink(opt, "a.example.com")+".tmp"); err != nil {
58 t.Fatal(err)
59 }
60 pins, err := BoxPins(opt, nil)
61 if err != nil || !pins.Has(VersionPath(opt, "bab", "v1", "km")) {
62 t.Fatalf("BoxPins = %v, %v; want the leftover's target pinned", pins, err)
63 }
64}
65
66// Losing a pin is how a running build gets overwritten, so an unreadable
67// entry stops the answer rather than narrowing it.
68func TestBoxPinsFailsOnAnUnreadableLink(t *testing.T) {
69 opt := t.TempDir()
70 if err := os.MkdirAll(filepath.Join(opt, "live"), 0o755); err != nil {
71 t.Fatal(err)
72 }
73 if err := os.WriteFile(filepath.Join(opt, "live", "not-a-link"), []byte("x"), 0o644); err != nil {
74 t.Fatal(err)
75 }
76 if _, err := BoxPins(opt, nil); err == nil {
77 t.Fatal("BoxPins read past an entry it could not interpret")
78 }
79}
80
81func TestBoxPinsWithNoLiveDir(t *testing.T) {
82 pins, err := BoxPins(t.TempDir(), nil)
83 if err != nil || len(pins) != 0 {
84 t.Fatalf("BoxPins on a fresh box = %v, %v; want empty and no error", pins, err)
85 }
86}
87
88func TestPinSourceFuncIsAPinSource(t *testing.T) {
89 var src PinSource = PinSourceFunc(func(context.Context, string) (StagedPaths, error) {
90 return StagedPaths{"/opt/carlos/versions/v1/km": true}, nil
91 })
92 got, err := src.StagedPins(context.Background(), "/opt/carlos")
93 if err != nil || !got.Has("/opt/carlos/versions/v1/./km") {
94 t.Fatalf("StagedPins = %v, %v", got, err)
95 }
96}
97
98// The documented format, written by hand, is what ReadGenerations reads.
99func TestReadGenerationsReadsTheDocumentedFormat(t *testing.T) {
100 path := GenerationsPath(t.TempDir())
101 if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
102 t.Fatal(err)
103 }
104 if err := os.WriteFile(path, []byte(`{"schema":1,"hosts":{"a.example.com":["v1","v2"]}}`), 0o644); err != nil {
105 t.Fatal(err)
106 }
107 g, err := ReadGenerations(path)
108 if err != nil || g == nil || !reflect.DeepEqual(g.Hosts, map[string][]string{"a.example.com": {"v1", "v2"}}) {
109 t.Fatalf("ReadGenerations = %+v, %v", g, err)
110 }
111 if !strings.HasSuffix(path, filepath.Join("zdd", "generations.json")) {
112 t.Fatalf("GenerationsPath = %s, want <data dir>/zdd/generations.json", path)
113 }
114}
115
116// Every box before plan 5 has no file: that is no generations, not unknown.
117func TestReadGenerationsAbsentIsNone(t *testing.T) {
118 g, err := ReadGenerations(GenerationsPath(t.TempDir()))
119 if err != nil || g != nil {
120 t.Fatalf("ReadGenerations on a box with no file = %+v, %v; want nil, nil", g, err)
121 }
122}
123
124func TestWriteGenerationsIsAtomicAndReadable(t *testing.T) {
125 dataDir := t.TempDir()
126 path := GenerationsPath(dataDir)
127 want := Generations{Hosts: map[string][]string{"a.example.com": {"v1"}, "b.example.com": {"v2", "v3"}}}
128 if err := WriteGenerations(path, want); err != nil {
129 t.Fatalf("WriteGenerations: %v", err)
130 }
131 got, err := ReadGenerations(path)
132 if err != nil || got.Schema != 1 || !reflect.DeepEqual(got.Hosts, want.Hosts) {
133 t.Fatalf("read back %+v, %v", got, err)
134 }
135 info, err := os.Stat(path)
136 if err != nil || info.Mode().Perm() != 0o644 {
137 t.Fatalf("mode = %v, %v; want 0644 so root and the agent can both read it", info.Mode(), err)
138 }
139 // Ruling R19 item 4 adds the intent file beside it.
140 entries, err := os.ReadDir(filepath.Dir(path))
141 if err != nil || len(entries) != 2 || entries[0].Name() != "generations.json" || entries[1].Name() != "generations.seq" {
142 t.Fatalf("zdd dir holds %v, %v; want only generations.json and generations.seq, no temp file left behind", entries, err)
143 }
144 if err := WriteGenerations(path, Generations{Hosts: map[string][]string{"a.example.com": {"../escape"}}}); err == nil {
145 t.Fatal("WriteGenerations wrote a version that is not a version")
146 }
147 if got, _ := ReadGenerations(path); !reflect.DeepEqual(got.Hosts, want.Hosts) {
148 t.Fatalf("a refused write changed the file: %+v", got)
149 }
150}
151
152func TestGenerationPinsPlaceVersionsUnderTheHostsApp(t *testing.T) {
153 opt, path := t.TempDir(), GenerationsPath(t.TempDir())
154 if err := WriteGenerations(path, Generations{Hosts: map[string][]string{"c.example.com": {"v3", "v4"}}}); err != nil {
155 t.Fatal(err)
156 }
157 rows := func() ([]registry.Route, error) {
158 return []registry.Route{{Host: "c.example.com", Kind: "instance", App: "km", Account: "kqmv"}}, nil
159 }
160 pins, err := GenerationPins(path, rows).StagedPins(context.Background(), opt)
161 if err != nil {
162 t.Fatalf("StagedPins: %v", err)
163 }
164 for _, v := range []string{"v3", "v4"} {
165 if !pins.Has(VersionPath(opt, "kqmv", v, "km")) {
166 t.Errorf("pins %v miss %s", pins, v)
167 }
168 }
169}
170
171// Review Focus 5: a file root cannot trust makes the pin set incomplete.
172func TestGenerationPinsFailClosed(t *testing.T) {
173 rows := func() ([]registry.Route, error) {
174 return []registry.Route{{Host: "a.example.com", Kind: "instance", App: "km"}}, nil
175 }
176 for name, body := range map[string]string{
177 "half-written": `{"schema":1,"hosts":{"a.example.com":["v1"`,
178 "newer schema": `{"schema":2,"hosts":{"a.example.com":["v1"]}}`,
179 "bad version": `{"schema":1,"hosts":{"a.example.com":["../v1"]}}`,
180 "host with no row": `{"schema":1,"hosts":{"gone.example.com":["v1"]}}`,
181 } {
182 t.Run(name, func(t *testing.T) {
183 path := GenerationsPath(t.TempDir())
184 if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
185 t.Fatal(err)
186 }
187 if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
188 t.Fatal(err)
189 }
190 if pins, err := GenerationPins(path, rows).StagedPins(context.Background(), t.TempDir()); err == nil {
191 t.Fatalf("StagedPins = %v, nil; want an error (incomplete pin set)", pins)
192 }
193 })
194 }
195}
196
197// A live link that names another link (an operator's own "current" pointer)
198// refers to every hop of the chain, the staged copy at its end included.
199func TestBoxPinsPinsEveryHopOfAChainedLink(t *testing.T) {
200 opt := t.TempDir()
201 if err := StageArtifact(opt, "bab", "v1", "km", []byte("v1")); err != nil {
202 t.Fatal(err)
203 }
204 current := filepath.Join(t.TempDir(), "operator-current")
205 if err := os.Symlink(VersionPath(opt, "bab", "v1", "km"), current); err != nil {
206 t.Fatal(err)
207 }
208 if err := os.MkdirAll(filepath.Join(opt, "live"), 0o755); err != nil {
209 t.Fatal(err)
210 }
211 if err := os.Symlink(current, LiveLink(opt, "h.example.com")); err != nil {
212 t.Fatal(err)
213 }
214 pins, err := BoxPins(opt, nil)
215 if err != nil {
216 t.Fatalf("BoxPins: %v", err)
217 }
218 for _, p := range []string{current, VersionPath(opt, "bab", "v1", "km")} {
219 if !pins.Has(p) {
220 t.Errorf("BoxPins = %v, missing %s", pins, p)
221 }
222 }
223}
224
225// A chain that cannot be followed to its end is an unknown reference: no
226// answer, so no restage.
227func TestBoxPinsFailsOnALinkLoop(t *testing.T) {
228 opt := t.TempDir()
229 dir := t.TempDir()
230 a, b := filepath.Join(dir, "a"), filepath.Join(dir, "b")
231 if err := os.Symlink(b, a); err != nil {
232 t.Fatal(err)
233 }
234 if err := os.Symlink(a, b); err != nil {
235 t.Fatal(err)
236 }
237 if err := os.MkdirAll(filepath.Join(opt, "live"), 0o755); err != nil {
238 t.Fatal(err)
239 }
240 if err := os.Symlink(a, LiveLink(opt, "h.example.com")); err != nil {
241 t.Fatal(err)
242 }
243 if pins, err := BoxPins(opt, nil); err == nil {
244 t.Fatalf("BoxPins = %v, nil; want an error for a link loop", pins)
245 }
246}
247
248// A relative hop under a symlinked directory pins what the OS reaches, the
249// staged file, along with the hop as written (Astra r2 N3, ruling R17).
250func TestBoxPinsPinsWhatARelativeHopUnderASymlinkedDirReaches(t *testing.T) {
251 opt := t.TempDir()
252 staged, current := aliasedChain(t, opt)
253 pins, err := BoxPins(opt, nil)
254 if err != nil {
255 t.Fatalf("BoxPins: %v", err)
256 }
257 for _, p := range []string{current, staged} {
258 if !pins.Has(p) {
259 t.Errorf("BoxPins = %v, missing %s", pins, p)
260 }
261 }
262}
263
264// Resolving a relative hop's parent must not lose the pin a box with a
265// symlinked opt dir had before: the copy is pinned under the opt dir's own
266// name, which is how VersionPath names it, as well as under its real path.
267func TestBoxPinsRelativeLinkUnderASymlinkedOptDir(t *testing.T) {
268 real := t.TempDir()
269 opt := filepath.Join(t.TempDir(), "opt")
270 if err := os.Symlink(real, opt); err != nil {
271 t.Fatal(err)
272 }
273 if err := StageArtifact(real, "bab", "v1", "km", []byte("v1")); err != nil {
274 t.Fatal(err)
275 }
276 if err := os.MkdirAll(filepath.Join(opt, "live"), 0o755); err != nil {
277 t.Fatal(err)
278 }
279 if err := os.Symlink(filepath.Join("..", "versions", "v1", "km"), LiveLink(opt, "h.example.com")); err != nil {
280 t.Fatal(err)
281 }
282 pins, err := BoxPins(opt, nil)
283 if err != nil {
284 t.Fatalf("BoxPins: %v", err)
285 }
286 for _, p := range []string{VersionPath(opt, "bab", "v1", "km"), VersionPath(real, "bab", "v1", "km")} {
287 if !pins.Has(p) {
288 t.Errorf("BoxPins = %v, missing %s", pins, p)
289 }
290 }
291}
292
293// Ruling R19 item 5: an absolute final hop reached through a symlinked
294// directory is pinned at the fully resolved real path too, the copy's
295// VersionPath here.
296func TestBoxPinsPinsTheRealPathOfTheChainsEnd(t *testing.T) {
297 opt := t.TempDir()
298 if err := StageArtifact(opt, "bab", "v1", "km", []byte("v1")); err != nil {
299 t.Fatal(err)
300 }
301 staged := VersionPath(opt, "bab", "v1", "km")
302 alias := filepath.Join(t.TempDir(), "alias")
303 if err := os.Symlink(filepath.Dir(staged), alias); err != nil {
304 t.Fatal(err)
305 }
306 if err := os.MkdirAll(filepath.Join(opt, "live"), 0o755); err != nil {
307 t.Fatal(err)
308 }
309 if err := os.Symlink(filepath.Join(alias, "km"), LiveLink(opt, "h.example.com")); err != nil {
310 t.Fatal(err)
311 }
312 pins, err := BoxPins(opt, nil)
313 if err != nil {
314 t.Fatalf("BoxPins: %v", err)
315 }
316 for _, p := range []string{filepath.Join(alias, "km"), staged} {
317 if !pins.Has(p) {
318 t.Errorf("BoxPins = %v, missing %s", pins, p)
319 }
320 }
321}
322
323// Refers compares a candidate's real path; a candidate that does not exist
324// is compared as written.
325func TestStagedPathsRefersComparesTheRealPath(t *testing.T) {
326 real := t.TempDir()
327 opt := filepath.Join(t.TempDir(), "opt")
328 if err := os.Symlink(real, opt); err != nil {
329 t.Fatal(err)
330 }
331 if err := StageArtifact(real, "bab", "v1", "km", []byte("v1")); err != nil {
332 t.Fatal(err)
333 }
334 pins := StagedPaths{}
335 pins.add(VersionPath(real, "bab", "v1", "km"))
336 if ok, err := pins.Refers(VersionPath(opt, "bab", "v1", "km")); err != nil || !ok {
337 t.Fatalf("Refers(the copy under the symlinked opt) = %v, %v; want true", ok, err)
338 }
339 if ok, err := pins.Refers(VersionPath(opt, "bab", "v9", "km")); err != nil || ok {
340 t.Fatalf("Refers(a missing copy) = %v, %v; want false, nil", ok, err)
341 }
342}
343
344// Spec §8.5: the good build is a pin. It is what a failed first wake falls
345// back to and what a hold's recovery boots, long after the row's Version and
346// PrevVersion have moved past it.
347func TestBoxPinsNamesTheGoodBuild(t *testing.T) {
348 opt := t.TempDir()
349 if err := StageArtifact(opt, "bab", "v0", "km", []byte("v0")); err != nil {
350 t.Fatal(err)
351 }
352 pins, err := BoxPins(opt, []registry.Route{{Host: "b.example.com", Kind: "instance", App: "km", Account: "bab", Version: "v2", PrevVersion: "v1", GoodVersion: "v0"}})
353 if err != nil {
354 t.Fatal(err)
355 }
356 if !pins.Has(VersionPath(opt, "bab", "v0", "km")) {
357 t.Fatal("GoodVersion is not a pin (spec §8.5)")
358 }
359}
360