From af84b34f59ed4259f8ea75d03a4d0fbd4689f05f Mon Sep 17 00:00:00 2001 From: roylibman <93214016+roylibman@users.noreply.github.com> Date: Tue, 27 Oct 2026 18:30:53 -0700 Subject: [PATCH 1/3] radar-hub: podLabels and hub.installPodLabels podLabels adds labels to the hub, web and bundled Postgres pods, and (as HUB_AGENT_JOB_POD_LABELS) to the AI investigation Jobs the hub starts. hub.installPodLabels (HUB_INSTALL_POD_LABELS) adds labels to the Radar install commands the hub shows. Both exist for Google Cloud Marketplace consumption tracking, which needs a goog-partner-solution label on every pod in the customer's project. The schema enforces Kubernetes label syntax, and the chart refuses its own selector keys in podLabels. Staged as 1.15.0-rc.1 with appVersion unchanged. Co-Authored-By: Claude Opus 5.5 --- charts/radar-hub/Chart.yaml | 2 +- charts/radar-hub/README.md | 23 ++++++++++++++ charts/radar-hub/templates/_helpers.tpl | 31 +++++++++++++++++++ .../radar-hub/templates/deployment-hub.yaml | 16 ++++++++++ .../radar-hub/templates/deployment-web.yaml | 3 ++ .../radar-hub/templates/postgres-bundled.yaml | 3 ++ charts/radar-hub/tests/render-matrix.sh | 20 ++++++++++++ charts/radar-hub/values.schema.json | 21 ++++++++++++- charts/radar-hub/values.yaml | 15 +++++++++ 9 files changed, 132 insertions(+), 2 deletions(-) diff --git a/charts/radar-hub/Chart.yaml b/charts/radar-hub/Chart.yaml index e27c5a6..f2165a8 100644 --- a/charts/radar-hub/Chart.yaml +++ b/charts/radar-hub/Chart.yaml @@ -3,7 +3,7 @@ name: radar-hub description: Radar Cloud control plane for self-hosted deployments — Go API + React web app + Postgres (bundled eval DB or bring-your-own). type: application -version: 1.14.1 +version: 1.15.0-rc.1 # The Hub release this chart installs. image.hub.tag and image.web.tag both # default to it when unset. Deliberately independent of `version` above — see # README.md. Rewritten by the release job in skyhook-dev/radar-hub; do not carry diff --git a/charts/radar-hub/README.md b/charts/radar-hub/README.md index 98f3166..f73d7d3 100644 --- a/charts/radar-hub/README.md +++ b/charts/radar-hub/README.md @@ -161,6 +161,29 @@ See [`values.yaml`](values.yaml) for the full set, and [`values.schema.json`](values.schema.json) for the enforced schema. Invalid values are rejected at install time rather than surfacing as a broken pod. +## Pod labels + +`podLabels` adds labels to every pod the chart runs: hub, web and the bundled +Postgres. On Hub releases that support it, they also go on the AI +investigation Jobs the hub starts. `hub.installPodLabels` adds labels to the +Radar install commands the hub shows, so the agent in each connected cluster +carries them; older Hub releases ignore it. + +If you bought Radar through Google Cloud Marketplace, set Google's +consumption-tracking label in both: + +```yaml +podLabels: + goog-partner-solution: +hub: + installPodLabels: + goog-partner-solution: +``` + +Keys and values follow Kubernetes label syntax, which the schema enforces. The +chart's own selector labels (`app.kubernetes.io/name`, `instance`, +`component`) can't be set through `podLabels`. + ## AI agent Release prerequisite: this feature requires a Hub release with Anthropic provider diff --git a/charts/radar-hub/templates/_helpers.tpl b/charts/radar-hub/templates/_helpers.tpl index 864f33d..2c8950f 100644 --- a/charts/radar-hub/templates/_helpers.tpl +++ b/charts/radar-hub/templates/_helpers.tpl @@ -39,6 +39,37 @@ app.kubernetes.io/name: {{ include "radar-hub.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} +{{/* +.Values.podLabels as YAML for a pod template's labels, or nothing. The chart's +own selector keys are refused: repeated in the same map they render a +duplicate key, and if a duplicate won it would detach the pod from its +Deployment or StatefulSet. +*/}} +{{- define "radar-hub.podLabels" -}} +{{- $reserved := list "app.kubernetes.io/name" "app.kubernetes.io/instance" "app.kubernetes.io/component" -}} +{{- range $k, $_ := .Values.podLabels -}} +{{- if has $k $reserved -}} +{{- fail (printf "podLabels: %s is one of the chart's own selector labels and can't be set" $k) -}} +{{- end -}} +{{- end -}} +{{- with .Values.podLabels -}} +{{- toYaml . -}} +{{- end -}} +{{- end }} + +{{/* +A label map as key=value,key=value for an environment variable. Label syntax +(enforced by the schema) has no "," or "=" in keys or values, so the hub can +split it back unambiguously. Keys come out sorted. +*/}} +{{- define "radar-hub.labelList" -}} +{{- $pairs := list -}} +{{- range $k, $v := . -}} +{{- $pairs = append $pairs (printf "%s=%s" $k $v) -}} +{{- end -}} +{{- join "," $pairs -}} +{{- end }} + {{- define "radar-hub.serviceAccountName" -}} {{- if .Values.serviceAccount.create }} {{- default (include "radar-hub.fullname" .) .Values.serviceAccount.name }} diff --git a/charts/radar-hub/templates/deployment-hub.yaml b/charts/radar-hub/templates/deployment-hub.yaml index 33e2e8a..31e36de 100644 --- a/charts/radar-hub/templates/deployment-hub.yaml +++ b/charts/radar-hub/templates/deployment-hub.yaml @@ -23,6 +23,9 @@ spec: labels: {{- include "radar-hub.selectorLabels" . | nindent 8 }} app.kubernetes.io/component: hub + {{- with include "radar-hub.podLabels" . }} + {{- . | nindent 8 }} + {{- end }} spec: serviceAccountName: {{ include "radar-hub.serviceAccountName" . }} {{- with .Values.imagePullSecrets }} @@ -389,6 +392,13 @@ spec: - name: HUB_ENROLLMENT_APPROVAL value: "manual" {{- end }} + {{- /* Labels for the Radar install commands the hub shows. + Emitted only when set, so an install that sets nothing + renders the same env as before. */}} + {{- with include "radar-hub.labelList" .Values.hub.installPodLabels }} + - name: HUB_INSTALL_POD_LABELS + value: {{ . | quote }} + {{- end }} {{- if include "radar-hub.webTLSIsPublic" . }} {{- /* The certificate that serves the hub's public address, public half only, so the hub can hand it to agents that @@ -452,6 +462,12 @@ spec: - name: HUB_AGENT_ENGINE_LABEL value: {{ . | quote }} {{- end }} + {{- /* podLabels go on the investigation Jobs too, so every pod + the release causes carries them. */}} + {{- with include "radar-hub.labelList" .Values.podLabels }} + - name: HUB_AGENT_JOB_POD_LABELS + value: {{ . | quote }} + {{- end }} {{- /* Fleet-wide kill switch for the AI analysis alert rules can attach to the issues they match. Emitted only when on: the hub defaults it off, so an absent variable is the same diff --git a/charts/radar-hub/templates/deployment-web.yaml b/charts/radar-hub/templates/deployment-web.yaml index 6ca5fa5..1eb4cfb 100644 --- a/charts/radar-hub/templates/deployment-web.yaml +++ b/charts/radar-hub/templates/deployment-web.yaml @@ -31,6 +31,9 @@ spec: labels: {{- include "radar-hub.selectorLabels" . | nindent 8 }} app.kubernetes.io/component: web + {{- with include "radar-hub.podLabels" . }} + {{- . | nindent 8 }} + {{- end }} spec: serviceAccountName: {{ include "radar-hub.serviceAccountName" . }} {{- with .Values.imagePullSecrets }} diff --git a/charts/radar-hub/templates/postgres-bundled.yaml b/charts/radar-hub/templates/postgres-bundled.yaml index 7ecdcab..cbcf25f 100644 --- a/charts/radar-hub/templates/postgres-bundled.yaml +++ b/charts/radar-hub/templates/postgres-bundled.yaml @@ -89,6 +89,9 @@ spec: labels: {{- include "radar-hub.selectorLabels" . | nindent 8 }} app.kubernetes.io/component: postgres + {{- with include "radar-hub.podLabels" . }} + {{- . | nindent 8 }} + {{- end }} spec: {{- with .Values.imagePullSecrets }} imagePullSecrets: diff --git a/charts/radar-hub/tests/render-matrix.sh b/charts/radar-hub/tests/render-matrix.sh index dd46aa4..3238809 100755 --- a/charts/radar-hub/tests/render-matrix.sh +++ b/charts/radar-hub/tests/render-matrix.sh @@ -442,6 +442,26 @@ if notes "${ING[@]}" | grep -qF "In-cluster agent address"; then printf ' FAIL else printf ' ok %-46s absent\n' "no in-cluster address without a listener"; fi rm -rf "$scratch" +echo "pod labels" +PL=(--set-string podLabels.goog-partner-solution=isol_plb32_0123abc) +PLRE='^\s+goog-partner-solution: isol_plb32_0123abc$' +has "hub pod carries podLabels" "$PLRE" "${PL[@]}" --show-only templates/deployment-hub.yaml +has "web pod carries podLabels" "$PLRE" "${PL[@]}" --show-only templates/deployment-web.yaml +has "bundled Postgres pod carries podLabels" "$PLRE" "${PL[@]}" --show-only templates/postgres-bundled.yaml +lacks "no podLabels by default" 'goog-partner-solution' +check "a selector key in podLabels" refuse --set-string 'podLabels.app\.kubernetes\.io/component=x' +check "a non-string podLabels value" refuse --set podLabels.team=1 +check "a podLabels value with a space" refuse --set-string 'podLabels.team=a b' +check "a podLabels key with a space" refuse --set-string 'podLabels.bad key=x' +check "a 64-character install label value" refuse --set-string "hub.installPodLabels.x=$(printf 'a%.0s' $(seq 64))" +check "a prefixed label key" render --set-string 'podLabels.example\.com/team=platform' +envis "installPodLabels reach the hub" HUB_INSTALL_POD_LABELS 'a=1,goog-partner-solution=isol_x' --set-string hub.installPodLabels.goog-partner-solution=isol_x --set-string hub.installPodLabels.a=1 +lacks "no install-labels env by default" 'name: HUB_INSTALL_POD_LABELS' +AGF=(-f ci/agent-anthropic-values.yaml) +envis "podLabels reach the AI investigation Jobs" HUB_AGENT_JOB_POD_LABELS 'goog-partner-solution=isol_plb32_0123abc' "${AGF[@]}" "${PL[@]}" +lacks "no Job-labels env without podLabels" 'name: HUB_AGENT_JOB_POD_LABELS' "${AGF[@]}" +lacks "no Job-labels env with the agent off" 'name: HUB_AGENT_JOB_POD_LABELS' "${PL[@]}" + echo if [ $fails -eq 0 ]; then echo "all checks passed"; else echo "$fails check(s) failed"; fi exit $fails diff --git a/charts/radar-hub/values.schema.json b/charts/radar-hub/values.schema.json index 7e2cc6b..bf6c704 100644 --- a/charts/radar-hub/values.schema.json +++ b/charts/radar-hub/values.schema.json @@ -201,6 +201,9 @@ "podAnnotations": { "type": "object" }, + "installPodLabels": { + "$ref": "#/definitions/labels" + }, "nodeSelector": { "type": "object" }, @@ -808,6 +811,9 @@ "securityContext": { "type": "object" }, + "podLabels": { + "$ref": "#/definitions/labels" + }, "nameOverride": { "type": "string" }, @@ -815,5 +821,18 @@ "type": "string" } }, - "additionalProperties": false + "additionalProperties": false, + "definitions": { + "labels": { + "description": "Kubernetes labels: keys are an optional DNS-subdomain prefix and a name of at most 63 characters; values are strings of at most 63 letters, digits, '-', '_' or '.', starting and ending with a letter or digit.", + "type": "object", + "propertyNames": { + "pattern": "^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?[A-Za-z0-9]([-A-Za-z0-9_.]{0,61}[A-Za-z0-9])?$" + }, + "additionalProperties": { + "type": "string", + "pattern": "^(([A-Za-z0-9][-A-Za-z0-9_.]{0,61})?[A-Za-z0-9])?$" + } + } + } } diff --git a/charts/radar-hub/values.yaml b/charts/radar-hub/values.yaml index 75cc0b5..166d34e 100644 --- a/charts/radar-hub/values.yaml +++ b/charts/radar-hub/values.yaml @@ -335,6 +335,11 @@ hub: memory: 1Gi podAnnotations: {} + # Labels the hub adds to the Radar install commands it shows for connecting + # a cluster, so the Radar agent in each cluster carries them. Bought through + # Google Cloud Marketplace? Set the same label as podLabels. Needs a Hub + # release that supports it; older ones ignore it. + installPodLabels: {} # Official images are published for linux/amd64 and linux/arm64, so only the # OS is pinned by default. Add kubernetes.io/arch: to constrain # it; Helm merges that key with this default selector. @@ -819,6 +824,16 @@ securityContext: drop: ["ALL"] readOnlyRootFilesystem: false # web image needs writable /var/cache/nginx + /tmp/run +# Extra labels on every pod this chart runs (hub, web and the bundled +# Postgres) and, on Hub releases that support it, on the AI investigation Jobs +# the hub starts. Bought through Google Cloud Marketplace? Set Google's +# consumption-tracking label here, and in hub.installPodLabels: +# podLabels: +# goog-partner-solution: +# Kubernetes label syntax. The chart's own selector labels +# (app.kubernetes.io/name, instance, component) can't be set here. +podLabels: {} + # --------------------------------------------------------------------------- # Names # --------------------------------------------------------------------------- From f03bda4ce6fd66970efb1f1b1bde18a1cbeee225 Mon Sep 17 00:00:00 2001 From: roylibman <93214016+roylibman@users.noreply.github.com> Date: Tue, 27 Oct 2026 19:14:26 -0700 Subject: [PATCH 2/3] radar-hub: pod-label checks pin the pod template; spell out selector keys Co-Authored-By: Claude Opus 5.5 --- charts/radar-hub/README.md | 5 +++-- charts/radar-hub/tests/render-matrix.sh | 3 ++- charts/radar-hub/values.yaml | 3 ++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/charts/radar-hub/README.md b/charts/radar-hub/README.md index f73d7d3..e2f1c0c 100644 --- a/charts/radar-hub/README.md +++ b/charts/radar-hub/README.md @@ -181,8 +181,9 @@ hub: ``` Keys and values follow Kubernetes label syntax, which the schema enforces. The -chart's own selector labels (`app.kubernetes.io/name`, `instance`, -`component`) can't be set through `podLabels`. +chart's own selector labels (`app.kubernetes.io/name`, +`app.kubernetes.io/instance` and `app.kubernetes.io/component`) can't be set +through `podLabels`. ## AI agent diff --git a/charts/radar-hub/tests/render-matrix.sh b/charts/radar-hub/tests/render-matrix.sh index 3238809..f453cd9 100755 --- a/charts/radar-hub/tests/render-matrix.sh +++ b/charts/radar-hub/tests/render-matrix.sh @@ -444,7 +444,8 @@ rm -rf "$scratch" echo "pod labels" PL=(--set-string podLabels.goog-partner-solution=isol_plb32_0123abc) -PLRE='^\s+goog-partner-solution: isol_plb32_0123abc$' +# 8 spaces: under spec.template.metadata.labels, not the Deployment's own labels. +PLRE='^ {8}goog-partner-solution: isol_plb32_0123abc$' has "hub pod carries podLabels" "$PLRE" "${PL[@]}" --show-only templates/deployment-hub.yaml has "web pod carries podLabels" "$PLRE" "${PL[@]}" --show-only templates/deployment-web.yaml has "bundled Postgres pod carries podLabels" "$PLRE" "${PL[@]}" --show-only templates/postgres-bundled.yaml diff --git a/charts/radar-hub/values.yaml b/charts/radar-hub/values.yaml index 166d34e..7539ce7 100644 --- a/charts/radar-hub/values.yaml +++ b/charts/radar-hub/values.yaml @@ -831,7 +831,8 @@ securityContext: # podLabels: # goog-partner-solution: # Kubernetes label syntax. The chart's own selector labels -# (app.kubernetes.io/name, instance, component) can't be set here. +# (app.kubernetes.io/name, app.kubernetes.io/instance and +# app.kubernetes.io/component) can't be set here. podLabels: {} # --------------------------------------------------------------------------- From 016b6caf379a5600e1dbf4f7b87ea4814c4608f0 Mon Sep 17 00:00:00 2001 From: roylibman <93214016+roylibman@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:25:14 -0700 Subject: [PATCH 3/3] radar-hub: refuse agent selector keys in installPodLabels; cap label prefixes hub.installPodLabels go on every Radar agent's pod template, so the agent chart's selector keys (app.kubernetes.io/name, app.kubernetes.io/instance) are refused there, as the chart's own are in podLabels. Label keys' prefix is capped at Kubernetes' 253 characters. Co-Authored-By: Claude Opus 5.5 --- charts/radar-hub/README.md | 3 ++- charts/radar-hub/templates/_helpers.tpl | 16 ++++++++++++++++ charts/radar-hub/templates/deployment-hub.yaml | 2 +- charts/radar-hub/tests/render-matrix.sh | 4 ++++ charts/radar-hub/values.schema.json | 7 +++++-- 5 files changed, 28 insertions(+), 4 deletions(-) diff --git a/charts/radar-hub/README.md b/charts/radar-hub/README.md index e2f1c0c..3eaad9a 100644 --- a/charts/radar-hub/README.md +++ b/charts/radar-hub/README.md @@ -183,7 +183,8 @@ hub: Keys and values follow Kubernetes label syntax, which the schema enforces. The chart's own selector labels (`app.kubernetes.io/name`, `app.kubernetes.io/instance` and `app.kubernetes.io/component`) can't be set -through `podLabels`. +through `podLabels`, nor the Radar agent chart's (`app.kubernetes.io/name` and +`app.kubernetes.io/instance`) through `hub.installPodLabels`. ## AI agent diff --git a/charts/radar-hub/templates/_helpers.tpl b/charts/radar-hub/templates/_helpers.tpl index 2c8950f..322c7bf 100644 --- a/charts/radar-hub/templates/_helpers.tpl +++ b/charts/radar-hub/templates/_helpers.tpl @@ -57,6 +57,22 @@ Deployment or StatefulSet. {{- end -}} {{- end }} +{{/* +.Values.hub.installPodLabels as key=value,key=value for HUB_INSTALL_POD_LABELS, +or nothing. The Radar agent chart's selector keys are refused: the hub puts +these labels on every agent's pod template, where either key would render a +duplicate or detach the pod from its Deployment. +*/}} +{{- define "radar-hub.installPodLabels" -}} +{{- $reserved := list "app.kubernetes.io/name" "app.kubernetes.io/instance" -}} +{{- range $k, $_ := .Values.hub.installPodLabels -}} +{{- if has $k $reserved -}} +{{- fail (printf "hub.installPodLabels: %s is one of the Radar chart's selector labels and can't be set" $k) -}} +{{- end -}} +{{- end -}} +{{- include "radar-hub.labelList" .Values.hub.installPodLabels -}} +{{- end }} + {{/* A label map as key=value,key=value for an environment variable. Label syntax (enforced by the schema) has no "," or "=" in keys or values, so the hub can diff --git a/charts/radar-hub/templates/deployment-hub.yaml b/charts/radar-hub/templates/deployment-hub.yaml index 31e36de..6137144 100644 --- a/charts/radar-hub/templates/deployment-hub.yaml +++ b/charts/radar-hub/templates/deployment-hub.yaml @@ -395,7 +395,7 @@ spec: {{- /* Labels for the Radar install commands the hub shows. Emitted only when set, so an install that sets nothing renders the same env as before. */}} - {{- with include "radar-hub.labelList" .Values.hub.installPodLabels }} + {{- with include "radar-hub.installPodLabels" . }} - name: HUB_INSTALL_POD_LABELS value: {{ . | quote }} {{- end }} diff --git a/charts/radar-hub/tests/render-matrix.sh b/charts/radar-hub/tests/render-matrix.sh index f453cd9..d999613 100755 --- a/charts/radar-hub/tests/render-matrix.sh +++ b/charts/radar-hub/tests/render-matrix.sh @@ -456,6 +456,10 @@ check "a podLabels value with a space" refuse --set-string 'podLabels.team check "a podLabels key with a space" refuse --set-string 'podLabels.bad key=x' check "a 64-character install label value" refuse --set-string "hub.installPodLabels.x=$(printf 'a%.0s' $(seq 64))" check "a prefixed label key" render --set-string 'podLabels.example\.com/team=platform' +check "a 253-character label prefix" render --set-string "podLabels.$(printf 'a%.0s' $(seq 253))/team=x" +check "a 254-character label prefix" refuse --set-string "podLabels.$(printf 'a%.0s' $(seq 254))/team=x" +check "an agent selector key in installPodLabels" refuse --set-string 'hub.installPodLabels.app\.kubernetes\.io/name=x' +check "the other agent selector key" refuse --set-string 'hub.installPodLabels.app\.kubernetes\.io/instance=x' envis "installPodLabels reach the hub" HUB_INSTALL_POD_LABELS 'a=1,goog-partner-solution=isol_x' --set-string hub.installPodLabels.goog-partner-solution=isol_x --set-string hub.installPodLabels.a=1 lacks "no install-labels env by default" 'name: HUB_INSTALL_POD_LABELS' AGF=(-f ci/agent-anthropic-values.yaml) diff --git a/charts/radar-hub/values.schema.json b/charts/radar-hub/values.schema.json index bf6c704..c4b3537 100644 --- a/charts/radar-hub/values.schema.json +++ b/charts/radar-hub/values.schema.json @@ -824,10 +824,13 @@ "additionalProperties": false, "definitions": { "labels": { - "description": "Kubernetes labels: keys are an optional DNS-subdomain prefix and a name of at most 63 characters; values are strings of at most 63 letters, digits, '-', '_' or '.', starting and ending with a letter or digit.", + "description": "Kubernetes labels: keys are an optional DNS-subdomain prefix (at most 253 characters) and a name of at most 63 characters; values are strings of at most 63 letters, digits, '-', '_' or '.', starting and ending with a letter or digit.", "type": "object", "propertyNames": { - "pattern": "^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?[A-Za-z0-9]([-A-Za-z0-9_.]{0,61}[A-Za-z0-9])?$" + "pattern": "^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?[A-Za-z0-9]([-A-Za-z0-9_.]{0,61}[A-Za-z0-9])?$", + "not": { + "pattern": "^[^/]{254,}/" + } }, "additionalProperties": { "type": "string",