From 7f7ecdd9b2d2a9a23c1270790f49ca67411f3bba Mon Sep 17 00:00:00 2001 From: Matthias Linhuber Date: Thu, 24 Sep 2026 15:19:54 +0200 Subject: [PATCH 1/2] fix: the EnvoyProxy must state its replica count, or the data plane stays down Envoy Gateway reconciles the data plane Deployment's replica count only while the EnvoyProxy names it. Left out, the controller writes the field once when it creates the Deployment and never looks at it again - deliberately, so an HPA can own it - which means a `kubectl scale --replicas=0` is permanent. That is how the shared Gateway on parma went down: the proxy Deployment was scaled to 0 through the Rancher UI on 2026-09-23, the Gateway went Programmed=False with reason NoResources, and nothing was listening on the node's :80/:443 any more, so bonn.eduide.aet.cit.tum.de and mannheim.eduide.aet.cit.tum.de refused connections while every pod in both namespaces stayed healthy. Nothing put it back, and nothing would have. `envoyProxy.replicas` now defaults to 1 and renders into the spec at provider.kubernetes.envoyDeployment.replicas, so the controller owns the field and reverts a manual scale. A spec that sets its own replicas still wins, and `replicas: null` hands the field back to an HPA. Co-Authored-By: Claude Opus 5 --- charts/eduide-cluster/README.md | 1 + .../templates/gateway/envoyproxy.yaml | 15 ++++++++++++++- charts/eduide-cluster/values.yaml | 7 +++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/charts/eduide-cluster/README.md b/charts/eduide-cluster/README.md index 1c1f450..2073836 100644 --- a/charts/eduide-cluster/README.md +++ b/charts/eduide-cluster/README.md @@ -22,6 +22,7 @@ cert-manager issuers. Install once per cluster, before any eduide release. | envoyProxy.labels | object | `{}` | | | envoyProxy.name | string | `"theia-shared-gateway"` | | | envoyProxy.namespace | string | `"envoy-gateway-system"` | | +| envoyProxy.replicas | int | `1` | Data plane replica count, rendered into the spec at `provider.kubernetes.envoyDeployment.replicas`. Envoy Gateway stops reconciling that field when the EnvoyProxy leaves it out, which makes a manual scale to zero permanent and the Gateway unreachable for good. Set to `null` to hand the field back to an HPA; a `spec` that names its own replicas wins over this. | | envoyProxy.spec | object | `{}` | | | gateway | object | `{"addresses":[],"allowedRoutes":{"namespaces":{"from":"All"}},"annotations":{},"className":"envoy","create":true,"labels":{},"listeners":[],"name":"theia-shared-gateway","namespace":"eduide-system","redirects":[]}` | ------------------------------------------------------------------------ | | gateway.create | bool | `true` | Create the shared Gateway. Set false only if you terminate and route traffic some other way; EduIDE then has no ingress of its own. | diff --git a/charts/eduide-cluster/templates/gateway/envoyproxy.yaml b/charts/eduide-cluster/templates/gateway/envoyproxy.yaml index 8671814..00efc34 100644 --- a/charts/eduide-cluster/templates/gateway/envoyproxy.yaml +++ b/charts/eduide-cluster/templates/gateway/envoyproxy.yaml @@ -1,4 +1,17 @@ {{- if .Values.envoyProxy.create }} +{{- /* +Envoy Gateway only owns the data plane Deployment's replica count while the +EnvoyProxy states it. Omit it and the controller writes the field once at +creation and never looks at it again, so a single `kubectl scale --replicas=0` +- or a stray click in a cluster UI - takes every Gateway on the class down +until somebody scales it back by hand. Default it here, and let a spec that +sets its own replicas (an HPA-driven cluster, say) win. +*/}} +{{- $spec := deepCopy (default (dict) .Values.envoyProxy.spec) }} +{{- if not (kindIs "invalid" .Values.envoyProxy.replicas) }} +{{- $default := dict "provider" (dict "kubernetes" (dict "envoyDeployment" (dict "replicas" .Values.envoyProxy.replicas))) }} +{{- $spec = mergeOverwrite $default $spec }} +{{- end }} apiVersion: gateway.envoyproxy.io/v1alpha1 kind: EnvoyProxy metadata: @@ -13,5 +26,5 @@ metadata: {{ toYaml . | nindent 4 }} {{- end }} spec: -{{ toYaml .Values.envoyProxy.spec | nindent 2 }} +{{ toYaml $spec | nindent 2 }} {{- end }} diff --git a/charts/eduide-cluster/values.yaml b/charts/eduide-cluster/values.yaml index 5844360..222c0ac 100644 --- a/charts/eduide-cluster/values.yaml +++ b/charts/eduide-cluster/values.yaml @@ -133,6 +133,13 @@ envoyProxy: namespace: envoy-gateway-system labels: {} annotations: {} + # -- Data plane replica count, rendered into the spec at + # `provider.kubernetes.envoyDeployment.replicas`. Envoy Gateway stops + # reconciling that field when the EnvoyProxy leaves it out, which makes a + # manual scale to zero permanent and the Gateway unreachable for good. Set to + # `null` to hand the field back to an HPA; a `spec` that names its own + # replicas wins over this. + replicas: 1 spec: {} managedCertificates: enabled: false From 779448e7b1aed8f5e47301a96bc994b93684046c Mon Sep 17 00:00:00 2001 From: Matthias Linhuber Date: Thu, 24 Sep 2026 15:28:36 +0200 Subject: [PATCH 2/2] fix: default replicas only where a Deployment is what actually scales The default injected `provider.kubernetes` unconditionally, which broke three specs the chart has to leave alone. An empty `envoyProxy.spec` rendered `provider` with no `type`. The CRD marks `type` required as soon as `provider` exists, so `create: true` with default values - valid before this branch, since `spec` itself is optional - produced a resource the API server rejects. The default now carries `type: Kubernetes`, and a spec that names its own type still wins. An `envoyDaemonSet` spec was rejected outright: the CRD's CEL rule permits envoyDeployment or envoyDaemonSet, never both. An `envoyHpa` spec already has an owner for the field, and a `Host` provider has no Deployment at all. All three are now skipped rather than merged into. Co-Authored-By: Claude Opus 5 --- charts/eduide-cluster/README.md | 2 +- .../templates/gateway/envoyproxy.yaml | 13 +++++++++++-- charts/eduide-cluster/values.yaml | 7 ++++--- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/charts/eduide-cluster/README.md b/charts/eduide-cluster/README.md index 2073836..be06d4c 100644 --- a/charts/eduide-cluster/README.md +++ b/charts/eduide-cluster/README.md @@ -22,7 +22,7 @@ cert-manager issuers. Install once per cluster, before any eduide release. | envoyProxy.labels | object | `{}` | | | envoyProxy.name | string | `"theia-shared-gateway"` | | | envoyProxy.namespace | string | `"envoy-gateway-system"` | | -| envoyProxy.replicas | int | `1` | Data plane replica count, rendered into the spec at `provider.kubernetes.envoyDeployment.replicas`. Envoy Gateway stops reconciling that field when the EnvoyProxy leaves it out, which makes a manual scale to zero permanent and the Gateway unreachable for good. Set to `null` to hand the field back to an HPA; a `spec` that names its own replicas wins over this. | +| envoyProxy.replicas | int | `1` | Data plane replica count, rendered into the spec at `provider.kubernetes.envoyDeployment.replicas`. Envoy Gateway stops reconciling that field when the EnvoyProxy leaves it out, which makes a manual scale to zero permanent and the Gateway unreachable for good. A `spec` that names its own replicas wins over this, and `null` leaves the field out entirely. Ignored for a spec that scales some other way - an `envoyHpa`, an `envoyDaemonSet`, or a `Host` provider. | | envoyProxy.spec | object | `{}` | | | gateway | object | `{"addresses":[],"allowedRoutes":{"namespaces":{"from":"All"}},"annotations":{},"className":"envoy","create":true,"labels":{},"listeners":[],"name":"theia-shared-gateway","namespace":"eduide-system","redirects":[]}` | ------------------------------------------------------------------------ | | gateway.create | bool | `true` | Create the shared Gateway. Set false only if you terminate and route traffic some other way; EduIDE then has no ingress of its own. | diff --git a/charts/eduide-cluster/templates/gateway/envoyproxy.yaml b/charts/eduide-cluster/templates/gateway/envoyproxy.yaml index 00efc34..8750d0c 100644 --- a/charts/eduide-cluster/templates/gateway/envoyproxy.yaml +++ b/charts/eduide-cluster/templates/gateway/envoyproxy.yaml @@ -6,10 +6,19 @@ creation and never looks at it again, so a single `kubectl scale --replicas=0` - or a stray click in a cluster UI - takes every Gateway on the class down until somebody scales it back by hand. Default it here, and let a spec that sets its own replicas (an HPA-driven cluster, say) win. + +Three specs must be left alone. A DaemonSet data plane rejects the resource +outright - the CRD permits envoyDeployment or envoyDaemonSet, never both - an +HPA already owns the field, and a `Host` provider has no Deployment to scale. +`type` comes along with the default because the CRD requires it as soon as +`provider` exists at all, and a spec of `{}` is a valid install. */}} {{- $spec := deepCopy (default (dict) .Values.envoyProxy.spec) }} -{{- if not (kindIs "invalid" .Values.envoyProxy.replicas) }} -{{- $default := dict "provider" (dict "kubernetes" (dict "envoyDeployment" (dict "replicas" .Values.envoyProxy.replicas))) }} +{{- $kubernetes := default (dict) (dig "provider" "kubernetes" (dict) $spec) }} +{{- $onKubernetes := eq (dig "provider" "type" "Kubernetes" $spec) "Kubernetes" }} +{{- $ownsReplicas := or (hasKey $kubernetes "envoyDaemonSet") (hasKey $kubernetes "envoyHpa") }} +{{- if and (not (kindIs "invalid" .Values.envoyProxy.replicas)) $onKubernetes (not $ownsReplicas) }} +{{- $default := dict "provider" (dict "type" "Kubernetes" "kubernetes" (dict "envoyDeployment" (dict "replicas" .Values.envoyProxy.replicas))) }} {{- $spec = mergeOverwrite $default $spec }} {{- end }} apiVersion: gateway.envoyproxy.io/v1alpha1 diff --git a/charts/eduide-cluster/values.yaml b/charts/eduide-cluster/values.yaml index 222c0ac..3365899 100644 --- a/charts/eduide-cluster/values.yaml +++ b/charts/eduide-cluster/values.yaml @@ -136,9 +136,10 @@ envoyProxy: # -- Data plane replica count, rendered into the spec at # `provider.kubernetes.envoyDeployment.replicas`. Envoy Gateway stops # reconciling that field when the EnvoyProxy leaves it out, which makes a - # manual scale to zero permanent and the Gateway unreachable for good. Set to - # `null` to hand the field back to an HPA; a `spec` that names its own - # replicas wins over this. + # manual scale to zero permanent and the Gateway unreachable for good. A + # `spec` that names its own replicas wins over this, and `null` leaves the + # field out entirely. Ignored for a spec that scales some other way - an + # `envoyHpa`, an `envoyDaemonSet`, or a `Host` provider. replicas: 1 spec: {} managedCertificates: