carlos / platform Public

zdd-6 task 13: carlos sidecars ls shows the serving build

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 14e0b7be912591c4aea7ca5a7bc4e4ceb883f436 parent eede32f
6 files changed, +68 −29
  • internal/console/apiinstances.go +5 −1
  • internal/console/apisidecars.go +9 −11
  • internal/console/apisidecars_test.go +9 −9
  • internal/status/snapshot.go +18 −1
  • internal/status/snapshot_test.go +21 −0
  • sidecars_cli.go +6 −7
diff --git a/internal/console/apiinstances.go b/internal/console/apiinstances.go
index 36f82cf3a..6a1a123d3 100644
--- a/internal/console/apiinstances.go
+++ b/internal/console/apiinstances.go
@@ -336,6 +336,7 @@ func (s *Server) healthByHost(ctx context.Context, routeFor map[string]registry.
// disagreement no reader could diagnose.
type boxJoin struct {
versions map[string]string
+ serving map[string]string
upstreams map[string][]status.UpstreamState
dbBytes map[string]int64
rollouts map[string]rolloutView
@@ -429,7 +430,7 @@ func loadBoxJoin(ctx context.Context, s release.Store, now time.Time) boxJoin {
// joinBoxes is loadBoxJoin's fold over snapshots already read, so a page that
// has its boxes reads them once.
func joinBoxes(boxes []status.Box, now time.Time) boxJoin {
- j := boxJoin{versions: map[string]string{}, upstreams: map[string][]status.UpstreamState{},
+ j := boxJoin{versions: map[string]string{}, serving: map[string]string{}, upstreams: map[string][]status.UpstreamState{},
dbBytes: map[string]int64{}, rollouts: map[string]rolloutView{}}
for _, b := range boxes {
if b.Metrics == nil || status.BoxStale(b, now, status.SnapshotPeriod) {
@@ -438,6 +439,9 @@ func joinBoxes(boxes []status.Box, now time.Time) boxJoin {
for host, v := range b.Metrics.RouteVersions {
j.versions[host] = v
}
+ for host, v := range b.Metrics.RouteServing {
+ j.serving[host] = v
+ }
for host, ups := range b.Metrics.Upstreams {
j.upstreams[host] = ups
}
diff --git a/internal/console/apisidecars.go b/internal/console/apisidecars.go
index e6a08b12a..67f9a491a 100644
--- a/internal/console/apisidecars.go
+++ b/internal/console/apisidecars.go
@@ -119,16 +119,14 @@ func (s *Server) sidecarManifest(ctx context.Context, w http.ResponseWriter, acc
// meant to touch.
type sidecarListRecord struct {
provision.SidecarRecord
- // RunningVersion is what the sidecar's WORKER last reported, read from
- // whichever box carries its route via that box's own
- // status/boxes/<label>.json (loadBoxJoin, which drops a box that has gone
- // stale rather than letting it answer as current). For a sidecar this is
- // specifically the version stamped by the worker's own /v1/ready call
- // (activator's SetVersions) — the adopt pass never stamps a sidecar row,
- // because release.Adoptable excludes them — so a sidecar that has never
- // reported reads EMPTY here and must be rendered "unknown", never as the
- // channel's desired version. A stamp that overclaims is worse than an
- // absent one.
+ // RunningVersion is the build the agent last started for the sidecar
+ // (registry ServingVersion, zero-downtime plan 5), read from whichever
+ // box carries its route via status/boxes/<label>.json's RouteServing,
+ // under loadBoxJoin's staleness rule. NOT RouteVersions: since plan 4
+ // that is root's install stamp, which can name a build nothing runs yet,
+ // and the worker's own /v1/ready report is display metadata only. A
+ // sidecar that has not started anywhere reads EMPTY and is rendered
+ // "unknown", never as the channel's desired version.
RunningVersion string `json:"running_version"`
}
@@ -151,7 +149,7 @@ func (s *Server) handleAPISidecarList(w http.ResponseWriter, r *http.Request) {
join := loadBoxJoin(ctx, s.cfg.Releases, time.Now())
out := make([]sidecarListRecord, 0, len(recs))
for _, rec := range recs {
- out = append(out, sidecarListRecord{SidecarRecord: rec, RunningVersion: join.versions[rec.Host]})
+ out = append(out, sidecarListRecord{SidecarRecord: rec, RunningVersion: join.serving[rec.Host]})
}
if s.cfg.HostClaim {
pending, err := s.claimPendingViews(ctx, acct.Sqid, app, "sidecar")
diff --git a/internal/console/apisidecars_test.go b/internal/console/apisidecars_test.go
index c1bfd088e..01a2111dd 100644
--- a/internal/console/apisidecars_test.go
+++ b/internal/console/apisidecars_test.go
@@ -698,20 +698,20 @@ func TestAPISidecarListAndGet(t *testing.T) {
}
}
- // A fresh box reporting alpha's version, and a STALE one reporting
- // beta's, which must drop out of the join rather than answer as current.
+ // A fresh box reporting alpha's serving version and gamma's installed
+ // version, and a stale box reporting beta's serving version.
fresh := status.CollectBox(nil, status.Probes{MachineID: "box1"}, time.Now())
- fresh.Metrics = &status.BoxMetrics{RouteVersions: map[string]string{
- "alpha.titogo.bab.sidecar.internal": "abc1234"}}
+ fresh.Metrics = &status.BoxMetrics{
+ RouteServing: map[string]string{"alpha.titogo.bab.sidecar.internal": "abc1234"},
+ // gamma is installed but not started: root's stamp is not a running build.
+ RouteVersions: map[string]string{"gamma.titogo.bab.sidecar.internal": "f00d123"},
+ }
if err := status.WriteBox(context.Background(), fx.rel, fresh); err != nil {
t.Fatal(err)
}
stale := status.CollectBox(nil, status.Probes{MachineID: "box2"}, time.Now().Add(-time.Hour))
- stale.Metrics = &status.BoxMetrics{RouteVersions: map[string]string{
+ stale.Metrics = &status.BoxMetrics{RouteServing: map[string]string{
"beta.titogo.bab.sidecar.internal": "deadbeef"}}
- if err := status.WriteBox(context.Background(), fx.rel, stale); err != nil {
- t.Fatal(err)
- }
code, _, out := rawReq(t, http.MethodGet, u, token, "")
if code != http.StatusOK {
@@ -752,7 +752,7 @@ func TestAPISidecarListAndGet(t *testing.T) {
t.Errorf("beta running_version = %q, want empty — its only reporting box is stale", got["beta"])
}
if got["gamma"] != "" {
- t.Errorf("gamma running_version = %q, want empty — no box has reported it", got["gamma"])
+ t.Errorf("gamma running_version = %q, want empty — an install stamp is not a running build", got["gamma"])
}
// GET one.
diff --git a/internal/status/snapshot.go b/internal/status/snapshot.go
index 45788606c..b0ba61923 100644
--- a/internal/status/snapshot.go
+++ b/internal/status/snapshot.go
@@ -454,6 +454,12 @@ type BoxMetrics struct {
// no versioned route, same "absent, not empty" rule as the rest of this
// block.
RouteVersions map[string]string `json:"route_versions,omitempty"`
+ // RouteServing is host -> the build the agent last started for that
+ // route (registry ServingVersion, zero-downtime plan 5), for this box's
+ // own non-platform routes that have one. RouteVersions is root's install
+ // stamp and can name a build nothing runs yet; this is what runs. Private
+ // and omitted when empty, for RouteVersions' reasons.
+ RouteServing map[string]string `json:"route_serving,omitempty"`
// Upstreams is the box's per-upstream probe table, route host -> the
// UpstreamState of each addr behind it (multi-upstream routes only; a
// single-addr route with no health config is never probed and never
@@ -765,6 +771,10 @@ func CollectBox(routes []registry.Route, p Probes, now time.Time) Box {
// box's Metrics attachment check below sees exactly "nothing to report"
// rather than a present-but-empty map.
var routeVersions map[string]string
+ // routeServing is plan 5's ServingVersion: host -> the build the agent
+ // last started, for this box's own non-platform routes that have one.
+ // Same nil-until-first-entry convention as routeVersions.
+ var routeServing map[string]string
// aliasReadings is issue #151's Metrics.Aliases source. Declared here,
// above the route loop, so the route loop below (a first-route claim's
// ROUTE readings) and the p.Aliases loop after it (genuine alias rows)
@@ -793,6 +803,12 @@ func CollectBox(routes []registry.Route, p Probes, now time.Time) Box {
}
routeVersions[r.Host] = r.Version
}
+ if r.ServingVersion != "" {
+ if routeServing == nil {
+ routeServing = map[string]string{}
+ }
+ routeServing[r.Host] = r.ServingVersion
+ }
if r.Kind != "static" && r.DBPath != "" && p.DBBytes != nil {
if n := p.DBBytes(r.DBPath); n != nil {
if routeDBBytes == nil {
@@ -914,7 +930,7 @@ func CollectBox(routes []registry.Route, p Probes, now time.Time) Box {
if p.MemTotalBytes != nil || p.MemAvailBytes != nil || p.Load1 != nil ||
p.DiskFreeBytes != nil || p.Release != "" || p.Tunnel != "" ||
p.InstanceID != "" || p.Platform != "" || p.SystemChannel != "" || p.System != nil ||
- len(routeVersions) > 0 || len(aliasReadings) > 0 || len(p.Upstreams) > 0 ||
+ len(routeVersions) > 0 || len(routeServing) > 0 || len(aliasReadings) > 0 || len(p.Upstreams) > 0 ||
len(routeDBBytes) > 0 || p.Layout != nil || len(p.ConfigRefusals) > 0 || len(p.ReleaseRefusals) > 0 || len(p.ProofWaits) > 0 || p.Rollout != nil || p.ConfigGuard != "" ||
p.Alerts != nil || p.GeoIP != nil {
box.Metrics = &BoxMetrics{
@@ -937,6 +953,7 @@ func CollectBox(routes []registry.Route, p Probes, now time.Time) Box {
InstanceID: p.InstanceID,
Alerts: p.Alerts,
RouteVersions: routeVersions,
+ RouteServing: routeServing,
Aliases: aliasReadings,
// Carried through as the prober handed it over: proberSet.State
// already returns a fresh copy per call, so there is nothing here
diff --git a/internal/status/snapshot_test.go b/internal/status/snapshot_test.go
index 1afd4c3b7..286e2dfea 100644
--- a/internal/status/snapshot_test.go
+++ b/internal/status/snapshot_test.go
@@ -7,6 +7,7 @@ import (
"testing"
"time"
+ "github.com/carlosframework/platform/internal/registry"
"github.com/carlosframework/platform/internal/release"
)
@@ -168,3 +169,23 @@ func TestTheRolloutReportRidesThePrivateMetrics(t *testing.T) {
t.Fatal("the flag off publishes no rollout block and no Metrics because of it")
}
}
+
+// RouteServing is the build the agent started for each route (plan 5's
+// ServingVersion), carried beside RouteVersions, root's install stamp.
+func TestCollectBoxCarriesTheServingBuild(t *testing.T) {
+ routes := []registry.Route{
+ {Host: "side.titogo.bab.sidecar.internal", Kind: "sidecar", Version: "v2", ServingVersion: "v1"},
+ {Host: "never.titogo.bab.sidecar.internal", Kind: "sidecar", Version: "v2"},
+ {Host: "alias.example.com", Platform: true, ServingVersion: "v9"},
+ }
+ b := CollectBox(routes, Probes{MachineID: "m"}, time.Now())
+ if b.Metrics == nil || b.Metrics.RouteServing["side.titogo.bab.sidecar.internal"] != "v1" {
+ t.Fatalf("RouteServing = %+v", b.Metrics)
+ }
+ if _, ok := b.Metrics.RouteServing["never.titogo.bab.sidecar.internal"]; ok {
+ t.Error("a route that never started has a serving build")
+ }
+ if _, ok := b.Metrics.RouteServing["alias.example.com"]; ok {
+ t.Error("a platform alias carries a serving build")
+ }
+}
diff --git a/sidecars_cli.go b/sidecars_cli.go
index ec7dc5e0c..652528eb3 100644
--- a/sidecars_cli.go
+++ b/sidecars_cli.go
@@ -366,13 +366,12 @@ func runSidecarsCreate(args []string) {
mutedf(stdout, " an always-on one (hibernate: false in the app's sidecar settings) starts as soon as the row lands")
}
-// sidecarVersionCell renders `ls`'s VERSION column: the raw running version
-// when the sidecar has reported one via /v1/ready, or the literal
-// "unknown" when it never has — the honesty rule this branch was built
-// around (design §9.2, binding on this task): a stamp that overclaims is
-// worse than an absent one, so this must NEVER render the record's own
-// Channel (the desired version) as a stand-in for a version nobody has
-// confirmed is actually running.
+// sidecarVersionCell renders `ls`'s VERSION column: the build the agent
+// last started for the sidecar, or the literal "unknown" when it never has
+// — the honesty rule this branch was built around (design §9.2, binding on
+// this task): a stamp that overclaims is worse than an absent one, so this
+// must NEVER render the record's own Channel (the desired version) as a
+// stand-in for a version nobody has confirmed is actually running.
func sidecarVersionCell(running string) string {
if running == "" {
return "unknown"