Stop repeating Kubernetes workload names in the demo (#2475)

The demo scenario renamed every generated deployment to one of four
curated names and every pod to one of ten, cycling by index. With the
default 14 deployments and 40 pods per cluster the Kubernetes overview
listed checkout-api four times in one namespace, and the curated
pending and crash-looping pods appeared more than once. Kubernetes
itself refuses duplicate names in a namespace, so the public demo
looked broken.

Ten more healthy services fill out the default deployment count, a
numeric suffix keeps any larger count unique, and pods past the curated
story become healthy ReplicaSet-style replicas of the cluster's own
deployments on ready nodes, so each curated failure appears once.

Checked on a mock stack: the Kubernetes overview lists 14 distinct
deployments and every pod name once, with the curated pending pod shown
once. The new test covers the default, 30/60, 0/40 and 15/631 shapes, a
single crash loop across the estate, healthy repeats, ready-node
placement and name stability across a metric refresh.
This commit is contained in:
rcourtman
2026-10-05 00:28:00 +01:00
committed by GitHub
2 changed files with 193 additions and 4 deletions
+91 -4
View File
@@ -267,6 +267,19 @@ var demoKubernetesDeploymentProfiles = []demoKubernetesDeploymentProfile{
{Name: "checkout-api", Namespace: "services", DesiredReplicas: 3, ReadyReplicas: 3},
{Name: "payments-worker", Namespace: "services", DesiredReplicas: 2, ReadyReplicas: 1},
{Name: "platform-observability", Namespace: "monitoring", DesiredReplicas: 2, ReadyReplicas: 2},
// Healthy services that fill out the default deployment count, so the
// demo never shows one deployment name repeated within a namespace (the
// Kubernetes API forbids that, and the demo looked broken with it).
{Name: "ingress-nginx-controller", Namespace: "ingress-nginx", DesiredReplicas: 2, ReadyReplicas: 2},
{Name: "storefront-web", Namespace: "apps", DesiredReplicas: 2, ReadyReplicas: 2},
{Name: "account-portal", Namespace: "apps", DesiredReplicas: 2, ReadyReplicas: 2},
{Name: "search-api", Namespace: "services", DesiredReplicas: 3, ReadyReplicas: 3},
{Name: "inventory-api", Namespace: "services", DesiredReplicas: 2, ReadyReplicas: 2},
{Name: "order-events-consumer", Namespace: "services", DesiredReplicas: 2, ReadyReplicas: 2},
{Name: "notification-worker", Namespace: "services", DesiredReplicas: 1, ReadyReplicas: 1},
{Name: "image-resizer", Namespace: "services", DesiredReplicas: 2, ReadyReplicas: 2},
{Name: "session-cache", Namespace: "services", DesiredReplicas: 1, ReadyReplicas: 1},
{Name: "metrics-gateway", Namespace: "monitoring", DesiredReplicas: 1, ReadyReplicas: 1},
}
var demoKubernetesPodProfiles = []demoKubernetesPodProfile{
@@ -750,6 +763,13 @@ func applyDemoKubernetesScenario(state *models.StateSnapshot, now time.Time) {
clusterCarriesDeployCrash := profile.Scenario == demoK8sScenarioCrashLoopBackOff
for i := range cluster.Deployments {
deploymentProfile := demoKubernetesDeploymentProfiles[i%len(demoKubernetesDeploymentProfiles)]
if round := i / len(demoKubernetesDeploymentProfiles); round > 0 {
// Repeats keep the name unique and stay healthy, so a curated
// degradation such as the under-replicated payments-worker
// is told once rather than once per round.
deploymentProfile.Name = fmt.Sprintf("%s-%d", deploymentProfile.Name, round+1)
deploymentProfile.ReadyReplicas = deploymentProfile.DesiredReplicas
}
desired := deploymentProfile.DesiredReplicas
ready := deploymentProfile.ReadyReplicas
// payments-worker is under-replicated only in the cluster that
@@ -770,11 +790,35 @@ func applyDemoKubernetesScenario(state *models.StateSnapshot, now time.Time) {
}
}
readyNodeIndexes := make([]int, 0, len(cluster.Nodes))
for i := range cluster.Nodes {
if cluster.Nodes[i].Ready {
readyNodeIndexes = append(readyNodeIndexes, i)
}
}
for i := range cluster.Pods {
podProfile := applyDemoKubernetesPodScenario(
demoKubernetesPodProfiles[i%len(demoKubernetesPodProfiles)],
profile.Scenario,
)
var podProfile demoKubernetesPodProfile
switch {
case i < len(demoKubernetesPodProfiles):
podProfile = applyDemoKubernetesPodScenario(demoKubernetesPodProfiles[i], profile.Scenario)
case len(cluster.Deployments) == 0:
// No deployments to own replicas: repeat a curated pod under
// a unique name, healthy, so its story is not told twice.
podProfile = demoKubernetesHealthyRepeatPodProfile(
demoKubernetesPodProfiles[i%len(demoKubernetesPodProfiles)],
i/len(demoKubernetesPodProfiles)+1,
)
default:
// Past the curated story, pods are healthy replicas of the
// cluster's deployments with unique names, so the curated
// failures appear once and no pod name repeats.
podProfile = demoKubernetesReplicaPodProfile(
cluster.ID,
cluster.Deployments[(i-len(demoKubernetesPodProfiles))%len(cluster.Deployments)],
i,
readyNodeIndexes,
)
}
pod := &cluster.Pods[i]
pod.Name = podProfile.Name
pod.Namespace = podProfile.Namespace
@@ -825,6 +869,49 @@ func applyDemoKubernetesScenario(state *models.StateSnapshot, now time.Time) {
syncMockKubernetesNodeHosts(state)
}
// demoKubernetesReplicaPodProfile describes one healthy pod of a demo
// deployment, named the way a ReplicaSet names its pods. The pod index feeds
// the suffix so every replica stays unique within the cluster.
func demoKubernetesReplicaPodProfile(
clusterID string,
deployment models.KubernetesDeployment,
podIndex int,
readyNodeIndexes []int,
) demoKubernetesPodProfile {
nodeIndex := -1
if len(readyNodeIndexes) > 0 {
nodeIndex = readyNodeIndexes[podIndex%len(readyNodeIndexes)]
}
replicaSetHash := mockStableHexString(10, clusterID, deployment.Namespace, deployment.Name, "replicaset")
// A hash alone can collide; ending the suffix with the pod index keeps
// every replica unique while the fixed-width hash keeps it looking real.
podSuffix := mockStableHexString(3, clusterID, deployment.Namespace, deployment.Name, "pod") +
strconv.FormatInt(int64(podIndex), 36)
return demoKubernetesPodProfile{
Name: fmt.Sprintf("%s-%s-%s", deployment.Name, replicaSetHash, podSuffix),
Namespace: deployment.Namespace,
NodeIndex: nodeIndex,
OwnerKind: "Deployment",
OwnerName: deployment.Name,
Phase: "Running",
Container: deployment.Name,
Image: fmt.Sprintf("ghcr.io/pulse-demo/%s:2026.04", deployment.Name),
ContainerOK: true,
}
}
// demoKubernetesHealthyRepeatPodProfile reuses a curated pod's shape under a
// unique name with its failure story scrubbed.
func demoKubernetesHealthyRepeatPodProfile(profile demoKubernetesPodProfile, round int) demoKubernetesPodProfile {
profile.Name = fmt.Sprintf("%s-%d", profile.Name, round)
profile.Phase = "Running"
profile.Reason = ""
profile.Message = ""
profile.Restarts = 0
profile.ContainerOK = true
return profile
}
// applyDemoKubernetesPodScenario rewrites a global pod profile so the
// cluster's chosen scenario is the only place its degraded state appears.
// Production EU stays healthy across the board; Staging EU keeps the
+102
View File
@@ -1,6 +1,8 @@
package mock
import (
"reflect"
"sort"
"strings"
"testing"
"time"
@@ -1011,3 +1013,103 @@ func TestDemoDockerContainerNamesAreUniquePerHost(t *testing.T) {
t.Fatal("expected the first round of curated names to stay unchanged")
}
}
func TestDemoKubernetesWorkloadNamesAreUniquePerNamespace(t *testing.T) {
curatedPods := map[string]struct{}{}
for _, profile := range demoKubernetesPodProfiles {
curatedPods[profile.Name] = struct{}{}
}
for _, tc := range []struct {
name string
deployments int
pods int
}{
{name: "default", deployments: DefaultConfig.K8sDeploymentsPerCluster, pods: DefaultConfig.K8sPodsPerCluster},
{name: "beyond curated profiles", deployments: 30, pods: 60},
{name: "no deployments", deployments: 0, pods: 40},
{name: "many replicas", deployments: 15, pods: 631},
} {
t.Run(tc.name, func(t *testing.T) {
cfg := DefaultConfig
cfg.RandomMetrics = false
cfg.K8sDeploymentsPerCluster = tc.deployments
cfg.K8sPodsPerCluster = tc.pods
start := time.Date(2026, time.April, 1, 12, 0, 0, 0, time.UTC)
graph := buildFixtureGraph(cfg, start)
before := kubernetesWorkloadKeys(graph)
// Kubernetes refuses two deployments or two pods with one name in
// a namespace. The demo cycled four deployment names and ten pod
// names, so the overview showed "checkout-api" four times and the
// curated crash-looping pod more than once.
totalCrashLooping := 0
for _, cluster := range graph.State.KubernetesClusters {
readyNodes := map[string]bool{}
for _, node := range cluster.Nodes {
readyNodes[node.Name] = node.Ready
}
deployments := map[string]struct{}{}
for _, deployment := range cluster.Deployments {
key := deployment.Namespace + "/" + deployment.Name
if _, dup := deployments[key]; dup {
t.Fatalf("cluster %s names two deployments %q", cluster.DisplayName, key)
}
deployments[key] = struct{}{}
if deployment.ReadyReplicas < deployment.DesiredReplicas && deployment.Name != "payments-worker" {
t.Fatalf("cluster %s repeats a degraded deployment as %q", cluster.DisplayName, key)
}
}
pods := map[string]struct{}{}
crashLooping := 0
for _, pod := range cluster.Pods {
key := pod.Namespace + "/" + pod.Name
if _, dup := pods[key]; dup {
t.Fatalf("cluster %s names two pods %q", cluster.DisplayName, key)
}
pods[key] = struct{}{}
if _, curated := curatedPods[pod.Name]; !curated && pod.NodeName != "" && !readyNodes[pod.NodeName] {
t.Fatalf("cluster %s placed replica %q on not-ready node %q", cluster.DisplayName, key, pod.NodeName)
}
if tc.deployments >= len(demoKubernetesDeploymentProfiles) && pod.OwnerKind == "Deployment" {
if _, ok := deployments[pod.Namespace+"/"+pod.OwnerName]; !ok {
t.Fatalf("cluster %s pod %q is owned by missing deployment %q", cluster.DisplayName, key, pod.OwnerName)
}
}
for _, container := range pod.Containers {
if container.Reason == "CrashLoopBackOff" {
crashLooping++
break
}
}
}
if crashLooping > 1 {
t.Fatalf("cluster %s repeats the curated crash loop on %d pods", cluster.DisplayName, crashLooping)
}
totalCrashLooping += crashLooping
}
if totalCrashLooping != 1 {
t.Fatalf("expected the curated crash loop exactly once across the estate, got %d", totalCrashLooping)
}
graph.UpdateMetrics(cfg, start.Add(45*time.Minute))
if after := kubernetesWorkloadKeys(graph); !reflect.DeepEqual(before, after) {
t.Fatal("expected Kubernetes workload names to survive a metric refresh unchanged")
}
})
}
}
func kubernetesWorkloadKeys(graph FixtureGraph) []string {
keys := []string{}
for _, cluster := range graph.State.KubernetesClusters {
for _, deployment := range cluster.Deployments {
keys = append(keys, cluster.ID+"/deploy/"+deployment.Namespace+"/"+deployment.Name)
}
for _, pod := range cluster.Pods {
keys = append(keys, cluster.ID+"/pod/"+pod.Namespace+"/"+pod.Name)
}
}
sort.Strings(keys)
return keys
}