From 01481be5117e4f9a4e1b7b294e7afb27351d71ac Mon Sep 17 00:00:00 2001 From: Cita Date: Mon, 30 Mar 2026 17:17:24 +0800 Subject: [PATCH] [codex] Add digest-safe k3s update-center assets (#326) * Add digest-safe k3s update-center assets * Harden k3s update-center assets * Trim k3s env secret names safely --- deploy/k3s/chart/Chart.yaml | 6 ++ deploy/k3s/chart/templates/_helpers.tpl | 27 +++++++ deploy/k3s/chart/templates/deployment.yaml | 59 +++++++++++++++ deploy/k3s/chart/templates/secret.yaml | 23 ++++++ deploy/k3s/chart/templates/service.yaml | 18 +++++ deploy/k3s/chart/values.yaml | 33 +++++++++ deploy/k3s/metapi-deploy-helper.yaml | 37 +++++++++- docs/k3s-update-center.md | 86 +++++++++++++++++++++- src/server/update-helper/k3sAssets.test.ts | 67 +++++++++++++++++ 9 files changed, 353 insertions(+), 3 deletions(-) create mode 100644 deploy/k3s/chart/Chart.yaml create mode 100644 deploy/k3s/chart/templates/_helpers.tpl create mode 100644 deploy/k3s/chart/templates/deployment.yaml create mode 100644 deploy/k3s/chart/templates/secret.yaml create mode 100644 deploy/k3s/chart/templates/service.yaml create mode 100644 deploy/k3s/chart/values.yaml create mode 100644 src/server/update-helper/k3sAssets.test.ts diff --git a/deploy/k3s/chart/Chart.yaml b/deploy/k3s/chart/Chart.yaml new file mode 100644 index 0000000..f72db67 --- /dev/null +++ b/deploy/k3s/chart/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: metapi +description: Minimal metapi chart for digest-aware k3s update-center setups +type: application +version: 0.1.0 +appVersion: "1.2.3" diff --git a/deploy/k3s/chart/templates/_helpers.tpl b/deploy/k3s/chart/templates/_helpers.tpl new file mode 100644 index 0000000..1fb9b21 --- /dev/null +++ b/deploy/k3s/chart/templates/_helpers.tpl @@ -0,0 +1,27 @@ +{{- define "metapi.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{- define "metapi.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := include "metapi.name" . -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{- define "metapi.envSecretName" -}} +{{- printf "%s-env" (include "metapi.fullname" .) | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{- define "metapi.labels" -}} +app.kubernetes.io/name: {{ include "metapi.name" . }} +helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end -}} diff --git a/deploy/k3s/chart/templates/deployment.yaml b/deploy/k3s/chart/templates/deployment.yaml new file mode 100644 index 0000000..5e07a11 --- /dev/null +++ b/deploy/k3s/chart/templates/deployment.yaml @@ -0,0 +1,59 @@ +{{- if and .Values.persistence.hostPath (gt (int .Values.replicaCount) 1) -}} +{{- fail "deploy/k3s/chart uses node-local hostPath storage; keep replicaCount at 1 or switch persistence.hostPath to shared storage before scaling" -}} +{{- end }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "metapi.fullname" . }} + labels: + {{- include "metapi.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + revisionHistoryLimit: 2 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: {{ .Values.deployment.strategy.maxSurge }} + maxUnavailable: {{ .Values.deployment.strategy.maxUnavailable }} + selector: + matchLabels: + app.kubernetes.io/name: {{ include "metapi.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + template: + metadata: + labels: + app.kubernetes.io/name: {{ include "metapi.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + annotations: + metapi/image-tag: {{ .Values.image.tag | quote }} + metapi/image-digest: {{ .Values.image.digest | quote }} + checksum/env-secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum | quote }} + spec: + containers: + - name: metapi + image: "{{ .Values.image.repository }}{{ if .Values.image.digest }}@{{ .Values.image.digest }}{{ else }}:{{ .Values.image.tag }}{{ end }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + envFrom: + - secretRef: + name: {{ include "metapi.envSecretName" . }} + ports: + - name: http + containerPort: 4000 + readinessProbe: + tcpSocket: + port: http + initialDelaySeconds: 10 + periodSeconds: 10 + livenessProbe: + tcpSocket: + port: http + initialDelaySeconds: 30 + periodSeconds: 20 + volumeMounts: + - name: metapi-data + mountPath: /app/data + volumes: + - name: metapi-data + hostPath: + path: {{ .Values.persistence.hostPath }} + type: DirectoryOrCreate diff --git a/deploy/k3s/chart/templates/secret.yaml b/deploy/k3s/chart/templates/secret.yaml new file mode 100644 index 0000000..56d8007 --- /dev/null +++ b/deploy/k3s/chart/templates/secret.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "metapi.envSecretName" . }} + labels: + {{- include "metapi.labels" . | nindent 4 }} +type: Opaque +stringData: + AUTH_TOKEN: {{ required "env.authToken is required" .Values.env.authToken | quote }} + PROXY_TOKEN: {{ required "env.proxyToken is required" .Values.env.proxyToken | quote }} + DB_TYPE: {{ .Values.env.dbType | quote }} + {{- if eq (lower (toString .Values.env.dbType)) "sqlite" }} + DB_URL: {{ default "./data/hub.db" .Values.env.dbUrl | quote }} + {{- else }} + DB_URL: {{ required "env.dbUrl is required when env.dbType is mysql/postgres" .Values.env.dbUrl | quote }} + {{- end }} + DB_SSL: {{ .Values.env.dbSsl | quote }} + DATA_DIR: {{ .Values.env.dataDir | quote }} + PORT: {{ .Values.env.port | quote }} + TZ: {{ .Values.env.tz | quote }} + CHECKIN_CRON: {{ .Values.env.checkinCron | quote }} + BALANCE_REFRESH_CRON: {{ .Values.env.balanceRefreshCron | quote }} + DEPLOY_HELPER_TOKEN: {{ .Values.env.deployHelperToken | quote }} diff --git a/deploy/k3s/chart/templates/service.yaml b/deploy/k3s/chart/templates/service.yaml new file mode 100644 index 0000000..a966a59 --- /dev/null +++ b/deploy/k3s/chart/templates/service.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "metapi.fullname" . }} + labels: + {{- include "metapi.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + selector: + app.kubernetes.io/name: {{ include "metapi.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + ports: + - name: http + port: {{ .Values.service.port }} + targetPort: http + {{- if and (or (eq .Values.service.type "NodePort") (eq .Values.service.type "LoadBalancer")) .Values.service.nodePort }} + nodePort: {{ .Values.service.nodePort }} + {{- end }} diff --git a/deploy/k3s/chart/values.yaml b/deploy/k3s/chart/values.yaml new file mode 100644 index 0000000..2c23e37 --- /dev/null +++ b/deploy/k3s/chart/values.yaml @@ -0,0 +1,33 @@ +replicaCount: 1 + +image: + repository: 1467078763/metapi + tag: latest + digest: "" + pullPolicy: Always + +service: + type: NodePort + port: 4000 + nodePort: 30400 + +deployment: + strategy: + maxSurge: 1 + maxUnavailable: 0 + +persistence: + hostPath: /opt/1panel/docker/compose/metapihub/data + +env: + authToken: "" + proxyToken: "" + dbType: postgres + dbUrl: "" + dbSsl: "false" + dataDir: /app/data + port: "4000" + tz: Asia/Shanghai + checkinCron: "0 8 * * *" + balanceRefreshCron: "0 * * * *" + deployHelperToken: "" diff --git a/deploy/k3s/metapi-deploy-helper.yaml b/deploy/k3s/metapi-deploy-helper.yaml index 531f6a4..a83950f 100644 --- a/deploy/k3s/metapi-deploy-helper.yaml +++ b/deploy/k3s/metapi-deploy-helper.yaml @@ -47,7 +47,33 @@ spec: - name: helper image: 1467078763/metapi:latest imagePullPolicy: Always - command: ["npm", "run", "start:deploy-helper"] + command: ["/bin/sh", "-lc"] + args: + - | + TOKEN="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" + NAMESPACE="$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)" + cat >/tmp/kubeconfig < { + it('ships a digest-aware example chart for update-center users', () => { + const requiredFiles = [ + 'deploy/k3s/chart/Chart.yaml', + 'deploy/k3s/chart/values.yaml', + 'deploy/k3s/chart/templates/_helpers.tpl', + 'deploy/k3s/chart/templates/deployment.yaml', + 'deploy/k3s/chart/templates/secret.yaml', + 'deploy/k3s/chart/templates/service.yaml', + ]; + + for (const filePath of requiredFiles) { + expect(existsSync(resolve(process.cwd(), filePath)), filePath).toBe(true); + } + + const values = readRepoFile('deploy/k3s/chart/values.yaml'); + const deploymentTemplate = readRepoFile('deploy/k3s/chart/templates/deployment.yaml'); + const serviceTemplate = readRepoFile('deploy/k3s/chart/templates/service.yaml'); + const helpersTemplate = readRepoFile('deploy/k3s/chart/templates/_helpers.tpl'); + const normalizedDeploymentTemplate = normalizeWhitespace(deploymentTemplate); + const normalizedServiceTemplate = normalizeWhitespace(serviceTemplate); + + expect(values).toContain('digest:'); + expect(values).toContain('pullPolicy: Always'); + expect(deploymentTemplate).toMatch(/\.Values\.image\.digest/); + expect(normalizedDeploymentTemplate).toContain('{{ if .Values.image.digest }}@{{ .Values.image.digest }}{{ else }}:{{ .Values.image.tag }}{{ end }}'); + expect(deploymentTemplate).toMatch(/metapi\/image-digest:\s*\{\{\s*\.Values\.image\.digest\s*\|\s*quote\s*\}\}/); + expect(deploymentTemplate).toMatch(/checksum\/env-secret:/); + expect(deploymentTemplate).toMatch(/replicaCount at 1 or switch persistence\.hostPath to shared storage before scaling/); + expect(normalizedServiceTemplate).toContain('{{- if and (or (eq .Values.service.type "NodePort") (eq .Values.service.type "LoadBalancer")) .Values.service.nodePort }}'); + expect(helpersTemplate).toContain('define "metapi.envSecretName"'); + expect(helpersTemplate).toContain('printf "%s-env"'); + expect(deploymentTemplate).toContain('include "metapi.envSecretName"'); + expect(readRepoFile('deploy/k3s/chart/templates/secret.yaml')).toContain('include "metapi.envSecretName"'); + }); + + it('keeps the helper manifest on a pull policy that can pick up fresh latest tags', () => { + const helperManifest = readRepoFile('deploy/k3s/metapi-deploy-helper.yaml'); + + expect(helperManifest).toContain('imagePullPolicy: Always'); + expect(helperManifest).toContain('mountPath: /opt/metapi-k3s/chart'); + expect(helperManifest).toContain('readOnly: true'); + expect(helperManifest).toContain('path: /opt/metapi-k3s/chart'); + }); + + it('documents the local chart path and digest requirement for new k3s users', () => { + const docs = readRepoFile('docs/k3s-update-center.md'); + + expect(docs).toContain('deploy/k3s/chart'); + expect(docs).toContain('/opt/metapi-k3s/chart'); + expect(docs).toContain('image.digest'); + expect(docs).toContain('imagePullPolicy: Always'); + expect(docs).toContain('repository@digest'); + }); +});