Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,13 @@ jobs:
# Gateway API and theia.cloud are not in the default schema set.
#
# HTTPRoute is skipped deliberately. templates/httproute-instances.yaml
# ships `rules: []` on purpose - the operator patches rules in at
# runtime to publish each new session. The CRDs-catalog schema declares
# minItems: 1 on spec.rules and so rejects it, but the upstream Gateway
# API CRD does not: gateway-api v1.2.1 sets only maxItems: 16 on that
# field. The catalog is stricter than what the cluster enforces, so this
# is a false positive rather than a defect in the chart.
# OMITS spec.rules entirely: it exists to carry the gateway attachment
# and hostnames that the operator copies onto the per-session route it
# creates for each session. The CRDs-catalog schema declares minItems: 1
# on spec.rules and so rejects a route without it, but the upstream
# Gateway API CRD does not: gateway-api v1.2.1 sets only maxItems: 16 on
# that field. The catalog is stricter than what the cluster enforces, so
# this is a false positive rather than a defect in the chart.
# Revisit when the gateway templates are restructured; validating against
# schemas converted from the real CRDs would let us drop the skip.
SCHEMAS=(
Expand Down
5 changes: 3 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,9 @@ watching `main`. Full procedure in the README, and as a skill in
drift.
- `kubeconform` skips `HTTPRoute`: the CRDs-catalog schema declares
`minItems: 1` on `spec.rules`, but the upstream Gateway API CRD does not, and
`httproute-instances.yaml` ships `rules: []` deliberately for the operator to
patch.
`httproute-instances.yaml` deliberately omits the field. The operator does not
patch that route - it reads its `parentRefs` and `hostnames` and copies them
onto a new, Session-owned route per session.

## Renovate

Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## [1.2.0] - estimated between 2025-11 and 2026-05

- [eduide] Redirect requests for ended sessions to the landing page instead of a bare 404 [#137](https://github.com/EduIDE/EduIDE-Helm/pull/137)

## [1.1.2] - 2025-09-26

- [theia-cloud] Use ingress path type ImplementationSpecific instead of Prefix [#99](https://github.com/eclipse-theia/theia-cloud-helm/pull/99)
Expand Down
2 changes: 1 addition & 1 deletion charts/eduide/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 2.1.5
version: 2.2.0
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
Expand Down
8 changes: 6 additions & 2 deletions charts/eduide/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# eduide

![Version: 2.1.5](https://img.shields.io/badge/Version-2.1.5-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.2.0](https://img.shields.io/badge/AppVersion-1.2.0-informational?style=flat-square)
![Version: 2.2.0](https://img.shields.io/badge/Version-2.2.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.2.0](https://img.shields.io/badge/AppVersion-1.2.0-informational?style=flat-square)

EduIDE tenant release: operator, REST service, landing page and routes for one
environment. Requires eduide-cluster to be installed on the cluster first.
Expand Down Expand Up @@ -37,7 +37,7 @@ environment. Requires eduide-cluster to be installed on the cluster first.
| demoApplication.pullSecret | string | `""` | the image pull secret. Leave empty if registry is public |
| demoApplication.timeout | string | `"30"` | Limit in minutes |
| eduide-shared-cache | object | `{"enabled":false}` | The Gradle build cache and Maven proxy. Optional: nothing reaches it unless operator.enableBuildCaching or operator.enableDependencyCaching is also turned on, so enabling this alone deploys a cache with no clients. |
| gateway | object | `{"className":"envoy","create":false,"enabled":true,"httpEnabled":false,"httpPort":80,"httpsPort":443,"instancesRouteName":"theia-cloud-demo-ws-route","instancesWildcardSecretNames":{},"name":"theia-cloud-gateway","parentRefs":[],"routes":{"enabled":true},"serviceRouteRequestTimeout":"60s","tls":true}` | Gateway API configuration (Envoy Gateway by default) |
| gateway | object | `{"className":"envoy","create":false,"enabled":true,"httpEnabled":false,"httpPort":80,"httpsPort":443,"instancesRouteName":"theia-cloud-demo-ws-route","instancesWildcardSecretNames":{},"name":"theia-cloud-gateway","parentRefs":[],"routes":{"enabled":true,"sessionEndedRedirect":{"enabled":true,"path":"/session-ended","statusCode":302}},"serviceRouteRequestTimeout":"60s","tls":true}` | Gateway API configuration (Envoy Gateway by default) |
| gateway.className | string | `"envoy"` | GatewayClassName to use (Envoy Gateway default is typically "envoy") |
| gateway.create | bool | `false` | Create a Gateway in this namespace. Leave this false. The supported model is one shared Gateway per cluster, installed by the eduide-cluster chart, which every environment attaches to through `gateway.parentRefs` below. That is what every real installation uses and the only path that is tested. Setting this true renders a Gateway whose HTTPS listeners reference the Secrets `ws-cert-secret`, `service-cert-secret` and `landing-page-cert-secret` - which NEITHER CHART CREATES. You would have to create all three yourself. The listeners sit at Programmed=False until you do, and nothing in `helm status` explains why. |
| gateway.enabled | bool | `true` | Master switch for Gateway API resources. |
Expand All @@ -48,6 +48,10 @@ environment. Requires eduide-cluster to be installed on the cluster first.
| gateway.name | string | `"theia-cloud-gateway"` | Name of the Gateway resource |
| gateway.parentRefs | list | `[]` | Optional explicit parentRefs for HTTPRoutes. If empty, routes attach to `gateway.name` in the same namespace. Example for a centralized shared gateway: parentRefs: - name: theia-shared-gateway namespace: eduide-system |
| gateway.routes.enabled | bool | `true` | Whether to render HTTPRoute resources. |
| gateway.routes.sessionEndedRedirect | object | (see details below) | Where a browser lands when it asks for a session that no longer exists. Session routes are owned by their Session and vanish with it, so a timed-out tab that reloads matches nothing. This turns the instances route's catch-all rule into a redirect to the landing page, which explains what happened and offers to resume, instead of the empty rule Gateway API defaults it to. It applies to every hostname on that route, webviews included - which is where students usually land first, since a dead session's webviews fail before its main frame does. Live sessions are unaffected: Gateway API ranks matches by path length, and a session's path always beats `/`. |
| gateway.routes.sessionEndedRedirect.enabled | bool | `true` | Whether to emit the redirect. Needs `landingPage.enabled`, since there is nowhere to send people otherwise. With this off, the instances route falls back to the defaulted empty catch-all rule. |
| gateway.routes.sessionEndedRedirect.path | string | `"/session-ended"` | Path on the landing page that explains the session ended. |
| gateway.routes.sessionEndedRedirect.statusCode | int | `302` | Redirect status code. Only 302 is accepted: eager sessions reuse instance paths, so a cached 301 would permanently break an instance in the user's browser. |
| gateway.serviceRouteRequestTimeout | string | `"60s"` | HTTPRoute request timeout for service-route (Envoy default can be 15s) |
| gateway.tls | bool | `true` | Does Theia Cloud expect TLS connections (true) or is TLS terminated outside of Theia Cloud (false) |
| hosts | object | (see details below) | You may adjust the hostname below. |
Expand Down
57 changes: 47 additions & 10 deletions charts/eduide/templates/httproute-instances.yaml
Original file line number Diff line number Diff line change
@@ -1,18 +1,39 @@
{{- if and .Values.gateway.enabled .Values.gateway.routes.enabled }}
{{- $redirect := .Values.gateway.routes.sessionEndedRedirect }}
{{- $emitRedirect := and $redirect.enabled .Values.landingPage.enabled }}
{{- $code := default 302 $redirect.statusCode }}
{{/*
`rules` is deliberately ABSENT, not empty. The operator patches one rule per
live session into this route at runtime, so the chart must not state a value
for the field at all.
The app-level route. This is a TEMPLATE the operator reads, not one it writes:
for each session it copies this route's `parentRefs` and `hostnames` onto a
brand-new HTTPRoute of its own (`IngressManager.buildSessionRoute` in
EduIDE-Cloud), owner-referenced to the Session so Kubernetes garbage-collects
it when the session ends. Nothing ever patches rules into this object.

`rules: []` was wrong twice over. Gateway API v1.5 added `minItems: 1`, so the
API server rejects the create outright - v1.3 accepted it, which is why this
only surfaced on a newer cluster. And because Helm would then carry `rules: []`
as desired state, every `helm upgrade` would patch the live route back to zero
rules and cut routing to every running session until the operator caught up.
THE ONE RULE HERE IS THE CATCH-ALL, and it has to live on this route rather
than on a route of its own.

Omitting the field avoids both: nothing to reject, and no patch generated for
a field the chart does not own.
Gateway API DEFAULTS an omitted `spec.rules` to a single
`PathPrefix: /` rule with no backendRefs - see the `default:` on
`spec.rules` in the HTTPRoute CRD. So this route always has a `/` rule
whether we write one or not, and an empty one merely answers 5xx.

A second route carrying its own `/` rule therefore cannot win: matches are
ranked by hostname, then by characters in the path, so the two `/` rules tie,
and the tie is broken by the older creation timestamp - which on any upgrade
is this route. Putting the redirect here replaces the defaulted rule instead
of losing to it.

Session routes are unaffected: their `/<uid>/` prefix and `Exact` rules are
longer than `/` and outrank it, and the operator copies only parentRefs and
hostnames from here - never rules.

`rules: []` would be wrong twice over. Gateway API v1.5 added `minItems: 1`, so
the API server rejects it outright, and Helm would carry it as desired state
and patch the live route back to zero rules on every upgrade.
*/}}
{{- if and $emitRedirect (ne (int $code) 302) }}
{{- fail (printf "gateway.routes.sessionEndedRedirect.statusCode is %v; only 302 is allowed. Eager sessions reuse instance paths, so a cached 301 would permanently break an instance in the user's browser." $code) }}
{{- end }}
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
Expand All @@ -24,4 +45,20 @@ spec:
hostnames:
- {{ include "theia-cloud.host.instance" . | quote }}
{{- include "theia-cloud.host.wildcardInstances" . | trim | nindent 2 }}
{{- if $emitRedirect }}
rules:
- matches:
- path:
type: PathPrefix
value: /
filters:
- type: RequestRedirect
requestRedirect:
scheme: https
hostname: {{ include "theia-cloud.host.landing" . | quote }}
statusCode: {{ int $code }}
path:
type: ReplaceFullPath
replaceFullPath: {{ $redirect.path | quote }}
{{- end }}
{{- end }}
25 changes: 25 additions & 0 deletions charts/eduide/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,31 @@ gateway:
routes:
# -- Whether to render HTTPRoute resources.
enabled: true
# -- Where a browser lands when it asks for a session that no longer exists.
#
# Session routes are owned by their Session and vanish with it, so a timed-out
# tab that reloads matches nothing. This turns the instances route's catch-all
# rule into a redirect to the landing page, which explains what happened and
# offers to resume, instead of the empty rule Gateway API defaults it to.
#
# It applies to every hostname on that route, webviews included - which is
# where students usually land first, since a dead session's webviews fail
# before its main frame does.
#
# Live sessions are unaffected: Gateway API ranks matches by path length, and
# a session's path always beats `/`.
# @default -- (see details below)
sessionEndedRedirect:
# -- Whether to emit the redirect. Needs `landingPage.enabled`, since there
# is nowhere to send people otherwise. With this off, the instances route
# falls back to the defaulted empty catch-all rule.
enabled: true
# -- Path on the landing page that explains the session ended.
path: /session-ended
# -- Redirect status code. Only 302 is accepted: eager sessions reuse
# instance paths, so a cached 301 would permanently break an instance in
# the user's browser.
statusCode: 302
# -- GatewayClassName to use (Envoy Gateway default is typically "envoy")
className: "envoy"
# -- Name of the Gateway resource
Expand Down
Loading