From b0aa3e4266a7548c55d81af82068a33bc8c4018c Mon Sep 17 00:00:00 2001 From: Rowan Ruseler Date: Mon, 23 Feb 2026 15:20:25 +0100 Subject: [PATCH] Fix duplicate port names in controller-manager containers (#751) * Fix duplicate port names in controller-manager containers Multiple controller-manager containers were using the same "heathz" port name, causing Kubernetes warnings about duplicate ports in the StatefulSet. This also affected the prometheus port "prom-cm". Changes: * Renamed healthz port to hp-cm (health port - controller manager) * Renamed prom-cm to pm-cm for consistency * Addedd {{ .portSuffix }} variable to differentiate external controller ports * Implemented port suffix logic The suffix logic handles cluster names by: 1. Names <9 chars: use full name as suffic * e.g.: child01 -> -child01 2. Names with trailing numbers: preserve the number format users chose * Detects 1-2 digit numbers with optional hyphen * Truncates base name to fit within 15 chars * e.g.: verlongcluster-01 -> -verylo-01 3. Names without numbers: use SHA-256 hash for uniqueness * Trunactes name to 5 chars and appends 3-char hash * e.g.: verlongclustername -> -veryl-a3f The logic separates container suffix (full name) from port suffix (truncated) so container names remain descriptive while port names stay compliant. Fixes #525 #655 Signed-off-by: Rowan Ruseler * Add optional port name overrides for ext. controller The auto-generated port name suffixes for external controller manager can collide when cluster names are similar, as the 3-character has provides only 4,096 possibilities. With the optional healthPortName and prometheusPortName fields to cluster configuration, allows users to explicity set port names when automatica generation creates collisions. Signed-off-by: Rowan Ruseler * Fix portSuffix generation Changed from "and" to "or", so portSuffic is calculated when either healthPortName or prometheusPortName is unset. Signed-off-by: Rowan Ruseler --------- Signed-off-by: Rowan Ruseler Co-authored-by: kfox1111 --- charts/spire/charts/spire-server/README.md | 2 +- .../_controller-manager-container.tpl | 58 +++++++++++++++++-- charts/spire/charts/spire-server/values.yaml | 4 +- 3 files changed, 56 insertions(+), 8 deletions(-) diff --git a/charts/spire/charts/spire-server/README.md b/charts/spire/charts/spire-server/README.md index 4d827ff..1b0c2bf 100644 --- a/charts/spire/charts/spire-server/README.md +++ b/charts/spire/charts/spire-server/README.md @@ -356,7 +356,7 @@ In order to run Tornjak with simple HTTP Connection only, make sure you don't cr | `externalControllerManagers.defaults.configMap.annotations` | Annotations to add to the Controller Manager ConfigMap | `{}` | | `externalControllerManagers.defaults.ignoreNamespaces` | These namespaces are ignored by controller manager | `[]` | | `externalControllerManagers.defaults.cacheNamespaces` | If specified restricts the manager's cache to watch objects in the desired namespaces. Defaults to all namespaces. | `{}` | -| `externalControllerManagers.clusters` | A dictionary of clusters to add with optional overrides. If empty, all clusters defined in kubeConfigs will be used. | `{}` | +| `externalControllerManagers.clusters` | A dictionary of clusters to add with optional overrides (kubeConfigName, reconcile, healthPortName, prometheusPortName). If empty, all clusters defined in kubeConfigs will be used. | `{}` | | `tools.kubectl.image.registry` | The OCI registry to pull the image from | `registry.k8s.io` | | `tools.kubectl.image.repository` | The repository within the registry | `kubectl` | | `tools.kubectl.image.pullPolicy` | The image pull policy | `IfNotPresent` | diff --git a/charts/spire/charts/spire-server/templates/_controller-manager-container.tpl b/charts/spire/charts/spire-server/templates/_controller-manager-container.tpl index 5ce76c3..2b56b78 100644 --- a/charts/spire/charts/spire-server/templates/_controller-manager-container.tpl +++ b/charts/spire/charts/spire-server/templates/_controller-manager-container.tpl @@ -13,7 +13,7 @@ {{- if or .Values.controllerManager.reconcile.clusterSPIFFEIDs .Values.controllerManager.reconcile.clusterStaticEntries }} {{- $reconcileEntries = add $reconcileEntries 1 }} {{- end }} -{{- include "spire-controller-manager.container" (dict "Values" .Values "Chart" .Chart "startPort" $startPort "suffix" "" "settings" $settings "defaults" $defaults "webhooksEnabled" $webhooksEnabled) }} +{{- include "spire-controller-manager.container" (dict "Values" .Values "Chart" .Chart "startPort" $startPort "suffix" "" "portSuffix" "" "healthPortName" "" "prometheusPortName" "" "settings" $settings "defaults" $defaults "webhooksEnabled" $webhooksEnabled) }} {{- end }} {{- if .Values.externalControllerManagers.enabled }} {{- $clusters := default .Values.kubeConfigs .Values.externalControllerManagers.clusters }} @@ -23,7 +23,43 @@ {{- if hasKey $root.Values.externalControllerManagers.clusters $name }} {{- $clusterSettings = index $root.Values.externalControllerManagers.clusters $name }} {{- end }} + +{{/* +Generate port names for controller-manager ports. +Can be explicitly set via healthPortName and prometheusPortName in cluster configuration. +Otherwise uses default prefixes (hp-cm/pm-cm) with auto-generated suffixes. +Auto-generation preserves trailing numbers from cluster names or uses hash for uniqueness. +*/}} {{- $suffix := printf "-%s" $name }} +{{- $portSuffix := $suffix }} +{{- $healthPortName := "" }} +{{- $prometheusPortName := "" }} +{{- if hasKey $clusterSettings "healthPortName" }} +{{- $healthPortName = $clusterSettings.healthPortName }} +{{- end }} +{{- if hasKey $clusterSettings "prometheusPortName" }} +{{- $prometheusPortName = $clusterSettings.prometheusPortName }} +{{- end }} +{{- if or (eq $healthPortName "") (eq $prometheusPortName "") }} +{{- if gt (len $name) 9 }} +{{- $numberMatch := regexFind "[-]?[0-9]{1,2}$" $name }} +{{- if $numberMatch }} +{{- $numLen := len $numberMatch }} +{{- $baseLen := sub (len $name) $numLen | int }} +{{- $baseName := substr 0 $baseLen $name }} +{{- if not (hasPrefix "-" $numberMatch) }} +{{- $numberMatch = printf "-%s" $numberMatch }} +{{- end }} +{{- $maxBase := sub 9 (len $numberMatch) | int }} +{{- $baseName = $baseName | trunc $maxBase | trimSuffix "-" }} +{{- $portSuffix = printf "-%s%s" $baseName $numberMatch }} +{{- else }} +{{- $hash := sha256sum $name | trunc 3 }} +{{- $portSuffix = printf "-%s-%s" ($name | trunc 5 | trimSuffix "-") $hash }} +{{- end }} +{{- end }} +{{- end }} + {{- $startPort = add $startPort 2 }} {{- $kubeConfig := $name }} {{- if hasKey $clusterSettings "kubeConfigName" }} @@ -41,7 +77,7 @@ {{- if gt $reconcileFederation 1 }} {{- fail "You can only have one controller-manager with reconcile.clusterFederatedTrustDomains set to true" }} {{- end }} -{{- include "spire-controller-manager.container" (dict "Values" $root.Values "Chart" $root.Chart "startPort" $startPort "suffix" $suffix "settings" $clusterSettings "defaults" $clusterDefaults "webhooksEnabled" false "kubeConfig" $kubeConfig ) }} +{{- include "spire-controller-manager.container" (dict "Values" $root.Values "Chart" $root.Chart "startPort" $startPort "suffix" $suffix "portSuffix" $portSuffix "healthPortName" $healthPortName "prometheusPortName" $prometheusPortName "settings" $clusterSettings "defaults" $clusterDefaults "webhooksEnabled" false "kubeConfig" $kubeConfig ) }} {{- end }} {{- end }} {{- end }} @@ -83,27 +119,37 @@ {{- if gt (len $extraEnv) 0 }} {{- $extraEnv | toYaml | nindent 4 }} {{- end }} + {{/* Port names: hp-cm (health), pm-cm (prometheus) - abbreviated for 15 char limit */}} + {{/* Can be overridden via healthPortName and prometheusPortName in cluster config */}} ports: {{- if .webhooksEnabled }} - name: https containerPort: 9443 protocol: TCP {{- end }} + {{- $hpName := .healthPortName }} + {{- if eq $hpName "" }} + {{- $hpName = printf "hp-cm%s" .portSuffix }} + {{- end }} - containerPort: {{ $healthPort }} - name: healthz + name: {{ $hpName }} {{- if or (dig "telemetry" "prometheus" "enabled" .Values.telemetry.prometheus.enabled .Values.global) (and (dig "spire" "recommendations" "enabled" false .Values.global) (dig "spire" "recommendations" "prometheus" true .Values.global)) }} + {{- $pmName := .prometheusPortName }} + {{- if eq $pmName "" }} + {{- $pmName = printf "pm-cm%s" .portSuffix }} + {{- end }} - containerPort: {{ $promPort }} - name: prom-cm{{ .suffix }} + name: {{ $pmName }} {{- end }} {{- if eq .Values.controllerManager.staticManifestMode "off" }} livenessProbe: httpGet: path: /healthz - port: healthz + port: {{ $hpName }} readinessProbe: httpGet: path: /readyz - port: healthz + port: {{ $hpName }} {{- end }} resources: {{- toYaml .Values.controllerManager.resources | nindent 4 }} diff --git a/charts/spire/charts/spire-server/values.yaml b/charts/spire/charts/spire-server/values.yaml index b9fd12a..926898b 100644 --- a/charts/spire/charts/spire-server/values.yaml +++ b/charts/spire/charts/spire-server/values.yaml @@ -850,13 +850,15 @@ externalControllerManagers: ## @param externalControllerManagers.defaults.cacheNamespaces [object] If specified restricts the manager's cache to watch objects in the desired namespaces. Defaults to all namespaces. cacheNamespaces: {} - ## @param externalControllerManagers.clusters [object] A dictionary of clusters to add with optional overrides. If empty, all clusters defined in kubeConfigs will be used. + ## @param externalControllerManagers.clusters [object] A dictionary of clusters to add with optional overrides (kubeConfigName, reconcile, healthPortName, prometheusPortName). If empty, all clusters defined in kubeConfigs will be used. clusters: {} # clustera: # Should match the name of the config in the kubeConfigs section # kubeConfigName: foo # reconcile: # clusterStaticEntries: true + # healthPortName: "hp-clustera-01" + # prometheusPortName: "pm-clustera-01" # other: {} tools: