diff --git a/helm/litellm-helm/Chart.yaml b/helm/litellm-helm/Chart.yaml index a3cb388ffc6..7c1ac121286 100644 --- a/helm/litellm-helm/Chart.yaml +++ b/helm/litellm-helm/Chart.yaml @@ -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 diff --git a/helm/litellm-helm/README.md b/helm/litellm-helm/README.md index bf4089404db..26fe6baf67f 100644 --- a/helm/litellm-helm/README.md +++ b/helm/litellm-helm/README.md @@ -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"` | diff --git a/helm/litellm-helm/templates/NOTES.txt b/helm/litellm-helm/templates/NOTES.txt index 017bbfa78bd..3010a642e75 100644 --- a/helm/litellm-helm/templates/NOTES.txt +++ b/helm/litellm-helm/templates/NOTES.txt @@ -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}") diff --git a/helm/litellm-helm/templates/httproute.yaml b/helm/litellm-helm/templates/httproute.yaml new file mode 100644 index 00000000000..9b83369f6cb --- /dev/null +++ b/helm/litellm-helm/templates/httproute.yaml @@ -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 }} diff --git a/helm/litellm-helm/tests/httproute_tests.yaml b/helm/litellm-helm/tests/httproute_tests.yaml new file mode 100644 index 00000000000..8dc171b65cf --- /dev/null +++ b/helm/litellm-helm/tests/httproute_tests.yaml @@ -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" + diff --git a/helm/litellm-helm/values.yaml b/helm/litellm-helm/values.yaml index fcee331a5aa..d456e1c4314 100644 --- a/helm/litellm-helm/values.yaml +++ b/helm/litellm-helm/values.yaml @@ -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