mirror of
https://github.com/iflytek/skillhub.git
synced 2026-08-28 11:25:00 +00:00
fix(helm): 修正组件启停与依赖配置并增加 values 校验
在配置进入 Kubernetes 前完成父 Chart 校验,并补齐剩余的依赖配置契约。 - 让 Service、HPA 和 PDB 正确遵循组件启用状态 - 校验 Ingress、自动扩缩容、外部依赖和共享存储的组合配置 - 增加 values.schema.json 和 Helm 配置契约测试并接入 CI - 统一 PostgreSQL Primary 与 Read Replica 的 max_connections 配置 - 修正 Redis Sentinel 节点、依赖等待和独立密码配置 - 允许覆盖依赖等待容器镜像,支持完整私有镜像仓库部署 - 兼容现代与旧式 IngressClass,并支持多域名 TLS 和证书 SAN - 增加 GitOps 稳定 Secret 模式,阻止离线渲染产生随机凭据漂移 - 引用用户可控的 ConfigMap 字符串,并推导 TLS 安全 Cookie 和设备认证默认值 - 补充 Sentinel、RWX 存储、TLS Cookie、PVC 保留、私有镜像和 GitOps 文档 - 增加 Redis 数据密码与 Sentinel 密码分离的应用配置测试 已通过 Helm 严格 lint、渲染场景、配置契约测试、kubeconform、后端测试套件和 Sentinel 专项配置测试。 Signed-off-by: lhb6540 <lhb6540@gmail.com>
This commit is contained in:
parent
3b3905be63
commit
5d379dcaaf
21 changed files with 1156 additions and 81 deletions
10
.github/workflows/pr-helm-chart.yml
vendored
10
.github/workflows/pr-helm-chart.yml
vendored
|
|
@ -44,7 +44,10 @@ jobs:
|
|||
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: |
|
||||
|
|
@ -118,6 +121,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
|
||||
|
|
@ -155,8 +159,8 @@ jobs:
|
|||
if: ${{ matrix.scenario.name == 'bitnami-default' }}
|
||||
run: |
|
||||
helm template test-release . --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 '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
|
||||
|
|
|
|||
2
.github/workflows/publish-chart.yml
vendored
2
.github/workflows/publish-chart.yml
vendored
|
|
@ -67,7 +67,7 @@ jobs:
|
|||
echo "version=$VER" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Lint chart
|
||||
run: helm lint .
|
||||
run: helm lint . -f tests/test-values.yaml
|
||||
|
||||
- name: Package and push
|
||||
run: |
|
||||
|
|
|
|||
|
|
@ -19,3 +19,6 @@ CLAUDE.md
|
|||
|
||||
# CI
|
||||
.github/
|
||||
|
||||
# Source-only contract tests
|
||||
tests/
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ keywords:
|
|||
- 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
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
- **微服务架构**:Server(Spring Boot)、Web(Nginx)、Scanner 分离部署
|
||||
- **高可用**:支持 HPA 自动扩缩容、PDB Pod 中断预算
|
||||
- **数据层**:使用 Bitnami PostgreSQL/Redis,支持主从复制、哨兵模式
|
||||
- **安全**:TLS 证书管理、Secret 密码保护、NetworkPolicy
|
||||
- **安全**:TLS 证书管理、Secret 密码保护;Bitnami 数据组件默认提供 NetworkPolicy
|
||||
- **可观测性**:内置 Prometheus metrics exporter
|
||||
|
||||
## 快速开始
|
||||
|
|
@ -20,20 +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
|
||||
```
|
||||
|
||||
|
|
@ -41,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 \
|
||||
|
|
@ -76,9 +103,37 @@ helm -n skillhub upgrade -i skillhub ./charts/skillhub \
|
|||
|
||||
```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 或密钥注入插件中。
|
||||
|
||||
## 配置参考
|
||||
|
||||
### 副本数配置
|
||||
|
|
@ -92,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。
|
||||
|
||||
### 服务配置
|
||||
|
||||
| 参数 | 描述 | 默认值 |
|
||||
|
|
@ -107,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='<密码>'
|
||||
```
|
||||
|
||||
### 数据库配置
|
||||
|
||||
| 参数 | 描述 | 默认值 |
|
||||
|
|
@ -116,18 +250,62 @@ 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 | `""` |
|
||||
|
||||
```bash
|
||||
# 默认使用本地 PVC
|
||||
helm -n skillhub upgrade -i skillhub ./charts/skillhub \
|
||||
--set bootstrapAdmin.password=your-secure-password
|
||||
-f values-production.yaml
|
||||
```
|
||||
|
||||
### S3 对象存储
|
||||
|
|
@ -149,7 +327,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 s3.enabled=true \
|
||||
--set s3.bucket=your-bucket \
|
||||
--set s3.endpoint=s3.amazonaws.com \
|
||||
|
|
@ -162,28 +340,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-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.tls.enabled=true \
|
||||
--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 后仍会保留,
|
||||
需要确认数据不再使用后手动删除。
|
||||
|
||||
## 依赖
|
||||
|
||||
| 依赖 | 版本 |
|
||||
|
|
|
|||
|
|
@ -171,9 +171,10 @@ app.kubernetes.io/component: scanner
|
|||
{{- $fullname := include "skillhub.redis.fullname" . -}}
|
||||
{{- $prefix := printf "%s-node" $fullname -}}
|
||||
{{- $headless := printf "%s-headless" $fullname -}}
|
||||
{{- $port := include "skillhub.redis.port" . -}}
|
||||
{{- /* 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 */}}
|
||||
|
|
@ -198,8 +199,13 @@ 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 */}}
|
||||
|
|
@ -235,6 +241,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 }}
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
|
|
|
|||
|
|
@ -9,21 +9,21 @@ 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-public-endpoint: {{ .Values.s3.publicEndpoint }}
|
||||
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 }}
|
||||
|
|
@ -32,22 +32,26 @@ data:
|
|||
|
||||
# 技能扫描器
|
||||
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 }}
|
||||
device-auth-verification-uri: {{ .Values.deviceAuthVerificationUri | 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 }}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -30,14 +30,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,7 +66,7 @@ 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
|
||||
|
|
@ -88,12 +89,12 @@ spec:
|
|||
- 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:
|
||||
|
|
@ -108,6 +109,17 @@ 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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
88
charts/skillhub/templates/validate.yaml
Normal file
88
charts/skillhub/templates/validate.yaml
Normal 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 -}}
|
||||
174
charts/skillhub/tests/configuration-contracts.sh
Executable file
174
charts/skillhub/tests/configuration-contracts.sh
Executable file
|
|
@ -0,0 +1,174 @@
|
|||
#!/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"
|
||||
|
||||
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 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"
|
||||
|
||||
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"
|
||||
14
charts/skillhub/tests/test-values.yaml
Normal file
14
charts/skillhub/tests/test-values.yaml
Normal 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
|
||||
453
charts/skillhub/values.schema.json
Normal file
453
charts/skillhub/values.schema.json
Normal file
|
|
@ -0,0 +1,453 @@
|
|||
{
|
||||
"$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", "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" }
|
||||
}
|
||||
},
|
||||
"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" }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -31,12 +31,13 @@ builtinSkills:
|
|||
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
|
||||
|
|
@ -86,6 +87,8 @@ springProfilesActive: docker
|
|||
existingSecret: ""
|
||||
|
||||
secrets:
|
||||
# 默认禁止随机 Secret;仅在非 GitOps 临时环境中按需启用
|
||||
allowAutoGenerated: false
|
||||
bootstrapAdminPassword: ""
|
||||
downloadAnonCookieSecret: ""
|
||||
oauth2GithubClientId: ""
|
||||
|
|
@ -122,8 +125,8 @@ postgresql:
|
|||
limits:
|
||||
cpu: 500m
|
||||
memory: 1Gi
|
||||
extraEnv:
|
||||
- name: POSTGRES_MAX_CONNECTIONS
|
||||
extraEnvVars:
|
||||
- name: POSTGRESQL_MAX_CONNECTIONS
|
||||
value: "500"
|
||||
podAnnotations: {}
|
||||
podSecurityContext:
|
||||
|
|
@ -155,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
|
||||
|
|
@ -162,7 +169,7 @@ postgresql:
|
|||
enabled: false
|
||||
|
||||
externalDatabase:
|
||||
host: postgres.example.com
|
||||
host: ""
|
||||
port: 5432
|
||||
database: skillhub
|
||||
username: skillhub
|
||||
|
|
@ -180,7 +187,6 @@ redis:
|
|||
auth:
|
||||
enabled: true
|
||||
password: ""
|
||||
sentinelPassword: ""
|
||||
|
||||
master:
|
||||
persistence:
|
||||
|
|
@ -222,11 +228,11 @@ redis:
|
|||
sentinel:
|
||||
enabled: false
|
||||
masterSet: mymaster
|
||||
nodes: ""
|
||||
service:
|
||||
enabled: true
|
||||
ports:
|
||||
sentinel: 26379
|
||||
containerPorts:
|
||||
sentinel: 26379
|
||||
|
||||
metrics:
|
||||
enabled: true
|
||||
|
|
@ -234,7 +240,7 @@ redis:
|
|||
enabled: false
|
||||
|
||||
externalRedis:
|
||||
host: redis.example.com
|
||||
host: ""
|
||||
port: 6379
|
||||
password: ""
|
||||
sentinel:
|
||||
|
|
@ -254,6 +260,13 @@ server:
|
|||
registry: ""
|
||||
tag: ""
|
||||
|
||||
dependencyWait:
|
||||
image:
|
||||
registry: docker.io
|
||||
repository: library/busybox
|
||||
tag: "1.37"
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
service:
|
||||
enabled: true
|
||||
type: ClusterIP
|
||||
|
|
|
|||
|
|
@ -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:}}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue