From 81e90d44b8797cc1da83c080bc96b3ffc8319adf Mon Sep 17 00:00:00 2001 From: Felix Delattre Date: Fri, 24 Jul 2026 01:37:48 +0200 Subject: [PATCH] feat: split browser and auth-proxy ingresses. --- .../eoapi/templates/_helpers/networking.tpl | 221 +++++++++++++++--- .../networking/auth-proxy-ingress.yaml | 4 + .../templates/networking/browser-ingress.yaml | 3 + .../eoapi/templates/networking/ingress.yaml | 71 +----- .../networking/traefik-middleware.yaml | 10 +- .../services/browser/deployment.yaml | 4 +- charts/eoapi/tests/browser_redirect_test.yaml | 70 ++++-- charts/eoapi/tests/gitsha_test.yaml | 1 + charts/eoapi/tests/ingress_test.yaml | 64 ++--- charts/eoapi/tests/profiles_core_test.yaml | 1 + .../tests/stac-auth-proxy-ingress_test.yaml | 106 +++++---- charts/eoapi/tests/stac_browser_tests.yaml | 62 ++++- .../eoapi/tests/traefik_middleware_test.yaml | 14 ++ docs/unified-ingress.md | 13 +- 14 files changed, 421 insertions(+), 223 deletions(-) create mode 100644 charts/eoapi/templates/networking/auth-proxy-ingress.yaml create mode 100644 charts/eoapi/templates/networking/browser-ingress.yaml diff --git a/charts/eoapi/templates/_helpers/networking.tpl b/charts/eoapi/templates/_helpers/networking.tpl index 0e91acab..c62e4d40 100644 --- a/charts/eoapi/templates/_helpers/networking.tpl +++ b/charts/eoapi/templates/_helpers/networking.tpl @@ -1,6 +1,6 @@ {{/* -Return JSON array of enabled ingress services with resolved path, backend, and rewrite metadata. -Browser remains on the main ingress; skipStripPrefix excludes it from Traefik strip-prefix only. +Return JSON array of enabled API ingress services with resolved path, backend, and rewrite metadata. +Browser is intentionally excluded; it uses its own rewrite-free ingress. */}} {{- define "eoapi.enabledIngressServices" -}} {{- $root := . -}} @@ -9,45 +9,191 @@ Browser remains on the main ingress; skipStripPrefix excludes it from Traefik st (dict "key" "raster") (dict "key" "vector") (dict "key" "multidim") - (dict "key" "browser" "defaultPath" "/browser" "hasOwnPort" true "skipStripPrefix" true) - (dict "key" "mockOidcServer" "actualName" "mock-oidc-server" "hasOwnPort" true) + (dict "key" "mockOidcServer" "actualName" "mock-oidc-server" "hasOwnPort" true "defaultPath" "/mock-oidc" "config" $root.Values.testing.mockOidcServer) -}} {{- $resolved := list -}} {{- range $entries }} {{- $entry := . -}} - {{- $service := ternary (index $root.Values "testing" "mockOidcServer") (index $root.Values $entry.key) (eq $entry.key "mockOidcServer") -}} + {{- $service := $entry.config | default (index $root.Values $entry.key) -}} {{- $ingress := (($service | default dict).ingress) | default dict -}} {{- if and $service $service.enabled (or (not $service.ingress) $service.ingress.enabled) }} {{- $path := $ingress.path | default $entry.defaultPath -}} {{- $useAuthProxy := and $entry.usesAuthProxy (index $root.Values "stac-auth-proxy" "enabled") -}} - {{/* nginxStrip: NGINX rewrite path shape; stripPrefix: Traefik middleware (matches main, includes "/") */}} - {{- $nginxStrip := and (ne $path "/") (not $useAuthProxy) -}} - {{- $stripPrefix := and (not $entry.skipStripPrefix) (not $useAuthProxy) -}} + {{- $stripPath := and (ne $path "/") (not $useAuthProxy) -}} {{- $serviceName := $entry.actualName | default $entry.key -}} {{- $port := $root.Values.service.port -}} {{- if $entry.hasOwnPort }} {{- $port = (($service.service).port | default 8080) }} {{- end }} - {{- $resolved = append $resolved (dict "path" $path "serviceName" $serviceName "port" $port "useAuthProxy" $useAuthProxy "stripPath" $nginxStrip "stripPrefix" $stripPrefix) -}} + {{- $resolved = append $resolved (dict "path" $path "serviceName" $serviceName "port" $port "useAuthProxy" $useAuthProxy "stripPath" $stripPath) -}} {{- end }} {{- end }} {{- toJson $resolved -}} {{- end -}} {{/* -Return true when at least one ingress service or doc server is enabled. +Compute ingress gating flags and Traefik strip-prefixes in one pass. +Returns JSON: mainIngress, passthroughIngress, nginxRewrite, stripPrefixes. */}} -{{- define "eoapi.hasEnabledService" -}} -{{- if or (include "eoapi.enabledIngressServices" . | fromJsonArray) .Values.docServer.enabled -}}true{{- end -}} +{{- define "eoapi.ingressFlags" -}} +{{- $root := . -}} +{{- $services := include "eoapi.enabledIngressServices" $root | fromJsonArray -}} +{{- $isNginx := eq $root.Values.ingress.className "nginx" -}} +{{- $hasNonAuth := false -}} +{{- $hasAuthProxy := false -}} +{{- $nginxRewrite := false -}} +{{- $prefixes := list -}} +{{- range $services }} + {{- if .useAuthProxy }}{{ $hasAuthProxy = true }}{{ else }}{{ $hasNonAuth = true }}{{ end -}} + {{- if .stripPath }} + {{- $nginxRewrite = true -}} + {{- if .path }}{{ $prefixes = append $prefixes .path }}{{ end -}} + {{- end -}} +{{- end -}} +{{- $mainIngress := false -}} +{{- if $isNginx -}} + {{- if or $hasNonAuth $root.Values.docServer.enabled }}{{ $mainIngress = true }}{{ end -}} +{{- else -}} + {{- if or $hasNonAuth $hasAuthProxy $root.Values.docServer.enabled }}{{ $mainIngress = true }}{{ end -}} +{{- end -}} +{{- toJson (dict + "mainIngress" $mainIngress + "passthroughIngress" (and $isNginx $hasAuthProxy) + "nginxRewrite" $nginxRewrite + "stripPrefixes" $prefixes +) -}} {{- end -}} {{/* -Generate ingress path rules for enabled services and doc server. +Shared ingress annotations: entrypoints, user annotations, then chart-owned keys (last wins). +Call with dict "root" . "owned" "omit" — omit drops keys after merge. */}} -{{- define "eoapi.ingressPaths" -}} -{{- $root := . -}} +{{- define "eoapi.ingressCommonAnnotations" -}} +{{- $root := .root -}} +{{- $owned := .owned | default dict -}} +{{- $annotations := dict -}} +{{- if and (eq $root.Values.ingress.className "traefik") $root.Values.ingress.entrypoints -}} +{{- $_ := set $annotations "traefik.ingress.kubernetes.io/router.entrypoints" ($root.Values.ingress.entrypoints | toString) -}} +{{- end -}} +{{- $annotations = mergeOverwrite $annotations ($root.Values.ingress.annotations | default dict) -}} +{{- $annotations = mergeOverwrite $annotations $owned -}} +{{- range (.omit | default list) }}{{ $_ := unset $annotations . }}{{ end -}} +{{- if not (empty $annotations) -}} +{{- toYaml $annotations -}} +{{- end -}} +{{- end -}} + +{{/* +Shared ingress rules block for host(s) and path lists. +Call with dict "root" . "pathsTemplate" "pathsMode" (empty mode is fine). +Always passes dict "root" $root "mode" $pathsMode to the paths template. +*/}} +{{- define "eoapi.ingressRules" -}} +{{- $root := .root -}} +{{- $pathsTemplate := .pathsTemplate -}} +{{- $pathsMode := .pathsMode | default "" -}} +{{- $pathsCtx := dict "root" $root "mode" $pathsMode -}} +{{- if $root.Values.ingress.hosts }} +{{- range $root.Values.ingress.hosts }} +- host: {{ . }} + http: + paths: + {{- include $pathsTemplate $pathsCtx | nindent 6 }} +{{- end }} +{{- else }} +- {{- if $root.Values.ingress.host }} + host: {{ $root.Values.ingress.host }} + {{- end }} + http: + paths: + {{- include $pathsTemplate $pathsCtx | nindent 6 }} +{{- end }} +{{- end -}} + +{{/* +Shared TLS block for ingress resources. +*/}} +{{- define "eoapi.ingressTls" -}} +{{- if and .Values.ingress.tls.enabled (or .Values.ingress.hosts .Values.ingress.host) }} +tls: + - hosts: + {{- if .Values.ingress.hosts }} + {{- range .Values.ingress.hosts }} + - {{ . }} + {{- end }} + {{- else if .Values.ingress.host }} + - {{ .Values.ingress.host }} + {{- end }} + secretName: {{ .Values.ingress.tls.secretName }} +{{- end }} +{{- end -}} + +{{/* +Render a full Ingress manifest. +Call with dict: + root, suffix, variant (main|auth-proxy|browser), pathsTemplate, + pathsMode (optional), flags (required for main; caller-computed eoapi.ingressFlags) +*/}} +{{- define "eoapi.ingressManifest" -}} +{{- $root := .root -}} +{{- $suffix := .suffix -}} +{{- $variant := .variant -}} +{{- $owned := dict -}} +{{- $omit := list -}} +{{- $nginxRewriteKeys := list "nginx.ingress.kubernetes.io/rewrite-target" "nginx.ingress.kubernetes.io/use-regex" -}} +{{- if eq $variant "main" }} + {{- $flags := .flags -}} + {{- if and (eq $root.Values.ingress.className "nginx") $flags.nginxRewrite }} + {{- $_ := set $owned "nginx.ingress.kubernetes.io/rewrite-target" "/$2" -}} + {{- $_ := set $owned "nginx.ingress.kubernetes.io/use-regex" "true" -}} + {{- end }} + {{- if and (eq $root.Values.ingress.className "traefik") (not (empty $flags.stripPrefixes)) }} + {{- $_ := set $owned "traefik.ingress.kubernetes.io/router.middlewares" (printf "%s-%s-strip-prefix-middleware@kubernetescrd" $root.Release.Namespace $root.Release.Name) -}} + {{- end }} +{{- else if eq $variant "auth-proxy" }} + {{- $omit = $nginxRewriteKeys -}} +{{- else if eq $variant "browser" }} + {{- $omit = $nginxRewriteKeys -}} + {{- if and (eq $root.Values.ingress.className "traefik") (include "eoapi.browserRedirectEnabled" $root | trim) }} + {{- $_ := set $owned "traefik.ingress.kubernetes.io/router.middlewares" (printf "%s-%s-browser-redirect-middleware@kubernetescrd" $root.Release.Namespace $root.Release.Name) -}} + {{- end }} +{{- end }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ $root.Release.Name }}-{{ $suffix }} + labels: + app: {{ $root.Release.Name }}-{{ $suffix }} + {{- with include "eoapi.ingressCommonAnnotations" (dict "root" $root "owned" $owned "omit" $omit) | trim }} + annotations: + {{- . | nindent 4 }} + {{- end }} +spec: + {{- with $root.Values.ingress.className }} + ingressClassName: {{ . }} + {{- end }} + rules: +{{- include "eoapi.ingressRules" (dict "root" $root "pathsTemplate" .pathsTemplate "pathsMode" (.pathsMode | default "")) | nindent 4 }} +{{- with include "eoapi.ingressTls" $root | trim }} +{{ . | indent 2 }} +{{- end }} +{{- end -}} + +{{/* +API ingress path rules. Call with dict "root" . "mode" "main"|"passthrough". +*/}} +{{- define "eoapi.apiIngressPaths" -}} +{{- $root := .root -}} +{{- $mode := .mode | default "main" -}} {{- $isNginx := eq $root.Values.ingress.className "nginx" -}} {{- range include "eoapi.enabledIngressServices" $root | fromJsonArray }} +{{- $include := false -}} +{{- if eq $mode "passthrough" }} + {{- $include = .useAuthProxy -}} +{{- else -}} + {{- $include = or (not $isNginx) (not .useAuthProxy) -}} +{{- end -}} +{{- if $include }} - pathType: {{ if and $isNginx .stripPath }}ImplementationSpecific{{ else }}Prefix{{ end }} path: {{ .path }}{{ if and $isNginx .stripPath }}(/|$)(.*){{ end }} backend: @@ -60,7 +206,8 @@ Generate ingress path rules for enabled services and doc server. port: number: {{ .port }} {{- end }} -{{- if $root.Values.docServer.enabled }} +{{- end }} +{{- if and (eq $mode "main") $root.Values.docServer.enabled }} - pathType: Prefix path: "/{{ $root.Values.ingress.rootPath | default "" }}" backend: @@ -72,29 +219,45 @@ Generate ingress path rules for enabled services and doc server. {{- end -}} {{/* -Return JSON array of path prefixes for Traefik strip-prefix middleware. +Return true when the browser should be exposed via its own ingress. */}} -{{- define "eoapi.traefikStripPrefixes" -}} -{{- $prefixes := list -}} -{{- range include "eoapi.enabledIngressServices" . | fromJsonArray }} - {{- if and .stripPrefix .path }} - {{- $prefixes = append $prefixes .path }} - {{- end }} -{{- end }} -{{- toJson $prefixes -}} +{{- define "eoapi.browserIngressEnabled" -}} +{{- $browser := .Values.browser -}} +{{- if and .Values.ingress.enabled $browser $browser.enabled (or (not $browser.ingress) $browser.ingress.enabled) -}}true{{- end -}} {{- end -}} {{/* -Return the configured browser ingress path without trailing slash. +Return the configured browser ingress path without trailing slash ("/" for a root path). */}} {{- define "eoapi.browserIngressPath" -}} {{- trimSuffix "/" ((((.Values.browser).ingress).path) | default "/browser") | default "/" -}} {{- end -}} {{/* -Return true when the Traefik bare-path redirect middleware is needed. +Return the canonical browser path prefix with trailing slash, as served by the pod (SB_pathPrefix). +*/}} +{{- define "eoapi.browserPathPrefix" -}} +{{- $bare := include "eoapi.browserIngressPath" . -}} +{{- if eq $bare "/" -}}/{{- else -}}{{ $bare }}/{{- end -}} +{{- end -}} + +{{/* +Return true when the Traefik bare-path redirect middleware is needed (non-root browser path). */}} {{- define "eoapi.browserRedirectEnabled" -}} -{{- $browser := .Values.browser -}} -{{- if and $browser $browser.enabled (or (not $browser.ingress) $browser.ingress.enabled) -}}true{{- end -}} +{{- if and (include "eoapi.browserIngressEnabled" .) (ne (include "eoapi.browserIngressPath" .) "/") -}}true{{- end -}} +{{- end -}} + +{{/* +Browser ingress path rule. Call with dict "root" . +*/}} +{{- define "eoapi.browserIngressPaths" -}} +{{- $root := .root -}} +- pathType: Prefix + path: {{ include "eoapi.browserIngressPath" $root }} + backend: + service: + name: {{ $root.Release.Name }}-browser + port: + number: {{ (($root.Values.browser.service).port | default 8080) }} {{- end -}} diff --git a/charts/eoapi/templates/networking/auth-proxy-ingress.yaml b/charts/eoapi/templates/networking/auth-proxy-ingress.yaml new file mode 100644 index 00000000..ca36211b --- /dev/null +++ b/charts/eoapi/templates/networking/auth-proxy-ingress.yaml @@ -0,0 +1,4 @@ +{{- $flags := include "eoapi.ingressFlags" . | fromJson -}} +{{- if and .Values.ingress.enabled $flags.passthroughIngress }} +{{- include "eoapi.ingressManifest" (dict "root" . "suffix" "auth-proxy-ingress" "variant" "auth-proxy" "pathsTemplate" "eoapi.apiIngressPaths" "pathsMode" "passthrough") }} +{{- end }} diff --git a/charts/eoapi/templates/networking/browser-ingress.yaml b/charts/eoapi/templates/networking/browser-ingress.yaml new file mode 100644 index 00000000..647b212b --- /dev/null +++ b/charts/eoapi/templates/networking/browser-ingress.yaml @@ -0,0 +1,3 @@ +{{- if include "eoapi.browserIngressEnabled" . | trim }} +{{- include "eoapi.ingressManifest" (dict "root" . "suffix" "browser-ingress" "variant" "browser" "pathsTemplate" "eoapi.browserIngressPaths") }} +{{- end }} diff --git a/charts/eoapi/templates/networking/ingress.yaml b/charts/eoapi/templates/networking/ingress.yaml index 20b189ae..1ab01b9c 100644 --- a/charts/eoapi/templates/networking/ingress.yaml +++ b/charts/eoapi/templates/networking/ingress.yaml @@ -1,69 +1,4 @@ -{{- if and .Values.ingress.enabled (include "eoapi.hasEnabledService" . | trim | eq "true") }} -{{- $annotations := dict -}} -{{- if and (eq .Values.ingress.className "traefik") .Values.ingress.entrypoints -}} -{{- $_ := set $annotations "traefik.ingress.kubernetes.io/router.entrypoints" (.Values.ingress.entrypoints | toString) -}} -{{- end -}} -{{- $annotations = mergeOverwrite $annotations (.Values.ingress.annotations | default dict) -}} -{{- if eq .Values.ingress.className "nginx" }} -{{- $_ := set $annotations "nginx.ingress.kubernetes.io/rewrite-target" "/$2" -}} -{{- $_ := set $annotations "nginx.ingress.kubernetes.io/use-regex" "true" -}} -{{- end }} -{{- if eq .Values.ingress.className "traefik" }} -{{- /* strip-prefix always; also chain the browser bare-path redirect when the browser ingress is on. - Keep this condition in sync with the redirect Middleware in traefik-middleware.yaml. */}} -{{- $mwPrefix := printf "%s-%s" .Release.Namespace .Release.Name }} -{{- $middlewares := list (printf "%s-strip-prefix-middleware@kubernetescrd" $mwPrefix) }} -{{- if include "eoapi.browserRedirectEnabled" . | trim }} -{{- $middlewares = append $middlewares (printf "%s-browser-redirect-middleware@kubernetescrd" $mwPrefix) }} -{{- end }} -{{- $_ := set $annotations "traefik.ingress.kubernetes.io/router.middlewares" (join "," $middlewares) -}} -{{- end }} -{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion }} -apiVersion: networking.k8s.io/v1 -{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion }} -apiVersion: networking.k8s.io/v1beta1 -{{- else }} -apiVersion: extensions/v1beta1 -{{- end }} -kind: Ingress -metadata: - name: {{ .Release.Name }}-ingress - labels: - app: {{ .Release.Name }}-ingress - {{- with $annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -spec: - {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} - ingressClassName: {{ .Values.ingress.className }} - {{- end }} - rules: - {{- if .Values.ingress.hosts }} - {{- range .Values.ingress.hosts }} - - host: {{ . }} - http: - paths: - {{- include "eoapi.ingressPaths" $ | nindent 10 }} - {{- end }} - {{- else }} - - {{- if .Values.ingress.host }} - host: {{ .Values.ingress.host }} - {{- end }} - http: - paths: - {{- include "eoapi.ingressPaths" . | nindent 10 }} - {{- end }} - {{- if and .Values.ingress.tls.enabled (or .Values.ingress.hosts .Values.ingress.host) }} - tls: - - hosts: - {{- if .Values.ingress.hosts }} - {{- range .Values.ingress.hosts }} - - {{ . }} - {{- end }} - {{- else if .Values.ingress.host }} - - {{ .Values.ingress.host }} - {{- end }} - secretName: {{ .Values.ingress.tls.secretName }} - {{- end }} +{{- $flags := include "eoapi.ingressFlags" . | fromJson -}} +{{- if and .Values.ingress.enabled $flags.mainIngress }} +{{- include "eoapi.ingressManifest" (dict "root" . "suffix" "ingress" "variant" "main" "pathsTemplate" "eoapi.apiIngressPaths" "pathsMode" "main" "flags" $flags) }} {{- end }} diff --git a/charts/eoapi/templates/networking/traefik-middleware.yaml b/charts/eoapi/templates/networking/traefik-middleware.yaml index cd2662c4..ace6c957 100644 --- a/charts/eoapi/templates/networking/traefik-middleware.yaml +++ b/charts/eoapi/templates/networking/traefik-middleware.yaml @@ -1,6 +1,6 @@ {{- if and .Values.ingress.enabled (eq .Values.ingress.className "traefik") }} -{{- $prefixes := include "eoapi.traefikStripPrefixes" . | fromJsonArray -}} -{{- if $prefixes }} +{{- $flags := include "eoapi.ingressFlags" . | fromJson -}} +{{- if not (empty $flags.stripPrefixes) }} apiVersion: traefik.io/v1alpha1 kind: Middleware metadata: @@ -10,14 +10,10 @@ metadata: spec: stripPrefix: prefixes: - {{- range $prefixes }} + {{- range $flags.stripPrefixes }} - {{ . }} {{- end }} {{- end }} - -{{- /* Bare browser path 404s (pathPrefix is baked into the image) and Traefik has no - append-slash, so redirect the exact bare path to its trailing-slash form. - Keep this condition in sync with the router annotation in ingress.yaml. */}} {{- if include "eoapi.browserRedirectEnabled" . | trim }} {{- $browserPath := include "eoapi.browserIngressPath" . }} --- diff --git a/charts/eoapi/templates/services/browser/deployment.yaml b/charts/eoapi/templates/services/browser/deployment.yaml index 80d5c0af..a9e8e6c5 100644 --- a/charts/eoapi/templates/services/browser/deployment.yaml +++ b/charts/eoapi/templates/services/browser/deployment.yaml @@ -26,6 +26,8 @@ spec: ports: - containerPort: 8080 env: + - name: SB_pathPrefix + value: {{ include "eoapi.browserPathPrefix" . | quote }} - name: SB_catalogUrl value: {{ .Values.browser.catalogUrl | default (printf "%s://%s%s" (ternary "https" "http" .Values.ingress.tls.enabled) .Values.ingress.host .Values.stac.ingress.path) | quote }} {{- if .Values.browser.catalogTitle }} @@ -47,7 +49,7 @@ spec: "openIdConnectUrl": "{{ .Values.browser.oidcDiscoveryUrl }}", "oidcConfig": { "client_id": "{{ .Values.browser.oidcClientId | default "test-client" }}", - "redirect_uri": "{{ if .Values.ingress.tls.enabled }}https{{ else }}http{{ end }}://{{ .Values.ingress.host }}{{ .Values.browser.ingress.path | default "/browser" | trimSuffix "/" }}/auth" + "redirect_uri": "{{ if .Values.ingress.tls.enabled }}https{{ else }}http{{ end }}://{{ .Values.ingress.host }}{{ trimSuffix "/" (((.Values.browser).ingress).path | default "/browser") }}/auth" } } {{- end }} diff --git a/charts/eoapi/tests/browser_redirect_test.yaml b/charts/eoapi/tests/browser_redirect_test.yaml index 60c02317..62f53295 100644 --- a/charts/eoapi/tests/browser_redirect_test.yaml +++ b/charts/eoapi/tests/browser_redirect_test.yaml @@ -1,20 +1,23 @@ suite: browser bare-path redirect middleware templates: - templates/_helpers/networking.tpl - - templates/networking/traefik-middleware.yaml + - templates/networking/browser-ingress.yaml - templates/networking/ingress.yaml + - templates/networking/traefik-middleware.yaml set: ingress.enabled: true - ingress.className: traefik ingress.host: eoapi.local stac.enabled: false raster.enabled: false vector.enabled: false multidim.enabled: false + docServer.enabled: false + tests: - it: "redirects the browser bare path to its trailing-slash form on traefik" template: templates/networking/traefik-middleware.yaml set: + ingress.className: traefik browser.enabled: true asserts: - hasDocuments: @@ -35,49 +38,80 @@ tests: - it: "honours a custom browser ingress path" template: templates/networking/traefik-middleware.yaml set: + ingress.className: traefik browser.enabled: true - browser.ingress.path: "/stac-browser" + browser.ingress.path: "/stac.browser" asserts: - equal: path: spec.redirectRegex.regex - value: "^(https?://[^/]+)/stac-browser$" + value: "^(https?://[^/]+)/stac.browser$" - equal: path: spec.redirectRegex.replacement - value: "${1}/stac-browser/" + value: "${1}/stac.browser/" - - it: "still renders the redirect middleware for a root browser path so the router reference never dangles" + - it: "creates no traefik middleware on the nginx controller" template: templates/networking/traefik-middleware.yaml set: + ingress.className: nginx browser.enabled: true - browser.ingress.path: "/" asserts: - hasDocuments: - count: 1 + count: 0 - - it: "creates no traefik middleware on the nginx controller" - template: templates/networking/traefik-middleware.yaml + - it: "strips nginx rewrite annotations from the browser ingress" + template: templates/networking/browser-ingress.yaml set: ingress.className: nginx + ingress.annotations: + nginx.ingress.kubernetes.io/rewrite-target: /$2 + nginx.ingress.kubernetes.io/use-regex: "true" + cert-manager.io/cluster-issuer: letsencrypt-prod browser.enabled: true asserts: - - hasDocuments: - count: 0 + - isKind: + of: Ingress + - equal: + path: spec.rules[0].http.paths[0].path + value: "/browser" + - notExists: + path: metadata.annotations["nginx.ingress.kubernetes.io/rewrite-target"] + - notExists: + path: metadata.annotations["nginx.ingress.kubernetes.io/use-regex"] + - equal: + path: metadata.annotations["cert-manager.io/cluster-issuer"] + value: letsencrypt-prod - - it: "chains the browser redirect middleware onto the traefik router annotation" - template: templates/networking/ingress.yaml + - it: "chains the browser redirect middleware onto the traefik browser ingress" + template: templates/networking/browser-ingress.yaml set: - stac.enabled: true + ingress.className: traefik browser.enabled: true asserts: - equal: path: metadata.annotations["traefik.ingress.kubernetes.io/router.middlewares"] - value: "NAMESPACE-RELEASE-NAME-strip-prefix-middleware@kubernetescrd,NAMESPACE-RELEASE-NAME-browser-redirect-middleware@kubernetescrd" + value: "NAMESPACE-RELEASE-NAME-browser-redirect-middleware@kubernetescrd" - - it: "omits the browser redirect from the annotation when the browser is disabled" + - it: "creates the browser ingress when browser is the only enabled service" + template: templates/networking/browser-ingress.yaml + set: + ingress.className: traefik + browser.enabled: true + asserts: + - isKind: + of: Ingress + - equal: + path: metadata.labels.app + value: RELEASE-NAME-browser-ingress + - equal: + path: spec.rules[0].http.paths[0].path + value: "/browser" + + - it: "omits the browser redirect from the main ingress when other services are enabled" template: templates/networking/ingress.yaml set: + ingress.className: traefik stac.enabled: true - browser.enabled: false + browser.enabled: true asserts: - equal: path: metadata.annotations["traefik.ingress.kubernetes.io/router.middlewares"] diff --git a/charts/eoapi/tests/gitsha_test.yaml b/charts/eoapi/tests/gitsha_test.yaml index 7d5f9811..b46c9994 100644 --- a/charts/eoapi/tests/gitsha_test.yaml +++ b/charts/eoapi/tests/gitsha_test.yaml @@ -3,6 +3,7 @@ templates: - templates/_helpers/core.tpl - templates/_helpers/services.tpl - templates/_helpers/database.tpl + - templates/_helpers/networking.tpl - templates/services/stac/deployment.yaml - templates/services/stac/configmap.yaml tests: diff --git a/charts/eoapi/tests/ingress_test.yaml b/charts/eoapi/tests/ingress_test.yaml index 05919c04..10153e21 100644 --- a/charts/eoapi/tests/ingress_test.yaml +++ b/charts/eoapi/tests/ingress_test.yaml @@ -2,6 +2,7 @@ suite: unified ingress tests templates: - templates/_helpers/networking.tpl - templates/networking/ingress.yaml + tests: - it: "should not create ingress when all services disabled" template: templates/networking/ingress.yaml @@ -73,6 +74,8 @@ tests: - equal: path: spec.ingressClassName value: "traefik" + - notExists: + path: metadata.annotations["traefik.ingress.kubernetes.io/router.middlewares"] - it: "vector ingress with nginx controller" template: templates/networking/ingress.yaml @@ -223,12 +226,10 @@ tests: - equal: path: spec.rules[0].http.paths[2].path value: "/features(/|$)(.*)" - - equal: - path: spec.rules[0].http.paths[3].path - value: "/browser(/|$)(.*)" - - equal: - path: spec.rules[0].http.paths[0].pathType - value: "ImplementationSpecific" + - notContains: + path: spec.rules[0].http.paths + content: + path: "/browser" - it: "multiple hosts with services" template: templates/networking/ingress.yaml @@ -313,27 +314,7 @@ tests: path: spec.tls[0].secretName value: "eoapi-tls" - - it: "stac with auth proxy enabled" - template: templates/networking/ingress.yaml - set: - ingress.enabled: true - ingress.className: "nginx" - raster.enabled: false - stac.enabled: true - stac.ingress.enabled: true - stac.ingress.path: "/stac" - stac-auth-proxy.enabled: true - vector.enabled: false - multidim.enabled: false - browser.enabled: false - asserts: - - isKind: - of: Ingress - - equal: - path: spec.rules[0].http.paths[0].backend.service.name - value: RELEASE-NAME-stac-auth-proxy - - - it: "browser with default path" + - it: "docServer creates root path entry" template: templates/networking/ingress.yaml set: ingress.enabled: true @@ -342,19 +323,19 @@ tests: stac.enabled: false vector.enabled: false multidim.enabled: false - browser.enabled: true - browser.ingress.enabled: true + browser.enabled: false + docServer.enabled: true asserts: - isKind: of: Ingress - equal: path: spec.rules[0].http.paths[0].path - value: "/browser(/|$)(.*)" + value: "/" - equal: path: spec.rules[0].http.paths[0].backend.service.name - value: RELEASE-NAME-browser + value: RELEASE-NAME-doc-server - - it: "docServer creates root path entry" + - it: "mockOidcServer with custom path" template: templates/networking/ingress.yaml set: ingress.enabled: true @@ -364,18 +345,21 @@ tests: vector.enabled: false multidim.enabled: false browser.enabled: false - docServer.enabled: true + testing.mockOidcServer.enabled: true + testing.mockOidcServer.service.port: 8080 + testing.mockOidcServer.ingress.enabled: true + testing.mockOidcServer.ingress.path: "/auth" asserts: - isKind: of: Ingress - equal: path: spec.rules[0].http.paths[0].path - value: "/" + value: "/auth(/|$)(.*)" - equal: path: spec.rules[0].http.paths[0].backend.service.name - value: RELEASE-NAME-doc-server + value: RELEASE-NAME-mock-oidc-server - - it: "mockOidcServer with custom path" + - it: "mockOidcServer without ingress block falls back to default path" template: templates/networking/ingress.yaml set: ingress.enabled: true @@ -386,18 +370,18 @@ tests: multidim.enabled: false browser.enabled: false testing.mockOidcServer.enabled: true - testing.mockOidcServer.service.port: 8080 - testing.mockOidcServer.ingress.enabled: true - testing.mockOidcServer.ingress.path: "/auth" asserts: - isKind: of: Ingress - equal: path: spec.rules[0].http.paths[0].path - value: "/auth(/|$)(.*)" + value: "/mock-oidc(/|$)(.*)" - equal: path: spec.rules[0].http.paths[0].backend.service.name value: RELEASE-NAME-mock-oidc-server + - equal: + path: spec.rules[0].http.paths[0].backend.service.port.number + value: 8080 - it: "custom annotations preserved" template: templates/networking/ingress.yaml diff --git a/charts/eoapi/tests/profiles_core_test.yaml b/charts/eoapi/tests/profiles_core_test.yaml index 27502927..06c11111 100644 --- a/charts/eoapi/tests/profiles_core_test.yaml +++ b/charts/eoapi/tests/profiles_core_test.yaml @@ -3,6 +3,7 @@ templates: - templates/_helpers/core.tpl - templates/_helpers/services.tpl - templates/_helpers/database.tpl + - templates/_helpers/networking.tpl - templates/services/stac/deployment.yaml - templates/services/stac/configmap.yaml - templates/testing/deployment.yaml diff --git a/charts/eoapi/tests/stac-auth-proxy-ingress_test.yaml b/charts/eoapi/tests/stac-auth-proxy-ingress_test.yaml index e25ae95c..3221f703 100644 --- a/charts/eoapi/tests/stac-auth-proxy-ingress_test.yaml +++ b/charts/eoapi/tests/stac-auth-proxy-ingress_test.yaml @@ -1,11 +1,13 @@ suite: test ingress routing without stripPrefix middleware templates: - templates/_helpers/networking.tpl + - templates/networking/auth-proxy-ingress.yaml + - templates/networking/browser-ingress.yaml - templates/networking/ingress.yaml tests: - - it: should route ingress to stac-auth-proxy when enabled - template: templates/networking/ingress.yaml + - it: should route auth-proxy on rewrite-free ingress when enabled on nginx + template: templates/networking/auth-proxy-ingress.yaml set: ingress.enabled: true ingress.className: nginx @@ -15,6 +17,9 @@ tests: stac-auth-proxy.enabled: true service.port: 8080 asserts: + - equal: + path: metadata.name + value: RELEASE-NAME-auth-proxy-ingress - contains: path: spec.rules[0].http.paths content: @@ -25,6 +30,29 @@ tests: name: RELEASE-NAME-stac-auth-proxy port: number: 8080 + - notExists: + path: metadata.annotations["nginx.ingress.kubernetes.io/rewrite-target"] + - notExists: + path: metadata.annotations["nginx.ingress.kubernetes.io/use-regex"] + + - it: should omit auth-proxy path from main nginx ingress + template: templates/networking/ingress.yaml + set: + ingress.enabled: true + ingress.className: nginx + stac.enabled: true + stac.ingress.path: "/stac" + stac-auth-proxy.enabled: true + raster.enabled: true + service.port: 8080 + asserts: + - notContains: + path: spec.rules[0].http.paths + content: + path: /stac + - equal: + path: metadata.annotations["nginx.ingress.kubernetes.io/rewrite-target"] + value: /$2 - it: should route ingress directly to stac when auth-proxy is disabled template: templates/networking/ingress.yaml @@ -48,27 +76,31 @@ tests: port: number: 8080 - - it: should not create ingress when both stac and browser are disabled - template: templates/networking/ingress.yaml + - it: should not create auth-proxy ingress when auth-proxy is disabled + template: templates/networking/auth-proxy-ingress.yaml set: ingress.enabled: true - stac.enabled: false - browser.enabled: false - stac-auth-proxy.enabled: true - raster.enabled: false - vector.enabled: false - multidim.enabled: false - docServer.enabled: false + ingress.className: nginx + stac.enabled: true + stac-auth-proxy.enabled: false asserts: - hasDocuments: count: 0 - - it: should route correctly with experimental profile + - it: should keep auth-proxy on main ingress for traefik template: templates/networking/ingress.yaml - values: - - ../profiles/experimental.yaml set: ingress.enabled: true + ingress.className: traefik + stac.enabled: true + stac.ingress.path: "/stac" + stac-auth-proxy.enabled: true + raster.enabled: false + vector.enabled: false + multidim.enabled: false + browser.enabled: false + docServer.enabled: false + service.port: 8080 asserts: - contains: path: spec.rules[0].http.paths @@ -81,29 +113,18 @@ tests: port: number: 8080 - - it: should route ingress to browser - template: templates/networking/ingress.yaml + - it: should not create auth-proxy ingress for traefik + template: templates/networking/auth-proxy-ingress.yaml set: ingress.enabled: true - ingress.className: nginx - browser.enabled: true - stac.enabled: false - raster.enabled: false - vector.enabled: false - multidim.enabled: false + ingress.className: traefik + stac.enabled: true + stac-auth-proxy.enabled: true asserts: - - contains: - path: spec.rules[0].http.paths - content: - pathType: ImplementationSpecific - path: /browser(/|$)(.*) - backend: - service: - name: RELEASE-NAME-browser - port: - number: 8080 + - hasDocuments: + count: 0 - - it: should include both stac and browser when both enabled + - it: should omit auth-proxy and browser from main nginx ingress when both enabled template: templates/networking/ingress.yaml set: ingress.enabled: true @@ -113,25 +134,14 @@ tests: stac.ingress.path: "/stac" stac-auth-proxy.enabled: true browser.enabled: true + raster.enabled: true service.port: 8080 asserts: - - contains: + - notContains: path: spec.rules[0].http.paths content: - pathType: Prefix path: /stac - backend: - service: - name: RELEASE-NAME-stac-auth-proxy - port: - number: 8080 - - contains: + - notContains: path: spec.rules[0].http.paths content: - pathType: ImplementationSpecific - path: /browser(/|$)(.*) - backend: - service: - name: RELEASE-NAME-browser - port: - number: 8080 + path: /browser diff --git a/charts/eoapi/tests/stac_browser_tests.yaml b/charts/eoapi/tests/stac_browser_tests.yaml index d321d327..025402fb 100644 --- a/charts/eoapi/tests/stac_browser_tests.yaml +++ b/charts/eoapi/tests/stac_browser_tests.yaml @@ -1,6 +1,7 @@ suite: stac browser service tests templates: - templates/_helpers/core.tpl + - templates/_helpers/networking.tpl - templates/services/browser/deployment.yaml - templates/services/browser/service.yaml tests: @@ -21,6 +22,27 @@ tests: - matchRegex: path: metadata.labels.app pattern: ^RELEASE-NAME-browser$ + - contains: + path: spec.template.spec.containers[0].env + content: + name: SB_pathPrefix + value: "/browser/" + - it: "stac browser deployment with custom ingress path" + set: + raster.enabled: false + stac.enabled: false + vector.enabled: false + multidim.enabled: false + browser.enabled: true + browser.ingress.path: "/stac-browser" + template: templates/services/browser/deployment.yaml + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: SB_pathPrefix + value: "/stac-browser/" + - it: "stac browser service" set: raster.enabled: false @@ -83,6 +105,35 @@ tests: "redirect_uri": "http://localhost/browser/auth" } } + - it: "stac browser auth redirect_uri with root ingress path" + set: + raster.enabled: false + stac.enabled: true + vector.enabled: false + multidim.enabled: false + browser.enabled: true + browser.ingress.path: "/" + stac-auth-proxy.enabled: true + stac-auth-proxy.env.OIDC_DISCOVERY_URL: "http://localhost/mock-oidc/.well-known/openid-configuration" + ingress.host: "localhost" + stac.ingress.path: "/stac" + browser.oidcClientId: "test-client" + browser.oidcDiscoveryUrl: "http://localhost/mock-oidc/.well-known/openid-configuration" + template: templates/services/browser/deployment.yaml + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: SB_authConfig + value: |- + { + "type": "openIdConnect", + "openIdConnectUrl": "http://localhost/mock-oidc/.well-known/openid-configuration", + "oidcConfig": { + "client_id": "test-client", + "redirect_uri": "http://localhost/auth" + } + } - it: "stac browser deployment with custom OIDC_DISCOVERY_URL" set: raster.enabled: false @@ -341,12 +392,15 @@ tests: # the built-in env renders first; extraEnv is appended after it (additive, not a replacement) - equal: path: spec.template.spec.containers[0].env[0].name - value: SB_catalogUrl + value: SB_pathPrefix - equal: path: spec.template.spec.containers[0].env[1].name + value: SB_catalogUrl + - equal: + path: spec.template.spec.containers[0].env[2].name value: SB_itemsPerPage - equal: - path: spec.template.spec.containers[0].env[1].value + path: spec.template.spec.containers[0].env[2].value value: "60" - it: "stac browser deployment renders no extra env when browser.extraEnv is unset" set: @@ -359,7 +413,7 @@ tests: asserts: - isKind: of: Deployment - # default extraEnv ([]) adds nothing — only the built-in SB_catalogUrl remains + # default extraEnv ([]) adds nothing — built-in SB_pathPrefix and SB_catalogUrl remain - lengthEqual: path: spec.template.spec.containers[0].env - count: 1 + count: 2 diff --git a/charts/eoapi/tests/traefik_middleware_test.yaml b/charts/eoapi/tests/traefik_middleware_test.yaml index b3339dbe..14773328 100644 --- a/charts/eoapi/tests/traefik_middleware_test.yaml +++ b/charts/eoapi/tests/traefik_middleware_test.yaml @@ -2,6 +2,7 @@ suite: traefik strip-prefix middleware templates: - templates/_helpers/networking.tpl - templates/networking/traefik-middleware.yaml + tests: - it: "traefik strip-prefix middleware renders enabled service prefixes" template: templates/networking/traefik-middleware.yaml @@ -42,3 +43,16 @@ tests: path: spec.stripPrefix.prefixes value: - /raster + - it: "renders no strip-prefix middleware when no path needs stripping" + template: templates/networking/traefik-middleware.yaml + set: + ingress.className: "traefik" + stac.enabled: true + stac.ingress.path: "/" + raster.enabled: false + vector.enabled: false + multidim.enabled: false + browser.enabled: false + asserts: + - hasDocuments: + count: 0 diff --git a/docs/unified-ingress.md b/docs/unified-ingress.md index 83ec93ce..97d4aa6e 100644 --- a/docs/unified-ingress.md +++ b/docs/unified-ingress.md @@ -31,7 +31,7 @@ eoAPI includes a single streamlined ingress configuration with smart defaults th - Custom paths: `raster.ingress.path: "/tiles"` - Internal-only services stay off ingress -**Note:** Ingress is only created when at least one service is enabled. +**Note:** The main ingress is created when at least one API service or doc server is enabled. The browser has its own rewrite-free ingress when `ingress.enabled` is true, browser is enabled, and `browser.ingress` is not disabled (omitting `browser.ingress` still enables it). ## Configuration @@ -91,7 +91,7 @@ All services use `pathType: Prefix` and handle their own path prefixes internall - **Raster**: `--root-path=/raster` - **Vector**: `--root-path=/vector` - **Multidim**: `--root-path=/multidim` -- **Browser**: Configured via environment variable +- **Browser**: Served from a separate rewrite-free ingress; the pod receives `SB_pathPrefix` from `browser.ingress.path` ### Ingress Controller Support @@ -141,21 +141,18 @@ Ingress strips /raster Service receives: GET /tiles/123 ``` -**Exception:** STAC with `stac-auth-proxy.enabled: true` receives full path `/stac/...` +**Exception:** STAC with `stac-auth-proxy.enabled: true` receives full path `/stac/...`. On NGINX this is served from a separate rewrite-free ingress; Traefik keeps auth-proxy on the main ingress and omits `/stac` from strip-prefix. ## STAC Browser Configuration -The STAC browser now uses a separate ingress configuration to handle its unique requirements: -- Fixed `/browser` path prefix -- Special rewrite rules for browser-specific routes -- Maintains compatibility with both NGINX and Traefik +The STAC browser uses its own ingress so path rewriting does not strip the browser prefix. The pod receives `SB_pathPrefix` from `browser.ingress.path`. On Traefik, a redirect middleware handles bare `/browser` → `/browser/` before the pod is ready. -The browser-specific ingress is automatically configured when browser is enabled: ```yaml browser: enabled: true ingress: enabled: true # Can be disabled independently + path: "/browser" ``` ### Custom Ingress Solutions