Merge pull request #1 from LHB6540/fix/pr445-followup-for-author

fix(helm): 接续完善 #445 的依赖、GitOps 与 Ingress 配置
This commit is contained in:
Jangrui 2026-07-21 00:19:04 +08:00 committed by GitHub
commit 512fc1be00
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 1501 additions and 161 deletions

View file

@ -4,6 +4,8 @@ on:
pull_request:
paths:
- charts/skillhub/**
- .github/workflows/pr-helm-chart.yml
- .github/workflows/publish-chart.yml
types:
- opened
- synchronize
@ -30,17 +32,22 @@ jobs:
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Helm
uses: azure/setup-helm@v4
with:
version: latest
version: v3.19.0
- name: Build dependencies
run: helm dependency build .
- name: Lint chart
run: helm lint .
run: helm lint --strict . -f tests/test-values.yaml
- name: Validate configuration contracts
run: bash tests/configuration-contracts.sh
- name: Validate chart metadata
run: |
@ -91,7 +98,8 @@ jobs:
description: Ingress + TLS + cert-manager
args: >-
--set ingress.enabled=true
--set ingress.tls.enabled=true
--set-json 'ingress.hosts=[{"host":"skills.example.com","paths":[{"path":"/","pathType":"Prefix"}]}]'
--set-json 'ingress.tls=[{"hosts":["skills.example.com"],"secretName":"skills-tls"}]'
--set ingress.certManager.enabled=true
- name: s3-storage
description: S3 存储
@ -114,6 +122,7 @@ jobs:
--set server.autoscaling.enabled=true
--set web.autoscaling.enabled=true
--set scanner.autoscaling.enabled=true
--set server.storage.accessMode=ReadWriteMany
--set server.podDisruptionBudget.enabled=true
--set web.podDisruptionBudget.enabled=true
--set scanner.podDisruptionBudget.enabled=true
@ -121,11 +130,13 @@ jobs:
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Helm
uses: azure/setup-helm@v4
with:
version: latest
version: v3.19.0
- name: Build dependencies
run: helm dependency build .
@ -133,7 +144,7 @@ jobs:
- name: Render template - ${{ matrix.scenario.name }}
run: |
echo "## ${{ matrix.scenario.description }}"
helm template test-release . ${{ matrix.scenario.args }} > rendered.yaml
helm template test-release . -f tests/test-values.yaml ${{ matrix.scenario.args }} > rendered.yaml
echo "✅ Template rendered successfully"
- name: Validate resources
@ -145,6 +156,25 @@ jobs:
exit 1
fi
- name: Validate default dependency wiring
if: ${{ matrix.scenario.name == 'bitnami-default' }}
run: |
helm template test-release . -f tests/test-values.yaml --show-only templates/server-deployment.yaml > server.yaml
grep -Fq 'value: "test-release-postgresql"' server.yaml
grep -Fq 'value: "test-release-redis-master"' server.yaml
grep -Fq 'name: test-release-postgresql' server.yaml
grep -Fq 'name: test-release-redis' server.yaml
grep -Fq 'key: password' server.yaml
grep -Fq 'key: redis-password' server.yaml
if grep -Fq 'test-release-skillhub-postgresql' server.yaml; then
echo 'ERROR: Server references a non-existent PostgreSQL service'
exit 1
fi
if grep -Fq 'test-release-skillhub-redis' server.yaml; then
echo 'ERROR: Server references a non-existent Redis service'
exit 1
fi
- name: Schema validation (kubeconform)
uses: docker://ghcr.io/yannh/kubeconform:latest
with:

View file

@ -4,6 +4,11 @@ on:
release:
types: [published]
workflow_dispatch:
inputs:
version:
description: Chart and application version (for example, 0.2.13)
required: true
type: string
concurrency:
group: publish-chart-${{ github.ref }}
@ -15,6 +20,11 @@ permissions:
jobs:
release:
if: >-
github.event_name == 'workflow_dispatch' ||
startsWith(github.ref_name, 'v') ||
startsWith(github.ref_name, 'chart-v') ||
startsWith(github.ref_name, 'helm-v')
runs-on: ubuntu-latest
defaults:
run:
@ -23,11 +33,13 @@ jobs:
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Helm
uses: azure/setup-helm@v4
with:
version: latest
version: v3.19.0
- name: Verify dependencies
run: helm dependency build .
@ -38,17 +50,24 @@ jobs:
- name: Parse version from tag
id: ver
run: |
REF="${{ github.ref_name }}"
# 兼容 v0.2.9、chart-v0.2.9、helm-v0.2.9 三种标签格式
if [[ "$REF" =~ ^(helm|chart)-v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
VER="${{ inputs.version }}"
elif [[ "${{ github.ref_name }}" =~ ^(helm|chart)-v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
VER="${BASH_REMATCH[2]}"
elif [[ "${{ github.ref_name }}" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
VER="${BASH_REMATCH[1]}"
else
VER="${REF#v}"
echo "ERROR: Unsupported release tag: ${{ github.ref_name }}"
exit 1
fi
if [[ ! "$VER" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "ERROR: Version must use MAJOR.MINOR.PATCH format: $VER"
exit 1
fi
echo "version=$VER" >> "$GITHUB_OUTPUT"
- name: Lint chart
run: helm lint .
run: helm lint . -f tests/test-values.yaml
- name: Package and push
run: |

View file

@ -19,3 +19,6 @@ CLAUDE.md
# CI
.github/
# Source-only contract tests
tests/

View file

@ -2,13 +2,14 @@ apiVersion: v2
name: skillhub
description: Self-hosted, open-source agent skill registry for enterprises.
type: application
version: 0.3.0
appVersion: 0.3.0
version: 0.1.0
appVersion: 0.2.13
keywords:
- skillhub
- ai
- skills
home: https://github.com/iflytek/skillhub
icon: https://raw.githubusercontent.com/iflytek/skillhub/main/skillhub-logo.svg
sources:
- https://github.com/iflytek/skillhub

View file

@ -7,7 +7,7 @@
- **微服务架构**ServerSpring Boot、WebNginx、Scanner 分离部署
- **高可用**:支持 HPA 自动扩缩容、PDB Pod 中断预算
- **数据层**:使用 Bitnami PostgreSQL/Redis支持主从复制、哨兵模式
- **安全**TLS 证书管理、Secret 密码保护NetworkPolicy
- **安全**TLS 证书管理、Secret 密码保护Bitnami 数据组件默认提供 NetworkPolicy
- **可观测性**:内置 Prometheus metrics exporter
## 快速开始
@ -20,19 +20,47 @@
### 安装
先创建受保护的 `values-production.yaml`。以下值必须替换为实际随机强密码:
```yaml
secrets:
allowAutoGenerated: false
bootstrapAdminPassword: "<固定管理员密码>"
downloadAnonCookieSecret: "<至少32字符的固定随机值>"
postgresql:
auth:
postgresPassword: "<固定PostgreSQL管理员密码>"
password: "<固定skillhub用户密码>"
redis:
auth:
password: "<固定Redis密码>"
```
```bash
kubectl create namespace skillhub
helm -n skillhub upgrade -i skillhub ./charts/skillhub \
--set bootstrapAdmin.password=your-secure-password
-f values-production.yaml \
--set publicBaseUrl=https://skills.example.com
```
未显式设置 `deviceAuthVerificationUri`Chart 使用
`<publicBaseUrl>/cli/auth`。所有 values 会先经过 `values.schema.json` 和跨字段校验,
无效的组件、Ingress、HPA 与存储组合会在安装前失败。
> **Ingress values 迁移:** 当前版本只支持结构化的 `ingress.hosts[]`
> `ingress.tls[]`。旧的 `ingress.host``ingress.tls.enabled`
> `ingress.tls.secretName` 不再接受,升级前必须改成本文 Ingress 示例中的数组结构。
### 高可用模式
```bash
helm -n skillhub upgrade -i skillhub ./charts/skillhub \
--set bootstrapAdmin.password=your-secure-password \
-f values-production.yaml \
--set postgresql.architecture=replication \
--set postgresql.auth.replicationPassword=your-replication-password \
--set redis.architecture=replication
```
@ -40,7 +68,7 @@ helm -n skillhub upgrade -i skillhub ./charts/skillhub \
```bash
helm -n skillhub upgrade -i skillhub ./charts/skillhub \
--set bootstrapAdmin.password=your-secure-password \
-f values-production.yaml \
--set postgresql.enabled=false \
--set redis.enabled=false \
--set externalDatabase.host=postgres.example.com \
@ -55,28 +83,57 @@ helm -n skillhub upgrade -i skillhub ./charts/skillhub \
### 使用 existingSecret
通过 `existingSecret` 引用已存在的 Secret 对象,避免在 values 中明文写入密码。该 Secret 必须包含以下 key
通过 `existingSecret` 引用已存在的 Secret 对象,避免在 values 中明文写入密码。
内置 PostgreSQL/Redis 使用各自的 Bitnami Secret不需要复制到该 Secret。
| Key | 必填 | 说明 |
|-----|------|------|
| `spring-datasource-url` | 是 | JDBC 连接 URL |
| `spring-datasource-username` | 是 | 数据库用户名 |
| `spring-datasource-password` | 是 | 数据库密码 |
| `redis-password` | 是 | Redis 密码 |
| `redis-sentinel-password` | 否 | Redis Sentinel 密码sentinel 模式) |
| `spring-datasource-password` | 使用外部 PostgreSQL 时 | 数据库密码 |
| `redis-password` | 使用外部 Redis 时 | Redis 密码 |
| `redis-sentinel-password` | 使用外部 Sentinel 时 | Redis Sentinel 密码 |
| `bootstrap-admin-password` | 是 | 初始管理员密码 |
| `skillhub-download-anon-cookie-secret` | 是 | 至少 32 字符的匿名下载 Cookie 签名密钥 |
| `oauth2-github-client-id` | 否 | GitHub OAuth2 Client ID |
| `oauth2-github-client-secret` | 否 | GitHub OAuth2 Client Secret |
| `skill-scanner-llm-api-key` | 否 | Scanner LLM API Key |
| `skill-scanner-llm-base-url` | 否 | Scanner 自定义 LLM API 地址 |
| `skill-scanner-llm-model` | 否 | Scanner LLM 模型名称 |
| `s3-access-key` | 否 | S3 Access Key |
| `s3-secret-key` | 否 | S3 Secret Key |
| `skillhub-storage-s3-access-key` | 否 | S3 Access Key |
| `skillhub-storage-s3-secret-key` | 否 | S3 Secret Key |
```bash
helm -n skillhub upgrade -i skillhub ./charts/skillhub \
-f values-production.yaml \
--set existingSecret=my-custom-secret
```
### GitOps 稳定 Secret
Argo CD 等 GitOps 工具使用离线 `helm template`,无法通过 Helm `lookup` 读取集群
中已有的 Secret。Bitnami 子 Chart 和父 Chart 的空密码会在每次渲染时重新随机
生成。Chart 默认禁止自动生成并要求提供固定值:
```yaml
secrets:
allowAutoGenerated: false
bootstrapAdminPassword: "<固定管理员密码>"
downloadAnonCookieSecret: "<至少32字符的固定随机值>"
postgresql:
auth:
postgresPassword: "<固定PostgreSQL管理员密码>"
password: "<固定skillhub用户密码>"
# replication 架构还必须配置 replicationPassword
redis:
auth:
password: "<固定Redis密码>"
```
也可以为三个组件分别配置 `existingSecret``allowAutoGenerated=false` 不会生成
可预测密码,而是在任何随机密码缺失时终止渲染并指出具体配置项。敏感值应放在
受保护的 values、External Secrets、Sealed Secrets 或密钥注入插件中。
## 配置参考
### 副本数配置
@ -90,11 +147,16 @@ helm -n skillhub upgrade -i skillhub ./charts/skillhub \
```bash
# 差异化副本配置
helm -n skillhub upgrade -i skillhub ./charts/skillhub \
-f values-production.yaml \
--set server.replicaCount=3 \
--set server.storage.accessMode=ReadWriteMany \
--set web.replicaCount=2 \
--set scanner.replicaCount=1
```
本地存储运行多个 Server 副本时,必须显式设置 `ReadWriteMany`,并使用支持 RWX
的 StorageClass。无法提供 RWX 时应改用 S3。
### 服务配置
| 参数 | 描述 | 默认值 |
@ -105,6 +167,80 @@ helm -n skillhub upgrade -i skillhub ./charts/skillhub \
| `web.service.port` | Web 端口 | `80` |
| `scanner.service.port` | Scanner 端口 | `8000` |
### 私有镜像仓库
使用私有仓库时,需要分别覆盖 SkillHub 镜像、依赖等待镜像和 Bitnami 子 Chart
镜像。以下示例中的数据库镜像标签均为明确版本,不使用 `latest`
```yaml
global:
imagePullSecrets:
- private-registry
security:
allowInsecureImages: true
images:
registry: registry.example.com/library
tag: v0.2.13
pullPolicy: IfNotPresent
server:
dependencyWait:
image:
registry: registry.example.com
repository: library/busybox
tag: "1.37"
pullPolicy: IfNotPresent
imagePullSecrets:
- name: private-registry
web:
imagePullSecrets:
- name: private-registry
scanner:
imagePullSecrets:
- name: private-registry
postgresql:
image:
registry: registry.example.com
repository: library/postgresql
tag: 18.4.0
metrics:
image:
registry: registry.example.com
repository: library/postgres-exporter
tag: 0.20.1
redis:
image:
registry: registry.example.com
repository: library/redis
tag: 8.8.0
sentinel:
image:
registry: registry.example.com
repository: library/redis-sentinel
tag: 8.8.0
metrics:
image:
registry: registry.example.com
repository: library/redis-exporter
tag: 1.86.0
```
`global.security.allowInsecureImages` 是 Bitnami 对自定义镜像仓库和镜像名称的校验
开关,并不表示使用不安全的 HTTP 仓库。先在目标 namespace 创建拉取凭据:
```bash
kubectl create secret docker-registry private-registry \
-n skillhub \
--docker-server=registry.example.com \
--docker-username='<用户名>' \
--docker-password='<密码>'
```
### 数据库配置
| 参数 | 描述 | 默认值 |
@ -114,18 +250,73 @@ helm -n skillhub upgrade -i skillhub ./charts/skillhub \
| `redis.enabled` | 启用内置 Redis | `true` |
| `redis.architecture` | 架构模式 | `standalone` |
#### 数据库架构支持边界
以下内置数据库目标架构已完成独立 namespace 的全新安装和运行时验证:
| 数据组件 | 已验证架构 | 运行时验证 |
|----------|------------|------------|
| PostgreSQL | standalone | Server 连接、Flyway 和应用启动 |
| PostgreSQL | replication | 1 Primary + 2 Read Replicas两个副本均处于 recovery流复制状态为 `streaming` |
| Redis | standalone | Server 读写和应用启动 |
| Redis | replication | 1 Master + 2 Replicas角色和数据复制正常 |
| Redis | replication + Sentinel | 3 个 Sentinel 节点 master 视图一致Server 可通过 Sentinel 读写 |
上述支持表示 Chart 能够全新部署目标架构,并为 SkillHub 配置正确的写节点或
Sentinel 地址。Chart **不负责数据库架构切换时的数据迁移**,也不承诺仅修改
`architecture``sentinel.enabled` 就能保留已有数据。已有数据的 PostgreSQL
standalone → replication、Redis standalone/replication → Sentinel 等切换,必须由
运维人员在 Chart 之外完成备份、恢复、PVC 复用或其他迁移方案。
### Redis Sentinel
内置 Sentinel 使用 Bitnami Redis 的同一份密码同时保护 Redis 数据节点和
Sentinel。节点地址由副本数自动生成不需要手动配置
```bash
helm -n skillhub upgrade -i skillhub ./charts/skillhub \
-f values-production.yaml \
--set redis.architecture=replication \
--set redis.sentinel.enabled=true
```
外部 Sentinel 必须提供至少一个 `host:port` 节点。Redis 数据密码和 Sentinel
密码可以不同;使用 `existingSecret` 时分别对应 `redis-password`
`redis-sentinel-password`
```bash
helm -n skillhub upgrade -i skillhub ./charts/skillhub \
-f values-production.yaml \
--set redis.enabled=false \
--set externalRedis.password=redis-password \
--set externalRedis.sentinel.enabled=true \
--set externalRedis.sentinel.password=sentinel-password \
--set-json 'externalRedis.sentinel.nodes=["sentinel-0.example.com:26379","sentinel-1.example.com:26379"]'
```
### 存储配置
| 参数 | 描述 | 默认值 |
|------|------|--------|
| `server.storage.accessMode` | 访问模式ReadWriteOnce单副本或 ReadWriteMany多副本 | `""` |
| `server.storage.accessMode` | 留空时单副本使用 ReadWriteOnce多副本必须显式使用 ReadWriteMany | `""` |
| `server.storage.size` | PVC 大小 | `10Gi` |
| `server.storage.storageClassName` | StorageClass | `""` |
| `server.podSecurityContext.fsGroup` | Server 本地存储的可写组 ID应与镜像内 app 用户组一致 | `101` |
| `server.podSecurityContext.fsGroupChangePolicy` | kubelet 调整 PVC 组权限的策略 | `OnRootMismatch` |
本地 PVC 会覆盖镜像内预先设置的目录所有者。Chart 默认通过 Pod `fsGroup=101`
使 Server 的非 root `app` 用户可以创建和更新技能文件。使用自定义 Server 镜像且其
运行组 ID 不同时,必须同步覆盖 `server.podSecurityContext.fsGroup`
使用本地 `ReadWriteOnce` PVC 时Server Deployment 自动采用 `Recreate`,避免
滚动升级期间新旧 Pod 同时挂载非共享卷而触发 Multi-Attach。单副本升级会有短暂
停机;使用支持 RWX 的 `ReadWriteMany` 存储或启用 S3 时Chart 保留
`RollingUpdate`
```bash
# 默认使用本地 PVC
helm -n skillhub upgrade -i skillhub ./charts/skillhub \
--set bootstrapAdmin.password=your-secure-password
-f values-production.yaml
```
### S3 对象存储
@ -137,13 +328,17 @@ helm -n skillhub upgrade -i skillhub ./charts/skillhub \
| `s3.enabled` | 启用 S3 | `false` |
| `s3.bucket` | Bucket 名称 | `skillhub-storage` |
| `s3.endpoint` | S3 端点 | `""` |
| `s3.publicEndpoint` | S3 公网访问端点 | `""` |
| `s3.region` | 区域 | `us-east-1` |
| `s3.forcePathStyle` | 强制 path-style 访问 | `true` |
| `s3.disableChunkedEncoding` | 禁用 aws-chunked 编码 | `false` |
| `s3.autoCreateBucket` | 自动创建 Bucket | `false` |
| `s3.accessKey` | Access Key | `""` |
| `s3.secretKey` | Secret Key | `""` |
```bash
helm -n skillhub upgrade -i skillhub ./charts/skillhub \
--set bootstrapAdmin.password=your-secure-password \
-f values-production.yaml \
--set s3.enabled=true \
--set s3.bucket=your-bucket \
--set s3.endpoint=s3.amazonaws.com \
@ -156,27 +351,74 @@ helm -n skillhub upgrade -i skillhub ./charts/skillhub \
```bash
helm -n skillhub upgrade -i skillhub ./charts/skillhub \
-f values-production.yaml \
--set ingress.enabled=true \
--set ingress.host=skills.example.com \
--set ingress.tls.enabled=true \
--set-json 'ingress.hosts=[{"host":"skills.example.com","paths":[{"path":"/","pathType":"Prefix"}]}]' \
--set-json 'ingress.tls=[{"hosts":["skills.example.com"],"secretName":"skills-tls"}]' \
--set publicBaseUrl=https://skills.example.com \
--set ingress.certManager.enabled=true
```
配置非空 `ingress.tls` 或启用 `ingress.certManager`Chart 会自动将 Session Cookie
标记为 Secure。Ingress 要求 Server 和 Web Service 均保持启用。
`ingress.className` 和旧式 `kubernetes.io/ingress.class` annotation 均受支持,
可以任选其一,也可以同时输出。仅使用旧式 annotation 时将 `className` 留空:
```yaml
ingress:
enabled: true
className: ""
annotations:
kubernetes.io/ingress.class: alb
alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":6443}]'
```
`hosts` 是至少包含一个条目的对象数组。Chart 自动将 `/api` 转发给 Server
`hosts[].paths` 中的路径转发给 Web因此 `/api` 是保留路径。`tls` 同样是数组,
可为不同证书分别配置域名TLS 域名会写入 cert-manager Certificate SAN
```yaml
ingress:
hosts:
- host: skills.example.com
paths:
- path: /
pathType: Prefix
- host: skills.internal.example.com
paths:
- path: /
pathType: Prefix
tls:
- hosts:
- skills.example.com
- skills.internal.example.com
secretName: skills-tls
```
### 自动扩缩容
```bash
helm -n skillhub upgrade -i skillhub ./charts/skillhub \
-f values-production.yaml \
--set server.autoscaling.enabled=true \
--set server.autoscaling.minReplicas=2 \
--set server.autoscaling.maxReplicas=10
--set server.autoscaling.maxReplicas=10 \
--set server.storage.accessMode=ReadWriteMany
```
每个 HPA 至少需要一个非零 CPU 或内存利用率目标。本地存储的 Server HPA 同样
要求 RWX也可以启用 S3 来避免共享 PVC。
## 卸载
```bash
helm -n skillhub uninstall skillhub
```
Server 数据 PVC 带有 `helm.sh/resource-policy: keep`,卸载 release 后仍会保留,
需要确认数据不再使用后手动删除。
## 依赖
| 依赖 | 版本 |

View file

@ -70,10 +70,38 @@ app.kubernetes.io/component: scanner
app.kubernetes.io/component: scanner
{{- end }}
{{- /* Bitnami PostgreSQL subchart */}}
{{- define "skillhub.postgresql.fullname" -}}
{{- if .Values.postgresql.fullnameOverride -}}
{{- .Values.postgresql.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- $name := default "postgresql" .Values.postgresql.nameOverride -}}
{{- if contains $name .Release.Name -}}
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- end }}
{{- /* Bitnami Redis subchart */}}
{{- define "skillhub.redis.fullname" -}}
{{- if .Values.redis.fullnameOverride -}}
{{- .Values.redis.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- $name := default "redis" .Values.redis.nameOverride -}}
{{- if contains $name .Release.Name -}}
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- end }}
{{- /* PostgreSQL Host */}}
{{- define "skillhub.postgresql.host" -}}
{{- if .Values.postgresql.enabled -}}
{{- $prefix := printf "%s-postgresql" (include "skillhub.fullname" .) -}}
{{- $prefix := include "skillhub.postgresql.fullname" . -}}
{{- if eq .Values.postgresql.architecture "replication" -}}
{{- printf "%s-primary" $prefix -}}
{{- else -}}
@ -114,12 +142,21 @@ app.kubernetes.io/component: scanner
{{- /* PostgreSQL Secret Name */}}
{{- define "skillhub.postgresql.secretName" -}}
{{- if .Values.postgresql.enabled -}}
{{- printf "%s-postgresql" (include "skillhub.fullname" .) -}}
{{- .Values.postgresql.auth.existingSecret | default (include "skillhub.postgresql.fullname" .) -}}
{{- else -}}
{{- include "skillhub.secretName" . -}}
{{- end -}}
{{- end }}
{{- /* PostgreSQL Secret keypostgres 使使 */}}
{{- define "skillhub.postgresql.passwordKey" -}}
{{- if eq .Values.postgresql.auth.username "postgres" -}}
{{- .Values.postgresql.auth.secretKeys.adminPasswordKey | default "postgres-password" -}}
{{- else -}}
{{- .Values.postgresql.auth.secretKeys.userPasswordKey | default "password" -}}
{{- end -}}
{{- end }}
{{- /* PostgreSQL JDBC URL */}}
{{- define "skillhub.jdbcUrl" -}}
{{- if .Values.postgresql.enabled -}}
@ -135,20 +172,22 @@ app.kubernetes.io/component: scanner
{{- /* Redis Sentinel Redisson pod FQDN: {pod}.{headless-svc}.{ns}.svc.cluster.local */}}
{{- define "skillhub.redis.sentinel.nodes" -}}
{{- $prefix := printf "%s-redis-node" (include "skillhub.fullname" .) -}}
{{- $headless := printf "%s-redis-headless" (include "skillhub.fullname" .) -}}
{{- $port := include "skillhub.redis.port" . -}}
{{- $fullname := include "skillhub.redis.fullname" . -}}
{{- $prefix := printf "%s-node" $fullname -}}
{{- $headless := printf "%s-headless" $fullname -}}
{{- /* Headless Service DNS resolves directly to pod IPs, so use the container port. */ -}}
{{- $port := .Values.redis.sentinel.containerPorts.sentinel | default 26379 -}}
{{- $replicas := .Values.redis.replica.replicaCount | default 3 | int -}}
{{- $nodes := list -}}{{- range $i := until $replicas -}}{{- $nodes = append $nodes (printf "%s-%d.%s.%s.svc.cluster.local:%s" $prefix $i $headless $.Release.Namespace $port) -}}{{- end -}}{{- join "," $nodes -}}
{{- $nodes := list -}}{{- range $i := until $replicas -}}{{- $nodes = append $nodes (printf "%s-%d.%s.%s.svc.cluster.local:%v" $prefix $i $headless $.Release.Namespace $port) -}}{{- end -}}{{- join "," $nodes -}}
{{- end }}
{{- /* Redis Host */}}
{{- define "skillhub.redis.host" -}}
{{- if .Values.redis.enabled -}}
{{- if .Values.redis.sentinel.enabled -}}
{{- printf "%s-redis" (include "skillhub.fullname" .) -}}
{{- include "skillhub.redis.fullname" . -}}
{{- else -}}
{{- printf "%s-redis-master" (include "skillhub.fullname" .) -}}
{{- printf "%s-master" (include "skillhub.redis.fullname" .) -}}
{{- end -}}
{{- else -}}
{{- .Values.externalRedis.host -}}
@ -164,19 +203,29 @@ app.kubernetes.io/component: scanner
{{- print "6379" -}}
{{- end -}}
{{- else -}}
{{- if .Values.externalRedis.sentinel.enabled -}}
{{- $node := first .Values.externalRedis.sentinel.nodes -}}
{{- last (splitList ":" $node) -}}
{{- else -}}
{{- .Values.externalRedis.port | default 6379 | int -}}
{{- end -}}
{{- end -}}
{{- end }}
{{- /* Redis Password Secret Name */}}
{{- define "skillhub.redis.secretName" -}}
{{- if .Values.redis.enabled -}}
{{- printf "%s-redis" (include "skillhub.fullname" .) -}}
{{- .Values.redis.auth.existingSecret | default (include "skillhub.redis.fullname" .) -}}
{{- else -}}
{{- include "skillhub.secretName" . -}}
{{- end -}}
{{- end }}
{{- /* Redis Secret key */}}
{{- define "skillhub.redis.passwordKey" -}}
{{- .Values.redis.auth.existingSecretPasswordKey | default "redis-password" -}}
{{- end }}
{{- /* Secret */}}
{{- define "skillhub.secretName" -}}
{{- .Values.existingSecret | default (printf "%s-secret" (include "skillhub.fullname" .)) }}
@ -196,6 +245,11 @@ app.kubernetes.io/component: scanner
{{- if .Values.redis.enabled -}}
{{- include "skillhub.redis.host" . -}}
{{- else -}}
{{- if .Values.externalRedis.sentinel.enabled -}}
{{- $node := first .Values.externalRedis.sentinel.nodes -}}
{{- first (splitList ":" $node) -}}
{{- else -}}
{{- .Values.externalRedis.host -}}
{{- end -}}
{{- end -}}
{{- end }}

View file

@ -1,18 +1,24 @@
{{- $secretName := .Values.ingress.tls.secretName | default (printf "%s-tls" (include "skillhub.fullname" .)) }}
{{- if and .Values.ingress.enabled .Values.ingress.certManager.enabled }}
{{- range $index, $tls := .Values.ingress.tls }}
{{- if $index }}
---
{{- end }}
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: {{ $secretName }}-cert
name: {{ $tls.secretName }}-cert
labels:
{{- include "skillhub.labels" . | nindent 4 }}
{{- include "skillhub.labels" $ | nindent 4 }}
spec:
secretName: {{ $secretName }}
secretName: {{ $tls.secretName }}
duration: 2160h
renewBefore: 360h
dnsNames:
- {{ .Values.ingress.host }}
{{- range $tls.hosts }}
- {{ . | quote }}
{{- end }}
issuerRef:
name: {{ .Values.ingress.certManager.issuerName }}
kind: {{ .Values.ingress.certManager.issuerKind }}
name: {{ $.Values.ingress.certManager.issuerName | quote }}
kind: {{ $.Values.ingress.certManager.issuerKind | quote }}
{{- end }}
{{- end }}

View file

@ -9,33 +9,49 @@ metadata:
{{- include "skillhub.labels" . | nindent 4 }}
data:
# Redis 配置
redis-host: {{ include "skillhub.redis.host" . }}
redis-host: {{ include "skillhub.redis.host" . | quote }}
redis-port: {{ include "skillhub.redis.port" . | quote }}
# 存储路径
storage-base-path: /var/lib/skillhub/storage
storage-base-path: "/var/lib/skillhub/storage"
# 存储提供者: local | s3
skillhub-storage-provider: {{ if .Values.s3.enabled }}s3{{ else }}local{{ end }}
skillhub-storage-provider: {{ if .Values.s3.enabled }}"s3"{{ else }}"local"{{ end }}
{{- if .Values.s3.enabled }}
# S3 配置
s3-bucket: {{ .Values.s3.bucket }}
s3-endpoint: {{ .Values.s3.endpoint }}
s3-region: {{ .Values.s3.region }}
s3-bucket: {{ .Values.s3.bucket | quote }}
s3-endpoint: {{ .Values.s3.endpoint | quote }}
s3-public-endpoint: {{ .Values.s3.publicEndpoint | quote }}
s3-region: {{ .Values.s3.region | quote }}
s3-force-path-style: {{ .Values.s3.forcePathStyle | quote }}
s3-disable-chunked-encoding: {{ .Values.s3.disableChunkedEncoding | quote }}
s3-auto-create-bucket: {{ .Values.s3.autoCreateBucket | quote }}
s3-presign-expiry: {{ .Values.s3.presignExpiry | quote }}
{{- end }}
# 技能扫描器
skill-scanner-enabled: {{ .Values.scanner.enabled | quote }}
skill-scanner-url: http://{{ include "skillhub.fullname" . }}-scanner:{{ .Values.scanner.service.port }}
skill-scanner-mode: upload
skill-scanner-url: {{ printf "http://%s-scanner:%v" (include "skillhub.fullname" .) .Values.scanner.service.port | quote }}
skill-scanner-mode: "upload"
# Bootstrap 管理员
bootstrap-admin-enabled: {{ .Values.bootstrapAdmin.enabled | quote }}
bootstrap-admin-user-id: {{ .Values.bootstrapAdmin.userId }}
bootstrap-admin-username: {{ .Values.bootstrapAdmin.username }}
bootstrap-admin-display-name: {{ .Values.bootstrapAdmin.displayName }}
bootstrap-admin-email: {{ .Values.bootstrapAdmin.email }}
bootstrap-admin-user-id: {{ .Values.bootstrapAdmin.userId | quote }}
bootstrap-admin-username: {{ .Values.bootstrapAdmin.username | quote }}
bootstrap-admin-display-name: {{ .Values.bootstrapAdmin.displayName | quote }}
bootstrap-admin-email: {{ .Values.bootstrapAdmin.email | quote }}
# Session
session-cookie-secure: {{ .Values.session.cookieSecure | quote }}
session-cookie-secure: {{ or .Values.session.cookieSecure (not (empty .Values.ingress.tls)) .Values.ingress.certManager.enabled | quote }}
# Public URL and authentication
public-base-url: {{ .Values.publicBaseUrl | quote }}
{{- $deviceAuthVerificationUri := .Values.deviceAuthVerificationUri }}
{{- if and (not $deviceAuthVerificationUri) .Values.publicBaseUrl }}
{{- $deviceAuthVerificationUri = printf "%s/cli/auth" (trimSuffix "/" .Values.publicBaseUrl) }}
{{- end }}
device-auth-verification-uri: {{ $deviceAuthVerificationUri | quote }}
auth-direct-enabled: {{ .Values.auth.direct.enabled | quote }}
auth-direct-provider: {{ .Values.auth.direct.provider | quote }}
builtin-skills-enabled: {{ .Values.builtinSkills.enabled | quote }}

View file

@ -1,6 +1,10 @@
{{- range $name := list "server" "web" "scanner" }}
{{- $component := index $.Values $name }}
{{- if and (default true $component.enabled) $component.autoscaling.enabled }}
{{- $enabled := true }}
{{- if hasKey $component "enabled" }}
{{- $enabled = $component.enabled }}
{{- end }}
{{- if and $enabled $component.autoscaling.enabled }}
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler

View file

@ -1,39 +1,43 @@
{{- if .Values.ingress.enabled }}
{{- $secretName := .Values.ingress.tls.secretName | default (printf "%s-tls" (include "skillhub.fullname" .)) }}
{{- $hosts := .Values.ingress.hosts }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "skillhub.fullname" . }}
labels:
{{- include "skillhub.labels" . | nindent 4 }}
{{- if .Values.ingress.annotations }}
annotations:
{{- if .Values.ingress.annotations }}
{{- toYaml .Values.ingress.annotations | nindent 4 }}
{{- end }}
{{- end }}
spec:
ingressClassName: {{ .Values.ingress.className }}
{{- if or .Values.ingress.tls.enabled .Values.ingress.certManager.enabled }}
{{- if .Values.ingress.className }}
ingressClassName: {{ .Values.ingress.className | quote }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
- hosts:
- {{ .Values.ingress.host }}
secretName: {{ $secretName }}
{{- toYaml .Values.ingress.tls | nindent 4 }}
{{- end }}
rules:
- host: {{ .Values.ingress.host }}
{{- range $host := $hosts }}
- host: {{ $host.host | quote }}
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: {{ include "skillhub.fullname" . }}-server
name: {{ include "skillhub.fullname" $ }}-server
port:
number: {{ .Values.server.service.port }}
- path: /
pathType: Prefix
number: {{ $.Values.server.service.port }}
{{- range $path := $host.paths }}
- path: {{ $path.path | quote }}
pathType: {{ $path.pathType }}
backend:
service:
name: {{ include "skillhub.fullname" . }}-web
name: {{ include "skillhub.fullname" $ }}-web
port:
number: {{ .Values.web.service.port }}
number: {{ $.Values.web.service.port }}
{{- end }}
{{- end }}
{{- end }}

View file

@ -1,6 +1,10 @@
{{- range $name := list "server" "web" "scanner" }}
{{- $component := index $.Values $name }}
{{- if and (default true $component.enabled) $component.podDisruptionBudget.enabled }}
{{- $enabled := true }}
{{- if hasKey $component "enabled" }}
{{- $enabled = $component.enabled }}
{{- end }}
{{- if and $enabled $component.podDisruptionBudget.enabled }}
---
apiVersion: policy/v1
kind: PodDisruptionBudget

View file

@ -10,16 +10,12 @@ metadata:
spec:
{{- $accessMode := .Values.server.storage.accessMode }}
{{- if not $accessMode }}
{{- if or (gt (.Values.server.replicaCount | int) 1) (and .Values.server.autoscaling.enabled (gt (.Values.server.autoscaling.maxReplicas | int) 1)) }}
{{- $accessMode = "ReadWriteMany" }}
{{- else }}
{{- $accessMode = "ReadWriteOnce" }}
{{- end }}
{{- end }}
accessModes:
- {{ $accessMode }}
{{- if .Values.server.storage.storageClassName }}
storageClassName: {{ .Values.server.storage.storageClassName }}
storageClassName: {{ .Values.server.storage.storageClassName | quote }}
{{- end }}
resources:
requests:

View file

@ -17,7 +17,7 @@ spec:
labels:
{{- include "skillhub.scanner.selectorLabels" . | nindent 8 }}
annotations:
checksum/config: {{ toYaml (dict "scanner" .Values.scanner) | sha256sum }}
checksum/config: {{ toYaml (dict "scanner" .Values.scanner "secrets" .Values.secrets "existingSecret" .Values.existingSecret) | sha256sum }}
{{- range $key, $val := .Values.scanner.podAnnotations }}
{{ $key }}: {{ $val }}
{{- end }}
@ -41,6 +41,12 @@ spec:
name: {{ include "skillhub.secretName" . }}
key: skill-scanner-llm-api-key
optional: true
- name: SKILL_SCANNER_LLM_BASE_URL
valueFrom:
secretKeyRef:
name: {{ include "skillhub.secretName" . }}
key: skill-scanner-llm-base-url
optional: true
- name: SKILL_SCANNER_LLM_MODEL
valueFrom:
secretKeyRef:

View file

@ -5,10 +5,6 @@ SkillHub 应用 Secret
*/}}
{{- if not .Values.existingSecret }}
{{- $secretName := include "skillhub.secretName" . }}
{{- $postgresSecretName := include "skillhub.postgresql.secretName" . }}
{{- $redisSecretName := include "skillhub.redis.secretName" . }}
{{- $postgresSecret := (lookup "v1" "Secret" $.Release.Namespace $postgresSecretName) }}
{{- $redisSecret := (lookup "v1" "Secret" $.Release.Namespace $redisSecretName) }}
{{- $appSecret := (lookup "v1" "Secret" $.Release.Namespace $secretName) }}
apiVersion: v1
kind: Secret
@ -18,41 +14,18 @@ metadata:
{{- include "skillhub.labels" . | nindent 4 }}
type: Opaque
stringData:
# 数据库连接 URL
spring-datasource-url: {{ include "skillhub.jdbcUrl" . | quote }}
spring-datasource-username: {{ include "skillhub.postgresql.username" . | quote }}
# 数据库密码
# 优先级: lookup PG Secret → externalDatabase.password → postgresql.auth.password
{{- if and $postgresSecret (index $postgresSecret.data "password") }}
spring-datasource-password: {{ index $postgresSecret.data "password" | b64dec | quote }}
{{- else if not .Values.postgresql.enabled }}
{{- if not .Values.postgresql.enabled }}
# 外部数据库密码;内置 PostgreSQL 直接引用 Bitnami Secret
spring-datasource-password: {{ .Values.externalDatabase.password | quote }}
{{- else }}
spring-datasource-password: {{ .Values.secrets.springDatasourcePassword | default .Values.postgresql.auth.password | quote }}
{{- end }}
# Redis 密码
# 优先级: lookup Redis Secret → externalRedis.password → redis.auth.password
{{- if $redisSecret }}
{{- if index $redisSecret.data "redis-password" }}
redis-password: {{ index $redisSecret.data "redis-password" | b64dec | quote }}
{{- end }}
{{- else if not .Values.redis.enabled }}
{{- if not .Values.redis.enabled }}
# 外部 Redis 密码;内置 Redis 直接引用 Bitnami Secret
redis-password: {{ .Values.externalRedis.password | default "" | quote }}
{{- else if .Values.redis.auth.password }}
redis-password: {{ .Values.redis.auth.password | quote }}
{{- end }}
# Redis Sentinel 密码(仅 sentinel 模式下生效)
# 优先级: lookup Bitnami Secret → sentinelPassword → auth.password → externalRedis.password
{{- if and .Values.redis.enabled .Values.redis.sentinel.enabled }}
{{- if and $redisSecret (index $redisSecret.data "redis-sentinel-password") }}
redis-sentinel-password: {{ index $redisSecret.data "redis-sentinel-password" | b64dec | quote }}
{{- else }}
redis-sentinel-password: {{ .Values.redis.auth.sentinelPassword | default .Values.redis.auth.password | quote }}
{{- end }}
{{- else if and (not .Values.redis.enabled) .Values.externalRedis.sentinel.enabled }}
{{- if and (not .Values.redis.enabled) .Values.externalRedis.sentinel.enabled }}
# 外部 Sentinel 可使用独立密码
redis-sentinel-password: {{ .Values.externalRedis.sentinel.password | default .Values.externalRedis.password | default "" | quote }}
{{- end }}
# Bootstrap 管理员密码
@ -67,6 +40,17 @@ stringData:
{{- end }}
{{- end }}
bootstrap-admin-password: {{ $baPwd | quote }}
# 匿名下载限流 Cookie 签名密钥
{{- $downloadSecret := .Values.secrets.downloadAnonCookieSecret | default "" }}
{{- if and (not $downloadSecret) $appSecret }}
{{- $downloadSecret = index $appSecret.data "skillhub-download-anon-cookie-secret" | default "" | b64dec }}
{{- end }}
{{- if not $downloadSecret }}
{{- $downloadSecret = randAlphaNum 48 }}
{{- end }}
skillhub-download-anon-cookie-secret: {{ $downloadSecret | quote }}
# OAuth2 GitHub (optional)
{{- if .Values.secrets.oauth2GithubClientId }}
oauth2-github-client-id: {{ .Values.secrets.oauth2GithubClientId | quote }}
@ -79,15 +63,18 @@ stringData:
{{- if .Values.secrets.scannerLlmApiKey }}
skill-scanner-llm-api-key: {{ .Values.secrets.scannerLlmApiKey | quote }}
{{- end }}
{{- if .Values.secrets.scannerLlmBaseUrl }}
skill-scanner-llm-base-url: {{ .Values.secrets.scannerLlmBaseUrl | quote }}
{{- end }}
{{- if .Values.secrets.scannerLlmModel }}
skill-scanner-llm-model: {{ .Values.secrets.scannerLlmModel | quote }}
{{- end }}
# S3 配置 (optional)
{{- if .Values.s3.accessKey }}
s3-access-key: {{ .Values.s3.accessKey | quote }}
skillhub-storage-s3-access-key: {{ .Values.s3.accessKey | quote }}
{{- end }}
{{- if .Values.s3.secretKey }}
s3-secret-key: {{ .Values.s3.secretKey | quote }}
skillhub-storage-s3-secret-key: {{ .Values.s3.secretKey | quote }}
{{- end }}
{{- end }}

View file

@ -9,6 +9,12 @@ spec:
{{- if not .Values.server.autoscaling.enabled }}
replicas: {{ .Values.server.replicaCount }}
{{- end }}
strategy:
{{- if and (not .Values.s3.enabled) (ne .Values.server.storage.accessMode "ReadWriteMany") }}
type: Recreate
{{- else }}
type: RollingUpdate
{{- end }}
selector:
matchLabels:
{{- include "skillhub.server.selectorLabels" . | nindent 6 }}
@ -23,6 +29,10 @@ spec:
{{ $key }}: {{ $val }}
{{- end }}
spec:
{{- with .Values.server.podSecurityContext }}
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- $secrets := .Values.server.imagePullSecrets }}
{{- if $secrets }}
imagePullSecrets:
@ -30,14 +40,15 @@ spec:
{{- end }}
initContainers:
- name: wait-for-dependencies
image: busybox:1.37
image: {{ printf "%s/%s:%s" .Values.server.dependencyWait.image.registry .Values.server.dependencyWait.image.repository .Values.server.dependencyWait.image.tag | quote }}
imagePullPolicy: {{ .Values.server.dependencyWait.image.pullPolicy }}
env:
- name: DB_HOST
value: {{ include "skillhub.postgresql.serviceName" . }}
value: {{ include "skillhub.postgresql.serviceName" . | quote }}
- name: DB_PORT
value: {{ include "skillhub.postgresql.port" . | quote }}
- name: REDIS_HOST
value: {{ include "skillhub.redis.serviceName" . }}
value: {{ include "skillhub.redis.serviceName" . | quote }}
- name: REDIS_PORT
value: {{ include "skillhub.redis.port" . | quote }}
command:
@ -65,36 +76,35 @@ spec:
{{- else if and (not .Values.redis.enabled) .Values.externalRedis.sentinel.enabled }}
{{- $profiles = printf "%s,redis-sentinel" $profiles }}
{{- end }}
value: {{ $profiles }}
value: {{ $profiles | quote }}
# Database
- name: SPRING_DATASOURCE_URL
valueFrom:
secretKeyRef:
name: {{ include "skillhub.secretName" . }}
key: spring-datasource-url
value: {{ include "skillhub.jdbcUrl" . | quote }}
- name: SPRING_DATASOURCE_USERNAME
valueFrom:
secretKeyRef:
name: {{ include "skillhub.secretName" . }}
key: spring-datasource-username
value: {{ include "skillhub.postgresql.username" . | quote }}
- name: SPRING_DATASOURCE_PASSWORD
valueFrom:
secretKeyRef:
{{- if .Values.postgresql.enabled }}
name: {{ include "skillhub.postgresql.secretName" . }}
key: {{ include "skillhub.postgresql.passwordKey" . }}
{{- else }}
name: {{ include "skillhub.secretName" . }}
key: spring-datasource-password
{{- end }}
# Redis
{{- if and .Values.redis.enabled .Values.redis.sentinel.enabled }}
- name: SPRING_DATA_REDIS_SENTINEL_MASTER
value: {{ .Values.redis.sentinel.masterSet | default "mymaster" | quote }}
- name: SPRING_DATA_REDIS_SENTINEL_NODES
value: {{ include "skillhub.redis.sentinel.nodes" . }}
value: {{ include "skillhub.redis.sentinel.nodes" . | quote }}
{{- else if and (not .Values.redis.enabled) .Values.externalRedis.sentinel.enabled }}
- name: SPRING_DATA_REDIS_SENTINEL_MASTER
value: {{ .Values.externalRedis.sentinel.masterSet | default "mymaster" | quote }}
- name: SPRING_DATA_REDIS_SENTINEL_NODES
value: {{ join "," .Values.externalRedis.sentinel.nodes }}
value: {{ join "," .Values.externalRedis.sentinel.nodes | quote }}
{{- else }}
- name: SPRING_DATA_REDIS_HOST
valueFrom:
@ -109,22 +119,38 @@ spec:
{{- end }}
{{- if or (and .Values.redis.enabled .Values.redis.sentinel.enabled) (and (not .Values.redis.enabled) .Values.externalRedis.sentinel.enabled) }}
- name: SPRING_DATA_REDIS_PASSWORD
valueFrom:
secretKeyRef:
{{- if .Values.redis.enabled }}
name: {{ include "skillhub.redis.secretName" . }}
key: {{ include "skillhub.redis.passwordKey" . }}
{{- else }}
name: {{ include "skillhub.secretName" . }}
key: redis-password
{{- end }}
optional: true
- name: SPRING_DATA_REDIS_SENTINEL_PASSWORD
valueFrom:
secretKeyRef:
name: {{ include "skillhub.secretName" . }}
{{- if and (not .Values.redis.enabled) .Values.externalRedis.sentinel.enabled }}
key: redis-sentinel-password
{{- if .Values.redis.enabled }}
name: {{ include "skillhub.redis.secretName" . }}
key: {{ include "skillhub.redis.passwordKey" . }}
{{- else }}
key: redis-password
name: {{ include "skillhub.secretName" . }}
key: redis-sentinel-password
{{- end }}
optional: true
{{- else if or .Values.redis.enabled .Values.externalRedis.password }}
- name: SPRING_DATA_REDIS_PASSWORD
valueFrom:
secretKeyRef:
{{- if .Values.redis.enabled }}
name: {{ include "skillhub.redis.secretName" . }}
{{- else }}
name: {{ include "skillhub.secretName" . }}
key: redis-password
{{- end }}
key: {{ if .Values.redis.enabled }}{{ include "skillhub.redis.passwordKey" . }}{{ else }}redis-password{{ end }}
optional: true
{{- end }}
@ -141,32 +167,57 @@ spec:
key: skillhub-storage-provider
{{- if .Values.s3.enabled }}
- name: SKILLHUB_S3_BUCKET
- name: SKILLHUB_STORAGE_S3_BUCKET
valueFrom:
configMapKeyRef:
name: {{ include "skillhub.fullname" . }}-config
key: s3-bucket
- name: SKILLHUB_S3_ENDPOINT
- name: SKILLHUB_STORAGE_S3_ENDPOINT
valueFrom:
configMapKeyRef:
name: {{ include "skillhub.fullname" . }}-config
key: s3-endpoint
- name: SKILLHUB_S3_REGION
- name: SKILLHUB_STORAGE_S3_PUBLIC_ENDPOINT
valueFrom:
configMapKeyRef:
name: {{ include "skillhub.fullname" . }}-config
key: s3-public-endpoint
- name: SKILLHUB_STORAGE_S3_REGION
valueFrom:
configMapKeyRef:
name: {{ include "skillhub.fullname" . }}-config
key: s3-region
- name: SKILLHUB_S3_ACCESS_KEY
- name: SKILLHUB_STORAGE_S3_FORCE_PATH_STYLE
valueFrom:
configMapKeyRef:
name: {{ include "skillhub.fullname" . }}-config
key: s3-force-path-style
- name: SKILLHUB_STORAGE_S3_DISABLE_CHUNKED_ENCODING
valueFrom:
configMapKeyRef:
name: {{ include "skillhub.fullname" . }}-config
key: s3-disable-chunked-encoding
- name: SKILLHUB_STORAGE_S3_AUTO_CREATE_BUCKET
valueFrom:
configMapKeyRef:
name: {{ include "skillhub.fullname" . }}-config
key: s3-auto-create-bucket
- name: SKILLHUB_STORAGE_S3_PRESIGN_EXPIRY
valueFrom:
configMapKeyRef:
name: {{ include "skillhub.fullname" . }}-config
key: s3-presign-expiry
- name: SKILLHUB_STORAGE_S3_ACCESS_KEY
valueFrom:
secretKeyRef:
name: {{ include "skillhub.secretName" . }}
key: s3-access-key
key: skillhub-storage-s3-access-key
optional: true
- name: SKILLHUB_S3_SECRET_KEY
- name: SKILLHUB_STORAGE_S3_SECRET_KEY
valueFrom:
secretKeyRef:
name: {{ include "skillhub.secretName" . }}
key: s3-secret-key
key: skillhub-storage-s3-secret-key
optional: true
{{- end }}
@ -194,6 +245,33 @@ spec:
name: {{ include "skillhub.fullname" . }}-config
key: session-cookie-secure
# Public URL and authentication
- name: SKILLHUB_PUBLIC_BASE_URL
valueFrom:
configMapKeyRef:
name: {{ include "skillhub.fullname" . }}-config
key: public-base-url
- name: DEVICE_AUTH_VERIFICATION_URI
valueFrom:
configMapKeyRef:
name: {{ include "skillhub.fullname" . }}-config
key: device-auth-verification-uri
- name: SKILLHUB_AUTH_DIRECT_ENABLED
valueFrom:
configMapKeyRef:
name: {{ include "skillhub.fullname" . }}-config
key: auth-direct-enabled
- name: SKILLHUB_BUILTIN_SKILLS_ENABLED
valueFrom:
configMapKeyRef:
name: {{ include "skillhub.fullname" . }}-config
key: builtin-skills-enabled
- name: SKILLHUB_DOWNLOAD_ANON_COOKIE_SECRET
valueFrom:
secretKeyRef:
name: {{ include "skillhub.secretName" . }}
key: skillhub-download-anon-cookie-secret
# Bootstrap Admin
- name: BOOTSTRAP_ADMIN_ENABLED
valueFrom:

View file

@ -6,7 +6,11 @@ SkillHub Service 资源
{{- range $name := list "server" "web" }}
{{- $component := index $.Values $name }}
{{- if $component.service.enabled }}
{{- $enabled := true }}
{{- if hasKey $component "enabled" }}
{{- $enabled = $component.enabled }}
{{- end }}
{{- if and $enabled $component.service.enabled }}
---
apiVersion: v1
kind: Service

View file

@ -0,0 +1,88 @@
{{- /* Cross-field validation that JSON Schema cannot express reliably. */ -}}
{{- if not .Values.server.enabled -}}
{{- fail "server.enabled=false is unsupported because the bundled web component requires the SkillHub server" -}}
{{- end -}}
{{- if and .Values.auth.direct.enabled (not .Values.auth.direct.provider) -}}
{{- fail "auth.direct.enabled=true requires auth.direct.provider" -}}
{{- end -}}
{{- if and .Values.ingress.enabled (not .Values.server.service.enabled) -}}
{{- fail "ingress.enabled=true requires server.service.enabled=true" -}}
{{- end -}}
{{- if and .Values.ingress.enabled (not .Values.web.service.enabled) -}}
{{- fail "ingress.enabled=true requires web.service.enabled=true" -}}
{{- end -}}
{{- if and .Values.ingress.enabled .Values.ingress.certManager.enabled (not .Values.ingress.tls) -}}
{{- fail "ingress.certManager.enabled=true requires at least one ingress.tls entry" -}}
{{- end -}}
{{- range $host := .Values.ingress.hosts -}}
{{- range $path := $host.paths -}}
{{- if regexMatch "^/api(?:/|$)" $path.path -}}
{{- fail "ingress.hosts[].paths reserves /api for the SkillHub server" -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- range $name := list "server" "web" "scanner" -}}
{{- $component := index $.Values $name -}}
{{- $enabled := true -}}
{{- if hasKey $component "enabled" -}}
{{- $enabled = $component.enabled -}}
{{- end -}}
{{- if and $enabled $component.autoscaling.enabled -}}
{{- if gt ($component.autoscaling.minReplicas | int) ($component.autoscaling.maxReplicas | int) -}}
{{- fail (printf "%s.autoscaling.minReplicas must not exceed maxReplicas" $name) -}}
{{- end -}}
{{- if and (not $component.autoscaling.targetCPUUtilizationPercentage) (not $component.autoscaling.targetMemoryUtilizationPercentage) -}}
{{- fail (printf "%s.autoscaling requires at least one CPU or memory utilization target" $name) -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- $localStorageReplicas := .Values.server.replicaCount | int -}}
{{- if .Values.server.autoscaling.enabled -}}
{{- $localStorageReplicas = .Values.server.autoscaling.maxReplicas | int -}}
{{- end -}}
{{- if and (not .Values.s3.enabled) (gt $localStorageReplicas 1) -}}
{{- if not .Values.server.storage.accessMode -}}
{{- fail "local storage with multiple server replicas requires server.storage.accessMode=ReadWriteMany and an RWX-capable StorageClass; use S3 otherwise" -}}
{{- end -}}
{{- if ne .Values.server.storage.accessMode "ReadWriteMany" -}}
{{- fail "local storage with multiple server replicas requires server.storage.accessMode=ReadWriteMany" -}}
{{- end -}}
{{- end -}}
{{- if and (not .Values.postgresql.enabled) (not .Values.externalDatabase.host) -}}
{{- fail "postgresql.enabled=false requires externalDatabase.host for dependency checks" -}}
{{- end -}}
{{- if and (not .Values.redis.enabled) (not .Values.externalRedis.sentinel.enabled) (not .Values.externalRedis.host) -}}
{{- fail "redis.enabled=false requires externalRedis.host" -}}
{{- end -}}
{{- if and (not .Values.redis.enabled) .Values.externalRedis.sentinel.enabled (not .Values.externalRedis.sentinel.nodes) -}}
{{- fail "external Redis Sentinel requires at least one externalRedis.sentinel.nodes entry" -}}
{{- end -}}
{{- if not .Values.secrets.allowAutoGenerated -}}
{{- if not .Values.existingSecret -}}
{{- if not (or .Values.secrets.bootstrapAdminPassword .Values.bootstrapAdmin.password) -}}
{{- fail "secrets.allowAutoGenerated=false requires secrets.bootstrapAdminPassword or bootstrapAdmin.password" -}}
{{- end -}}
{{- if not .Values.secrets.downloadAnonCookieSecret -}}
{{- fail "secrets.allowAutoGenerated=false requires secrets.downloadAnonCookieSecret" -}}
{{- end -}}
{{- end -}}
{{- if and .Values.postgresql.enabled (not .Values.postgresql.auth.existingSecret) -}}
{{- if and .Values.postgresql.auth.enablePostgresUser (not .Values.postgresql.auth.postgresPassword) -}}
{{- fail "secrets.allowAutoGenerated=false requires postgresql.auth.postgresPassword or postgresql.auth.existingSecret" -}}
{{- end -}}
{{- if and .Values.postgresql.auth.username (ne .Values.postgresql.auth.username "postgres") (not .Values.postgresql.auth.password) -}}
{{- fail "secrets.allowAutoGenerated=false requires postgresql.auth.password or postgresql.auth.existingSecret" -}}
{{- end -}}
{{- if and (eq .Values.postgresql.architecture "replication") (not .Values.postgresql.auth.replicationPassword) -}}
{{- fail "secrets.allowAutoGenerated=false requires postgresql.auth.replicationPassword for replication architecture" -}}
{{- end -}}
{{- end -}}
{{- if and .Values.redis.enabled .Values.redis.auth.enabled (not .Values.redis.auth.existingSecret) (not .Values.redis.auth.password) -}}
{{- fail "secrets.allowAutoGenerated=false requires redis.auth.password or redis.auth.existingSecret" -}}
{{- end -}}
{{- end -}}

View file

@ -16,7 +16,7 @@ spec:
labels:
{{- include "skillhub.web.selectorLabels" . | nindent 8 }}
annotations:
checksum/config: {{ toYaml (dict "web" .Values.web) | sha256sum }}
checksum/config: {{ toYaml (dict "web" .Values.web "publicBaseUrl" .Values.publicBaseUrl "auth" .Values.auth) | sha256sum }}
{{- range $key, $val := .Values.web.podAnnotations }}
{{ $key }}: {{ $val }}
{{- end }}
@ -33,6 +33,21 @@ spec:
env:
- name: SKILLHUB_API_UPSTREAM
value: http://{{ include "skillhub.fullname" . }}-server:{{ .Values.server.service.port }}
- name: SKILLHUB_PUBLIC_BASE_URL
valueFrom:
configMapKeyRef:
name: {{ include "skillhub.fullname" . }}-config
key: public-base-url
- name: SKILLHUB_WEB_AUTH_DIRECT_ENABLED
valueFrom:
configMapKeyRef:
name: {{ include "skillhub.fullname" . }}-config
key: auth-direct-enabled
- name: SKILLHUB_WEB_AUTH_DIRECT_PROVIDER
valueFrom:
configMapKeyRef:
name: {{ include "skillhub.fullname" . }}-config
key: auth-direct-provider
{{- with .Values.web.extraEnv }}
{{- toYaml . | nindent 12 }}
{{- end }}

View file

@ -0,0 +1,215 @@
#!/usr/bin/env bash
set -euo pipefail
CHART_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
TEST_VALUES="$CHART_DIR/tests/test-values.yaml"
TMP_DIR=$(mktemp -d)
trap 'rm -rf "$TMP_DIR"' EXIT
fail() {
echo "ERROR: $*" >&2
exit 1
}
render() {
helm template "$@" -f "$TEST_VALUES"
}
assert_rejected() {
local name=$1
shift
if render "$name" "$CHART_DIR" "$@" >"$TMP_DIR/$name.yaml" 2>"$TMP_DIR/$name.err"; then
fail "$name should have been rejected"
fi
}
render verify "$CHART_DIR" >"$TMP_DIR/default.yaml"
grep -Fq 'name: POSTGRESQL_MAX_CONNECTIONS' "$TMP_DIR/default.yaml"
grep -Fq 'value: "verify-postgresql"' "$TMP_DIR/default.yaml"
grep -Fq 'value: "verify-redis-master"' "$TMP_DIR/default.yaml"
grep -Fq 'fsGroup: 101' "$TMP_DIR/default.yaml"
grep -Fq 'fsGroupChangePolicy: OnRootMismatch' "$TMP_DIR/default.yaml"
grep -Fq 'type: Recreate' "$TMP_DIR/default.yaml"
render custom-server-fsgroup "$CHART_DIR" \
--set server.podSecurityContext.fsGroup=2000 \
--set server.podSecurityContext.fsGroupChangePolicy=Always \
--show-only templates/server-deployment.yaml >"$TMP_DIR/custom-server-fsgroup.yaml"
grep -Fq 'fsGroup: 2000' "$TMP_DIR/custom-server-fsgroup.yaml"
grep -Fq 'fsGroupChangePolicy: Always' "$TMP_DIR/custom-server-fsgroup.yaml"
stable_args=(
--set-string secrets.bootstrapAdminPassword=stable-bootstrap-password
--set-string secrets.downloadAnonCookieSecret=stable-download-cookie-secret
--set-string postgresql.auth.postgresPassword=stable-postgres-password
--set-string postgresql.auth.password=stable-user-password
--set-string redis.auth.password=stable-redis-password
)
render stable "$CHART_DIR" "${stable_args[@]}" >"$TMP_DIR/stable-a.yaml"
render stable "$CHART_DIR" "${stable_args[@]}" >"$TMP_DIR/stable-b.yaml"
cmp "$TMP_DIR/stable-a.yaml" "$TMP_DIR/stable-b.yaml"
render private-registry "$CHART_DIR" \
--set server.dependencyWait.image.registry=registry.example.com \
--set server.dependencyWait.image.repository=library/busybox \
--show-only templates/server-deployment.yaml >"$TMP_DIR/private-registry.yaml"
grep -Fq 'image: "registry.example.com/library/busybox:1.37"' "$TMP_DIR/private-registry.yaml"
render postgresql-replication "$CHART_DIR" \
--set postgresql.architecture=replication >"$TMP_DIR/postgresql-replication.yaml"
if [[ $(grep -Fc 'name: POSTGRESQL_MAX_CONNECTIONS' "$TMP_DIR/postgresql-replication.yaml") -ne 2 ]]; then
fail "PostgreSQL primary and read replica must use the same max_connections setting"
fi
render custom "$CHART_DIR" \
--set postgresql.auth.existingSecret=custom-pg \
--set postgresql.auth.secretKeys.userPasswordKey=custom-pg-key \
--set redis.auth.existingSecret=custom-redis \
--set redis.auth.existingSecretPasswordKey=custom-redis-key \
--show-only templates/server-deployment.yaml >"$TMP_DIR/custom.yaml"
grep -Fq 'name: custom-pg' "$TMP_DIR/custom.yaml"
grep -Fq 'key: custom-pg-key' "$TMP_DIR/custom.yaml"
grep -Fq 'name: custom-redis' "$TMP_DIR/custom.yaml"
grep -Fq 'key: custom-redis-key' "$TMP_DIR/custom.yaml"
render postgresql-admin "$CHART_DIR" \
--set postgresql.auth.username=postgres \
--show-only templates/server-deployment.yaml >"$TMP_DIR/postgresql-admin.yaml"
grep -Fq 'value: "postgres"' "$TMP_DIR/postgresql-admin.yaml"
grep -Fq 'key: postgres-password' "$TMP_DIR/postgresql-admin.yaml"
render postgresql-admin-secret "$CHART_DIR" \
--set postgresql.auth.username=postgres \
--show-only charts/postgresql/templates/secrets.yaml >"$TMP_DIR/postgresql-admin-secret.yaml"
grep -Eq '^ postgres-password:' "$TMP_DIR/postgresql-admin-secret.yaml"
if grep -Eq '^ password:' "$TMP_DIR/postgresql-admin-secret.yaml"; then
fail "Bitnami PostgreSQL must not create a custom-user password key for username=postgres"
fi
render postgresql-admin-existing-secret "$CHART_DIR" \
--set postgresql.auth.username=postgres \
--set postgresql.auth.existingSecret=custom-pg-admin \
--set postgresql.auth.secretKeys.adminPasswordKey=custom-admin-key \
--show-only templates/server-deployment.yaml >"$TMP_DIR/postgresql-admin-existing-secret.yaml"
grep -Fq 'name: custom-pg-admin' "$TMP_DIR/postgresql-admin-existing-secret.yaml"
grep -Fq 'key: custom-admin-key' "$TMP_DIR/postgresql-admin-existing-secret.yaml"
render sentinel "$CHART_DIR" \
--set redis.architecture=replication \
--set redis.sentinel.enabled=true \
--show-only templates/server-deployment.yaml >"$TMP_DIR/sentinel.yaml"
grep -Fq 'value: "docker,redis-sentinel"' "$TMP_DIR/sentinel.yaml"
grep -Fq 'value: "mymaster"' "$TMP_DIR/sentinel.yaml"
grep -Fq '.svc.cluster.local:26379' "$TMP_DIR/sentinel.yaml"
grep -Fq 'name: SPRING_DATA_REDIS_PASSWORD' "$TMP_DIR/sentinel.yaml"
grep -Fq 'name: SPRING_DATA_REDIS_SENTINEL_PASSWORD' "$TMP_DIR/sentinel.yaml"
render external-sentinel "$CHART_DIR" \
--set postgresql.enabled=false \
--set externalDatabase.host=db.example.com \
--set redis.enabled=false \
--set externalRedis.password=redis-password \
--set externalRedis.sentinel.enabled=true \
--set externalRedis.sentinel.password=sentinel-password \
--set-json 'externalRedis.sentinel.nodes=["sentinel-a:26379","sentinel-b:26379"]' \
--show-only templates/server-deployment.yaml >"$TMP_DIR/external-sentinel.yaml"
grep -Fq 'value: "sentinel-a"' "$TMP_DIR/external-sentinel.yaml"
grep -Fq 'name: SPRING_DATA_REDIS_PASSWORD' "$TMP_DIR/external-sentinel.yaml"
grep -Fq 'name: SPRING_DATA_REDIS_SENTINEL_PASSWORD' "$TMP_DIR/external-sentinel.yaml"
render special "$CHART_DIR" \
--set-string 'bootstrapAdmin.displayName=Ops: Admin' \
--show-only templates/configmap.yaml >"$TMP_DIR/special.yaml"
grep -Fq 'bootstrap-admin-display-name: "Ops: Admin"' "$TMP_DIR/special.yaml"
render device "$CHART_DIR" \
--set publicBaseUrl=https://skills.example.com \
--show-only templates/configmap.yaml >"$TMP_DIR/device.yaml"
grep -Fq 'device-auth-verification-uri: "https://skills.example.com/cli/auth"' "$TMP_DIR/device.yaml"
render tls "$CHART_DIR" \
--set ingress.enabled=true \
--set-json 'ingress.tls=[{"hosts":["skills.example.com"],"secretName":"skills-tls"}]' \
--show-only templates/configmap.yaml >"$TMP_DIR/tls.yaml"
grep -Fq 'session-cookie-secure: "true"' "$TMP_DIR/tls.yaml"
render legacy-ingress "$CHART_DIR" \
--set ingress.enabled=true \
--set-string ingress.className= \
--set-json 'ingress.annotations={"kubernetes.io/ingress.class":"alb","alb.ingress.kubernetes.io/listen-ports":"[{\"HTTPS\":6443}]"}' \
--show-only templates/ingress.yaml >"$TMP_DIR/legacy-ingress.yaml"
grep -Fq 'kubernetes.io/ingress.class: alb' "$TMP_DIR/legacy-ingress.yaml"
grep -Fq 'alb.ingress.kubernetes.io/listen-ports:' "$TMP_DIR/legacy-ingress.yaml"
if grep -Fq 'ingressClassName:' "$TMP_DIR/legacy-ingress.yaml"; then
fail "empty ingress.className must omit spec.ingressClassName"
fi
render multi-host-ingress "$CHART_DIR" \
--set ingress.enabled=true \
--set ingress.certManager.enabled=true \
--set-json 'ingress.hosts=[{"host":"skills-a.example.com","paths":[{"path":"/","pathType":"Prefix"}]},{"host":"skills-b.example.com","paths":[{"path":"/portal","pathType":"Prefix"}]}]' \
--set-json 'ingress.tls=[{"hosts":["skills-a.example.com","skills-b.example.com"],"secretName":"skills-tls"}]' \
--show-only templates/ingress.yaml \
--show-only templates/certificate.yaml >"$TMP_DIR/multi-host-ingress.yaml"
if [[ $(grep -Fc 'skills-a.example.com' "$TMP_DIR/multi-host-ingress.yaml") -ne 3 ]]; then
fail "first ingress host must be rendered in rule, TLS and Certificate"
fi
if [[ $(grep -Fc 'skills-b.example.com' "$TMP_DIR/multi-host-ingress.yaml") -ne 3 ]]; then
fail "second ingress host must be rendered in rule, TLS and Certificate"
fi
render scanner-off "$CHART_DIR" \
--set scanner.enabled=false \
--set scanner.autoscaling.enabled=true \
--set scanner.podDisruptionBudget.enabled=true >"$TMP_DIR/scanner-off.yaml"
if awk '
$1 == "kind:" { kind=$2 }
kind ~ /^(Deployment|Service|HorizontalPodAutoscaler|PodDisruptionBudget)$/ &&
$1 == "name:" && $2 == "scanner-off-skillhub-scanner" { found=1 }
END { exit found ? 0 : 1 }
' "$TMP_DIR/scanner-off.yaml"; then
fail "disabled scanner rendered workload resources"
fi
render multi-rwx "$CHART_DIR" \
--set server.replicaCount=2 \
--set server.storage.accessMode=ReadWriteMany >"$TMP_DIR/multi-rwx.yaml"
grep -Fq -- '- ReadWriteMany' "$TMP_DIR/multi-rwx.yaml"
grep -Fq 'type: RollingUpdate' "$TMP_DIR/multi-rwx.yaml"
render s3-rolling "$CHART_DIR" \
--set s3.enabled=true \
--set s3.bucket=skillhub \
--set s3.endpoint=https://s3.example.com \
--set s3.accessKey=access-key \
--set s3.secretKey=secret-key \
--show-only templates/server-deployment.yaml >"$TMP_DIR/s3-rolling.yaml"
grep -Fq 'type: RollingUpdate' "$TMP_DIR/s3-rolling.yaml"
assert_rejected server-off --set server.enabled=false
assert_rejected direct-auth-without-provider \
--set auth.direct.enabled=true \
--set-string auth.direct.provider=
assert_rejected ingress-without-server-service --set ingress.enabled=true --set server.service.enabled=false
assert_rejected ingress-without-web-service --set ingress.enabled=true --set web.service.enabled=false
assert_rejected multi-without-rwx --set server.replicaCount=2
assert_rejected hpa-without-metrics \
--set server.autoscaling.enabled=true \
--set server.autoscaling.targetCPUUtilizationPercentage=0 \
--set server.autoscaling.targetMemoryUtilizationPercentage=0
assert_rejected old-postgres-env --set-json 'postgresql.primary.extraEnv=[{"name":"X","value":"Y"}]'
assert_rejected old-sentinel-password --set redis.auth.sentinelPassword=unused
assert_rejected old-sentinel-nodes --set redis.sentinel.nodes=unused
assert_rejected old-sentinel-service-switch --set redis.sentinel.service.enabled=false
assert_rejected invalid-fullname --set fullnameOverride=INVALID_NAME
assert_rejected old-ingress-host --set ingress.host=old.example.com
assert_rejected old-ingress-tls-object --set ingress.tls.enabled=true
assert_rejected empty-ingress-hosts --set-json 'ingress.hosts=[]'
assert_rejected cert-manager-without-tls \
--set ingress.enabled=true \
--set ingress.certManager.enabled=true \
--set-json 'ingress.tls=[]'
if helm template missing-credentials "$CHART_DIR" >"$TMP_DIR/missing-credentials.yaml" 2>"$TMP_DIR/missing-credentials.err"; then
fail "default rendering without stable credentials should have been rejected"
fi
echo "Helm configuration contract tests passed"

View file

@ -0,0 +1,14 @@
# Non-production credentials used only for deterministic chart tests.
secrets:
bootstrapAdminPassword: test-bootstrap-password
downloadAnonCookieSecret: test-download-cookie-secret-at-least-32-chars
postgresql:
auth:
postgresPassword: test-postgres-password
password: test-postgresql-user-password
replicationPassword: test-postgresql-replication-password
redis:
auth:
password: test-redis-password

View file

@ -0,0 +1,462 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"additionalProperties": false,
"properties": {
"global": { "type": "object" },
"images": {
"type": "object",
"additionalProperties": false,
"required": ["registry", "tag", "pullPolicy"],
"properties": {
"registry": { "type": "string", "minLength": 1 },
"tag": { "type": "string" },
"pullPolicy": { "enum": ["Always", "IfNotPresent", "Never"] }
}
},
"nameOverride": { "$ref": "#/definitions/optionalDnsLabel" },
"fullnameOverride": { "$ref": "#/definitions/optionalDnsLabel" },
"publicBaseUrl": { "type": "string" },
"deviceAuthVerificationUri": { "type": "string" },
"auth": {
"type": "object",
"additionalProperties": false,
"required": ["direct"],
"properties": {
"direct": {
"type": "object",
"additionalProperties": false,
"required": ["enabled", "provider"],
"properties": {
"enabled": { "type": "boolean" },
"provider": { "type": "string" }
}
}
}
},
"builtinSkills": {
"type": "object",
"additionalProperties": false,
"required": ["enabled"],
"properties": { "enabled": { "type": "boolean" } }
},
"ingress": {
"type": "object",
"additionalProperties": false,
"required": ["enabled", "className", "hosts", "annotations", "tls", "certManager"],
"properties": {
"enabled": { "type": "boolean" },
"className": {
"oneOf": [
{ "type": "string", "enum": [""] },
{ "$ref": "#/definitions/dnsSubdomain" }
]
},
"hosts": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["host", "paths"],
"properties": {
"host": { "type": "string", "minLength": 1, "pattern": "^(\\*\\.)?[A-Za-z0-9.-]+$" },
"paths": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["path", "pathType"],
"properties": {
"path": { "type": "string", "pattern": "^/" },
"pathType": { "enum": ["Exact", "Prefix", "ImplementationSpecific"] }
}
}
}
}
}
},
"annotations": { "$ref": "#/definitions/stringMap" },
"tls": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["hosts", "secretName"],
"properties": {
"hosts": {
"type": "array",
"minItems": 1,
"uniqueItems": true,
"items": { "type": "string", "minLength": 1, "pattern": "^(\\*\\.)?[A-Za-z0-9.-]+$" }
},
"secretName": { "type": "string", "minLength": 1 }
}
}
},
"certManager": {
"type": "object",
"additionalProperties": false,
"required": ["enabled", "issuerName", "issuerKind"],
"properties": {
"enabled": { "type": "boolean" },
"issuerName": { "type": "string", "minLength": 1 },
"issuerKind": { "type": "string", "minLength": 1 }
}
}
}
},
"s3": {
"type": "object",
"additionalProperties": false,
"required": ["enabled", "bucket", "endpoint", "publicEndpoint", "region", "forcePathStyle", "disableChunkedEncoding", "autoCreateBucket", "presignExpiry", "accessKey", "secretKey"],
"properties": {
"enabled": { "type": "boolean" },
"bucket": { "type": "string", "minLength": 1 },
"endpoint": { "type": "string" },
"publicEndpoint": { "type": "string" },
"region": { "type": "string", "minLength": 1 },
"forcePathStyle": { "type": "boolean" },
"disableChunkedEncoding": { "type": "boolean" },
"autoCreateBucket": { "type": "boolean" },
"presignExpiry": { "type": "string", "pattern": "^P" },
"accessKey": { "type": "string" },
"secretKey": { "type": "string" }
}
},
"session": {
"type": "object",
"additionalProperties": false,
"required": ["cookieSecure"],
"properties": { "cookieSecure": { "type": "boolean" } }
},
"bootstrapAdmin": {
"type": "object",
"additionalProperties": false,
"required": ["enabled", "userId", "username", "displayName", "email", "password"],
"properties": {
"enabled": { "type": "boolean" },
"userId": { "type": "string", "minLength": 1 },
"username": { "type": "string", "minLength": 1 },
"displayName": { "type": "string", "minLength": 1 },
"email": { "type": "string", "minLength": 1 },
"password": { "type": "string" }
}
},
"springProfilesActive": { "type": "string", "minLength": 1 },
"existingSecret": { "type": "string" },
"secrets": {
"type": "object",
"additionalProperties": false,
"required": ["allowAutoGenerated"],
"properties": {
"allowAutoGenerated": { "type": "boolean" },
"bootstrapAdminPassword": { "type": "string" },
"downloadAnonCookieSecret": { "type": "string" },
"oauth2GithubClientId": { "type": "string" },
"oauth2GithubClientSecret": { "type": "string" },
"scannerLlmApiKey": { "type": "string" },
"scannerLlmBaseUrl": { "type": "string" },
"scannerLlmModel": { "type": "string" }
}
},
"postgresql": {
"type": "object",
"properties": {
"enabled": { "type": "boolean" },
"architecture": { "enum": ["standalone", "replication"] },
"auth": { "type": "object" },
"primary": {
"type": "object",
"properties": { "extraEnv": false }
}
}
},
"externalDatabase": {
"type": "object",
"additionalProperties": false,
"required": ["host", "port", "database", "username", "password", "jdbcUrl"],
"properties": {
"host": { "type": "string", "pattern": "^$|^[A-Za-z0-9._-]+$" },
"port": { "type": "integer", "minimum": 1, "maximum": 65535 },
"database": { "type": "string", "minLength": 1 },
"username": { "type": "string", "minLength": 1 },
"password": { "type": "string" },
"jdbcUrl": { "type": "string" }
}
},
"redis": {
"type": "object",
"properties": {
"enabled": { "type": "boolean" },
"architecture": { "enum": ["standalone", "replication"] },
"auth": {
"type": "object",
"properties": { "sentinelPassword": false }
},
"sentinel": {
"type": "object",
"properties": {
"nodes": false,
"service": {
"type": "object",
"properties": { "enabled": false }
}
}
}
}
},
"externalRedis": {
"type": "object",
"additionalProperties": false,
"required": ["host", "port", "password", "sentinel"],
"properties": {
"host": { "type": "string", "pattern": "^$|^[A-Za-z0-9._-]+$" },
"port": { "type": "integer", "minimum": 1, "maximum": 65535 },
"password": { "type": "string" },
"sentinel": {
"type": "object",
"additionalProperties": false,
"required": ["enabled", "masterSet", "nodes", "password"],
"properties": {
"enabled": { "type": "boolean" },
"masterSet": { "type": "string", "minLength": 1 },
"nodes": {
"type": "array",
"items": { "type": "string", "pattern": "^[^:]+:[0-9]+$" }
},
"password": { "type": "string" }
}
}
}
},
"server": { "$ref": "#/definitions/serverComponent" },
"web": { "$ref": "#/definitions/webComponent" },
"scanner": { "$ref": "#/definitions/scannerComponent" }
},
"required": ["images", "auth", "builtinSkills", "ingress", "s3", "session", "bootstrapAdmin", "springProfilesActive", "secrets", "postgresql", "externalDatabase", "redis", "externalRedis", "server", "web", "scanner"],
"definitions": {
"dnsLabel": {
"type": "string",
"minLength": 1,
"maxLength": 63,
"pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$"
},
"dnsSubdomain": {
"type": "string",
"minLength": 1,
"maxLength": 253,
"pattern": "^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$"
},
"optionalDnsLabel": {
"type": "string",
"maxLength": 63,
"pattern": "^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?$"
},
"stringMap": {
"type": "object",
"additionalProperties": { "type": "string" }
},
"image": {
"type": "object",
"additionalProperties": false,
"required": ["registry", "tag"],
"properties": {
"registry": { "type": "string" },
"tag": { "type": "string" }
}
},
"service": {
"type": "object",
"additionalProperties": false,
"required": ["enabled", "type", "port", "nodePort", "loadBalancerIP", "loadBalancerSourceRanges"],
"properties": {
"enabled": { "type": "boolean" },
"type": { "enum": ["ClusterIP", "NodePort", "LoadBalancer"] },
"port": { "type": "integer", "minimum": 1, "maximum": 65535 },
"nodePort": {
"oneOf": [
{ "type": "string", "enum": [""] },
{ "type": "integer", "minimum": 1, "maximum": 65535 }
]
},
"loadBalancerIP": { "type": "string" },
"loadBalancerSourceRanges": {
"type": "array",
"items": { "type": "string" }
}
}
},
"resources": { "type": "object" },
"autoscaling": {
"type": "object",
"additionalProperties": false,
"required": ["enabled", "minReplicas", "maxReplicas", "targetCPUUtilizationPercentage", "targetMemoryUtilizationPercentage"],
"properties": {
"enabled": { "type": "boolean" },
"minReplicas": { "type": "integer", "minimum": 1 },
"maxReplicas": { "type": "integer", "minimum": 1 },
"targetCPUUtilizationPercentage": { "type": "integer", "minimum": 0 },
"targetMemoryUtilizationPercentage": { "type": "integer", "minimum": 0 }
}
},
"pdb": {
"type": "object",
"additionalProperties": false,
"required": ["enabled", "minAvailable"],
"properties": {
"enabled": { "type": "boolean" },
"minAvailable": {
"oneOf": [
{ "type": "integer", "minimum": 0 },
{ "type": "string", "pattern": "^[0-9]+%$" }
]
}
}
},
"commonPod": {
"type": "object",
"properties": {
"resources": { "$ref": "#/definitions/resources" },
"extraEnv": { "type": "array", "items": { "type": "object" } },
"podAnnotations": { "$ref": "#/definitions/stringMap" },
"imagePullSecrets": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["name"],
"properties": { "name": { "type": "string", "minLength": 1 } }
}
},
"nodeSelector": { "$ref": "#/definitions/stringMap" },
"tolerations": { "type": "array", "items": { "type": "object" } },
"affinity": { "type": "object" },
"probes": { "type": "object" },
"autoscaling": { "$ref": "#/definitions/autoscaling" },
"podDisruptionBudget": { "$ref": "#/definitions/pdb" }
}
},
"serverComponent": {
"allOf": [
{ "$ref": "#/definitions/commonPod" },
{
"type": "object",
"additionalProperties": false,
"required": ["enabled", "replicaCount", "image", "dependencyWait", "service", "storage", "podSecurityContext", "resources", "javaOpts", "extraEnv", "podAnnotations", "imagePullSecrets", "nodeSelector", "tolerations", "affinity", "probes", "autoscaling", "podDisruptionBudget"],
"properties": {
"enabled": { "type": "boolean" },
"replicaCount": { "type": "integer", "minimum": 1 },
"image": { "$ref": "#/definitions/image" },
"dependencyWait": {
"type": "object",
"additionalProperties": false,
"required": ["image"],
"properties": {
"image": {
"type": "object",
"additionalProperties": false,
"required": ["registry", "repository", "tag", "pullPolicy"],
"properties": {
"registry": { "type": "string", "minLength": 1 },
"repository": { "type": "string", "minLength": 1 },
"tag": { "type": "string", "minLength": 1 },
"pullPolicy": { "enum": ["Always", "IfNotPresent", "Never"] }
}
}
}
},
"service": { "$ref": "#/definitions/service" },
"storage": {
"type": "object",
"additionalProperties": false,
"required": ["accessMode", "size", "storageClassName"],
"properties": {
"accessMode": { "enum": ["", "ReadWriteOnce", "ReadWriteMany"] },
"size": { "type": "string", "minLength": 1 },
"storageClassName": { "type": "string" }
}
},
"podSecurityContext": {
"type": "object",
"additionalProperties": false,
"required": ["fsGroup", "fsGroupChangePolicy"],
"properties": {
"fsGroup": { "type": "integer", "minimum": 1 },
"fsGroupChangePolicy": { "enum": ["Always", "OnRootMismatch"] }
}
},
"resources": { "$ref": "#/definitions/resources" },
"javaOpts": { "type": "string" },
"extraEnv": { "type": "array", "items": { "type": "object" } },
"podAnnotations": { "$ref": "#/definitions/stringMap" },
"imagePullSecrets": { "type": "array", "items": { "type": "object" } },
"nodeSelector": { "$ref": "#/definitions/stringMap" },
"tolerations": { "type": "array", "items": { "type": "object" } },
"affinity": { "type": "object" },
"probes": { "type": "object" },
"autoscaling": { "$ref": "#/definitions/autoscaling" },
"podDisruptionBudget": { "$ref": "#/definitions/pdb" }
}
}
]
},
"webComponent": {
"allOf": [
{ "$ref": "#/definitions/commonPod" },
{
"type": "object",
"additionalProperties": false,
"required": ["replicaCount", "image", "service", "resources", "extraEnv", "podAnnotations", "imagePullSecrets", "nodeSelector", "tolerations", "affinity", "probes", "autoscaling", "podDisruptionBudget"],
"properties": {
"replicaCount": { "type": "integer", "minimum": 1 },
"image": { "$ref": "#/definitions/image" },
"service": { "$ref": "#/definitions/service" },
"resources": { "$ref": "#/definitions/resources" },
"extraEnv": { "type": "array", "items": { "type": "object" } },
"podAnnotations": { "$ref": "#/definitions/stringMap" },
"imagePullSecrets": { "type": "array", "items": { "type": "object" } },
"nodeSelector": { "$ref": "#/definitions/stringMap" },
"tolerations": { "type": "array", "items": { "type": "object" } },
"affinity": { "type": "object" },
"probes": { "type": "object" },
"autoscaling": { "$ref": "#/definitions/autoscaling" },
"podDisruptionBudget": { "$ref": "#/definitions/pdb" }
}
}
]
},
"scannerComponent": {
"allOf": [
{ "$ref": "#/definitions/commonPod" },
{
"type": "object",
"additionalProperties": false,
"required": ["enabled", "replicaCount", "image", "service", "resources", "extraEnv", "podAnnotations", "imagePullSecrets", "nodeSelector", "tolerations", "affinity", "probes", "autoscaling", "podDisruptionBudget"],
"properties": {
"enabled": { "type": "boolean" },
"replicaCount": { "type": "integer", "minimum": 1 },
"image": { "$ref": "#/definitions/image" },
"service": {
"type": "object",
"additionalProperties": false,
"required": ["port"],
"properties": { "port": { "type": "integer", "minimum": 1, "maximum": 65535 } }
},
"resources": { "$ref": "#/definitions/resources" },
"extraEnv": { "type": "array", "items": { "type": "object" } },
"podAnnotations": { "$ref": "#/definitions/stringMap" },
"imagePullSecrets": { "type": "array", "items": { "type": "object" } },
"nodeSelector": { "$ref": "#/definitions/stringMap" },
"tolerations": { "type": "array", "items": { "type": "object" } },
"affinity": { "type": "object" },
"probes": { "type": "object" },
"autoscaling": { "$ref": "#/definitions/autoscaling" },
"podDisruptionBudget": { "$ref": "#/definitions/pdb" }
}
}
]
}
}
}

View file

@ -13,18 +13,31 @@ images:
nameOverride: ""
fullnameOverride: ""
# 浏览器、CLI 和 OAuth 回调访问的公开地址(不带末尾斜杠)
publicBaseUrl: ""
deviceAuthVerificationUri: ""
auth:
direct:
enabled: true
provider: local
builtinSkills:
enabled: true
# ============================================================================
# Ingress 配置
# ============================================================================
ingress:
enabled: false
className: nginx
host: skills.example.com
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: 100m
tls:
enabled: false
secretName: ""
hosts:
- host: skills.example.com
paths:
- path: /
pathType: Prefix
annotations: {}
tls: []
certManager:
enabled: false
issuerName: letsencrypt-prod
@ -37,7 +50,12 @@ s3:
enabled: false
bucket: skillhub-storage
endpoint: ""
publicEndpoint: ""
region: us-east-1
forcePathStyle: true
disableChunkedEncoding: false
autoCreateBucket: false
presignExpiry: PT10M
accessKey: ""
secretKey: ""
@ -69,11 +87,14 @@ springProfilesActive: docker
existingSecret: ""
secrets:
springDatasourcePassword: ""
# 默认禁止随机 Secret仅在非 GitOps 临时环境中按需启用
allowAutoGenerated: false
bootstrapAdminPassword: ""
downloadAnonCookieSecret: ""
oauth2GithubClientId: ""
oauth2GithubClientSecret: ""
scannerLlmApiKey: ""
scannerLlmBaseUrl: ""
scannerLlmModel: ""
# ============================================================================
@ -104,8 +125,8 @@ postgresql:
limits:
cpu: 500m
memory: 1Gi
extraEnv:
- name: POSTGRES_MAX_CONNECTIONS
extraEnvVars:
- name: POSTGRESQL_MAX_CONNECTIONS
value: "500"
podAnnotations: {}
podSecurityContext:
@ -137,6 +158,10 @@ postgresql:
limits:
cpu: 500m
memory: 1Gi
# Hot standbys must not use a lower max_connections than the primary.
extraEnvVars:
- name: POSTGRESQL_MAX_CONNECTIONS
value: "500"
metrics:
enabled: true
@ -144,7 +169,7 @@ postgresql:
enabled: false
externalDatabase:
host: postgres.example.com
host: ""
port: 5432
database: skillhub
username: skillhub
@ -162,7 +187,6 @@ redis:
auth:
enabled: true
password: ""
sentinelPassword: ""
master:
persistence:
@ -204,11 +228,11 @@ redis:
sentinel:
enabled: false
masterSet: mymaster
nodes: ""
service:
enabled: true
ports:
sentinel: 26379
containerPorts:
sentinel: 26379
metrics:
enabled: true
@ -216,7 +240,7 @@ redis:
enabled: false
externalRedis:
host: redis.example.com
host: ""
port: 6379
password: ""
sentinel:
@ -236,6 +260,13 @@ server:
registry: ""
tag: ""
dependencyWait:
image:
registry: docker.io
repository: library/busybox
tag: "1.37"
pullPolicy: IfNotPresent
service:
enabled: true
type: ClusterIP
@ -250,6 +281,11 @@ server:
size: 10Gi
storageClassName: ""
# PVC 挂载会覆盖镜像内目录权限;使用镜像中 app 用户的组 ID 使本地存储可写
podSecurityContext:
fsGroup: 101
fsGroupChangePolicy: OnRootMismatch
resources:
requests:
cpu: 500m

View file

@ -1,7 +1,7 @@
spring:
data:
redis:
password: ${SPRING_DATA_REDIS_SENTINEL_PASSWORD:${SPRING_DATA_REDIS_PASSWORD:${REDIS_PASSWORD:}}}
password: ${SPRING_DATA_REDIS_PASSWORD:${REDIS_PASSWORD:${SPRING_DATA_REDIS_SENTINEL_PASSWORD:}}}
sentinel:
master: ${SPRING_DATA_REDIS_SENTINEL_MASTER:${REDIS_SENTINEL_MASTER:mymaster}}
nodes: ${SPRING_DATA_REDIS_SENTINEL_NODES:${REDIS_SENTINEL_NODES:}}

View file

@ -0,0 +1,56 @@
package com.iflytek.skillhub.config;
import org.junit.jupiter.api.Test;
import org.springframework.boot.env.YamlPropertySourceLoader;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
import org.springframework.core.env.PropertySourcesPropertyResolver;
import org.springframework.core.io.ClassPathResource;
import java.io.IOException;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
class RedisSentinelProfileConfigurationTest {
private final PropertySource<?> sentinelProfile = loadSentinelProfile();
@Test
void separateDataAndSentinelPasswordsResolveIndependently() {
assertThat(resolve("spring.data.redis.password", Map.of(
"SPRING_DATA_REDIS_PASSWORD", "data-password",
"SPRING_DATA_REDIS_SENTINEL_PASSWORD", "sentinel-password"
))).isEqualTo("data-password");
assertThat(resolve("spring.data.redis.sentinel.password", Map.of(
"SPRING_DATA_REDIS_PASSWORD", "data-password",
"SPRING_DATA_REDIS_SENTINEL_PASSWORD", "sentinel-password"
))).isEqualTo("sentinel-password");
}
@Test
void sentinelPasswordRemainsADataPasswordFallback() {
assertThat(resolve("spring.data.redis.password", Map.of(
"SPRING_DATA_REDIS_SENTINEL_PASSWORD", "legacy-password"
))).isEqualTo("legacy-password");
}
private String resolve(String propertyName, Map<String, Object> environment) {
MutablePropertySources sources = new MutablePropertySources();
sources.addFirst(new MapPropertySource("test-environment", environment));
PropertySourcesPropertyResolver resolver = new PropertySourcesPropertyResolver(sources);
return resolver.resolveRequiredPlaceholders((String) sentinelProfile.getProperty(propertyName));
}
private static PropertySource<?> loadSentinelProfile() {
try {
return new YamlPropertySourceLoader()
.load("redis-sentinel", new ClassPathResource("application-redis-sentinel.yml"))
.getFirst();
} catch (IOException e) {
throw new IllegalStateException("Failed to load Redis Sentinel profile", e);
}
}
}