Skip to content

feat(eduide): maintenance page instead of Envoy's raw error, and rolling updates that keep a ready pod (chart 2.4.0) - #47

Open
Mtze wants to merge 1 commit into
mainfrom
feat/maintenance-page
Open

Mtze wants to merge 1 commit into
mainfrom
feat/maintenance-page

Conversation

@Mtze

@Mtze Mtze commented Sep 29, 2026 •

Copy link
Copy Markdown
Member

Why

During deploys the landing page sometimes had no ready pod, and visitors got Envoy's raw no healthy upstream / 503. This adds a static maintenance page and removes most of the downtime at its source.

What

Maintenance page

  • files/maintenance.html: self-contained page ("EduIDE is currently unavailable", short German line, inline logo, light/dark, reloads every 30s, no external assets).
  • templates/maintenance-page.yaml: ConfigMap maintenance-page + Envoy Gateway BackendTrafficPolicy on landing-route that answers 502/503/504 with the page. The status code stays 5xx. maintenancePage.enabled (default true).
  • Landing route only. The REST service and session routes keep their own errors.
  • Showing it on purpose means scaling landing-page-deployment to 0. EduIDE-deployment gets a workflow for that.

Rolling updates keep a ready pod

  • Landing page + REST service: maxUnavailable: 0 / maxSurge: 1, preStop sleep 10s.
  • REST service: tcpSocket readiness probe. It had none, so the old pod was killed before Quarkus listened.
  • landingPage.replicas, service.replicas (default 1). Above 1: a PodDisruptionBudget each (podDisruptionBudget.enabled, turn off on single-node clusters) and soft spreading across nodes.

Gotcha found on test1

Envoy treats the override body as a format string. A % in the page (height: 100%) makes Envoy drop the override silently: the policy still reports Accepted, and visitors get an empty 503. The page has no % now, and the test guards against it.

Tests

  • New scripts/test-maintenance-page.sh (bash + yq, same style as test-app-consistency.sh), wired into the lint job: policy, ConfigMap, toggles, rollout settings, PDB conditions, page size / no external assets / no %.
  • helm lint, test-app-consistency.sh, kubeconform (BTP + PDB validate against CRDs-catalog), and render-envs.sh diff: only the intended objects/fields per environment.

Verified on test1 (tum-student, Envoy Gateway v1.8.3)

  • Installed from this branch: policy Accepted on all test1 listeners, 2 PDBs, replicas on different nodes.
  • Landing scaled to 0: 503, text/html, 3222-byte page on / and deep links. Service unaffected. Scaled back: 200.
  • Polled landing + service every 0.5s during rollout restart: 0 errors. The first upgrade from the old template still had 2 single 503s, because those pods lacked the probe and preStop.
  • tum-production runs Envoy Gateway v1.5.9. Its CRD has responseOverride.valueRef.

Follow-up: EduIDE-deployment PR (maintenance workflow, replicas 2, pins) once 2.4.0 is released.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added a static maintenance page for landing-page outages, enabled by default when Envoy Gateway is available.
    • Added configurable replica counts for the landing page and REST service, with safer rolling updates and pod distribution across nodes.
    • Added disruption budgets for deployments with multiple replicas.
    • Added readiness checks and graceful shutdown handling for the REST service.
  • Documentation
    • Updated chart documentation and changelog for version 2.4.0.
  • Tests
    • Added automated checks for maintenance-page behavior and deployment settings.

…nd keep deploys from needing it

While the landing page had no ready pod, visitors got Envoy's plain-text
"no healthy upstream". A BackendTrafficPolicy on landing-route now answers
502/503/504 with files/maintenance.html, served by Envoy from a ConfigMap.

The page must not contain "%": Envoy reads the body as a format string and
otherwise drops the override while the policy still reports Accepted.

Deploys now keep a ready pod: maxUnavailable 0 and a 10s preStop drain for the
landing page and REST service, and a tcpSocket readiness probe for the service,
which had none. landingPage.replicas and service.replicas above 1 add a
PodDisruptionBudget each (podDisruptionBudget.enabled) and spread over nodes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

📝 Walkthrough

Walkthrough

The chart adds an Envoy Gateway maintenance response for landing-route errors 502, 503, and 504. It also adds configurable replicas and rollout protections for the landing-page and REST Deployments, with conditional PodDisruptionBudgets for multi-replica deployments.

Changes

Maintenance handling and workload availability

Layer / File(s) Summary
Landing-route maintenance response
charts/eduide/values.yaml, charts/eduide/files/maintenance.html, charts/eduide/templates/maintenance-page.yaml, charts/eduide/README.md, docs/charts.md, scripts/test-maintenance-page.sh, .github/workflows/ci.yml, charts/eduide/Chart.yaml, CHANGELOG.md
The chart adds an HTML maintenance page and a BackendTrafficPolicy that uses it for landing-route 502, 503, and 504 responses. The values, documentation, and CI test cover the configuration, rendered resources, and page constraints.
Deployment availability and scaling
charts/eduide/values.yaml, charts/eduide/templates/_helpers.tpl, charts/eduide/templates/landing-page.yaml, charts/eduide/templates/service.yaml, charts/eduide/templates/pod-disruption-budgets.yaml, charts/eduide/README.md, docs/charts.md, scripts/test-maintenance-page.sh
Both Deployments gain configurable replica counts, rolling updates, node spreading, and a 10-second preStop hook. The REST Deployment adds a readiness probe. PodDisruptionBudgets render for enabled Deployments with more than one replica.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant EnvoyGateway
  participant LandingRoute
  participant BackendTrafficPolicy
  participant MaintenanceConfigMap
  Client->>EnvoyGateway: Send request to landing route
  EnvoyGateway->>LandingRoute: Forward request
  LandingRoute-->>EnvoyGateway: Return 502, 503, or 504
  EnvoyGateway->>BackendTrafficPolicy: Apply configured response override
  BackendTrafficPolicy->>MaintenanceConfigMap: Reference maintenance HTML body
  EnvoyGateway-->>Client: Return HTML body with 5xx status
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 7 functions across 1 files. (12 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the two main changes: the maintenance page and rolling-update settings that keep a ready pod. It is specific and directly related to the pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 7 functions across 1 files. (12 skipped: 12 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown

Rendered diff across all environments

1368 lines changed
diff -ru out-base/bonn.eduide.aet.cit.tum.de.yaml out-head/bonn.eduide.aet.cit.tum.de.yaml
--- out-base/bonn.eduide.aet.cit.tum.de.yaml	2026-09-29 08:10:36.956967541 +0000
+++ out-head/bonn.eduide.aet.cit.tum.de.yaml	2026-09-29 08:10:39.565966735 +0000
@@ -106,6 +106,52 @@
       sentryEnvironment: "eduide-bonn",
     };
 ---
+# Source: eduide/templates/maintenance-page.yaml
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: maintenance-page
+  namespace: eduide-bonn
+data:
+  response.body: |
+    <!doctype html>
+    <html lang="en">
+    <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <meta http-equiv="refresh" content="30">
+    <meta name="robots" content="noindex">
+    <title>EduIDE - currently unavailable</title>
+    <style>
+      :root { color-scheme: light dark; --bg: #f5f5f5; --fg: #1a1a1a; --muted: #5c5c5c; }
+      @media (prefers-color-scheme: dark) {
+        :root { --bg: #121212; --fg: #f0f0f0; --muted: #a8a8a8; }
+      }
+      body { min-height: 100vh; margin: 0; }
+      body {
+        display: flex; align-items: center; justify-content: center;
+        padding: 0 16px; box-sizing: border-box;
+        background: var(--bg); color: var(--fg);
+        font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
+        text-align: center;
+      }
+      main { max-width: 32rem; }
+      .logo { width: 8rem; height: auto; margin-bottom: 2rem; }
+      h1 { font-size: 1.6rem; margin: 0 0 1rem; }
+      p { line-height: 1.5; margin: 0 0 0.75rem; }
+      .de { color: var(--muted); font-size: 0.9rem; }
+    </style>
+    </head>
+    <body>
+    <main>
+    <svg class="logo" role="img" aria-label="EduIDE" version="1.1" xmlns="http://www.w3.org/2000/svg" x="0" y="0" preserveAspectRatio="xMinYMin meet" viewBox="0, 0, 1150, 540.6"> <g id="Layer_1" fill="#0d82ff"> <path d="M880.199,2.8 C1028.1,2.8 1147.9,122.6 1147.9,270.5 C1147.9,418.3 1028.1,538.2 880.2,538.2 L290.1,538.2 C269,538.2 251.9,521.1 251.9,500 C251.9,478.9 269,461.8 290.1,461.8 L427.6,461.8 C448.6,461.8 465.7,444.7 465.7,423.6 C465.7,402.5 448.6,385.4 427.6,385.4 L396.999,385.4 C375.9,385.4 358.8,368.3 358.8,347.2 C358.8,326.1 375.9,309 397,309 L488.703,309 C509.918,308.941 526.373,291.65 526.9,270.8 C526.9,249.7 509.8,232.6 488.7,232.6 L167.8,232.6 C146.7,232.6 129.6,215.5 129.6,194.4 C129.6,173.3 146.7,156.2 167.8,156.2 L404.604,156.2 C425.818,156.141 442.273,138.85 442.8,118 C442.8,96.9 425.7,79.8 404.6,79.8 L351.2,79.8 C330.1,79.8 313,62.7 313,41.6 C313,20.5 330.1,2.4 351.2,2.4 L880.199,2.8 z M837.4,92 L837.4,92 C755.2,92 688.7,158.6 688.7,240.7 L688.7,300.2 C688.7,382.4 755.2,448.9 837.4,448.9 C919.5,448.9 986.1,382.4 986.1,300.2 L986.1,240.7 C986.1,158.6 919.5,92 837.4,92 L837.4,92 z M888.2,232.6 C908,232.6 924.1,248.7 924.1,268.5 L924.1,273.1 C924.1,292.9 908,309 888.2,309 L776.6,309 C756.8,309 740.7,292.9 740.7,273.1 L740.7,268.5 C740.7,248.7 756.8,232.6 776.6,232.6 L888.2,232.6 z" /> <path d="M170.1,461.8 C190,461.8 206,477.8 206,497.7 L206,502.3 C206,522.1 190,538.2 170.1,538.2 L38,538.2 C18.2,538.2 2.1,522.1 2.1,502.3 L2.1,497.7 C2.1,477.8 18.2,461.8 38,461.8 L170.1,461.8 z" /> <path d="M231.3,3.4 C251.1,3.4 267.1,19.5 267.1,39.3 L267.1,44 C267.1,63.8 251.1,79.8 231.3,79.8 L83.8,79.8 C64,79.8 47.9,63.8 47.9,44 L47.9,39.3 C47.9,19.5 64,3.4 83.8,3.4 L231.3,3.4 z" /> <path d="M277.1,309 C296.9,309 313,325.1 313,344.9 L313,349.5 C313,369.3 296.9,385.4 277.1,385.4 L196.1,385.4 C176.3,385.4 160.2,369.3 160.2,349.5 L160.2,344.9 C160.2,325.1 176.3,309 196.1,309 L277.1,309 z" /> </g> </svg> 
+    <h1>EduIDE is currently unavailable</h1>
+    <p>We're updating EduIDE and will be back in a few minutes. This page reloads automatically.</p>
+    <p class="de" lang="de">EduIDE wird gerade aktualisiert und ist in wenigen Minuten wieder da.</p>
+    </main>
+    </body>
+    </html>
+---
 # Source: eduide/templates/oauth2-configmap-htmlpage.yaml
 apiVersion: v1
 kind: ConfigMap
@@ -439,6 +485,11 @@
   namespace: eduide-bonn
 spec:
   replicas: 1
+  strategy:
+    type: RollingUpdate
+    rollingUpdate:
+      maxUnavailable: 0
+      maxSurge: 1
   selector:
     matchLabels:
       app: landing-page
@@ -451,6 +502,13 @@
         checksum/config: 25978d9c243b6ba9ea937030ae36d822203f11582eca5d8b6f39dc35a9caf724
     spec:
       automountServiceAccountToken: false
+      topologySpreadConstraints:
+        - maxSkew: 1
+          topologyKey: kubernetes.io/hostname
+          whenUnsatisfiable: ScheduleAnyway
+          labelSelector:
+            matchLabels:
+              app: landing-page
       containers:
         - name: landing-page-container
           image: ghcr.io/eduide/eduide-landing-page:1.2.2
@@ -468,6 +526,10 @@
               port: 80
             initialDelaySeconds: 5
             periodSeconds: 5
+          lifecycle:
+            preStop:
+              exec:
+                command: ["sleep", "10"]
           volumeMounts:
             - name: landing-page-config
               mountPath: /usr/share/nginx/html/config.js
@@ -555,6 +617,11 @@
   namespace: eduide-bonn
 spec:
   replicas: 1
+  strategy:
+    type: RollingUpdate
+    rollingUpdate:
+      maxUnavailable: 0
+      maxSurge: 1
   selector:
     matchLabels:
       app: service
@@ -569,6 +636,13 @@
       # we need to be able to create and read sessions
       automountServiceAccountToken: true
       serviceAccountName: service-api-service-account
+      topologySpreadConstraints:
+        - maxSkew: 1
+          topologyKey: kubernetes.io/hostname
+          whenUnsatisfiable: ScheduleAnyway
+          labelSelector:
+            matchLabels:
+              app: service
       containers:
       - name: service-container
         image: ghcr.io/eduide/eduide-cloud/service:1.2.0
@@ -577,6 +651,15 @@
         - name: http
           containerPort: 8081
           protocol: TCP
+        readinessProbe:
+          tcpSocket:
+            port: http
+          initialDelaySeconds: 5
+          periodSeconds: 5
+        lifecycle:
+          preStop:
+            exec:
+              command: ["sleep", "10"]
         envFrom:
         - configMapRef:
             name: service-config
@@ -630,6 +713,35 @@
     dataBridgeEnabled: "true"
     dataBridgePort: "16281"
 ---
+# Source: eduide/templates/maintenance-page.yaml
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: BackendTrafficPolicy
+metadata:
+  name: landing-maintenance-page
+  namespace: eduide-bonn
+spec:
+  targetRefs:
+  - group: gateway.networking.k8s.io
+    kind: HTTPRoute
+    name: landing-route
+  responseOverride:
+  - match:
+      statusCodes:
+      - type: Value
+        value: 502
+      - type: Value
+        value: 503
+      - type: Value
+        value: 504
+    response:
+      contentType: text/html; charset=utf-8
+      body:
+        type: ValueRef
+        valueRef:
+          group: ""
+          kind: ConfigMap
+          name: maintenance-page
+---
 # Source: eduide/templates/httproute-instances.yaml
 apiVersion: gateway.networking.k8s.io/v1
 kind: HTTPRoute
diff -ru out-base/e2e.eduide.student.k8s.aet.cit.tum.de.yaml out-head/e2e.eduide.student.k8s.aet.cit.tum.de.yaml
--- out-base/e2e.eduide.student.k8s.aet.cit.tum.de.yaml	2026-09-29 08:10:37.196951888 +0000
+++ out-head/e2e.eduide.student.k8s.aet.cit.tum.de.yaml	2026-09-29 08:10:39.814968290 +0000
@@ -298,6 +298,52 @@
       sentryEnvironment: "eduide-e2e-test",
     };
 ---
+# Source: eduide/templates/maintenance-page.yaml
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: maintenance-page
+  namespace: eduide-e2e-test
+data:
+  response.body: |
+    <!doctype html>
+    <html lang="en">
+    <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <meta http-equiv="refresh" content="30">
+    <meta name="robots" content="noindex">
+    <title>EduIDE - currently unavailable</title>
+    <style>
+      :root { color-scheme: light dark; --bg: #f5f5f5; --fg: #1a1a1a; --muted: #5c5c5c; }
+      @media (prefers-color-scheme: dark) {
+        :root { --bg: #121212; --fg: #f0f0f0; --muted: #a8a8a8; }
+      }
+      body { min-height: 100vh; margin: 0; }
+      body {
+        display: flex; align-items: center; justify-content: center;
+        padding: 0 16px; box-sizing: border-box;
+        background: var(--bg); color: var(--fg);
+        font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
+        text-align: center;
+      }
+      main { max-width: 32rem; }
+      .logo { width: 8rem; height: auto; margin-bottom: 2rem; }
+      h1 { font-size: 1.6rem; margin: 0 0 1rem; }
+      p { line-height: 1.5; margin: 0 0 0.75rem; }
+      .de { color: var(--muted); font-size: 0.9rem; }
+    </style>
+    </head>
+    <body>
+    <main>
+    <svg class="logo" role="img" aria-label="EduIDE" version="1.1" xmlns="http://www.w3.org/2000/svg" x="0" y="0" preserveAspectRatio="xMinYMin meet" viewBox="0, 0, 1150, 540.6"> <g id="Layer_1" fill="#0d82ff"> <path d="M880.199,2.8 C1028.1,2.8 1147.9,122.6 1147.9,270.5 C1147.9,418.3 1028.1,538.2 880.2,538.2 L290.1,538.2 C269,538.2 251.9,521.1 251.9,500 C251.9,478.9 269,461.8 290.1,461.8 L427.6,461.8 C448.6,461.8 465.7,444.7 465.7,423.6 C465.7,402.5 448.6,385.4 427.6,385.4 L396.999,385.4 C375.9,385.4 358.8,368.3 358.8,347.2 C358.8,326.1 375.9,309 397,309 L488.703,309 C509.918,308.941 526.373,291.65 526.9,270.8 C526.9,249.7 509.8,232.6 488.7,232.6 L167.8,232.6 C146.7,232.6 129.6,215.5 129.6,194.4 C129.6,173.3 146.7,156.2 167.8,156.2 L404.604,156.2 C425.818,156.141 442.273,138.85 442.8,118 C442.8,96.9 425.7,79.8 404.6,79.8 L351.2,79.8 C330.1,79.8 313,62.7 313,41.6 C313,20.5 330.1,2.4 351.2,2.4 L880.199,2.8 z M837.4,92 L837.4,92 C755.2,92 688.7,158.6 688.7,240.7 L688.7,300.2 C688.7,382.4 755.2,448.9 837.4,448.9 C919.5,448.9 986.1,382.4 986.1,300.2 L986.1,240.7 C986.1,158.6 919.5,92 837.4,92 L837.4,92 z M888.2,232.6 C908,232.6 924.1,248.7 924.1,268.5 L924.1,273.1 C924.1,292.9 908,309 888.2,309 L776.6,309 C756.8,309 740.7,292.9 740.7,273.1 L740.7,268.5 C740.7,248.7 756.8,232.6 776.6,232.6 L888.2,232.6 z" /> <path d="M170.1,461.8 C190,461.8 206,477.8 206,497.7 L206,502.3 C206,522.1 190,538.2 170.1,538.2 L38,538.2 C18.2,538.2 2.1,522.1 2.1,502.3 L2.1,497.7 C2.1,477.8 18.2,461.8 38,461.8 L170.1,461.8 z" /> <path d="M231.3,3.4 C251.1,3.4 267.1,19.5 267.1,39.3 L267.1,44 C267.1,63.8 251.1,79.8 231.3,79.8 L83.8,79.8 C64,79.8 47.9,63.8 47.9,44 L47.9,39.3 C47.9,19.5 64,3.4 83.8,3.4 L231.3,3.4 z" /> <path d="M277.1,309 C296.9,309 313,325.1 313,344.9 L313,349.5 C313,369.3 296.9,385.4 277.1,385.4 L196.1,385.4 C176.3,385.4 160.2,369.3 160.2,349.5 L160.2,344.9 C160.2,325.1 176.3,309 196.1,309 L277.1,309 z" /> </g> </svg> 
+    <h1>EduIDE is currently unavailable</h1>
+    <p>We're updating EduIDE and will be back in a few minutes. This page reloads automatically.</p>
+    <p class="de" lang="de">EduIDE wird gerade aktualisiert und ist in wenigen Minuten wieder da.</p>
+    </main>
+    </body>
+    </html>
+---
 # Source: eduide/templates/oauth2-configmap-htmlpage.yaml
 apiVersion: v1
 kind: ConfigMap
@@ -1088,6 +1134,11 @@
   namespace: eduide-e2e-test
 spec:
   replicas: 1
+  strategy:
+    type: RollingUpdate
+    rollingUpdate:
+      maxUnavailable: 0
+      maxSurge: 1
   selector:
     matchLabels:
       app: landing-page
@@ -1100,6 +1151,13 @@
         checksum/config: 5e4ecb4fa3e445e157447556fc69675a3df3b4f576a2c0b94ab6183741b48726
     spec:
       automountServiceAccountToken: false
+      topologySpreadConstraints:
+        - maxSkew: 1
+          topologyKey: kubernetes.io/hostname
+          whenUnsatisfiable: ScheduleAnyway
+          labelSelector:
+            matchLabels:
+              app: landing-page
       containers:
         - name: landing-page-container
           image: ghcr.io/eduide/eduide-landing-page:1.2.2
@@ -1117,6 +1175,10 @@
               port: 80
             initialDelaySeconds: 5
             periodSeconds: 5
+          lifecycle:
+            preStop:
+              exec:
+                command: ["sleep", "10"]
           volumeMounts:
             - name: landing-page-config
               mountPath: /usr/share/nginx/html/config.js
@@ -1211,6 +1273,11 @@
   namespace: eduide-e2e-test
 spec:
   replicas: 1
+  strategy:
+    type: RollingUpdate
+    rollingUpdate:
+      maxUnavailable: 0
+      maxSurge: 1
   selector:
     matchLabels:
       app: service
@@ -1225,6 +1292,13 @@
       # we need to be able to create and read sessions
       automountServiceAccountToken: true
       serviceAccountName: service-api-service-account
+      topologySpreadConstraints:
+        - maxSkew: 1
+          topologyKey: kubernetes.io/hostname
+          whenUnsatisfiable: ScheduleAnyway
+          labelSelector:
+            matchLabels:
+              app: service
       containers:
       - name: service-container
         image: ghcr.io/eduide/eduide-cloud/service:1.2.0
@@ -1233,6 +1307,15 @@
         - name: http
           containerPort: 8081
           protocol: TCP
+        readinessProbe:
+          tcpSocket:
+            port: http
+          initialDelaySeconds: 5
+          periodSeconds: 5
+        lifecycle:
+          preStop:
+            exec:
+              command: ["sleep", "10"]
         envFrom:
         - configMapRef:
             name: service-config
@@ -1573,6 +1656,35 @@
     dataBridgeEnabled: "true"
     dataBridgePort: "16281"
 ---
+# Source: eduide/templates/maintenance-page.yaml
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: BackendTrafficPolicy
+metadata:
+  name: landing-maintenance-page
+  namespace: eduide-e2e-test
+spec:
+  targetRefs:
+  - group: gateway.networking.k8s.io
+    kind: HTTPRoute
+    name: landing-route
+  responseOverride:
+  - match:
+      statusCodes:
+      - type: Value
+        value: 502
+      - type: Value
+        value: 503
+      - type: Value
+        value: 504
+    response:
+      contentType: text/html; charset=utf-8
+      body:
+        type: ValueRef
+        valueRef:
+          group: ""
+          kind: ConfigMap
+          name: maintenance-page
+---
 # Source: eduide/templates/httproute-instances.yaml
 apiVersion: gateway.networking.k8s.io/v1
 kind: HTTPRoute
diff -ru out-base/eduide.artemis.cit.tum.de.yaml out-head/eduide.artemis.cit.tum.de.yaml
--- out-base/eduide.artemis.cit.tum.de.yaml	2026-09-29 08:10:37.407953219 +0000
+++ out-head/eduide.artemis.cit.tum.de.yaml	2026-09-29 08:10:40.029969632 +0000
@@ -144,6 +144,52 @@
       sentryEnvironment: "eduide-tum-production",
     };
 ---
+# Source: eduide/templates/maintenance-page.yaml
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: maintenance-page
+  namespace: eduide-tum-production
+data:
+  response.body: |
+    <!doctype html>
+    <html lang="en">
+    <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <meta http-equiv="refresh" content="30">
+    <meta name="robots" content="noindex">
+    <title>EduIDE - currently unavailable</title>
+    <style>
+      :root { color-scheme: light dark; --bg: #f5f5f5; --fg: #1a1a1a; --muted: #5c5c5c; }
+      @media (prefers-color-scheme: dark) {
+        :root { --bg: #121212; --fg: #f0f0f0; --muted: #a8a8a8; }
+      }
+      body { min-height: 100vh; margin: 0; }
+      body {
+        display: flex; align-items: center; justify-content: center;
+        padding: 0 16px; box-sizing: border-box;
+        background: var(--bg); color: var(--fg);
+        font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
+        text-align: center;
+      }
+      main { max-width: 32rem; }
+      .logo { width: 8rem; height: auto; margin-bottom: 2rem; }
+      h1 { font-size: 1.6rem; margin: 0 0 1rem; }
+      p { line-height: 1.5; margin: 0 0 0.75rem; }
+      .de { color: var(--muted); font-size: 0.9rem; }
+    </style>
+    </head>
+    <body>
+    <main>
+    <svg class="logo" role="img" aria-label="EduIDE" version="1.1" xmlns="http://www.w3.org/2000/svg" x="0" y="0" preserveAspectRatio="xMinYMin meet" viewBox="0, 0, 1150, 540.6"> <g id="Layer_1" fill="#0d82ff"> <path d="M880.199,2.8 C1028.1,2.8 1147.9,122.6 1147.9,270.5 C1147.9,418.3 1028.1,538.2 880.2,538.2 L290.1,538.2 C269,538.2 251.9,521.1 251.9,500 C251.9,478.9 269,461.8 290.1,461.8 L427.6,461.8 C448.6,461.8 465.7,444.7 465.7,423.6 C465.7,402.5 448.6,385.4 427.6,385.4 L396.999,385.4 C375.9,385.4 358.8,368.3 358.8,347.2 C358.8,326.1 375.9,309 397,309 L488.703,309 C509.918,308.941 526.373,291.65 526.9,270.8 C526.9,249.7 509.8,232.6 488.7,232.6 L167.8,232.6 C146.7,232.6 129.6,215.5 129.6,194.4 C129.6,173.3 146.7,156.2 167.8,156.2 L404.604,156.2 C425.818,156.141 442.273,138.85 442.8,118 C442.8,96.9 425.7,79.8 404.6,79.8 L351.2,79.8 C330.1,79.8 313,62.7 313,41.6 C313,20.5 330.1,2.4 351.2,2.4 L880.199,2.8 z M837.4,92 L837.4,92 C755.2,92 688.7,158.6 688.7,240.7 L688.7,300.2 C688.7,382.4 755.2,448.9 837.4,448.9 C919.5,448.9 986.1,382.4 986.1,300.2 L986.1,240.7 C986.1,158.6 919.5,92 837.4,92 L837.4,92 z M888.2,232.6 C908,232.6 924.1,248.7 924.1,268.5 L924.1,273.1 C924.1,292.9 908,309 888.2,309 L776.6,309 C756.8,309 740.7,292.9 740.7,273.1 L740.7,268.5 C740.7,248.7 756.8,232.6 776.6,232.6 L888.2,232.6 z" /> <path d="M170.1,461.8 C190,461.8 206,477.8 206,497.7 L206,502.3 C206,522.1 190,538.2 170.1,538.2 L38,538.2 C18.2,538.2 2.1,522.1 2.1,502.3 L2.1,497.7 C2.1,477.8 18.2,461.8 38,461.8 L170.1,461.8 z" /> <path d="M231.3,3.4 C251.1,3.4 267.1,19.5 267.1,39.3 L267.1,44 C267.1,63.8 251.1,79.8 231.3,79.8 L83.8,79.8 C64,79.8 47.9,63.8 47.9,44 L47.9,39.3 C47.9,19.5 64,3.4 83.8,3.4 L231.3,3.4 z" /> <path d="M277.1,309 C296.9,309 313,325.1 313,344.9 L313,349.5 C313,369.3 296.9,385.4 277.1,385.4 L196.1,385.4 C176.3,385.4 160.2,369.3 160.2,349.5 L160.2,344.9 C160.2,325.1 176.3,309 196.1,309 L277.1,309 z" /> </g> </svg> 
+    <h1>EduIDE is currently unavailable</h1>
+    <p>We're updating EduIDE and will be back in a few minutes. This page reloads automatically.</p>
+    <p class="de" lang="de">EduIDE wird gerade aktualisiert und ist in wenigen Minuten wieder da.</p>
+    </main>
+    </body>
+    </html>
+---
 # Source: eduide/templates/oauth2-configmap-htmlpage.yaml
 apiVersion: v1
 kind: ConfigMap
@@ -523,6 +569,11 @@
   namespace: eduide-tum-production
 spec:
   replicas: 1
+  strategy:
+    type: RollingUpdate
+    rollingUpdate:
+      maxUnavailable: 0
+      maxSurge: 1
   selector:
     matchLabels:
       app: landing-page
@@ -535,6 +586,13 @@
         checksum/config: 2fe3aabc4f560bc33677b38cc681f0cb15b161aef5d14bb5e7a5eb4ac09ab8d0
     spec:
       automountServiceAccountToken: false
+      topologySpreadConstraints:
+        - maxSkew: 1
+          topologyKey: kubernetes.io/hostname
+          whenUnsatisfiable: ScheduleAnyway
+          labelSelector:
+            matchLabels:
+              app: landing-page
       containers:
         - name: landing-page-container
           image: ghcr.io/eduide/eduide-landing-page:1.2.2
@@ -552,6 +610,10 @@
               port: 80
             initialDelaySeconds: 5
             periodSeconds: 5
+          lifecycle:
+            preStop:
+              exec:
+                command: ["sleep", "10"]
           volumeMounts:
             - name: landing-page-config
               mountPath: /usr/share/nginx/html/config.js
@@ -646,6 +708,11 @@
   namespace: eduide-tum-production
 spec:
   replicas: 1
+  strategy:
+    type: RollingUpdate
+    rollingUpdate:
+      maxUnavailable: 0
+      maxSurge: 1
   selector:
     matchLabels:
       app: service
@@ -660,6 +727,13 @@
       # we need to be able to create and read sessions
       automountServiceAccountToken: true
       serviceAccountName: service-api-service-account
+      topologySpreadConstraints:
+        - maxSkew: 1
+          topologyKey: kubernetes.io/hostname
+          whenUnsatisfiable: ScheduleAnyway
+          labelSelector:
+            matchLabels:
+              app: service
       containers:
       - name: service-container
         image: ghcr.io/eduide/eduide-cloud/service:1.2.0
@@ -668,6 +742,15 @@
         - name: http
           containerPort: 8081
           protocol: TCP
+        readinessProbe:
+          tcpSocket:
+            port: http
+          initialDelaySeconds: 5
+          periodSeconds: 5
+        lifecycle:
+          preStop:
+            exec:
+              command: ["sleep", "10"]
         envFrom:
         - configMapRef:
             name: service-config
@@ -1008,6 +1091,35 @@
     dataBridgeEnabled: "true"
     dataBridgePort: "16281"
 ---
+# Source: eduide/templates/maintenance-page.yaml
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: BackendTrafficPolicy
+metadata:
+  name: landing-maintenance-page
+  namespace: eduide-tum-production
+spec:
+  targetRefs:
+  - group: gateway.networking.k8s.io
+    kind: HTTPRoute
+    name: landing-route
+  responseOverride:
+  - match:
+      statusCodes:
+      - type: Value
+        value: 502
+      - type: Value
+        value: 503
+      - type: Value
+        value: 504
+    response:
+      contentType: text/html; charset=utf-8
+      body:
+        type: ValueRef
+        valueRef:
+          group: ""
+          kind: ConfigMap
+          name: maintenance-page
+---
 # Source: eduide/templates/httproute-instances.yaml
 apiVersion: gateway.networking.k8s.io/v1
 kind: HTTPRoute
diff -ru out-base/mannheim.eduide.aet.cit.tum.de.yaml out-head/mannheim.eduide.aet.cit.tum.de.yaml
--- out-base/mannheim.eduide.aet.cit.tum.de.yaml	2026-09-29 08:10:37.609954493 +0000
+++ out-head/mannheim.eduide.aet.cit.tum.de.yaml	2026-09-29 08:10:40.231970894 +0000
@@ -107,6 +107,52 @@
       sentryEnvironment: "eduide-mannheim",
     };
 ---
+# Source: eduide/templates/maintenance-page.yaml
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: maintenance-page
+  namespace: eduide-mannheim
+data:
+  response.body: |
+    <!doctype html>
+    <html lang="en">
+    <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <meta http-equiv="refresh" content="30">
+    <meta name="robots" content="noindex">
+    <title>EduIDE - currently unavailable</title>
+    <style>
+      :root { color-scheme: light dark; --bg: #f5f5f5; --fg: #1a1a1a; --muted: #5c5c5c; }
+      @media (prefers-color-scheme: dark) {
+        :root { --bg: #121212; --fg: #f0f0f0; --muted: #a8a8a8; }
+      }
+      body { min-height: 100vh; margin: 0; }
+      body {
+        display: flex; align-items: center; justify-content: center;
+        padding: 0 16px; box-sizing: border-box;
+        background: var(--bg); color: var(--fg);
+        font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
+        text-align: center;
+      }
+      main { max-width: 32rem; }
+      .logo { width: 8rem; height: auto; margin-bottom: 2rem; }
+      h1 { font-size: 1.6rem; margin: 0 0 1rem; }
+      p { line-height: 1.5; margin: 0 0 0.75rem; }
+      .de { color: var(--muted); font-size: 0.9rem; }
+    </style>
+    </head>
+    <body>
+    <main>
+    <svg class="logo" role="img" aria-label="EduIDE" version="1.1" xmlns="http://www.w3.org/2000/svg" x="0" y="0" preserveAspectRatio="xMinYMin meet" viewBox="0, 0, 1150, 540.6"> <g id="Layer_1" fill="#0d82ff"> <path d="M880.199,2.8 C1028.1,2.8 1147.9,122.6 1147.9,270.5 C1147.9,418.3 1028.1,538.2 880.2,538.2 L290.1,538.2 C269,538.2 251.9,521.1 251.9,500 C251.9,478.9 269,461.8 290.1,461.8 L427.6,461.8 C448.6,461.8 465.7,444.7 465.7,423.6 C465.7,402.5 448.6,385.4 427.6,385.4 L396.999,385.4 C375.9,385.4 358.8,368.3 358.8,347.2 C358.8,326.1 375.9,309 397,309 L488.703,309 C509.918,308.941 526.373,291.65 526.9,270.8 C526.9,249.7 509.8,232.6 488.7,232.6 L167.8,232.6 C146.7,232.6 129.6,215.5 129.6,194.4 C129.6,173.3 146.7,156.2 167.8,156.2 L404.604,156.2 C425.818,156.141 442.273,138.85 442.8,118 C442.8,96.9 425.7,79.8 404.6,79.8 L351.2,79.8 C330.1,79.8 313,62.7 313,41.6 C313,20.5 330.1,2.4 351.2,2.4 L880.199,2.8 z M837.4,92 L837.4,92 C755.2,92 688.7,158.6 688.7,240.7 L688.7,300.2 C688.7,382.4 755.2,448.9 837.4,448.9 C919.5,448.9 986.1,382.4 986.1,300.2 L986.1,240.7 C986.1,158.6 919.5,92 837.4,92 L837.4,92 z M888.2,232.6 C908,232.6 924.1,248.7 924.1,268.5 L924.1,273.1 C924.1,292.9 908,309 888.2,309 L776.6,309 C756.8,309 740.7,292.9 740.7,273.1 L740.7,268.5 C740.7,248.7 756.8,232.6 776.6,232.6 L888.2,232.6 z" /> <path d="M170.1,461.8 C190,461.8 206,477.8 206,497.7 L206,502.3 C206,522.1 190,538.2 170.1,538.2 L38,538.2 C18.2,538.2 2.1,522.1 2.1,502.3 L2.1,497.7 C2.1,477.8 18.2,461.8 38,461.8 L170.1,461.8 z" /> <path d="M231.3,3.4 C251.1,3.4 267.1,19.5 267.1,39.3 L267.1,44 C267.1,63.8 251.1,79.8 231.3,79.8 L83.8,79.8 C64,79.8 47.9,63.8 47.9,44 L47.9,39.3 C47.9,19.5 64,3.4 83.8,3.4 L231.3,3.4 z" /> <path d="M277.1,309 C296.9,309 313,325.1 313,344.9 L313,349.5 C313,369.3 296.9,385.4 277.1,385.4 L196.1,385.4 C176.3,385.4 160.2,369.3 160.2,349.5 L160.2,344.9 C160.2,325.1 176.3,309 196.1,309 L277.1,309 z" /> </g> </svg> 
+    <h1>EduIDE is currently unavailable</h1>
+    <p>We're updating EduIDE and will be back in a few minutes. This page reloads automatically.</p>
+    <p class="de" lang="de">EduIDE wird gerade aktualisiert und ist in wenigen Minuten wieder da.</p>
+    </main>
+    </body>
+    </html>
+---
 # Source: eduide/templates/oauth2-configmap-htmlpage.yaml
 apiVersion: v1
 kind: ConfigMap
@@ -444,6 +490,11 @@
   namespace: eduide-mannheim
 spec:
   replicas: 1
+  strategy:
+    type: RollingUpdate
+    rollingUpdate:
+      maxUnavailable: 0
+      maxSurge: 1
   selector:
     matchLabels:
       app: landing-page
@@ -456,6 +507,13 @@
         checksum/config: c6796bb9738bafd7d486eb374d61da1e3e76bd1d2a0755424eeb75ebfd7ac735
     spec:
       automountServiceAccountToken: false
+      topologySpreadConstraints:
+        - maxSkew: 1
+          topologyKey: kubernetes.io/hostname
+          whenUnsatisfiable: ScheduleAnyway
+          labelSelector:
+            matchLabels:
+              app: landing-page
       containers:
         - name: landing-page-container
           image: ghcr.io/eduide/eduide-landing-page:1.2.2
@@ -473,6 +531,10 @@
               port: 80
             initialDelaySeconds: 5
             periodSeconds: 5
+          lifecycle:
+            preStop:
+              exec:
+                command: ["sleep", "10"]
           volumeMounts:
             - name: landing-page-config
               mountPath: /usr/share/nginx/html/config.js
@@ -567,6 +629,11 @@
   namespace: eduide-mannheim
 spec:
   replicas: 1
+  strategy:
+    type: RollingUpdate
+    rollingUpdate:
+      maxUnavailable: 0
+      maxSurge: 1
   selector:
     matchLabels:
       app: service
@@ -581,6 +648,13 @@
       # we need to be able to create and read sessions
       automountServiceAccountToken: true
       serviceAccountName: service-api-service-account
+      topologySpreadConstraints:
+        - maxSkew: 1
+          topologyKey: kubernetes.io/hostname
+          whenUnsatisfiable: ScheduleAnyway
+          labelSelector:
+            matchLabels:
+              app: service
       containers:
       - name: service-container
         image: ghcr.io/eduide/eduide-cloud/service:1.2.0
@@ -589,6 +663,15 @@
         - name: http
           containerPort: 8081
           protocol: TCP
+        readinessProbe:
+          tcpSocket:
+            port: http
+          initialDelaySeconds: 5
+          periodSeconds: 5
+        lifecycle:
+          preStop:
+            exec:
+              command: ["sleep", "10"]
         envFrom:
         - configMapRef:
             name: service-config
@@ -642,6 +725,35 @@
     dataBridgeEnabled: "true"
     dataBridgePort: "16281"
 ---
+# Source: eduide/templates/maintenance-page.yaml
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: BackendTrafficPolicy
+metadata:
+  name: landing-maintenance-page
+  namespace: eduide-mannheim
+spec:
+  targetRefs:
+  - group: gateway.networking.k8s.io
+    kind: HTTPRoute
+    name: landing-route
+  responseOverride:
+  - match:
+      statusCodes:
+      - type: Value
+        value: 502
+      - type: Value
+        value: 503
+      - type: Value
+        value: 504
+    response:
+      contentType: text/html; charset=utf-8
+      body:
+        type: ValueRef
+        valueRef:
+          group: ""
+          kind: ConfigMap
+          name: maintenance-page
+---
 # Source: eduide/templates/httproute-instances.yaml
 apiVersion: gateway.networking.k8s.io/v1
 kind: HTTPRoute
diff -ru out-base/staging.eduide.student.k8s.aet.cit.tum.de.yaml out-head/staging.eduide.student.k8s.aet.cit.tum.de.yaml
--- out-base/staging.eduide.student.k8s.aet.cit.tum.de.yaml	2026-09-29 08:10:37.845955982 +0000
+++ out-head/staging.eduide.student.k8s.aet.cit.tum.de.yaml	2026-09-29 08:10:40.476972423 +0000
@@ -298,6 +298,52 @@
       sentryEnvironment: "eduide-staging",
     };
 ---
+# Source: eduide/templates/maintenance-page.yaml
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: maintenance-page
+  namespace: eduide-staging
+data:
+  response.body: |
+    <!doctype html>
+    <html lang="en">
+    <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <meta http-equiv="refresh" content="30">
+    <meta name="robots" content="noindex">
+    <title>EduIDE - currently unavailable</title>
+    <style>
+      :root { color-scheme: light dark; --bg: #f5f5f5; --fg: #1a1a1a; --muted: #5c5c5c; }
+      @media (prefers-color-scheme: dark) {
+        :root { --bg: #121212; --fg: #f0f0f0; --muted: #a8a8a8; }
+      }
+      body { min-height: 100vh; margin: 0; }
+      body {
+        display: flex; align-items: center; justify-content: center;
+        padding: 0 16px; box-sizing: border-box;
+        background: var(--bg); color: var(--fg);
+        font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
+        text-align: center;
+      }
+      main { max-width: 32rem; }
+      .logo { width: 8rem; height: auto; margin-bottom: 2rem; }
+      h1 { font-size: 1.6rem; margin: 0 0 1rem; }
+      p { line-height: 1.5; margin: 0 0 0.75rem; }
+      .de { color: var(--muted); font-size: 0.9rem; }
+    </style>
+    </head>
+    <body>
+    <main>
+    <svg class="logo" role="img" aria-label="EduIDE" version="1.1" xmlns="http://www.w3.org/2000/svg" x="0" y="0" preserveAspectRatio="xMinYMin meet" viewBox="0, 0, 1150, 540.6"> <g id="Layer_1" fill="#0d82ff"> <path d="M880.199,2.8 C1028.1,2.8 1147.9,122.6 1147.9,270.5 C1147.9,418.3 1028.1,538.2 880.2,538.2 L290.1,538.2 C269,538.2 251.9,521.1 251.9,500 C251.9,478.9 269,461.8 290.1,461.8 L427.6,461.8 C448.6,461.8 465.7,444.7 465.7,423.6 C465.7,402.5 448.6,385.4 427.6,385.4 L396.999,385.4 C375.9,385.4 358.8,368.3 358.8,347.2 C358.8,326.1 375.9,309 397,309 L488.703,309 C509.918,308.941 526.373,291.65 526.9,270.8 C526.9,249.7 509.8,232.6 488.7,232.6 L167.8,232.6 C146.7,232.6 129.6,215.5 129.6,194.4 C129.6,173.3 146.7,156.2 167.8,156.2 L404.604,156.2 C425.818,156.141 442.273,138.85 442.8,118 C442.8,96.9 425.7,79.8 404.6,79.8 L351.2,79.8 C330.1,79.8 313,62.7 313,41.6 C313,20.5 330.1,2.4 351.2,2.4 L880.199,2.8 z M837.4,92 L837.4,92 C755.2,92 688.7,158.6 688.7,240.7 L688.7,300.2 C688.7,382.4 755.2,448.9 837.4,448.9 C919.5,448.9 986.1,382.4 986.1,300.2 L986.1,240.7 C986.1,158.6 919.5,92 837.4,92 L837.4,92 z M888.2,232.6 C908,232.6 924.1,248.7 924.1,268.5 L924.1,273.1 C924.1,292.9 908,309 888.2,309 L776.6,309 C756.8,309 740.7,292.9 740.7,273.1 L740.7,268.5 C740.7,248.7 756.8,232.6 776.6,232.6 L888.2,232.6 z" /> <path d="M170.1,461.8 C190,461.8 206,477.8 206,497.7 L206,502.3 C206,522.1 190,538.2 170.1,538.2 L38,538.2 C18.2,538.2 2.1,522.1 2.1,502.3 L2.1,497.7 C2.1,477.8 18.2,461.8 38,461.8 L170.1,461.8 z" /> <path d="M231.3,3.4 C251.1,3.4 267.1,19.5 267.1,39.3 L267.1,44 C267.1,63.8 251.1,79.8 231.3,79.8 L83.8,79.8 C64,79.8 47.9,63.8 47.9,44 L47.9,39.3 C47.9,19.5 64,3.4 83.8,3.4 L231.3,3.4 z" /> <path d="M277.1,309 C296.9,309 313,325.1 313,344.9 L313,349.5 C313,369.3 296.9,385.4 277.1,385.4 L196.1,385.4 C176.3,385.4 160.2,369.3 160.2,349.5 L160.2,344.9 C160.2,325.1 176.3,309 196.1,309 L277.1,309 z" /> </g> </svg> 
+    <h1>EduIDE is currently unavailable</h1>
+    <p>We're updating EduIDE and will be back in a few minutes. This page reloads automatically.</p>
+    <p class="de" lang="de">EduIDE wird gerade aktualisiert und ist in wenigen Minuten wieder da.</p>
+    </main>
+    </body>
+    </html>
+---
 # Source: eduide/templates/oauth2-configmap-htmlpage.yaml
 apiVersion: v1
 kind: ConfigMap
@@ -1088,6 +1134,11 @@
   namespace: eduide-staging
 spec:
   replicas: 1
+  strategy:
+    type: RollingUpdate
+    rollingUpdate:
+      maxUnavailable: 0
+      maxSurge: 1
   selector:
     matchLabels:
       app: landing-page
@@ -1100,6 +1151,13 @@
         checksum/config: ba74a0ec9e8d86587ba80a17aa671547d7341ef2eefab0417b86eaa9a808efcd
     spec:
       automountServiceAccountToken: false
+      topologySpreadConstraints:
+        - maxSkew: 1
+          topologyKey: kubernetes.io/hostname
+          whenUnsatisfiable: ScheduleAnyway
+          labelSelector:
+            matchLabels:
+              app: landing-page
       containers:
         - name: landing-page-container
           image: ghcr.io/eduide/eduide-landing-page:1.2.2
@@ -1117,6 +1175,10 @@
               port: 80
             initialDelaySeconds: 5
             periodSeconds: 5
+          lifecycle:
+            preStop:
+              exec:
+                command: ["sleep", "10"]
           volumeMounts:
             - name: landing-page-config
               mountPath: /usr/share/nginx/html/config.js
@@ -1211,6 +1273,11 @@
   namespace: eduide-staging
 spec:
   replicas: 1
+  strategy:
+    type: RollingUpdate
+    rollingUpdate:
+      maxUnavailable: 0
+      maxSurge: 1
   selector:
     matchLabels:
       app: service
@@ -1225,6 +1292,13 @@
       # we need to be able to create and read sessions
       automountServiceAccountToken: true
       serviceAccountName: service-api-service-account
+      topologySpreadConstraints:
+        - maxSkew: 1
+          topologyKey: kubernetes.io/hostname
+          whenUnsatisfiable: ScheduleAnyway
+          labelSelector:
+            matchLabels:
+              app: service
       containers:
       - name: service-container
         image: ghcr.io/eduide/eduide-cloud/service:1.2.0
@@ -1233,6 +1307,15 @@
         - name: http
           containerPort: 8081
           protocol: TCP
+        readinessProbe:
+          tcpSocket:
+            port: http
+          initialDelaySeconds: 5
+          periodSeconds: 5
+        lifecycle:
+          preStop:
+            exec:
+              command: ["sleep", "10"]
         envFrom:
         - configMapRef:
             name: service-config
@@ -1573,6 +1656,35 @@
     dataBridgeEnabled: "true"
     dataBridgePort: "16281"
 ---
+# Source: eduide/templates/maintenance-page.yaml
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: BackendTrafficPolicy
+metadata:
+  name: landing-maintenance-page
+  namespace: eduide-staging
+spec:
+  targetRefs:
+  - group: gateway.networking.k8s.io
+    kind: HTTPRoute
+    name: landing-route
+  responseOverride:
+  - match:
+      statusCodes:
+      - type: Value
+        value: 502
+      - type: Value
+        value: 503
+      - type: Value
+        value: 504
+    response:
+      contentType: text/html; charset=utf-8
+      body:
+        type: ValueRef
+        valueRef:
+          group: ""
+          kind: ConfigMap
+          name: maintenance-page
+---
 # Source: eduide/templates/httproute-instances.yaml
 apiVersion: gateway.networking.k8s.io/v1
 kind: HTTPRoute
diff -ru out-base/test1.eduide.student.k8s.aet.cit.tum.de.yaml out-head/test1.eduide.student.k8s.aet.cit.tum.de.yaml
--- out-base/test1.eduide.student.k8s.aet.cit.tum.de.yaml	2026-09-29 08:10:38.082957475 +0000
+++ out-head/test1.eduide.student.k8s.aet.cit.tum.de.yaml	2026-09-29 08:10:40.716973922 +0000
@@ -298,6 +298,52 @@
       sentryEnvironment: "eduide-test1",
     };
 ---
+# Source: eduide/templates/maintenance-page.yaml
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: maintenance-page
+  namespace: eduide-test1
+data:
+  response.body: |
+    <!doctype html>
+    <html lang="en">
+    <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <meta http-equiv="refresh" content="30">
+    <meta name="robots" content="noindex">
+    <title>EduIDE - currently unavailable</title>
+    <style>
+      :root { color-scheme: light dark; --bg: #f5f5f5; --fg: #1a1a1a; --muted: #5c5c5c; }
+      @media (prefers-color-scheme: dark) {
+        :root { --bg: #121212; --fg: #f0f0f0; --muted: #a8a8a8; }
+      }
+      body { min-height: 100vh; margin: 0; }
+      body {
+        display: flex; align-items: center; justify-content: center;
+        padding: 0 16px; box-sizing: border-box;
+        background: var(--bg); color: var(--fg);
+        font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
+        text-align: center;
+      }
+      main { max-width: 32rem; }
+      .logo { width: 8rem; height: auto; margin-bottom: 2rem; }
+      h1 { font-size: 1.6rem; margin: 0 0 1rem; }
+      p { line-height: 1.5; margin: 0 0 0.75rem; }
+      .de { color: var(--muted); font-size: 0.9rem; }
+    </style>
+    </head>
+    <body>
+    <main>
+    <svg class="logo" role="img" aria-label="EduIDE" version="1.1" xmlns="http://www.w3.org/2000/svg" x="0" y="0" preserveAspectRatio="xMinYMin meet" viewBox="0, 0, 1150, 540.6"> <g id="Layer_1" fill="#0d82ff"> <path d="M880.199,2.8 C1028.1,2.8 1147.9,122.6 1147.9,270.5 C1147.9,418.3 1028.1,538.2 880.2,538.2 L290.1,538.2 C269,538.2 251.9,521.1 251.9,500 C251.9,478.9 269,461.8 290.1,461.8 L427.6,461.8 C448.6,461.8 465.7,444.7 465.7,423.6 C465.7,402.5 448.6,385.4 427.6,385.4 L396.999,385.4 C375.9,385.4 358.8,368.3 358.8,347.2 C358.8,326.1 375.9,309 397,309 L488.703,309 C509.918,308.941 526.373,291.65 526.9,270.8 C526.9,249.7 509.8,232.6 488.7,232.6 L167.8,232.6 C146.7,232.6 129.6,215.5 129.6,194.4 C129.6,173.3 146.7,156.2 167.8,156.2 L404.604,156.2 C425.818,156.141 442.273,138.85 442.8,118 C442.8,96.9 425.7,79.8 404.6,79.8 L351.2,79.8 C330.1,79.8 313,62.7 313,41.6 C313,20.5 330.1,2.4 351.2,2.4 L880.199,2.8 z M837.4,92 L837.4,92 C755.2,92 688.7,158.6 688.7,240.7 L688.7,300.2 C688.7,382.4 755.2,448.9 837.4,448.9 C919.5,448.9 986.1,382.4 986.1,300.2 L986.1,240.7 C986.1,158.6 919.5,92 837.4,92 L837.4,92 z M888.2,232.6 C908,232.6 924.1,248.7 924.1,268.5 L924.1,273.1 C924.1,292.9 908,309 888.2,309 L776.6,309 C756.8,309 740.7,292.9 740.7,273.1 L740.7,268.5 C740.7,248.7 756.8,232.6 776.6,232.6 L888.2,232.6 z" /> <path d="M170.1,461.8 C190,461.8 206,477.8 206,497.7 L206,502.3 C206,522.1 190,538.2 170.1,538.2 L38,538.2 C18.2,538.2 2.1,522.1 2.1,502.3 L2.1,497.7 C2.1,477.8 18.2,461.8 38,461.8 L170.1,461.8 z" /> <path d="M231.3,3.4 C251.1,3.4 267.1,19.5 267.1,39.3 L267.1,44 C267.1,63.8 251.1,79.8 231.3,79.8 L83.8,79.8 C64,79.8 47.9,63.8 47.9,44 L47.9,39.3 C47.9,19.5 64,3.4 83.8,3.4 L231.3,3.4 z" /> <path d="M277.1,309 C296.9,309 313,325.1 313,344.9 L313,349.5 C313,369.3 296.9,385.4 277.1,385.4 L196.1,385.4 C176.3,385.4 160.2,369.3 160.2,349.5 L160.2,344.9 C160.2,325.1 176.3,309 196.1,309 L277.1,309 z" /> </g> </svg> 
+    <h1>EduIDE is currently unavailable</h1>
+    <p>We're updating EduIDE and will be back in a few minutes. This page reloads automatically.</p>
+    <p class="de" lang="de">EduIDE wird gerade aktualisiert und ist in wenigen Minuten wieder da.</p>
+    </main>
+    </body>
+    </html>
+---
 # Source: eduide/templates/oauth2-configmap-htmlpage.yaml
 apiVersion: v1
 kind: ConfigMap
@@ -1088,6 +1134,11 @@
   namespace: eduide-test1
 spec:
   replicas: 1
+  strategy:
+    type: RollingUpdate
+    rollingUpdate:
+      maxUnavailable: 0
+      maxSurge: 1
   selector:
     matchLabels:
       app: landing-page
@@ -1100,6 +1151,13 @@
         checksum/config: bac9bcef6af4cfc81417882b92a5db9366528fff9276b88169d620c4cc2e127a
     spec:
       automountServiceAccountToken: false
+      topologySpreadConstraints:
+        - maxSkew: 1
+          topologyKey: kubernetes.io/hostname
+          whenUnsatisfiable: ScheduleAnyway
+          labelSelector:
+            matchLabels:
+              app: landing-page
       containers:
         - name: landing-page-container
           image: ghcr.io/eduide/eduide-landing-page:1.2.2
@@ -1117,6 +1175,10 @@
               port: 80
             initialDelaySeconds: 5
             periodSeconds: 5
+          lifecycle:
+            preStop:
+              exec:
+                command: ["sleep", "10"]
           volumeMounts:
             - name: landing-page-config
               mountPath: /usr/share/nginx/html/config.js
@@ -1211,6 +1273,11 @@
   namespace: eduide-test1
 spec:
   replicas: 1
+  strategy:
+    type: RollingUpdate
+    rollingUpdate:
+      maxUnavailable: 0
+      maxSurge: 1
   selector:
     matchLabels:
       app: service
@@ -1225,6 +1292,13 @@
       # we need to be able to create and read sessions
       automountServiceAccountToken: true
       serviceAccountName: service-api-service-account
+      topologySpreadConstraints:
+        - maxSkew: 1
+          topologyKey: kubernetes.io/hostname
+          whenUnsatisfiable: ScheduleAnyway
+          labelSelector:
+            matchLabels:
+              app: service
       containers:
       - name: service-container
         image: ghcr.io/eduide/eduide-cloud/service:1.2.0
@@ -1233,6 +1307,15 @@
         - name: http
           containerPort: 8081
           protocol: TCP
+        readinessProbe:
+          tcpSocket:
+            port: http
+          initialDelaySeconds: 5
+          periodSeconds: 5
+        lifecycle:
+          preStop:
+            exec:
+              command: ["sleep", "10"]
         envFrom:
         - configMapRef:
             name: service-config
@@ -1573,6 +1656,35 @@
     dataBridgeEnabled: "true"
     dataBridgePort: "16281"
 ---
+# Source: eduide/templates/maintenance-page.yaml
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: BackendTrafficPolicy
+metadata:
+  name: landing-maintenance-page
+  namespace: eduide-test1
+spec:
+  targetRefs:
+  - group: gateway.networking.k8s.io
+    kind: HTTPRoute
+    name: landing-route
+  responseOverride:
+  - match:
+      statusCodes:
+      - type: Value
+        value: 502
+      - type: Value
+        value: 503
+      - type: Value
+        value: 504
+    response:
+      contentType: text/html; charset=utf-8
+      body:
+        type: ValueRef
+        valueRef:
+          group: ""
+          kind: ConfigMap
+          name: maintenance-page
+---
 # Source: eduide/templates/httproute-instances.yaml
 apiVersion: gateway.networking.k8s.io/v1
 kind: HTTPRoute
diff -ru out-base/test2.eduide.student.k8s.aet.cit.tum.de.yaml out-head/test2.eduide.student.k8s.aet.cit.tum.de.yaml
--- out-base/test2.eduide.student.k8s.aet.cit.tum.de.yaml	2026-09-29 08:10:38.323958980 +0000
+++ out-head/test2.eduide.student.k8s.aet.cit.tum.de.yaml	2026-09-29 08:10:40.990975633 +0000
@@ -298,6 +298,52 @@
       sentryEnvironment: "eduide-test2",
     };
 ---
+# Source: eduide/templates/maintenance-page.yaml
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: maintenance-page
+  namespace: eduide-test2
+data:
+  response.body: |
+    <!doctype html>
+    <html lang="en">
+    <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <meta http-equiv="refresh" content="30">
+    <meta name="robots" content="noindex">
+    <title>EduIDE - currently unavailable</title>
+    <style>
+      :root { color-scheme: light dark; --bg: #f5f5f5; --fg: #1a1a1a; --muted: #5c5c5c; }
+      @media (prefers-color-scheme: dark) {
+        :root { --bg: #121212; --fg: #f0f0f0; --muted: #a8a8a8; }
+      }
+      body { min-height: 100vh; margin: 0; }
+      body {
+        display: flex; align-items: center; justify-content: center;
+        padding: 0 16px; box-sizing: border-box;
+        background: var(--bg); color: var(--fg);
+        font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
+        text-align: center;
+      }
+      main { max-width: 32rem; }
+      .logo { width: 8rem; height: auto; margin-bottom: 2rem; }
+      h1 { font-size: 1.6rem; margin: 0 0 1rem; }
+      p { line-height: 1.5; margin: 0 0 0.75rem; }
+      .de { color: var(--muted); font-size: 0.9rem; }
+    </style>
+    </head>
+    <body>
+    <main>
+    <svg class="logo" role="img" aria-label="EduIDE" version="1.1" xmlns="http://www.w3.org/2000/svg" x="0" y="0" preserveAspectRatio="xMinYMin meet" viewBox="0, 0, 1150, 540.6"> <g id="Layer_1" fill="#0d82ff"> <path d="M880.199,2.8 C1028.1,2.8 1147.9,122.6 1147.9,270.5 C1147.9,418.3 1028.1,538.2 880.2,538.2 L290.1,538.2 C269,538.2 251.9,521.1 251.9,500 C251.9,478.9 269,461.8 290.1,461.8 L427.6,461.8 C448.6,461.8 465.7,444.7 465.7,423.6 C465.7,402.5 448.6,385.4 427.6,385.4 L396.999,385.4 C375.9,385.4 358.8,368.3 358.8,347.2 C358.8,326.1 375.9,309 397,309 L488.703,309 C509.918,308.941 526.373,291.65 526.9,270.8 C526.9,249.7 509.8,232.6 488.7,232.6 L167.8,232.6 C146.7,232.6 129.6,215.5 129.6,194.4 C129.6,173.3 146.7,156.2 167.8,156.2 L404.604,156.2 C425.818,156.141 442.273,138.85 442.8,118 C442.8,96.9 425.7,79.8 404.6,79.8 L351.2,79.8 C330.1,79.8 313,62.7 313,41.6 C313,20.5 330.1,2.4 351.2,2.4 L880.199,2.8 z M837.4,92 L837.4,92 C755.2,92 688.7,158.6 688.7,240.7 L688.7,300.2 C688.7,382.4 755.2,448.9 837.4,448.9 C919.5,448.9 986.1,382.4 986.1,300.2 L986.1,240.7 C986.1,158.6 919.5,92 837.4,92 L837.4,92 z M888.2,232.6 C908,232.6 924.1,248.7 924.1,268.5 L924.1,273.1 C924.1,292.9 908,309 888.2,309 L776.6,309 C756.8,309 740.7,292.9 740.7,273.1 L740.7,268.5 C740.7,248.7 756.8,232.6 776.6,232.6 L888.2,232.6 z" /> <path d="M170.1,461.8 C190,461.8 206,477.8 206,497.7 L206,502.3 C206,522.1 190,538.2 170.1,538.2 L38,538.2 C18.2,538.2 2.1,522.1 2.1,502.3 L2.1,497.7 C2.1,477.8 18.2,461.8 38,461.8 L170.1,461.8 z" /> <path d="M231.3,3.4 C251.1,3.4 267.1,19.5 267.1,39.3 L267.1,44 C267.1,63.8 251.1,79.8 231.3,79.8 L83.8,79.8 C64,79.8 47.9,63.8 47.9,44 L47.9,39.3 C47.9,19.5 64,3.4 83.8,3.4 L231.3,3.4 z" /> <path d="M277.1,309 C296.9,309 313,325.1 313,344.9 L313,349.5 C313,369.3 296.9,385.4 277.1,385.4 L196.1,385.4 C176.3,385.4 160.2,369.3 160.2,349.5 L160.2,344.9 C160.2,325.1 176.3,309 196.1,309 L277.1,309 z" /> </g> </svg> 
+    <h1>EduIDE is currently unavailable</h1>
+    <p>We're updating EduIDE and will be back in a few minutes. This page reloads automatically.</p>
+    <p class="de" lang="de">EduIDE wird gerade aktualisiert und ist in wenigen Minuten wieder da.</p>
+    </main>
+    </body>
+    </html>
+---
 # Source: eduide/templates/oauth2-configmap-htmlpage.yaml
 apiVersion: v1
 kind: ConfigMap
@@ -1088,6 +1134,11 @@
   namespace: eduide-test2
 spec:
   replicas: 1
+  strategy:
+    type: RollingUpdate
+    rollingUpdate:
+      maxUnavailable: 0
+      maxSurge: 1
   selector:
     matchLabels:
       app: landing-page
@@ -1100,6 +1151,13 @@
         checksum/config: f7580e72dce962a2ebec2547fc826a8646a449ff791a7a43c8be8d9bbb8918fe
     spec:
       automountServiceAccountToken: false
+      topologySpreadConstraints:
+        - maxSkew: 1
+          topologyKey: kubernetes.io/hostname
+          whenUnsatisfiable: ScheduleAnyway
+          labelSelector:
+            matchLabels:
+              app: landing-page
       containers:
         - name: landing-page-container
           image: ghcr.io/eduide/eduide-landing-page:1.2.2
@@ -1117,6 +1175,10 @@
               port: 80
             initialDelaySeconds: 5
             periodSeconds: 5
+          lifecycle:
+            preStop:
+              exec:
+                command: ["sleep", "10"]
           volumeMounts:
             - name: landing-page-config
               mountPath: /usr/share/nginx/html/config.js
@@ -1211,6 +1273,11 @@
   namespace: eduide-test2
 spec:
   replicas: 1
+  strategy:
+    type: RollingUpdate
+    rollingUpdate:
+      maxUnavailable: 0
+      maxSurge: 1
   selector:
     matchLabels:
       app: service
@@ -1225,6 +1292,13 @@
       # we need to be able to create and read sessions
       automountServiceAccountToken: true
       serviceAccountName: service-api-service-account
+      topologySpreadConstraints:
+        - maxSkew: 1
+          topologyKey: kubernetes.io/hostname
+          whenUnsatisfiable: ScheduleAnyway
+          labelSelector:
+            matchLabels:
+              app: service
       containers:
       - name: service-container
         image: ghcr.io/eduide/eduide-cloud/service:1.2.0
@@ -1233,6 +1307,15 @@
         - name: http
           containerPort: 8081
           protocol: TCP
+        readinessProbe:
+          tcpSocket:
+            port: http
+          initialDelaySeconds: 5
+          periodSeconds: 5
+        lifecycle:
+          preStop:
+            exec:
+              command: ["sleep", "10"]
         envFrom:
         - configMapRef:
             name: service-config
@@ -1573,6 +1656,35 @@
     dataBridgeEnabled: "true"
     dataBridgePort: "16281"
 ---
+# Source: eduide/templates/maintenance-page.yaml
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: BackendTrafficPolicy
+metadata:
+  name: landing-maintenance-page
+  namespace: eduide-test2
+spec:
+  targetRefs:
+  - group: gateway.networking.k8s.io
+    kind: HTTPRoute
+    name: landing-route
+  responseOverride:
+  - match:
+      statusCodes:
+      - type: Value
+        value: 502
+      - type: Value
+        value: 503
+      - type: Value
+        value: 504
+    response:
+      contentType: text/html; charset=utf-8
+      body:
+        type: ValueRef
+        valueRef:
+          group: ""
+          kind: ConfigMap
+          name: maintenance-page
+---
 # Source: eduide/templates/httproute-instances.yaml
 apiVersion: gateway.networking.k8s.io/v1
 kind: HTTPRoute
diff -ru out-base/test3.eduide.student.k8s.aet.cit.tum.de.yaml out-head/test3.eduide.student.k8s.aet.cit.tum.de.yaml
--- out-base/test3.eduide.student.k8s.aet.cit.tum.de.yaml	2026-09-29 08:10:38.564960485 +0000
+++ out-head/test3.eduide.student.k8s.aet.cit.tum.de.yaml	2026-09-29 08:10:41.353977899 +0000
@@ -298,6 +298,52 @@
       sentryEnvironment: "eduide-test3",
     };
 ---
+# Source: eduide/templates/maintenance-page.yaml
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: maintenance-page
+  namespace: eduide-test3
+data:
+  response.body: |
+    <!doctype html>
+    <html lang="en">
+    <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <meta http-equiv="refresh" content="30">
+    <meta name="robots" content="noindex">
+    <title>EduIDE - currently unavailable</title>
+    <style>
+      :root { color-scheme: light dark; --bg: #f5f5f5; --fg: #1a1a1a; --muted: #5c5c5c; }
+      @media (prefers-color-scheme: dark) {
+        :root { --bg: #121212; --fg: #f0f0f0; --muted: #a8a8a8; }
+      }
+      body { min-height: 100vh; margin: 0; }
+      body {
+        display: flex; align-items: center; justify-content: center;
+        padding: 0 16px; box-sizing: border-box;
+        background: var(--bg); color: var(--fg);
+        font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
+        text-align: center;
+      }
+      main { max-width: 32rem; }
+      .logo { width: 8rem; height: auto; margin-bottom: 2rem; }
+      h1 { font-size: 1.6rem; margin: 0 0 1rem; }
+      p { line-height: 1.5; margin: 0 0 0.75rem; }
+      .de { color: var(--muted); font-size: 0.9rem; }
+    </style>
+    </head>
+    <body>
+    <main>
+    <svg class="logo" role="img" aria-label="EduIDE" version="1.1" xmlns="http://www.w3.org/2000/svg" x="0" y="0" preserveAspectRatio="xMinYMin meet" viewBox="0, 0, 1150, 540.6"> <g id="Layer_1" fill="#0d82ff"> <path d="M880.199,2.8 C1028.1,2.8 1147.9,122.6 1147.9,270.5 C1147.9,418.3 1028.1,538.2 880.2,538.2 L290.1,538.2 C269,538.2 251.9,521.1 251.9,500 C251.9,478.9 269,461.8 290.1,461.8 L427.6,461.8 C448.6,461.8 465.7,444.7 465.7,423.6 C465.7,402.5 448.6,385.4 427.6,385.4 L396.999,385.4 C375.9,385.4 358.8,368.3 358.8,347.2 C358.8,326.1 375.9,309 397,309 L488.703,309 C509.918,308.941 526.373,291.65 526.9,270.8 C526.9,249.7 509.8,232.6 488.7,232.6 L167.8,232.6 C146.7,232.6 129.6,215.5 129.6,194.4 C129.6,173.3 146.7,156.2 167.8,156.2 L404.604,156.2 C425.818,156.141 442.273,138.85 442.8,118 C442.8,96.9 425.7,79.8 404.6,79.8 L351.2,79.8 C330.1,79.8 313,62.7 313,41.6 C313,20.5 330.1,2.4 351.2,2.4 L880.199,2.8 z M837.4,92 L837.4,92 C755.2,92 688.7,158.6 688.7,240.7 L688.7,300.2 C688.7,382.4 755.2,448.9 837.4,448.9 C919.5,448.9 986.1,382.4 986.1,300.2 L986.1,240.7 C986.1,158.6 919.5,92 837.4,92 L837.4,92 z M888.2,232.6 C908,232.6 924.1,248.7 924.1,268.5 L924.1,273.1 C924.1,292.9 908,309 888.2,309 L776.6,309 C756.8,309 740.7,292.9 740.7,273.1 L740.7,268.5 C740.7,248.7 756.8,232.6 776.6,232.6 L888.2,232.6 z" /> <path d="M170.1,461.8 C190,461.8 206,477.8 206,497.7 L206,502.3 C206,522.1 190,538.2 170.1,538.2 L38,538.2 C18.2,538.2 2.1,522.1 2.1,502.3 L2.1,497.7 C2.1,477.8 18.2,461.8 38,461.8 L170.1,461.8 z" /> <path d="M231.3,3.4 C251.1,3.4 267.1,19.5 267.1,39.3 L267.1,44 C267.1,63.8 251.1,79.8 231.3,79.8 L83.8,79.8 C64,79.8 47.9,63.8 47.9,44 L47.9,39.3 C47.9,19.5 64,3.4 83.8,3.4 L231.3,3.4 z" /> <path d="M277.1,309 C296.9,309 313,325.1 313,344.9 L313,349.5 C313,369.3 296.9,385.4 277.1,385.4 L196.1,385.4 C176.3,385.4 160.2,369.3 160.2,349.5 L160.2,344.9 C160.2,325.1 176.3,309 196.1,309 L277.1,309 z" /> </g> </svg> 
+    <h1>EduIDE is currently unavailable</h1>
+    <p>We're updating EduIDE and will be back in a few minutes. This page reloads automatically.</p>
+    <p class="de" lang="de">EduIDE wird gerade aktualisiert und ist in wenigen Minuten wieder da.</p>
+    </main>
+    </body>
+    </html>
+---
 # Source: eduide/templates/oauth2-configmap-htmlpage.yaml
 apiVersion: v1
 kind: ConfigMap
@@ -1088,6 +1134,11 @@
   namespace: eduide-test3
 spec:
   replicas: 1
+  strategy:
+    type: RollingUpdate
+    rollingUpdate:
+      maxUnavailable: 0
+      maxSurge: 1
   selector:
     matchLabels:
       app: landing-page
@@ -1100,6 +1151,13 @@
         checksum/config: ba3967c9f9d5aee392d1678ea55f7e0863722c6013d9e48b1183263e1d19f68e
     spec:
       automountServiceAccountToken: false
+      topologySpreadConstraints:
+        - maxSkew: 1
+          topologyKey: kubernetes.io/hostname
+          whenUnsatisfiable: ScheduleAnyway
+          labelSelector:
+            matchLabels:
+              app: landing-page
       containers:
         - name: landing-page-container
           image: ghcr.io/eduide/eduide-landing-page:1.2.2
@@ -1117,6 +1175,10 @@
               port: 80
             initialDelaySeconds: 5
             periodSeconds: 5
+          lifecycle:
+            preStop:
+              exec:
+                command: ["sleep", "10"]
           volumeMounts:
             - name: landing-page-config
               mountPath: /usr/share/nginx/html/config.js
@@ -1211,6 +1273,11 @@
   namespace: eduide-test3
 spec:
   replicas: 1
+  strategy:
+    type: RollingUpdate
+    rollingUpdate:
+      maxUnavailable: 0
+      maxSurge: 1
   selector:
     matchLabels:
       app: service
@@ -1225,6 +1292,13 @@
       # we need to be able to create and read sessions
       automountServiceAccountToken: true
       serviceAccountName: service-api-service-account
+      topologySpreadConstraints:
+        - maxSkew: 1
+          topologyKey: kubernetes.io/hostname
+          whenUnsatisfiable: ScheduleAnyway
+          labelSelector:
+            matchLabels:
+              app: service
       containers:
       - name: service-container
         image: ghcr.io/eduide/eduide-cloud/service:1.2.0
@@ -1233,6 +1307,15 @@
         - name: http
           containerPort: 8081
           protocol: TCP
+        readinessProbe:
+          tcpSocket:
+            port: http
+          initialDelaySeconds: 5
+          periodSeconds: 5
+        lifecycle:
+          preStop:
+            exec:
+              command: ["sleep", "10"]
         envFrom:
         - configMapRef:
             name: service-config
@@ -1573,6 +1656,35 @@
     dataBridgeEnabled: "true"
     dataBridgePort: "16281"
 ---
+# Source: eduide/templates/maintenance-page.yaml
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: BackendTrafficPolicy
+metadata:
+  name: landing-maintenance-page
+  namespace: eduide-test3
+spec:
+  targetRefs:
+  - group: gateway.networking.k8s.io
+    kind: HTTPRoute
+    name: landing-route
+  responseOverride:
+  - match:
+      statusCodes:
+      - type: Value
+        value: 502
+      - type: Value
+        value: 503
+      - type: Value
+        value: 504
+    response:
+      contentType: text/html; charset=utf-8
+      body:
+        type: ValueRef
+        valueRef:
+          group: ""
+          kind: ConfigMap
+          name: maintenance-page
+---
 # Source: eduide/templates/httproute-instances.yaml
 apiVersion: gateway.networking.k8s.io/v1
 kind: HTTPRoute

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant