diff --git a/charts/aisix-cp/Chart.yaml b/charts/aisix-cp/Chart.yaml index 8e81215..97739ce 100644 --- a/charts/aisix-cp/Chart.yaml +++ b/charts/aisix-cp/Chart.yaml @@ -2,8 +2,8 @@ apiVersion: v2 name: aisix-cp description: Helm chart for AISIX control plane (cp-api, dp-manager, dashboard) type: application -version: 1.3.0 -appVersion: "1.3.0" +version: 1.4.0 +appVersion: "1.4.0" maintainers: - name: API7 diff --git a/charts/aisix-cp/README.md b/charts/aisix-cp/README.md index d1091da..0a613b6 100644 --- a/charts/aisix-cp/README.md +++ b/charts/aisix-cp/README.md @@ -1,6 +1,6 @@ # aisix-cp -![Version: 1.3.0](https://img.shields.io/badge/Version-1.3.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.3.0](https://img.shields.io/badge/AppVersion-1.3.0-informational?style=flat-square) +![Version: 1.4.0](https://img.shields.io/badge/Version-1.4.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.4.0](https://img.shields.io/badge/AppVersion-1.4.0-informational?style=flat-square) Helm chart for AISIX control plane (cp-api, dp-manager, dashboard) @@ -104,7 +104,7 @@ helm repo add api7 https://charts.api7.ai helm repo update helm install aisix-cp api7/aisix-cp --namespace aisix --create-namespace \ - --version 1.3.0 \ + --version 1.4.0 \ -f cp-values.yaml ``` @@ -113,7 +113,7 @@ same install, as long as you record what you generated: ```sh helm install aisix-cp api7/aisix-cp --namespace aisix --create-namespace \ - --version 1.3.0 \ + --version 1.4.0 \ --set secrets.masterKey="$(openssl rand -base64 32)" \ --set secrets.betterAuthSecret="$(openssl rand -base64 48)" \ --set postgresql.auth.postgresPassword="$(openssl rand -hex 24)" \ @@ -159,7 +159,7 @@ fails the handshake with nothing logged to say why. The console's generated commands carry a gateway image, taken from `api.dpImage`. Left empty — the default — it follows the chart's `appVersion`, -so this release hands out `docker.io/api7/aisix:1.3.0`. +so this release hands out `docker.io/api7/aisix:1.4.0`. Set it only to pin a different image, and remember that a value set explicitly carries forward across upgrades, so gateways added later come up on the old image. @@ -335,7 +335,7 @@ install time: ```sh helm install aisix-cp api7/aisix-cp --namespace aisix --create-namespace \ - --version 1.3.0 \ + --version 1.4.0 \ -f cp-values.yaml \ --set postgresql.primary.podSecurityContext.enabled=false \ --set postgresql.primary.containerSecurityContext.enabled=false @@ -386,121 +386,126 @@ api: ## Parameters +An empty `api.image.tag`, `dpm.image.tag` or `ui.image.tag` resolves to the +chart's `appVersion`, and an empty `api.dpImage` to +`docker.io/api7/aisix:`, so a default install runs the control +plane and hands out the gateway image from the same release as the chart. + ## Values | Key | Type | Default | Description | |-----|------|---------|-------------| -| api.affinity | object | `{}` | | -| api.corsAllowedOrigins | list | `[]` | | -| api.dpImage | string | `""` | | -| api.dpmgrBaseURL | string | `""` | | -| api.extraEnvVars | list | `[]` | | -| api.image.pullPolicy | string | `"IfNotPresent"` | | -| api.image.repository | string | `"docker.io/api7/aisix-cp-api"` | | -| api.image.tag | string | `""` | | -| api.metrics.enabled | bool | `true` | | -| api.metrics.port | int | `9090` | | -| api.metrics.service.annotations | object | `{}` | | -| api.metrics.service.port | int | `9090` | | -| api.metrics.serviceMonitor.enabled | bool | `false` | | -| api.metrics.serviceMonitor.interval | string | `"30s"` | | -| api.metrics.serviceMonitor.labels | object | `{}` | | -| api.metrics.serviceMonitor.metricRelabelings | list | `[]` | | -| api.metrics.serviceMonitor.namespace | string | `""` | | -| api.metrics.serviceMonitor.relabelings | list | `[]` | | -| api.metrics.serviceMonitor.scrapeTimeout | string | `""` | | -| api.nodeSelector | object | `{}` | | -| api.notifyAllowPrivateURLs | bool | `false` | | -| api.oauthEnabled | bool | `false` | | -| api.playgroundAllowPrivateIPs | bool | `false` | | -| api.podSecurityContext.runAsNonRoot | bool | `true` | | -| api.podSecurityContext.seccompProfile.type | string | `"RuntimeDefault"` | | -| api.publicBaseURL | string | `"http://localhost:8080"` | | -| api.replicaCount | int | `1` | | -| api.resources.limits.cpu | string | `"1"` | | -| api.resources.limits.memory | string | `"512Mi"` | | -| api.resources.requests.cpu | string | `"100m"` | | -| api.resources.requests.memory | string | `"128Mi"` | | -| api.securityContext.allowPrivilegeEscalation | bool | `false` | | -| api.securityContext.capabilities.drop[0] | string | `"ALL"` | | -| api.securityContext.readOnlyRootFilesystem | bool | `true` | | -| api.service.nodePort | string | `""` | | -| api.service.port | int | `8080` | | -| api.service.type | string | `"ClusterIP"` | | -| api.tolerations | list | `[]` | | -| dpm.affinity | object | `{}` | | -| dpm.extraEnvVars | list | `[]` | | -| dpm.image.pullPolicy | string | `"IfNotPresent"` | | -| dpm.image.repository | string | `"docker.io/api7/aisix-cp-dpm"` | | -| dpm.image.tag | string | `""` | | -| dpm.nodeSelector | object | `{}` | | -| dpm.podSecurityContext.runAsNonRoot | bool | `true` | | -| dpm.podSecurityContext.seccompProfile.type | string | `"RuntimeDefault"` | | -| dpm.replicaCount | int | `1` | | -| dpm.resources.limits.cpu | string | `"1"` | | -| dpm.resources.limits.memory | string | `"512Mi"` | | -| dpm.resources.requests.cpu | string | `"100m"` | | -| dpm.resources.requests.memory | string | `"128Mi"` | | -| dpm.securityContext.allowPrivilegeEscalation | bool | `false` | | -| dpm.securityContext.capabilities.drop[0] | string | `"ALL"` | | -| dpm.securityContext.readOnlyRootFilesystem | bool | `true` | | -| dpm.service.healthListen | string | `":7946"` | | -| dpm.service.nodePort | string | `""` | | -| dpm.service.port | int | `7944` | | -| dpm.service.type | string | `"ClusterIP"` | | -| dpm.tolerations | list | `[]` | | -| externalDatabase.database | string | `"aisix_cloud"` | | -| externalDatabase.existingSecret | string | `""` | | -| externalDatabase.host | string | `""` | | -| externalDatabase.password | string | `""` | | -| externalDatabase.port | int | `5432` | | -| externalDatabase.sslmode | string | `"disable"` | | -| externalDatabase.username | string | `"aisix"` | | -| global.imagePullSecrets | list | `[]` | | -| global.storageClass | string | `""` | | -| postgresql.auth.database | string | `"aisix_cloud"` | | -| postgresql.auth.existingSecret | string | `""` | | -| postgresql.auth.password | string | `"changeme"` | | -| postgresql.auth.postgresPassword | string | `"changeme"` | | -| postgresql.auth.usePostgresUserForAppConnections | bool | `true` | | -| postgresql.auth.username | string | `"aisix"` | | -| postgresql.builtin | bool | `true` | | -| postgresql.fullnameOverride | string | `""` | | -| postgresql.image.registry | string | `"docker.io"` | | -| postgresql.image.repository | string | `"api7/postgresql"` | | -| postgresql.image.tag | string | `"15.4.0-debian-11-r45"` | | -| postgresql.primary.persistence.size | string | `"8Gi"` | | -| postgresql.primary.service.ports.postgresql | int | `5432` | | -| secrets.betterAuthSecret | string | `"CHANGE_ME_GENERATE_WITH_openssl_rand_-base64_48"` | | -| secrets.masterKey | string | `"CHANGE_ME_GENERATE_WITH_openssl_rand_-base64_32"` | | -| secrets.masterKeyID | string | `"env:default"` | | -| serviceAccount.annotations | object | `{}` | | -| serviceAccount.create | bool | `true` | | -| serviceAccount.name | string | `""` | | -| twoDSN.enabled | bool | `false` | | -| twoDSN.existingSecret | string | `""` | | -| twoDSN.existingSecretKey | string | `"serving-password"` | | -| twoDSN.servingPassword | string | `""` | | -| ui.affinity | object | `{}` | | -| ui.defaultLocale | string | `"en"` | | -| ui.extraEnvVars | list | `[]` | | +| api.affinity | object | `{}` | Affinity rules for the cp-api pod | +| api.corsAllowedOrigins | list | `[]` | Browser origins allowed to call cp-api cross-origin, as bare origins (scheme://host[:port]). Empty — the default — writes no CORS headers at all, which is what a normal install wants: cp-api serves the API and proxies the dashboard, so the browser has only one origin and nothing is ever cross-origin. Populate it only to let a dashboard served from somewhere else reach this API directly, such as a frontend-only PR preview. An entry is a bare origin (https://host[:port]), or `https://*` plus a suffix naming at least three labels (`https://*-api7ai.vercel.app`) — a PR preview's host carries the branch name, so an exact list would mean redeploying cp-api for every pull request. Read the suffix as collision-resistance, NOT as a guarantee: it is a byte suffix, and on a shared host like `.vercel.app` another account can claim a name ending the same way, so a grant that needs a real boundary belongs on a domain you control. Every entry must be spelled the way a BROWSER serializes an origin, and the chart refuses one that is not, naming the spelling to use — a bare `*`, a path, query, fragment or userinfo, a trailing dot, a non-https origin (loopback may use http), an internationalized host given as anything but punycode, a numeric last label that is not a dotted quad, an IPv6 address in any spelling but the one it is sent in, the scheme's default port written out, a port with a leading zero, and a port above 65535. cp-api applies the same rules at startup, so a value this chart accepts is one it will start with. | +| api.dpImage | string | `""` | Gateway image the console hands out in the install snippets it generates for a new data plane | +| api.dpmgrBaseURL | string | `""` | dp-manager /dp/* mTLS endpoint that data-plane hosts dial, as baked into the generated install snippets. Also passed to the dpm deployment, which seeds this host into the TLS server certificate it presents — so an IP address works here, not only a DNS name. When dpm.service.type is NodePort, use https://:. | +| api.extraEnvVars | list | `[]` | Extra environment for cp-api. This is also where AISIX_ALLOW_UNSUPPORTED_UPGRADE=1 goes when an upgrade has to proceed from a release older than this one supports upgrading from — cp-api refuses that upgrade at startup and names the version it needs. | +| api.image.pullPolicy | string | `"IfNotPresent"` | Image pull policy | +| api.image.repository | string | `"docker.io/api7/aisix-cp-api"` | cp-api image repository | +| api.image.tag | string | `""` | Image tag | +| api.metrics.enabled | bool | `true` | Serve Prometheus metrics from cp-api. On by default, matching the gateway chart so both planes are scraped the same way; the port is reachable only inside the cluster (ClusterIP). cp-api binds nothing unless this sets the address, so turning it off leaves the port genuinely unbound inside the pod rather than merely unexposed by the Service. | +| api.metrics.port | int | `9090` | Port the metrics listener binds inside the container | +| api.metrics.service.annotations | object | `{}` | Extra annotations for the metrics Service, e.g. scrape hints for a Prometheus that discovers by annotation rather than ServiceMonitor | +| api.metrics.service.port | int | `9090` | Metrics Service port, on a separate ClusterIP Service so scraping never rides the API Service | +| api.metrics.serviceMonitor.enabled | bool | `false` | Create a Prometheus Operator ServiceMonitor for the metrics Service | +| api.metrics.serviceMonitor.interval | string | `"30s"` | Scrape interval | +| api.metrics.serviceMonitor.labels | object | `{}` | Extra labels, e.g. the `release` label your Prometheus selects on | +| api.metrics.serviceMonitor.metricRelabelings | list | `[]` | Metric relabeling rules | +| api.metrics.serviceMonitor.namespace | string | `""` | Namespace to create the ServiceMonitor in. Empty uses the release namespace | +| api.metrics.serviceMonitor.relabelings | list | `[]` | Scrape-time relabeling rules | +| api.metrics.serviceMonitor.scrapeTimeout | string | `""` | Scrape timeout. Empty leaves the Prometheus default | +| api.nodeSelector | object | `{}` | Node selector for the cp-api pod | +| api.notifyAllowPrivateURLs | bool | `false` | Allow notification channels (budget alert webhooks / Slack) to point at private / internal addresses. Blocked by default (SSRF guard); enable only for On-Premises deployments whose webhook receivers live on an intranet the cp-api pod can route to. | +| api.oauthEnabled | bool | `false` | Offer Google / GitHub sign-in on the console's login page. Leave off unless the deployment carries OAuth client credentials | +| api.playgroundAllowPrivateIPs | bool | `false` | Allow the dashboard playground to reach LLM endpoints on private / internal networks. cp-api blocks private IPs by default (SSRF guard); enable this only for On-Premises deployments whose models live on an internal network the cp-api pod can route to. | +| api.podSecurityContext.runAsNonRoot | bool | `true` | Refuse to run the cp-api pod as root | +| api.podSecurityContext.seccompProfile.type | string | `"RuntimeDefault"` | Seccomp profile for the cp-api pod | +| api.publicBaseURL | string | `"http://localhost:8080"` | Publicly reachable cp-api URL. Better Auth validates the session JWT issuer against it, so a value that does not match the address the browser uses breaks sign-in. When api.service.type is NodePort, this must name the API NodePort endpoint (or the TLS reverse proxy in front). | +| api.replicaCount | int | `1` | Number of cp-api replicas | +| api.resources.limits.cpu | string | `"1"` | cp-api CPU limit | +| api.resources.limits.memory | string | `"512Mi"` | cp-api memory limit | +| api.resources.requests.cpu | string | `"100m"` | cp-api CPU request | +| api.resources.requests.memory | string | `"128Mi"` | cp-api memory request | +| api.securityContext.allowPrivilegeEscalation | bool | `false` | Forbid privilege escalation in the cp-api container | +| api.securityContext.capabilities.drop | list | `["ALL"]` | Linux capabilities dropped from the cp-api container | +| api.securityContext.readOnlyRootFilesystem | bool | `true` | Mount the cp-api root filesystem read-only | +| api.service.nodePort | string | `""` | Optional fixed NodePort. Used only when type is NodePort; leave empty to let Kubernetes allocate a port dynamically. Direct NodePort access is plain HTTP, so use a trusted private network or a TLS reverse proxy. | +| api.service.port | int | `8080` | cp-api Service port. This port carries both the Admin API and the Dashboard | +| api.service.type | string | `"ClusterIP"` | cp-api Service type. ClusterIP keeps it inside the cluster; use NodePort or an Ingress to reach the console from outside | +| api.tolerations | list | `[]` | Tolerations for the cp-api pod | +| dpm.affinity | object | `{}` | Affinity rules for the dp-manager pod | +| dpm.extraEnvVars | list | `[]` | Extra environment for dp-manager | +| dpm.image.pullPolicy | string | `"IfNotPresent"` | Image pull policy | +| dpm.image.repository | string | `"docker.io/api7/aisix-cp-dpm"` | dp-manager image repository | +| dpm.image.tag | string | `""` | Image tag | +| dpm.nodeSelector | object | `{}` | Node selector for the dp-manager pod | +| dpm.podSecurityContext.runAsNonRoot | bool | `true` | Refuse to run the dp-manager pod as root | +| dpm.podSecurityContext.seccompProfile.type | string | `"RuntimeDefault"` | Seccomp profile for the dp-manager pod | +| dpm.replicaCount | int | `1` | Number of dp-manager replicas | +| dpm.resources.limits.cpu | string | `"1"` | dp-manager CPU limit | +| dpm.resources.limits.memory | string | `"512Mi"` | dp-manager memory limit | +| dpm.resources.requests.cpu | string | `"100m"` | dp-manager CPU request | +| dpm.resources.requests.memory | string | `"128Mi"` | dp-manager memory request | +| dpm.securityContext.allowPrivilegeEscalation | bool | `false` | Forbid privilege escalation in the dp-manager container | +| dpm.securityContext.capabilities.drop | list | `["ALL"]` | Linux capabilities dropped from the dp-manager container | +| dpm.securityContext.readOnlyRootFilesystem | bool | `true` | Mount the dp-manager root filesystem read-only | +| dpm.service.healthListen | string | `":7946"` | Plain-HTTP listen address for /healthz (outbox-poller liveness), probed by the kubelet (the mTLS port 7944 cannot be probed). Set to "" to disable the health server + httpGet probes (the probes then fall back to a TCP check on the mTLS port). | +| dpm.service.nodePort | string | `""` | Optional fixed NodePort for the mTLS port. Used only when type is NodePort; leave empty to let Kubernetes allocate one | +| dpm.service.port | int | `7944` | dp-manager mTLS Service port, the one gateways dial | +| dpm.service.type | string | `"ClusterIP"` | dp-manager Service type. Use NodePort or a LoadBalancer when the gateways run outside this cluster | +| dpm.tolerations | list | `[]` | Tolerations for the dp-manager pod | +| externalDatabase.database | string | `"aisix_cloud"` | Database name | +| externalDatabase.existingSecret | string | `""` | Name of an existing Secret containing the database password (key: "password"). | +| externalDatabase.host | string | `""` | PostgreSQL host | +| externalDatabase.password | string | `""` | If existingSecret is empty, this password is used directly. Use a URL-safe value: it is embedded in a `postgres://` DSN | +| externalDatabase.port | int | `5432` | PostgreSQL port | +| externalDatabase.sslmode | string | `"disable"` | libpq sslmode for the connection, e.g. `require` or `verify-full` for a managed database reached over a network you do not control | +| externalDatabase.username | string | `"aisix"` | Database role the control plane connects as. It has to be able to run the schema migrations cp-api applies at startup | +| global.imagePullSecrets | list | `[]` | Image pull secrets applied to every pod created by this chart | +| global.storageClass | string | `""` | StorageClass for the built-in PostgreSQL volume. Empty uses the cluster's default StorageClass | +| postgresql.auth.database | string | `"aisix_cloud"` | Database name the control plane uses | +| postgresql.auth.existingSecret | string | `""` | Inject DB credentials from a pre-created Secret instead of the values below (the secure path). When set, the password fields are ignored and the placeholder rejection is skipped. | +| postgresql.auth.password | string | `"changeme"` | REQUIRED with builtin=true (unless existingSecret is set). Password for the application role. The chart REJECTS the default `changeme` at render time. Generate a URL-SAFE value with `openssl rand -hex 24`: the password is embedded in a `postgres://` DSN, so the `+`, `/` and `=` that `openssl rand -base64` produces corrupt URL parsing. PostgreSQL bakes it into the data volume on first init, so it has to stay the same across re-deploys | +| postgresql.auth.postgresPassword | string | `"changeme"` | REQUIRED with builtin=true (unless existingSecret is set). Password for the PostgreSQL superuser, under the same URL-safe rule and the same placeholder rejection as `password` above | +| postgresql.auth.usePostgresUserForAppConnections | bool | `true` | Use the built-in PostgreSQL superuser for application connections. cp-api currently runs schema and role migrations on startup, including ALTER ROLE statements that require superuser privileges. | +| postgresql.auth.username | string | `"aisix"` | Application database role cp-api connects as when usePostgresUserForAppConnections is false | +| postgresql.builtin | bool | `true` | Deploy the bundled PostgreSQL subchart. Set to false to point the control plane at the database configured under externalDatabase | +| postgresql.fullnameOverride | string | `""` | Override the generated name of the PostgreSQL resources | +| postgresql.image.registry | string | `"docker.io"` | PostgreSQL image registry | +| postgresql.image.repository | string | `"api7/postgresql"` | PostgreSQL image repository | +| postgresql.image.tag | string | `"15.4.0-debian-11-r45"` | PostgreSQL image tag | +| postgresql.primary.persistence.size | string | `"8Gi"` | Size of the PostgreSQL data volume. It holds every configured resource plus the request telemetry the console reports on | +| postgresql.primary.service.ports.postgresql | int | `5432` | PostgreSQL Service port | +| secrets.betterAuthSecret | string | `"CHANGE_ME_GENERATE_WITH_openssl_rand_-base64_48"` | REQUIRED. HMAC signing secret for Better Auth sessions. Generate with `openssl rand -base64 48`. Changing it signs every signed-in user out | +| secrets.masterKey | string | `"CHANGE_ME_GENERATE_WITH_openssl_rand_-base64_32"` | REQUIRED. Base64-encoded 32-byte AES-256 key that envelope-encrypts the stored upstream provider credentials. Generate with `openssl rand -base64 32`. It is the only thing that can decrypt what was stored under it, so a lost key means re-entering every provider key | +| secrets.masterKeyID | string | `"env:default"` | Identifier recorded alongside every value encrypted with the current master key, so a future key rotation can tell the generations apart | +| serviceAccount.annotations | object | `{}` | Annotations for the ServiceAccount, e.g. a cloud IAM role binding | +| serviceAccount.create | bool | `true` | Create a ServiceAccount for the control-plane pods | +| serviceAccount.name | string | `""` | Name of the ServiceAccount. Empty derives one from the release name; set it to use a ServiceAccount you manage yourself (create: false) | +| twoDSN.enabled | bool | `false` | Open a second, Row-Level-Security-enforced pool for tenant queries. Leave off for an On-Premises deployment | +| twoDSN.existingSecret | string | `""` | Optionally source the serving password from an existing Secret instead of servingPassword above. Key defaults to "serving-password". | +| twoDSN.existingSecretKey | string | `"serving-password"` | Key inside twoDSN.existingSecret holding the serving password | +| twoDSN.servingPassword | string | `""` | Password cp-api assigns to the cp_api_app serving role and uses in the serving DSN. Required when enabled unless existingSecret is set. Use a URL-safe value, from `openssl rand -hex 24`: it is embedded in a `postgres://` DSN, so `+`, `/` and `=` corrupt URL parsing | +| ui.affinity | object | `{}` | Affinity rules for the dashboard pod | +| ui.defaultLocale | string | `"en"` | Fixed dashboard UI language for this deployment. Supported: "en", "zh". There is no in-UI language switcher; the whole console renders in this locale (default English). Read server-side at request time. | +| ui.extraEnvVars | list | `[]` | Extra environment for the dashboard | | ui.extraVolumeMounts | list | `[]` | Additional mounts for the dashboard container. Use readOnly for CA certificates. | | ui.extraVolumes | list | `[]` | Additional volumes for the dashboard Pod (for example, a private database CA). | -| ui.image.pullPolicy | string | `"IfNotPresent"` | | -| ui.image.repository | string | `"docker.io/api7/aisix-cp-ui"` | | -| ui.image.tag | string | `""` | | -| ui.nodeSelector | object | `{}` | | -| ui.podSecurityContext.runAsNonRoot | bool | `true` | | -| ui.podSecurityContext.seccompProfile.type | string | `"RuntimeDefault"` | | -| ui.replicaCount | int | `1` | | -| ui.resources.limits.cpu | string | `"500m"` | | -| ui.resources.limits.memory | string | `"256Mi"` | | -| ui.resources.requests.cpu | string | `"50m"` | | -| ui.resources.requests.memory | string | `"64Mi"` | | -| ui.securityContext.allowPrivilegeEscalation | bool | `false` | | -| ui.securityContext.capabilities.drop[0] | string | `"ALL"` | | -| ui.securityContext.readOnlyRootFilesystem | bool | `true` | | -| ui.service.nodePort | string | `""` | | -| ui.service.port | int | `3000` | | -| ui.service.type | string | `"ClusterIP"` | | -| ui.tolerations | list | `[]` | | +| ui.image.pullPolicy | string | `"IfNotPresent"` | Image pull policy | +| ui.image.repository | string | `"docker.io/api7/aisix-cp-ui"` | Dashboard image repository | +| ui.image.tag | string | `""` | Image tag | +| ui.nodeSelector | object | `{}` | Node selector for the dashboard pod | +| ui.podSecurityContext.runAsNonRoot | bool | `true` | Refuse to run the dashboard pod as root | +| ui.podSecurityContext.seccompProfile.type | string | `"RuntimeDefault"` | Seccomp profile for the dashboard pod | +| ui.replicaCount | int | `1` | Number of dashboard replicas | +| ui.resources.limits.cpu | string | `"500m"` | Dashboard CPU limit | +| ui.resources.limits.memory | string | `"256Mi"` | Dashboard memory limit | +| ui.resources.requests.cpu | string | `"50m"` | Dashboard CPU request | +| ui.resources.requests.memory | string | `"64Mi"` | Dashboard memory request | +| ui.securityContext.allowPrivilegeEscalation | bool | `false` | Forbid privilege escalation in the dashboard container | +| ui.securityContext.capabilities.drop | list | `["ALL"]` | Linux capabilities dropped from the dashboard container | +| ui.securityContext.readOnlyRootFilesystem | bool | `true` | Mount the dashboard root filesystem read-only | +| ui.service.nodePort | string | `""` | Optional fixed NodePort. Used only when type is NodePort; leave empty to let Kubernetes allocate a port dynamically. This plain-HTTP endpoint is not a standalone Dashboard entry point: use it only behind a same-origin reverse proxy that sends Dashboard pages here and /api/* to cp-api. The proxy should terminate TLS unless it is on a trusted network. | +| ui.service.port | int | `3000` | Dashboard Service port, which cp-api proxies browser traffic to | +| ui.service.type | string | `"ClusterIP"` | Dashboard Service type | +| ui.tolerations | list | `[]` | Tolerations for the dashboard pod | diff --git a/charts/aisix-cp/README.md.gotmpl b/charts/aisix-cp/README.md.gotmpl index 2428898..f06644b 100644 --- a/charts/aisix-cp/README.md.gotmpl +++ b/charts/aisix-cp/README.md.gotmpl @@ -378,4 +378,9 @@ api: ## Parameters +An empty `api.image.tag`, `dpm.image.tag` or `ui.image.tag` resolves to the +chart's `appVersion`, and an empty `api.dpImage` to +`docker.io/api7/aisix:`, so a default install runs the control +plane and hands out the gateway image from the same release as the chart. + {{ template "chart.valuesSection" . }} diff --git a/charts/aisix-cp/templates/api-deployment.yaml b/charts/aisix-cp/templates/api-deployment.yaml index 6cb2cbc..745eca8 100644 --- a/charts/aisix-cp/templates/api-deployment.yaml +++ b/charts/aisix-cp/templates/api-deployment.yaml @@ -101,7 +101,12 @@ spec: {{- if or (not (regexMatch "^[a-z0-9._-]+$" $suffix)) (hasSuffix "." $suffix) }} {{- fail (printf "api.corsAllowedOrigins entry %q: a wildcard suffix must be a bare host suffix of ASCII letters, digits, dot, hyphen and underscore — a path, query, fragment, userinfo, port, trailing dot or non-ASCII name never matches a browser's Origin." .) }} {{- end }} - {{- $body := trimPrefix "." (trimPrefix "-" $suffix) }} + {{- /* Every leading separator goes, not one of each: cp-api + trims the whole run (`strings.TrimLeft(suffix, "-.")`) + before counting labels, so `https://*..a.b` counted three + labels here and two there — rendered, then refused at + startup. */ -}} + {{- $body := regexReplaceAll "^[-.]+" $suffix "" }} {{- if lt (len (splitList "." $body)) 3 }} {{- fail (printf "api.corsAllowedOrigins entry %q: a wildcard suffix must name at least three labels, or it covers a whole public suffix (https://*.vercel.app would admit every project on that host)." .) }} {{- end }} @@ -131,14 +136,20 @@ spec: {{- fail (printf "api.corsAllowedOrigins entry %q must be a bare origin like https://host[:port] — a trailing dot never matches a browser's Origin header." .) }} {{- end }} {{- if hasPrefix "[" $hostOnly }} - {{- /* An IPv6 address has many spellings and a browser sends one. - This cannot canonicalize an address, but it can refuse the - spellings that differ from the canonical one by a written-out - or zero-padded hextet, which is every form anyone types by - hand. cp-api makes the exact check. */ -}} + {{- /* An IPv6 address has many spellings and a browser sends + exactly one: the URL standard's serializer. This block + reproduces it, so the render refuses precisely the entries + cp-api refuses at startup. + + A zero hextet is NOT simply forbidden. The serializer + compresses the first-longest run of at least TWO zero + hextets and writes a lone one out, so `1:0:2:3:4:5:6:7` and + `1::1:0` are the canonical spellings of their addresses — + and the rule that refused every written-out `0` rejected + 1409 addresses a browser sends exactly as typed. */ -}} {{- $inner := trimSuffix "]" (trimPrefix "[" $hostOnly) }} - {{- if or (regexMatch "(^|:)0{1,4}(:|$)" $inner) (regexMatch "(^|:)0[0-9a-f]" $inner) }} - {{- fail (printf "api.corsAllowedOrigins entry %q: an IPv6 origin must be written the way a browser sends it — compress the zero hextets and drop the leading zeros." .) }} + {{- if regexMatch "(^|:)0[0-9a-f]" $inner }} + {{- fail (printf "api.corsAllowedOrigins entry %q: an IPv6 origin must be written the way a browser sends it — drop the leading zeros from each hextet." .) }} {{- end }} {{- /* Counting groups alone is not enough to tell an address from a typo: `without … ""` drops the empty components a doubled @@ -183,6 +194,64 @@ spec: {{- else if ne (len $groups) 8 }} {{- fail (printf "api.corsAllowedOrigins entry %q is not an IPv6 address — it needs eight groups, or `::` where they are omitted." .) }} {{- end }} + {{- /* The address is well formed here, so expand it to its eight + hextets and re-serialize it the way the URL standard does: + the first-longest run of two or more zero hextets becomes + `::`, everything else is written out. Any spelling other + than that one is an entry no Origin header can equal. + + An IPv4-mapped address keeps its hex spelling through this, + which is what a browser sends: `[::ffff:c0a8:1]`, never + `[::ffff:192.168.0.1]`. */ -}} + {{- $halves := splitList "::" $inner }} + {{- $lead := without (splitList ":" (index $halves 0)) "" }} + {{- $tail := list }} + {{- if contains "::" $inner }} + {{- $tail = without (splitList ":" (index $halves 1)) "" }} + {{- end }} + {{- $hextets := $lead }} + {{- range until (int (sub 8 (add (len $lead) (len $tail)))) }} + {{- $hextets = append $hextets "0" }} + {{- end }} + {{- range $tail }} + {{- $hextets = append $hextets . }} + {{- end }} + {{- /* First-longest run of two or more zero hextets. */ -}} + {{- $compress := -1 }} + {{- $best := 1 }} + {{- $run := 0 }} + {{- $runStart := 0 }} + {{- range $i, $h := $hextets }} + {{- if eq $h "0" }} + {{- if eq $run 0 }}{{- $runStart = $i }}{{- end }} + {{- $run = add1 $run }} + {{- if gt $run $best }}{{- $best = $run }}{{- $compress = $runStart }}{{- end }} + {{- else }} + {{- $run = 0 }} + {{- end }} + {{- end }} + {{- $canonical := "" }} + {{- $skipZeros := false }} + {{- range $i, $h := $hextets }} + {{- if and $skipZeros (eq $h "0") }} + {{- else }} + {{- $skipZeros = false }} + {{- if eq $i $compress }} + {{- if eq $i 0 }} + {{- $canonical = printf "%s::" $canonical }} + {{- else }} + {{- $canonical = printf "%s:" $canonical }} + {{- end }} + {{- $skipZeros = true }} + {{- else }} + {{- $canonical = printf "%s%s" $canonical $h }} + {{- if ne $i 7 }}{{- $canonical = printf "%s:" $canonical }}{{- end }} + {{- end }} + {{- end }} + {{- end }} + {{- if ne $canonical $inner }} + {{- fail (printf "api.corsAllowedOrigins entry %q: an IPv6 origin must be written the way a browser sends it, which for this address is `[%s]`." $entry $canonical) }} + {{- end }} {{- else }} {{- /* A browser re-reads a host whose last label is numeric as an IPv4 address and rewrites it: `https://2130706433` is sent as diff --git a/charts/aisix-cp/templates/secret.yaml b/charts/aisix-cp/templates/secret.yaml index 4665cb4..8532da6 100644 --- a/charts/aisix-cp/templates/secret.yaml +++ b/charts/aisix-cp/templates/secret.yaml @@ -19,7 +19,7 @@ password. Skipped when an existingSecret injects the credentials. {{- range $field := list "password" "postgresPassword" }} {{- $val := get $.Values.postgresql.auth $field | toString }} {{- if or (eq $val "") (eq (lower $val) "changeme") (hasPrefix "CHANGE_ME" $val) }} - {{- fail (printf "postgresql.auth.%s must be set to a real password when postgresql.builtin=true (the default 'changeme' is rejected). Generate one with: openssl rand -base64 24 — or inject credentials via postgresql.auth.existingSecret." $field) }} + {{- fail (printf "postgresql.auth.%s must be set to a real password when postgresql.builtin=true (the default 'changeme' is rejected). Generate a URL-safe one with: openssl rand -hex 24 — a base64 password carries + / = and corrupts the postgres:// DSN it is embedded in. Or inject credentials via postgresql.auth.existingSecret." $field) }} {{- end }} {{- end }} {{- end }} diff --git a/charts/aisix-cp/values.yaml b/charts/aisix-cp/values.yaml index ca3e561..05c2b15 100644 --- a/charts/aisix-cp/values.yaml +++ b/charts/aisix-cp/values.yaml @@ -1,21 +1,35 @@ ## @section Global parameters global: + # -- StorageClass for the built-in PostgreSQL volume. Empty uses the + # cluster's default StorageClass storageClass: "" + # -- Image pull secrets applied to every pod created by this chart imagePullSecrets: [] ## @section cp-api +## +## cp-api serves the Admin API and reverse-proxies the Dashboard, so it is the +## one entry point a browser talks to. api: + # -- Number of cp-api replicas replicaCount: 1 image: + # -- cp-api image repository repository: docker.io/api7/aisix-cp-api + # -- Image pull policy pullPolicy: IfNotPresent + # -- Image tag tag: "" # empty -> .Chart.AppVersion service: + # -- cp-api Service type. ClusterIP keeps it inside the cluster; use + # NodePort or an Ingress to reach the console from outside type: ClusterIP + # -- cp-api Service port. This port carries both the Admin API and the + # Dashboard port: 8080 - ## Optional fixed NodePort. Used only when type is NodePort; leave empty - ## to let Kubernetes allocate a port dynamically. Direct NodePort access - ## is plain HTTP, so use a trusted private network or a TLS reverse proxy. + # -- Optional fixed NodePort. Used only when type is NodePort; leave empty + # to let Kubernetes allocate a port dynamically. Direct NodePort access + # is plain HTTP, so use a trusted private network or a TLS reverse proxy. nodePort: "" ## Operator telemetry. Served on its OWN listener, never on the API port: ## that one carries the Admin API and proxies the Dashboard and is the @@ -24,216 +38,264 @@ api: ## shows comes from here. Metric names are the `aisix_cp_` family, so one ## Prometheus can hold both planes and tell them apart by prefix. metrics: - ## Serve Prometheus metrics from cp-api. On by default, matching the - ## gateway chart so both planes are scraped the same way; the port is - ## reachable only inside the cluster (ClusterIP). cp-api binds nothing - ## unless this sets the address, so turning it off leaves the port - ## genuinely unbound inside the pod rather than merely unexposed by - ## the Service. + # -- Serve Prometheus metrics from cp-api. On by default, matching the + # gateway chart so both planes are scraped the same way; the port is + # reachable only inside the cluster (ClusterIP). cp-api binds nothing + # unless this sets the address, so turning it off leaves the port + # genuinely unbound inside the pod rather than merely unexposed by + # the Service. enabled: true - ## Port the metrics listener binds inside the container + # -- Port the metrics listener binds inside the container port: 9090 service: - ## Metrics Service port, on a separate ClusterIP Service so scraping - ## never rides the API Service + # -- Metrics Service port, on a separate ClusterIP Service so scraping + # never rides the API Service port: 9090 - ## Extra annotations for the metrics Service, e.g. scrape hints for a - ## Prometheus that discovers by annotation rather than ServiceMonitor + # -- Extra annotations for the metrics Service, e.g. scrape hints for a + # Prometheus that discovers by annotation rather than ServiceMonitor annotations: {} serviceMonitor: - ## Create a Prometheus Operator ServiceMonitor for the metrics Service + # -- Create a Prometheus Operator ServiceMonitor for the metrics Service enabled: false - ## Namespace to create the ServiceMonitor in. Empty uses the release namespace + # -- Namespace to create the ServiceMonitor in. Empty uses the release + # namespace namespace: "" - ## Scrape interval + # -- Scrape interval interval: 30s - ## Scrape timeout. Empty leaves the Prometheus default + # -- Scrape timeout. Empty leaves the Prometheus default scrapeTimeout: "" - ## Extra labels, e.g. the `release` label your Prometheus selects on + # -- Extra labels, e.g. the `release` label your Prometheus selects on labels: {} - ## Scrape-time relabeling rules + # -- Scrape-time relabeling rules relabelings: [] - ## Metric relabeling rules + # -- Metric relabeling rules metricRelabelings: [] resources: requests: + # -- cp-api CPU request cpu: 100m + # -- cp-api memory request memory: 128Mi limits: + # -- cp-api CPU limit cpu: "1" + # -- cp-api memory limit memory: 512Mi ## No runAsUser / runAsGroup / fsGroup: the image runs under whatever UID ## the platform assigns (OpenShift's restricted-v2 SCC picks a random one), ## and nothing here is written to a volume that would need a matching owner. ## Set those three keys to pin a fixed UID back. podSecurityContext: + # -- Refuse to run the cp-api pod as root runAsNonRoot: true seccompProfile: + # -- Seccomp profile for the cp-api pod type: RuntimeDefault securityContext: + # -- Forbid privilege escalation in the cp-api container allowPrivilegeEscalation: false + # -- Mount the cp-api root filesystem read-only readOnlyRootFilesystem: true capabilities: + # -- Linux capabilities dropped from the cp-api container drop: - ALL + # -- Node selector for the cp-api pod nodeSelector: {} + # -- Tolerations for the cp-api pod tolerations: [] + # -- Affinity rules for the cp-api pod affinity: {} - ## Extra environment for cp-api. This is also where - ## AISIX_ALLOW_UNSUPPORTED_UPGRADE=1 goes when an upgrade has to proceed - ## from a release older than this one supports upgrading from — cp-api - ## refuses that upgrade at startup and names the version it needs. + # -- Extra environment for cp-api. This is also where + # AISIX_ALLOW_UNSUPPORTED_UPGRADE=1 goes when an upgrade has to proceed + # from a release older than this one supports upgrading from — cp-api + # refuses that upgrade at startup and names the version it needs. extraEnvVars: [] - ## Publicly reachable cp-api URL. When api.service.type is NodePort, this - ## must name the API NodePort endpoint (or the TLS reverse proxy in front). + # -- Publicly reachable cp-api URL. Better Auth validates the session JWT + # issuer against it, so a value that does not match the address the browser + # uses breaks sign-in. When api.service.type is NodePort, this must name the + # API NodePort endpoint (or the TLS reverse proxy in front). publicBaseURL: "http://localhost:8080" - ## Browser origins allowed to call cp-api cross-origin, as bare origins - ## (scheme://host[:port]). Empty — the default — writes no CORS headers - ## at all, which is what a normal install wants: cp-api serves the API - ## and proxies the dashboard, so the browser has only one origin and - ## nothing is ever cross-origin. - ## - ## Populate it only to let a dashboard served from somewhere else reach - ## this API directly, such as a frontend-only PR preview. - ## - ## An entry is a bare origin (https://host[:port]), or `https://*` plus a - ## suffix — a PR preview's host carries the branch name, so an exact list - ## would mean redeploying cp-api for every pull request: - ## - ## corsAllowedOrigins: - ## - https://*-api7ai.vercel.app - ## - ## Read the suffix as collision-resistance, NOT as a guarantee. It is a - ## byte suffix: "-" is legal inside a DNS label rather than a separator, - ## and a .vercel.app name is allocated first-come-first-served from a - ## project name its owner picks, so another account can claim one ending - ## the same way. A grant that needs a real boundary belongs on a domain - ## you control — point previews at one with Vercel's preview deployment - ## suffix and allowlist https://*.preview.. - ## - ## The governing rule: an entry must be spelled the way a BROWSER - ## serializes an origin. Anything that parses but serializes differently - ## could never equal an Origin header, so it is refused here with the - ## spelling to use rather than accepted as an entry that quietly matches - ## nothing. cp-api applies the same rules at startup, so a value this - ## chart accepts is one it will start with. - ## - ## Refused: bare "*", a suffix not starting with "-" or ".", a suffix - ## naming fewer than three labels (https://*.vercel.app would cover every - ## project on that host), anything carrying a path, query, fragment or - ## userinfo, a trailing dot, any non-https origin (loopback may use - ## http), an internationalized host given as anything but its punycode - ## form, a host whose last label is numeric without being a dotted quad - ## (a browser re-reads https://2130706433 as https://127.0.0.1), an IPv6 - ## address written in any spelling but the one it is sent in, the - ## scheme's default port written out (":443" on https), a port carrying - ## a leading zero, and a port above 65535. + # -- Browser origins allowed to call cp-api cross-origin, as bare origins + # (scheme://host[:port]). Empty — the default — writes no CORS headers + # at all, which is what a normal install wants: cp-api serves the API + # and proxies the dashboard, so the browser has only one origin and + # nothing is ever cross-origin. + # Populate it only to let a dashboard served from somewhere else reach + # this API directly, such as a frontend-only PR preview. An entry is a bare + # origin (https://host[:port]), or `https://*` plus a suffix naming at least + # three labels (`https://*-api7ai.vercel.app`) — a PR preview's host carries + # the branch name, so an exact list would mean redeploying cp-api for every + # pull request. Read the suffix as collision-resistance, NOT as a guarantee: + # it is a byte suffix, and on a shared host like `.vercel.app` another + # account can claim a name ending the same way, so a grant that needs a real + # boundary belongs on a domain you control. Every entry must be spelled the + # way a BROWSER serializes an origin, and the chart refuses one that is not, + # naming the spelling to use — a bare `*`, a path, query, fragment or + # userinfo, a trailing dot, a non-https origin (loopback may use http), an + # internationalized host given as anything but punycode, a numeric last + # label that is not a dotted quad, an IPv6 address in any spelling but the + # one it is sent in, the scheme's default port written out, a port with a + # leading zero, and a port above 65535. cp-api applies the same rules at + # startup, so a value this chart accepts is one it will start with. corsAllowedOrigins: [] - ## dp-manager /dp/* mTLS endpoint that data-plane hosts dial, as baked - ## into the generated install snippets. Also passed to the dpm - ## deployment, which seeds this host into the TLS server certificate it - ## presents — so an IP address works here, not only a DNS name. When - ## dpm.service.type is NodePort, use https://:. + # -- dp-manager /dp/* mTLS endpoint that data-plane hosts dial, as baked + # into the generated install snippets. Also passed to the dpm + # deployment, which seeds this host into the TLS server certificate it + # presents — so an IP address works here, not only a DNS name. When + # dpm.service.type is NodePort, use https://:. dpmgrBaseURL: "" + # -- Offer Google / GitHub sign-in on the console's login page. Leave off + # unless the deployment carries OAuth client credentials oauthEnabled: false + # -- Gateway image the console hands out in the install snippets it + # generates for a new data plane dpImage: "" # empty -> docker.io/api7/aisix: - ## Allow the dashboard playground to reach LLM endpoints on private / - ## internal networks. cp-api blocks private IPs by default (SSRF guard); - ## enable this only for On-Premises deployments whose models live on an - ## internal network the cp-api pod can route to. + # -- Allow the dashboard playground to reach LLM endpoints on private / + # internal networks. cp-api blocks private IPs by default (SSRF guard); + # enable this only for On-Premises deployments whose models live on an + # internal network the cp-api pod can route to. playgroundAllowPrivateIPs: false - ## Allow notification channels (budget alert webhooks / Slack) to point - ## at private / internal addresses. Blocked by default (SSRF guard); - ## enable only for On-Premises deployments whose webhook receivers live - ## on an intranet the cp-api pod can route to. + # -- Allow notification channels (budget alert webhooks / Slack) to point + # at private / internal addresses. Blocked by default (SSRF guard); + # enable only for On-Premises deployments whose webhook receivers live + # on an intranet the cp-api pod can route to. notifyAllowPrivateURLs: false ## @section dp-manager +## +## dp-manager terminates the mTLS connection every gateway dials and serves it +## the configuration cp-api projects. Gateways reach this endpoint directly, so +## it is the one component that needs to be reachable from wherever they run. dpm: + # -- Number of dp-manager replicas replicaCount: 1 image: + # -- dp-manager image repository repository: docker.io/api7/aisix-cp-dpm + # -- Image pull policy pullPolicy: IfNotPresent + # -- Image tag tag: "" # empty -> .Chart.AppVersion service: + # -- dp-manager Service type. Use NodePort or a LoadBalancer when the + # gateways run outside this cluster type: ClusterIP + # -- dp-manager mTLS Service port, the one gateways dial port: 7944 - # Plain-HTTP listen address for /healthz (outbox-poller liveness), + # -- Plain-HTTP listen address for /healthz (outbox-poller liveness), # probed by the kubelet (the mTLS port 7944 cannot be probed). Set to # "" to disable the health server + httpGet probes (the probes then # fall back to a TCP check on the mTLS port). healthListen: ":7946" + # -- Optional fixed NodePort for the mTLS port. Used only when type is + # NodePort; leave empty to let Kubernetes allocate one nodePort: "" resources: requests: + # -- dp-manager CPU request cpu: 100m + # -- dp-manager memory request memory: 128Mi limits: + # -- dp-manager CPU limit cpu: "1" + # -- dp-manager memory limit memory: 512Mi ## No runAsUser / runAsGroup / fsGroup: the image runs under whatever UID ## the platform assigns (OpenShift's restricted-v2 SCC picks a random one), ## and nothing here is written to a volume that would need a matching owner. ## Set those three keys to pin a fixed UID back. podSecurityContext: + # -- Refuse to run the dp-manager pod as root runAsNonRoot: true seccompProfile: + # -- Seccomp profile for the dp-manager pod type: RuntimeDefault securityContext: + # -- Forbid privilege escalation in the dp-manager container allowPrivilegeEscalation: false + # -- Mount the dp-manager root filesystem read-only readOnlyRootFilesystem: true capabilities: + # -- Linux capabilities dropped from the dp-manager container drop: - ALL + # -- Node selector for the dp-manager pod nodeSelector: {} + # -- Tolerations for the dp-manager pod tolerations: [] + # -- Affinity rules for the dp-manager pod affinity: {} + # -- Extra environment for dp-manager extraEnvVars: [] ## @section dashboard (Next.js) +## +## The console's web tier. It is not a separate entry point: cp-api proxies +## browser traffic to it, so it stays a ClusterIP Service in a normal install. ui: + # -- Number of dashboard replicas replicaCount: 1 - # Fixed dashboard UI language for this deployment. Supported: "en", "zh". + # -- Fixed dashboard UI language for this deployment. Supported: "en", "zh". # There is no in-UI language switcher; the whole console renders in this # locale (default English). Read server-side at request time. defaultLocale: "en" image: + # -- Dashboard image repository repository: docker.io/api7/aisix-cp-ui + # -- Image pull policy pullPolicy: IfNotPresent + # -- Image tag tag: "" # empty -> .Chart.AppVersion service: + # -- Dashboard Service type type: ClusterIP + # -- Dashboard Service port, which cp-api proxies browser traffic to port: 3000 - ## Optional fixed NodePort. Used only when type is NodePort; leave empty - ## to let Kubernetes allocate a port dynamically. This plain-HTTP endpoint - ## is not a standalone Dashboard entry point: use it only behind a - ## same-origin reverse proxy that sends Dashboard pages here and /api/* to - ## cp-api. The proxy should terminate TLS unless it is on a trusted network. + # -- Optional fixed NodePort. Used only when type is NodePort; leave empty + # to let Kubernetes allocate a port dynamically. This plain-HTTP endpoint + # is not a standalone Dashboard entry point: use it only behind a + # same-origin reverse proxy that sends Dashboard pages here and /api/* to + # cp-api. The proxy should terminate TLS unless it is on a trusted network. nodePort: "" resources: requests: + # -- Dashboard CPU request cpu: 50m + # -- Dashboard memory request memory: 64Mi limits: + # -- Dashboard CPU limit cpu: 500m + # -- Dashboard memory limit memory: 256Mi ## No runAsUser / runAsGroup / fsGroup: the image runs under whatever UID ## the platform assigns (OpenShift's restricted-v2 SCC picks a random one). ## The .next/cache emptyDir needs no fsGroup to be writable — the kubelet ## creates it world-writable. Set those three keys to pin a fixed UID back. podSecurityContext: + # -- Refuse to run the dashboard pod as root runAsNonRoot: true seccompProfile: + # -- Seccomp profile for the dashboard pod type: RuntimeDefault securityContext: + # -- Forbid privilege escalation in the dashboard container allowPrivilegeEscalation: false + # -- Mount the dashboard root filesystem read-only readOnlyRootFilesystem: true capabilities: + # -- Linux capabilities dropped from the dashboard container drop: - ALL + # -- Node selector for the dashboard pod nodeSelector: {} + # -- Tolerations for the dashboard pod tolerations: [] + # -- Affinity rules for the dashboard pod affinity: {} + # -- Extra environment for the dashboard extraEnvVars: [] # -- Additional volumes for the dashboard Pod (for example, a private database CA). extraVolumes: [] @@ -242,30 +304,42 @@ ui: ## @section ServiceAccount serviceAccount: + # -- Create a ServiceAccount for the control-plane pods create: true + # -- Name of the ServiceAccount. Empty derives one from the release name; + # set it to use a ServiceAccount you manage yourself (create: false) name: "" + # -- Annotations for the ServiceAccount, e.g. a cloud IAM role binding annotations: {} ## @section Secrets -## IMPORTANT: You MUST replace the placeholder values below before deploying. -## Deploying with CHANGE_ME placeholders will fail at helm render/install time. ## -## masterKey: base64-encoded 32-byte AES-256 key for envelope encryption. -## Generate with: openssl rand -base64 32 -## betterAuthSecret: random string used as HMAC signing secret for Better Auth sessions. -## Generate with: openssl rand -base64 48 +## REQUIRED. The chart refuses to render while these carry their CHANGE_ME +## placeholders, so an install cannot come up with known secrets. Both values +## have to stay the same for the life of the deployment: the master key is the +## only thing that can decrypt the provider keys stored under it, and the Better +## Auth secret signs every session. Keep them somewhere you can restore from. secrets: + # -- REQUIRED. Base64-encoded 32-byte AES-256 key that envelope-encrypts the + # stored upstream provider credentials. Generate with + # `openssl rand -base64 32`. It is the only thing that can decrypt what was + # stored under it, so a lost key means re-entering every provider key masterKey: "CHANGE_ME_GENERATE_WITH_openssl_rand_-base64_32" + # -- Identifier recorded alongside every value encrypted with the current + # master key, so a future key rotation can tell the generations apart masterKeyID: "env:default" + # -- REQUIRED. HMAC signing secret for Better Auth sessions. Generate with + # `openssl rand -base64 48`. Changing it signs every signed-in user out betterAuthSecret: "CHANGE_ME_GENERATE_WITH_openssl_rand_-base64_48" ## @section PostgreSQL -## Set builtin=true to deploy Bitnami PostgreSQL subchart. -## Set builtin=false and configure externalDatabase to use an existing PostgreSQL instance. -## API/DPM wait for PostgreSQL only in builtin mode. -## An external database must be ready before starting the control plane. ## -## The bundled Bitnami subchart pins a UID of its own — fsGroup 1001 in +## Set builtin=true to deploy the bundled PostgreSQL subchart. Set +## builtin=false and configure externalDatabase to use a PostgreSQL instance +## you run yourself. The api and dpm pods wait for PostgreSQL only in builtin +## mode; an external database must be ready before the control plane starts. +## +## The bundled subchart pins a UID of its own — fsGroup 1001 in ## primary.podSecurityContext and runAsUser 1001 in ## primary.containerSecurityContext, both behind `enabled: true`. A subchart's ## values cannot be made conditional from here, so on a platform that assigns @@ -282,58 +356,82 @@ secrets: ## query with "could not resize shared memory segment". ## postgresql: + # -- Deploy the bundled PostgreSQL subchart. Set to false to point the + # control plane at the database configured under externalDatabase builtin: true + # -- Override the generated name of the PostgreSQL resources fullnameOverride: "" ## Also used by the wait-for-pg init container on the api and dpm pods, which ## carries no securityContext of its own: since the chart supplies no ## runAsUser, `runAsNonRoot: true` is checked against THIS image's USER, and a ## replacement that runs as root (or names its user) is refused at admission. image: + # -- PostgreSQL image registry registry: docker.io + # -- PostgreSQL image repository repository: api7/postgresql + # -- PostgreSQL image tag tag: 15.4.0-debian-11-r45 primary: persistence: + # -- Size of the PostgreSQL data volume. It holds every configured + # resource plus the request telemetry the console reports on size: 8Gi service: ports: + # -- PostgreSQL Service port postgresql: 5432 auth: - ## Use the built-in PostgreSQL superuser for application connections. - ## cp-api currently runs schema and role migrations on startup, including - ## ALTER ROLE statements that require superuser privileges. + # -- Use the built-in PostgreSQL superuser for application connections. + # cp-api currently runs schema and role migrations on startup, including + # ALTER ROLE statements that require superuser privileges. usePostgresUserForAppConnections: true + # -- Application database role cp-api connects as when + # usePostgresUserForAppConnections is false username: aisix - ## Inject DB credentials from a pre-created Secret instead of the values - ## below (the secure path). When set, the password fields are ignored and - ## the placeholder rejection is skipped. + # -- Inject DB credentials from a pre-created Secret instead of the values + # below (the secure path). When set, the password fields are ignored and + # the placeholder rejection is skipped. existingSecret: "" - ## IMPORTANT: Set a stable password before deploying. - ## If left as default, the password is baked into the PG data volume on first init - ## and must remain unchanged across re-deploys. - ## The chart REJECTS the default 'changeme' at render/install time when - ## postgresql.builtin=true — set a real password (openssl rand -hex 24). - ## Use a URL-safe value: the password is embedded in a postgres:// DSN, - ## so + / = (as produced by `openssl rand -base64`) corrupt URL parsing. - ## Or use existingSecret above. + # -- REQUIRED with builtin=true (unless existingSecret is set). Password + # for the application role. The chart REJECTS the default `changeme` at + # render time. Generate a URL-SAFE value with `openssl rand -hex 24`: the + # password is embedded in a `postgres://` DSN, so the `+`, `/` and `=` + # that `openssl rand -base64` produces corrupt URL parsing. PostgreSQL + # bakes it into the data volume on first init, so it has to stay the same + # across re-deploys password: changeme + # -- REQUIRED with builtin=true (unless existingSecret is set). Password + # for the PostgreSQL superuser, under the same URL-safe rule and the same + # placeholder rejection as `password` above postgresPassword: changeme + # -- Database name the control plane uses database: aisix_cloud ## @section External Database -## Only used when postgresql.builtin is false. +## +## Read only when postgresql.builtin is false. externalDatabase: + # -- PostgreSQL host host: "" + # -- PostgreSQL port port: 5432 + # -- Database role the control plane connects as. It has to be able to run + # the schema migrations cp-api applies at startup username: aisix + # -- Database name database: aisix_cloud - ## Name of an existing Secret containing the database password (key: "password"). + # -- Name of an existing Secret containing the database password (key: "password"). existingSecret: "" - ## If existingSecret is empty, this password is used directly. + # -- If existingSecret is empty, this password is used directly. Use a + # URL-safe value: it is embedded in a `postgres://` DSN password: "" + # -- libpq sslmode for the connection, e.g. `require` or `verify-full` for a + # managed database reached over a network you do not control sslmode: disable ## @section Two-DSN tenant isolation (SaaS / multi-tenant only) +## ## When enabled, cp-api opens TWO pools against the same database: ## - an admin pool on the privileged DSN above (builtin postgres or the ## externalDatabase user) that runs migrations + the cross-tenant paths; @@ -343,17 +441,20 @@ externalDatabase: ## cp-api provisions cp_api_app's LOGIN + password (from servingPassword) ## on the admin pool at boot — you do NOT pre-create the role. ## -## Leave DISABLED for single-tenant / private (OP) deployments: there every -## org is one trust domain, RLS is inert, and the single privileged DSN -## handles everything (the current default behavior). See the internal -## docs/rls-architecture.md for the full model. +## Leave DISABLED for single-tenant / private (On-Premises) deployments: there +## every organization is one trust domain, RLS is inert, and the single +## privileged DSN handles everything. twoDSN: + # -- Open a second, Row-Level-Security-enforced pool for tenant queries. + # Leave off for an On-Premises deployment enabled: false - ## Password cp-api assigns to the cp_api_app serving role and uses in the - ## serving DSN. Required when enabled unless existingSecret is set. - ## Use a URL-safe value (no + / =): it is embedded in a postgres:// DSN. + # -- Password cp-api assigns to the cp_api_app serving role and uses in the + # serving DSN. Required when enabled unless existingSecret is set. + # Use a URL-safe value, from `openssl rand -hex 24`: it is embedded in a + # `postgres://` DSN, so `+`, `/` and `=` corrupt URL parsing servingPassword: "" - ## Optionally source the serving password from an existing Secret instead - ## of servingPassword above. Key defaults to "serving-password". + # -- Optionally source the serving password from an existing Secret instead + # of servingPassword above. Key defaults to "serving-password". existingSecret: "" + # -- Key inside twoDSN.existingSecret holding the serving password existingSecretKey: "serving-password" diff --git a/charts/aisix/Chart.yaml b/charts/aisix/Chart.yaml index 64e3b99..a0dfeec 100644 --- a/charts/aisix/Chart.yaml +++ b/charts/aisix/Chart.yaml @@ -2,8 +2,8 @@ apiVersion: v2 name: aisix description: Helm chart for the AISIX AI gateway data plane type: application -version: 1.3.0 -appVersion: "1.3.0" +version: 1.4.0 +appVersion: "1.4.0" keywords: - ai-gateway diff --git a/charts/aisix/README.md b/charts/aisix/README.md index 2c5cce5..85ed98e 100644 --- a/charts/aisix/README.md +++ b/charts/aisix/README.md @@ -1,6 +1,6 @@ # aisix -![Version: 1.3.0](https://img.shields.io/badge/Version-1.3.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.3.0](https://img.shields.io/badge/AppVersion-1.3.0-informational?style=flat-square) +![Version: 1.4.0](https://img.shields.io/badge/Version-1.4.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.4.0](https://img.shields.io/badge/AppVersion-1.4.0-informational?style=flat-square) Helm chart for the AISIX AI gateway data plane