feat(helm): render a Gateway API HTTPRoute for the litellm-helm chart

Clusters served by a Gateway rather than an ingress controller had no way to
publish the proxy from this chart, so operators kept a hand written HTTPRoute
outside the release.

httpRoute.enabled renders one, attached to the Gateways named in
httpRoute.parentRefs, answering on httpRoute.hostnames, with every rule backed
by the chart's own Service on service.port. It is independent of
ingress.enabled, so both can run during a migration.

Signed-off-by: younsl <cysl@kakao.com>
This commit is contained in:
younsl 2026-09-15 14:44:55 +09:00
parent 91588221cd
commit b6997292d4
No known key found for this signature in database
GPG key ID: 3507298BE46FCED3
6 changed files with 288 additions and 1 deletions

View file

@ -18,7 +18,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: 1.1.3
version: 1.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

View file

@ -43,6 +43,11 @@ If `db.useStackgresOperator` is used (not yet implemented):
| `service.loadBalancerClass` | Optional LoadBalancer implementation class (only used when `service.type` is `LoadBalancer`) | `""` |
| `ingress.labels` | Additional labels for the Ingress resource | `{}` |
| `ingress.*` | See [values.yaml](./values.yaml) for example settings | N/A |
| `httpRoute.enabled` | Create a Gateway API `HTTPRoute` instead of (or alongside) the Ingress. Needs the Gateway API CRDs in the cluster. | `false` |
| `httpRoute.parentRefs` | Gateways the route attaches to. Required when `httpRoute.enabled` is `true`. | `[]` |
| `httpRoute.hostnames` | Hostnames the route answers on. Empty inherits every hostname of the parent listener. | `[]` |
| `httpRoute.rules` | Routing rules. Every rule is backed by this chart's Service; `matches`, `filters`, and `timeouts` are passed through as written. | One `PathPrefix` `/` rule |
| `httpRoute.*` | See [values.yaml](./values.yaml) for example settings | N/A |
| `proxyConfigMap.create` | When `true`, render a ConfigMap from `.Values.proxy_config` and mount it. | `true` |
| `proxyConfigMap.name` | When `create=false`, name of the existing ConfigMap to mount. | `""` |
| `proxyConfigMap.key` | Key in the ConfigMap that contains the proxy config file. | `"config.yaml"` |

View file

@ -5,6 +5,18 @@
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
{{- end }}
{{- end }}
{{- end }}
{{- if .Values.httpRoute.enabled }}
{{- if .Values.httpRoute.hostnames }}
{{- range .Values.httpRoute.hostnames }}
Served by the parent Gateway on: {{ . }}
{{- end }}
{{- else }}
The HTTPRoute inherits the hostnames of its parent Gateway listener:
kubectl get --namespace {{ .Release.Namespace }} httproute {{ include "litellm.fullname" . }} -o jsonpath="{.spec.parentRefs}"
{{- end }}
{{- end }}
{{- if or .Values.ingress.enabled .Values.httpRoute.enabled }}
{{- else if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "litellm.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")

View file

@ -0,0 +1,49 @@
{{- if .Values.httpRoute.enabled -}}
{{- $fullName := include "litellm.fullname" . -}}
{{- $svcPort := .Values.service.port -}}
{{- if not .Values.httpRoute.parentRefs }}
{{- fail "httpRoute.parentRefs must name at least one Gateway when httpRoute.enabled is true. An HTTPRoute with no parent is never attached to a listener, so it accepts no traffic." }}
{{- end }}
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: {{ $fullName }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "litellm.labels" . | nindent 4 }}
{{- with .Values.httpRoute.labels }}
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.httpRoute.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
parentRefs:
{{- toYaml .Values.httpRoute.parentRefs | nindent 4 }}
{{- with .Values.httpRoute.hostnames }}
hostnames:
{{- toYaml . | nindent 4 }}
{{- end }}
rules:
{{- range .Values.httpRoute.rules }}
- backendRefs:
- group: ''
kind: Service
name: {{ $fullName }}
port: {{ $svcPort }}
weight: 1
{{- with .filters }}
filters:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .matches }}
matches:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .timeouts }}
timeouts:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}
{{- end }}

View file

@ -0,0 +1,182 @@
suite: HTTPRoute Configuration Tests
templates:
- httproute.yaml
tests:
- it: should not create an HTTPRoute by default
asserts:
- hasDocuments:
count: 0
- it: should fail when enabled without parentRefs
set:
httpRoute.enabled: true
asserts:
- failedTemplate:
errorMessage: httpRoute.parentRefs must name at least one Gateway when httpRoute.enabled is true. An HTTPRoute with no parent is never attached to a listener, so it accepts no traffic.
- it: should create a v1 HTTPRoute routing / to the chart Service
set:
httpRoute.enabled: true
httpRoute.parentRefs:
- name: external
asserts:
- hasDocuments:
count: 1
- isKind:
of: HTTPRoute
- isAPIVersion:
of: gateway.networking.k8s.io/v1
- equal:
path: metadata.name
value: RELEASE-NAME-litellm
- equal:
path: spec.parentRefs[0].name
value: external
- equal:
path: spec.rules[0].matches[0].path.type
value: PathPrefix
- equal:
path: spec.rules[0].matches[0].path.value
value: /
- equal:
path: spec.rules[0].backendRefs[0].kind
value: Service
- equal:
path: spec.rules[0].backendRefs[0].name
value: RELEASE-NAME-litellm
- equal:
path: spec.rules[0].backendRefs[0].port
value: 4000
- notExists:
path: spec.hostnames
- it: should point the backendRef at the overridden service port
set:
httpRoute.enabled: true
httpRoute.parentRefs:
- name: external
service.port: 8080
asserts:
- equal:
path: spec.rules[0].backendRefs[0].port
value: 8080
- it: should name the route and its backend after fullnameOverride
set:
httpRoute.enabled: true
httpRoute.parentRefs:
- name: external
fullnameOverride: litellm-proxy
asserts:
- equal:
path: metadata.name
value: litellm-proxy
- equal:
path: spec.rules[0].backendRefs[0].name
value: litellm-proxy
- it: should keep the namespace and sectionName of every parentRef
set:
httpRoute.enabled: true
httpRoute.parentRefs:
- name: external
namespace: gateway-system
sectionName: https
- name: internal
asserts:
- equal:
path: spec.parentRefs[0].namespace
value: gateway-system
- equal:
path: spec.parentRefs[0].sectionName
value: https
- equal:
path: spec.parentRefs[1].name
value: internal
- notExists:
path: spec.parentRefs[1].namespace
- it: should render the configured hostnames
set:
httpRoute.enabled: true
httpRoute.parentRefs:
- name: external
httpRoute.hostnames:
- api.example.local
- api2.example.local
asserts:
- equal:
path: spec.hostnames[0]
value: api.example.local
- equal:
path: spec.hostnames[1]
value: api2.example.local
- it: should back every rule with the chart Service and keep matches, filters, and timeouts
set:
httpRoute.enabled: true
httpRoute.parentRefs:
- name: external
httpRoute.rules:
- matches:
- path:
type: PathPrefix
value: /v1
filters:
- type: RequestHeaderModifier
requestHeaderModifier:
set:
- name: X-Forwarded-Proto
value: https
- matches:
- path:
type: Exact
value: /health/readiness
timeouts:
request: 10s
backendRequest: 2s
asserts:
- lengthEqual:
path: spec.rules
count: 2
- equal:
path: spec.rules[0].matches[0].path.value
value: /v1
- equal:
path: spec.rules[0].filters[0].requestHeaderModifier.set[0].name
value: X-Forwarded-Proto
- equal:
path: spec.rules[0].backendRefs[0].name
value: RELEASE-NAME-litellm
- notExists:
path: spec.rules[0].timeouts
- equal:
path: spec.rules[1].matches[0].path.type
value: Exact
- equal:
path: spec.rules[1].timeouts.request
value: 10s
- equal:
path: spec.rules[1].backendRefs[0].port
value: 4000
- it: should carry the chart labels plus any custom labels and annotations
set:
httpRoute.enabled: true
httpRoute.parentRefs:
- name: external
httpRoute.labels:
custom-label: "true"
httpRoute.annotations:
custom-annotation: "value"
asserts:
- equal:
path: metadata.labels["app.kubernetes.io/name"]
value: litellm
- equal:
path: metadata.labels["custom-label"]
value: "true"
- equal:
path: metadata.annotations["custom-annotation"]
value: "value"

View file

@ -132,6 +132,45 @@ ingress:
# hosts:
# - chart-example.local
# Gateway API route, as an alternative to ingress.* on clusters served by a
# Gateway rather than an ingress controller. Independent of ingress.enabled:
# both can be on during a migration. The Gateway itself is not created here;
# it is usually owned by the platform team and shared across namespaces, so
# this chart only attaches a route to it. Needs the Gateway API CRDs installed
# in the cluster.
httpRoute:
enabled: false
labels: {}
annotations: {}
# The Gateway(s) this route attaches to. Required when enabled: a route with
# no parent is never attached to a listener, so it accepts no traffic. Set
# `namespace` for a Gateway outside the release namespace, whose listener
# must also allow routes from this namespace.
parentRefs: []
# - name: example-gateway
# namespace: example-gateway-namespace
# sectionName: https
# Hostnames this route answers on. Each has to match a hostname the parent
# listener serves. Leave empty to inherit every hostname of the listener.
hostnames: []
# - api.example.local
# Routing rules. Every rule is backed by this chart's Service on
# service.port; matches, filters, and timeouts are passed through as written.
rules:
- matches:
- path:
type: PathPrefix
value: /
# filters:
# - type: RequestHeaderModifier
# requestHeaderModifier:
# add:
# - name: X-Custom-Header
# value: custom-value
# timeouts:
# request: 10s
# backendRequest: 2s
# masterkey: changeit
# if set, use this secret for the master key; otherwise, autogenerate a new one