diff --git a/charts/aisix-cp/Chart.yaml b/charts/aisix-cp/Chart.yaml index 5e5e04f..8e81215 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.2.1 -appVersion: "1.2.0" +version: 1.3.0 +appVersion: "1.3.0" maintainers: - name: API7 diff --git a/charts/aisix-cp/README.md b/charts/aisix-cp/README.md index 3d0cc3a..1dadf24 100644 --- a/charts/aisix-cp/README.md +++ b/charts/aisix-cp/README.md @@ -1,6 +1,6 @@ # aisix-cp -![Version: 1.2.1](https://img.shields.io/badge/Version-1.2.1-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.2.0](https://img.shields.io/badge/AppVersion-1.2.0-informational?style=flat-square) +![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) Helm chart for AISIX control plane (cp-api, dp-manager, dashboard) @@ -64,20 +64,29 @@ in your deployment source so subsequent GitOps syncs preserve the configuration. | 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.fsGroup | int | `101` | | -| api.podSecurityContext.runAsGroup | int | `101` | | | api.podSecurityContext.runAsNonRoot | bool | `true` | | -| api.podSecurityContext.runAsUser | int | `10001` | | | api.podSecurityContext.seccompProfile.type | string | `"RuntimeDefault"` | | | api.publicBaseURL | string | `"http://localhost:8080"` | | | api.replicaCount | int | `1` | | @@ -98,10 +107,7 @@ in your deployment source so subsequent GitOps syncs preserve the configuration. | dpm.image.repository | string | `"docker.io/api7/aisix-cp-dpm"` | | | dpm.image.tag | string | `""` | | | dpm.nodeSelector | object | `{}` | | -| dpm.podSecurityContext.fsGroup | int | `101` | | -| dpm.podSecurityContext.runAsGroup | int | `101` | | | dpm.podSecurityContext.runAsNonRoot | bool | `true` | | -| dpm.podSecurityContext.runAsUser | int | `10001` | | | dpm.podSecurityContext.seccompProfile.type | string | `"RuntimeDefault"` | | | dpm.replicaCount | int | `1` | | | dpm.resources.limits.cpu | string | `"1"` | | @@ -157,10 +163,7 @@ in your deployment source so subsequent GitOps syncs preserve the configuration. | ui.image.repository | string | `"docker.io/api7/aisix-cp-ui"` | | | ui.image.tag | string | `""` | | | ui.nodeSelector | object | `{}` | | -| ui.podSecurityContext.fsGroup | int | `65533` | | -| ui.podSecurityContext.runAsGroup | int | `65533` | | | ui.podSecurityContext.runAsNonRoot | bool | `true` | | -| ui.podSecurityContext.runAsUser | int | `1001` | | | ui.podSecurityContext.seccompProfile.type | string | `"RuntimeDefault"` | | | ui.replicaCount | int | `1` | | | ui.resources.limits.cpu | string | `"500m"` | | diff --git a/charts/aisix-cp/templates/api-deployment.yaml b/charts/aisix-cp/templates/api-deployment.yaml index fe671c5..6cb2cbc 100644 --- a/charts/aisix-cp/templates/api-deployment.yaml +++ b/charts/aisix-cp/templates/api-deployment.yaml @@ -38,6 +38,11 @@ spec: - name: http containerPort: 8080 protocol: TCP + {{- if .Values.api.metrics.enabled }} + - name: metrics + containerPort: {{ .Values.api.metrics.port }} + protocol: TCP + {{- end }} env: - name: PGPASSWORD valueFrom: @@ -53,6 +58,10 @@ spec: {{- end }} - name: AISIX_CLOUD_LISTEN value: ":8080" + {{- if .Values.api.metrics.enabled }} + - name: AISIX_CLOUD_METRICS_LISTEN + value: {{ printf ":%v" .Values.api.metrics.port | quote }} + {{- end }} {{- if .Values.twoDSN.enabled }} - name: AISIX_CLOUD_ADMIN_DATABASE_URL value: {{ include "aisix-cp.databaseURL" . }} @@ -64,6 +73,152 @@ spec: {{- end }} - name: AISIX_CLOUD_PUBLIC_BASE_URL value: {{ .Values.api.publicBaseURL | quote }} + {{- with .Values.api.corsAllowedOrigins }} + {{- /* cp-api refuses these at startup, which an operator meets as a + CrashLoopBackOff and a log line rather than as a failed + upgrade. Fail the render instead, where the message lands + next to the value that caused it. + + These rules are cors.ValidateOrigin's, and they are only + worth having if they are the SAME rules: a shape this + template waves through and cp-api then refuses turns the + crash loop it exists to prevent back on. Compared + lowercased, because that is what cp-api normalizes to + before deciding. */ -}} + {{- range . }} + {{- $entry := lower (trim .) }} + {{- if eq $entry "*" }} + {{- fail "api.corsAllowedOrigins must name explicit origins, not \"*\": it is a blanket grant, and cp-api refuses to start with one." }} + {{- end }} + {{- if hasPrefix "https://*" $entry }} + {{- /* A wildcard suffix has to begin at a label separator, or it + also matches a longer label: https://*api7ai.vercel.app + would admit evilapi7ai.vercel.app. */ -}} + {{- $suffix := trimPrefix "https://*" $entry }} + {{- if not (or (hasPrefix "-" $suffix) (hasPrefix "." $suffix)) }} + {{- fail (printf "api.corsAllowedOrigins entry %q: a wildcard suffix must start with \"-\" or \".\" so it cannot match a longer label." .) }} + {{- end }} + {{- 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) }} + {{- 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 }} + {{- else }} + {{- /* Exact origin. https is required, with the same loopback + exception cp-api makes so a port-forwarded dev console can + be listed. */ -}} + {{- $loopback := or (hasPrefix "http://localhost:" $entry) (hasPrefix "http://127.0.0.1:" $entry) (hasPrefix "http://[::1]:" $entry) (eq $entry "http://localhost") (eq $entry "http://127.0.0.1") (eq $entry "http://[::1]") }} + {{- if not (or (hasPrefix "https://" $entry) $loopback) }} + {{- fail (printf "api.corsAllowedOrigins entry %q must use https:// (loopback may use http)." .) }} + {{- end }} + {{- /* A positive shape check, not a blacklist of bad characters. + Blacklisting let `https://host:abc`, `https://`, an + unbalanced `[::1` and a percent-escape through — each of + which cp-api then refuses at startup, which is the crash + loop this whole block exists to convert into a message. + + Underscores and edge hyphens are admitted because a browser + admits them: refusing one here blocked an upgrade over a + value cp-api accepts. */ -}} + {{- $rest := trimPrefix "http://" (trimPrefix "https://" $entry) }} + {{- if not (regexMatch "^(\\[[0-9a-f:]+\\]|[a-z0-9_.-]+)(:[0-9]{1,5})?$" $rest) }} + {{- fail (printf "api.corsAllowedOrigins entry %q must be a bare origin like https://host[:port] — a path, query, fragment, userinfo, trailing dot, empty or non-numeric port, or a non-ASCII host (write its punycode form) never matches a browser's Origin header." .) }} + {{- end }} + {{- $hostOnly := regexReplaceAll ":[0-9]+$" $rest "" }} + {{- if hasSuffix "." $hostOnly }} + {{- 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. */ -}} + {{- $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." .) }} + {{- end }} + {{- /* Counting groups alone is not enough to tell an address from + a typo: `without … ""` drops the empty components a doubled + or misplaced colon produces, so `1::2::3`, `:::`, `1:::2`, + `12345::1`, `1:2:3:4:5:6:7:8:`, `:1:2:3:4:5:6:7:8`, `1::2:` + and `:1::2` all reached the count as if well formed, and + cp-api refused every one of them at startup. + + The three rules below refuse a spelling that is not an + address at all: more than one `::`, a group that is not one + to four hex digits, and a colon on either end that is not + part of the `::`. + + They do NOT establish that what survives is a spelling + cp-api takes. That test is `ip.String() == host` — the + canonical form, which is stricter than parsing — and only + the shared case list decides whether the two agree. Add a + case to internal/cpapi/cors/testdata/origin-cases.txt; the + parity step in .github/workflows/ci-helm.yml runs the whole + list through this template and through cp-api and fails on + any entry they answer differently. */ -}} + {{- $groups := without (splitList ":" $inner) "" }} + {{- if or (contains ":::" $inner) (gt (len (splitList "::" $inner)) 2) }} + {{- fail (printf "api.corsAllowedOrigins entry %q is not an IPv6 address — `::` may appear once, and stands for the omitted groups." .) }} + {{- end }} + {{- range $groups }} + {{- if not (regexMatch "^[0-9a-f]{1,4}$" .) }} + {{- fail (printf "api.corsAllowedOrigins entry %q is not an IPv6 address — %q is not a group of one to four hex digits." $entry .) }} + {{- end }} + {{- end }} + {{- /* A colon on either end belongs to a `::`. On its own it is an + empty group, which `$groups` has already dropped. */ -}} + {{- if or (and (hasPrefix ":" $inner) (not (hasPrefix "::" $inner))) (and (hasSuffix ":" $inner) (not (hasSuffix "::" $inner))) }} + {{- fail (printf "api.corsAllowedOrigins entry %q is not an IPv6 address — a leading or trailing colon has no group in front of it." .) }} + {{- end }} + {{- /* `::` stands for at least one omitted group, so a spelling + that also writes all eight is not an address at all. */ -}} + {{- if contains "::" $inner }} + {{- if gt (len $groups) 6 }} + {{- fail (printf "api.corsAllowedOrigins entry %q is not an IPv6 address — `::` stands for at least two omitted groups — a single zero group is written out." .) }} + {{- end }} + {{- 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 }} + {{- else }} + {{- /* A browser re-reads a host whose last label is numeric as an + IPv4 address and rewrites it: `https://2130706433` is sent as + `https://127.0.0.1`. Such an entry can only match if it + already is a dotted quad. */ -}} + {{- $lastLabel := last (splitList "." $hostOnly) }} + {{- if regexMatch "^(0[xX][0-9a-fA-F]*|[0-9]+)$" $lastLabel }} + {{- if not (regexMatch "^([1-9]?[0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])(\\.([1-9]?[0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])){3}$" $hostOnly) }} + {{- fail (printf "api.corsAllowedOrigins entry %q: a host whose last label is numeric is read as an IPv4 address and rewritten — write the dotted-quad form a browser sends." .) }} + {{- end }} + {{- end }} + {{- end }} + {{- /* A port has one spelling. Five digits is a shape, not a port: + a browser refuses a URL above 65535, strips leading zeros, + and omits the scheme's default port when it serializes an + origin — so `:0080` and `:443` on https parse fine here and + then match nothing. Port 0 is legal and simply dead. */ -}} + {{- $port := regexFind ":[0-9]+$" $rest }} + {{- if $port }} + {{- $digits := trimPrefix ":" $port }} + {{- $n := atoi $digits }} + {{- if gt $n 65535 }} + {{- fail (printf "api.corsAllowedOrigins entry %q: %s is not a port — a browser refuses a URL above 65535." . $port) }} + {{- end }} + {{- if ne (printf "%d" $n) $digits }} + {{- fail (printf "api.corsAllowedOrigins entry %q: a browser writes the port as %q, not %q." . (printf "%d" $n) $digits) }} + {{- end }} + {{- if or (and (hasPrefix "https://" $entry) (eq $n 443)) (and (hasPrefix "http://" $entry) (eq $n 80)) }} + {{- fail (printf "api.corsAllowedOrigins entry %q: a browser omits the scheme's default port — drop the %q." . $port) }} + {{- end }} + {{- end }} + {{- end }} + {{- end }} + - name: AISIX_CLOUD_CORS_ALLOWED_ORIGINS + value: {{ join "," . | quote }} + {{- end }} - name: AISIX_CLOUD_MASTER_KEY valueFrom: secretKeyRef: @@ -101,12 +256,12 @@ spec: # # 30 minutes, because a migration that REWRITES a table is # bounded by table size, not by a fixed bootstrap cost. The - # 0.9.0 widening of dpmgr_usage_events.request_id (uuid -> - # text) measured 6m21s on 10M rows — roughly what 30 days of - # retention holds at a sustained 4 req/s — and it scales - # linearly from there. The previous 5-minute budget would have - # turned that upgrade into the exact crash loop this probe - # exists to prevent. + # AISIX-Cloud#1288 widening of dpmgr_usage_events.request_id + # (uuid -> text) measured 6m21s on 10M rows — roughly what 30 + # days of retention holds at a sustained 4 req/s — and it + # scales linearly from there. The previous 5-minute budget + # would have turned that upgrade into the exact crash loop + # this probe exists to prevent. # # The cost is that a genuinely wedged cp-api goes unrestarted # for up to 30 minutes on FIRST start only; once the probe diff --git a/charts/aisix-cp/templates/api-service-metrics.yaml b/charts/aisix-cp/templates/api-service-metrics.yaml new file mode 100644 index 0000000..1405faa --- /dev/null +++ b/charts/aisix-cp/templates/api-service-metrics.yaml @@ -0,0 +1,22 @@ +{{- if .Values.api.metrics.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "aisix-cp.fullname" . }}-api-metrics + labels: + {{- include "aisix-cp.labels" . | nindent 4 }} + {{- include "aisix-cp.selectorLabels" (dict "root" . "component" "api") | nindent 4 }} + {{- with .Values.api.metrics.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: ClusterIP + ports: + - name: metrics + port: {{ .Values.api.metrics.service.port }} + targetPort: metrics + protocol: TCP + selector: + {{- include "aisix-cp.selectorLabels" (dict "root" . "component" "api") | nindent 4 }} +{{- end }} diff --git a/charts/aisix-cp/templates/api-servicemonitor.yaml b/charts/aisix-cp/templates/api-servicemonitor.yaml new file mode 100644 index 0000000..efae377 --- /dev/null +++ b/charts/aisix-cp/templates/api-servicemonitor.yaml @@ -0,0 +1,35 @@ +{{- if and .Values.api.metrics.enabled .Values.api.metrics.serviceMonitor.enabled }} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ include "aisix-cp.fullname" . }}-api + namespace: {{ .Values.api.metrics.serviceMonitor.namespace | default .Release.Namespace }} + labels: + {{- include "aisix-cp.labels" . | nindent 4 }} + {{- include "aisix-cp.selectorLabels" (dict "root" . "component" "api") | nindent 4 }} + {{- with .Values.api.metrics.serviceMonitor.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + namespaceSelector: + matchNames: + - {{ .Release.Namespace }} + selector: + matchLabels: + {{- include "aisix-cp.selectorLabels" (dict "root" . "component" "api") | nindent 6 }} + endpoints: + - port: metrics + path: /metrics + interval: {{ .Values.api.metrics.serviceMonitor.interval }} + {{- with .Values.api.metrics.serviceMonitor.scrapeTimeout }} + scrapeTimeout: {{ . }} + {{- end }} + {{- with .Values.api.metrics.serviceMonitor.relabelings }} + relabelings: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.api.metrics.serviceMonitor.metricRelabelings }} + metricRelabelings: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} diff --git a/charts/aisix-cp/values.yaml b/charts/aisix-cp/values.yaml index 3dd5486..ca3e561 100644 --- a/charts/aisix-cp/values.yaml +++ b/charts/aisix-cp/values.yaml @@ -17,6 +17,44 @@ api: ## 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 + ## externally reachable entry point, while these are internal operational + ## numbers (write volumes, resource cardinality). Nothing the Dashboard + ## 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. + enabled: true + ## 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 + port: 9090 + ## 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 + enabled: false + ## Namespace to create the ServiceMonitor in. Empty uses the release namespace + namespace: "" + ## Scrape interval + interval: 30s + ## Scrape timeout. Empty leaves the Prometheus default + scrapeTimeout: "" + ## Extra labels, e.g. the `release` label your Prometheus selects on + labels: {} + ## Scrape-time relabeling rules + relabelings: [] + ## Metric relabeling rules + metricRelabelings: [] resources: requests: cpu: 100m @@ -24,11 +62,12 @@ api: limits: cpu: "1" 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: runAsNonRoot: true - runAsUser: 10001 - runAsGroup: 101 - fsGroup: 101 seccompProfile: type: RuntimeDefault securityContext: @@ -48,6 +87,48 @@ api: ## 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). 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. + 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 @@ -90,11 +171,12 @@ dpm: limits: cpu: "1" 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: runAsNonRoot: true - runAsUser: 10001 - runAsGroup: 101 - fsGroup: 101 seccompProfile: type: RuntimeDefault securityContext: @@ -135,11 +217,12 @@ ui: limits: cpu: 500m 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: runAsNonRoot: true - runAsUser: 1001 - runAsGroup: 65533 - fsGroup: 65533 seccompProfile: type: RuntimeDefault securityContext: @@ -181,9 +264,30 @@ secrets: ## 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 +## 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 +## the UID itself (OpenShift's restricted-v2 SCC) turn those two off at install +## time: +## +## --set postgresql.primary.podSecurityContext.enabled=false \ +## --set postgresql.primary.containerSecurityContext.enabled=false +## +## Nothing else needs disabling. volumePermissions (the root chown init +## container) is already off by default, and the /dev/shm emptyDir needs no +## fsGroup — the kubelet mounts it world-writable. Turning shmVolume off would +## drop PostgreSQL back to the runtime's 64Mi /dev/shm and break parallel +## query with "could not resize shared memory segment". +## postgresql: builtin: true 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: registry: docker.io repository: api7/postgresql diff --git a/charts/aisix/Chart.yaml b/charts/aisix/Chart.yaml index b73fc82..64e3b99 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.2.0 -appVersion: "1.2.0" +version: 1.3.0 +appVersion: "1.3.0" keywords: - ai-gateway diff --git a/charts/aisix/README.md b/charts/aisix/README.md index de79241..2c5cce5 100644 --- a/charts/aisix/README.md +++ b/charts/aisix/README.md @@ -1,6 +1,6 @@ # aisix -![Version: 1.2.0](https://img.shields.io/badge/Version-1.2.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.2.0](https://img.shields.io/badge/AppVersion-1.2.0-informational?style=flat-square) +![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) Helm chart for the AISIX AI gateway data plane