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 "bytes"
5 "context"
6 "crypto/ed25519"
7 "errors"
8 "os"
9 "path/filepath"
10 "strings"
11 "testing"
12 "time"
13
14 "github.com/carlosframework/platform/internal/registry"
15)
16
17// reuseFixture: h1 follows canary/x and serves v1. Then v2 is shipped as
18// "new v2" while a stale copy of v2 ("old v2", from an earlier release under
19// the same name that retention has since pruned) is already staged, and the
20// channel moves to v2.
21type reuseFixture struct {
22 s Store
23 priv ed25519.PrivateKey
24 pub ed25519.PublicKey
25 opt string
26 reg *registry.Registry
27}
28
29func newReuseFixture(t *testing.T) reuseFixture {
30 t.Helper()
31 return newReuseFixtureAt(t, t.TempDir())
32}
33
34// newReuseFixtureAt is newReuseFixture with the opt dir given.
35func newReuseFixtureAt(t *testing.T, opt string) reuseFixture {
36 t.Helper()
37 ctx := context.Background()
38 f := reuseFixture{s: NewDirStore(t.TempDir()), opt: opt, reg: testRegistry(t)}
39 f.priv, f.pub = testKeys(t)
40 shipRelease(t, f.s, "bab", "myapp", "v1", adoptPlatform, []byte("v1"))
41 if err := Promote(ctx, f.s, f.priv, "bab", "myapp", "canary/x", "v1"); err != nil {
42 t.Fatal(err)
43 }
44 addRoute(t, f.reg, "h1.example.com", "myapp", "canary/x", false)
45 if err := Pass(ctx, f.reg, f.s, f.pub, f.opt, func(string) error { return nil }); err != nil {
46 t.Fatal(err)
47 }
48 if v, _ := LiveVersion(f.opt, "h1.example.com"); v != "v1" {
49 t.Fatalf("setup: live = %q, want v1", v)
50 }
51 if err := StageArtifact(f.opt, "bab", "v2", "myapp", []byte("old v2")); err != nil {
52 t.Fatal(err)
53 }
54 shipRelease(t, f.s, "bab", "myapp", "v2", adoptPlatform, []byte("new v2"))
55 if err := Promote(ctx, f.s, f.priv, "bab", "myapp", "canary/x", "v2"); err != nil {
56 t.Fatal(err)
57 }
58 return f
59}
60
61func (f reuseFixture) pass(t *testing.T, opts ...PassOption) string {
62 t.Helper()
63 var out bytes.Buffer
64 withCapturedStdout(t, &out, func() {
65 if err := Pass(context.Background(), f.reg, f.s, f.pub, f.opt, func(string) error { return nil }, opts...); err != nil {
66 t.Fatalf("Pass: %v", err)
67 }
68 })
69 return out.String()
70}
71
72func (f reuseFixture) stagedV2(t *testing.T) string {
73 t.Helper()
74 b, err := os.ReadFile(VersionPath(f.opt, "bab", "v2", "myapp"))
75 if err != nil {
76 t.Fatal(err)
77 }
78 return string(b)
79}
80
81// assertRefused: h1 stays on v1, the stale copy is untouched, and the pass
82// said why.
83func (f reuseFixture) assertRefused(t *testing.T, out, why string) {
84 t.Helper()
85 if got := f.stagedV2(t); got != "old v2" {
86 t.Fatalf("the referenced copy was overwritten: it now holds %q", got)
87 }
88 if v, _ := LiveVersion(f.opt, "h1.example.com"); v != "v1" {
89 t.Fatalf("h1 live = %q, want v1: the reused name must not install", v)
90 }
91 if rt, _ := f.reg.Get("h1.example.com"); rt == nil || rt.Version != "v1" {
92 t.Fatalf("h1 row = %+v, want Version v1", rt)
93 }
94 if !strings.Contains(out, why) {
95 t.Fatalf("pass printed %q, want it to contain %q", out, why)
96 }
97}
98
99// Spec §12 "Restage refusal": an unreferenced stale copy is restaged.
100func TestUnreferencedStaleCopyIsRestaged(t *testing.T) {
101 f := newReuseFixture(t)
102 out := f.pass(t)
103 if got := f.stagedV2(t); got != "new v2" {
104 t.Fatalf("staged v2 = %q, want the canonical bytes", got)
105 }
106 if v, _ := LiveVersion(f.opt, "h1.example.com"); v != "v2" {
107 t.Fatalf("h1 live = %q, want v2", v)
108 }
109 raw, _, err := f.s.Get(context.Background(), ManifestKey("bab", "myapp", "v2"))
110 if err != nil {
111 t.Fatal(err)
112 }
113 sib, err := os.ReadFile(StagedManifestPath(f.opt, "bab", "v2", "myapp"))
114 if err != nil || !bytes.Equal(sib, raw) {
115 t.Fatalf("sibling = %s, %v; want the canonical manifest bytes", sib, err)
116 }
117 if !strings.Contains(out, "restaging") {
118 t.Fatalf("pass printed %q; want a line saying the copy was restaged", out)
119 }
120}
121
122// A row's PrevVersion is a reference (spec §4.2's pin set).
123func TestStaleCopyARegistryRowNamesIsNotOverwritten(t *testing.T) {
124 f := newReuseFixture(t)
125 // h2 is mid-provision, so no pass adopts it, but its row still names v2.
126 if err := f.reg.Upsert(registry.Route{Host: "h2.example.com", Kind: "instance", App: "myapp", Channel: "canary/x", Status: "creating"}); err != nil {
127 t.Fatal(err)
128 }
129 if err := f.reg.SetVersions("h2.example.com", "v1", "v2"); err != nil {
130 t.Fatal(err)
131 }
132 f.assertRefused(t, f.pass(t), ErrVersionReused.Error())
133}
134
135func TestStaleCopyALiveLinkNamesIsNotOverwritten(t *testing.T) {
136 f := newReuseFixture(t)
137 if err := PointLive(f.opt, "h2.example.com", "bab", "v2", "myapp"); err != nil {
138 t.Fatal(err)
139 }
140 f.assertRefused(t, f.pass(t), ErrVersionReused.Error())
141}
142
143// A live link that reaches the stale copy through another link still refers
144// to it: every hop of the chain is a reference.
145func TestStaleCopyAChainedLiveLinkNamesIsNotOverwritten(t *testing.T) {
146 f := newReuseFixture(t)
147 current := filepath.Join(t.TempDir(), "operator-current")
148 if err := os.Symlink(VersionPath(f.opt, "bab", "v2", "myapp"), current); err != nil {
149 t.Fatal(err)
150 }
151 if err := os.Symlink(current, LiveLink(f.opt, "h2.example.com")); err != nil {
152 t.Fatal(err)
153 }
154 f.assertRefused(t, f.pass(t), ErrVersionReused.Error())
155}
156
157// GoodVersion and live generations arrive through a PinSource (plan 5).
158func TestStaleCopyAPinSourceNamesIsNotOverwritten(t *testing.T) {
159 f := newReuseFixture(t)
160 src := PinSourceFunc(func(context.Context, string) (StagedPaths, error) {
161 return StagedPaths{VersionPath(f.opt, "bab", "v2", "myapp"): true}, nil
162 })
163 f.assertRefused(t, f.pass(t, WithPinSource(src)), ErrVersionReused.Error())
164}
165
166// A generation running v2 on another host, as plan 5's agent records it.
167func TestAGenerationsFileEntryProtectsAStagedCopy(t *testing.T) {
168 f := newReuseFixture(t)
169 if err := f.reg.Upsert(registry.Route{Host: "h2.example.com", Kind: "instance", App: "myapp", Channel: "canary/x", Status: "creating"}); err != nil {
170 t.Fatal(err)
171 }
172 dataDir := t.TempDir()
173 if err := WriteGenerations(GenerationsPath(dataDir), Generations{Hosts: map[string][]string{"h2.example.com": {"v2"}}}); err != nil {
174 t.Fatal(err)
175 }
176 f.assertRefused(t, f.pass(t, BakeDir(dataDir)), ErrVersionReused.Error())
177}
178
179// Review Focus 5: a generations file root cannot read is an incomplete pin
180// set, and no restage happens that pass.
181func TestAnUnreadableGenerationsFileNeverPermitsARestage(t *testing.T) {
182 f := newReuseFixture(t)
183 dataDir := t.TempDir()
184 path := GenerationsPath(dataDir)
185 if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
186 t.Fatal(err)
187 }
188 if err := os.WriteFile(path, []byte(`{"schema":1,"hosts":{"h2.exa`), 0o644); err != nil {
189 t.Fatal(err)
190 }
191 f.assertRefused(t, f.pass(t, BakeDir(dataDir)), "what refers to it is unknown")
192}
193
194// Every box before plan 5: no file is no generations, and an unreferenced
195// stale copy is restaged as usual.
196func TestAnAbsentGenerationsFileIsNoGenerations(t *testing.T) {
197 f := newReuseFixture(t)
198 f.pass(t, BakeDir(t.TempDir()))
199 if got := f.stagedV2(t); got != "new v2" {
200 t.Fatalf("staged v2 = %q, want it restaged", got)
201 }
202}
203
204func TestUnknownReferencesNeverPermitARestage(t *testing.T) {
205 f := newReuseFixture(t)
206 src := PinSourceFunc(func(context.Context, string) (StagedPaths, error) {
207 return nil, errors.New("generation report unreadable")
208 })
209 f.assertRefused(t, f.pass(t, WithPinSource(src)), "what refers to it is unknown")
210}
211
212// Two hosts moving onto one stale build in one pass: restaged once, both move.
213func TestOneRestageServesEveryHostInThePass(t *testing.T) {
214 f := newReuseFixture(t)
215 addRoute(t, f.reg, "h3.example.com", "myapp", "canary/x", false)
216 out := f.pass(t)
217 if n := strings.Count(out, "restaging"); n != 1 {
218 t.Fatalf("restaged %d times, want once:\n%s", n, out)
219 }
220 for _, h := range []string{"h1.example.com", "h3.example.com"} {
221 if v, _ := LiveVersion(f.opt, h); v != "v2" {
222 t.Fatalf("%s live = %q, want v2", h, v)
223 }
224 }
225}
226
227// Unchanged behaviour: a reused copy that matches is not downloaded again.
228func TestMatchingStagedCopyIsReusedWithoutADownload(t *testing.T) {
229 s := NewDirStore(t.TempDir())
230 ctx := context.Background()
231 priv, pub := testKeys(t)
232 opt := t.TempDir()
233 reg := testRegistry(t)
234 shipRelease(t, s, "bab", "myapp", "v2", adoptPlatform, []byte("v2 bytes"))
235 if err := Promote(ctx, s, priv, "bab", "myapp", "canary/x", "v2"); err != nil {
236 t.Fatal(err)
237 }
238 if err := StageArtifact(opt, "bab", "v2", "myapp", []byte("v2 bytes")); err != nil {
239 t.Fatal(err)
240 }
241 old := time.Unix(1_600_000_000, 0)
242 bin := VersionPath(opt, "bab", "v2", "myapp")
243 if err := os.Chtimes(bin, old, old); err != nil {
244 t.Fatal(err)
245 }
246 addRoute(t, reg, "h1.example.com", "myapp", "canary/x", false)
247 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
248 t.Fatal(err)
249 }
250 if v, _ := LiveVersion(opt, "h1.example.com"); v != "v2" {
251 t.Fatalf("live = %q, want v2", v)
252 }
253 if info, err := os.Stat(bin); err != nil || !info.ModTime().Equal(old) {
254 t.Fatalf("the matching copy was rewritten (mtime %v, %v)", info.ModTime(), err)
255 }
256}
257
258// Review Focus 3: when the bucket cannot answer for the manifest, a staged
259// copy keeps today's answer. It is used, never restaged, and nothing claims a
260// mismatch the box could not establish.
261func TestUnreadableManifestKeepsTodaysAnswerForAStagedCopy(t *testing.T) {
262 dir := t.TempDir()
263 s := NewDirStore(dir)
264 ctx := context.Background()
265 priv, pub := testKeys(t)
266 opt := t.TempDir()
267 reg := testRegistry(t)
268 shipRelease(t, s, "bab", "myapp", "v2", adoptPlatform, []byte("new v2"))
269 if err := Promote(ctx, s, priv, "bab", "myapp", "canary/x", "v2"); err != nil {
270 t.Fatal(err)
271 }
272 if err := StageArtifact(opt, "bab", "v2", "myapp", []byte("old v2")); err != nil {
273 t.Fatal(err)
274 }
275 if err := os.Remove(filepath.Join(dir, filepath.FromSlash(ManifestKey("bab", "myapp", "v2")))); err != nil {
276 t.Fatal(err)
277 }
278 addRoute(t, reg, "h1.example.com", "myapp", "canary/x", false)
279 var out bytes.Buffer
280 withCapturedStdout(t, &out, func() {
281 if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil {
282 t.Fatal(err)
283 }
284 })
285 if v, _ := LiveVersion(opt, "h1.example.com"); v != "v2" {
286 t.Fatalf("live = %q, want v2 (today's answer)", v)
287 }
288 b, _ := os.ReadFile(VersionPath(opt, "bab", "v2", "myapp"))
289 if string(b) != "old v2" {
290 t.Fatalf("staged v2 = %q: nothing may be restaged from a manifest the box could not read", b)
291 }
292 if strings.Contains(out.String(), "restaging") || strings.Contains(out.String(), ErrVersionReused.Error()) {
293 t.Fatalf("pass claimed a mismatch it could not establish:\n%s", out.String())
294 }
295}
296
297// The held stage has no registry rows, generations file or pin source in hand,
298// so it cannot know what refers to a staged copy. A copy that differs from its
299// release is refused and left as it was; only a full pass restages it.
300func TestStageHeldNeverRestagesADifferingCopy(t *testing.T) {
301 ctx, s, opt := heldFixture(t)
302 if err := StageArtifact(opt, "bab", "v1", "console", []byte("old v1")); err != nil {
303 t.Fatal(err)
304 }
305 m, err := StageHeld(ctx, s, heldPub, nr(t), opt, heldRoute(), "v1")
306 if err == nil || m != nil {
307 t.Fatalf("StageHeld = %+v, %v; want a refusal", m, err)
308 }
309 if !strings.Contains(err.Error(), "what refers to it is unknown") {
310 t.Fatalf("error = %q, want it to say what refers to the copy is unknown", err)
311 }
312 b, rerr := os.ReadFile(VersionPath(opt, "bab", "v1", "console"))
313 if rerr != nil || string(b) != "old v1" {
314 t.Fatalf("staged v1 = %q, %v; want it untouched", b, rerr)
315 }
316}
317
318// Astra r3 N4, ruling R19 item 5: the opt dir is itself a symlink
319// (/opt/carlos -> /srv/carlos), and live/h2 reaches the stale v2 through two
320// relative hops: live/h2 -> ../current, current -> versions/v2/bab/myapp.
321// The hops resolve under the real dir. The pass now refuses the symlinked
322// opt directory before writing its bake clock, preserving the staged copy.
323func TestStaleCopyTwoRelativeHopsUnderASymlinkedOptDirNameIsNotOverwritten(t *testing.T) {
324 real := t.TempDir()
325 opt := filepath.Join(t.TempDir(), "opt")
326 if err := os.Symlink(real, opt); err != nil {
327 t.Fatal(err)
328 }
329 f := newReuseFixtureAt(t, real)
330 f.opt = opt
331 rel, err := filepath.Rel(opt, VersionPath(opt, "bab", "v2", "myapp"))
332 if err != nil {
333 t.Fatal(err)
334 }
335 if err := os.Symlink(rel, filepath.Join(opt, "current")); err != nil {
336 t.Fatal(err)
337 }
338 if err := os.Symlink(filepath.Join("..", "current"), LiveLink(opt, "h2.example.com")); err != nil {
339 t.Fatal(err)
340 }
341 pins, err := BoxPins(opt, nil)
342 if err != nil {
343 t.Fatalf("BoxPins: %v", err)
344 }
345 if pins.Has(VersionPath(opt, "bab", "v2", "myapp")) {
346 t.Fatalf("setup: BoxPins = %v already names the copy lexically; the test would not exercise the real-path check", pins)
347 }
348 err = Pass(context.Background(), f.reg, f.s, f.pub, f.opt, func(string) error { return nil })
349 if err == nil {
350 t.Fatal("pass accepted a symlinked opt directory")
351 }
352 f.opt = real
353 f.assertRefused(t, err.Error(), "record bake clock")
354}
355
356// sleeperRow is a held sleeper of app the pass does not adopt (mid
357// provision), whose only reference to its builds is its settings and build
358// state copy: no Version, PrevVersion, GoodVersion, live link or generation.
359func (f reuseFixture) sleeperRow(t *testing.T, host, app string, u UpdateSettings, bs BuildStateCopy) {
360 t.Helper()
361 if err := f.reg.Upsert(registry.Route{Host: host, Kind: "instance", App: app, Channel: "canary/x", Status: "creating",
362 Hibernate: true, Socket: "/run/carlos/" + host + ".sock"}); err != nil {
363 t.Fatal(err)
364 }
365 if err := f.reg.SetUpdateSettings(host, u.Column()); err != nil {
366 t.Fatal(err)
367 }
368 if err := f.reg.SetBuildState(host, bs.Column()); err != nil {
369 t.Fatal(err)
370 }
371}
372
373// Astra round 1 finding 2 (§4.1 retained identity): a sleeper last ran v2,
374// then received cold installs of later builds, so only its build state names
375// v2. Another instance adopting a promoted reuse of the name v2 must not
376// replace that copy (executable and sibling): the restaging pin set holds
377// every build a settings pin, a last_executed or a resolved T names.
378func TestStaleCopyABuildStateOrPinNamesIsNotOverwritten(t *testing.T) {
379 manual := UpdateSettings{Updates: UpdatesManual, Channel: "canary/x"}
380 pinned := UpdateSettings{Updates: UpdatesAuto, Channel: "canary/x", Pin: "v2"}
381 for name, c := range map[string]struct {
382 u UpdateSettings
383 bs BuildStateCopy
384 }{
385 "last_executed": {manual, BuildStateCopy{Known: true, Replica: true, State: &BuildState{LastExecuted: "v2"}}},
386 "resolved T": {manual, BuildStateCopy{Known: true, Replica: true, State: &BuildState{LastExecuted: "v1", Resolved: &ResolvedRequest{ID: 3, Target: "v2"}}}},
387 "pin": {pinned, BuildStateCopy{Known: true, Replica: true, State: &BuildState{LastExecuted: "v1"}}},
388 } {
389 t.Run(name, func(t *testing.T) {
390 f := newReuseFixture(t)
391 sib, err := os.ReadFile(StagedManifestPath(f.opt, "bab", "v2", "myapp"))
392 if err != nil && !os.IsNotExist(err) {
393 t.Fatal(err)
394 }
395 f.sleeperRow(t, "h2.example.com", "myapp", c.u, c.bs)
396 f.assertRefused(t, f.pass(t), ErrVersionReused.Error())
397 if now, _ := os.ReadFile(StagedManifestPath(f.opt, "bab", "v2", "myapp")); !bytes.Equal(now, sib) {
398 t.Fatalf("the referenced copy's sibling was replaced: %q, was %q", now, sib)
399 }
400 })
401 }
402}
403
404// Finding 2's completeness half: a held row of the same app whose copy cannot
405// be read completely (never read, or its last refresh failed) refuses the
406// restage, since its last_executed may be the reused name. A row of another
407// app with the same problem does not block this one.
408func TestAnIncompleteBuildStateRefusesARestageOnlyForItsApp(t *testing.T) {
409 manual := UpdateSettings{Updates: UpdatesManual, Channel: "canary/x"}
410 for name, bs := range map[string]BuildStateCopy{
411 "never read": {},
412 "refresh failed": {Known: true, Replica: true, ReadErr: "s3: timeout", State: &BuildState{LastExecuted: "v1"}},
413 } {
414 t.Run(name, func(t *testing.T) {
415 f := newReuseFixture(t)
416 f.sleeperRow(t, "h2.example.com", "myapp", manual, bs)
417 f.assertRefused(t, f.pass(t), "what refers to it is unknown")
418 })
419 t.Run(name+", another app", func(t *testing.T) {
420 f := newReuseFixture(t)
421 f.sleeperRow(t, "h3.example.com", "otherapp", manual, bs)
422 f.pass(t)
423 if got := f.stagedV2(t); got != "new v2" {
424 t.Fatalf("staged v2 = %q: another app's unreadable build state blocked this app's restage", got)
425 }
426 })
427 }
428}
429