From 03d771430bf0aac44239b924a87bc6da24110d5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mats=20T=C3=B6rnberg?= Date: Fri, 16 Oct 2026 10:44:30 +0200 Subject: [PATCH] Console: return [] for an empty group list, and fix its deployment manifest GET /api/clients returned "groups": null when there were no groups, because the merged list was a nil slice. Found against the real cluster. The console manifest, never applied before, had an image name that does not exist here, no image pull secret or architecture selector (the resolver's pods have both), and pointed at a Service (fancy-dns-admin) that is not there. It now uses the registry image, the pull secret, and the existing fancy-dns-web Service. Co-Authored-By: Claude Sonnet 5.5 --- deploy/k8s/console/console.yaml | 9 +++++++-- internal/app/console_e2e_test.go | 6 +++++- internal/console/hooks.go | 3 ++- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/deploy/k8s/console/console.yaml b/deploy/k8s/console/console.yaml index 41bfee6..1f41a9f 100644 --- a/deploy/k8s/console/console.yaml +++ b/deploy/k8s/console/console.yaml @@ -72,19 +72,24 @@ spec: labels: {app.kubernetes.io/name: fancy-dns-console} spec: serviceAccountName: fancy-dns-console + nodeSelector: + kubernetes.io/arch: amd64 + imagePullSecrets: + - name: reg-k6n securityContext: runAsNonRoot: true runAsUser: 65532 seccompProfile: {type: RuntimeDefault} containers: - name: console - image: fancydns/fancy-dns:latest + image: registry.k6n.net/fancydns + imagePullPolicy: Always command: ["/usr/local/bin/fancy-dns-console"] args: - -config=/etc/fancy-dns/console.yaml # A Service in front of the resolver pods. With several replicas, the # per-pod views (status, cache, upstreams) come from whichever pod answers. - - -runtime-url=http://fancy-dns-admin.dns.svc:8080 + - -runtime-url=http://fancy-dns-web.dns.svc:8080 env: - name: FANCYDNS_RUNTIME_KEY valueFrom: {secretKeyRef: {name: fancy-dns-console-runtime, key: FANCYDNS_RUNTIME_KEY}} diff --git a/internal/app/console_e2e_test.go b/internal/app/console_e2e_test.go index 9690e22..5977794 100644 --- a/internal/app/console_e2e_test.go +++ b/internal/app/console_e2e_test.go @@ -104,7 +104,11 @@ func TestConsoleEditsReachTheResolver(t *testing.T) { // A group the console just removed disappears from its own view at once. ok("PUT", "/api/clients", `{"groups":[]}`) view = clients{} - if err := json.Unmarshal([]byte(ok("GET", "/api/clients", "")), &view); err != nil || len(view.Groups) != 0 { + raw := ok("GET", "/api/clients", "") + if !strings.Contains(raw, `"groups":[]`) { + t.Errorf("an empty group list must be [] and not null: %s", raw) + } + if err := json.Unmarshal([]byte(raw), &view); err != nil || len(view.Groups) != 0 { t.Fatalf("clients after delete = %+v (%v)", view, err) } eventually(t, "the resolver to drop the deleted group", func() bool { diff --git a/internal/console/hooks.go b/internal/console/hooks.go index ad9074a..5a0cf2b 100644 --- a/internal/console/hooks.go +++ b/internal/console/hooks.go @@ -179,7 +179,8 @@ func (c *Console) handleClients(w http.ResponseWriter, r *http.Request) { inCluster[o.Name] = o } groups, _ := view["groups"].([]any) - var merged []any + // Never nil: an empty list must be [] on the wire, not null. + merged := []any{} seen := map[string]bool{} for _, g := range groups { m, _ := g.(map[string]any)