package service import ( "encoding/json" "net/http" "net/http/httptest" "os" "reflect" "strings" "testing" "time" "metis/pkg/sentinel" ) func TestStageDesiredNodeMetadataMergesInventoryAndLiveCluster(t *testing.T) { kube := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case r.Method == http.MethodGet && r.URL.Path == "/api/v1/nodes": _ = json.NewEncoder(w).Encode(map[string]any{ "items": []any{ map[string]any{ "metadata": map[string]any{ "name": "titan-15", "labels": map[string]string{ "hardware": "rpi5", "rack": "a1", "maintenance.bstein.dev/color": "blue", "kubernetes.io/arch": "arm64", "node-role.kubernetes.io/worker": "true", }, "annotations": map[string]string{ "maintenance.bstein.dev/owner": "atlas", "volumes.kubernetes.io/controller-managed-attach-detach": "true", }, }, "spec": map[string]any{ "unschedulable": true, "taints": []any{ map[string]any{"key": "dedicated", "value": "recovery", "effect": "NoSchedule"}, map[string]any{"key": "node.kubernetes.io/unreachable", "effect": "NoExecute"}, }, }, }, }, }) default: http.NotFound(w, r) } })) defer kube.Close() installKubeFactory(t, kube) app := newTestApp(t) app.inventory.Nodes[0].Labels = map[string]string{"hardware": "rpi4", "rack": "a1"} app.inventory.Nodes[0].Taints = []string{"flash=true:NoSchedule"} app.desiredMetadata["titan-15"] = DesiredNodeMetadata{ Node: "titan-15", Annotations: map[string]string{"maintenance.bstein.dev/legacy": "keep"}, } desired, err := app.stageDesiredNodeMetadata("titan-15") if err != nil { t.Fatalf("stageDesiredNodeMetadata: %v", err) } if desired.Hostname != "titan-15" || !desired.Unschedulable { t.Fatalf("unexpected desired metadata header: %#v", desired) } if desired.Labels["hardware"] != "rpi5" || desired.Labels["rack"] != "a1" || desired.Labels["maintenance.bstein.dev/color"] != "blue" { t.Fatalf("unexpected desired labels: %#v", desired.Labels) } if _, ok := desired.Labels["kubernetes.io/arch"]; ok { t.Fatalf("system labels should not be persisted: %#v", desired.Labels) } if desired.Annotations["maintenance.bstein.dev/owner"] != "atlas" || desired.Annotations["maintenance.bstein.dev/legacy"] != "keep" { t.Fatalf("unexpected desired annotations: %#v", desired.Annotations) } if _, ok := desired.Annotations["volumes.kubernetes.io/controller-managed-attach-detach"]; ok { t.Fatalf("controller annotations should not be persisted: %#v", desired.Annotations) } if !reflect.DeepEqual(desired.Taints, []string{"dedicated=recovery:NoSchedule"}) { t.Fatalf("unexpected desired taints: %#v", desired.Taints) } data, err := os.ReadFile(app.settings.DesiredMetadataPath) if err != nil { t.Fatalf("read desired metadata file: %v", err) } if !strings.Contains(string(data), "titan-15") { t.Fatalf("desired metadata file missing titan-15: %s", string(data)) } } func TestStoreSnapshotRestoresDesiredNodeMetadata(t *testing.T) { var patchBody map[string]any kube := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case r.Method == http.MethodGet && r.URL.Path == "/api/v1/nodes": _ = json.NewEncoder(w).Encode(map[string]any{ "items": []any{ map[string]any{ "metadata": map[string]any{ "name": "titan-15", "labels": map[string]string{ "hardware": "rpi4", "maintenance.bstein.dev/old": "1", }, "annotations": map[string]string{ "maintenance.bstein.dev/mode": "old", }, }, "spec": map[string]any{ "unschedulable": true, "taints": []any{ map[string]any{"key": "dedicated", "value": "old", "effect": "NoSchedule"}, map[string]any{"key": "node.kubernetes.io/unreachable", "effect": "NoExecute"}, }, }, }, }, }) case r.Method == http.MethodPatch && r.URL.Path == "/api/v1/nodes/titan-15": if err := json.NewDecoder(r.Body).Decode(&patchBody); err != nil { t.Fatalf("decode patch: %v", err) } _ = json.NewEncoder(w).Encode(map[string]any{"status": "ok"}) default: http.NotFound(w, r) } })) defer kube.Close() installKubeFactory(t, kube) app := newTestApp(t) app.desiredMetadata["titan-15"] = DesiredNodeMetadata{ Node: "titan-15", Hostname: "titan-15", Labels: map[string]string{"hardware": "rpi5"}, Annotations: map[string]string{"maintenance.bstein.dev/mode": "recovery"}, Taints: []string{"dedicated=recovery:NoSchedule"}, Unschedulable: false, } if err := app.StoreSnapshot(SnapshotRecord{ Node: "titan-15", CollectedAt: time.Date(2026, 4, 24, 6, 0, 0, 0, time.UTC), Snapshot: sentinel.Snapshot{Hostname: "titan-15"}, }); err != nil { t.Fatalf("StoreSnapshot: %v", err) } if patchBody == nil { t.Fatal("expected desired metadata patch") } metadata := patchBody["metadata"].(map[string]any) labels := metadata["labels"].(map[string]any) if labels["hardware"] != "rpi5" || labels["maintenance.bstein.dev/old"] != nil { t.Fatalf("unexpected label patch: %#v", labels) } annotations := metadata["annotations"].(map[string]any) if annotations["maintenance.bstein.dev/mode"] != "recovery" { t.Fatalf("unexpected annotation patch: %#v", annotations) } spec := patchBody["spec"].(map[string]any) if spec["unschedulable"] != false { t.Fatalf("unexpected spec patch: %#v", spec) } taints := spec["taints"].([]any) if len(taints) != 2 { t.Fatalf("unexpected taint payload: %#v", taints) } entries := map[string]map[string]any{} for _, raw := range taints { entry := raw.(map[string]any) key := entry["key"].(string) entries[key] = entry } if entries["dedicated"]["value"] != "recovery" || entries["dedicated"]["effect"] != "NoSchedule" { t.Fatalf("missing desired taint replacement: %#v", entries) } if entries["node.kubernetes.io/unreachable"]["effect"] != "NoExecute" { t.Fatalf("system taint should be preserved: %#v", entries) } } func TestDesiredNodeMetadataHelpers(t *testing.T) { app := newTestApp(t) if _, ok := app.desiredMetadataForNode("missing"); ok { t.Fatal("expected no desired metadata for missing node") } if err := app.syncDesiredNodeMetadata(SnapshotRecord{Node: "missing"}); err != nil { t.Fatalf("syncDesiredNodeMetadata missing should noop: %v", err) } if _, ok := liveClusterNode(""); ok { t.Fatal("empty liveClusterNode lookup should fail") } if !isRestorableLabel("maintenance.bstein.dev/role") || isRestorableLabel("kubernetes.io/arch") { t.Fatal("unexpected label restoration filter") } if !isRestorableAnnotation("maintenance.bstein.dev/state") || isRestorableAnnotation("volumes.kubernetes.io/foo") { t.Fatal("unexpected annotation restoration filter") } if !isRestorableTaint("dedicated=recovery:NoSchedule") || isRestorableTaint("node.kubernetes.io/not-ready:NoExecute") { t.Fatal("unexpected taint restoration filter") } key, value, effect := splitTaint("dedicated=recovery:NoSchedule") if key != "dedicated" || value != "recovery" || effect != "NoSchedule" { t.Fatalf("splitTaint mismatch: %q %q %q", key, value, effect) } if key, value, effect := splitTaint("just-a-key"); key != "just-a-key" || value != "" || effect != "" { t.Fatalf("splitTaint key-only mismatch: %q %q %q", key, value, effect) } labels := filteredRestorableLabels(map[string]string{"hardware": "rpi5", "kubernetes.io/arch": "arm64"}) if !reflect.DeepEqual(labels, map[string]string{"hardware": "rpi5"}) { t.Fatalf("filteredRestorableLabels = %#v", labels) } annotations := filteredRestorableAnnotations(map[string]string{"maintenance.bstein.dev/state": "ok", "volumes.kubernetes.io/foo": "bar"}) if !reflect.DeepEqual(annotations, map[string]string{"maintenance.bstein.dev/state": "ok"}) { t.Fatalf("filteredRestorableAnnotations = %#v", annotations) } patch := metadataStringPatch( map[string]string{"hardware": "rpi4", "maintenance.bstein.dev/old": "1"}, map[string]string{"hardware": "rpi5"}, isRestorableLabel, ) if patch["hardware"] != "rpi5" || patch["maintenance.bstein.dev/old"] != nil { t.Fatalf("metadataStringPatch = %#v", patch) } mergedTaints := mergeLiveAndDesiredTaints( []string{"node.kubernetes.io/unreachable:NoExecute", "dedicated=old:NoSchedule"}, []string{"dedicated=new:NoSchedule", "dedicated=new:NoSchedule"}, ) if !reflect.DeepEqual(mergedTaints, []string{"dedicated=new:NoSchedule", "node.kubernetes.io/unreachable:NoExecute"}) { t.Fatalf("mergeLiveAndDesiredTaints = %#v", mergedTaints) } payload := taintPatchPayload([]string{"dedicated=new:NoSchedule"}) if len(payload) != 1 || payload[0]["key"] != "dedicated" || payload[0]["value"] != "new" || payload[0]["effect"] != "NoSchedule" { t.Fatalf("taintPatchPayload = %#v", payload) } original := DesiredNodeMetadata{Labels: map[string]string{"hardware": "rpi5"}, Taints: []string{"dedicated=new:NoSchedule"}} cloned := cloneDesiredNodeMetadata(original) cloned.Labels["hardware"] = "mutated" cloned.Taints[0] = "changed" if original.Labels["hardware"] != "rpi5" || original.Taints[0] != "dedicated=new:NoSchedule" { t.Fatalf("cloneDesiredNodeMetadata should deep-copy slices/maps: %#v %#v", original, cloned) } if err := patchDesiredNodeMetadata( clusterNode{Name: "titan-15", Labels: map[string]string{"hardware": "rpi5"}, Taints: []string{"dedicated=new:NoSchedule"}}, DesiredNodeMetadata{Node: "titan-15", Labels: map[string]string{"hardware": "rpi5"}, Taints: []string{"dedicated=new:NoSchedule"}}, ); err != nil { t.Fatalf("patchDesiredNodeMetadata should noop when already in sync: %v", err) } if event := desiredNodeMetadataSyncEvent("titan-15", os.ErrPermission); event.Kind != "sentinel.node-metadata" || event.Details["node"] != "titan-15" { t.Fatalf("desiredNodeMetadataSyncEvent = %#v", event) } }