the board answers on a phone, and a pinned-back sidecar is not the service

The table is wider than a phone and had no box to scroll in, so the page itself
scrolled sideways and carried the header and nav with it: a reader could see
that something was wrong but not what, which is the one thing the four columns
exist to say.

And two containers can share a repository at different versions on purpose — a
sidecar held back while the app moves on. Reading whichever came last reported
the pin unapplied on a service whose app container was carrying it, so the board
named drift where there was none. A workload runs the declared version if any of
its containers does.
This commit is contained in:
zooqueen
2026-08-17 09:17:59 -07:00
parent d05bc6dbbc
commit 7253cc21ea
4 changed files with 46 additions and 0 deletions
+2
View File
@@ -189,6 +189,7 @@ var fleetTmpl = template.Must(template.New("fleet").Funcs(template.FuncMap{
</div>{{end}} </div>{{end}}
{{if .Services}} {{if .Services}}
<div class="scroll">
<table> <table>
<thead><tr> <thead><tr>
<th>Service</th> <th>Service</th>
@@ -233,6 +234,7 @@ var fleetTmpl = template.Must(template.New("fleet").Funcs(template.FuncMap{
</tr> </tr>
{{end}} {{end}}
</tbody></table> </tbody></table>
</div>
{{else}} {{else}}
<div class="empty"> <div class="empty">
No services visible.<br> No services visible.<br>
+5
View File
@@ -39,6 +39,11 @@ nav .who{margin-left:auto;align-self:center;color:var(--text-muted);font-size:va
background:color-mix(in srgb, var(--warning) 10%, transparent); background:color-mix(in srgb, var(--warning) 10%, transparent);
color:var(--warning-light);font-size:var(--font-size-sm)} color:var(--warning-light);font-size:var(--font-size-sm)}
/* The four versions are the answer, so they must survive a narrow screen. The
table scrolls inside its own box; without one the page scrolls sideways and
takes the header and nav with it, leaving a reader able to see that something
is wrong but not what. */
.scroll{overflow-x:auto;-webkit-overflow-scrolling:touch}
table{width:100%;border-collapse:collapse} table{width:100%;border-collapse:collapse}
th{position:sticky;top:0;background:var(--surface-1);text-align:left; th{position:sticky;top:0;background:var(--surface-1);text-align:left;
font-size:var(--font-size-xs);text-transform:uppercase;letter-spacing:.06em; font-size:var(--font-size-xs);text-transform:uppercase;letter-spacing:.06em;
+10
View File
@@ -277,6 +277,16 @@ func assemble(pins []pin, lives []live, links map[string]link) []Service {
// service declares; its sidecars are not the service. // service declares; its sidecars are not the service.
continue continue
} }
// A workload runs the declared version if ANY of its containers does.
// Two containers can share the repository and hold different versions on
// purpose — a sidecar pinned back while the app moves on — and taking
// whichever the loop reached last reported the pin unapplied on a service
// whose app container was carrying it. Preferring the match answers the
// question the column asks: is what we declared running here. For the
// single-container case, which is nearly every row, nothing changes.
if same, decided := alike(s.Running, s.Declared); decided && same {
continue
}
s.Running, s.Ready, s.Want = l.Version, l.Ready, l.Want s.Running, s.Ready, s.Want = l.Version, l.Ready, l.Want
} }
for _, s := range byName { for _, s := range byName {
+29
View File
@@ -337,6 +337,35 @@ func TestOneImageIsSeveralServices(t *testing.T) {
} }
} }
// TestSidecarOnTheSameImageDoesNotHideThePin — two containers can share the
// repository and hold different versions deliberately, and taking whichever the
// loop reached last reported the pin unapplied on a service whose app container
// was carrying it. studio pins its build-manifest sidecar back to v0.19.26 while
// the app runs v0.19.31; the board called that drift and named a service that was
// in fact current.
func TestSidecarOnTheSameImageDoesNotHideThePin(t *testing.T) {
studio := "ghcr.io/hanzoai/studio"
pins := []pin{{Name: "studio", Namespace: "hanzo", Image: studio,
Version: Version{Tag: "v0.19.31"}}}
// Sidecar last, so a last-writer-wins read takes the pinned-back one.
lives := []live{
{Name: "studio", Namespace: "hanzo", Image: studio, Version: Version{Tag: "v0.19.31"}, Ready: 1, Want: 1},
{Name: "studio", Namespace: "hanzo", Image: studio, Version: Version{Tag: "v0.19.26"}, Ready: 1, Want: 1},
}
got := assemble(pins, lives, map[string]link{})
if len(got) != 1 {
t.Fatalf("assembled %d rows from one declaration: %+v", len(got), got)
}
if got[0].Running.Tag != "v0.19.31" {
t.Errorf("running = %q; a workload runs the declared version if any container does",
got[0].Running.Tag)
}
if !got[0].current() {
t.Errorf("drift = %v; the pin was applied and the app container carries it", got[0].Drift)
}
}
// TestRunningMatchesOnNameAndImage — a workload of the right name still has to be // TestRunningMatchesOnNameAndImage — a workload of the right name still has to be
// running the image the service declares, so a sidecar is never read as the // running the image the service declares, so a sidecar is never read as the
// service. // service.