diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3578aed..db7191e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -36,7 +36,8 @@ jobs: && ct lint \ --charts charts/api7 \ --charts charts/gateway \ - --charts charts/ingress-controller' + --charts charts/ingress-controller \ + --charts charts/aisix-cloud' - name: Verify Chart.lock files run: | diff --git a/AGENTS.md b/AGENTS.md index 5763ca6..2ba2c18 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,6 +8,7 @@ This repo (`api7/api7-helm-chart`) holds the Helm charts for API7 EE components | `gateway` (Data Plane) | `charts/gateway` | yes — appVersion is the EE version | | `api7-ingress-controller` | `charts/ingress-controller` | no — independent product version | | `developer-portal-fe` | `charts/developer-portal-fe` | no — independent product version | +| `aisix-cloud` (AISIX private-deployment control plane) | `charts/aisix-cloud` | no — independent product version (source of truth: `api7/AISIX-Cloud` `helm/aisix-cloud`) | ## Multi-line maintenance model diff --git a/charts/aisix-cloud/.helmignore b/charts/aisix-cloud/.helmignore new file mode 100644 index 0000000..6b8710a --- /dev/null +++ b/charts/aisix-cloud/.helmignore @@ -0,0 +1 @@ +.git diff --git a/charts/aisix-cloud/Chart.lock b/charts/aisix-cloud/Chart.lock new file mode 100644 index 0000000..cd7018e --- /dev/null +++ b/charts/aisix-cloud/Chart.lock @@ -0,0 +1,6 @@ +dependencies: +- name: postgresql + repository: https://charts.bitnami.com/bitnami + version: 12.12.10 +digest: sha256:3b8c03cf5b8742b8110494d29a4793f20920294a504bd85940d02bb00d0bc0ea +generated: "2026-05-13T18:33:38.3659398+08:00" diff --git a/charts/aisix-cloud/Chart.yaml b/charts/aisix-cloud/Chart.yaml new file mode 100644 index 0000000..7462896 --- /dev/null +++ b/charts/aisix-cloud/Chart.yaml @@ -0,0 +1,17 @@ +apiVersion: v2 +name: aisix-cloud +description: Helm chart for AISIX-Cloud control plane (cp-api, dp-manager, dashboard) +type: application +version: 0.1.0 +appVersion: "0.1.0" + +maintainers: + - name: API7 + email: support@api7.ai + url: https://api7.ai + +dependencies: + - name: postgresql + condition: postgresql.builtin + version: "12.12.10" + repository: "https://charts.bitnami.com/bitnami" diff --git a/charts/aisix-cloud/README.md b/charts/aisix-cloud/README.md new file mode 100644 index 0000000..d2d8564 --- /dev/null +++ b/charts/aisix-cloud/README.md @@ -0,0 +1,123 @@ +# aisix-cloud + +![Version: 0.1.0](https://img.shields.io/badge/Version-0.1.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.1.0](https://img.shields.io/badge/AppVersion-0.1.0-informational?style=flat-square) + +Helm chart for AISIX-Cloud control plane (cp-api, dp-manager, dashboard) + +## Maintainers + +| Name | Email | Url | +| ---- | ------ | --- | +| API7 | | | + +## Requirements + +| Repository | Name | Version | +|------------|------|---------| +| https://charts.bitnami.com/bitnami | postgresql | 12.12.10 | + +## Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| api.affinity | object | `{}` | | +| api.dpImage | string | `""` | | +| api.dpmgrBaseURL | string | `""` | | +| api.extraEnvVars | list | `[]` | | +| api.image.pullPolicy | string | `"IfNotPresent"` | | +| api.image.repository | string | `"ghcr.io/api7/aisix-cp-api"` | | +| api.image.tag | string | `""` | | +| api.nodeSelector | object | `{}` | | +| api.oauthEnabled | 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` | | +| 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.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 | `"ghcr.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"` | | +| 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.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 | `""` | | +| ui.affinity | object | `{}` | | +| ui.defaultLocale | string | `"en"` | | +| ui.extraEnvVars | list | `[]` | | +| ui.image.pullPolicy | string | `"IfNotPresent"` | | +| ui.image.repository | string | `"ghcr.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"` | | +| 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.port | int | `3000` | | +| ui.service.type | string | `"ClusterIP"` | | +| ui.tolerations | list | `[]` | | + diff --git a/charts/aisix-cloud/charts/postgresql-12.12.10.tgz b/charts/aisix-cloud/charts/postgresql-12.12.10.tgz new file mode 100644 index 0000000..89bcc97 Binary files /dev/null and b/charts/aisix-cloud/charts/postgresql-12.12.10.tgz differ diff --git a/charts/aisix-cloud/templates/NOTES.txt b/charts/aisix-cloud/templates/NOTES.txt new file mode 100644 index 0000000..167e878 --- /dev/null +++ b/charts/aisix-cloud/templates/NOTES.txt @@ -0,0 +1,37 @@ +AISIX-Cloud control plane has been deployed. + +Components: + - cp-api: {{ include "aisix-cloud.fullname" . }}-api:{{ .Values.api.service.port }} + - dp-manager: {{ include "aisix-cloud.fullname" . }}-dpm:{{ .Values.dpm.service.port }} + - dashboard: {{ include "aisix-cloud.fullname" . }}-ui:{{ .Values.ui.service.port }} + - PostgreSQL: {{ include "aisix-cloud.pgHost" . }}:{{ include "aisix-cloud.pgPort" . }} + +Access the dashboard via cp-api (reverse proxy): + kubectl port-forward svc/{{ include "aisix-cloud.fullname" . }}-api {{ .Values.api.service.port }}:{{ .Values.api.service.port }} -n {{ .Release.Namespace }} + Then open http://localhost:{{ .Values.api.service.port }} + +Connect a data-plane (managed mode): +{{- if eq .Values.dpm.service.type "NodePort" }} + DPM is exposed via NodePort. + {{- if .Values.dpm.service.nodePort }} + NodePort: {{ .Values.dpm.service.nodePort }} + {{- else }} + Get the assigned port: + kubectl get svc/{{ include "aisix-cloud.fullname" . }}-dpm -n {{ .Release.Namespace }} -o jsonpath='{.spec.ports[0].nodePort}' + {{- end }} + + Run the DP container (replace and with actual values): + + docker run --rm \ + -e AISIX_CONFIG_PATH=/etc/aisix/config.managed.yaml \ + -e AISIX_MANAGED__CP_BASE_URL=https://: \ + -e AISIX_MANAGED__CP_ETCD_ENDPOINT=: \ + -e AISIX_MANAGED__CP_CERT_PEM='' \ + -e AISIX_MANAGED__CP_KEY_PEM='' \ + -e AISIX_MANAGED__CP_CA_PEM='' \ + -v aisix-mtls:/var/lib/aisix \ + {{ .Values.api.dpImage | default (printf "ghcr.io/api7/aisix:%s" .Chart.AppVersion) }} +{{- else }} + DPM is a ClusterIP service. Expose it via Ingress, LoadBalancer, or port-forward: + kubectl port-forward svc/{{ include "aisix-cloud.fullname" . }}-dpm {{ .Values.dpm.service.port }}:{{ .Values.dpm.service.port }} -n {{ .Release.Namespace }} +{{- end }} diff --git a/charts/aisix-cloud/templates/_helpers.tpl b/charts/aisix-cloud/templates/_helpers.tpl new file mode 100644 index 0000000..ce723c7 --- /dev/null +++ b/charts/aisix-cloud/templates/_helpers.tpl @@ -0,0 +1,201 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "aisix-cloud.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +*/}} +{{- define "aisix-cloud.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "aisix-cloud.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "aisix-cloud.labels" -}} +helm.sh/chart: {{ include "aisix-cloud.chart" . }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +app.kubernetes.io/part-of: aisix-cloud +{{- end }} + +{{/* +Selector labels for a component. +Usage: {{ include "aisix-cloud.selectorLabels" (dict "root" . "component" "api") }} +*/}} +{{- define "aisix-cloud.selectorLabels" -}} +app.kubernetes.io/name: {{ include "aisix-cloud.name" .root }} +app.kubernetes.io/instance: {{ .root.Release.Name }} +app.kubernetes.io/component: {{ .component }} +{{- end }} + +{{/* +PostgreSQL host — uses builtin subchart service name or external host. +*/}} +{{- define "aisix-cloud.pgHost" -}} +{{- if .Values.postgresql.builtin }} +{{- if .Values.postgresql.fullnameOverride }} +{{- .Values.postgresql.fullnameOverride }} +{{- else }} +{{- printf "%s-postgresql" .Release.Name }} +{{- end }} +{{- else }} +{{- required "externalDatabase.host is required when postgresql.builtin is false" .Values.externalDatabase.host }} +{{- end }} +{{- end }} + +{{/* +PostgreSQL port. +*/}} +{{- define "aisix-cloud.pgPort" -}} +{{- if .Values.postgresql.builtin }} +{{- .Values.postgresql.primary.service.ports.postgresql }} +{{- else }} +{{- .Values.externalDatabase.port }} +{{- end }} +{{- end }} + +{{/* +PostgreSQL username. +*/}} +{{- define "aisix-cloud.pgUser" -}} +{{- if .Values.postgresql.builtin }} +{{- if .Values.postgresql.auth.usePostgresUserForAppConnections }} +{{- "postgres" }} +{{- else }} +{{- .Values.postgresql.auth.username }} +{{- end }} +{{- else }} +{{- .Values.externalDatabase.username }} +{{- end }} +{{- end }} + +{{/* +PostgreSQL database name. +*/}} +{{- define "aisix-cloud.pgDatabase" -}} +{{- if .Values.postgresql.builtin }} +{{- .Values.postgresql.auth.database }} +{{- else }} +{{- .Values.externalDatabase.database }} +{{- end }} +{{- end }} + +{{/* +PostgreSQL SSL mode. +*/}} +{{- define "aisix-cloud.pgSSLMode" -}} +{{- if .Values.postgresql.builtin }} +{{- "disable" }} +{{- else }} +{{- .Values.externalDatabase.sslmode | default "disable" }} +{{- end }} +{{- end }} + +{{/* +Database URL constructed from postgresql config. +Uses $(PGPASSWORD) env var substitution for runtime secret injection. +*/}} +{{- define "aisix-cloud.databaseURL" -}} +postgres://{{ include "aisix-cloud.pgUser" . }}:$(PGPASSWORD)@{{ include "aisix-cloud.pgHost" . }}:{{ include "aisix-cloud.pgPort" . }}/{{ include "aisix-cloud.pgDatabase" . }}?sslmode={{ include "aisix-cloud.pgSSLMode" . }} +{{- end }} + +{{/* +Secret name for aisix-cloud secrets. +*/}} +{{- define "aisix-cloud.secretName" -}} +{{ include "aisix-cloud.fullname" . }}-secrets +{{- end }} + +{{/* +Name of the Secret containing the PostgreSQL password. +*/}} +{{- define "aisix-cloud.pgSecretName" -}} +{{- if .Values.postgresql.builtin }} +{{- if .Values.postgresql.auth.existingSecret }} +{{- .Values.postgresql.auth.existingSecret }} +{{- else if .Values.postgresql.fullnameOverride }} +{{- .Values.postgresql.fullnameOverride }} +{{- else }} +{{- printf "%s-postgresql" .Release.Name }} +{{- end }} +{{- else if .Values.externalDatabase.existingSecret }} +{{- .Values.externalDatabase.existingSecret }} +{{- else }} +{{- printf "%s-external-db" (include "aisix-cloud.fullname" .) }} +{{- end }} +{{- end }} + +{{/* +Secret key containing the PostgreSQL password for application connections. +*/}} +{{- define "aisix-cloud.pgPasswordSecretKey" -}} +{{- if and .Values.postgresql.builtin .Values.postgresql.auth.usePostgresUserForAppConnections }} +{{- "postgres-password" }} +{{- else }} +{{- "password" }} +{{- end }} +{{- end }} + +{{/* +ServiceAccount name. +*/}} +{{- define "aisix-cloud.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "aisix-cloud.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + +{{/* +Image pull secrets from global config. +*/}} +{{- define "aisix-cloud.imagePullSecrets" -}} +{{- with .Values.global.imagePullSecrets }} +imagePullSecrets: + {{- toYaml . | nindent 2 }} +{{- end }} +{{- end }} + +{{/* +Init container that blocks until PostgreSQL accepts connections. +Reuses the same PG image shipped by the chart so no extra pull is needed. +*/}} +{{- define "aisix-cloud.pgWaitInitContainer" -}} +- name: wait-for-pg + image: "{{ .Values.postgresql.image.registry }}/{{ .Values.postgresql.image.repository }}:{{ .Values.postgresql.image.tag }}" + imagePullPolicy: IfNotPresent + env: + - name: PGPASSWORD + valueFrom: + secretKeyRef: + name: {{ include "aisix-cloud.pgSecretName" . }} + key: {{ include "aisix-cloud.pgPasswordSecretKey" . }} + command: + - sh + - -c + - | + until pg_isready -h {{ include "aisix-cloud.pgHost" . }} -p {{ include "aisix-cloud.pgPort" . }} -U {{ include "aisix-cloud.pgUser" . }}; do + echo "waiting for postgresql..." + sleep 2 + done +{{- end }} diff --git a/charts/aisix-cloud/templates/api-deployment.yaml b/charts/aisix-cloud/templates/api-deployment.yaml new file mode 100644 index 0000000..788f34f --- /dev/null +++ b/charts/aisix-cloud/templates/api-deployment.yaml @@ -0,0 +1,98 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "aisix-cloud.fullname" . }}-api + labels: + {{- include "aisix-cloud.labels" . | nindent 4 }} + {{- include "aisix-cloud.selectorLabels" (dict "root" . "component" "api") | nindent 4 }} +spec: + replicas: {{ .Values.api.replicaCount }} + selector: + matchLabels: + {{- include "aisix-cloud.selectorLabels" (dict "root" . "component" "api") | nindent 6 }} + template: + metadata: + labels: + {{- include "aisix-cloud.labels" . | nindent 8 }} + {{- include "aisix-cloud.selectorLabels" (dict "root" . "component" "api") | nindent 8 }} + spec: + {{- include "aisix-cloud.imagePullSecrets" . | nindent 6 }} + serviceAccountName: {{ include "aisix-cloud.serviceAccountName" . }} + {{- with .Values.api.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + initContainers: + {{- include "aisix-cloud.pgWaitInitContainer" . | nindent 8 }} + containers: + - name: api + image: "{{ .Values.api.image.repository }}:{{ .Values.api.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.api.image.pullPolicy }} + {{- with .Values.api.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + ports: + - name: http + containerPort: 8080 + protocol: TCP + env: + - name: PGPASSWORD + valueFrom: + secretKeyRef: + name: {{ include "aisix-cloud.pgSecretName" . }} + key: {{ include "aisix-cloud.pgPasswordSecretKey" . }} + - name: AISIX_CLOUD_LISTEN + value: ":8080" + - name: AISIX_CLOUD_DATABASE_URL + value: {{ include "aisix-cloud.databaseURL" . }} + - name: AISIX_CLOUD_PUBLIC_BASE_URL + value: {{ .Values.api.publicBaseURL | quote }} + - name: AISIX_CLOUD_MASTER_KEY + valueFrom: + secretKeyRef: + name: {{ include "aisix-cloud.secretName" . }} + key: master-key + - name: AISIX_CLOUD_MASTER_KEY_ID + value: {{ .Values.secrets.masterKeyID | quote }} + - name: AISIX_CLOUD_DASHBOARD_URL + value: "http://{{ include "aisix-cloud.fullname" . }}-ui:{{ .Values.ui.service.port }}" + {{- if .Values.api.dpmgrBaseURL }} + - name: AISIX_CLOUD_DPMGR_BASE_URL + value: {{ .Values.api.dpmgrBaseURL | quote }} + {{- end }} + - name: AISIX_CLOUD_OAUTH_ENABLED + value: {{ .Values.api.oauthEnabled | quote }} + - name: AISIX_CLOUD_DP_IMAGE + value: {{ .Values.api.dpImage | default (printf "ghcr.io/api7/aisix:%s" .Chart.AppVersion) | quote }} + {{- with .Values.api.extraEnvVars }} + {{- toYaml . | nindent 12 }} + {{- end }} + livenessProbe: + httpGet: + path: /healthz + port: http + initialDelaySeconds: 10 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /healthz + port: http + initialDelaySeconds: 5 + periodSeconds: 5 + {{- with .Values.api.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.api.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.api.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.api.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/charts/aisix-cloud/templates/api-service.yaml b/charts/aisix-cloud/templates/api-service.yaml new file mode 100644 index 0000000..58a9c1c --- /dev/null +++ b/charts/aisix-cloud/templates/api-service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "aisix-cloud.fullname" . }}-api + labels: + {{- include "aisix-cloud.labels" . | nindent 4 }} + {{- include "aisix-cloud.selectorLabels" (dict "root" . "component" "api") | nindent 4 }} +spec: + type: {{ .Values.api.service.type }} + ports: + - port: {{ .Values.api.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "aisix-cloud.selectorLabels" (dict "root" . "component" "api") | nindent 4 }} diff --git a/charts/aisix-cloud/templates/dpm-deployment.yaml b/charts/aisix-cloud/templates/dpm-deployment.yaml new file mode 100644 index 0000000..75fbdc8 --- /dev/null +++ b/charts/aisix-cloud/templates/dpm-deployment.yaml @@ -0,0 +1,84 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "aisix-cloud.fullname" . }}-dpm + labels: + {{- include "aisix-cloud.labels" . | nindent 4 }} + {{- include "aisix-cloud.selectorLabels" (dict "root" . "component" "dpm") | nindent 4 }} +spec: + replicas: {{ .Values.dpm.replicaCount }} + selector: + matchLabels: + {{- include "aisix-cloud.selectorLabels" (dict "root" . "component" "dpm") | nindent 6 }} + template: + metadata: + labels: + {{- include "aisix-cloud.labels" . | nindent 8 }} + {{- include "aisix-cloud.selectorLabels" (dict "root" . "component" "dpm") | nindent 8 }} + spec: + {{- include "aisix-cloud.imagePullSecrets" . | nindent 6 }} + serviceAccountName: {{ include "aisix-cloud.serviceAccountName" . }} + {{- with .Values.dpm.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + initContainers: + {{- include "aisix-cloud.pgWaitInitContainer" . | nindent 8 }} + containers: + - name: dpm + image: "{{ .Values.dpm.image.repository }}:{{ .Values.dpm.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.dpm.image.pullPolicy }} + {{- with .Values.dpm.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + ports: + - name: tls + containerPort: 7944 + protocol: TCP + env: + - name: PGPASSWORD + valueFrom: + secretKeyRef: + name: {{ include "aisix-cloud.pgSecretName" . }} + key: {{ include "aisix-cloud.pgPasswordSecretKey" . }} + - name: AISIX_DPMGR_LISTEN + value: ":7944" + - name: AISIX_DPMGR_DATABASE_URL + value: {{ include "aisix-cloud.databaseURL" . }} + - name: AISIX_DPMGR_MASTER_KEY + valueFrom: + secretKeyRef: + name: {{ include "aisix-cloud.secretName" . }} + key: master-key + - name: AISIX_DPMGR_MASTER_KEY_ID + value: {{ .Values.secrets.masterKeyID | quote }} + {{- with .Values.dpm.extraEnvVars }} + {{- toYaml . | nindent 12 }} + {{- end }} + readinessProbe: + tcpSocket: + port: tls + initialDelaySeconds: 5 + periodSeconds: 5 + livenessProbe: + tcpSocket: + port: tls + initialDelaySeconds: 10 + periodSeconds: 10 + {{- with .Values.dpm.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.dpm.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.dpm.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.dpm.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/charts/aisix-cloud/templates/dpm-service.yaml b/charts/aisix-cloud/templates/dpm-service.yaml new file mode 100644 index 0000000..b3221ec --- /dev/null +++ b/charts/aisix-cloud/templates/dpm-service.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "aisix-cloud.fullname" . }}-dpm + labels: + {{- include "aisix-cloud.labels" . | nindent 4 }} + {{- include "aisix-cloud.selectorLabels" (dict "root" . "component" "dpm") | nindent 4 }} +spec: + type: {{ .Values.dpm.service.type }} + ports: + - port: {{ .Values.dpm.service.port }} + targetPort: tls + protocol: TCP + name: tls + {{- if and (eq .Values.dpm.service.type "NodePort") .Values.dpm.service.nodePort }} + nodePort: {{ .Values.dpm.service.nodePort }} + {{- end }} + selector: + {{- include "aisix-cloud.selectorLabels" (dict "root" . "component" "dpm") | nindent 4 }} diff --git a/charts/aisix-cloud/templates/external-db-secret.yaml b/charts/aisix-cloud/templates/external-db-secret.yaml new file mode 100644 index 0000000..fb9088b --- /dev/null +++ b/charts/aisix-cloud/templates/external-db-secret.yaml @@ -0,0 +1,11 @@ +{{- if and (not .Values.postgresql.builtin) (not .Values.externalDatabase.existingSecret) }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "aisix-cloud.fullname" . }}-external-db + labels: + {{- include "aisix-cloud.labels" . | nindent 4 }} +type: Opaque +data: + password: {{ required "externalDatabase.password is required when existingSecret is not set" .Values.externalDatabase.password | b64enc }} +{{- end }} diff --git a/charts/aisix-cloud/templates/secret.yaml b/charts/aisix-cloud/templates/secret.yaml new file mode 100644 index 0000000..00f5035 --- /dev/null +++ b/charts/aisix-cloud/templates/secret.yaml @@ -0,0 +1,37 @@ +{{/* +Secrets rendered from explicit values. No auto-generation — values must +be set explicitly to guarantee stability across re-deploys where helm +lookup is unavailable during server-side rendering (ArgoCD, Flux, etc.). +*/}} +{{- if or (eq .Values.secrets.masterKey "") (hasPrefix "CHANGE_ME" .Values.secrets.masterKey) }} + {{- fail "secrets.masterKey must be set to a real base64-encoded 32-byte key (generate with: openssl rand -base64 32)" }} +{{- end }} +{{- if or (eq .Values.secrets.betterAuthSecret "") (hasPrefix "CHANGE_ME" .Values.secrets.betterAuthSecret) }} + {{- fail "secrets.betterAuthSecret must be set to a real secret (generate with: openssl rand -base64 48)" }} +{{- end }} +{{/* +Hold builtin PostgreSQL credentials to the same standard as the +application secrets above: reject the documented placeholder so an +operator can't deploy a cluster-reachable database with a known +password. Skipped when an existingSecret injects the credentials. +*/}} +{{- if and .Values.postgresql.builtin (not .Values.postgresql.auth.existingSecret) }} + {{- 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) }} + {{- end }} + {{- end }} +{{- end }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "aisix-cloud.secretName" . }} + labels: + {{- include "aisix-cloud.labels" . | nindent 4 }} + annotations: + helm.sh/resource-policy: keep +type: Opaque +data: + master-key: {{ .Values.secrets.masterKey | b64enc }} + better-auth-secret: {{ .Values.secrets.betterAuthSecret | b64enc }} diff --git a/charts/aisix-cloud/templates/serviceaccount.yaml b/charts/aisix-cloud/templates/serviceaccount.yaml new file mode 100644 index 0000000..dda2447 --- /dev/null +++ b/charts/aisix-cloud/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "aisix-cloud.serviceAccountName" . }} + labels: + {{- include "aisix-cloud.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: false +{{- end }} diff --git a/charts/aisix-cloud/templates/ui-deployment.yaml b/charts/aisix-cloud/templates/ui-deployment.yaml new file mode 100644 index 0000000..a90c421 --- /dev/null +++ b/charts/aisix-cloud/templates/ui-deployment.yaml @@ -0,0 +1,84 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "aisix-cloud.fullname" . }}-ui + labels: + {{- include "aisix-cloud.labels" . | nindent 4 }} + {{- include "aisix-cloud.selectorLabels" (dict "root" . "component" "ui") | nindent 4 }} +spec: + replicas: {{ .Values.ui.replicaCount }} + selector: + matchLabels: + {{- include "aisix-cloud.selectorLabels" (dict "root" . "component" "ui") | nindent 6 }} + template: + metadata: + labels: + {{- include "aisix-cloud.labels" . | nindent 8 }} + {{- include "aisix-cloud.selectorLabels" (dict "root" . "component" "ui") | nindent 8 }} + spec: + {{- include "aisix-cloud.imagePullSecrets" . | nindent 6 }} + serviceAccountName: {{ include "aisix-cloud.serviceAccountName" . }} + {{- with .Values.ui.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: ui + image: "{{ .Values.ui.image.repository }}:{{ .Values.ui.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.ui.image.pullPolicy }} + {{- with .Values.ui.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + ports: + - name: http + containerPort: 3000 + protocol: TCP + env: + - name: PGPASSWORD + valueFrom: + secretKeyRef: + name: {{ include "aisix-cloud.pgSecretName" . }} + key: {{ include "aisix-cloud.pgPasswordSecretKey" . }} + - name: BETTER_AUTH_URL + value: {{ .Values.api.publicBaseURL | quote }} + - name: BETTER_AUTH_SECRET + valueFrom: + secretKeyRef: + name: {{ include "aisix-cloud.secretName" . }} + key: better-auth-secret + - name: AISIX_CLOUD_DATABASE_URL + value: {{ include "aisix-cloud.databaseURL" . }} + - name: AISIX_DASHBOARD_LOCALE + value: {{ .Values.ui.defaultLocale | quote }} + {{- with .Values.ui.extraEnvVars }} + {{- toYaml . | nindent 12 }} + {{- end }} + livenessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 10 + periodSeconds: 10 + readinessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 5 + periodSeconds: 5 + {{- with .Values.ui.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.ui.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.ui.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.ui.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/charts/aisix-cloud/templates/ui-service.yaml b/charts/aisix-cloud/templates/ui-service.yaml new file mode 100644 index 0000000..968ebfd --- /dev/null +++ b/charts/aisix-cloud/templates/ui-service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "aisix-cloud.fullname" . }}-ui + labels: + {{- include "aisix-cloud.labels" . | nindent 4 }} + {{- include "aisix-cloud.selectorLabels" (dict "root" . "component" "ui") | nindent 4 }} +spec: + type: {{ .Values.ui.service.type }} + ports: + - port: {{ .Values.ui.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "aisix-cloud.selectorLabels" (dict "root" . "component" "ui") | nindent 4 }} diff --git a/charts/aisix-cloud/values.yaml b/charts/aisix-cloud/values.yaml new file mode 100644 index 0000000..82d6567 --- /dev/null +++ b/charts/aisix-cloud/values.yaml @@ -0,0 +1,186 @@ +## @section Global parameters +global: + storageClass: "" + imagePullSecrets: [] + +## @section cp-api +api: + replicaCount: 1 + image: + repository: ghcr.io/api7/aisix-cp-api + pullPolicy: IfNotPresent + tag: "" # empty -> .Chart.AppVersion + service: + type: ClusterIP + port: 8080 + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: "1" + memory: 512Mi + podSecurityContext: + runAsNonRoot: true + runAsUser: 10001 + runAsGroup: 101 + fsGroup: 101 + seccompProfile: + type: RuntimeDefault + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + nodeSelector: {} + tolerations: [] + affinity: {} + extraEnvVars: [] + publicBaseURL: "http://localhost:8080" + dpmgrBaseURL: "" + oauthEnabled: false + dpImage: "" # empty -> ghcr.io/api7/aisix: + +## @section dp-manager +dpm: + replicaCount: 1 + image: + repository: ghcr.io/api7/aisix-cp-dpm + pullPolicy: IfNotPresent + tag: "" # empty -> .Chart.AppVersion + service: + type: ClusterIP + port: 7944 + nodePort: "" + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: "1" + memory: 512Mi + podSecurityContext: + runAsNonRoot: true + runAsUser: 10001 + runAsGroup: 101 + fsGroup: 101 + seccompProfile: + type: RuntimeDefault + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + nodeSelector: {} + tolerations: [] + affinity: {} + extraEnvVars: [] + +## @section dashboard (Next.js) +ui: + replicaCount: 1 + # 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: + repository: ghcr.io/api7/aisix-cp-ui + pullPolicy: IfNotPresent + tag: "" # empty -> .Chart.AppVersion + service: + type: ClusterIP + port: 3000 + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 500m + memory: 256Mi + podSecurityContext: + runAsNonRoot: true + runAsUser: 1001 + runAsGroup: 65533 + fsGroup: 65533 + seccompProfile: + type: RuntimeDefault + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + nodeSelector: {} + tolerations: [] + affinity: {} + extraEnvVars: [] + +## @section ServiceAccount +serviceAccount: + create: true + name: "" + 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 +secrets: + masterKey: "CHANGE_ME_GENERATE_WITH_openssl_rand_-base64_32" + masterKeyID: "env:default" + 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. +postgresql: + builtin: true + fullnameOverride: "" + image: + registry: docker.io + repository: api7/postgresql + tag: 15.4.0-debian-11-r45 + primary: + persistence: + size: 8Gi + service: + ports: + 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. + usePostgresUserForAppConnections: true + 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. + 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 -base64 24) + ## or use existingSecret above. + password: changeme + postgresPassword: changeme + database: aisix_cloud + +## @section External Database +## Only used when postgresql.builtin is false. +externalDatabase: + host: "" + port: 5432 + username: aisix + database: aisix_cloud + ## Name of an existing Secret containing the database password (key: "password"). + existingSecret: "" + ## If existingSecret is empty, this password is used directly. + password: "" + sslmode: disable