Stabilise label-derived tags without hiding metadata changes

Docker and Kubernetes label maps became ordered tag arrays in random map iteration order, forcing unchanged resources into connected-dashboard deltas (#2441, found while profiling #2199). Sort only the deduplicated label-derived list. Preserve existing label values, source maps, first-choice image references and identity ordering. Exercise all seven affected Docker adapters and twenty-six Kubernetes kinds through registry rebuilds, monitor projection, per-client queues and receiver replay, including real replace/add/remove/clear/restore changes, tenant isolation and a thousand-pod heartbeat.

Contract-Neutral: Deterministic ordering of existing label-derived tag sets only; no public JSON shape, canonical ownership or ordered identity-list change.
Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-10-03 16:05:05 +01:00
parent b62042798c
commit 4e57511d1e
4 changed files with 439 additions and 1 deletions
+7 -1
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"net"
"net/url"
"slices"
"strconv"
"strings"
"time"
@@ -3781,7 +3782,12 @@ func labelsToTags(labels map[string]string) []string {
}
out = append(out, key+":"+v)
}
return uniqueStrings(out)
// Labels are a set, but tags are sent as an ordered JSON array. Stabilise
// only this map-derived list so rebuilds do not create spurious socket
// deltas. uniqueStrings also serves ordered identity lists; do not sort it.
out = uniqueStrings(out)
slices.Sort(out)
return out
}
func convertInterfaces(interfaces []models.HostNetworkInterface) []NetworkInterface {
@@ -0,0 +1,67 @@
package unifiedresources
import (
"maps"
"reflect"
"testing"
"github.com/rcourtman/pulse-go-rewrite/internal/models"
)
func TestLabelTagsStableAcrossMapIterations(t *testing.T) {
labels := map[string]string{
"app": "api", "env": "production", "team": "ops", "tier": "backend",
"region": "west", "release": "stable", "managed": "yes", "version": "1",
}
before := maps.Clone(labels)
want := []string{"app:api", "env:production", "managed:yes", "region:west", "release:stable", "team:ops", "tier:backend", "version:1"}
for i := 0; i < 256; i++ {
if got := labelsToTags(labels); !reflect.DeepEqual(got, want) {
t.Fatalf("iteration %d: tags = %v, want stable %v", i, got, want)
}
}
if !reflect.DeepEqual(labels, before) {
t.Fatal("tag conversion mutated source labels")
}
}
func TestLabelTagsRetainExistingValueSemantics(t *testing.T) {
for _, tc := range []struct {
name string
labels map[string]string
want []string
}{
{"nil", nil, nil},
{"empty", map[string]string{}, nil},
{"blank-key", map[string]string{" ": "ignored"}, []string{}},
{"bare-keys", map[string]string{" z ": "\t", "a": ""}, []string{"a", "z"}},
{"deduplicated-trimmed-keys", map[string]string{" a": "v", "a ": "v"}, []string{"a:v"}},
{"case-colon-and-value-spaces", map[string]string{"Z": "https://example.invalid/a:b", "a": " Mixed Case "}, []string{"Z:https://example.invalid/a:b", "a: Mixed Case"}},
} {
t.Run(tc.name, func(t *testing.T) {
if got := labelsToTags(tc.labels); !reflect.DeepEqual(got, tc.want) {
t.Fatalf("tags = %#v, want %#v", got, tc.want)
}
})
}
}
func TestLabelTagsDoNotSortOrderedIdentityInputs(t *testing.T) {
values := []string{" z ", "a", "z", "b", ""}
before := append([]string(nil), values...)
if got := uniqueStrings(values); !reflect.DeepEqual(got, []string{"z", "a", "b"}) {
t.Fatalf("first-occurrence order changed: %v", got)
}
if !reflect.DeepEqual(values, before) {
t.Fatal("deduplication changed the input list")
}
image, identity := resourceFromDockerImage(models.DockerImage{
ID: "sha256:example", RepoTags: []string{"z:last", "a:first"},
Labels: map[string]string{"z": "last", "a": "first"},
}, models.DockerHost{Hostname: "runtime-host"})
if image.Name != "z:last" || !reflect.DeepEqual(image.Docker.RepoTags, []string{"z:last", "a:first"}) ||
!reflect.DeepEqual(identity.Hostnames, []string{"z:last", "runtime-host:z:last"}) {
t.Fatalf("label sorting reordered image references or identity: %+v / %+v", image, identity)
}
}
@@ -32,3 +32,39 @@ func NewBroadcastProjectionProbeForTest(getState func(string) interface{}, recip
return bytes, nil
}, nil
}
// NewBroadcastProjectionCaptureForTest retains the actual per-client frames,
// including a nil frame for a quiet or unrelated tenant. It runs the production
// current-state getter, projection, delta baseline and queue path without timers
// or a network socket, and is available only in the test binary.
func NewBroadcastProjectionCaptureForTest(getState func(string) interface{}, orgIDs []string) (func(string) ([][]byte, error), error) {
hub := NewHub(getState)
clients := make([]*Client, len(orgIDs))
for i, orgID := range orgIDs {
client := &Client{hub: hub, orgID: orgID, send: make(chan []byte, 1)}
if _, sent, err := client.queueFullState("initialState", getState(orgID)); err != nil || !sent {
return nil, fmt.Errorf("initial state: sent=%v error=%v", sent, err)
}
<-client.send
clients[i] = client
hub.clients[client] = true
if hub.clientsByTenant[orgID] == nil {
hub.clientsByTenant[orgID] = make(map[*Client]bool)
}
hub.clientsByTenant[orgID][client] = true
}
return func(orgID string) ([][]byte, error) {
hub.dispatchStateBroadcast(&Message{Type: "rawData", Data: stateBroadcastRequest{}}, orgID)
frames := make([][]byte, len(clients))
for i, client := range clients {
if !hub.clients[client] {
return nil, fmt.Errorf("broadcast removed client %d", i)
}
select {
case frames[i] = <-client.send:
default:
}
}
return frames, nil
}, nil
}
@@ -0,0 +1,329 @@
package websocket_test
import (
"bytes"
"encoding/json"
"fmt"
"maps"
"reflect"
"slices"
"strings"
"testing"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/models"
"github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
"github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
"github.com/rcourtman/pulse-go-rewrite/internal/websocket"
)
// These are source-native inventories, not pre-sorted ResourceFrontend fixtures.
// Every label-derived Docker adapter and Kubernetes kind passes through a real
// registry generation, monitor projection, wire snapshot and client delta queue.
func labelTagInventory(now time.Time, labels map[string]string) models.StateSnapshot {
cluster := models.KubernetesCluster{ID: "label-cluster", Name: "label-cluster", Status: "online", LastSeen: now}
// The Kubernetes inventory has many kinds with the same UID/name/labels
// envelope. Include each one declared by models; the explicit count below
// prevents an empty or partial fixture from silently satisfying the test.
value := reflect.ValueOf(&cluster).Elem()
for i := 0; i < value.NumField(); i++ {
field := value.Field(i)
if field.Kind() != reflect.Slice || field.Type().Elem().Kind() != reflect.Struct {
continue
}
entry := reflect.New(field.Type().Elem()).Elem()
labelField := entry.FieldByName("Labels")
if !labelField.IsValid() {
continue
}
name := "label-" + strings.ToLower(value.Type().Field(i).Name)
for key, text := range map[string]string{"UID": "uid-" + name, "Name": name, "Namespace": "workloads", "Phase": "Running"} {
if target := entry.FieldByName(key); target.IsValid() && target.Kind() == reflect.String {
target.SetString(text)
}
}
if value.Type().Field(i).Name == "Namespaces" {
entry.FieldByName("Name").SetString("workloads")
entry.FieldByName("Phase").SetString("Active")
}
labelField.Set(reflect.ValueOf(labels))
field.Set(reflect.Append(field, entry))
}
return models.StateSnapshot{
LastUpdate: now,
DockerHosts: []models.DockerHost{{
ID: "label-docker", Hostname: "label-docker", Status: "online", LastSeen: now,
Swarm: &models.DockerSwarmInfo{ClusterID: "label-swarm", ClusterName: "label-swarm"},
Images: []models.DockerImage{{ID: "sha256:label-image", RepoTags: []string{"z:latest", "a:old"}, Labels: labels}},
Volumes: []models.DockerVolume{{Name: "label-volume", Labels: labels}},
Networks: []models.DockerNetwork{{ID: "label-network", Name: "label-network", Labels: labels}},
Services: []models.DockerService{{ID: "label-service", Name: "label-service", Labels: labels}},
Nodes: []models.DockerNode{{ID: "label-node", Hostname: "swarm-node", Labels: labels}},
Secrets: []models.DockerSecret{{ID: "label-secret", Name: "label-secret", Labels: labels}},
Configs: []models.DockerConfig{{ID: "label-config", Name: "label-config", Labels: labels}},
}},
KubernetesClusters: []models.KubernetesCluster{cluster},
}
}
func labelTagValues() map[string]string {
return map[string]string{"app": "api", "env": "production", "team": "ops", "tier": "backend", "region": "west", "release": "stable", "managed": "yes", "version": "1"}
}
type labelTagProjection struct {
adapter *unifiedresources.MonitorAdapter
store *unifiedresources.MemoryStore
monitor *monitoring.Monitor
}
func newLabelTagProjection(snapshot models.StateSnapshot) *labelTagProjection {
store := unifiedresources.NewMemoryStore()
adapter := unifiedresources.NewMonitorAdapter(unifiedresources.NewRegistry(store))
adapter.PopulateFromSnapshot(snapshot)
monitor := &monitoring.Monitor{}
// Completed ingest is the baseline: a dashboard read must not replace it
// with this test Monitor's empty live polling state.
monitor.SetResourceStore(&broadcastReadStore{adapter})
return &labelTagProjection{adapter: adapter, store: store, monitor: monitor}
}
func (p *labelTagProjection) state() models.StateFrontend {
return p.monitor.BuildBroadcastFrontendState()
}
func (p *labelTagProjection) changes(t *testing.T) int {
t.Helper()
count, err := p.store.CountRecentChanges("", time.Time{})
if err != nil {
t.Fatal(err)
}
return count
}
type labelTagWireDelta struct {
Upserts []map[string]any `json:"upserts"`
Removed []string `json:"removed"`
Order []string `json:"order"`
}
func labelTagFrameDelta(t *testing.T, frame []byte) labelTagWireDelta {
t.Helper()
if len(frame) == 0 {
return labelTagWireDelta{}
}
var message struct {
Type string `json:"type"`
Data map[string]json.RawMessage `json:"data"`
}
if err := json.Unmarshal(frame, &message); err != nil || message.Type != "rawData" {
t.Fatalf("invalid state frame: %s / %v", frame, err)
}
if _, full := message.Data["resources"]; full {
t.Fatal("connected client unexpectedly received a full resource array")
}
var delta labelTagWireDelta
if encoded, ok := message.Data["resourceDelta"]; ok {
if err := json.Unmarshal(encoded, &delta); err != nil {
t.Fatal(err)
}
}
return delta
}
func labelTagResourceMaps(t *testing.T, state models.StateFrontend) map[string]map[string]any {
t.Helper()
encoded, err := json.Marshal(state.Resources)
if err != nil {
t.Fatal(err)
}
var resources []map[string]any
if err := json.Unmarshal(encoded, &resources); err != nil {
t.Fatal(err)
}
out := make(map[string]map[string]any, len(resources))
for _, resource := range resources {
out[resource["id"].(string)] = resource
}
return out
}
// Independent RFC 7396 receiver oracle; do not reuse the production diff builder.
func applyLabelTagMergePatch(previous map[string]any, patch map[string]any) map[string]any {
out := maps.Clone(previous)
if out == nil {
out = make(map[string]any)
}
for key, value := range patch {
if value == nil {
delete(out, key)
} else if object, ok := value.(map[string]any); ok {
old, _ := out[key].(map[string]any)
out[key] = applyLabelTagMergePatch(old, object)
} else {
out[key] = value
}
}
return out
}
func TestLabelTagsConnectedRebuildsAreQuiet(t *testing.T) {
labels := labelTagValues()
before := maps.Clone(labels)
seed := labelTagInventory(time.Now().UTC(), labels)
projection := newLabelTagProjection(seed)
tagged := 0
for _, resource := range projection.state().Resources {
if len(resource.Tags) == len(labels) {
tagged++
}
}
if tagged != 33 { // Seven Docker adapters and twenty-six Kubernetes kinds.
t.Fatalf("fixture covers %d label-derived resources, want 33", tagged)
}
broadcast, err := websocket.NewBroadcastProjectionCaptureForTest(func(string) interface{} { return projection.state() }, []string{"default", "default"})
if err != nil {
t.Fatal(err)
}
changes := projection.changes(t)
var tagPatches, upserts, wireBytes int
for i := 0; i < 32; i++ {
projection.adapter.PopulateFromSnapshot(seed)
frames, err := broadcast("default")
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(frames[0], frames[1]) {
t.Fatal("equal client baselines received different frames")
}
for _, frame := range frames {
wireBytes += len(frame)
delta := labelTagFrameDelta(t, frame)
if len(delta.Removed) != 0 || len(delta.Order) != 0 {
t.Fatal("equivalent rebuild changed resource identity or order")
}
upserts += len(delta.Upserts)
for _, patch := range delta.Upserts {
if _, ok := patch["tags"]; ok {
tagPatches++
}
}
}
}
t.Logf("33 label-derived resources, 32 rebuilds, two clients: tag patches=%d, all resource upserts=%d, queued wire bytes=%d", tagPatches, upserts, wireBytes)
if tagPatches != 0 || upserts != 0 {
t.Fatalf("unchanged label sets created %d tag patches / %d resource upserts", tagPatches, upserts)
}
if projection.changes(t) != changes || !reflect.DeepEqual(labels, before) {
t.Fatal("equivalent rebuild changed history or mutated source labels")
}
}
func TestLabelTagsConnectedChangesAndClearsRemainVisible(t *testing.T) {
labels := labelTagValues()
now := time.Now().UTC()
projection := newLabelTagProjection(labelTagInventory(now, labels))
otherTenant := newLabelTagProjection(labelTagInventory(now, map[string]string{"private": "other-tenant"}))
broadcast, err := websocket.NewBroadcastProjectionCaptureForTest(func(org string) interface{} {
if org == "other" {
return otherTenant.state()
}
return projection.state()
}, []string{"default", "default", "other"})
if err != nil {
t.Fatal(err)
}
received := labelTagResourceMaps(t, projection.state())
changes := projection.changes(t)
for _, name := range []string{"replace", "add", "remove", "clear", "restore"} {
t.Run(name, func(t *testing.T) {
labels = maps.Clone(labels)
switch name {
case "replace":
labels["env"] = "staging"
case "add":
labels["new"] = "visible"
case "remove":
delete(labels, "app")
case "clear":
labels = nil
case "restore":
labels = labelTagValues()
}
projection.adapter.PopulateFromSnapshot(labelTagInventory(now, labels))
frames, err := broadcast("default")
if err != nil {
t.Fatal(err)
}
if len(frames[0]) == 0 || !bytes.Equal(frames[0], frames[1]) || len(frames[2]) != 0 {
t.Fatal("change was lost, differed per client or reached another tenant")
}
delta := labelTagFrameDelta(t, frames[0])
if len(delta.Upserts) != 33 || len(delta.Removed) != 0 || len(delta.Order) != 0 {
t.Fatalf("metadata edit changed identity/order or missed a resource: %+v", delta)
}
for _, patch := range delta.Upserts {
if _, changed := patch["tags"]; !changed {
t.Fatalf("real label change omitted tags: %v", patch)
}
id := patch["id"].(string)
received[id] = applyLabelTagMergePatch(received[id], patch)
}
want := labelTagResourceMaps(t, projection.state())
if !reflect.DeepEqual(received, want) {
t.Fatal("receiver replay differs from the full current projection (including labels/tags clears)")
}
for _, resource := range projection.state().Resources {
if !slices.IsSorted(resource.Tags) {
t.Fatalf("changed tags are not sorted for %s: %v", resource.ID, resource.Tags)
}
if name == "clear" && (len(resource.Tags) != 0 || len(resource.Labels) != 0) {
t.Fatalf("cleared metadata remained on %s", resource.ID)
}
}
changes += 33
if got := projection.changes(t); got != changes {
t.Fatalf("history lost or duplicated real changes: %d, want %d", got, changes)
}
})
}
}
func TestLabelTagsFleetHeartbeatDeltas(t *testing.T) {
now := time.Now().UTC()
labels := labelTagValues()
seed := models.StateSnapshot{LastUpdate: now, KubernetesClusters: []models.KubernetesCluster{{ID: "fleet", Name: "fleet", Status: "online", LastSeen: now}}}
for i := 0; i < 1000; i++ {
seed.KubernetesClusters[0].Pods = append(seed.KubernetesClusters[0].Pods, models.KubernetesPod{
UID: fmt.Sprintf("pod-%04d", i), Name: fmt.Sprintf("pod-%04d", i), Namespace: "workloads", Phase: "Running", Labels: labels,
})
}
projection := newLabelTagProjection(seed)
broadcast, err := websocket.NewBroadcastProjectionCaptureForTest(func(string) interface{} { return projection.state() }, []string{"default"})
if err != nil {
t.Fatal(err)
}
var tagPatches, wireBytes int
for i := 0; i < 8; i++ {
// Advance observed freshness as a real heartbeat does, not labels.
seed.LastUpdate = now.Add(time.Duration(i+1) * time.Second)
seed.KubernetesClusters[0].LastSeen = seed.LastUpdate
projection.adapter.PopulateFromSnapshot(seed)
frames, err := broadcast("default")
if err != nil || len(frames[0]) == 0 {
t.Fatalf("fresh heartbeat was not queued: %v", err)
}
wireBytes += len(frames[0])
for _, patch := range labelTagFrameDelta(t, frames[0]).Upserts {
if _, changed := patch["tags"]; changed {
tagPatches++
}
if _, changed := patch["labels"]; changed {
t.Fatal("heartbeat changed label values")
}
}
}
t.Logf("1,000 pods, eight fresh heartbeats, one client: false tag patches=%d, queued wire bytes=%d (includes real freshness deltas)", tagPatches, wireBytes)
if tagPatches != 0 {
t.Fatalf("unchanged fleet labels created %d tag patches", tagPatches)
}
}