| index 02572cff0..e59e9d29b 100644 |
| --- a/internal/release/adopt_test.go |
| +++ b/internal/release/adopt_test.go |
| @@ -768,9 +768,16 @@ func TestEnsureStagedWritesTheSiblingManifest(t *testing.T) { |
| t.Fatalf("Ship: %v", err) |
| } |
| |
| - if err := ensureStaged(ctx, s, opt, "bab", "keymail", "v1abc", "keymail.example.com"); err != nil { |
| + inHand, err := ensureStaged(ctx, s, opt, "bab", "keymail", "v1abc", "keymail.example.com") |
| + if err != nil { |
| t.Fatalf("ensureStaged: %v", err) |
| } |
| + // The manifest ensureStaged HAS IN HAND is what adoptHost's converge |
| + // builds its plan from (Task 7's first pin) — not a re-read of the |
| + // sibling it just wrote. |
| + if inHand == nil || inHand.Capabilities == nil || inHand.Capabilities.Command != "relay" { |
| + t.Fatalf("ensureStaged returned %+v, want the fetched manifest with its capability block", inHand) |
| + } |
| |
| got, err := ReadStagedManifest(opt, "bab", "v1abc", "keymail") |
| if err != nil { |
| @@ -822,9 +829,13 @@ func TestEnsureStagedBackfillsASiblingForAnAlreadyStagedBinary(t *testing.T) { |
| t.Fatalf("sibling should be absent before backfill: %+v, %v", m2, err) |
| } |
| |
| - if err := ensureStaged(ctx, s, opt, "bab", "keymail", "v1abc", "keymail.example.com"); err != nil { |
| + inHand, err := ensureStaged(ctx, s, opt, "bab", "keymail", "v1abc", "keymail.example.com") |
| + if err != nil { |
| t.Fatalf("ensureStaged (backfill): %v", err) |
| } |
| + if inHand == nil || inHand.Capabilities == nil || !inHand.Capabilities.Daemon { |
| + t.Fatalf("back-fill returned %+v, want the fetched manifest in hand", inHand) |
| + } |
| |
| got, err := ReadStagedManifest(opt, "bab", "v1abc", "keymail") |
| if err != nil { |
| @@ -879,9 +890,17 @@ func TestEnsureStagedSiblingWriteFailureIsNotFatalToStagingTheBinary(t *testing. |
| t.Fatalf("pre-create tmp block: %v", err) |
| } |
| |
| - if err := ensureStaged(ctx, s, opt, "bab", "keymail", "v1abc", "h.example.com"); err != nil { |
| + inHand, err := ensureStaged(ctx, s, opt, "bab", "keymail", "v1abc", "h.example.com") |
| + if err != nil { |
| t.Fatalf("ensureStaged returned an error for a failed SIBLING write: %v — the binary staged fine and that must not be fatal", err) |
| } |
| + // Task 7's first pin, in its sharpest form: the sibling write FAILED, so |
| + // a converge that re-read the sibling would see (nil, nil) and leave the |
| + // previous version's drop-ins in place under a new binary. The manifest |
| + // this function returns is the fetched one and is unaffected. |
| + if inHand == nil || inHand.Capabilities == nil || !inHand.Capabilities.Daemon { |
| + t.Fatalf("ensureStaged returned %+v after a failed sibling write, want the fetched manifest still in hand", inHand) |
| + } |
| |
| body, err := os.ReadFile(VersionPath(opt, "bab", "v1abc", "keymail")) |
| if err != nil || string(body) != "relay binary" { |
| @@ -909,9 +928,17 @@ func TestEnsureStagedBackfillFetchFailureIsNotFatal(t *testing.T) { |
| t.Fatalf("StageArtifact: %v", err) |
| } |
| |
| - if err := ensureStaged(ctx, s, opt, "bab", "keymail", "v1abc", "h.example.com"); err != nil { |
| + inHand, err := ensureStaged(ctx, s, opt, "bab", "keymail", "v1abc", "h.example.com") |
| + if err != nil { |
| t.Fatalf("ensureStaged returned an error for an already-staged binary's failed back-fill fetch: %v — adoption of an already-running version must proceed", err) |
| } |
| + // This is the ONE path with no manifest to give. It stays (nil, nil) so |
| + // ordinary adoption proceeds; a capability-armed pass turns that nil into |
| + // a refusal of the version rather than a silent leave-alone — see |
| + // TestArmedPassRefusesAVersionWhoseManifestItCouldNotObtain. |
| + if inHand != nil { |
| + t.Fatalf("ensureStaged returned %+v, want nil (no manifest could be obtained)", inHand) |
| + } |
| |
| if got, err := ReadStagedManifest(opt, "bab", "v1abc", "keymail"); err != nil || got != nil { |
| t.Fatalf("sibling = %+v, %v; want absent (the fetch failed, nothing to write)", got, err) |
| @@ -978,3 +1005,690 @@ func TestPassBackfillsTheLiveVersionsSiblingOnAnUnchangedPass(t *testing.T) { |
| t.Fatalf("live version moved unexpectedly: %q", v) |
| } |
| } |
| + |
| +// ---- Task 7: the capability converge inside adoptHost ----------------------- |
| + |
| +// capsRecorder is a tiny in-memory systemd for the capability tests in this |
| +// package, plus the one thing internal/caps's own fake cannot observe: WHERE |
| +// THE LIVE SYMLINK POINTED at the moment each verb ran. That is how the |
| +// ordering rule — converge before PointLive — is asserted as a fact about |
| +// this pass rather than as a reading of the code. |
| +type capsRecorder struct { |
| + opt, host string |
| + calls []string |
| + liveAt map[string]string // verb+unit -> LiveVersion at its FIRST call |
| + active map[string]bool |
| + enabled map[string]bool |
| + fail map[string]error // "verb unit" -> error |
| +} |
| + |
| +func newCapsRecorder(opt, host string) *capsRecorder { |
| + return &capsRecorder{ |
| + opt: opt, host: host, |
| + liveAt: map[string]string{}, |
| + active: map[string]bool{}, |
| + enabled: map[string]bool{}, |
| + fail: map[string]error{}, |
| + } |
| +} |
| + |
| +func (r *capsRecorder) run(args ...string) error { |
| + call := strings.Join(args, " ") |
| + r.calls = append(r.calls, call) |
| + if _, seen := r.liveAt[call]; !seen { |
| + v, _ := LiveVersion(r.opt, r.host) |
| + r.liveAt[call] = v |
| + } |
| + if err := r.fail[call]; err != nil { |
| + return err |
| + } |
| + if len(args) >= 2 { |
| + switch args[0] { |
| + case "start", "restart": |
| + r.active[args[1]] = true |
| + case "stop": |
| + r.active[args[1]] = false |
| + case "enable": |
| + r.enabled[args[1]] = true |
| + } |
| + } |
| + return nil |
| +} |
| + |
| +func (r *capsRecorder) query(args ...string) bool { |
| + if len(args) < 2 { |
| + return false |
| + } |
| + switch args[0] { |
| + case "is-active": |
| + return r.active[args[1]] |
| + case "is-enabled": |
| + return r.enabled[args[1]] |
| + } |
| + return false |
| +} |
| + |
| +func (r *capsRecorder) called(call string) bool { |
| + for _, c := range r.calls { |
| + if c == call { |
| + return true |
| + } |
| + } |
| + return false |
| +} |
| + |
| +// capsTestOps wires every caps seam at a temp dir, so a Pass in this package |
| +// touches no systemd, no /etc, no /run and no /var/lib. Logf is left nil on |
| +// purpose: converge's own lines then go to os.Stdout with every other adopt |
| +// line, which is what withCapturedStdout asserts against. |
| +func capsTestOps(t *testing.T, r *capsRecorder) (caps.Ops, string) { |
| + t.Helper() |
| + etc := t.TempDir() |
| + return caps.Ops{ |
| + EtcDir: etc, |
| + StateRoot: t.TempDir(), |
| + RunDir: t.TempDir(), |
| + Systemctl: r.run, |
| + Query: r.query, |
| + EnsureUser: func(string) error { return nil }, |
| + }, etc |
| +} |
| + |
| +// addCapRoute adds a route with a backing and a granted capability column — |
| +// addRoute plus the two columns Task 7 reads. |
| +func addCapRoute(t *testing.T, reg *registry.Registry, host, app, channel, backing, grants string, hibernate, platform bool) { |
| + t.Helper() |
| + if err := reg.Upsert(registry.Route{ |
| + Host: host, Kind: "instance", App: app, Channel: channel, |
| + Hibernate: hibernate, Backing: backing, Caps: grants, Platform: platform, |
| + }); err != nil { |
| + t.Fatalf("upsert %s: %v", host, err) |
| + } |
| +} |
| + |
| +// shipCapRelease is shipRelease with a capability block. |
| +func shipCapRelease(t *testing.T, s Store, acct, app, version string, set *caps.Set, body []byte) { |
| + t.Helper() |
| + m := Manifest{ |
| + App: app, Version: version, Kind: "binary", |
| + Artifacts: map[string]Artifact{adoptPlatform: {}}, |
| + Capabilities: set, |
| + } |
| + if err := Ship(context.Background(), s, acct, m, map[string][]byte{adoptPlatform: body}); err != nil { |
| + t.Fatalf("Ship %s/%s: %v", app, version, err) |
| + } |
| +} |
| + |
| +func relaySet() *caps.Set { |
| + return &caps.Set{ |
| + V: 1, Command: "relay", Daemon: true, RuntimeDir: "keymail", |
| + Ports: []caps.Port{{Port: 25, Name: "smtp"}}, |
| + } |
| +} |
| + |
| +// TestAdoptConvergesCapabilitiesBeforePointLive is the ordering rule of spec |
| +// §4d, asserted where it can actually fail: the drop-ins are written and |
| +// daemon-reloaded while the live symlink still names the OLD version, and the |
| +// daemon's restart happens after it names the new one. Converge after |
| +// PointLive would leave a window where the new binary is live under the old |
| +// unit shape; converge without a reload would leave bytes systemd never read. |
| +func TestAdoptConvergesCapabilitiesBeforePointLive(t *testing.T) { |
| + s := NewDirStore(t.TempDir()) |
| + ctx := context.Background() |
| + priv, pub := testKeys(t) |
| + opt := t.TempDir() |
| + reg := testRegistry(t) |
| + host := "keymail.dev" |
| + |
| + // The box is up on v1, which declares nothing. |
| + shipRelease(t, s, "bab", "keymail", "v1", adoptPlatform, []byte("old relay")) |
| + if err := StageArtifact(opt, "bab", "v1", "keymail", []byte("old relay")); err != nil { |
| + t.Fatal(err) |
| + } |
| + addCapRoute(t, reg, host, "keymail", "canary/x", registry.BackingUnit, "port:25", false, false) |
| + if err := PointLive(opt, host, "bab", "v1", "keymail"); err != nil { |
| + t.Fatal(err) |
| + } |
| + |
| + set := relaySet() |
| + shipCapRelease(t, s, "bab", "keymail", "v2", set, []byte("new relay")) |
| + if err := Promote(ctx, s, priv, "bab", "keymail", "canary/x", "v2"); err != nil { |
| + t.Fatalf("Promote v2: %v", err) |
| + } |
| + |
| + rec := newCapsRecorder(opt, host) |
| + o, etc := capsTestOps(t, rec) |
| + var restarted []string |
| + restart := func(h string) error { restarted = append(restarted, h); return nil } |
| + if err := Pass(ctx, reg, s, pub, opt, restart, WithCapabilities(o)); err != nil { |
| + t.Fatalf("Pass: %v", err) |
| + } |
| + |
| + if v, _ := LiveVersion(opt, host); v != "v2" { |
| + t.Fatalf("live = %q, want v2", v) |
| + } |
| + if v, seen := rec.liveAt["daemon-reload"]; !seen || v != "v1" { |
| + t.Fatalf("daemon-reload observed live=%q (seen=%v), want v1 — the drop-ins must be loaded BEFORE the symlink moves", v, seen) |
| + } |
| + |
| + // The bytes are the renderer's, byte for byte — this test pins the |
| + // wiring, not a second copy of the drop-in format. |
| + g := caps.Grants{Ports: map[int]bool{25: true}} |
| + svcPath, sockPath := caps.DropInPaths(etc, host) |
| + wantSvc, err := caps.RenderService("keymail", set, g) |
| + if err != nil { |
| + t.Fatalf("RenderService: %v", err) |
| + } |
| + gotSvc, err := os.ReadFile(svcPath) |
| + if err != nil || !bytes.Equal(gotSvc, wantSvc) { |
| + t.Fatalf("service drop-in = %q (%v), want %q", gotSvc, err, wantSvc) |
| + } |
| + wantSock, err := caps.RenderSocket(set, g) |
| + if err != nil { |
| + t.Fatalf("RenderSocket: %v", err) |
| + } |
| + gotSock, err := os.ReadFile(sockPath) |
| + if err != nil || !bytes.Equal(gotSock, wantSock) { |
| + t.Fatalf("socket drop-in = %q (%v), want %q", gotSock, err, wantSock) |
| + } |
| + |
| + // A converged DAEMON's restart is caps.Restart, not the caller's |
| + // try-restart callback — that is what makes its failure real enough to |
| + // roll back (restartRoute swallows every error it sees). |
| + if len(restarted) != 0 { |
| + t.Fatalf("restart callback called for %v; a converged daemon's restart is caps.Restart's", restarted) |
| + } |
| + svcRestart := "restart " + caps.ServiceUnit(host) |
| + if v, seen := rec.liveAt[svcRestart]; !seen || v != "v2" { |
| + t.Fatalf("%q observed live=%q (seen=%v), want v2 — the daemon restarts onto the NEW binary", svcRestart, v, seen) |
| + } |
| +} |
| + |
| +// TestRestartFailureRestoresBothTheSymlinkAndTheDropIns is the rollback that |
| +// keeps a bad build off a mail server: when the daemon will not come back on |
| +// the new version, the box must end the pass exactly where it started — old |
| +// symlink, old unit text, the version poisoned for the rest of the pass. |
| +// Restoring one without the other is the divergence this pairing exists to |
| +// prevent (bytes that say :465 under a binary that never asked for it). |
| +func TestRestartFailureRestoresBothTheSymlinkAndTheDropIns(t *testing.T) { |
| + s := NewDirStore(t.TempDir()) |
| + ctx := context.Background() |
| + priv, pub := testKeys(t) |
| + opt := t.TempDir() |
| + reg := testRegistry(t) |
| + host := "a.example.com" |
| + |
| + v1 := &caps.Set{V: 1, Command: "relay", Daemon: true, Ports: []caps.Port{{Port: 25}}} |
| + v2 := &caps.Set{V: 1, Command: "relay", Daemon: true, Ports: []caps.Port{{Port: 25}, {Port: 465}}} |
| + shipCapRelease(t, s, "bab", "keymail", "v1", v1, []byte("good relay")) |
| + shipCapRelease(t, s, "bab", "keymail", "v2", v2, []byte("bad relay")) |
| + if err := Promote(ctx, s, priv, "bab", "keymail", "canary/x", "v1"); err != nil { |
| + t.Fatalf("Promote v1: %v", err) |
| + } |
| + addCapRoute(t, reg, host, "keymail", "canary/x", registry.BackingUnit, "port:25,port:465", false, false) |
| + // b is exec-backed (materializes nothing) and exists only to prove the |
| + // poison: it must never be moved onto the version a's restart rejected. |
| + addCapRoute(t, reg, "b.example.com", "keymail", "canary/x", "", "", false, false) |
| + |
| + rec := newCapsRecorder(opt, host) |
| + o, etc := capsTestOps(t, rec) |
| + if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }, WithCapabilities(o)); err != nil { |
| + t.Fatalf("Pass v1: %v", err) |
| + } |
| + svcPath, sockPath := caps.DropInPaths(etc, host) |
| + wantSvc, err := os.ReadFile(svcPath) |
| + if err != nil { |
| + t.Fatalf("read converged service drop-in: %v", err) |
| + } |
| + wantSock, err := os.ReadFile(sockPath) |
| + if err != nil { |
| + t.Fatalf("read converged socket drop-in: %v", err) |
| + } |
| + if !strings.Contains(string(wantSock), "ListenStream=25") || strings.Contains(string(wantSock), "465") { |
| + t.Fatalf("v1 socket drop-in = %q, want :25 only", wantSock) |
| + } |
| + |
| + if err := Promote(ctx, s, priv, "bab", "keymail", "canary/x", "v2"); err != nil { |
| + t.Fatalf("Promote v2: %v", err) |
| + } |
| + rec.fail["restart "+caps.ServiceUnit(host)] = errors.New("relay failed to start") |
| + |
| + var out bytes.Buffer |
| + withCapturedStdout(t, &out, func() { |
| + if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }, WithCapabilities(o)); err != nil { |
| + t.Fatalf("Pass v2: %v", err) |
| + } |
| + }) |
| + |
| + if v, _ := LiveVersion(opt, host); v != "v1" { |
| + t.Fatalf("live = %q, want v1 — the symlink must be rolled back", v) |
| + } |
| + gotSvc, _ := os.ReadFile(svcPath) |
| + gotSock, _ := os.ReadFile(sockPath) |
| + if !bytes.Equal(gotSvc, wantSvc) { |
| + t.Fatalf("service drop-in = %q, want v1's %q", gotSvc, wantSvc) |
| + } |
| + if !bytes.Equal(gotSock, wantSock) { |
| + t.Fatalf("socket drop-in = %q, want v1's %q — a rolled-back version must not leave :465 bound", gotSock, wantSock) |
| + } |
| + // Bytes are not a binding: restoring the socket half has to re-run the |
| + // socket sequence, or systemd keeps the listener the rolled-back version |
| + // asked for. |
| + if !rec.called("restart " + caps.SocketUnit(host)) { |
| + t.Fatalf("calls = %v, want the socket unit re-bound during the restore", rec.calls) |
| + } |
| + if v, _ := LiveVersion(opt, "b.example.com"); v != "v1" { |
| + t.Fatalf("b live = %q, want v1 — the failed version must be poisoned for the rest of the pass", v) |
| + } |
| + line := out.String() |
| + if !strings.Contains(line, "rolled the symlink back") { |
| + t.Fatalf("printed %q, want a line saying the symlink was rolled back", line) |
| + } |
| + if !strings.Contains(line, "capability drop-ins") { |
| + t.Fatalf("printed %q, want a line saying the drop-ins were restored too", line) |
| + } |
| +} |
| + |
| +// TestExecBackedRouteWithCapabilitiesMaterializesNothing: Backing == "" is |
| +// the whole existing fleet. A release declaring the full relay set must |
| +// change no unit text on such a route (its process is an exec child, not a |
| +// carlos-app@ unit) while adoption itself proceeds exactly as before. This |
| +// is also why no existing test in this package can reach the converge path. |
| +func TestExecBackedRouteWithCapabilitiesMaterializesNothing(t *testing.T) { |
| + s := NewDirStore(t.TempDir()) |
| + ctx := context.Background() |
| + priv, pub := testKeys(t) |
| + opt := t.TempDir() |
| + reg := testRegistry(t) |
| + host := "exec.example.com" |
| + |
| + shipCapRelease(t, s, "bab", "keymail", "v1", relaySet(), []byte("relay")) |
| + if err := Promote(ctx, s, priv, "bab", "keymail", "canary/x", "v1"); err != nil { |
| + t.Fatalf("Promote: %v", err) |
| + } |
| + addCapRoute(t, reg, host, "keymail", "canary/x", "", "port:25", false, false) |
| + |
| + rec := newCapsRecorder(opt, host) |
| + o, etc := capsTestOps(t, rec) |
| + var restarted []string |
| + if err := Pass(ctx, reg, s, pub, opt, func(h string) error { |
| + restarted = append(restarted, h) |
| + return nil |
| + }, WithCapabilities(o)); err != nil { |
| + t.Fatalf("Pass: %v", err) |
| + } |
| + |
| + assertEmptyDir(t, etc, "an exec-backed route must materialize no unit text") |
| + if len(rec.calls) != 0 { |
| + t.Fatalf("systemctl calls = %v, want none", rec.calls) |
| + } |
| + if v, _ := LiveVersion(opt, host); v != "v1" { |
| + t.Fatalf("live = %q, want v1 — adoption itself must proceed", v) |
| + } |
| + if len(restarted) != 1 || restarted[0] != host { |
| + t.Fatalf("restarted = %v, want the ordinary callback for %s", restarted, host) |
| + } |
| +} |
| + |
| +// TestPlatformAliasRouteMaterializesNothing: a platform alias is a second |
| +// name for one process. A second carlos-app@ unit claiming :25 would break |
| +// the real one, so an alias materializes nothing — and, being outside |
| +// adoptable(), is not adopted at all. |
| +func TestPlatformAliasRouteMaterializesNothing(t *testing.T) { |
| + s := NewDirStore(t.TempDir()) |
| + ctx := context.Background() |
| + priv, pub := testKeys(t) |
| + opt := t.TempDir() |
| + reg := testRegistry(t) |
| + host := "keymail.bab.apps.example.com" |
| + |
| + shipCapRelease(t, s, "bab", "keymail", "v1", relaySet(), []byte("relay")) |
| + if err := Promote(ctx, s, priv, "bab", "keymail", "canary/x", "v1"); err != nil { |
| + t.Fatalf("Promote: %v", err) |
| + } |
| + addCapRoute(t, reg, host, "keymail", "canary/x", registry.BackingUnit, "port:25", false, true) |
| + |
| + rec := newCapsRecorder(opt, host) |
| + o, etc := capsTestOps(t, rec) |
| + if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }, WithCapabilities(o)); err != nil { |
| + t.Fatalf("Pass: %v", err) |
| + } |
| + |
| + assertEmptyDir(t, etc, "a platform alias must materialize no unit text") |
| + if len(rec.calls) != 0 { |
| + t.Fatalf("systemctl calls = %v, want none", rec.calls) |
| + } |
| + if v, _ := LiveVersion(opt, host); v != "" { |
| + t.Fatalf("live = %q, want unset — a platform alias is not adopted at all", v) |
| + } |
| +} |
| + |
| +// TestPassWithoutWithCapabilitiesIsUnchanged is the byte-identical-to-before |
| +// assertion, and the reason this task could land on its own: a unit-backed |
| +// route with a full capability manifest, adopted by a Pass that was never |
| +// given the option, writes no unit text anywhere and adopts exactly as it |
| +// always did. |
| +func TestPassWithoutWithCapabilitiesIsUnchanged(t *testing.T) { |
| + s := NewDirStore(t.TempDir()) |
| + ctx := context.Background() |
| + priv, pub := testKeys(t) |
| + opt := t.TempDir() |
| + reg := testRegistry(t) |
| + host := "unit.example.com" |
| + etc := t.TempDir() // stands in for /etc/systemd/system; nothing may reach it |
| + |
| + shipCapRelease(t, s, "bab", "keymail", "v1", relaySet(), []byte("relay")) |
| + if err := Promote(ctx, s, priv, "bab", "keymail", "canary/x", "v1"); err != nil { |
| + t.Fatalf("Promote: %v", err) |
| + } |
| + addCapRoute(t, reg, host, "keymail", "canary/x", registry.BackingUnit, "port:25", false, false) |
| + |
| + var restarted []string |
| + if err := Pass(ctx, reg, s, pub, opt, func(h string) error { |
| + restarted = append(restarted, h) |
| + return nil |
| + }); err != nil { |
| + t.Fatalf("Pass: %v", err) |
| + } |
| + |
| + assertEmptyDir(t, etc, "a Pass without WithCapabilities must write no unit text") |
| + if v, _ := LiveVersion(opt, host); v != "v1" { |
| + t.Fatalf("live = %q, want v1", v) |
| + } |
| + if len(restarted) != 1 || restarted[0] != host { |
| + t.Fatalf("restarted = %v, want the ordinary callback for %s", restarted, host) |
| + } |
| +} |
| + |
| +// TestUnconvergedRepointOfACapabilityRouteLogsTheWindow pins the 2026-08-03 |
| +// ruling on the canary fast loop. That loop runs inside carlos-edge.service |
| +// (ProtectSystem=strict) and CANNOT write /etc/systemd/system, so it repoints |
| +// a capability-bearing canary and leaves the unit shape to the hourly root |
| +// pass — a window of up to an hour where a new binary runs under the previous |
| +// version's drop-ins. The window is accepted; being silent about it is not. |
| +// |
| +// The signal is deliberately the cheapest honest one: "the new version's |
| +// manifest declares capabilities at all", not "the new set differs from what |
| +// is materialized" — the difference test needs the grants and a render of |
| +// both sides, and this line exists to mark the window, not to diff it. |
| +func TestUnconvergedRepointOfACapabilityRouteLogsTheWindow(t *testing.T) { |
| + s := NewDirStore(t.TempDir()) |
| + ctx := context.Background() |
| + priv, pub := testKeys(t) |
| + opt := t.TempDir() |
| + reg := testRegistry(t) |
| + |
| + shipCapRelease(t, s, "bab", "keymail", "v1", relaySet(), []byte("relay")) |
| + shipRelease(t, s, "bab", "plain", "v1", adoptPlatform, []byte("plain")) |
| + for _, app := range []string{"keymail", "plain"} { |
| + if err := Promote(ctx, s, priv, "bab", app, "canary/x", "v1"); err != nil { |
| + t.Fatalf("Promote %s: %v", app, err) |
| + } |
| + } |
| + addCapRoute(t, reg, "caps.example.com", "keymail", "canary/x", registry.BackingUnit, "port:25", false, false) |
| + // A unit-backed route whose release declares NOTHING must stay quiet — |
| + // otherwise the line fires on every ordinary tenant and nobody reads it. |
| + addCapRoute(t, reg, "plain.example.com", "plain", "canary/x", registry.BackingUnit, "", false, false) |
| + |
| + var out bytes.Buffer |
| + withCapturedStdout(t, &out, func() { |
| + if err := PassFilter(ctx, reg, s, pub, opt, func(string) error { return nil }, isCanaryTestRoute); err != nil { |
| + t.Fatalf("PassFilter: %v", err) |
| + } |
| + }) |
| + |
| + line := out.String() |
| + if !strings.Contains(line, "caps.example.com: v1 declares capabilities") { |
| + t.Fatalf("printed %q, want one line naming the unconverged capability repoint", line) |
| + } |
| + if !strings.Contains(line, "carlos-adopt.service") { |
| + t.Fatalf("printed %q, want the line to name what closes the window", line) |
| + } |
| + if strings.Contains(line, "plain.example.com: v1 declares") { |
| + t.Fatalf("printed %q, want silence for a route whose release declares nothing", line) |
| + } |
| +} |
| + |
| +// TestHibernatingUnitRouteConvergesWithoutStarting: a hibernating route |
| +// reaches adoptHost with restart == nil. Its drop-ins are unit TEXT, not a |
| +// wake, so they converge — but nothing may start the service, or an hourly |
| +// timer would defeat hibernation. |
| +func TestHibernatingUnitRouteConvergesWithoutStarting(t *testing.T) { |
| + s := NewDirStore(t.TempDir()) |
| + ctx := context.Background() |
| + priv, pub := testKeys(t) |
| + opt := t.TempDir() |
| + reg := testRegistry(t) |
| + host := "sleepy.example.com" |
| + |
| + // No daemon: daemon + hibernate is a contradiction converge refuses. |
| + set := &caps.Set{V: 1, Command: "web", Ports: []caps.Port{{Port: 25}}} |
| + shipCapRelease(t, s, "bab", "keymail", "v1", set, []byte("relay")) |
| + if err := Promote(ctx, s, priv, "bab", "keymail", "canary/x", "v1"); err != nil { |
| + t.Fatalf("Promote: %v", err) |
| + } |
| + addCapRoute(t, reg, host, "keymail", "canary/x", registry.BackingUnit, "port:25", true, false) |
| + |
| + rec := newCapsRecorder(opt, host) |
| + o, etc := capsTestOps(t, rec) |
| + if err := Pass(ctx, reg, s, pub, opt, func(string) error { |
| + t.Fatal("a hibernating host must never reach the restart callback") |
| + return nil |
| + }, WithCapabilities(o)); err != nil { |
| + t.Fatalf("Pass: %v", err) |
| + } |
| + |
| + svcPath, _ := caps.DropInPaths(etc, host) |
| + if _, err := os.Stat(svcPath); err != nil { |
| + t.Fatalf("stat %s: %v — a hibernating route's drop-ins still converge", svcPath, err) |
| + } |
| + if v, _ := LiveVersion(opt, host); v != "v1" { |
| + t.Fatalf("live = %q, want v1", v) |
| + } |
| + for _, c := range rec.calls { |
| + if strings.HasSuffix(c, caps.ServiceUnit(host)) { |
| + t.Fatalf("calls = %v, want no verb against the SERVICE unit — a drop-in is not a wake", rec.calls) |
| + } |
| + } |
| +} |
| + |
| +// TestConvergeRefusalRefusesTheVersion: a refusal from converge falls exactly |
| +// where an ensureStaged failure falls — nothing pointed live, the box keeps |
| +// serving what it has, one line naming the operator's fix. Pointing live |
| +// anyway would run a new binary against a unit shape the platform has just |
| +// declined to write. |
| +// |
| +// The refusal used here is spec §4e's daemon + hibernate contradiction, |
| +// because it is refused with nothing written at all. |
| +func TestConvergeRefusalRefusesTheVersion(t *testing.T) { |
| + s := NewDirStore(t.TempDir()) |
| + ctx := context.Background() |
| + priv, pub := testKeys(t) |
| + opt := t.TempDir() |
| + reg := testRegistry(t) |
| + host := "sleepy.example.com" |
| + |
| + shipRelease(t, s, "bab", "keymail", "v1", adoptPlatform, []byte("old")) |
| + if err := StageArtifact(opt, "bab", "v1", "keymail", []byte("old")); err != nil { |
| + t.Fatal(err) |
| + } |
| + addCapRoute(t, reg, host, "keymail", "canary/x", registry.BackingUnit, "port:25", true, false) |
| + if err := PointLive(opt, host, "bab", "v1", "keymail"); err != nil { |
| + t.Fatal(err) |
| + } |
| + shipCapRelease(t, s, "bab", "keymail", "v2", relaySet(), []byte("new")) |
| + if err := Promote(ctx, s, priv, "bab", "keymail", "canary/x", "v2"); err != nil { |
| + t.Fatalf("Promote v2: %v", err) |
| + } |
| + |
| + rec := newCapsRecorder(opt, host) |
| + o, etc := capsTestOps(t, rec) |
| + var out bytes.Buffer |
| + withCapturedStdout(t, &out, func() { |
| + if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }, WithCapabilities(o)); err != nil { |
| + t.Fatalf("Pass: %v", err) |
| + } |
| + }) |
| + |
| + if v, _ := LiveVersion(opt, host); v != "v1" { |
| + t.Fatalf("live = %q, want v1 — a refused converge must not point live", v) |
| + } |
| + assertEmptyDir(t, etc, "a refusal writes nothing") |
| + if len(rec.calls) != 0 { |
| + t.Fatalf("systemctl calls = %v, want none", rec.calls) |
| + } |
| + line := out.String() |
| + if !strings.Contains(line, "not pointed live") { |
| + t.Fatalf("printed %q, want a line saying the version was not pointed live", line) |
| + } |
| + if !strings.Contains(line, "-hibernate=false") { |
| + t.Fatalf("printed %q, want converge's own line naming the operator's fix", line) |
| + } |
| +} |
| + |
| +// TestArmedPassRefusesAVersionWhoseManifestItCouldNotObtain is the second of |
| +// Task 7's two pins, in the one place it can bite: the binary is staged (so |
| +// ensureStaged succeeds) but its manifest is obtainable neither from the |
| +// sibling nor from the bucket. caps.Plan's nil Set means "leave the drop-ins |
| +// alone" — correct for a pass surveying the box, and exactly wrong here, |
| +// where it would point a new binary at the previous version's unit shape and |
| +// say nothing. So an armed pass refuses the version. |
| +// |
| +// The second half is the compatibility half: the SAME situation on a pass |
| +// with no WithCapabilities adopts normally, which is what keeps every |
| +// existing caller byte-identical. |
| +func TestArmedPassRefusesAVersionWhoseManifestItCouldNotObtain(t *testing.T) { |
| + dir := t.TempDir() |
| + s := NewDirStore(dir) |
| + ctx := context.Background() |
| + priv, pub := testKeys(t) |
| + opt := t.TempDir() |
| + reg := testRegistry(t) |
| + host := "keymail.dev" |
| + |
| + shipRelease(t, s, "bab", "keymail", "v1", adoptPlatform, []byte("old")) |
| + if err := StageArtifact(opt, "bab", "v1", "keymail", []byte("old")); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := StageManifest(opt, "bab", "v1", "keymail", []byte(`{"schema":1,"app":"keymail","version":"v1","kind":"binary"}`)); err != nil { |
| + t.Fatal(err) |
| + } |
| + addCapRoute(t, reg, host, "keymail", "canary/x", registry.BackingUnit, "port:25", false, false) |
| + if err := PointLive(opt, host, "bab", "v1", "keymail"); err != nil { |
| + t.Fatal(err) |
| + } |
| + |
| + // v2's binary is on disk with no sibling, and its manifest object is then |
| + // removed from the store: the back-fill fetch cannot answer either. |
| + shipCapRelease(t, s, "bab", "keymail", "v2", relaySet(), []byte("new")) |
| + if err := Promote(ctx, s, priv, "bab", "keymail", "canary/x", "v2"); err != nil { |
| + t.Fatalf("Promote v2: %v", err) |
| + } |
| + if err := StageArtifact(opt, "bab", "v2", "keymail", []byte("new")); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Remove(filepath.Join(dir, filepath.FromSlash(ManifestKey("bab", "keymail", "v2")))); err != nil { |
| + t.Fatalf("remove v2 manifest: %v", err) |
| + } |
| + |
| + rec := newCapsRecorder(opt, host) |
| + o, etc := capsTestOps(t, rec) |
| + var out bytes.Buffer |
| + withCapturedStdout(t, &out, func() { |
| + if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }, WithCapabilities(o)); err != nil { |
| + t.Fatalf("armed Pass: %v", err) |
| + } |
| + }) |
| + if v, _ := LiveVersion(opt, host); v != "v1" { |
| + t.Fatalf("live = %q, want v1 — an unknown unit shape must refuse the version", v) |
| + } |
| + assertEmptyDir(t, etc, "a refusal writes nothing") |
| + if line := out.String(); !strings.Contains(line, "not pointed live") { |
| + t.Fatalf("printed %q, want a line saying the version was not pointed live", line) |
| + } |
| + |
| + // Unarmed, the identical situation adopts exactly as it always did. |
| + if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }); err != nil { |
| + t.Fatalf("unarmed Pass: %v", err) |
| + } |
| + if v, _ := LiveVersion(opt, host); v != "v2" { |
| + t.Fatalf("live = %q after an unarmed pass, want v2 — capabilities off means behaviour unchanged", v) |
| + } |
| +} |
| + |
| +// assertEmptyDir fails unless dir has no entries at all. |
| +func assertEmptyDir(t *testing.T, dir, why string) { |
| + t.Helper() |
| + ents, err := os.ReadDir(dir) |
| + if err != nil { |
| + t.Fatalf("read %s: %v", dir, err) |
| + } |
| + if len(ents) != 0 { |
| + var names []string |
| + for _, e := range ents { |
| + names = append(names, e.Name()) |
| + } |
| + t.Fatalf("%s contains %v — %s", dir, names, why) |
| + } |
| +} |
| + |
| +// TestAdoptRemovesDropInsWhenTheNewVersionDeclaresNothing is Task 7's second |
| +// pin, and the mutant it kills is a one-word one: Plan{Set: m.Capabilities}. |
| +// Manifest.Capabilities is a nil pointer for BOTH a legacy manifest and a |
| +// fetch that never happened, and caps.Plan reads a nil Set as "leave the |
| +// drop-ins exactly as they are". Passing it through verbatim would mean a |
| +// capability DROPPED from a manifest is never removed — a relay that stopped |
| +// asking for :25 would keep binding it, which is the exact opposite of spec |
| +// §4d's removal semantics. A manifest that RESOLVED always carries a Set, |
| +// empty when it declares nothing. |
| +func TestAdoptRemovesDropInsWhenTheNewVersionDeclaresNothing(t *testing.T) { |
| + s := NewDirStore(t.TempDir()) |
| + ctx := context.Background() |
| + priv, pub := testKeys(t) |
| + opt := t.TempDir() |
| + reg := testRegistry(t) |
| + host := "keymail.dev" |
| + |
| + shipCapRelease(t, s, "bab", "keymail", "v1", relaySet(), []byte("relay")) |
| + if err := Promote(ctx, s, priv, "bab", "keymail", "canary/x", "v1"); err != nil { |
| + t.Fatalf("Promote v1: %v", err) |
| + } |
| + addCapRoute(t, reg, host, "keymail", "canary/x", registry.BackingUnit, "port:25", false, false) |
| + |
| + rec := newCapsRecorder(opt, host) |
| + o, etc := capsTestOps(t, rec) |
| + if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }, WithCapabilities(o)); err != nil { |
| + t.Fatalf("Pass v1: %v", err) |
| + } |
| + svcPath, sockPath := caps.DropInPaths(etc, host) |
| + for _, p := range []string{svcPath, sockPath} { |
| + if _, err := os.Stat(p); err != nil { |
| + t.Fatalf("stat %s after v1: %v", p, err) |
| + } |
| + } |
| + |
| + // v2 declares nothing at all — a plain manifest, exactly like every |
| + // release in every production bucket today. |
| + shipRelease(t, s, "bab", "keymail", "v2", adoptPlatform, []byte("plain relay")) |
| + if err := Promote(ctx, s, priv, "bab", "keymail", "canary/x", "v2"); err != nil { |
| + t.Fatalf("Promote v2: %v", err) |
| + } |
| + if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }, WithCapabilities(o)); err != nil { |
| + t.Fatalf("Pass v2: %v", err) |
| + } |
| + |
| + if v, _ := LiveVersion(opt, host); v != "v2" { |
| + t.Fatalf("live = %q, want v2", v) |
| + } |
| + for _, p := range []string{svcPath, sockPath} { |
| + if _, err := os.Stat(p); !os.IsNotExist(err) { |
| + b, _ := os.ReadFile(p) |
| + t.Fatalf("%s still exists after a version that declares nothing (%q) — a dropped capability must be removed", p, b) |
| + } |
| + } |
| + // Removing the socket half has to reach systemd, or the listener the |
| + // removed file asked for is still bound. |
| + if !rec.called("restart " + caps.SocketUnit(host)) { |
| + t.Fatalf("calls = %v, want the socket unit re-bound when its drop-in was removed", rec.calls) |
| + } |
| +} |