From 8a3fb045100a9265ba147bb75843bd58cc868150 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Banaszewski?= Date: Wed, 22 Jul 2026 17:42:42 +0200 Subject: [PATCH] feat: add Helm chart for standalone AI Gateway (#27256) Adds the `coder-ai-gateway` Helm chart for deploying the Coder AI Gateway as a standalone Kubernetes workload. Adds the coder-ai-gateway Helm chart for deploying the Coder AI Gateway as a standalone Kubernetes workload. The chart supports AI Gateway keys from an existing Secret or environment configuration, Coder connectivity through CODER_URL, listener and Coder-facing TLS, and optional Service, Ingress, and Gateway API HTTPRoute resources. Integrates the chart with existing Helm build, lint, golden generation, release artifact, Helm repository, and OCI publishing workflows. --- .github/workflows/release.yaml | 6 +- .github/workflows/tag-and-release.yaml | 6 +- Makefile | 8 +- enterprise/cli/aigatewaystart.go | 3 + helm/Makefile | 10 +- helm/ai-gateway/.helmignore | 1 + helm/ai-gateway/Chart.lock | 6 + helm/ai-gateway/Chart.yaml | 24 ++ helm/ai-gateway/README.md | 189 +++++++++++++++ helm/ai-gateway/templates/NOTES.txt | 13 + helm/ai-gateway/templates/_aigateway.tpl | 150 ++++++++++++ helm/ai-gateway/templates/_helpers.tpl | 73 ++++++ helm/ai-gateway/templates/aigateway.yaml | 8 + .../ai-gateway/templates/extra-templates.yaml | 4 + helm/ai-gateway/templates/httproute.yaml | 30 +++ helm/ai-gateway/templates/ingress.yaml | 36 +++ helm/ai-gateway/templates/service.yaml | 35 +++ helm/ai-gateway/tests/chart_test.go | 229 ++++++++++++++++++ helm/ai-gateway/tests/testdata/custom.yaml | 69 ++++++ .../testdata/custom_ai-gateway-test.golden | 214 ++++++++++++++++ .../tests/testdata/default_values.golden | 133 ++++++++++ .../tests/testdata/default_values.yaml | 13 + .../fails_httproute_without_parent_refs.yaml | 8 + .../fails_httproute_without_service.yaml | 10 + .../testdata/fails_ingress_without_host.yaml | 8 + .../fails_ingress_without_service.yaml | 11 + .../testdata/fails_missing_key_field.yaml | 7 + .../fails_nodeport_with_clusterip.yaml | 8 + .../tests/testdata/fails_partial_ca_tls.yaml | 10 + .../testdata/fails_partial_client_tls.yaml | 10 + .../testdata/fails_partial_listener_tls.yaml | 9 + .../testdata/listener_tls_with_ingress.golden | 180 ++++++++++++++ .../testdata/listener_tls_with_ingress.yaml | 14 ++ .../ai-gateway/tests/testdata/networking.yaml | 33 +++ .../networking_ai-gateway-test.golden | 206 ++++++++++++++++ .../ai-gateway/tests/testdata/nodeport.golden | 141 +++++++++++ helm/ai-gateway/tests/testdata/nodeport.yaml | 14 ++ helm/ai-gateway/values.schema.json | 143 +++++++++++ helm/ai-gateway/values.yaml | 228 +++++++++++++++++ scripts/helm.sh | 6 +- 40 files changed, 2299 insertions(+), 7 deletions(-) create mode 100644 helm/ai-gateway/.helmignore create mode 100644 helm/ai-gateway/Chart.lock create mode 100644 helm/ai-gateway/Chart.yaml create mode 100644 helm/ai-gateway/README.md create mode 100644 helm/ai-gateway/templates/NOTES.txt create mode 100644 helm/ai-gateway/templates/_aigateway.tpl create mode 100644 helm/ai-gateway/templates/_helpers.tpl create mode 100644 helm/ai-gateway/templates/aigateway.yaml create mode 100644 helm/ai-gateway/templates/extra-templates.yaml create mode 100644 helm/ai-gateway/templates/httproute.yaml create mode 100644 helm/ai-gateway/templates/ingress.yaml create mode 100644 helm/ai-gateway/templates/service.yaml create mode 100644 helm/ai-gateway/tests/chart_test.go create mode 100644 helm/ai-gateway/tests/testdata/custom.yaml create mode 100644 helm/ai-gateway/tests/testdata/custom_ai-gateway-test.golden create mode 100644 helm/ai-gateway/tests/testdata/default_values.golden create mode 100644 helm/ai-gateway/tests/testdata/default_values.yaml create mode 100644 helm/ai-gateway/tests/testdata/fails_httproute_without_parent_refs.yaml create mode 100644 helm/ai-gateway/tests/testdata/fails_httproute_without_service.yaml create mode 100644 helm/ai-gateway/tests/testdata/fails_ingress_without_host.yaml create mode 100644 helm/ai-gateway/tests/testdata/fails_ingress_without_service.yaml create mode 100644 helm/ai-gateway/tests/testdata/fails_missing_key_field.yaml create mode 100644 helm/ai-gateway/tests/testdata/fails_nodeport_with_clusterip.yaml create mode 100644 helm/ai-gateway/tests/testdata/fails_partial_ca_tls.yaml create mode 100644 helm/ai-gateway/tests/testdata/fails_partial_client_tls.yaml create mode 100644 helm/ai-gateway/tests/testdata/fails_partial_listener_tls.yaml create mode 100644 helm/ai-gateway/tests/testdata/listener_tls_with_ingress.golden create mode 100644 helm/ai-gateway/tests/testdata/listener_tls_with_ingress.yaml create mode 100644 helm/ai-gateway/tests/testdata/networking.yaml create mode 100644 helm/ai-gateway/tests/testdata/networking_ai-gateway-test.golden create mode 100644 helm/ai-gateway/tests/testdata/nodeport.golden create mode 100644 helm/ai-gateway/tests/testdata/nodeport.yaml create mode 100644 helm/ai-gateway/values.schema.json create mode 100644 helm/ai-gateway/values.yaml diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index f37135f667..95690adb25 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -254,7 +254,8 @@ jobs: build/coder_"$version"_{darwin,windows}_{amd64,arm64}.zip \ build/coder_"$version"_windows_amd64_installer.exe \ build/coder_helm_"$version".tgz \ - build/provisioner_helm_"$version".tgz + build/provisioner_helm_"$version".tgz \ + build/ai-gateway_helm_"$version".tgz env: CODER_SIGN_WINDOWS: "1" CODER_SIGN_DARWIN: "1" @@ -629,14 +630,17 @@ jobs: mkdir -p build/helm cp "build/coder_helm_${version}.tgz" build/helm cp "build/provisioner_helm_${version}.tgz" build/helm + cp "build/ai-gateway_helm_${version}.tgz" build/helm gsutil cp gs://helm.coder.com/v2/index.yaml build/helm/index.yaml helm repo index build/helm --url https://helm.coder.com/v2 --merge build/helm/index.yaml gsutil -h "Cache-Control:no-cache,max-age=0" cp "build/helm/coder_helm_${version}.tgz" gs://helm.coder.com/v2 gsutil -h "Cache-Control:no-cache,max-age=0" cp "build/helm/provisioner_helm_${version}.tgz" gs://helm.coder.com/v2 + gsutil -h "Cache-Control:no-cache,max-age=0" cp "build/helm/ai-gateway_helm_${version}.tgz" gs://helm.coder.com/v2 gsutil -h "Cache-Control:no-cache,max-age=0" cp "build/helm/index.yaml" gs://helm.coder.com/v2 gsutil -h "Cache-Control:no-cache,max-age=0" cp "helm/artifacthub-repo.yml" gs://helm.coder.com/v2 helm push "build/coder_helm_${version}.tgz" oci://ghcr.io/coder/chart helm push "build/provisioner_helm_${version}.tgz" oci://ghcr.io/coder/chart + helm push "build/ai-gateway_helm_${version}.tgz" oci://ghcr.io/coder/chart - name: Upload artifacts to actions (if dry-run) if: ${{ inputs.dry_run }} diff --git a/.github/workflows/tag-and-release.yaml b/.github/workflows/tag-and-release.yaml index 6d4c84d3dd..c786a1fa8d 100644 --- a/.github/workflows/tag-and-release.yaml +++ b/.github/workflows/tag-and-release.yaml @@ -347,7 +347,8 @@ jobs: build/coder_"$version"_{darwin,windows}_{amd64,arm64}.zip \ build/coder_"$version"_windows_amd64_installer.exe \ build/coder_helm_"$version".tgz \ - build/provisioner_helm_"$version".tgz + build/provisioner_helm_"$version".tgz \ + build/ai-gateway_helm_"$version".tgz env: CODER_SIGN_WINDOWS: "1" CODER_SIGN_DARWIN: "1" @@ -697,14 +698,17 @@ jobs: mkdir -p build/helm cp "build/coder_helm_${version}.tgz" build/helm cp "build/provisioner_helm_${version}.tgz" build/helm + cp "build/ai-gateway_helm_${version}.tgz" build/helm gsutil cp gs://helm.coder.com/v2/index.yaml build/helm/index.yaml helm repo index build/helm --url https://helm.coder.com/v2 --merge build/helm/index.yaml gsutil -h "Cache-Control:no-cache,max-age=0" cp "build/helm/coder_helm_${version}.tgz" gs://helm.coder.com/v2 gsutil -h "Cache-Control:no-cache,max-age=0" cp "build/helm/provisioner_helm_${version}.tgz" gs://helm.coder.com/v2 + gsutil -h "Cache-Control:no-cache,max-age=0" cp "build/helm/ai-gateway_helm_${version}.tgz" gs://helm.coder.com/v2 gsutil -h "Cache-Control:no-cache,max-age=0" cp "build/helm/index.yaml" gs://helm.coder.com/v2 gsutil -h "Cache-Control:no-cache,max-age=0" cp "helm/artifacthub-repo.yml" gs://helm.coder.com/v2 helm push "build/coder_helm_${version}.tgz" oci://ghcr.io/coder/chart helm push "build/provisioner_helm_${version}.tgz" oci://ghcr.io/coder/chart + helm push "build/ai-gateway_helm_${version}.tgz" oci://ghcr.io/coder/chart - name: Send repository-dispatch event if: ${{ inputs.release_type != 'rc' && inputs.release_type != 'create-release-branch' }} diff --git a/Makefile b/Makefile index a1aa57d4e0..5ddc4058cd 100644 --- a/Makefile +++ b/Makefile @@ -542,7 +542,7 @@ push/$(CODER_MAIN_IMAGE): $(CODER_MAIN_IMAGE) .PHONY: push/$(CODER_MAIN_IMAGE) # Helm charts that are available -charts = coder provisioner +charts = coder provisioner ai-gateway # Shortcut for Helm chart package. $(foreach chart,$(charts),build/$(chart)_helm.tgz): build/%_helm.tgz: build/%_helm_$(VERSION).tgz @@ -1060,6 +1060,7 @@ gen/golden-files: \ enterprise/tailnet/testdata/.gen-golden \ helm/coder/tests/testdata/.gen-golden \ helm/provisioner/tests/testdata/.gen-golden \ + helm/ai-gateway/tests/testdata/.gen-golden \ provisioner/terraform/testdata/.gen-golden \ tailnet/testdata/.gen-golden .PHONY: gen/golden-files @@ -1396,6 +1397,7 @@ clean/golden-files: enterprise/tailnet/testdata \ helm/coder/tests/testdata \ helm/provisioner/tests/testdata \ + helm/ai-gateway/tests/testdata \ provisioner/terraform/testdata \ tailnet/testdata \ -type f -name '*.golden' -delete @@ -1437,6 +1439,10 @@ helm/provisioner/tests/testdata/.gen-golden: $(wildcard helm/provisioner/tests/t fi touch "$@" +helm/ai-gateway/tests/testdata/.gen-golden: $(wildcard helm/ai-gateway/tests/testdata/*.yaml) $(wildcard helm/ai-gateway/tests/testdata/*.golden) $(GO_SRC_FILES) $(wildcard helm/ai-gateway/tests/*_test.go) + TZ=UTC go test ./helm/ai-gateway/tests -run=TestUpdateGoldenFiles -update + touch "$@" + coderd/.gen-golden: $(wildcard coderd/testdata/*/*.golden) $(GO_SRC_FILES) $(wildcard coderd/*_test.go) TZ=UTC go test ./coderd -run="Test.*Golden$$" -update touch "$@" diff --git a/enterprise/cli/aigatewaystart.go b/enterprise/cli/aigatewaystart.go index b45700dc25..7faba2f970 100644 --- a/enterprise/cli/aigatewaystart.go +++ b/enterprise/cli/aigatewaystart.go @@ -35,6 +35,9 @@ import ( ) const ( + // helm/ai-gateway's terminationGracePeriodSeconds must exceed + // shutdownTimeout so graceful shutdown completes before Kubernetes sends + // SIGKILL. shutdownTimeout = 5 * time.Minute traceShutdownTimeout = 5 * time.Second diff --git a/helm/Makefile b/helm/Makefile index 467d4e6e36..e7ae090a95 100644 --- a/helm/Makefile +++ b/helm/Makefile @@ -13,7 +13,7 @@ all: lint lint: lint/helm .PHONY: lint -lint/helm: lint/helm/coder lint/helm/provisioner +lint/helm: lint/helm/coder lint/helm/provisioner lint/helm/ai-gateway .PHONY: lint/helm lint/helm/coder: @@ -25,3 +25,11 @@ lint/helm/provisioner: helm dependency update --skip-refresh provisioner/ helm lint --strict --set coder.image.tag=v0.0.1 provisioner/ .PHONY: lint/helm/provisioner + +lint/helm/ai-gateway: + helm dependency update --skip-refresh ai-gateway/ + helm lint --strict \ + --set coder.image.tag=v0.0.1 \ + --set aigateway.keySecret.name=test \ + ai-gateway/ +.PHONY: lint/helm/ai-gateway diff --git a/helm/ai-gateway/.helmignore b/helm/ai-gateway/.helmignore new file mode 100644 index 0000000000..2b29f27645 --- /dev/null +++ b/helm/ai-gateway/.helmignore @@ -0,0 +1 @@ +tests diff --git a/helm/ai-gateway/Chart.lock b/helm/ai-gateway/Chart.lock new file mode 100644 index 0000000000..e504f949f3 --- /dev/null +++ b/helm/ai-gateway/Chart.lock @@ -0,0 +1,6 @@ +dependencies: +- name: libcoder + repository: file://../libcoder + version: 0.1.0 +digest: sha256:5c9a99109258073b590a9f98268490ef387fde24c0c7c7ade9c1a8c7ef5e6e10 +generated: "2026-07-14T17:15:50.670279008Z" diff --git a/helm/ai-gateway/Chart.yaml b/helm/ai-gateway/Chart.yaml new file mode 100644 index 0000000000..ecb3e8f7d3 --- /dev/null +++ b/helm/ai-gateway/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: coder-ai-gateway +description: Standalone AI Gateway for Coder +home: https://github.com/coder/coder +# These versions are replaced when Coder releases the chart. +type: application +version: "0.1.0" +appVersion: "0.1.0" +kubeVersion: ">= 1.23.0-0" +keywords: + - coder + - ai +sources: + - https://github.com/coder/coder/tree/main/helm/ai-gateway +icon: https://helm.coder.com/coder_logo_black.png +maintainers: + - name: Coder Technologies, Inc. + email: support@coder.com + url: https://coder.com/contact + +dependencies: + - name: libcoder + version: 0.1.0 + repository: file://../libcoder diff --git a/helm/ai-gateway/README.md b/helm/ai-gateway/README.md new file mode 100644 index 0000000000..40a5643f9c --- /dev/null +++ b/helm/ai-gateway/README.md @@ -0,0 +1,189 @@ +# Coder AI Gateway Helm chart + +This chart deploys the Coder AI Gateway as a standalone Kubernetes Deployment. +The Gateway connects to Coder using `CODER_URL` and an AI Gateway key. To forward +proxied AI traffic to the standalone Gateway, configure the Coder AI Gateway +Proxy (`aibridgeproxyd`) after installing the chart. + +The chart does not create credentials or TLS Secrets. + +## Install + +### Prerequisites + +- An AI Gateway key created in Coder. +- A Coder image that includes the `coder ai-gateway start` command. The official + Coder v2.36.0 image is the first version to include this command. + +### Configure the chart + +Create a `values.yaml` file with the Coder URL and an AI Gateway key source. The +following example uses a Kubernetes Secret in the Helm release namespace: + +```console +kubectl create secret generic coder-ai-gateway-key \ + --namespace \ + --from-literal=key='' +``` + +```yaml +coder: + image: + # Required when installing the chart directly from Git. + tag: "" + env: + - name: CODER_URL + value: https://coder.example.com + +aigateway: + keySecret: + name: coder-ai-gateway-key +``` + +The Gateway can also connect to Coder through an in-cluster Service, for +example: + +```yaml +coder: + env: + - name: CODER_URL + value: http://coder.coder.svc.cluster.local:80 +``` + +For HTTPS, the Coder certificate must cover the internal Service hostname and +the Gateway must trust its issuing CA. + +Instead of `aigateway.keySecret`, set `CODER_AI_GATEWAY_KEY` or +`CODER_AI_GATEWAY_KEY_FILE` through `coder.env`. Environment variables can also +be supplied through `coder.envFrom`. The chart does not check for variable +conflicts, regardless of whether values come from Helm options, `coder.env`, or +`coder.envFrom`. + +When installing a released chart package, the chart automatically uses the +matching Coder image version. Set `coder.image.tag` only when installing +directly from Git or overriding the image version. Custom images must provide +the `coder ai-gateway start` command. + +### Install the chart + +```console +helm install ai-gateway ./helm/ai-gateway \ + --namespace \ + --values values.yaml +``` + +## Connect Coder to the standalone Gateway + +To route proxied AI requests through the standalone Gateway, configure the Coder +AI Gateway Proxy with a target URL. When `service.enable` is true, the chart +notes show the direct in-cluster Service URL, including the scheme selected by +`aigateway.listenerTLS`. Retrieve it with: + +```console +helm get notes ai-gateway --namespace +``` + +The chart notes do not show an Ingress or `HTTPRoute` URL. To route through one +of these entry points, set `CODER_AI_GATEWAY_PROXY_TARGET` to its URL instead. +When `service.enable` is false, set the target to the URL of your user-managed +route to the Deployment. + +When listener TLS uses a private CA, the AI Gateway Proxy must trust that CA to +connect directly to the Service over HTTPS. + +## TLS + +For Gateway-to-Coder HTTPS with a private CA, set +`aigateway.coderTLS.caSecret`. If Coder requires client mTLS, also set +`aigateway.coderTLS.clientSecret`. + +Prefer terminating client-facing TLS at a Kubernetes Ingress or a `Gateway` +resource from the Kubernetes Gateway API. To terminate TLS in the AI Gateway +process, set `aigateway.listenerTLS.name` to an existing TLS Secret. + +Client-facing TLS and backend TLS are independent. The `ingress.tls` settings +configure TLS between clients and the Ingress. For `HTTPRoute`, the Gateway +listener that accepts client connections is configured outside this chart. +These settings do not configure whether the Ingress or Gateway connects to the +AI Gateway Service using HTTP or HTTPS. + +When `aigateway.listenerTLS` is enabled behind an Ingress or `HTTPRoute`, +configure the entry point to connect to the Service using HTTPS and trust the AI +Gateway certificate. Ingress backend TLS is controller-specific and can usually +be configured with `ingress.annotations`. Gateway API backend TLS uses a +separate `BackendTLSPolicy`, which can be managed outside this chart or rendered +with `extraTemplates`. The chart does not infer or validate this +controller-specific configuration. Without backend TLS, the entry point sends +plaintext HTTP to the HTTPS listener, which typically results in a TLS handshake +error reported as HTTP 502. + +All referenced TLS Secrets must exist in the Helm release namespace. + +## Networking + +The data-plane Service, which carries LLM traffic, is a `ClusterIP` by default. +`NodePort` and `LoadBalancer` are explicit alternatives. Ingress and `HTTPRoute` +are optional and both route to the data-plane Service. If you enable Ingress or +`HTTPRoute`, use a `ClusterIP` Service unless you intentionally need a second +external entry point through a `LoadBalancer` Service. + +## Scaling and resources + +Set `coder.replicaCount` to run multiple AI Gateway replicas. The default +resource requests are 1 CPU and 1 GiB of memory per replica. These requests are +a starting point, not a capacity guarantee. CPU and memory usage depend heavily +on concurrent requests and payload size. + +Adjust `coder.resources` after observing production traffic. Consider setting +`CODER_AI_GATEWAY_MAX_CONCURRENCY` through `coder.env` to bound concurrent +requests per replica. The application default is unlimited. The chart does not +set resource limits by default, which avoids CPU throttling and fixed memory +limits for bursty workloads. Manage resources such as a Horizontal Pod +Autoscaler or PodDisruptionBudget through your platform configuration or +`extraTemplates`. + +## Metrics + +Every pod runs an unauthenticated metrics listener on the named `metrics` port, +which maps to port `2112`. The chart does not create monitoring discovery +resources. Configure pod-based discovery with `coder.podAnnotations`, for +example: + +```yaml +coder: + podAnnotations: + prometheus.io/scrape: "true" + prometheus.io/port: "2112" +``` + +Alternatively, create discovery resources such as a `ServiceMonitor` through +your monitoring stack or `extraTemplates`. + +## Key rotation + +1. Create a new AI Gateway key in Coder. +2. Update the configured key source: + - For `aigateway.keySecret`, update the referenced Secret or set `name` to a + new Secret. + - For a key supplied through `coder.env`, update the environment variable or + the file it references. +3. If the update did not trigger a rollout, restart the Deployment, for example: + + ```console + kubectl rollout restart deployment/coder-ai-gateway \ + --namespace + ``` + +4. Verify every replica is ready and serving with the new key. +5. Revoke the old key. + +Secret updates do not change the Deployment pod template automatically. A +reloader controller can be configured through `coder.annotations` or +`coder.podAnnotations`. + +## Extra manifests + +`extraTemplates` renders additional Kubernetes manifests as part of the Helm +release. Entries can be YAML strings or Kubernetes objects, and can use Helm +release values and chart helpers. Use them for small companion resources, such +as a `NetworkPolicy`. diff --git a/helm/ai-gateway/templates/NOTES.txt b/helm/ai-gateway/templates/NOTES.txt new file mode 100644 index 0000000000..1bfa597412 --- /dev/null +++ b/helm/ai-gateway/templates/NOTES.txt @@ -0,0 +1,13 @@ +Coder AI Gateway is installed as {{ include "coder.name" . }}. + +{{- if .Values.service.enable }} +Configure the Coder deployment with: + + CODER_AI_GATEWAY_PROXY_TARGET={{ ternary "https" "http" (not (empty .Values.aigateway.listenerTLS.name)) }}://{{ include "coder.name" . }}.{{ .Release.Namespace }}.svc.cluster.local:{{ .Values.service.port }} +{{- else }} +The chart-managed Service is disabled. Configure CODER_AI_GATEWAY_PROXY_TARGET +with the URL of the user-managed route to this deployment. +{{- end }} + +The metrics listener is active on port 2112. Configure discovery with +coder.podAnnotations or resources managed by your monitoring stack. diff --git a/helm/ai-gateway/templates/_aigateway.tpl b/helm/ai-gateway/templates/_aigateway.tpl new file mode 100644 index 0000000000..8414222016 --- /dev/null +++ b/helm/ai-gateway/templates/_aigateway.tpl @@ -0,0 +1,150 @@ +{{/* +Service account to merge into the libcoder template. The Gateway never needs +the Kubernetes API, so the token is not mounted. +*/}} +{{- define "coder-ai-gateway.serviceaccount" -}} +automountServiceAccountToken: false +{{- end }} + +{{/* +HTTP probe shared by startup, liveness, and readiness configuration. +*/}} +{{- define "coder-ai-gateway.probe" -}} +httpGet: + path: {{ .path }} + port: http + scheme: {{ .scheme }} +initialDelaySeconds: {{ .probe.initialDelaySeconds }} +{{- range $field := list "periodSeconds" "timeoutSeconds" "successThreshold" "failureThreshold" }} +{{- if hasKey $.probe $field }} +{{ $field }}: {{ index $.probe $field }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Component annotation for pod metadata. +*/}} +{{- define "coder.componentAnnotation" -}} +app.kubernetes.io/component: ai-gateway +{{- end }} + +{{/* +Deployment to merge into the libcoder template. +*/}} +{{- define "coder-ai-gateway.deployment" -}} +spec: + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 0 + maxSurge: 1 + template: + spec: + automountServiceAccountToken: false + terminationGracePeriodSeconds: {{ .Values.aigateway.terminationGracePeriodSeconds }} + containers: + - +{{ include "libcoder.containerspec" (list . "coder-ai-gateway.containerspec") | indent 8 }} + volumes: + {{- if .Values.aigateway.keySecret.name }} + - name: ai-gateway-auth + secret: + secretName: {{ .Values.aigateway.keySecret.name }} + items: + - key: {{ .Values.aigateway.keySecret.key }} + path: key + {{- end }} + {{- if .Values.aigateway.listenerTLS.name }} + - name: ai-gateway-listener + secret: + secretName: {{ .Values.aigateway.listenerTLS.name }} + items: + - key: {{ .Values.aigateway.listenerTLS.certKey }} + path: tls.crt + - key: {{ .Values.aigateway.listenerTLS.keyKey }} + path: tls.key + {{- end }} + {{- if .Values.aigateway.coderTLS.caSecret.name }} + - name: coder-client-ca + secret: + secretName: {{ .Values.aigateway.coderTLS.caSecret.name }} + items: + - key: {{ .Values.aigateway.coderTLS.caSecret.key }} + path: ca.crt + {{- end }} + {{- if .Values.aigateway.coderTLS.clientSecret.name }} + - name: coder-client-tls + secret: + secretName: {{ .Values.aigateway.coderTLS.clientSecret.name }} + items: + - key: {{ .Values.aigateway.coderTLS.clientSecret.certKey }} + path: tls.crt + - key: {{ .Values.aigateway.coderTLS.clientSecret.keyKey }} + path: tls.key + {{- end }} + {{- include "coder.volumeList" . | nindent 8 }} +{{- end }} + +{{/* +ContainerSpec for the AI Gateway container of the deployment. +*/}} +{{- define "coder-ai-gateway.containerspec" -}} +args: +- ai-gateway +- start +{{- with .Values.coder.envFrom }} +envFrom: +{{ toYaml . }} +{{- end }} +env: +{{ include "coder-ai-gateway.defaultEnv" . }} +{{/* +User additions follow chart defaults so they may reference or override them. +*/}} +{{- with .Values.coder.env }} +{{ toYaml . }} +{{- end }} +ports: +- name: http + containerPort: 4001 + protocol: TCP +- name: metrics + containerPort: 2112 + protocol: TCP +{{- $scheme := ternary "HTTPS" "HTTP" (not (empty .Values.aigateway.listenerTLS.name)) }} +{{- if .Values.coder.startupProbe.enabled }} +startupProbe: +{{ include "coder-ai-gateway.probe" (dict "probe" .Values.coder.startupProbe "path" "/healthz" "scheme" $scheme) | indent 2 }} +{{- end }} +{{- if .Values.coder.livenessProbe.enabled }} +livenessProbe: +{{ include "coder-ai-gateway.probe" (dict "probe" .Values.coder.livenessProbe "path" "/healthz" "scheme" $scheme) | indent 2 }} +{{- end }} +{{- if .Values.coder.readinessProbe.enabled }} +readinessProbe: +{{ include "coder-ai-gateway.probe" (dict "probe" .Values.coder.readinessProbe "path" "/readyz" "scheme" $scheme) | indent 2 }} +{{- end }} +volumeMounts: +{{- if .Values.aigateway.keySecret.name }} +- name: ai-gateway-auth + mountPath: /etc/coder/ai-gateway-auth + readOnly: true +{{- end }} +{{- if .Values.aigateway.listenerTLS.name }} +- name: ai-gateway-listener + mountPath: /etc/coder/ai-gateway-listener + readOnly: true +{{- end }} +{{- if .Values.aigateway.coderTLS.caSecret.name }} +- name: coder-client-ca + mountPath: /etc/coder/coder-client-ca + readOnly: true +{{- end }} +{{- if .Values.aigateway.coderTLS.clientSecret.name }} +- name: coder-client-tls + mountPath: /etc/coder/coder-client-tls + readOnly: true +{{- end }} +{{- include "coder.volumeMountList" . | nindent 0 }} +{{- end }} diff --git a/helm/ai-gateway/templates/_helpers.tpl b/helm/ai-gateway/templates/_helpers.tpl new file mode 100644 index 0000000000..091d2b828c --- /dev/null +++ b/helm/ai-gateway/templates/_helpers.tpl @@ -0,0 +1,73 @@ +{{/* +Environment variables configured by the chart. User-supplied variables follow +these entries, including when a name is duplicated. +*/}} +{{- define "coder-ai-gateway.defaultEnv" -}} +- name: CODER_AI_GATEWAY_HTTP_ADDRESS + value: 0.0.0.0:4001 +{{- if .Values.aigateway.keySecret.name }} +- name: CODER_AI_GATEWAY_KEY_FILE + value: /etc/coder/ai-gateway-auth/key +{{- end }} +- name: CODER_PROMETHEUS_ENABLE + value: "true" +- name: CODER_PROMETHEUS_ADDRESS + value: 0.0.0.0:2112 +{{- if .Values.aigateway.listenerTLS.name }} +- name: CODER_AI_GATEWAY_TLS_CERT_FILE + value: /etc/coder/ai-gateway-listener/tls.crt +- name: CODER_AI_GATEWAY_TLS_KEY_FILE + value: /etc/coder/ai-gateway-listener/tls.key +{{- end }} +{{- if .Values.aigateway.coderTLS.caSecret.name }} +- name: CODER_CLIENT_TLS_CA_FILE + value: /etc/coder/coder-client-ca/ca.crt +{{- end }} +{{- if .Values.aigateway.coderTLS.clientSecret.name }} +- name: CODER_CLIENT_TLS_CERT_FILE + value: /etc/coder/coder-client-tls/tls.crt +- name: CODER_CLIENT_TLS_KEY_FILE + value: /etc/coder/coder-client-tls/tls.key +{{- end }} +{{- end -}} + +{{/* +Cross-field validation, invoked once from aigateway.yaml. Emits nothing and +aborts rendering with a specific message on inconsistent values. Each failure +message is asserted verbatim in tests/chart_test.go. +*/}} +{{- define "coder-ai-gateway.validate" -}} +{{- if and .Values.aigateway.keySecret.name (not .Values.aigateway.keySecret.key) }} +{{- fail "aigateway.keySecret.key is required when name is set." }} +{{- end }} +{{- if and .Values.ingress.enable (not .Values.service.enable) }} +{{- fail "service.enable must be true when ingress.enable is true." }} +{{- end }} +{{- if and .Values.ingress.enable (not .Values.ingress.host) }} +{{- fail "ingress.host is required when ingress.enable is true." }} +{{- end }} +{{- if and .Values.httproute.enable (not .Values.service.enable) }} +{{- fail "service.enable must be true when httproute.enable is true." }} +{{- end }} +{{- if and .Values.httproute.enable (empty .Values.httproute.parentRefs) }} +{{- fail "httproute.parentRefs is required when httproute.enable is true." }} +{{- end }} +{{- $listener := .Values.aigateway.listenerTLS }} +{{- if and $listener.name (or (not $listener.certKey) (not $listener.keyKey)) }} +{{- fail "aigateway.listenerTLS.certKey and keyKey are required when name is set." }} +{{- end }} +{{- if and .Values.httproute.enable (not (.Capabilities.APIVersions.Has "gateway.networking.k8s.io/v1/HTTPRoute")) }} +{{- fail "httproute.enable requires the gateway.networking.k8s.io/v1 HTTPRoute CRD." }} +{{- end }} +{{- $client := .Values.aigateway.coderTLS.clientSecret }} +{{- if and $client.name (or (not $client.certKey) (not $client.keyKey)) }} +{{- fail "aigateway.coderTLS.clientSecret.certKey and keyKey are required when name is set." }} +{{- end }} +{{- $ca := .Values.aigateway.coderTLS.caSecret }} +{{- if and $ca.name (not $ca.key) }} +{{- fail "aigateway.coderTLS.caSecret.key is required when name is set." }} +{{- end }} +{{- if and .Values.service.nodePort (not (has .Values.service.type (list "NodePort" "LoadBalancer"))) }} +{{- fail "service.nodePort requires service.type to be NodePort or LoadBalancer." }} +{{- end }} +{{- end -}} diff --git a/helm/ai-gateway/templates/aigateway.yaml b/helm/ai-gateway/templates/aigateway.yaml new file mode 100644 index 0000000000..4e9647d74e --- /dev/null +++ b/helm/ai-gateway/templates/aigateway.yaml @@ -0,0 +1,8 @@ +{{- include "coder-ai-gateway.validate" . -}} +--- +{{- if not .Values.coder.serviceAccount.disableCreate }} +{{ include "libcoder.serviceaccount" (list . "coder-ai-gateway.serviceaccount") }} +{{- end }} + +--- +{{ include "libcoder.deployment" (list . "coder-ai-gateway.deployment") }} diff --git a/helm/ai-gateway/templates/extra-templates.yaml b/helm/ai-gateway/templates/extra-templates.yaml new file mode 100644 index 0000000000..e047658100 --- /dev/null +++ b/helm/ai-gateway/templates/extra-templates.yaml @@ -0,0 +1,4 @@ +{{- range .Values.extraTemplates }} +--- +{{ include "coder.renderTemplate" (dict "value" . "context" $) }} +{{- end }} diff --git a/helm/ai-gateway/templates/httproute.yaml b/helm/ai-gateway/templates/httproute.yaml new file mode 100644 index 0000000000..e3827b5fdc --- /dev/null +++ b/helm/ai-gateway/templates/httproute.yaml @@ -0,0 +1,30 @@ +{{- if .Values.httproute.enable }} +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: {{ include "coder.name" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "coder.labels" . | nindent 4 }} + {{- with .Values.httproute.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- with .Values.httproute.parentRefs }} + parentRefs: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.httproute.hostnames }} + hostnames: + {{- toYaml . | nindent 4 }} + {{- end }} + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: {{ include "coder.name" . }} + port: {{ .Values.service.port }} +{{- end }} diff --git a/helm/ai-gateway/templates/ingress.yaml b/helm/ai-gateway/templates/ingress.yaml new file mode 100644 index 0000000000..6ccf9f022f --- /dev/null +++ b/helm/ai-gateway/templates/ingress.yaml @@ -0,0 +1,36 @@ +{{- if .Values.ingress.enable }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "coder.name" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "coder.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- with .Values.ingress.className }} + ingressClassName: {{ . }} + {{- end }} + {{- if .Values.ingress.tls.enable }} + tls: + - hosts: + - {{ .Values.ingress.host | quote }} + {{- with .Values.ingress.tls.secretName }} + secretName: {{ . }} + {{- end }} + {{- end }} + rules: + - host: {{ .Values.ingress.host | quote }} + http: + paths: + - path: {{ .Values.ingress.path }} + pathType: {{ .Values.ingress.pathType }} + backend: + service: + name: {{ include "coder.name" . }} + port: + number: {{ .Values.service.port }} +{{- end }} diff --git a/helm/ai-gateway/templates/service.yaml b/helm/ai-gateway/templates/service.yaml new file mode 100644 index 0000000000..e32cac88de --- /dev/null +++ b/helm/ai-gateway/templates/service.yaml @@ -0,0 +1,35 @@ +{{- if .Values.service.enable }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "coder.name" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "coder.labels" . | nindent 4 }} + {{- with .Values.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.service.type }} + sessionAffinity: {{ .Values.service.sessionAffinity }} + {{- if or (eq .Values.service.type "LoadBalancer") (eq .Values.service.type "NodePort") }} + externalTrafficPolicy: {{ .Values.service.externalTrafficPolicy }} + {{- end }} + {{- with .Values.service.loadBalancerClass }} + loadBalancerClass: {{ . }} + {{- end }} + {{- with .Values.service.loadBalancerIP }} + loadBalancerIP: {{ . }} + {{- end }} + selector: + {{- include "coder.selectorLabels" . | nindent 4 }} + ports: + - name: http + port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + {{- with .Values.service.nodePort }} + nodePort: {{ . }} + {{- end }} +{{- end }} diff --git a/helm/ai-gateway/tests/chart_test.go b/helm/ai-gateway/tests/chart_test.go new file mode 100644 index 0000000000..835a22fa51 --- /dev/null +++ b/helm/ai-gateway/tests/chart_test.go @@ -0,0 +1,229 @@ +package tests // nolint: testpackage + +import ( + "bytes" + "flag" + "os" + "os/exec" + "path/filepath" + "runtime" + "sync" + "testing" + + "github.com/stretchr/testify/require" + "golang.org/x/xerrors" + + "github.com/coder/coder/v2/testutil" +) + +var updateGoldenFiles = flag.Bool("update", false, "Update golden files") + +var testCases = []testCase{ + { + name: "default_values", + fixture: "default_values", + }, + { + name: "networking", + fixture: "networking", + namespace: "ai-gateway-test", + apiVersions: []string{"gateway.networking.k8s.io/v1/HTTPRoute"}, + }, + { + name: "custom", + fixture: "custom", + namespace: "ai-gateway-test", + }, + { + name: "nodeport", + fixture: "nodeport", + }, + { + name: "missing_key_field", + fixture: "fails_missing_key_field", + expectedError: "aigateway.keySecret.key is required when name is set.", + }, + { + name: "partial_listener_tls", + fixture: "fails_partial_listener_tls", + expectedError: "aigateway.listenerTLS.certKey and keyKey are required when name is set.", + }, + // This verifies that listener TLS and Ingress can be rendered together. + // Production use requires controller-specific backend TLS and certificate + // trust configuration outside this chart. + { + name: "listener_tls_with_ingress", + fixture: "listener_tls_with_ingress", + }, + { + name: "partial_client_tls", + fixture: "fails_partial_client_tls", + expectedError: "aigateway.coderTLS.clientSecret.certKey and keyKey are required when name is set.", + }, + { + name: "partial_ca_tls", + fixture: "fails_partial_ca_tls", + expectedError: "aigateway.coderTLS.caSecret.key is required when name is set.", + }, + { + name: "ingress_without_service", + fixture: "fails_ingress_without_service", + expectedError: "service.enable must be true when ingress.enable is true.", + }, + { + name: "ingress_without_host", + fixture: "fails_ingress_without_host", + expectedError: "ingress.host is required when ingress.enable is true.", + }, + { + name: "httproute_without_service", + fixture: "fails_httproute_without_service", + expectedError: "service.enable must be true when httproute.enable is true.", + }, + { + name: "httproute_without_parent_refs", + fixture: "fails_httproute_without_parent_refs", + expectedError: "httproute.parentRefs is required when httproute.enable is true.", + apiVersions: []string{"gateway.networking.k8s.io/v1/HTTPRoute"}, + }, + { + name: "httproute_without_crd", + fixture: "networking", + expectedError: "httproute.enable requires the gateway.networking.k8s.io/v1 HTTPRoute CRD.", + }, + { + name: "nodeport_with_clusterip", + fixture: "fails_nodeport_with_clusterip", + expectedError: "service.nodePort requires service.type to be NodePort or LoadBalancer.", + }, +} + +type testCase struct { + name string + fixture string + namespace string + expectedError string + apiVersions []string +} + +func (tc testCase) valuesFilePath() string { + return filepath.Join("testdata", tc.fixture+".yaml") +} + +func (tc testCase) goldenFilePath() string { + if tc.namespace == "default" { + return filepath.Join("testdata", tc.name+".golden") + } + return filepath.Join("testdata", tc.name+"_"+tc.namespace+".golden") +} + +func TestRenderChart(t *testing.T) { + t.Parallel() + if *updateGoldenFiles { + t.Skip("Golden files are being updated") + } + if testutil.InCI() && (runtime.GOOS == "windows" || runtime.GOOS == "darwin") { + t.Skip("Skipping Helm tests on Windows and macOS in CI") + } + + helmPath := lookupHelm(t) + err := updateHelmDependencies(t, helmPath, "..") + require.NoError(t, err, "failed to build Helm dependencies") + for _, tc := range testCases { + tc := tc + if tc.namespace == "" { + tc.namespace = "default" + } + t.Run(tc.namespace+"/"+tc.name, func(t *testing.T) { + t.Parallel() + output, err := runHelmTemplate(t, helmPath, tc.valuesFilePath(), tc.namespace, tc.apiVersions) + if tc.expectedError != "" { + require.Error(t, err) + require.Contains(t, output, tc.expectedError) + return + } + require.NoError(t, err, output) + golden, err := os.ReadFile(tc.goldenFilePath()) + require.NoError(t, err) + golden = bytes.ReplaceAll(golden, []byte("\r"), nil) + require.Equal(t, string(golden), output) + }) + } +} + +func TestUpdateGoldenFiles(t *testing.T) { + t.Parallel() + if !*updateGoldenFiles { + t.Skip("Run with -update to update golden files") + } + + helmPath := lookupHelm(t) + err := updateHelmDependencies(t, helmPath, "..") + require.NoError(t, err, "failed to build Helm dependencies") + for _, tc := range testCases { + if tc.expectedError != "" { + continue + } + if tc.namespace == "" { + tc.namespace = "default" + } + output, err := runHelmTemplate(t, helmPath, tc.valuesFilePath(), tc.namespace, tc.apiVersions) + require.NoError(t, err, output) + require.NoError(t, os.WriteFile(tc.goldenFilePath(), []byte(output), 0o644)) // nolint:gosec + } +} + +func runHelmTemplate(t testing.TB, helmPath, valuesFile, namespace string, apiVersions []string) (string, error) { + t.Helper() + args := []string{"template", "ai-gateway", "..", "-f", valuesFile, "--namespace", namespace} + for _, apiVersion := range apiVersions { + args = append(args, "--api-versions", apiVersion) + } + cmd := exec.Command(helmPath, args...) + cmd.Dir = "." + output, err := cmd.CombinedOutput() + return string(output), err +} + +// updateDepsOnce guards updateHelmDependencies: parallel top-level tests +// share the charts/ directory, and rebuilding it while another test +// templates the chart races. +var ( + updateDepsOnce sync.Once + errUpdateDeps error +) + +// updateHelmDependencies runs `helm dependency update .` on the given chartDir. +func updateHelmDependencies(t testing.TB, helmPath, chartDir string) error { + t.Helper() + updateDepsOnce.Do(func() { + // Remove charts/ from chartDir if it exists. + err := os.RemoveAll(filepath.Join(chartDir, "charts")) + if err != nil { + errUpdateDeps = xerrors.Errorf("failed to remove charts/ directory: %w", err) + return + } + + cmd := exec.Command(helmPath, "dependency", "update", "--skip-refresh", ".") + cmd.Dir = chartDir + t.Logf("exec command: %v", cmd.Args) + out, err := cmd.CombinedOutput() + if err != nil { + errUpdateDeps = xerrors.Errorf("failed to run `helm dependency update`: %w\noutput: %s", err, out) + return + } + }) + return errUpdateDeps +} + +func lookupHelm(t testing.TB) string { + t.Helper() + helmPath, err := exec.LookPath("helm") + require.NoError(t, err, "helm not found in PATH") + return helmPath +} + +func TestMain(m *testing.M) { + flag.Parse() + os.Exit(m.Run()) +} diff --git a/helm/ai-gateway/tests/testdata/custom.yaml b/helm/ai-gateway/tests/testdata/custom.yaml new file mode 100644 index 0000000000..f46ca14124 --- /dev/null +++ b/helm/ai-gateway/tests/testdata/custom.yaml @@ -0,0 +1,69 @@ +coder: + image: + tag: v2.36.0 + startupProbe: + enabled: true + initialDelaySeconds: 2 + periodSeconds: 5 + timeoutSeconds: 3 + successThreshold: 1 + failureThreshold: 7 + livenessProbe: + enabled: true + initialDelaySeconds: 12 + periodSeconds: 15 + timeoutSeconds: 13 + successThreshold: 1 + failureThreshold: 17 + readinessProbe: + enabled: true + initialDelaySeconds: 22 + periodSeconds: 25 + timeoutSeconds: 23 + successThreshold: 21 + failureThreshold: 27 + serviceAccount: + name: workload-identity + disableCreate: true + env: + - name: CODER_URL + value: https://coder-primary.coder-system.svc.cluster.local:8443 + - name: CODER_AI_GATEWAY_HTTP_ADDRESS + value: 127.0.0.1:4444 + - name: CODER_VERBOSE + value: "true" + envFrom: + - configMapRef: + name: gateway-config +aigateway: + keySecret: + name: ai-gateway-key + key: token + coderTLS: + caSecret: + name: coder-ca + key: root.pem + clientSecret: + name: coder-client + certKey: client.crt + keyKey: client.key + listenerTLS: + name: gateway-tls + certKey: cert.pem + keyKey: key.pem +extraTemplates: + - | + apiVersion: v1 + kind: ConfigMap + metadata: + name: '{{ include "coder.name" . }}-extra' + namespace: '{{ .Release.Namespace }}' + data: + release: '{{ .Release.Name }}' + - apiVersion: v1 + kind: ConfigMap + metadata: + name: '{{ include "coder.name" . }}-object' + namespace: '{{ .Release.Namespace }}' + data: + namespace: '{{ .Release.Namespace }}' diff --git a/helm/ai-gateway/tests/testdata/custom_ai-gateway-test.golden b/helm/ai-gateway/tests/testdata/custom_ai-gateway-test.golden new file mode 100644 index 0000000000..2f3d327845 --- /dev/null +++ b/helm/ai-gateway/tests/testdata/custom_ai-gateway-test.golden @@ -0,0 +1,214 @@ +--- +# Source: coder-ai-gateway/templates/extra-templates.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: 'coder-ai-gateway-extra' + namespace: 'ai-gateway-test' +data: + release: 'ai-gateway' +--- +# Source: coder-ai-gateway/templates/extra-templates.yaml +apiVersion: v1 +data: + namespace: 'ai-gateway-test' +kind: ConfigMap +metadata: + name: 'coder-ai-gateway-object' + namespace: 'ai-gateway-test' +--- +# Source: coder-ai-gateway/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: coder-ai-gateway + namespace: ai-gateway-test + labels: + helm.sh/chart: coder-ai-gateway-0.1.0 + app.kubernetes.io/name: coder-ai-gateway + app.kubernetes.io/instance: ai-gateway + app.kubernetes.io/part-of: coder-ai-gateway + app.kubernetes.io/version: "0.1.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + sessionAffinity: None + selector: + app.kubernetes.io/name: coder-ai-gateway + app.kubernetes.io/instance: ai-gateway + ports: + - name: http + port: 80 + targetPort: http + protocol: TCP +--- +# Source: coder-ai-gateway/templates/aigateway.yaml +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: ai-gateway + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder-ai-gateway + app.kubernetes.io/part-of: coder-ai-gateway + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-ai-gateway-0.1.0 + name: coder-ai-gateway + namespace: ai-gateway-test +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: ai-gateway + app.kubernetes.io/name: coder-ai-gateway + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + type: RollingUpdate + template: + metadata: + annotations: + app.kubernetes.io/component: ai-gateway + labels: + app.kubernetes.io/instance: ai-gateway + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder-ai-gateway + app.kubernetes.io/part-of: coder-ai-gateway + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-ai-gateway-0.1.0 + spec: + automountServiceAccountToken: false + containers: + - args: + - ai-gateway + - start + command: + - /opt/coder + env: + - name: CODER_AI_GATEWAY_HTTP_ADDRESS + value: 0.0.0.0:4001 + - name: CODER_AI_GATEWAY_KEY_FILE + value: /etc/coder/ai-gateway-auth/key + - name: CODER_PROMETHEUS_ENABLE + value: "true" + - name: CODER_PROMETHEUS_ADDRESS + value: 0.0.0.0:2112 + - name: CODER_AI_GATEWAY_TLS_CERT_FILE + value: /etc/coder/ai-gateway-listener/tls.crt + - name: CODER_AI_GATEWAY_TLS_KEY_FILE + value: /etc/coder/ai-gateway-listener/tls.key + - name: CODER_CLIENT_TLS_CA_FILE + value: /etc/coder/coder-client-ca/ca.crt + - name: CODER_CLIENT_TLS_CERT_FILE + value: /etc/coder/coder-client-tls/tls.crt + - name: CODER_CLIENT_TLS_KEY_FILE + value: /etc/coder/coder-client-tls/tls.key + - name: CODER_URL + value: https://coder-primary.coder-system.svc.cluster.local:8443 + - name: CODER_AI_GATEWAY_HTTP_ADDRESS + value: 127.0.0.1:4444 + - name: CODER_VERBOSE + value: "true" + envFrom: + - configMapRef: + name: gateway-config + image: ghcr.io/coder/coder:v2.36.0 + imagePullPolicy: IfNotPresent + lifecycle: {} + livenessProbe: + failureThreshold: 17 + httpGet: + path: /healthz + port: http + scheme: HTTPS + initialDelaySeconds: 12 + periodSeconds: 15 + successThreshold: 1 + timeoutSeconds: 13 + name: coder + ports: + - containerPort: 4001 + name: http + protocol: TCP + - containerPort: 2112 + name: metrics + protocol: TCP + readinessProbe: + failureThreshold: 27 + httpGet: + path: /readyz + port: http + scheme: HTTPS + initialDelaySeconds: 22 + periodSeconds: 25 + successThreshold: 21 + timeoutSeconds: 23 + resources: + requests: + cpu: "1" + memory: 1Gi + securityContext: + allowPrivilegeEscalation: false + runAsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + startupProbe: + failureThreshold: 7 + httpGet: + path: /healthz + port: http + scheme: HTTPS + initialDelaySeconds: 2 + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 3 + volumeMounts: + - mountPath: /etc/coder/ai-gateway-auth + name: ai-gateway-auth + readOnly: true + - mountPath: /etc/coder/ai-gateway-listener + name: ai-gateway-listener + readOnly: true + - mountPath: /etc/coder/coder-client-ca + name: coder-client-ca + readOnly: true + - mountPath: /etc/coder/coder-client-tls + name: coder-client-tls + readOnly: true + restartPolicy: Always + serviceAccountName: workload-identity + terminationGracePeriodSeconds: 330 + volumes: + - name: ai-gateway-auth + secret: + items: + - key: token + path: key + secretName: ai-gateway-key + - name: ai-gateway-listener + secret: + items: + - key: cert.pem + path: tls.crt + - key: key.pem + path: tls.key + secretName: gateway-tls + - name: coder-client-ca + secret: + items: + - key: root.pem + path: ca.crt + secretName: coder-ca + - name: coder-client-tls + secret: + items: + - key: client.crt + path: tls.crt + - key: client.key + path: tls.key + secretName: coder-client diff --git a/helm/ai-gateway/tests/testdata/default_values.golden b/helm/ai-gateway/tests/testdata/default_values.golden new file mode 100644 index 0000000000..cdee78607f --- /dev/null +++ b/helm/ai-gateway/tests/testdata/default_values.golden @@ -0,0 +1,133 @@ +--- +# Source: coder-ai-gateway/templates/aigateway.yaml +apiVersion: v1 +automountServiceAccountToken: false +kind: ServiceAccount +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: ai-gateway + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder-ai-gateway + app.kubernetes.io/part-of: coder-ai-gateway + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-ai-gateway-0.1.0 + name: coder-ai-gateway + namespace: default +--- +# Source: coder-ai-gateway/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: coder-ai-gateway + namespace: default + labels: + helm.sh/chart: coder-ai-gateway-0.1.0 + app.kubernetes.io/name: coder-ai-gateway + app.kubernetes.io/instance: ai-gateway + app.kubernetes.io/part-of: coder-ai-gateway + app.kubernetes.io/version: "0.1.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + sessionAffinity: None + selector: + app.kubernetes.io/name: coder-ai-gateway + app.kubernetes.io/instance: ai-gateway + ports: + - name: http + port: 80 + targetPort: http + protocol: TCP +--- +# Source: coder-ai-gateway/templates/aigateway.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: ai-gateway + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder-ai-gateway + app.kubernetes.io/part-of: coder-ai-gateway + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-ai-gateway-0.1.0 + name: coder-ai-gateway + namespace: default +spec: + replicas: 3 + selector: + matchLabels: + app.kubernetes.io/instance: ai-gateway + app.kubernetes.io/name: coder-ai-gateway + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + type: RollingUpdate + template: + metadata: + annotations: + app.kubernetes.io/component: ai-gateway + labels: + app.kubernetes.io/instance: ai-gateway + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder-ai-gateway + app.kubernetes.io/part-of: coder-ai-gateway + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-ai-gateway-0.1.0 + spec: + automountServiceAccountToken: false + containers: + - args: + - ai-gateway + - start + command: + - /opt/coder + env: + - name: CODER_AI_GATEWAY_HTTP_ADDRESS + value: 0.0.0.0:4001 + - name: CODER_PROMETHEUS_ENABLE + value: "true" + - name: CODER_PROMETHEUS_ADDRESS + value: 0.0.0.0:2112 + - name: CODER_URL + value: https://coder.example.com + - name: CODER_AI_GATEWAY_KEY + valueFrom: + secretKeyRef: + key: key + name: ai-gateway-key + image: ghcr.io/coder/coder:v2.36.0 + imagePullPolicy: IfNotPresent + lifecycle: {} + name: coder + ports: + - containerPort: 4001 + name: http + protocol: TCP + - containerPort: 2112 + name: metrics + protocol: TCP + readinessProbe: + httpGet: + path: /readyz + port: http + scheme: HTTP + initialDelaySeconds: 0 + resources: + requests: + cpu: "1" + memory: 1Gi + securityContext: + allowPrivilegeEscalation: false + runAsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + volumeMounts: [] + restartPolicy: Always + serviceAccountName: coder-ai-gateway + terminationGracePeriodSeconds: 330 + volumes: [] diff --git a/helm/ai-gateway/tests/testdata/default_values.yaml b/helm/ai-gateway/tests/testdata/default_values.yaml new file mode 100644 index 0000000000..5eb707ca0c --- /dev/null +++ b/helm/ai-gateway/tests/testdata/default_values.yaml @@ -0,0 +1,13 @@ +coder: + replicaCount: 3 + image: + tag: v2.36.0 + env: + - name: CODER_URL + value: https://coder.example.com + - name: CODER_AI_GATEWAY_KEY + valueFrom: + secretKeyRef: + name: ai-gateway-key + key: key +aigateway: {} diff --git a/helm/ai-gateway/tests/testdata/fails_httproute_without_parent_refs.yaml b/helm/ai-gateway/tests/testdata/fails_httproute_without_parent_refs.yaml new file mode 100644 index 0000000000..ece727de66 --- /dev/null +++ b/helm/ai-gateway/tests/testdata/fails_httproute_without_parent_refs.yaml @@ -0,0 +1,8 @@ +coder: + image: + tag: v2.36.0 +aigateway: + keySecret: + name: ai-gateway-key +httproute: + enable: true diff --git a/helm/ai-gateway/tests/testdata/fails_httproute_without_service.yaml b/helm/ai-gateway/tests/testdata/fails_httproute_without_service.yaml new file mode 100644 index 0000000000..ed5e0db2ac --- /dev/null +++ b/helm/ai-gateway/tests/testdata/fails_httproute_without_service.yaml @@ -0,0 +1,10 @@ +coder: + image: + tag: v2.36.0 +aigateway: + keySecret: + name: ai-gateway-key +service: + enable: false +httproute: + enable: true diff --git a/helm/ai-gateway/tests/testdata/fails_ingress_without_host.yaml b/helm/ai-gateway/tests/testdata/fails_ingress_without_host.yaml new file mode 100644 index 0000000000..313fc9ed7f --- /dev/null +++ b/helm/ai-gateway/tests/testdata/fails_ingress_without_host.yaml @@ -0,0 +1,8 @@ +coder: + image: + tag: v2.36.0 +aigateway: + keySecret: + name: key +ingress: + enable: true diff --git a/helm/ai-gateway/tests/testdata/fails_ingress_without_service.yaml b/helm/ai-gateway/tests/testdata/fails_ingress_without_service.yaml new file mode 100644 index 0000000000..621d715d5d --- /dev/null +++ b/helm/ai-gateway/tests/testdata/fails_ingress_without_service.yaml @@ -0,0 +1,11 @@ +coder: + image: + tag: v2.36.0 +aigateway: + keySecret: + name: key +service: + enable: false +ingress: + enable: true + host: ai.example.com diff --git a/helm/ai-gateway/tests/testdata/fails_missing_key_field.yaml b/helm/ai-gateway/tests/testdata/fails_missing_key_field.yaml new file mode 100644 index 0000000000..0775724d8c --- /dev/null +++ b/helm/ai-gateway/tests/testdata/fails_missing_key_field.yaml @@ -0,0 +1,7 @@ +coder: + image: + tag: v2.36.0 +aigateway: + keySecret: + name: ai-gateway-key + key: "" diff --git a/helm/ai-gateway/tests/testdata/fails_nodeport_with_clusterip.yaml b/helm/ai-gateway/tests/testdata/fails_nodeport_with_clusterip.yaml new file mode 100644 index 0000000000..5d06928561 --- /dev/null +++ b/helm/ai-gateway/tests/testdata/fails_nodeport_with_clusterip.yaml @@ -0,0 +1,8 @@ +coder: + image: + tag: v2.36.0 +aigateway: + keySecret: + name: ai-gateway-key +service: + nodePort: 30080 diff --git a/helm/ai-gateway/tests/testdata/fails_partial_ca_tls.yaml b/helm/ai-gateway/tests/testdata/fails_partial_ca_tls.yaml new file mode 100644 index 0000000000..491648fce9 --- /dev/null +++ b/helm/ai-gateway/tests/testdata/fails_partial_ca_tls.yaml @@ -0,0 +1,10 @@ +coder: + image: + tag: v2.36.0 +aigateway: + keySecret: + name: ai-gateway-key + coderTLS: + caSecret: + name: coder-ca + key: "" diff --git a/helm/ai-gateway/tests/testdata/fails_partial_client_tls.yaml b/helm/ai-gateway/tests/testdata/fails_partial_client_tls.yaml new file mode 100644 index 0000000000..278bd5514b --- /dev/null +++ b/helm/ai-gateway/tests/testdata/fails_partial_client_tls.yaml @@ -0,0 +1,10 @@ +coder: + image: + tag: v2.36.0 +aigateway: + keySecret: + name: key + coderTLS: + clientSecret: + name: client + certKey: "" diff --git a/helm/ai-gateway/tests/testdata/fails_partial_listener_tls.yaml b/helm/ai-gateway/tests/testdata/fails_partial_listener_tls.yaml new file mode 100644 index 0000000000..fa82ed4420 --- /dev/null +++ b/helm/ai-gateway/tests/testdata/fails_partial_listener_tls.yaml @@ -0,0 +1,9 @@ +coder: + image: + tag: v2.36.0 +aigateway: + keySecret: + name: key + listenerTLS: + name: listener + keyKey: "" diff --git a/helm/ai-gateway/tests/testdata/listener_tls_with_ingress.golden b/helm/ai-gateway/tests/testdata/listener_tls_with_ingress.golden new file mode 100644 index 0000000000..75418126d3 --- /dev/null +++ b/helm/ai-gateway/tests/testdata/listener_tls_with_ingress.golden @@ -0,0 +1,180 @@ +--- +# Source: coder-ai-gateway/templates/aigateway.yaml +apiVersion: v1 +automountServiceAccountToken: false +kind: ServiceAccount +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: ai-gateway + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder-ai-gateway + app.kubernetes.io/part-of: coder-ai-gateway + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-ai-gateway-0.1.0 + name: coder-ai-gateway + namespace: default +--- +# Source: coder-ai-gateway/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: coder-ai-gateway + namespace: default + labels: + helm.sh/chart: coder-ai-gateway-0.1.0 + app.kubernetes.io/name: coder-ai-gateway + app.kubernetes.io/instance: ai-gateway + app.kubernetes.io/part-of: coder-ai-gateway + app.kubernetes.io/version: "0.1.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + sessionAffinity: None + selector: + app.kubernetes.io/name: coder-ai-gateway + app.kubernetes.io/instance: ai-gateway + ports: + - name: http + port: 80 + targetPort: http + protocol: TCP +--- +# Source: coder-ai-gateway/templates/aigateway.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: ai-gateway + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder-ai-gateway + app.kubernetes.io/part-of: coder-ai-gateway + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-ai-gateway-0.1.0 + name: coder-ai-gateway + namespace: default +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: ai-gateway + app.kubernetes.io/name: coder-ai-gateway + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + type: RollingUpdate + template: + metadata: + annotations: + app.kubernetes.io/component: ai-gateway + labels: + app.kubernetes.io/instance: ai-gateway + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder-ai-gateway + app.kubernetes.io/part-of: coder-ai-gateway + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-ai-gateway-0.1.0 + spec: + automountServiceAccountToken: false + containers: + - args: + - ai-gateway + - start + command: + - /opt/coder + env: + - name: CODER_AI_GATEWAY_HTTP_ADDRESS + value: 0.0.0.0:4001 + - name: CODER_AI_GATEWAY_KEY_FILE + value: /etc/coder/ai-gateway-auth/key + - name: CODER_PROMETHEUS_ENABLE + value: "true" + - name: CODER_PROMETHEUS_ADDRESS + value: 0.0.0.0:2112 + - name: CODER_AI_GATEWAY_TLS_CERT_FILE + value: /etc/coder/ai-gateway-listener/tls.crt + - name: CODER_AI_GATEWAY_TLS_KEY_FILE + value: /etc/coder/ai-gateway-listener/tls.key + - name: CODER_URL + value: http://coder.default.svc.cluster.local:80 + image: ghcr.io/coder/coder:v2.36.0 + imagePullPolicy: IfNotPresent + lifecycle: {} + name: coder + ports: + - containerPort: 4001 + name: http + protocol: TCP + - containerPort: 2112 + name: metrics + protocol: TCP + readinessProbe: + httpGet: + path: /readyz + port: http + scheme: HTTPS + initialDelaySeconds: 0 + resources: + requests: + cpu: "1" + memory: 1Gi + securityContext: + allowPrivilegeEscalation: false + runAsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + volumeMounts: + - mountPath: /etc/coder/ai-gateway-auth + name: ai-gateway-auth + readOnly: true + - mountPath: /etc/coder/ai-gateway-listener + name: ai-gateway-listener + readOnly: true + restartPolicy: Always + serviceAccountName: coder-ai-gateway + terminationGracePeriodSeconds: 330 + volumes: + - name: ai-gateway-auth + secret: + items: + - key: key + path: key + secretName: ai-gateway-key + - name: ai-gateway-listener + secret: + items: + - key: tls.crt + path: tls.crt + - key: tls.key + path: tls.key + secretName: listener-tls +--- +# Source: coder-ai-gateway/templates/ingress.yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: coder-ai-gateway + namespace: default + labels: + helm.sh/chart: coder-ai-gateway-0.1.0 + app.kubernetes.io/name: coder-ai-gateway + app.kubernetes.io/instance: ai-gateway + app.kubernetes.io/part-of: coder-ai-gateway + app.kubernetes.io/version: "0.1.0" + app.kubernetes.io/managed-by: Helm +spec: + rules: + - host: "ai.example.com" + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: coder-ai-gateway + port: + number: 80 diff --git a/helm/ai-gateway/tests/testdata/listener_tls_with_ingress.yaml b/helm/ai-gateway/tests/testdata/listener_tls_with_ingress.yaml new file mode 100644 index 0000000000..84271c13d1 --- /dev/null +++ b/helm/ai-gateway/tests/testdata/listener_tls_with_ingress.yaml @@ -0,0 +1,14 @@ +coder: + image: + tag: v2.36.0 + env: + - name: CODER_URL + value: http://coder.default.svc.cluster.local:80 +aigateway: + keySecret: + name: ai-gateway-key + listenerTLS: + name: listener-tls +ingress: + enable: true + host: ai.example.com diff --git a/helm/ai-gateway/tests/testdata/networking.yaml b/helm/ai-gateway/tests/testdata/networking.yaml new file mode 100644 index 0000000000..337592ad6e --- /dev/null +++ b/helm/ai-gateway/tests/testdata/networking.yaml @@ -0,0 +1,33 @@ +coder: + image: + tag: v2.36.0 + env: + - name: CODER_URL + value: http://coder.ai-gateway-test.svc.cluster.local:80 +aigateway: + keySecret: + name: ai-gateway-key +service: + type: LoadBalancer + port: 443 + annotations: + service.example.com/internal: "true" + loadBalancerClass: example.com/lb +ingress: + enable: true + className: nginx + annotations: + nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" + host: ai.example.com + tls: + enable: true + secretName: ai-example-tls +httproute: + enable: true + parentRefs: + - name: shared-gateway + namespace: gateway-system + hostnames: + - ai.example.com + annotations: + example.com/route: ai diff --git a/helm/ai-gateway/tests/testdata/networking_ai-gateway-test.golden b/helm/ai-gateway/tests/testdata/networking_ai-gateway-test.golden new file mode 100644 index 0000000000..2374dbc040 --- /dev/null +++ b/helm/ai-gateway/tests/testdata/networking_ai-gateway-test.golden @@ -0,0 +1,206 @@ +--- +# Source: coder-ai-gateway/templates/aigateway.yaml +apiVersion: v1 +automountServiceAccountToken: false +kind: ServiceAccount +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: ai-gateway + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder-ai-gateway + app.kubernetes.io/part-of: coder-ai-gateway + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-ai-gateway-0.1.0 + name: coder-ai-gateway + namespace: ai-gateway-test +--- +# Source: coder-ai-gateway/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: coder-ai-gateway + namespace: ai-gateway-test + labels: + helm.sh/chart: coder-ai-gateway-0.1.0 + app.kubernetes.io/name: coder-ai-gateway + app.kubernetes.io/instance: ai-gateway + app.kubernetes.io/part-of: coder-ai-gateway + app.kubernetes.io/version: "0.1.0" + app.kubernetes.io/managed-by: Helm + annotations: + service.example.com/internal: "true" +spec: + type: LoadBalancer + sessionAffinity: None + externalTrafficPolicy: Cluster + loadBalancerClass: example.com/lb + selector: + app.kubernetes.io/name: coder-ai-gateway + app.kubernetes.io/instance: ai-gateway + ports: + - name: http + port: 443 + targetPort: http + protocol: TCP +--- +# Source: coder-ai-gateway/templates/aigateway.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: ai-gateway + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder-ai-gateway + app.kubernetes.io/part-of: coder-ai-gateway + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-ai-gateway-0.1.0 + name: coder-ai-gateway + namespace: ai-gateway-test +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: ai-gateway + app.kubernetes.io/name: coder-ai-gateway + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + type: RollingUpdate + template: + metadata: + annotations: + app.kubernetes.io/component: ai-gateway + labels: + app.kubernetes.io/instance: ai-gateway + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder-ai-gateway + app.kubernetes.io/part-of: coder-ai-gateway + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-ai-gateway-0.1.0 + spec: + automountServiceAccountToken: false + containers: + - args: + - ai-gateway + - start + command: + - /opt/coder + env: + - name: CODER_AI_GATEWAY_HTTP_ADDRESS + value: 0.0.0.0:4001 + - name: CODER_AI_GATEWAY_KEY_FILE + value: /etc/coder/ai-gateway-auth/key + - name: CODER_PROMETHEUS_ENABLE + value: "true" + - name: CODER_PROMETHEUS_ADDRESS + value: 0.0.0.0:2112 + - name: CODER_URL + value: http://coder.ai-gateway-test.svc.cluster.local:80 + image: ghcr.io/coder/coder:v2.36.0 + imagePullPolicy: IfNotPresent + lifecycle: {} + name: coder + ports: + - containerPort: 4001 + name: http + protocol: TCP + - containerPort: 2112 + name: metrics + protocol: TCP + readinessProbe: + httpGet: + path: /readyz + port: http + scheme: HTTP + initialDelaySeconds: 0 + resources: + requests: + cpu: "1" + memory: 1Gi + securityContext: + allowPrivilegeEscalation: false + runAsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + volumeMounts: + - mountPath: /etc/coder/ai-gateway-auth + name: ai-gateway-auth + readOnly: true + restartPolicy: Always + serviceAccountName: coder-ai-gateway + terminationGracePeriodSeconds: 330 + volumes: + - name: ai-gateway-auth + secret: + items: + - key: key + path: key + secretName: ai-gateway-key +--- +# Source: coder-ai-gateway/templates/ingress.yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: coder-ai-gateway + namespace: ai-gateway-test + labels: + helm.sh/chart: coder-ai-gateway-0.1.0 + app.kubernetes.io/name: coder-ai-gateway + app.kubernetes.io/instance: ai-gateway + app.kubernetes.io/part-of: coder-ai-gateway + app.kubernetes.io/version: "0.1.0" + app.kubernetes.io/managed-by: Helm + annotations: + nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" +spec: + ingressClassName: nginx + tls: + - hosts: + - "ai.example.com" + secretName: ai-example-tls + rules: + - host: "ai.example.com" + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: coder-ai-gateway + port: + number: 443 +--- +# Source: coder-ai-gateway/templates/httproute.yaml +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: coder-ai-gateway + namespace: ai-gateway-test + labels: + helm.sh/chart: coder-ai-gateway-0.1.0 + app.kubernetes.io/name: coder-ai-gateway + app.kubernetes.io/instance: ai-gateway + app.kubernetes.io/part-of: coder-ai-gateway + app.kubernetes.io/version: "0.1.0" + app.kubernetes.io/managed-by: Helm + annotations: + example.com/route: ai +spec: + parentRefs: + - name: shared-gateway + namespace: gateway-system + hostnames: + - ai.example.com + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: coder-ai-gateway + port: 443 diff --git a/helm/ai-gateway/tests/testdata/nodeport.golden b/helm/ai-gateway/tests/testdata/nodeport.golden new file mode 100644 index 0000000000..423e3b1d21 --- /dev/null +++ b/helm/ai-gateway/tests/testdata/nodeport.golden @@ -0,0 +1,141 @@ +--- +# Source: coder-ai-gateway/templates/aigateway.yaml +apiVersion: v1 +automountServiceAccountToken: false +kind: ServiceAccount +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: ai-gateway + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder-ai-gateway + app.kubernetes.io/part-of: coder-ai-gateway + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-ai-gateway-0.1.0 + name: coder-ai-gateway + namespace: default +--- +# Source: coder-ai-gateway/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: coder-ai-gateway + namespace: default + labels: + helm.sh/chart: coder-ai-gateway-0.1.0 + app.kubernetes.io/name: coder-ai-gateway + app.kubernetes.io/instance: ai-gateway + app.kubernetes.io/part-of: coder-ai-gateway + app.kubernetes.io/version: "0.1.0" + app.kubernetes.io/managed-by: Helm +spec: + type: NodePort + sessionAffinity: ClientIP + externalTrafficPolicy: Local + selector: + app.kubernetes.io/name: coder-ai-gateway + app.kubernetes.io/instance: ai-gateway + ports: + - name: http + port: 80 + targetPort: http + protocol: TCP + nodePort: 30401 +--- +# Source: coder-ai-gateway/templates/aigateway.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: {} + labels: + app.kubernetes.io/instance: ai-gateway + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder-ai-gateway + app.kubernetes.io/part-of: coder-ai-gateway + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-ai-gateway-0.1.0 + name: coder-ai-gateway + namespace: default +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: ai-gateway + app.kubernetes.io/name: coder-ai-gateway + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + type: RollingUpdate + template: + metadata: + annotations: + app.kubernetes.io/component: ai-gateway + labels: + app.kubernetes.io/instance: ai-gateway + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: coder-ai-gateway + app.kubernetes.io/part-of: coder-ai-gateway + app.kubernetes.io/version: 0.1.0 + helm.sh/chart: coder-ai-gateway-0.1.0 + spec: + automountServiceAccountToken: false + containers: + - args: + - ai-gateway + - start + command: + - /opt/coder + env: + - name: CODER_AI_GATEWAY_HTTP_ADDRESS + value: 0.0.0.0:4001 + - name: CODER_AI_GATEWAY_KEY_FILE + value: /etc/coder/ai-gateway-auth/key + - name: CODER_PROMETHEUS_ENABLE + value: "true" + - name: CODER_PROMETHEUS_ADDRESS + value: 0.0.0.0:2112 + - name: CODER_URL + value: http://coder.default.svc.cluster.local:80 + image: ghcr.io/coder/coder:v2.36.0 + imagePullPolicy: IfNotPresent + lifecycle: {} + name: coder + ports: + - containerPort: 4001 + name: http + protocol: TCP + - containerPort: 2112 + name: metrics + protocol: TCP + readinessProbe: + httpGet: + path: /readyz + port: http + scheme: HTTP + initialDelaySeconds: 0 + resources: + requests: + cpu: "1" + memory: 1Gi + securityContext: + allowPrivilegeEscalation: false + runAsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + volumeMounts: + - mountPath: /etc/coder/ai-gateway-auth + name: ai-gateway-auth + readOnly: true + restartPolicy: Always + serviceAccountName: coder-ai-gateway + terminationGracePeriodSeconds: 330 + volumes: + - name: ai-gateway-auth + secret: + items: + - key: key + path: key + secretName: ai-gateway-key diff --git a/helm/ai-gateway/tests/testdata/nodeport.yaml b/helm/ai-gateway/tests/testdata/nodeport.yaml new file mode 100644 index 0000000000..0bd2daf7c2 --- /dev/null +++ b/helm/ai-gateway/tests/testdata/nodeport.yaml @@ -0,0 +1,14 @@ +coder: + image: + tag: v2.36.0 + env: + - name: CODER_URL + value: http://coder.default.svc.cluster.local:80 +aigateway: + keySecret: + name: ai-gateway-key +service: + type: NodePort + nodePort: 30401 + externalTrafficPolicy: Local + sessionAffinity: ClientIP diff --git a/helm/ai-gateway/values.schema.json b/helm/ai-gateway/values.schema.json new file mode 100644 index 0000000000..bb41786cfa --- /dev/null +++ b/helm/ai-gateway/values.schema.json @@ -0,0 +1,143 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "properties": { + "libcoder": {"type": "object"}, + "nameOverride": {"type": "string"}, + "coder": { + "type": "object", "additionalProperties": false, + "required": ["env", "envFrom", "image", "initContainers", "annotations", "labels", "podAnnotations", "podLabels", "serviceAccount", "securityContext", "podSecurityContext", "volumes", "volumeMounts", "replicaCount", "lifecycle", "resources", "startupProbe", "livenessProbe", "readinessProbe", "certs", "affinity", "tolerations", "nodeSelector", "topologySpreadConstraints", "priorityClassName", "command"], + "properties": { + "env": {"type": "array", "items": {"type": "object", "required": ["name"]}}, + "envFrom": {"type": "array", "items": {"type": "object"}}, + "image": { + "type": "object", "additionalProperties": false, + "required": ["repo", "tag", "pullPolicy", "pullSecrets"], + "properties": { + "repo": {"type": "string", "minLength": 1}, + "tag": {"type": "string"}, + "pullPolicy": {"enum": ["Always", "IfNotPresent", "Never"]}, + "pullSecrets": {"type": "array", "items": {"type": "object"}} + } + }, + "initContainers": {"type": "array"}, + "annotations": {"type": "object"}, + "labels": {"type": "object"}, + "podAnnotations": {"type": "object"}, + "podLabels": {"type": "object"}, + "serviceAccount": { + "type": "object", "additionalProperties": false, + "required": ["annotations", "name", "disableCreate"], + "properties": { + "annotations": {"type": "object"}, + "name": {"type": "string", "minLength": 1}, + "disableCreate": {"type": "boolean"}, + "labels": {"type": "object"} + } + }, + "securityContext": {"type": "object"}, + "podSecurityContext": {"type": "object"}, + "volumes": {"type": "array"}, + "volumeMounts": {"type": "array"}, + "replicaCount": {"type": "integer", "minimum": 0}, + "lifecycle": {"type": "object"}, + "resources": {"type": "object"}, + "startupProbe": {"$ref": "#/definitions/healthProbe"}, + "livenessProbe": {"$ref": "#/definitions/healthProbe"}, + "readinessProbe": {"$ref": "#/definitions/probe"}, + "certs": { + "type": "object", "additionalProperties": false, "required": ["secrets"], + "properties": { + "secrets": {"type": "array", "items": {"type": "object", "required": ["name", "key"]}} + } + }, + "affinity": {"type": "object"}, + "tolerations": {"type": "array"}, + "nodeSelector": {"type": "object"}, + "topologySpreadConstraints": {"type": "array"}, + "priorityClassName": {"type": "string"}, + "command": {"type": "array", "minItems": 1, "items": {"type": "string"}} + } + }, + "aigateway": { + "type": "object", "additionalProperties": false, + "required": ["keySecret", "coderTLS", "listenerTLS", "terminationGracePeriodSeconds"], + "properties": { + "keySecret": { + "type": "object", "additionalProperties": false, + "required": ["name", "key"], + "properties": {"name": {"type": "string"}, "key": {"type": "string"}} + }, + "coderTLS": { + "type": "object", "additionalProperties": false, + "required": ["caSecret", "clientSecret"], + "properties": { + "caSecret": { + "type": "object", "additionalProperties": false, "required": ["name", "key"], + "properties": {"name": {"type": "string"}, "key": {"type": "string"}} + }, + "clientSecret": { + "type": "object", "additionalProperties": false, "required": ["name", "certKey", "keyKey"], + "properties": {"name": {"type": "string"}, "certKey": {"type": "string"}, "keyKey": {"type": "string"}} + } + } + }, + "listenerTLS": { + "type": "object", "additionalProperties": false, "required": ["name", "certKey", "keyKey"], + "properties": {"name": {"type": "string"}, "certKey": {"type": "string"}, "keyKey": {"type": "string"}} + }, + "terminationGracePeriodSeconds": {"type": "integer", "minimum": 0} + } + }, + "service": { + "type": "object", "additionalProperties": false, + "required": ["enable", "type", "port", "annotations", "sessionAffinity", "externalTrafficPolicy", "loadBalancerClass", "loadBalancerIP", "nodePort"], + "properties": { + "enable": {"type": "boolean"}, + "type": {"enum": ["ClusterIP", "NodePort", "LoadBalancer"]}, + "port": {"type": "integer", "minimum": 1, "maximum": 65535}, + "annotations": {"type": "object"}, + "sessionAffinity": {"enum": ["None", "ClientIP"]}, + "externalTrafficPolicy": {"enum": ["Cluster", "Local"]}, + "loadBalancerClass": {"type": "string"}, + "loadBalancerIP": {"type": "string"}, + "nodePort": {"type": ["integer", "null"], "minimum": 30000, "maximum": 32767} + } + }, + "ingress": { + "type": "object", "additionalProperties": false, + "required": ["enable", "className", "annotations", "host", "path", "pathType", "tls"], + "properties": { + "enable": {"type": "boolean"}, "className": {"type": "string"}, "annotations": {"type": "object"}, + "host": {"type": "string"}, "path": {"type": "string"}, "pathType": {"enum": ["Exact", "Prefix", "ImplementationSpecific"]}, + "tls": {"type": "object", "additionalProperties": false, "required": ["enable", "secretName"], "properties": {"enable": {"type": "boolean"}, "secretName": {"type": "string"}}} + } + }, + "httproute": { + "type": "object", "additionalProperties": false, "required": ["enable", "parentRefs", "hostnames", "annotations"], + "properties": {"enable": {"type": "boolean"}, "parentRefs": {"type": "array"}, "hostnames": {"type": "array", "items": {"type": "string"}}, "annotations": {"type": "object"}} + }, + "extraTemplates": {"type": "array", "items": {"oneOf": [{"type": "string"}, {"type": "object"}]}} + }, + "required": ["coder", "aigateway", "service", "ingress", "httproute", "extraTemplates"], + "definitions": { + "healthProbe": { + "allOf": [ + {"$ref": "#/definitions/probe"}, + {"properties": {"successThreshold": {"type": "integer", "const": 1}}} + ] + }, + "probe": { + "type": "object", "additionalProperties": false, "required": ["enabled", "initialDelaySeconds"], + "properties": { + "enabled": {"type": "boolean"}, + "initialDelaySeconds": {"type": "integer", "minimum": 0}, + "periodSeconds": {"type": "integer", "minimum": 1}, + "timeoutSeconds": {"type": "integer", "minimum": 1}, + "successThreshold": {"type": "integer", "minimum": 1}, + "failureThreshold": {"type": "integer", "minimum": 1} + } + } + } +} diff --git a/helm/ai-gateway/values.yaml b/helm/ai-gateway/values.yaml new file mode 100644 index 0000000000..a3232d4566 --- /dev/null +++ b/helm/ai-gateway/values.yaml @@ -0,0 +1,228 @@ +# coder -- Common workload configuration options. +coder: + # coder.env -- Additional environment variables for the AI Gateway + # container. These are rendered after chart-configured variables, including + # when a variable name is duplicated. + env: [] + # - name: "CODER_VERBOSE" + # value: "true" + + # coder.envFrom -- Additional envFrom sources for the AI Gateway container. + envFrom: [] + + # coder.image -- The image to use for the AI Gateway. + image: + # coder.image.repo -- The repository of the image. + repo: "ghcr.io/coder/coder" + # coder.image.tag -- The tag of the image, defaults to {{.Chart.AppVersion}} + # if not set. If you're using the chart directly from git, the default + # app version will not work and you'll need to set this value. The helm + # chart helpfully fails quickly in this case. + tag: "" + # coder.image.pullPolicy -- The pull policy to use for the image. See: + # https://kubernetes.io/docs/concepts/containers/images/#image-pull-policy + pullPolicy: IfNotPresent + # coder.image.pullSecrets -- The secrets used for pulling the Coder image + # from a private registry. + pullSecrets: [] + # - name: "pull-secret" + + # coder.initContainers -- Init containers for the deployment. See: + # https://kubernetes.io/docs/concepts/workloads/pods/init-containers/ + initContainers: + [] + # - name: init-container + # image: busybox:1.28 + # command: ['sh', '-c', "sleep 2"] + + # coder.annotations -- The Deployment annotations. See: + # https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ + annotations: {} + + # coder.labels -- The Deployment labels. See: + # https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ + labels: {} + + # coder.podAnnotations -- The pod annotations. See: + # https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ + # The annotation `app.kubernetes.io/component: ai-gateway` is added + # automatically. + podAnnotations: {} + + # coder.podLabels -- The pod labels. See: + # https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ + podLabels: {} + + # coder.serviceAccount -- Configuration for the automatically created + # service account. + serviceAccount: + # coder.serviceAccount.annotations -- The service account annotations. + annotations: {} + # coder.serviceAccount.name -- The service account name. + name: coder-ai-gateway + # coder.serviceAccount.disableCreate -- Whether to create the service + # account or use an existing service account. + disableCreate: false + + # coder.securityContext -- Fields related to the container's security + # context (as opposed to the pod). Some fields are also present in the pod + # security context, in which case these values will take precedence. + securityContext: + # coder.securityContext.runAsNonRoot -- Requires that the container + # runs as an unprivileged user. + runAsNonRoot: true + # coder.securityContext.runAsUser -- Sets the user id of the container. + runAsUser: 1000 + # coder.securityContext.runAsGroup -- Sets the group id of the container. + runAsGroup: 1000 + # coder.securityContext.seccompProfile -- Sets the seccomp profile for + # the container. + seccompProfile: + type: RuntimeDefault + # coder.securityContext.allowPrivilegeEscalation -- Controls whether + # the container can gain additional privileges. + allowPrivilegeEscalation: false + + # coder.podSecurityContext -- Pod-level security context settings. See: + # https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ + podSecurityContext: {} + + # coder.volumes -- A list of extra volumes to add to the AI Gateway pod. + volumes: [] + # - name: "my-volume" + # emptyDir: {} + + # coder.volumeMounts -- A list of extra volume mounts to add to the AI + # Gateway container. + volumeMounts: [] + # - name: "my-volume" + # mountPath: "/mnt/my-volume" + + # coder.replicaCount -- The number of AI Gateway pods. + replicaCount: 1 + + # coder.lifecycle -- Container lifecycle handlers, allowing for lifecycle + # events such as postStart and preStop. See: + # https://kubernetes.io/docs/tasks/configure-pod-container/attach-handler-lifecycle-event/ + lifecycle: + {} + # postStart: + # exec: + # command: ["/bin/sh", "-c", "echo postStart"] + + # coder.resources -- The resources to request for the AI Gateway container. + resources: + requests: + cpu: "1" + memory: 1Gi + + # coder.startupProbe -- Startup probe configuration for the AI Gateway. + # Enable this with a failure threshold long enough for initial provider load + # before enabling the liveness probe. + startupProbe: + enabled: false + initialDelaySeconds: 0 + + # coder.livenessProbe -- Liveness probe configuration for the AI Gateway. + # Without this probe, Kubernetes does not restart a running but unresponsive + # Gateway. Enable and tune the startup probe before enabling this probe. + livenessProbe: + enabled: false + initialDelaySeconds: 0 + + # coder.readinessProbe -- Readiness probe configuration for the AI Gateway. + readinessProbe: + enabled: true + initialDelaySeconds: 0 + + # coder.certs -- CA bundles to mount inside the AI Gateway pod. + certs: + # coder.certs.secrets -- A list of CA bundle secrets to mount into the + # pod. The secrets should exist in the same namespace as the Helm + # deployment. The given key in each secret is mounted at + # `/etc/ssl/certs/{secret_name}.crt`. + secrets: + [] + # - name: "my-ca-bundle" + # key: "ca-bundle.crt" + + # coder.affinity -- Allows specifying an affinity rule for the deployment. + affinity: {} + + # coder.tolerations -- Tolerations for tainted nodes. See: + # https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ + tolerations: [] + + # coder.nodeSelector -- Node labels for constraining pods to nodes. See: + # https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector + nodeSelector: {} + + # coder.topologySpreadConstraints -- Topology spread constraints for the + # pods. See: + # https://kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints/ + topologySpreadConstraints: [] + + # coder.priorityClassName -- The priority class name for the pods. + priorityClassName: "" + + # coder.command -- The command to use when running the container. Used + # for customizing the location of the `coder` binary in your image. + command: + - /opt/coder + +# Authentication and listener settings for the standalone gateway. +aigateway: + # Optional existing Secret containing an AI Gateway key. When unset, provide + # CODER_AI_GATEWAY_KEY or CODER_AI_GATEWAY_KEY_FILE through coder.env. + keySecret: + name: "" + key: key + coderTLS: + caSecret: + name: "" + key: ca.crt + clientSecret: + name: "" + certKey: tls.crt + keyKey: tls.key + # The certificate must cover every hostname clients use. + listenerTLS: + name: "" + certKey: tls.crt + keyKey: tls.key + # This must exceed the application's 300-second shutdown timeout. + terminationGracePeriodSeconds: 330 + +# Stable data-plane Service fronting the container's port 4001 listener. +service: + enable: true + type: ClusterIP + port: 80 + annotations: {} + sessionAffinity: None + externalTrafficPolicy: Cluster + loadBalancerClass: "" + loadBalancerIP: "" + nodePort: null + +# Optional networking.k8s.io/v1 Ingress. +ingress: + enable: false + className: "" + annotations: {} + host: "" + path: / + pathType: Prefix + tls: + enable: false + secretName: "" + +# Optional Gateway API HTTPRoute. Requires the v1 HTTPRoute CRD. +httproute: + enable: false + parentRefs: [] + hostnames: [] + annotations: {} + +# Additional templated Kubernetes manifests. +extraTemplates: [] diff --git a/scripts/helm.sh b/scripts/helm.sh index fa3283b1ac..42f1563162 100755 --- a/scripts/helm.sh +++ b/scripts/helm.sh @@ -4,7 +4,7 @@ # .tgz file at the specified path, and may optionally push it to the Coder OSS # repo. # -# ./helm.sh [--version 1.2.3] [--chart coder|provisioner] [--output path/to/coder.tgz] +# ./helm.sh [--version 1.2.3] [--chart coder|provisioner|ai-gateway] [--output path/to/coder.tgz] # # If no version is specified, defaults to the version from ./version.sh. # @@ -56,8 +56,8 @@ fi if [[ "$chart" == "" ]]; then chart="coder" fi -if ! [[ "$chart" =~ ^(coder|provisioner)$ ]]; then - error "--chart value must be one of (coder, provisioner)" +if ! [[ "$chart" =~ ^(coder|provisioner|ai-gateway)$ ]]; then + error "--chart value must be one of (coder, provisioner, ai-gateway)" fi if [[ "$output_path" == "" ]]; then