From 034d331c17cb9e2c793eb2692d9572fa78a5b84b Mon Sep 17 00:00:00 2001 From: Jarvis Date: Wed, 16 Sep 2026 11:33:38 +0000 Subject: [PATCH] feat(aisix): standalone mode, and drop the pinned uid from podSecurityContext MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two changes to the data-plane chart. The pod security context no longer pins `runAsUser: 10001` / `fsGroup: 10001`. Those were there because the image declared its user by name, which the kubelet cannot verify against `runAsNonRoot`; the image now declares it numerically, so `runAsNonRoot` alone is enough. Nothing in the chart depended on the numbers — the only writable path is an emptyDir — and a fixed uid/gid can be pinned back by setting the two keys again. `controlPlane.enabled` (default true, so existing installs render byte-for-byte as before) selects between the two ways the gateway can be run. With false it runs standalone, as the open-source gateway: no control plane, no certificate bundle, no `AISIX_MANAGED__*` environment. The chart renders a startup config into a ConfigMap pointing `resources_file` at a mounted `resources.yaml` and leaving the admin listener unbound, and mounts both under their own directories so neither shadows the image's own `/etc/aisix/config.managed.yaml`. Resources come from exactly one of `standalone.resources` (inline, rendered into a Secret rather than a ConfigMap because provider keys are credentials), `standalone.existingSecret`, or `standalone.existingConfigMap`. Everything the chart already sets through environment variables — proxy address, metrics address, the rate-limit backend, `extraEnvVars` — stays that way in both modes, so the rendered config carries only what env cannot express. The gateway re-reads the resources file on SIGHUP only, and the chart does not send one. A checksum of the rendered config and inline resources rides on the pod template, so `helm upgrade` rolls the pods when either changes; an out-of-band edit to an existing Secret or ConfigMap needs a rollout restart, which the values comments, the README and NOTES all say. CI gains `charts/aisix/ci/standalone-values.yaml`, which `ct lint` picks up alongside the existing default values, plus a kind install step that is the chart's first real install test. It installs against `ghcr.io/api7/aisix:dev` because the released image for the current appVersion still declares a named USER and so cannot start without a pinned uid. --- .github/workflows/ci.yaml | 24 +++++ charts/aisix/README.md | 139 +++++++++++++++++++++++-- charts/aisix/README.md.gotmpl | 133 +++++++++++++++++++++-- charts/aisix/ci/standalone-values.yaml | 46 ++++++++ charts/aisix/templates/NOTES.txt | 18 ++++ charts/aisix/templates/_helpers.tpl | 36 ++++++- charts/aisix/templates/configmap.yaml | 21 ++++ charts/aisix/templates/deployment.yaml | 43 +++++++- charts/aisix/templates/secret.yaml | 17 ++- charts/aisix/values.yaml | 45 ++++++-- 10 files changed, 495 insertions(+), 27 deletions(-) create mode 100644 charts/aisix/ci/standalone-values.yaml create mode 100644 charts/aisix/templates/configmap.yaml diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index dd27a7d2..2c688d0b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -93,6 +93,30 @@ jobs: && ct install \ --charts charts/gateway --helm-extra-set-args "--set etcd.enabled=true --set apisix.extraEnvVars[0].name=API7_SKIP_FIRST_HEARTBEAT_DEBUG --set-string apisix.extraEnvVars[0].value=true"' + - name: Test aisix chart in standalone mode + run: | + kubectl cluster-info + docker run --rm --interactive --network host \ + --name ct-aisix-standalone \ + --volume $HOME/.kube/config:/root/.kube/config \ + --volume $PWD:/workdir \ + --workdir /workdir \ + quay.io/helmpack/chart-testing:v3.10.1 sh -c ' + set -e + ns=aisix-standalone + kubectl create namespace "$ns" + if ! helm install aisix charts/aisix --namespace "$ns" \ + --values charts/aisix/ci/standalone-values.yaml \ + --wait --timeout 5m; then + kubectl -n "$ns" get pods -o wide || true + kubectl -n "$ns" describe pods || true + kubectl -n "$ns" logs -l app.kubernetes.io/name=aisix --tail=200 || true + exit 1 + fi + helm uninstall aisix --namespace "$ns" + kubectl delete namespace "$ns" + ' + - name: Setup Go uses: actions/setup-go@v5 with: diff --git a/charts/aisix/README.md b/charts/aisix/README.md index 5eba0ffc..ddf72ebb 100644 --- a/charts/aisix/README.md +++ b/charts/aisix/README.md @@ -8,13 +8,20 @@ AISIX is an AI gateway: it fronts LLM providers with routing, rate limiting, bud caching, guardrails, and observability behind an OpenAI-compatible API. This chart installs the **data plane** — the component that serves live AI traffic. -The data plane is configured by the AISIX control plane, not by this chart. It -connects out to the control plane's data-plane manager over mutual TLS, using a -gateway certificate bundle issued from the console, and receives its models, API -keys, and policies from there. Install the control plane first — with the -[`aisix-cp`](../aisix-cp/README.md) chart, or any of the other options in the +The chart installs it in either of two modes, chosen with `controlPlane.enabled`. + +By default the gateway is configured by the AISIX control plane, not by this +chart. It connects out to the control plane's data-plane manager over mutual TLS, +using a gateway certificate bundle issued from the console, and receives its +models, API keys, and policies from there. Install the control plane first — with +the [`aisix-cp`](../aisix-cp/README.md) chart, or any of the other options in the [on-premises installation guide](https://docs.api7.ai/ai-gateway/on-premises/deployment). +With `controlPlane.enabled: false` the gateway runs standalone, as the +open-source AI gateway with no control plane at all: every resource comes from +one declarative `resources.yaml` you supply through the chart. See +[Standalone mode](#standalone-mode-no-control-plane) below. + **Homepage:** ## Maintainers @@ -31,8 +38,10 @@ keys, and policies from there. Install the control plane first — with the * Kubernetes v1.23+ * Helm v3+ -* An AISIX control plane, reachable from the cluster -* A gateway certificate bundle for the environment this gateway should serve +* For the default mode: an AISIX control plane reachable from the cluster, and a + gateway certificate bundle for the environment this gateway should serve +* For standalone mode: a `resources.yaml` declaring the provider keys, models and + caller API keys the gateway should serve ## Install @@ -71,6 +80,116 @@ certificate. helm delete aisix --namespace aisix ``` +## Standalone mode (no control plane) + +Set `controlPlane.enabled: false` to run the open-source gateway on its own. +Nothing under `controlPlane` is read, no certificate bundle is needed, and the +gateway reads every resource — provider keys, models, caller API keys, +guardrails, MCP servers, rate-limit policies — from one `resources.yaml`. The +chart renders a startup configuration pointing at it, mounts it read-only at +`/etc/aisix/resources/resources.yaml`, and leaves the admin API unbound, so the +file is the only way resources are declared. + +Supply the file through exactly one of `standalone.resources`, +`standalone.existingSecret`, or `standalone.existingConfigMap`; setting none or +more than one fails the render. + +`standalone.resources` takes the file inline, as a map, and renders it into a +chart-managed Secret — a Secret rather than a ConfigMap because provider keys are +credentials. Credentials do not have to live in your values file even so: a +`${VAR}` reference is resolved from the container's environment when the file +loads, so the value itself can come from `extraEnvVars` or from a Secret you +manage separately. + +```yaml +controlPlane: + enabled: false + +standalone: + resources: + _format_version: "1" + provider_keys: + - display_name: openai-main + provider: openai + adapter: openai + api_key: ${OPENAI_API_KEY} + api_base: https://api.openai.com/v1 + models: + - display_name: gpt-4o-mini + provider: openai + model_name: gpt-4o-mini + provider_key: openai-main + api_keys: + - display_name: my-caller + key_env: CALLER_API_KEY + allowed_models: + - gpt-4o-mini + +extraEnvVars: + - name: OPENAI_API_KEY + valueFrom: + secretKeyRef: + name: openai-credentials + key: api-key + - name: CALLER_API_KEY + valueFrom: + secretKeyRef: + name: aisix-caller-keys + key: my-caller +``` + +`standalone.existingSecret` and `standalone.existingConfigMap` read the file from +an object you already manage, under the key `resources.yaml`: + +```sh +kubectl -n aisix create secret generic aisix-resources \ + --from-file=resources.yaml=./resources.yaml +``` + +```yaml +controlPlane: + enabled: false + +standalone: + existingSecret: aisix-resources +``` + +Validate a file before you install it, without starting a listener: + +```sh +docker run --rm -v "$(pwd):/work:ro" \ + --entrypoint /usr/local/bin/aisix api7/aisix: \ + validate --resources /work/resources.yaml +``` + +The file's own schema — every resource kind and field — is documented in the +[open-source gateway quickstart](https://docs.api7.ai/ai-gateway/getting-started/gateway-quickstart) +and the reference pages it links. + +### Applying a change + +The gateway re-reads `resources.yaml` on `SIGHUP` only, and this chart never +sends one, so a rollout is what applies a change. + +Editing `standalone.resources` and running `helm upgrade` does that on its own: +the pod template carries a checksum of the rendered file, so the change rolls the +pods. Editing the Secret or ConfigMap behind `standalone.existingSecret` / +`standalone.existingConfigMap` does not — Kubernetes updates the mounted file in +place and nothing tells the gateway. Apply it with: + +```sh +kubectl rollout restart deploy/-aisix -n +``` + +### What is not available + +Standalone mode has no control plane, so there is no console, no usage or budget +reporting, and no per-environment configuration distribution. The admin API is +left unbound as well: it is read-only against a file source, and binding it would +require admin keys the chart does not manage. Turn it on with +`extraEnvVars` — `AISIX_ADMIN__ENABLED`, `AISIX_ADMIN__ADDR` and +`AISIX_ADMIN__ADMIN_KEYS` — if you want it. + ## Termination and draining `terminationGracePeriodSeconds` defaults to 1230 seconds, far above the @@ -271,6 +390,7 @@ extraEnvVars: | controlPlane.certificate.existingSecret | string | `""` | Read the bundle from an existing Secret instead of the PEM values below. Recommended: it keeps the private key out of your values file | | controlPlane.certificate.key | string | `""` | Private key PEM. Used only when `existingSecret` is empty | | controlPlane.certificate.keyKey | string | `"key.pem"` | Secret key holding the private key PEM | +| controlPlane.enabled | bool | `true` | Read configuration from an AISIX control plane. Set to false to run standalone, from the `resources.yaml` file configured under `standalone` | | controlPlane.etcdEndpoint | string | `""` | Control-plane etcd endpoint as bare `host:port`. Leave empty unless the control plane publishes an etcd endpoint distinct from `baseURL` | | controlPlane.heartbeatIntervalSeconds | int | `15` | Heartbeat interval in seconds. The control plane marks a gateway connected on its first heartbeat. Clamped to [5, 300] by the gateway | | extraEnvVars | list | `[]` | Extra environment variables for the gateway container. Every gateway configuration field is reachable as `AISIX_
__` | @@ -312,9 +432,7 @@ extraEnvVars: | podDisruptionBudget.maxUnavailable | int | `1` | Maximum unavailable pods | | podDisruptionBudget.minAvailable | string | `""` | Minimum available pods. Takes precedence over `maxUnavailable` | | podLabels | object | `{}` | Labels for the gateway pods | -| podSecurityContext.fsGroup | int | `10001` | | | podSecurityContext.runAsNonRoot | bool | `true` | | -| podSecurityContext.runAsUser | int | `10001` | | | podSecurityContext.seccompProfile.type | string | `"RuntimeDefault"` | | | preStopSleepSeconds | int | `30` | Seconds to sleep in a `preStop` hook before the gateway receives SIGTERM. Endpoint removal and SIGTERM are concurrent, so without this pause a terminating pod can still be handed new connections by a kube-proxy that has not caught up. Set to 0 to drop the hook. This covers balancers that learn about the pod from the Kubernetes API. One that polls a health check instead learns nothing here — the pod is still fully ready throughout the sleep — and is covered by the gateway's own drain window (`shutdown.min_drain_secs`, 30s by default), which starts at SIGTERM with `/readyz` already answering 503. | | priorityClassName | string | `""` | Pod priority class | @@ -341,6 +459,9 @@ extraEnvVars: | serviceAccount.annotations | object | `{}` | ServiceAccount annotations | | serviceAccount.create | bool | `true` | Create a ServiceAccount for the gateway | | serviceAccount.name | string | `""` | ServiceAccount name. Defaults to the release fullname | +| standalone.existingConfigMap | string | `""` | Read `resources.yaml` from an existing ConfigMap instead, under key `resources.yaml`. Use only when every credential in it is a `${VAR}` reference resolved from `extraEnvVars` | +| standalone.existingSecret | string | `""` | Read `resources.yaml` from an existing Secret instead, under key `resources.yaml`. Recommended when the file carries literal credentials | +| standalone.resources | object | `{}` | Inline `resources.yaml` content, as a map. Rendered into a chart-managed Secret, because provider keys are credentials. Values may reference environment variables as `${VAR}` — supply them through `extraEnvVars` — so the credential itself need not live in this file | | startupProbe.enabled | bool | `true` | Gate liveness and readiness until the proxy listener is bound. In etcd mode that happens only after the gateway's first configuration apply succeeds, so the budget here (period x threshold) has to cover reaching the configuration source and applying what it holds — not merely starting the process. How long that apply takes scales with how much configuration the environment holds, so the 300s default (2s x 150) is deliberately generous headroom for a large one rather than a bound tuned to a measured boot. The period stays short so an ordinary boot still passes within a couple of seconds and rollouts are not slowed by the headroom; only the pathological case waits. The budget is also wide enough to contain the gateway's own retry schedule. It keeps retrying the configuration read on an exponential backoff — capped at a minute between attempts — for as long as it is up, and binds the moment one attempt succeeds. A configuration source that comes back inside the budget is therefore retried while the budget still has room, and the instance binds on its own, with no restart. A budget much shorter than the backoff's cap truncates that schedule instead, and kills the container in the gap before the retry that would have worked. Once the budget does expire the kubelet kills and restarts the container — the Pod is not recreated — which remains the intended outcome for a source that stays unreachable: an instance that has never applied a configuration has nothing to serve, and the restarted container simply resumes the same wait. Boots that bind immediately — file mode, and an etcd-mode boot that restores a usable snapshot cache — are unaffected. | | startupProbe.failureThreshold | int | `150` | | | startupProbe.periodSeconds | int | `2` | | diff --git a/charts/aisix/README.md.gotmpl b/charts/aisix/README.md.gotmpl index fdd4cc26..fbace591 100644 --- a/charts/aisix/README.md.gotmpl +++ b/charts/aisix/README.md.gotmpl @@ -8,13 +8,20 @@ AISIX is an AI gateway: it fronts LLM providers with routing, rate limiting, bud caching, guardrails, and observability behind an OpenAI-compatible API. This chart installs the **data plane** — the component that serves live AI traffic. -The data plane is configured by the AISIX control plane, not by this chart. It -connects out to the control plane's data-plane manager over mutual TLS, using a -gateway certificate bundle issued from the console, and receives its models, API -keys, and policies from there. Install the control plane first — with the -[`aisix-cp`](../aisix-cp/README.md) chart, or any of the other options in the +The chart installs it in either of two modes, chosen with `controlPlane.enabled`. + +By default the gateway is configured by the AISIX control plane, not by this +chart. It connects out to the control plane's data-plane manager over mutual TLS, +using a gateway certificate bundle issued from the console, and receives its +models, API keys, and policies from there. Install the control plane first — with +the [`aisix-cp`](../aisix-cp/README.md) chart, or any of the other options in the [on-premises installation guide](https://docs.api7.ai/ai-gateway/on-premises/deployment). +With `controlPlane.enabled: false` the gateway runs standalone, as the +open-source AI gateway with no control plane at all: every resource comes from +one declarative `resources.yaml` you supply through the chart. See +[Standalone mode](#standalone-mode-no-control-plane) below. + {{ template "chart.homepageLine" . }} {{ template "chart.maintainersSection" . }} @@ -25,8 +32,10 @@ keys, and policies from there. Install the control plane first — with the * Kubernetes v1.23+ * Helm v3+ -* An AISIX control plane, reachable from the cluster -* A gateway certificate bundle for the environment this gateway should serve +* For the default mode: an AISIX control plane reachable from the cluster, and a + gateway certificate bundle for the environment this gateway should serve +* For standalone mode: a `resources.yaml` declaring the provider keys, models and + caller API keys the gateway should serve ## Install @@ -65,6 +74,116 @@ certificate. helm delete aisix --namespace aisix ``` +## Standalone mode (no control plane) + +Set `controlPlane.enabled: false` to run the open-source gateway on its own. +Nothing under `controlPlane` is read, no certificate bundle is needed, and the +gateway reads every resource — provider keys, models, caller API keys, +guardrails, MCP servers, rate-limit policies — from one `resources.yaml`. The +chart renders a startup configuration pointing at it, mounts it read-only at +`/etc/aisix/resources/resources.yaml`, and leaves the admin API unbound, so the +file is the only way resources are declared. + +Supply the file through exactly one of `standalone.resources`, +`standalone.existingSecret`, or `standalone.existingConfigMap`; setting none or +more than one fails the render. + +`standalone.resources` takes the file inline, as a map, and renders it into a +chart-managed Secret — a Secret rather than a ConfigMap because provider keys are +credentials. Credentials do not have to live in your values file even so: a +`${VAR}` reference is resolved from the container's environment when the file +loads, so the value itself can come from `extraEnvVars` or from a Secret you +manage separately. + +```yaml +controlPlane: + enabled: false + +standalone: + resources: + _format_version: "1" + provider_keys: + - display_name: openai-main + provider: openai + adapter: openai + api_key: ${OPENAI_API_KEY} + api_base: https://api.openai.com/v1 + models: + - display_name: gpt-4o-mini + provider: openai + model_name: gpt-4o-mini + provider_key: openai-main + api_keys: + - display_name: my-caller + key_env: CALLER_API_KEY + allowed_models: + - gpt-4o-mini + +extraEnvVars: + - name: OPENAI_API_KEY + valueFrom: + secretKeyRef: + name: openai-credentials + key: api-key + - name: CALLER_API_KEY + valueFrom: + secretKeyRef: + name: aisix-caller-keys + key: my-caller +``` + +`standalone.existingSecret` and `standalone.existingConfigMap` read the file from +an object you already manage, under the key `resources.yaml`: + +```sh +kubectl -n aisix create secret generic aisix-resources \ + --from-file=resources.yaml=./resources.yaml +``` + +```yaml +controlPlane: + enabled: false + +standalone: + existingSecret: aisix-resources +``` + +Validate a file before you install it, without starting a listener: + +```sh +docker run --rm -v "$(pwd):/work:ro" \ + --entrypoint /usr/local/bin/aisix api7/aisix: \ + validate --resources /work/resources.yaml +``` + +The file's own schema — every resource kind and field — is documented in the +[open-source gateway quickstart](https://docs.api7.ai/ai-gateway/getting-started/gateway-quickstart) +and the reference pages it links. + +### Applying a change + +The gateway re-reads `resources.yaml` on `SIGHUP` only, and this chart never +sends one, so a rollout is what applies a change. + +Editing `standalone.resources` and running `helm upgrade` does that on its own: +the pod template carries a checksum of the rendered file, so the change rolls the +pods. Editing the Secret or ConfigMap behind `standalone.existingSecret` / +`standalone.existingConfigMap` does not — Kubernetes updates the mounted file in +place and nothing tells the gateway. Apply it with: + +```sh +kubectl rollout restart deploy/-aisix -n +``` + +### What is not available + +Standalone mode has no control plane, so there is no console, no usage or budget +reporting, and no per-environment configuration distribution. The admin API is +left unbound as well: it is read-only against a file source, and binding it would +require admin keys the chart does not manage. Turn it on with +`extraEnvVars` — `AISIX_ADMIN__ENABLED`, `AISIX_ADMIN__ADDR` and +`AISIX_ADMIN__ADMIN_KEYS` — if you want it. + ## Termination and draining `terminationGracePeriodSeconds` defaults to 1230 seconds, far above the diff --git a/charts/aisix/ci/standalone-values.yaml b/charts/aisix/ci/standalone-values.yaml new file mode 100644 index 00000000..71ddd0ac --- /dev/null +++ b/charts/aisix/ci/standalone-values.yaml @@ -0,0 +1,46 @@ +# Values used by chart-testing for the standalone (no control plane) mode. +# Unlike ci/default-values.yaml this file drives `ct install` as well as +# `ct lint`, so everything it references has to exist in the cluster. +# +# The chart's default image is the released appVersion, whose USER is a name +# rather than a uid; `runAsNonRoot` cannot verify that, so the kubelet refuses +# to start it now that podSecurityContext no longer pins `runAsUser`. Install +# against the development image, which declares the uid numerically, until an +# appVersion that does the same is released. +image: + repository: ghcr.io/api7/aisix + tag: dev + +controlPlane: + enabled: false + +replicaCount: 1 + +standalone: + resources: + _format_version: "1" + provider_keys: + - display_name: openai-main + provider: openai + adapter: openai + api_key: ${OPENAI_API_KEY} + api_base: https://api.openai.com/v1 + models: + - display_name: gpt-4o-mini + provider: openai + model_name: gpt-4o-mini + provider_key: openai-main + api_keys: + - display_name: ci-caller + key_env: CALLER_API_KEY + allowed_models: + - gpt-4o-mini + +# Both variables are referenced by the resources file above, which fails to +# load if either is unset. No request is sent upstream during the install +# test, so the values only have to be non-empty. +extraEnvVars: + - name: OPENAI_API_KEY + value: sk-ci-placeholder-not-a-real-key + - name: CALLER_API_KEY + value: ci-caller-placeholder diff --git a/charts/aisix/templates/NOTES.txt b/charts/aisix/templates/NOTES.txt index cf0ea82d..8148bffd 100644 --- a/charts/aisix/templates/NOTES.txt +++ b/charts/aisix/templates/NOTES.txt @@ -1,7 +1,25 @@ AISIX gateway {{ .Chart.AppVersion }} has been deployed as {{ include "aisix.fullname" . }}. +{{ if .Values.controlPlane.enabled -}} It connects out to the control plane at {{ .Values.controlPlane.baseURL }} and appears in that environment's Data planes view once its first heartbeat lands. +{{- else -}} +It runs standalone, serving the resources declared in +{{ include "aisix.standaloneResourcesPath" . }} +{{- if .Values.standalone.existingSecret }} (from Secret {{ .Values.standalone.existingSecret }}){{ end }} +{{- if .Values.standalone.existingConfigMap }} (from ConfigMap {{ .Values.standalone.existingConfigMap }}){{ end }}. +The admin API is not exposed; resources are declarative. + +The gateway re-reads that file on SIGHUP only, which this chart never sends. +{{- if .Values.standalone.resources }} +Editing `standalone.resources` and running `helm upgrade` rolls the pods, so +the change applies on its own. +{{- else }} +After editing the {{ if .Values.standalone.existingSecret }}Secret{{ else }}ConfigMap{{ end }} out of band, apply it with: + + kubectl rollout restart deploy/{{ include "aisix.fullname" . }} -n {{ .Release.Namespace }} +{{- end }} +{{- end }} Watch it come up: diff --git a/charts/aisix/templates/_helpers.tpl b/charts/aisix/templates/_helpers.tpl index 27593b97..a2ec893f 100644 --- a/charts/aisix/templates/_helpers.tpl +++ b/charts/aisix/templates/_helpers.tpl @@ -79,6 +79,30 @@ Name of the Secret holding the gateway certificate bundle. {{- end }} {{- end }} +{{/* +Standalone mode: the directory the startup config is mounted in, the file the +gateway reads from it, and the resources file it points at. Both live under +their own directory so neither mount shadows the image's own +/etc/aisix/config.managed.yaml. +*/}} +{{- define "aisix.standaloneConfigDir" -}}/etc/aisix/standalone{{- end }} +{{- define "aisix.standaloneConfigPath" -}}{{ include "aisix.standaloneConfigDir" . }}/config.yaml{{- end }} +{{- define "aisix.standaloneResourcesDir" -}}/etc/aisix/resources{{- end }} +{{- define "aisix.standaloneResourcesPath" -}}{{ include "aisix.standaloneResourcesDir" . }}/resources.yaml{{- end }} + +{{/* +Name of the Secret or ConfigMap holding resources.yaml. +*/}} +{{- define "aisix.resourcesObjectName" -}} +{{- if .Values.standalone.existingSecret }} +{{- .Values.standalone.existingSecret }} +{{- else if .Values.standalone.existingConfigMap }} +{{- .Values.standalone.existingConfigMap }} +{{- else }} +{{- printf "%s-resources" (include "aisix.fullname" .) }} +{{- end }} +{{- end }} + {{/* Name of the Secret holding the rate-limit Redis URL. */}} @@ -105,14 +129,24 @@ Secret key holding the rate-limit Redis URL. Reject value combinations that render successfully but cannot run. */}} {{- define "aisix.validateValues" -}} +{{- if .Values.controlPlane.enabled }} {{- if not .Values.controlPlane.baseURL }} -{{- fail "controlPlane.baseURL is required: set it to the data-plane manager endpoint shown in the control plane's Data planes view" }} +{{- fail "controlPlane.baseURL is required: set it to the data-plane manager endpoint shown in the control plane's Data planes view (or set controlPlane.enabled=false to run standalone)" }} {{- end }} {{- if not .Values.controlPlane.certificate.existingSecret }} {{- if not (and .Values.controlPlane.certificate.cert .Values.controlPlane.certificate.key .Values.controlPlane.certificate.ca) }} {{- fail "a gateway certificate bundle is required: set controlPlane.certificate.existingSecret, or all three of controlPlane.certificate.{cert,key,ca}" }} {{- end }} {{- end }} +{{- else }} +{{- $sources := 0 }} +{{- if .Values.standalone.resources }}{{ $sources = add1 $sources }}{{ end }} +{{- if .Values.standalone.existingSecret }}{{ $sources = add1 $sources }}{{ end }} +{{- if .Values.standalone.existingConfigMap }}{{ $sources = add1 $sources }}{{ end }} +{{- if ne $sources 1 }} +{{- fail "controlPlane.enabled=false requires exactly one resource source: standalone.resources, standalone.existingSecret, or standalone.existingConfigMap" }} +{{- end }} +{{- end }} {{- if and .Values.autoscaling.enabled .Values.keda.enabled }} {{- fail "autoscaling.enabled and keda.enabled are mutually exclusive: two controllers writing spec.replicas fight over the replica count" }} {{- end }} diff --git a/charts/aisix/templates/configmap.yaml b/charts/aisix/templates/configmap.yaml new file mode 100644 index 00000000..36fe8f1c --- /dev/null +++ b/charts/aisix/templates/configmap.yaml @@ -0,0 +1,21 @@ +{{- if not .Values.controlPlane.enabled }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "aisix.fullname" . }}-config + labels: + {{- include "aisix.labels" . | nindent 4 }} + {{- include "aisix.selectorLabels" . | nindent 4 }} +data: + # Startup configuration for standalone mode. It carries only what an + # AISIX_-prefixed environment variable cannot express as a default the + # user can still override: the resources file to read, and the admin + # listener, which defaults to enabled and would refuse to boot without + # admin keys. Everything else — the proxy and metrics addresses, the + # rate-limit backend, and anything set through `extraEnvVars` — reaches + # the gateway as environment variables, which override this file. + config.yaml: | + resources_file: {{ include "aisix.standaloneResourcesPath" . }} + admin: + enabled: false +{{- end }} diff --git a/charts/aisix/templates/deployment.yaml b/charts/aisix/templates/deployment.yaml index 2b54de9a..37fcb82c 100644 --- a/charts/aisix/templates/deployment.yaml +++ b/charts/aisix/templates/deployment.yaml @@ -18,14 +18,26 @@ spec: {{- include "aisix.selectorLabels" . | nindent 6 }} template: metadata: - {{- if or (not .Values.controlPlane.certificate.existingSecret) .Values.podAnnotations }} + {{- if or (not .Values.controlPlane.enabled) (not .Values.controlPlane.certificate.existingSecret) .Values.podAnnotations }} annotations: + {{- if .Values.controlPlane.enabled }} {{- if not .Values.controlPlane.certificate.existingSecret }} # Roll the pods when the chart-managed bundle changes, so a rotated # certificate takes effect without a manual restart. An existingSecret # has its own lifecycle — restart the Deployment after rotating it. checksum/mtls: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} {{- end }} + {{- else }} + # Roll the pods when the rendered startup config or the chart-managed + # resources file changes. The gateway re-reads the resources file on + # SIGHUP only and the chart never sends one, so the rollout is what + # applies the change. An existingSecret / existingConfigMap has its own + # lifecycle — restart the Deployment after editing it. + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + {{- if .Values.standalone.resources }} + checksum/resources: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} + {{- end }} + {{- end }} {{- with .Values.podAnnotations }} {{- toYaml . | nindent 8 }} {{- end }} @@ -72,11 +84,16 @@ spec: protocol: TCP env: - name: AISIX_CONFIG_PATH + {{- if .Values.controlPlane.enabled }} value: /etc/aisix/config.managed.yaml + {{- else }} + value: {{ include "aisix.standaloneConfigPath" . }} + {{- end }} - name: AISIX_PROXY__ADDR value: "0.0.0.0:{{ .Values.containerPorts.proxy }}" - name: AISIX_OBSERVABILITY__METRICS__PROMETHEUS__ADDR value: "0.0.0.0:{{ .Values.containerPorts.metrics }}" + {{- if .Values.controlPlane.enabled }} - name: AISIX_MANAGED__CP_BASE_URL value: {{ .Values.controlPlane.baseURL | quote }} {{- with .Values.controlPlane.etcdEndpoint }} @@ -100,6 +117,7 @@ spec: secretKeyRef: name: {{ include "aisix.certSecretName" . }} key: {{ .Values.controlPlane.certificate.caKey }} + {{- end }} {{- if eq .Values.rateLimit.backend "redis" }} - name: AISIX_RATELIMIT__BACKEND value: "redis" @@ -173,12 +191,35 @@ spec: # instances, and a restarted pod re-registers. - name: state mountPath: /var/lib/aisix + {{- if not .Values.controlPlane.enabled }} + # Each in its own directory, so neither mount shadows the + # /etc/aisix/config.managed.yaml the image ships. + - name: config + mountPath: {{ include "aisix.standaloneConfigDir" . }} + readOnly: true + - name: resources + mountPath: {{ include "aisix.standaloneResourcesDir" . }} + readOnly: true + {{- end }} {{- with .Values.extraVolumeMounts }} {{- toYaml . | nindent 12 }} {{- end }} volumes: - name: state emptyDir: {} + {{- if not .Values.controlPlane.enabled }} + - name: config + configMap: + name: {{ include "aisix.fullname" . }}-config + - name: resources + {{- if .Values.standalone.existingConfigMap }} + configMap: + name: {{ include "aisix.resourcesObjectName" . }} + {{- else }} + secret: + secretName: {{ include "aisix.resourcesObjectName" . }} + {{- end }} + {{- end }} {{- with .Values.extraVolumes }} {{- toYaml . | nindent 8 }} {{- end }} diff --git a/charts/aisix/templates/secret.yaml b/charts/aisix/templates/secret.yaml index d0a00809..e2625760 100644 --- a/charts/aisix/templates/secret.yaml +++ b/charts/aisix/templates/secret.yaml @@ -1,5 +1,5 @@ {{- include "aisix.validateValues" . }} -{{- if not .Values.controlPlane.certificate.existingSecret }} +{{- if and .Values.controlPlane.enabled (not .Values.controlPlane.certificate.existingSecret) }} apiVersion: v1 kind: Secret metadata: @@ -16,6 +16,21 @@ stringData: {{ .Values.controlPlane.certificate.caKey }}: |- {{ .Values.controlPlane.certificate.ca | trim | indent 4 }} {{- end }} +{{- if and (not .Values.controlPlane.enabled) .Values.standalone.resources }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "aisix.resourcesObjectName" . }} + labels: + {{- include "aisix.labels" . | nindent 4 }} + {{- include "aisix.selectorLabels" . | nindent 4 }} +type: Opaque +stringData: + # A Secret rather than a ConfigMap: resources.yaml carries provider keys. + resources.yaml: | +{{ toYaml .Values.standalone.resources | indent 4 }} +{{- end }} {{- if and (eq .Values.rateLimit.backend "redis") (not .Values.rateLimit.redis.existingSecret) }} --- apiVersion: v1 diff --git a/charts/aisix/values.yaml b/charts/aisix/values.yaml index 3bee8b90..8f690d9f 100644 --- a/charts/aisix/values.yaml +++ b/charts/aisix/values.yaml @@ -24,7 +24,14 @@ image: ## control plane's data-plane manager with the mTLS bundle issued from ## **Data planes** in the console, then reads its configuration from the ## control plane. Everything in this section comes from that view. +## +## Set `controlPlane.enabled: false` to run the open-source gateway instead, +## with no control plane at all: resources come from a `resources.yaml` file +## supplied under `standalone` below, and nothing in this section is read. controlPlane: + # -- Read configuration from an AISIX control plane. Set to false to run + # standalone, from the `resources.yaml` file configured under `standalone` + enabled: true # -- Data-plane manager mTLS endpoint the gateway connects out to, e.g. # `https://dpm.example.com:7944`. Required. baseURL: "" @@ -51,6 +58,32 @@ controlPlane: # -- CA bundle PEM. Used only when `existingSecret` is empty ca: "" +## Standalone (open-source) mode, read only when `controlPlane.enabled` is +## false. Every resource — provider keys, models, caller API keys, guardrails, +## MCP servers, rate-limit policies — comes from one `resources.yaml`, supplied +## through exactly one of the three keys below. The chart renders a startup +## config pointing the gateway at it and mounts it read-only. +## +## The gateway re-reads the file on SIGHUP only, and the chart does not send +## one. A change to `standalone.resources` rolls the pods on `helm upgrade` +## (the pod template carries a checksum of the rendered content), so it takes +## effect on its own. An out-of-band edit to `standalone.existingSecret` or +## `standalone.existingConfigMap` does not — run +## `kubectl rollout restart deploy/-aisix` after it. +standalone: + # -- Inline `resources.yaml` content, as a map. Rendered into a + # chart-managed Secret, because provider keys are credentials. Values may + # reference environment variables as `${VAR}` — supply them through + # `extraEnvVars` — so the credential itself need not live in this file + resources: {} + # -- Read `resources.yaml` from an existing Secret instead, under key + # `resources.yaml`. Recommended when the file carries literal credentials + existingSecret: "" + # -- Read `resources.yaml` from an existing ConfigMap instead, under key + # `resources.yaml`. Use only when every credential in it is a `${VAR}` + # reference resolved from `extraEnvVars` + existingConfigMap: "" + containerPorts: # -- Port the proxy listener binds inside the container. The image carries # the `CAP_NET_BIND_SERVICE` file capability, so a privileged port works @@ -197,16 +230,12 @@ resources: limits: memory: 1Gi -## The image already runs as uid 10001. `runAsUser` is repeated here because -## the image declares its user by name, and the kubelet cannot verify a named -## user against `runAsNonRoot`. +## The image already runs as uid 10001 and declares it numerically, which is +## what `runAsNonRoot` needs to verify without the uid being repeated here. +## Add `runAsUser` and `fsGroup` back to pin a fixed uid/gid: nothing in the +## chart depends on the numbers, since the one writable path is an emptyDir. podSecurityContext: runAsNonRoot: true - runAsUser: 10001 - # fsGroup owns the state directory emptyDir so the gateway can write its - # mTLS material, gateway id, and configuration snapshot under a read-only - # root filesystem. - fsGroup: 10001 seccompProfile: type: RuntimeDefault