diff --git a/helm/Chart.yaml b/helm/Chart.yaml index dab0ad62a..9c44dceee 100644 --- a/helm/Chart.yaml +++ b/helm/Chart.yaml @@ -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: 0.1.12 +version: 0.1.13 maintainers: - name: apostac diff --git a/helm/README.md b/helm/README.md index 2f865f56b..fb1dae3b0 100644 --- a/helm/README.md +++ b/helm/README.md @@ -35,6 +35,7 @@ This table documents all available configuration values for the Production Stack ### Table of Contents +- [Global Configuration](#global-configuration) - [Serving Engine Configuration](#serving-engine-configuration) - [Router Configuration](#router-configuration) - [Cache Server Configuration](#cache-server-configuration) @@ -43,6 +44,35 @@ This table documents all available configuration values for the Production Stack - [Shared Storage Configuration](#shared-storage-configuration) - [Other Configuration](#other-configuration) +### Global Configuration + +Set `global.imageRegistry` to pull images from a shared registry: + +```yaml +global: + imageRegistry: "registry.example.com:5000" +``` + +The override replaces an explicit registry (such as `ghcr.io`) or prefixes an +unqualified image. Repository paths, tags and digests are preserved. For example, +`lmcache/lmstack-router:latest` becomes +`registry.example.com:5000/lmcache/lmstack-router:latest`. +Mirror images at their corresponding repository paths before enabling this option. + +This applies to the router, serving engines, cache server, LoRA controller and +sidecar, Ray head and worker containers, and their init containers. An empty +value (the default) leaves configured images unchanged. Continue to configure +`imagePullSecrets` per component when authentication is required. + +Helm also passes global values to dependencies. The bundled +`kube-prometheus-stack` supports this setting. The bundled `prometheus-adapter` +requires its own `prometheus-adapter.image.repository` override. Images in +user-supplied `extraObjects` are not rewritten. + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `global.imageRegistry` | string | `""` | Registry override for images managed by this chart | + ### Serving Engine Configuration | Field | Type | Default | Description | diff --git a/helm/templates/_helpers.tpl b/helm/templates/_helpers.tpl index 910ec48a3..c571045d6 100644 --- a/helm/templates/_helpers.tpl +++ b/helm/templates/_helpers.tpl @@ -1,3 +1,23 @@ +{{/* +Override an image's registry while preserving its repository path, tag and digest. +Usage: include "chart.image" (dict "image" $image "global" $.Values.global) +*/}} +{{- define "chart.image" -}} +{{- $global := .global | default dict -}} +{{- $registry := $global.imageRegistry | default "" | trimSuffix "/" -}} +{{- $image := .image | default "" -}} +{{- if and $registry $image -}} + {{- $parts := splitList "/" $image -}} + {{- $first := first $parts -}} + {{- if and (gt (len $parts) 1) (or (contains "." $first) (contains ":" $first) (eq $first "localhost")) -}} + {{- $image = join "/" (rest $parts) -}} + {{- end -}} + {{- printf "%s/%s" $registry $image -}} +{{- else -}} + {{- $image -}} +{{- end -}} +{{- end -}} + {{/* Define ports for the pods */}} diff --git a/helm/templates/deployment-cache-server.yaml b/helm/templates/deployment-cache-server.yaml index cbbe613d7..4e50d1676 100644 --- a/helm/templates/deployment-cache-server.yaml +++ b/helm/templates/deployment-cache-server.yaml @@ -58,7 +58,7 @@ spec: {{- end }} containers: - name: "lmcache-server" - image: "{{ .Values.cacheserverSpec.image.repository }}:{{ .Values.cacheserverSpec.image.tag }}" + image: {{ include "chart.image" (dict "image" (printf "%s:%s" .Values.cacheserverSpec.image.repository .Values.cacheserverSpec.image.tag) "global" $.Values.global) | quote }} command: - "/opt/venv/bin/lmcache_server" - "0.0.0.0" diff --git a/helm/templates/deployment-lora-controller.yaml b/helm/templates/deployment-lora-controller.yaml index 2f0557568..c9418c0b2 100644 --- a/helm/templates/deployment-lora-controller.yaml +++ b/helm/templates/deployment-lora-controller.yaml @@ -44,7 +44,7 @@ spec: {{- if $.Values.sharedPvcStorage.enabled }} initContainers: - name: fix-perms - image: "{{ $.Values.sharedPvcStorage.image.repository }}:{{ $.Values.sharedPvcStorage.image.tag }}" + image: {{ include "chart.image" (dict "image" (printf "%s:%s" $.Values.sharedPvcStorage.image.repository $.Values.sharedPvcStorage.image.tag) "global" $.Values.global) | quote }} command: ["sh", "-c", "chown -R 65532:65532 /data/shared-pvc-storage"] securityContext: runAsUser: 0 @@ -56,7 +56,7 @@ spec: {{- end }} containers: - name: manager - image: "{{ .Values.loraController.image.repository }}:{{ .Values.loraController.image.tag }}" + image: {{ include "chart.image" (dict "image" (printf "%s:%s" .Values.loraController.image.repository .Values.loraController.image.tag) "global" $.Values.global) | quote }} imagePullPolicy: {{ .Values.loraController.image.pullPolicy }} args: - --leader-elect diff --git a/helm/templates/deployment-router.yaml b/helm/templates/deployment-router.yaml index ab979dfbf..6ac0a1bf4 100644 --- a/helm/templates/deployment-router.yaml +++ b/helm/templates/deployment-router.yaml @@ -52,7 +52,7 @@ spec: {{- end }} containers: - name: router-container - image: "{{ .Values.routerSpec.repository }}:{{ .Values.routerSpec.tag }}" + image: {{ include "chart.image" (dict "image" (printf "%s:%s" .Values.routerSpec.repository .Values.routerSpec.tag) "global" $.Values.global) | quote }} imagePullPolicy: "{{ .Values.routerSpec.imagePullPolicy }}" {{- if .Values.routerSpec.containerSecurityContext }} securityContext: diff --git a/helm/templates/deployment-vllm-multi.yaml b/helm/templates/deployment-vllm-multi.yaml index 7b57d6a33..a1a522c49 100644 --- a/helm/templates/deployment-vllm-multi.yaml +++ b/helm/templates/deployment-vllm-multi.yaml @@ -63,7 +63,7 @@ spec: {{- $container := $modelSpec.initContainer }} initContainers: - name: {{ $container.name }} - image: {{ $container.image }} + image: {{ include "chart.image" (dict "image" $container.image "global" $.Values.global) | quote }} {{- if $container.command }} command: {{ toYaml $container.command | nindent 12 }} {{- end }} @@ -119,7 +119,7 @@ spec: subdomain: "{{ $.Release.Name }}-{{ $modelSpec.name }}-engine-service" containers: - name: "vllm" - image: "{{ required "Required value 'modelSpec.repository' must be defined !" $modelSpec.repository }}:{{ required "Required value 'modelSpec.tag' must be defined !" $modelSpec.tag }}" + image: {{ include "chart.image" (dict "image" (printf "%s:%s" (required "Required value 'modelSpec.repository' must be defined !" $modelSpec.repository) (required "Required value 'modelSpec.tag' must be defined !" $modelSpec.tag)) "global" $.Values.global) | quote }} {{- with .Values.servingEngineSpec.containerSecurityContext }} securityContext: {{- toYaml . | nindent 12 }} @@ -446,7 +446,7 @@ spec: {{- end }} {{- if and $modelSpec.enableLoRA (hasKey $modelSpec "pvcStorage") }} - name: sidecar - image: {{ .Values.servingEngineSpec.sidecar.image }} + image: {{ include "chart.image" (dict "image" .Values.servingEngineSpec.sidecar.image "global" $.Values.global) | quote }} imagePullPolicy: {{ .Values.servingEngineSpec.sidecar.imagePullPolicy }} env: - name: PORT diff --git a/helm/templates/ray-cluster.yaml b/helm/templates/ray-cluster.yaml index 29d314f1c..c0f6ee319 100644 --- a/helm/templates/ray-cluster.yaml +++ b/helm/templates/ray-cluster.yaml @@ -45,7 +45,7 @@ spec: {{- $container := $modelSpec.raySpec.headNode.initContainer }} initContainers: - name: {{ $container.name }} - image: {{ $container.image }} + image: {{ include "chart.image" (dict "image" $container.image "global" $.Values.global) | quote }} {{- if $container.command }} command: {{ toYaml $container.command | nindent 14 }} {{- end }} @@ -88,7 +88,7 @@ spec: {{- end }} containers: - name: vllm-ray-head - image: "{{ required "Required value 'modelSpec.repository' must be defined !" $modelSpec.repository }}:{{ required "Required value 'modelSpec.tag' must be defined !" $modelSpec.tag }}" + image: {{ include "chart.image" (dict "image" (printf "%s:%s" (required "Required value 'modelSpec.repository' must be defined !" $modelSpec.repository) (required "Required value 'modelSpec.tag' must be defined !" $modelSpec.tag)) "global" $.Values.global) | quote }} imagePullPolicy: "{{ .Values.servingEngineSpec.imagePullPolicy | default "Always" }}" command: - >- @@ -355,7 +355,7 @@ spec: {{- $container := $modelSpec.initContainer }} initContainers: - name: {{ $container.name }} - image: {{ $container.image }} + image: {{ include "chart.image" (dict "image" $container.image "global" $.Values.global) | quote }} {{- if $container.command }} command: {{ toYaml $container.command | nindent 16 }} {{- end }} @@ -398,7 +398,7 @@ spec: {{- end }} containers: - name: vllm-ray-worker - image: "{{ required "Required value 'modelSpec.repository' must be defined !" $modelSpec.repository }}:{{ required "Required value 'modelSpec.tag' must be defined !" $modelSpec.tag }}" + image: {{ include "chart.image" (dict "image" (printf "%s:%s" (required "Required value 'modelSpec.repository' must be defined !" $modelSpec.repository) (required "Required value 'modelSpec.tag' must be defined !" $modelSpec.tag)) "global" $.Values.global) | quote }} imagePullPolicy: "{{ .Values.servingEngineSpec.imagePullPolicy | default "Always" }}" env: - name: VLLM_HOST_IP diff --git a/helm/tests/imageRegistry_test.yaml b/helm/tests/imageRegistry_test.yaml new file mode 100644 index 000000000..72ed42824 --- /dev/null +++ b/helm/tests/imageRegistry_test.yaml @@ -0,0 +1,150 @@ +suite: global image registry +templates: + - deployment-router.yaml + - deployment-cache-server.yaml + - deployment-lora-controller.yaml + - deployment-vllm-multi.yaml + - ray-cluster.yaml +values: + - values/image-registry.yaml +tests: + - it: preserves configured images when the override is empty + set: + global.imageRegistry: "" + asserts: + - template: deployment-router.yaml + equal: + path: spec.template.spec.containers[0].image + value: "ghcr.io/lmcache/lmstack-router:v1" + - template: deployment-cache-server.yaml + equal: + path: spec.template.spec.containers[0].image + value: "registry.example.org:5000/lmcache/cache:v2" + - template: deployment-lora-controller.yaml + equal: + path: spec.template.spec.containers[0].image + value: "lmcache/lmstack-lora-controller:latest" + - template: deployment-lora-controller.yaml + equal: + path: spec.template.spec.initContainers[0].image + value: "busybox:1.37" + - template: deployment-vllm-multi.yaml + equal: + path: spec.template.spec.containers[0].image + value: "vllm/vllm-openai:v3" + - template: deployment-vllm-multi.yaml + equal: + path: spec.template.spec.containers[1].image + value: "lmcache/lmstack-sidecar:latest" + - template: deployment-vllm-multi.yaml + equal: + path: spec.template.spec.initContainers[0].image + value: "busybox:1.36" + - template: ray-cluster.yaml + documentIndex: 0 + equal: + path: spec.headGroupSpec.template.spec.containers[0].image + value: "localhost:5000/team/ray-vllm:v4" + - template: ray-cluster.yaml + documentIndex: 0 + equal: + path: spec.workerGroupSpecs[0].template.spec.containers[0].image + value: "localhost:5000/team/ray-vllm:v4" + - template: ray-cluster.yaml + documentIndex: 0 + equal: + path: spec.headGroupSpec.template.spec.initContainers[0].image + value: "localhost/tools/init@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + - template: ray-cluster.yaml + documentIndex: 0 + equal: + path: spec.workerGroupSpecs[0].template.spec.initContainers[0].image + value: "docker.io/library/busybox:1.36" + - it: overrides images for all chart-managed containers + set: + global.imageRegistry: "mirror.example.com:5000" + asserts: + - template: deployment-router.yaml + equal: + path: spec.template.spec.containers[0].image + value: "mirror.example.com:5000/lmcache/lmstack-router:v1" + - template: deployment-cache-server.yaml + equal: + path: spec.template.spec.containers[0].image + value: "mirror.example.com:5000/lmcache/cache:v2" + - template: deployment-lora-controller.yaml + equal: + path: spec.template.spec.containers[0].image + value: "mirror.example.com:5000/lmcache/lmstack-lora-controller:latest" + - template: deployment-lora-controller.yaml + equal: + path: spec.template.spec.initContainers[0].image + value: "mirror.example.com:5000/busybox:1.37" + - template: deployment-vllm-multi.yaml + equal: + path: spec.template.spec.containers[0].image + value: "mirror.example.com:5000/vllm/vllm-openai:v3" + - template: deployment-vllm-multi.yaml + equal: + path: spec.template.spec.containers[1].image + value: "mirror.example.com:5000/lmcache/lmstack-sidecar:latest" + - template: deployment-vllm-multi.yaml + equal: + path: spec.template.spec.initContainers[0].image + value: "mirror.example.com:5000/busybox:1.36" + - template: ray-cluster.yaml + documentIndex: 0 + equal: + path: spec.headGroupSpec.template.spec.containers[0].image + value: "mirror.example.com:5000/team/ray-vllm:v4" + - template: ray-cluster.yaml + documentIndex: 0 + equal: + path: spec.workerGroupSpecs[0].template.spec.containers[0].image + value: "mirror.example.com:5000/team/ray-vllm:v4" + - template: ray-cluster.yaml + documentIndex: 0 + equal: + path: spec.headGroupSpec.template.spec.initContainers[0].image + value: "mirror.example.com:5000/tools/init@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + - template: ray-cluster.yaml + documentIndex: 0 + equal: + path: spec.workerGroupSpecs[0].template.spec.initContainers[0].image + value: "mirror.example.com:5000/library/busybox:1.36" + - it: keeps nested namespaces + set: + global.imageRegistry: "mirror.example.com" + routerSpec.repository: "team/subteam/router" + asserts: + - template: deployment-router.yaml + equal: + path: spec.template.spec.containers[0].image + value: "mirror.example.com/team/subteam/router:v1" + - it: does not duplicate an already matching registry + set: + global.imageRegistry: "mirror.example.com" + routerSpec.repository: "mirror.example.com/team/router" + asserts: + - template: deployment-router.yaml + equal: + path: spec.template.spec.containers[0].image + value: "mirror.example.com/team/router:v1" + - it: allows a registry path prefix and trailing slash + set: + global.imageRegistry: "mirror.example.com/cache/" + routerSpec.repository: "ghcr.io/team/router" + asserts: + - template: deployment-router.yaml + equal: + path: spec.template.spec.containers[0].image + value: "mirror.example.com/cache/team/router:v1" + - it: does not manufacture a registry-only reference for an empty image + set: + global.imageRegistry: "mirror.example.com" + servingEngineSpec.sidecar.image: "" + asserts: + - template: deployment-vllm-multi.yaml + equal: + path: spec.template.spec.containers[1].image + value: "" diff --git a/helm/tests/values/image-registry.yaml b/helm/tests/values/image-registry.yaml new file mode 100644 index 000000000..3fe0e5b63 --- /dev/null +++ b/helm/tests/values/image-registry.yaml @@ -0,0 +1,47 @@ +routerSpec: + repository: ghcr.io/lmcache/lmstack-router + tag: v1 +cacheserverSpec: + enabled: true + image: + repository: registry.example.org:5000/lmcache/cache + tag: v2 +loraController: + enableLoraController: true +sharedPvcStorage: + enabled: true +servingEngineSpec: + modelSpec: + - name: engine + repository: vllm/vllm-openai + tag: v3 + modelURL: example/model + replicaCount: 1 + requestCPU: 1 + requestMemory: 1Gi + requestGPU: 0 + pvcStorage: 1Gi + enableLoRA: true + initContainer: + name: init + image: busybox:1.36 + - name: ray + repository: localhost:5000/team/ray-vllm + tag: v4 + modelURL: example/model + replicaCount: 1 + requestCPU: 1 + requestMemory: 1Gi + requestGPU: 0 + initContainer: + name: worker-init + image: docker.io/library/busybox:1.36 + raySpec: + enabled: true + headNode: + requestCPU: 1 + requestMemory: 1Gi + requestGPU: 0 + initContainer: + name: head-init + image: localhost/tools/init@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa diff --git a/helm/values.schema.json b/helm/values.schema.json index bae9926ef..e728f81aa 100644 --- a/helm/values.schema.json +++ b/helm/values.schema.json @@ -174,6 +174,16 @@ "description": "Array of extra Kubernetes objects to deploy. Each object should be a valid Kubernetes manifest in YAML format. This can be used to deploy additional resources such as ConfigMaps, Secrets, or custom resources that are not directly supported by the chart's built-in configuration. Supports use of custom Helm templates.", "type": "array" }, + "global": { + "description": "Global configuration", + "type": "object", + "properties": { + "imageRegistry": { + "description": "Override the registry of images managed by this chart. Leave empty to use each configured image unchanged.", + "type": "string" + } + } + }, "grafanaDashboards": { "description": "Set to true do deploy dashboards stored in the \"dashboards\" directory as configmaps. This requires the kube-prometheus-stack to be deployed with Grafana enabled and properly configured to pick up dashboards from configmaps.", "type": "object", diff --git a/helm/values.yaml b/helm/values.yaml index 728cc372a..d6b9d2e76 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -1,6 +1,11 @@ # -- Default values for llmstack helm chart # -- Declare variables to be passed into your templates. +# -- Global configuration +global: + # -- Override the registry of images managed by this chart. Leave empty to use each configured image unchanged. + imageRegistry: "" + # -- Serving engine configuration servingEngineSpec: # -- Whether to enable the serving engine