From e101449816f2e399a4cd693f84d436c5e9d17cce Mon Sep 17 00:00:00 2001 From: tin-berri Date: Tue, 27 Oct 2026 21:19:09 -0400 Subject: [PATCH] feat(helm): route extra ingress paths to custom services (#48674) --- helm/litellm/README.md | 20 +++ helm/litellm/templates/ingress.yaml | 29 +++- .../tests/ingress_extra_paths_tests.yaml | 134 ++++++++++++++++++ 3 files changed, 180 insertions(+), 3 deletions(-) diff --git a/helm/litellm/README.md b/helm/litellm/README.md index 53dc300b886..271052dd09f 100644 --- a/helm/litellm/README.md +++ b/helm/litellm/README.md @@ -23,3 +23,23 @@ ui: backend: gatewayUrl: https://gateway.example.com ``` + +## Routes to another Service + +For a shared hostname, `ingress.extraPaths` can route to a Service in the same namespace using `serviceName` and `servicePort`. The port can be a number or a named Service port. Omit `service` when using these fields + +```yaml +ingress: + enabled: true + className: alb + host: gateway.example.com + extraPaths: + - path: /moyai + pathType: Prefix + serviceName: moyai-ingress + servicePort: 8080 +``` + +The Service and application must already support the path prefix. The chart adds the path to the existing Ingress and leaves its built-in routes in place. It does not create the target Service or rewrite requests + +For AWS ALB, configure the target Service's `alb.ingress.kubernetes.io/healthcheck-path` annotation when the application needs a different health check from LiteLLM. Existing `service` aliases (`gateway`, `backend`, `ui`) remain supported diff --git a/helm/litellm/templates/ingress.yaml b/helm/litellm/templates/ingress.yaml index 22ff8a95566..e5dcfd28316 100644 --- a/helm/litellm/templates/ingress.yaml +++ b/helm/litellm/templates/ingress.yaml @@ -277,11 +277,34 @@ spec: {{- if not $extra.path }} {{- fail (printf "ingress.extraPaths[%d]: 'path' is required" $idx) }} {{- end }} + {{- $target := dict }} + {{- $port := dict }} + {{- if or (hasKey $extra "serviceName") (hasKey $extra "servicePort") }} + {{- if hasKey $extra "service" }} + {{- fail (printf "ingress.extraPaths[%d]: service cannot be combined with serviceName or servicePort" $idx) }} + {{- end }} + {{- if not (and (kindIs "string" $extra.serviceName) $extra.serviceName $extra.servicePort) }} + {{- fail (printf "ingress.extraPaths[%d]: a non-empty serviceName and servicePort are required for a custom Service" $idx) }} + {{- end }} + {{- $target = dict "name" $extra.serviceName }} + {{- if kindIs "string" $extra.servicePort }} + {{- $port = dict "name" $extra.servicePort }} + {{- else if or (kindIs "float64" $extra.servicePort) (kindIs "int64" $extra.servicePort) (kindIs "int" $extra.servicePort) }} + {{- if or (lt (float64 $extra.servicePort) 1.0) (gt (float64 $extra.servicePort) 65535.0) (ne (float64 $extra.servicePort) (float64 (int $extra.servicePort))) }} + {{- fail (printf "ingress.extraPaths[%d]: numeric servicePort must be an integer from 1 to 65535" $idx) }} + {{- end }} + {{- $port = dict "number" (int $extra.servicePort) }} + {{- else }} + {{- fail (printf "ingress.extraPaths[%d]: servicePort must be a port number or name" $idx) }} + {{- end }} + {{- else }} {{- $service := $extra.service | default "gateway" }} - {{- $target := get $extraPathBackends $service }} + {{- $target = get $extraPathBackends $service }} {{- if not $target }} {{- fail (printf "ingress.extraPaths[%d] (path %s): unknown service %q, expected one of backend, gateway, ui" $idx $extra.path $service) }} {{- end }} + {{- $port = dict "number" $target.port }} + {{- end }} {{- $requestedPathType := $extra.pathType | default "Prefix" }} {{- if not (has $requestedPathType (list "Prefix" "Exact" "ImplementationSpecific")) }} {{- fail (printf "ingress.extraPaths[%d] (path %s): unknown pathType %q, expected one of Exact, ImplementationSpecific, Prefix" $idx $extra.path $requestedPathType) }} @@ -297,9 +320,9 @@ spec: pathType: {{ $pathType }} backend: service: - name: {{ $target.name }} + name: {{ $target.name | quote }} port: - number: {{ $target.port }} + {{- toYaml $port | nindent 18 }} {{- end }} # --- Catch-all → backend (management API: /key/*, /user/*, /team/*, ...) --- - path: / diff --git a/helm/litellm/tests/ingress_extra_paths_tests.yaml b/helm/litellm/tests/ingress_extra_paths_tests.yaml index 1305af15ae2..dee46528d04 100644 --- a/helm/litellm/tests/ingress_extra_paths_tests.yaml +++ b/helm/litellm/tests/ingress_extra_paths_tests.yaml @@ -326,3 +326,137 @@ tests: asserts: - failedTemplate: errorMessage: "ingress.extraPaths[0]: each entry must be a mapping with a 'path' key" + + - it: routes a custom Service by number without replacing the existing ingress + set: + ingress.enabled: true + ingress.className: alb + ingress.host: gateway.example.com + ingress.extraPaths: + - path: /moyai + serviceName: moyai-ingress + servicePort: 8080 + asserts: + - hasDocuments: + count: 1 + - equal: + path: metadata.name + value: RELEASE-NAME-litellm + - equal: + path: spec.ingressClassName + value: alb + - equal: + path: spec.rules[0].host + value: gateway.example.com + - equal: + path: spec.rules[0].http.paths[-2] + value: + path: /moyai + pathType: Prefix + backend: + service: + name: moyai-ingress + port: + number: 8080 + - equal: + path: spec.rules[0].http.paths[-1].backend.service.name + value: RELEASE-NAME-litellm-backend + - equal: + path: spec.rules[0].http.paths[0].backend.service.name + value: RELEASE-NAME-litellm-ui + + - it: routes a custom Service by named port alongside a component alias + set: + ingress.enabled: true + ingress.extraPaths: + - path: /moyai + serviceName: moyai-ingress + servicePort: http + - path: /watsonx + service: gateway + asserts: + - equal: + path: spec.rules[0].http.paths[-3].backend.service + value: + name: moyai-ingress + port: + name: http + - equal: + path: spec.rules[0].http.paths[-2].backend.service.name + value: RELEASE-NAME-litellm-gateway + + - it: rejects ambiguous component and custom Service selectors + set: + ingress.enabled: true + ingress.extraPaths: + - path: /moyai + service: gateway + serviceName: moyai-ingress + servicePort: 8080 + asserts: + - failedTemplate: + errorMessage: 'ingress.extraPaths[0]: service cannot be combined with serviceName or servicePort' + + - it: rejects a custom Service without its port + set: + ingress.enabled: true + ingress.extraPaths: + - path: /moyai + serviceName: moyai-ingress + asserts: + - failedTemplate: + errorMessage: 'ingress.extraPaths[0]: a non-empty serviceName and servicePort are required for a custom Service' + + - it: rejects a custom Service port without its name + set: + ingress.enabled: true + ingress.extraPaths: + - path: /moyai + servicePort: 8080 + asserts: + - failedTemplate: + errorMessage: 'ingress.extraPaths[0]: a non-empty serviceName and servicePort are required for a custom Service' + + - it: rejects a non scalar port + set: + ingress.enabled: true + ingress.extraPaths: + - path: /moyai + serviceName: moyai-ingress + servicePort: {number: 8080} + asserts: + - failedTemplate: + errorMessage: 'ingress.extraPaths[0]: servicePort must be a port number or name' + + - it: rejects a fractional port + set: + ingress.enabled: true + ingress.extraPaths: + - path: /moyai + serviceName: moyai-ingress + servicePort: 8080.5 + asserts: + - failedTemplate: + errorMessage: 'ingress.extraPaths[0]: numeric servicePort must be an integer from 1 to 65535' + + - it: rejects an out of range port + set: + ingress.enabled: true + ingress.extraPaths: + - path: /moyai + serviceName: moyai-ingress + servicePort: 65536 + asserts: + - failedTemplate: + errorMessage: 'ingress.extraPaths[0]: numeric servicePort must be an integer from 1 to 65535' + + - it: rejects a custom Service taking over a built in path + set: + ingress.enabled: true + ingress.extraPaths: + - path: /v1/chat + serviceName: moyai-ingress + servicePort: 8080 + asserts: + - failedTemplate: + errorMessage: 'ingress.extraPaths[0]: path /v1/chat with pathType Prefix is already routed by this chart, and a duplicate would take it over rather than add to it'