mirror of
https://github.com/iflytek/skillhub.git
synced 2026-09-11 22:51:04 +00:00
Merge pull request #445 from jangrui/feature/helm-chart
feat(chart): 添加 Helm Chart 部署方案
This commit is contained in:
commit
e9cd8322a0
34 changed files with 3764 additions and 9 deletions
217
.github/workflows/pr-helm-chart.yml
vendored
Normal file
217
.github/workflows/pr-helm-chart.yml
vendored
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
name: PR Helm Chart
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- charts/skillhub/**
|
||||
- .github/workflows/pr-helm-chart.yml
|
||||
- .github/workflows/publish-chart.yml
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
- ready_for_review
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: pr-helm-chart-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint Chart
|
||||
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: charts/skillhub
|
||||
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Helm
|
||||
uses: azure/setup-helm@v4
|
||||
with:
|
||||
version: v3.19.0
|
||||
|
||||
- name: Build dependencies
|
||||
run: helm dependency build .
|
||||
|
||||
- name: Lint chart
|
||||
run: helm lint --strict . -f tests/test-values.yaml
|
||||
|
||||
- name: Validate configuration contracts
|
||||
run: bash tests/configuration-contracts.sh
|
||||
|
||||
- name: Validate chart metadata
|
||||
run: |
|
||||
CHART_VERSION=$(helm show chart . | grep '^version:' | awk '{print $2}')
|
||||
APP_VERSION=$(helm show chart . | grep '^appVersion:' | awk '{print $2}')
|
||||
echo "Chart version: $CHART_VERSION"
|
||||
echo "App version: $APP_VERSION"
|
||||
if [ -z "$CHART_VERSION" ]; then
|
||||
echo "ERROR: Chart version is empty"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
template:
|
||||
name: Template Validation
|
||||
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: charts/skillhub
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
scenario:
|
||||
- name: bitnami-default
|
||||
description: Bitnami 默认配置
|
||||
args: ""
|
||||
- name: external-db-redis
|
||||
description: 外部 PostgreSQL + Redis
|
||||
args: >-
|
||||
--set postgresql.enabled=false
|
||||
--set redis.enabled=false
|
||||
--set externalDatabase.host=postgres.example.com
|
||||
--set externalDatabase.password=secret
|
||||
--set externalRedis.host=redis.example.com
|
||||
--set externalRedis.password=secret
|
||||
- name: postgresql-replication
|
||||
description: PostgreSQL 主从 + Redis 主从
|
||||
args: >-
|
||||
--set postgresql.architecture=replication
|
||||
--set redis.architecture=replication
|
||||
- name: redis-sentinel
|
||||
description: Redis 哨兵模式
|
||||
args: >-
|
||||
--set redis.architecture=replication
|
||||
--set redis.sentinel.enabled=true
|
||||
- name: ingress-tls-certmanager
|
||||
description: Ingress + TLS + cert-manager
|
||||
args: >-
|
||||
--set ingress.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 存储
|
||||
args: >-
|
||||
--set s3.enabled=true
|
||||
--set s3.bucket=test-bucket
|
||||
--set s3.endpoint=https://s3.amazonaws.com
|
||||
--set s3.region=us-east-1
|
||||
- name: external-secret
|
||||
description: 外部 Secret
|
||||
args: >-
|
||||
--set existingSecret=my-custom-secret
|
||||
- name: scanner-disabled
|
||||
description: 禁用 Scanner
|
||||
args: >-
|
||||
--set scanner.enabled=false
|
||||
- name: hpa-pdb
|
||||
description: HPA + PDB
|
||||
args: >-
|
||||
--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
|
||||
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Helm
|
||||
uses: azure/setup-helm@v4
|
||||
with:
|
||||
version: v3.19.0
|
||||
|
||||
- name: Build dependencies
|
||||
run: helm dependency build .
|
||||
|
||||
- name: Render template - ${{ matrix.scenario.name }}
|
||||
run: |
|
||||
echo "## ${{ matrix.scenario.description }}"
|
||||
helm template test-release . -f tests/test-values.yaml ${{ matrix.scenario.args }} > rendered.yaml
|
||||
echo "✅ Template rendered successfully"
|
||||
|
||||
- name: Validate resources
|
||||
run: |
|
||||
RESOURCES=$(grep -c '^kind:' rendered.yaml || true)
|
||||
echo "Rendered $RESOURCES resources for ${{ matrix.scenario.name }}"
|
||||
if [ "$RESOURCES" -eq 0 ]; then
|
||||
echo "ERROR: No resources rendered for ${{ matrix.scenario.name }}"
|
||||
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@sha256:faffaf43f95aa6425306e1ab8d6fcad72acb9049158f38e574c085ea1ec0f64e # v0.8.0
|
||||
with:
|
||||
entrypoint: '/kubeconform'
|
||||
args: "-strict -summary -output text -schema-location default -schema-location https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json charts/skillhub/rendered.yaml"
|
||||
|
||||
install-upgrade:
|
||||
name: Install and Upgrade Smoke (${{ matrix.scenario }})
|
||||
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
scenario:
|
||||
- default
|
||||
- sentinel
|
||||
- s3
|
||||
- ingress-tls
|
||||
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Helm
|
||||
uses: azure/setup-helm@v4
|
||||
with:
|
||||
version: v3.19.0
|
||||
|
||||
- name: Create Kubernetes cluster
|
||||
uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1.14.0
|
||||
with:
|
||||
cluster_name: skillhub-helm-smoke
|
||||
wait: 120s
|
||||
|
||||
- name: Run install and upgrade smoke
|
||||
env:
|
||||
HELM_SMOKE_SCENARIO: ${{ matrix.scenario }}
|
||||
run: bash charts/skillhub/tests/install-upgrade-smoke.sh
|
||||
2
.github/workflows/pr-scripts.yml
vendored
2
.github/workflows/pr-scripts.yml
vendored
|
|
@ -12,7 +12,9 @@ on:
|
|||
- 'web/nginx.conf.template'
|
||||
- '.github/workflows/pr-cli.yml'
|
||||
- '.github/workflows/pr-e2e.yml'
|
||||
- '.github/workflows/pr-helm-chart.yml'
|
||||
- '.github/workflows/pr-tests.yml'
|
||||
- '.github/workflows/publish-chart.yml'
|
||||
- '.github/workflows/security.yml'
|
||||
- '.github/workflows/pr-scripts.yml'
|
||||
- '**/*.py'
|
||||
|
|
|
|||
86
.github/workflows/publish-chart.yml
vendored
Normal file
86
.github/workflows/publish-chart.yml
vendored
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
name: Publish Helm Chart
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: Chart and application version (for example, 0.2.14)
|
||||
required: true
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: publish-chart-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
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:
|
||||
working-directory: charts/skillhub
|
||||
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Helm
|
||||
uses: azure/setup-helm@v4
|
||||
with:
|
||||
version: v3.19.0
|
||||
|
||||
- name: Verify dependencies
|
||||
run: helm dependency build .
|
||||
|
||||
- name: Login to GHCR
|
||||
run: echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ghcr.io -u ${{ github.actor }} --password-stdin
|
||||
|
||||
- name: Parse version from tag
|
||||
id: ver
|
||||
run: |
|
||||
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
|
||||
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 . -f tests/test-values.yaml
|
||||
|
||||
- name: Package and push
|
||||
run: |
|
||||
helm package . \
|
||||
--version "${{ steps.ver.outputs.version }}" \
|
||||
--app-version "${{ steps.ver.outputs.version }}" \
|
||||
--destination /tmp/helm-charts
|
||||
helm push /tmp/helm-charts/skillhub-${{ steps.ver.outputs.version }}.tgz \
|
||||
oci://ghcr.io/${{ github.repository_owner }}/charts
|
||||
|
||||
- name: Upload chart artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: skillhub-${{ steps.ver.outputs.version }}.tgz
|
||||
path: /tmp/helm-charts/skillhub-${{ steps.ver.outputs.version }}.tgz
|
||||
retention-days: 90
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -84,5 +84,8 @@ docs/superpowers/
|
|||
# Local workspace metadata
|
||||
CLAUDE.md
|
||||
|
||||
# Helm chart dependencies
|
||||
charts/skillhub/charts/*.tgz
|
||||
|
||||
# Local config file
|
||||
.mcp.json
|
||||
|
|
|
|||
14
README.md
14
README.md
|
|
@ -345,6 +345,20 @@ Basic Kubernetes manifests are available under [`deploy/k8s/`](./deploy/k8s):
|
|||
- `services.yaml`
|
||||
- `ingress.yaml`
|
||||
|
||||
For a configurable deployment with bundled PostgreSQL and Redis dependencies,
|
||||
use the Helm chart under [`charts/skillhub/`](./charts/skillhub):
|
||||
|
||||
```bash
|
||||
helm dependency build ./charts/skillhub
|
||||
helm upgrade --install skillhub ./charts/skillhub \
|
||||
--namespace skillhub \
|
||||
--create-namespace \
|
||||
-f values-production.yaml
|
||||
```
|
||||
|
||||
See the [Helm chart guide](./charts/skillhub/README.md) for required secrets,
|
||||
Ingress/TLS, external data services, storage migration, and upgrade constraints.
|
||||
|
||||
Apply them after creating your own secret:
|
||||
|
||||
```bash
|
||||
|
|
|
|||
10
README_zh.md
10
README_zh.md
|
|
@ -228,8 +228,10 @@ curl -fsSL https://imageless.oss-cn-beijing.aliyuncs.com/runtime.sh | sh -s -- u
|
|||
# 应用 Kubernetes 清单
|
||||
kubectl apply -f deploy/k8s/
|
||||
|
||||
# 或使用 Helm(即将推出)
|
||||
helm install skillhub ./deploy/helm
|
||||
# 或使用 Helm Chart
|
||||
helm dependency build ./charts/skillhub
|
||||
helm upgrade --install skillhub ./charts/skillhub -n skillhub --create-namespace \
|
||||
-f values-production.yaml
|
||||
```
|
||||
|
||||
### 环境变量
|
||||
|
|
@ -319,7 +321,7 @@ SkillHub 采用清晰的分层架构:
|
|||
### 基础设施
|
||||
- **容器化**:Docker & Docker Compose
|
||||
- **监控**:Prometheus + Grafana
|
||||
- **部署**:Kubernetes 清单
|
||||
- **部署**:Kubernetes 清单与 Helm Chart
|
||||
- **CI/CD**:GitHub Actions
|
||||
|
||||
## 路线图
|
||||
|
|
@ -332,7 +334,7 @@ SkillHub 采用清晰的分层架构:
|
|||
- [x] API 令牌管理
|
||||
- [x] 账户合并
|
||||
- [x] 国际化支持
|
||||
- [ ] Helm Chart 部署
|
||||
- [x] Helm Chart 部署
|
||||
- [ ] 高级搜索过滤器
|
||||
- [ ] 技能依赖管理
|
||||
- [ ] Webhook 集成
|
||||
|
|
|
|||
24
charts/skillhub/.helmignore
Normal file
24
charts/skillhub/.helmignore
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Editors / IDEs
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Local tooling
|
||||
.claude/
|
||||
CLAUDE.md
|
||||
|
||||
# Git
|
||||
.git/
|
||||
.gitignore
|
||||
.gitattributes
|
||||
|
||||
# CI
|
||||
.github/
|
||||
|
||||
# Source-only contract tests
|
||||
tests/
|
||||
9
charts/skillhub/Chart.lock
Normal file
9
charts/skillhub/Chart.lock
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
dependencies:
|
||||
- name: postgresql
|
||||
repository: oci://registry-1.docker.io/bitnamicharts
|
||||
version: 18.6.10
|
||||
- name: redis
|
||||
repository: oci://registry-1.docker.io/bitnamicharts
|
||||
version: 25.5.3
|
||||
digest: sha256:20336709650cc49c81b8b4afdac0efeeea00cb88ff87820be9272ef5a7d545cc
|
||||
generated: "2026-05-31T08:35:16.614393+08:00"
|
||||
27
charts/skillhub/Chart.yaml
Normal file
27
charts/skillhub/Chart.yaml
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
apiVersion: v2
|
||||
name: skillhub
|
||||
description: Self-hosted, open-source agent skill registry for enterprises.
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: 0.2.14
|
||||
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
|
||||
|
||||
dependencies:
|
||||
# PostgreSQL - Bitnami 官方 chart,支持 HA、备份、监控
|
||||
- name: postgresql
|
||||
version: "18.6.10"
|
||||
repository: "oci://registry-1.docker.io/bitnamicharts"
|
||||
condition: postgresql.enabled
|
||||
|
||||
# Redis - Bitnami 官方 chart,支持集群模式、哨兵模式
|
||||
- name: redis
|
||||
version: "25.5.3"
|
||||
repository: "oci://registry-1.docker.io/bitnamicharts"
|
||||
condition: redis.enabled
|
||||
475
charts/skillhub/README.md
Normal file
475
charts/skillhub/README.md
Normal file
|
|
@ -0,0 +1,475 @@
|
|||
# SkillHub Helm Chart
|
||||
|
||||
企业级 AI 技能中心私有化部署方案,基于 Kubernetes 和 Helm。
|
||||
|
||||
## 特性
|
||||
|
||||
- **微服务架构**:Server(Spring Boot)、Web(Nginx)、Scanner 分离部署
|
||||
- **高可用**:支持 HPA 自动扩缩容、PDB Pod 中断预算
|
||||
- **数据层**:使用 Bitnami PostgreSQL/Redis,支持主从复制、哨兵模式
|
||||
- **安全**:TLS 证书管理、Secret 密码保护;Bitnami 数据组件默认提供 NetworkPolicy
|
||||
- **可观测性**:内置 Prometheus metrics exporter
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 前置要求
|
||||
|
||||
- Kubernetes 1.24+
|
||||
- Helm 3.8+
|
||||
- kubectl configured
|
||||
|
||||
### 安装
|
||||
|
||||
先创建受保护的 `values-production.yaml`。以下值必须替换为实际随机强密码:
|
||||
|
||||
```yaml
|
||||
secrets:
|
||||
allowAutoGenerated: false
|
||||
bootstrapAdminPassword: "<固定管理员密码>"
|
||||
downloadAnonCookieSecret: "<至少32字符的固定随机值>"
|
||||
|
||||
postgresql:
|
||||
auth:
|
||||
postgresPassword: "<固定PostgreSQL管理员密码>"
|
||||
password: "<固定skillhub用户密码>"
|
||||
|
||||
redis:
|
||||
auth:
|
||||
password: "<固定Redis密码>"
|
||||
```
|
||||
|
||||
```bash
|
||||
helm dependency build ./charts/skillhub
|
||||
kubectl create namespace skillhub
|
||||
|
||||
helm -n skillhub upgrade -i skillhub ./charts/skillhub \
|
||||
-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 示例中的数组结构。
|
||||
|
||||
合并或发布前,可在一个空的测试 Kubernetes 集群中运行可重复的安装/升级 smoke:
|
||||
|
||||
```bash
|
||||
for scenario in default sentinel s3 ingress-tls; do
|
||||
HELM_SMOKE_SCENARIO="$scenario" \
|
||||
bash charts/skillhub/tests/install-upgrade-smoke.sh
|
||||
done
|
||||
```
|
||||
|
||||
脚本验证 `install -> Ready -> HTTP health -> upgrade -> Ready`,并确认 Secret 数据、
|
||||
PVC UID 与绑定 PV 在升级前后保持不变。四个场景分别覆盖默认依赖、Redis
|
||||
Sentinel、实际 MinIO S3 连接,以及由 Kubernetes API 接受的 TLS Ingress 路由。
|
||||
默认清理自己创建的 namespace;设置 `KEEP_HELM_SMOKE=true` 可保留现场用于排查。
|
||||
|
||||
### 高可用模式
|
||||
|
||||
```bash
|
||||
helm -n skillhub upgrade -i skillhub ./charts/skillhub \
|
||||
-f values-production.yaml \
|
||||
--set postgresql.architecture=replication \
|
||||
--set postgresql.auth.replicationPassword=your-replication-password \
|
||||
--set redis.architecture=replication
|
||||
```
|
||||
|
||||
### 外部数据库模式
|
||||
|
||||
```bash
|
||||
helm -n skillhub upgrade -i skillhub ./charts/skillhub \
|
||||
-f values-production.yaml \
|
||||
--set postgresql.enabled=false \
|
||||
--set redis.enabled=false \
|
||||
--set externalDatabase.host=postgres.example.com \
|
||||
--set externalDatabase.port=5432 \
|
||||
--set externalDatabase.database=skillhub \
|
||||
--set externalDatabase.username=skillhub \
|
||||
--set externalDatabase.password=your-db-password \
|
||||
--set externalRedis.host=redis.example.com \
|
||||
--set externalRedis.port=6379 \
|
||||
--set externalRedis.password=your-redis-password
|
||||
```
|
||||
|
||||
### 使用 existingSecret
|
||||
|
||||
通过 `existingSecret` 引用已存在的 Secret 对象,避免在 values 中明文写入密码。
|
||||
内置 PostgreSQL/Redis 使用各自的 Bitnami Secret,不需要复制到该 Secret。
|
||||
|
||||
| Key | 必填 | 说明 |
|
||||
|-----|------|------|
|
||||
| `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 模型名称 |
|
||||
| `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 或密钥注入插件中。
|
||||
|
||||
内置 PostgreSQL、Redis、Sentinel 及 metrics exporter 镜像默认使用不可变的
|
||||
多架构 manifest digest,避免 Bitnami 子 Chart 的 `latest` 默认值造成不可复现的
|
||||
安装和回滚,同时保留 amd64/arm64 支持。覆盖私有镜像仓库或 tag 时,必须同时把
|
||||
对应的 `image.digest` 设为空,或改成私有仓库中该镜像的真实 digest;digest
|
||||
非空时会优先于 tag。
|
||||
|
||||
## 配置参考
|
||||
|
||||
### 副本数配置
|
||||
|
||||
| 参数 | 描述 | 默认值 |
|
||||
|------|------|--------|
|
||||
| `server.replicaCount` | Server 副本数 | `1` |
|
||||
| `web.replicaCount` | Web 副本数 | `1` |
|
||||
| `scanner.replicaCount` | Scanner 副本数 | `1` |
|
||||
|
||||
```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。
|
||||
|
||||
### 服务配置
|
||||
|
||||
| 参数 | 描述 | 默认值 |
|
||||
|------|------|--------|
|
||||
| `server.service.type` | Server Service 类型 | `ClusterIP` |
|
||||
| `server.service.port` | Server 端口 | `8080` |
|
||||
| `web.service.type` | Web Service 类型 | `ClusterIP` |
|
||||
| `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.14
|
||||
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
|
||||
digest: ""
|
||||
metrics:
|
||||
image:
|
||||
registry: registry.example.com
|
||||
repository: library/postgres-exporter
|
||||
tag: 0.20.1
|
||||
digest: ""
|
||||
|
||||
redis:
|
||||
image:
|
||||
registry: registry.example.com
|
||||
repository: library/redis
|
||||
tag: 8.8.0
|
||||
digest: ""
|
||||
sentinel:
|
||||
image:
|
||||
registry: registry.example.com
|
||||
repository: library/redis-sentinel
|
||||
tag: 8.8.0
|
||||
digest: ""
|
||||
metrics:
|
||||
image:
|
||||
registry: registry.example.com
|
||||
repository: library/redis-exporter
|
||||
tag: 1.86.0
|
||||
digest: ""
|
||||
```
|
||||
|
||||
`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='<密码>'
|
||||
```
|
||||
|
||||
### 数据库配置
|
||||
|
||||
| 参数 | 描述 | 默认值 |
|
||||
|------|------|--------|
|
||||
| `postgresql.enabled` | 启用内置 PostgreSQL | `true` |
|
||||
| `postgresql.architecture` | 架构模式 | `standalone` |
|
||||
| `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。节点地址由副本数自动生成,不需要手动配置。由于 Bitnami Sentinel
|
||||
上报的 Pod 地址可能与客户端连接的 Headless Service FQDN 不同,Chart 仅在该
|
||||
内置模式下关闭 Redisson 的 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`。外部 Sentinel 默认保留 Redisson 地址一致性检查;
|
||||
只有已确认服务发现会改写节点地址时,才通过 `server.extraEnv` 显式设置
|
||||
`SKILLHUB_REDIS_SENTINEL_CHECK_SENTINELS_LIST=false`:
|
||||
|
||||
```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"]'
|
||||
```
|
||||
|
||||
确需关闭检查时,在 values 文件中显式记录该兼容例外:
|
||||
|
||||
```yaml
|
||||
server:
|
||||
extraEnv:
|
||||
- name: SKILLHUB_REDIS_SENTINEL_CHECK_SENTINELS_LIST
|
||||
value: "false"
|
||||
```
|
||||
|
||||
### 存储配置
|
||||
|
||||
| 参数 | 描述 | 默认值 |
|
||||
|------|------|--------|
|
||||
| `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 \
|
||||
-f values-production.yaml
|
||||
```
|
||||
|
||||
### S3 对象存储
|
||||
|
||||
`s3.enabled=true` 时,不创建 PVC,应用使用 S3 作为存储后端。
|
||||
|
||||
| 参数 | 描述 | 默认值 |
|
||||
|------|------|--------|
|
||||
| `s3.enabled` | 启用 S3 | `false` |
|
||||
| `s3.bucket` | Bucket 名称 | `skillhub-storage` |
|
||||
| `s3.endpoint` | S3 端点,非空时必须是绝对 HTTP(S) URL | `""` |
|
||||
| `s3.publicEndpoint` | S3 公网访问端点,非空时必须是绝对 HTTP(S) URL | `""` |
|
||||
| `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 \
|
||||
-f values-production.yaml \
|
||||
--set s3.enabled=true \
|
||||
--set s3.bucket=your-bucket \
|
||||
--set s3.endpoint=https://s3.amazonaws.com \
|
||||
--set s3.region=us-east-1 \
|
||||
--set s3.accessKey=your-access-key \
|
||||
--set s3.secretKey=your-secret-key
|
||||
```
|
||||
|
||||
### Ingress + TLS
|
||||
|
||||
```bash
|
||||
helm -n skillhub upgrade -i skillhub ./charts/skillhub \
|
||||
-f values-production.yaml \
|
||||
--set ingress.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`、`/oauth2`、
|
||||
`/login/oauth2` 和 `/.well-known` 直接转发给 Server,确保 TLS 终止后的 OAuth
|
||||
回调协议保持正确;`hosts[].paths` 中的其他路径转发给 Web,因此上述四个前缀
|
||||
均为保留路径。`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.storage.accessMode=ReadWriteMany
|
||||
```
|
||||
|
||||
每个 HPA 至少需要一个非零 CPU 或内存利用率目标。本地存储的 Server HPA 同样
|
||||
要求 RWX;也可以启用 S3 来避免共享 PVC。
|
||||
|
||||
## 发布
|
||||
|
||||
`.github/workflows/publish-chart.yml` 在 GitHub Release 发布后或手动
|
||||
`workflow_dispatch` 时运行。Release tag 必须使用 `vX.Y.Z`、`chart-vX.Y.Z` 或
|
||||
`helm-vX.Y.Z`;手动运行时显式输入 `X.Y.Z`。工作流按该版本打包 Chart,并推送到
|
||||
`oci://ghcr.io/iflytek/charts`,同时保留构建 artifact。
|
||||
|
||||
## 卸载
|
||||
|
||||
```bash
|
||||
helm -n skillhub uninstall skillhub
|
||||
```
|
||||
|
||||
Server 数据 PVC 带有 `helm.sh/resource-policy: keep`,卸载 release 后仍会保留,
|
||||
需要确认数据不再使用后手动删除。
|
||||
|
||||
## 依赖
|
||||
|
||||
| 依赖 | 版本 |
|
||||
|------|------|
|
||||
| postgresql | 18.6.10 |
|
||||
| redis | 25.5.3 |
|
||||
255
charts/skillhub/templates/_helpers.tpl
Normal file
255
charts/skillhub/templates/_helpers.tpl
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
{{- /*
|
||||
SkillHub Helm Chart 模板辅助函数
|
||||
*/}}
|
||||
|
||||
{{- /* 名称 */}}
|
||||
{{- define "skillhub.name" -}}
|
||||
{{- default "skillhub" .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{- /* 完整名称 */}}
|
||||
{{- define "skillhub.fullname" -}}
|
||||
{{- if .Values.fullnameOverride }}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- $name := default "skillhub" .Values.nameOverride }}
|
||||
{{- if contains $name .Release.Name }}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- /* Chart 标签 */}}
|
||||
{{- define "skillhub.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{- /* 通用标签 */}}
|
||||
{{- define "skillhub.labels" -}}
|
||||
helm.sh/chart: {{ include "skillhub.chart" . }}
|
||||
{{ include "skillhub.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
app.kubernetes.io/part-of: skillhub
|
||||
{{- end }}
|
||||
|
||||
{{- /* 选择器标签 */}}
|
||||
{{- define "skillhub.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "skillhub.name" . }}
|
||||
{{- end }}
|
||||
|
||||
{{- /* 组件标签 */}}
|
||||
{{- define "skillhub.server.labels" -}}
|
||||
{{ include "skillhub.labels" . }}
|
||||
app.kubernetes.io/component: server
|
||||
{{- end }}
|
||||
{{- define "skillhub.server.selectorLabels" -}}
|
||||
{{ include "skillhub.selectorLabels" . }}
|
||||
app.kubernetes.io/component: server
|
||||
{{- end }}
|
||||
|
||||
{{- define "skillhub.web.labels" -}}
|
||||
{{ include "skillhub.labels" . }}
|
||||
app.kubernetes.io/component: web
|
||||
{{- end }}
|
||||
{{- define "skillhub.web.selectorLabels" -}}
|
||||
{{ include "skillhub.selectorLabels" . }}
|
||||
app.kubernetes.io/component: web
|
||||
{{- end }}
|
||||
|
||||
{{- define "skillhub.scanner.labels" -}}
|
||||
{{ include "skillhub.labels" . }}
|
||||
app.kubernetes.io/component: scanner
|
||||
{{- end }}
|
||||
{{- define "skillhub.scanner.selectorLabels" -}}
|
||||
{{ include "skillhub.selectorLabels" . }}
|
||||
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 := include "skillhub.postgresql.fullname" . -}}
|
||||
{{- if eq .Values.postgresql.architecture "replication" -}}
|
||||
{{- printf "%s-primary" $prefix -}}
|
||||
{{- else -}}
|
||||
{{- $prefix -}}
|
||||
{{- end -}}
|
||||
{{- else -}}
|
||||
{{- .Values.externalDatabase.host -}}
|
||||
{{- end -}}
|
||||
{{- end }}
|
||||
|
||||
{{- /* PostgreSQL Port */}}
|
||||
{{- define "skillhub.postgresql.port" -}}
|
||||
{{- if .Values.postgresql.enabled -}}
|
||||
{{- print "5432" -}}
|
||||
{{- else -}}
|
||||
{{- .Values.externalDatabase.port | default 5432 | int -}}
|
||||
{{- end -}}
|
||||
{{- end }}
|
||||
|
||||
{{- /* PostgreSQL Database */}}
|
||||
{{- define "skillhub.postgresql.database" -}}
|
||||
{{- if .Values.postgresql.enabled -}}
|
||||
{{- .Values.postgresql.auth.database -}}
|
||||
{{- else -}}
|
||||
{{- .Values.externalDatabase.database -}}
|
||||
{{- end -}}
|
||||
{{- end }}
|
||||
|
||||
{{- /* PostgreSQL Username */}}
|
||||
{{- define "skillhub.postgresql.username" -}}
|
||||
{{- if .Values.postgresql.enabled -}}
|
||||
{{- .Values.postgresql.auth.username -}}
|
||||
{{- else -}}
|
||||
{{- .Values.externalDatabase.username -}}
|
||||
{{- end -}}
|
||||
{{- end }}
|
||||
|
||||
{{- /* PostgreSQL Secret Name */}}
|
||||
{{- define "skillhub.postgresql.secretName" -}}
|
||||
{{- if .Values.postgresql.enabled -}}
|
||||
{{- .Values.postgresql.auth.existingSecret | default (include "skillhub.postgresql.fullname" .) -}}
|
||||
{{- else -}}
|
||||
{{- include "skillhub.secretName" . -}}
|
||||
{{- end -}}
|
||||
{{- end }}
|
||||
|
||||
{{- /* PostgreSQL 密码 Secret key;postgres 使用管理员密码,其他用户使用应用密码 */}}
|
||||
{{- 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 -}}
|
||||
{{- printf "jdbc:postgresql://%s:5432/%s" (include "skillhub.postgresql.host" .) .Values.postgresql.auth.database -}}
|
||||
{{- else -}}
|
||||
{{- if .Values.externalDatabase.jdbcUrl -}}
|
||||
{{- .Values.externalDatabase.jdbcUrl -}}
|
||||
{{- else -}}
|
||||
{{- printf "jdbc:postgresql://%s:%d/%s" .Values.externalDatabase.host (.Values.externalDatabase.port | default 5432 | int) .Values.externalDatabase.database -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end }}
|
||||
|
||||
{{- /* Redis Sentinel 节点列表(Redisson 需要具体 pod FQDN,格式: {pod}.{headless-svc}.{ns}.svc.cluster.local) */}}
|
||||
{{- define "skillhub.redis.sentinel.nodes" -}}
|
||||
{{- $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:%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 -}}
|
||||
{{- include "skillhub.redis.fullname" . -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s-master" (include "skillhub.redis.fullname" .) -}}
|
||||
{{- end -}}
|
||||
{{- else -}}
|
||||
{{- .Values.externalRedis.host -}}
|
||||
{{- end -}}
|
||||
{{- end }}
|
||||
|
||||
{{- /* Redis Port */}}
|
||||
{{- define "skillhub.redis.port" -}}
|
||||
{{- if .Values.redis.enabled -}}
|
||||
{{- if .Values.redis.sentinel.enabled -}}
|
||||
{{- .Values.redis.sentinel.service.ports.sentinel | default 26379 -}}
|
||||
{{- else -}}
|
||||
{{- 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 -}}
|
||||
{{- .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" .)) }}
|
||||
{{- end }}
|
||||
|
||||
{{- /* PostgreSQL Service 名称(用于 server initContainer 等待) */}}
|
||||
{{- define "skillhub.postgresql.serviceName" -}}
|
||||
{{- if .Values.postgresql.enabled -}}
|
||||
{{- include "skillhub.postgresql.host" . -}}
|
||||
{{- else -}}
|
||||
{{- .Values.externalDatabase.host -}}
|
||||
{{- end -}}
|
||||
{{- end }}
|
||||
|
||||
{{- /* Redis Service 名称(用于 server initContainer 等待) */}}
|
||||
{{- define "skillhub.redis.serviceName" -}}
|
||||
{{- 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 }}
|
||||
24
charts/skillhub/templates/certificate.yaml
Normal file
24
charts/skillhub/templates/certificate.yaml
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{{- 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: {{ $tls.secretName }}-cert
|
||||
labels:
|
||||
{{- include "skillhub.labels" $ | nindent 4 }}
|
||||
spec:
|
||||
secretName: {{ $tls.secretName }}
|
||||
duration: 2160h
|
||||
renewBefore: 360h
|
||||
dnsNames:
|
||||
{{- range $tls.hosts }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
issuerRef:
|
||||
name: {{ $.Values.ingress.certManager.issuerName | quote }}
|
||||
kind: {{ $.Values.ingress.certManager.issuerKind | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
57
charts/skillhub/templates/configmap.yaml
Normal file
57
charts/skillhub/templates/configmap.yaml
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
{{- /*
|
||||
SkillHub 应用 ConfigMap
|
||||
*/}}
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ include "skillhub.fullname" . }}-config
|
||||
labels:
|
||||
{{- include "skillhub.labels" . | nindent 4 }}
|
||||
data:
|
||||
# Redis 配置
|
||||
redis-host: {{ include "skillhub.redis.host" . | quote }}
|
||||
redis-port: {{ include "skillhub.redis.port" . | quote }}
|
||||
|
||||
# 存储路径
|
||||
storage-base-path: "/var/lib/skillhub/storage"
|
||||
|
||||
# 存储提供者: local | s3
|
||||
skillhub-storage-provider: {{ if .Values.s3.enabled }}"s3"{{ else }}"local"{{ end }}
|
||||
|
||||
{{- if .Values.s3.enabled }}
|
||||
# S3 配置
|
||||
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: {{ 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 | 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: {{ 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 }}
|
||||
40
charts/skillhub/templates/hpa.yaml
Normal file
40
charts/skillhub/templates/hpa.yaml
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
{{- 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 }}
|
||||
---
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ include "skillhub.fullname" $ }}-{{ $name }}
|
||||
labels:
|
||||
{{- include (printf "skillhub.%s.labels" $name) $ | nindent 4 }}
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: {{ include "skillhub.fullname" $ }}-{{ $name }}
|
||||
minReplicas: {{ $component.autoscaling.minReplicas }}
|
||||
maxReplicas: {{ $component.autoscaling.maxReplicas }}
|
||||
metrics:
|
||||
{{- if $component.autoscaling.targetCPUUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ $component.autoscaling.targetCPUUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- if $component.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: memory
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ $component.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
64
charts/skillhub/templates/ingress.yaml
Normal file
64
charts/skillhub/templates/ingress.yaml
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
{{- if .Values.ingress.enabled }}
|
||||
{{- $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:
|
||||
{{- toYaml .Values.ingress.annotations | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if .Values.ingress.className }}
|
||||
ingressClassName: {{ .Values.ingress.className | quote }}
|
||||
{{- end }}
|
||||
{{- if .Values.ingress.tls }}
|
||||
tls:
|
||||
{{- toYaml .Values.ingress.tls | nindent 4 }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- range $host := $hosts }}
|
||||
- host: {{ $host.host | quote }}
|
||||
http:
|
||||
paths:
|
||||
- path: /api
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: {{ include "skillhub.fullname" $ }}-server
|
||||
port:
|
||||
number: {{ $.Values.server.service.port }}
|
||||
- path: /oauth2
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: {{ include "skillhub.fullname" $ }}-server
|
||||
port:
|
||||
number: {{ $.Values.server.service.port }}
|
||||
- path: /login/oauth2
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: {{ include "skillhub.fullname" $ }}-server
|
||||
port:
|
||||
number: {{ $.Values.server.service.port }}
|
||||
- path: /.well-known
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: {{ include "skillhub.fullname" $ }}-server
|
||||
port:
|
||||
number: {{ $.Values.server.service.port }}
|
||||
{{- range $path := $host.paths }}
|
||||
- path: {{ $path.path | quote }}
|
||||
pathType: {{ $path.pathType }}
|
||||
backend:
|
||||
service:
|
||||
name: {{ include "skillhub.fullname" $ }}-web
|
||||
port:
|
||||
number: {{ $.Values.web.service.port }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
21
charts/skillhub/templates/pdb.yaml
Normal file
21
charts/skillhub/templates/pdb.yaml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{{- range $name := list "server" "web" "scanner" }}
|
||||
{{- $component := index $.Values $name }}
|
||||
{{- $enabled := true }}
|
||||
{{- if hasKey $component "enabled" }}
|
||||
{{- $enabled = $component.enabled }}
|
||||
{{- end }}
|
||||
{{- if and $enabled $component.podDisruptionBudget.enabled }}
|
||||
---
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: {{ include "skillhub.fullname" $ }}-{{ $name }}
|
||||
labels:
|
||||
{{- include (printf "skillhub.%s.labels" $name) $ | nindent 4 }}
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include (printf "skillhub.%s.selectorLabels" $name) $ | nindent 6 }}
|
||||
minAvailable: {{ $component.podDisruptionBudget.minAvailable }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
23
charts/skillhub/templates/pvc.yaml
Normal file
23
charts/skillhub/templates/pvc.yaml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{{- if and .Values.server.enabled (not .Values.s3.enabled) }}
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: {{ include "skillhub.fullname" . }}-server-data
|
||||
labels:
|
||||
{{- include "skillhub.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
helm.sh/resource-policy: keep
|
||||
spec:
|
||||
{{- $accessMode := .Values.server.storage.accessMode }}
|
||||
{{- if not $accessMode }}
|
||||
{{- $accessMode = "ReadWriteOnce" }}
|
||||
{{- end }}
|
||||
accessModes:
|
||||
- {{ $accessMode }}
|
||||
{{- if .Values.server.storage.storageClassName }}
|
||||
storageClassName: {{ .Values.server.storage.storageClassName | quote }}
|
||||
{{- end }}
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.server.storage.size }}
|
||||
{{- end }}
|
||||
77
charts/skillhub/templates/scanner-deployment.yaml
Normal file
77
charts/skillhub/templates/scanner-deployment.yaml
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
{{- if .Values.scanner.enabled }}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "skillhub.fullname" . }}-scanner
|
||||
labels:
|
||||
{{- include "skillhub.scanner.labels" . | nindent 4 }}
|
||||
spec:
|
||||
{{- if not .Values.scanner.autoscaling.enabled }}
|
||||
replicas: {{ .Values.scanner.replicaCount }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "skillhub.scanner.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "skillhub.scanner.selectorLabels" . | nindent 8 }}
|
||||
annotations:
|
||||
checksum/config: {{ toYaml (dict "scanner" .Values.scanner "secrets" .Values.secrets "existingSecret" .Values.existingSecret) | sha256sum }}
|
||||
{{- range $key, $val := .Values.scanner.podAnnotations }}
|
||||
{{ $key }}: {{ $val }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- $secrets := .Values.scanner.imagePullSecrets }}
|
||||
{{- if $secrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml $secrets | nindent 8 }}
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: scanner
|
||||
image: {{ .Values.scanner.image.registry | default .Values.images.registry }}/skillhub-scanner:{{ .Values.scanner.image.tag | default .Values.images.tag | default (printf "v%s" .Chart.AppVersion) }}
|
||||
imagePullPolicy: {{ .Values.images.pullPolicy }}
|
||||
ports:
|
||||
- containerPort: {{ .Values.scanner.service.port }}
|
||||
name: http
|
||||
env:
|
||||
- name: SKILL_SCANNER_LLM_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
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:
|
||||
name: {{ include "skillhub.secretName" . }}
|
||||
key: skill-scanner-llm-model
|
||||
optional: true
|
||||
{{- with .Values.scanner.extraEnv }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
resources:
|
||||
{{- toYaml .Values.scanner.resources | nindent 12 }}
|
||||
readinessProbe:
|
||||
{{- toYaml .Values.scanner.probes.readiness | nindent 12 }}
|
||||
livenessProbe:
|
||||
{{- toYaml .Values.scanner.probes.liveness | nindent 12 }}
|
||||
{{- with .Values.scanner.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.scanner.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.scanner.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
80
charts/skillhub/templates/secret.yaml
Normal file
80
charts/skillhub/templates/secret.yaml
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
{{- /*
|
||||
SkillHub 应用 Secret
|
||||
- 内置 PostgreSQL/Redis:密码由 Bitnami 管理,从对应 Secret 读取
|
||||
- 外部 PostgreSQL/Redis:密码从 values 或 existingSecret 读取
|
||||
*/}}
|
||||
{{- if not .Values.existingSecret }}
|
||||
{{- $secretName := include "skillhub.secretName" . }}
|
||||
{{- $appSecret := (lookup "v1" "Secret" $.Release.Namespace $secretName) }}
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ $secretName }}
|
||||
labels:
|
||||
{{- include "skillhub.labels" . | nindent 4 }}
|
||||
type: Opaque
|
||||
stringData:
|
||||
{{- if not .Values.postgresql.enabled }}
|
||||
# 外部数据库密码;内置 PostgreSQL 直接引用 Bitnami Secret
|
||||
spring-datasource-password: {{ .Values.externalDatabase.password | quote }}
|
||||
{{- end }}
|
||||
|
||||
{{- if not .Values.redis.enabled }}
|
||||
# 外部 Redis 密码;内置 Redis 直接引用 Bitnami Secret
|
||||
redis-password: {{ .Values.externalRedis.password | default "" | quote }}
|
||||
{{- end }}
|
||||
|
||||
{{- 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 管理员密码
|
||||
# 优先级: secrets.bootstrapAdminPassword → bootstrapAdmin.password → 集群已有 Secret → 随机生成
|
||||
{{- $baPwd := .Values.secrets.bootstrapAdminPassword | default .Values.bootstrapAdmin.password | default "" }}
|
||||
{{- if not $baPwd }}
|
||||
{{- if $appSecret }}
|
||||
{{- $baPwd = index $appSecret.data "bootstrap-admin-password" | default "" | b64dec }}
|
||||
{{- end }}
|
||||
{{- if not $baPwd }}
|
||||
{{- $baPwd = randAlphaNum 16 }}
|
||||
{{- 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 }}
|
||||
{{- end }}
|
||||
{{- if .Values.secrets.oauth2GithubClientSecret }}
|
||||
oauth2-github-client-secret: {{ .Values.secrets.oauth2GithubClientSecret | quote }}
|
||||
{{- end }}
|
||||
|
||||
# Scanner LLM 配置 (optional)
|
||||
{{- 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 }}
|
||||
skillhub-storage-s3-access-key: {{ .Values.s3.accessKey | quote }}
|
||||
{{- end }}
|
||||
{{- if .Values.s3.secretKey }}
|
||||
skillhub-storage-s3-secret-key: {{ .Values.s3.secretKey | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
370
charts/skillhub/templates/server-deployment.yaml
Normal file
370
charts/skillhub/templates/server-deployment.yaml
Normal file
|
|
@ -0,0 +1,370 @@
|
|||
{{- if .Values.server.enabled }}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "skillhub.fullname" . }}-server
|
||||
labels:
|
||||
{{- include "skillhub.server.labels" . | nindent 4 }}
|
||||
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 }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "skillhub.server.selectorLabels" . | nindent 8 }}
|
||||
annotations:
|
||||
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
|
||||
checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}
|
||||
{{- range $key, $val := .Values.server.podAnnotations }}
|
||||
{{ $key }}: {{ $val }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- with .Values.server.podSecurityContext }}
|
||||
securityContext:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- $secrets := .Values.server.imagePullSecrets }}
|
||||
{{- if $secrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml $secrets | nindent 8 }}
|
||||
{{- end }}
|
||||
initContainers:
|
||||
- name: wait-for-dependencies
|
||||
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" . | quote }}
|
||||
- name: DB_PORT
|
||||
value: {{ include "skillhub.postgresql.port" . | quote }}
|
||||
- name: REDIS_HOST
|
||||
value: {{ include "skillhub.redis.serviceName" . | quote }}
|
||||
- name: REDIS_PORT
|
||||
value: {{ include "skillhub.redis.port" . | quote }}
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
echo "Waiting for PostgreSQL at ${DB_HOST}:${DB_PORT}..."
|
||||
until nc -z -w 2 "${DB_HOST}" "${DB_PORT}"; do sleep 2; done
|
||||
echo "PostgreSQL is ready!"
|
||||
echo "Waiting for Redis at ${REDIS_HOST}:${REDIS_PORT}..."
|
||||
until nc -z -w 2 "${REDIS_HOST}" "${REDIS_PORT}"; do sleep 2; done
|
||||
echo "Redis is ready!"
|
||||
containers:
|
||||
- name: server
|
||||
image: {{ .Values.server.image.registry | default .Values.images.registry }}/skillhub-server:{{ .Values.server.image.tag | default .Values.images.tag | default (printf "v%s" .Chart.AppVersion) }}
|
||||
imagePullPolicy: {{ .Values.images.pullPolicy }}
|
||||
ports:
|
||||
- containerPort: {{ .Values.server.service.port }}
|
||||
name: http
|
||||
env:
|
||||
- name: SPRING_PROFILES_ACTIVE
|
||||
{{- $profiles := .Values.springProfilesActive }}
|
||||
{{- if and .Values.redis.enabled .Values.redis.sentinel.enabled }}
|
||||
{{- $profiles = printf "%s,redis-sentinel" $profiles }}
|
||||
{{- else if and (not .Values.redis.enabled) .Values.externalRedis.sentinel.enabled }}
|
||||
{{- $profiles = printf "%s,redis-sentinel" $profiles }}
|
||||
{{- end }}
|
||||
value: {{ $profiles | quote }}
|
||||
|
||||
# Database
|
||||
- name: SPRING_DATASOURCE_URL
|
||||
value: {{ include "skillhub.jdbcUrl" . | quote }}
|
||||
- name: 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" . | quote }}
|
||||
# Bitnami Sentinel pods advertise pod-local addresses that can differ from
|
||||
# the headless-service FQDNs used by clients inside Kubernetes.
|
||||
- name: SKILLHUB_REDIS_SENTINEL_CHECK_SENTINELS_LIST
|
||||
value: "false"
|
||||
{{- 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 | quote }}
|
||||
{{- else }}
|
||||
- name: SPRING_DATA_REDIS_HOST
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: {{ include "skillhub.fullname" . }}-config
|
||||
key: redis-host
|
||||
- name: SPRING_DATA_REDIS_PORT
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: {{ include "skillhub.fullname" . }}-config
|
||||
key: redis-port
|
||||
{{- 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:
|
||||
{{- if .Values.redis.enabled }}
|
||||
name: {{ include "skillhub.redis.secretName" . }}
|
||||
key: {{ include "skillhub.redis.passwordKey" . }}
|
||||
{{- else }}
|
||||
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" . }}
|
||||
{{- end }}
|
||||
key: {{ if .Values.redis.enabled }}{{ include "skillhub.redis.passwordKey" . }}{{ else }}redis-password{{ end }}
|
||||
optional: true
|
||||
{{- end }}
|
||||
|
||||
# Storage
|
||||
- name: STORAGE_BASE_PATH
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: {{ include "skillhub.fullname" . }}-config
|
||||
key: storage-base-path
|
||||
- name: SKILLHUB_STORAGE_PROVIDER
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: {{ include "skillhub.fullname" . }}-config
|
||||
key: skillhub-storage-provider
|
||||
|
||||
{{- if .Values.s3.enabled }}
|
||||
- name: SKILLHUB_STORAGE_S3_BUCKET
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: {{ include "skillhub.fullname" . }}-config
|
||||
key: s3-bucket
|
||||
- name: SKILLHUB_STORAGE_S3_ENDPOINT
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: {{ include "skillhub.fullname" . }}-config
|
||||
key: s3-endpoint
|
||||
- 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_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: skillhub-storage-s3-access-key
|
||||
optional: true
|
||||
- name: SKILLHUB_STORAGE_S3_SECRET_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "skillhub.secretName" . }}
|
||||
key: skillhub-storage-s3-secret-key
|
||||
optional: true
|
||||
{{- end }}
|
||||
|
||||
# Scanner
|
||||
- name: SKILLHUB_SECURITY_SCANNER_ENABLED
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: {{ include "skillhub.fullname" . }}-config
|
||||
key: skill-scanner-enabled
|
||||
- name: SKILLHUB_SECURITY_SCANNER_URL
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: {{ include "skillhub.fullname" . }}-config
|
||||
key: skill-scanner-url
|
||||
- name: SKILLHUB_SECURITY_SCANNER_MODE
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: {{ include "skillhub.fullname" . }}-config
|
||||
key: skill-scanner-mode
|
||||
|
||||
# Session
|
||||
- name: SESSION_COOKIE_SECURE
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
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:
|
||||
configMapKeyRef:
|
||||
name: {{ include "skillhub.fullname" . }}-config
|
||||
key: bootstrap-admin-enabled
|
||||
- name: BOOTSTRAP_ADMIN_USER_ID
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: {{ include "skillhub.fullname" . }}-config
|
||||
key: bootstrap-admin-user-id
|
||||
- name: BOOTSTRAP_ADMIN_USERNAME
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: {{ include "skillhub.fullname" . }}-config
|
||||
key: bootstrap-admin-username
|
||||
- name: BOOTSTRAP_ADMIN_DISPLAY_NAME
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: {{ include "skillhub.fullname" . }}-config
|
||||
key: bootstrap-admin-display-name
|
||||
- name: BOOTSTRAP_ADMIN_EMAIL
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: {{ include "skillhub.fullname" . }}-config
|
||||
key: bootstrap-admin-email
|
||||
- name: BOOTSTRAP_ADMIN_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "skillhub.secretName" . }}
|
||||
key: bootstrap-admin-password
|
||||
optional: true
|
||||
|
||||
# OAuth2 GitHub (optional)
|
||||
- name: OAUTH2_GITHUB_CLIENT_ID
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "skillhub.secretName" . }}
|
||||
key: oauth2-github-client-id
|
||||
optional: true
|
||||
- name: OAUTH2_GITHUB_CLIENT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "skillhub.secretName" . }}
|
||||
key: oauth2-github-client-secret
|
||||
optional: true
|
||||
|
||||
{{- if .Values.server.javaOpts }}
|
||||
- name: JAVA_OPTS
|
||||
value: {{ .Values.server.javaOpts }}
|
||||
{{- end }}
|
||||
|
||||
{{- with .Values.server.extraEnv }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
|
||||
{{- if and .Values.server.enabled (not .Values.s3.enabled) }}
|
||||
volumeMounts:
|
||||
- name: skillhub-storage
|
||||
mountPath: /var/lib/skillhub/storage
|
||||
{{- end }}
|
||||
|
||||
resources:
|
||||
{{- toYaml .Values.server.resources | nindent 12 }}
|
||||
|
||||
startupProbe:
|
||||
{{- toYaml .Values.server.probes.startup | nindent 12 }}
|
||||
readinessProbe:
|
||||
{{- toYaml .Values.server.probes.readiness | nindent 12 }}
|
||||
livenessProbe:
|
||||
{{- toYaml .Values.server.probes.liveness | nindent 12 }}
|
||||
|
||||
{{- if and .Values.server.enabled (not .Values.s3.enabled) }}
|
||||
volumes:
|
||||
- name: skillhub-storage
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ include "skillhub.fullname" . }}-server-data
|
||||
{{- end }}
|
||||
{{- with .Values.server.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.server.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.server.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
|
||||
{{- end }}
|
||||
61
charts/skillhub/templates/services.yaml
Normal file
61
charts/skillhub/templates/services.yaml
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
{{- /*
|
||||
SkillHub Service 资源
|
||||
- server/web: 使用组件自己的 service.type 配置(共享同一模板)
|
||||
- scanner: 固定 ClusterIP(仅供内部调用)
|
||||
*/}}
|
||||
|
||||
{{- range $name := list "server" "web" }}
|
||||
{{- $component := index $.Values $name }}
|
||||
{{- $enabled := true }}
|
||||
{{- if hasKey $component "enabled" }}
|
||||
{{- $enabled = $component.enabled }}
|
||||
{{- end }}
|
||||
{{- if and $enabled $component.service.enabled }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "skillhub.fullname" $ }}-{{ $name }}
|
||||
labels:
|
||||
{{- include (printf "skillhub.%s.labels" $name) $ | nindent 4 }}
|
||||
spec:
|
||||
type: {{ $component.service.type }}
|
||||
{{- if eq $component.service.type "LoadBalancer" }}
|
||||
{{- if $component.service.loadBalancerIP }}
|
||||
loadBalancerIP: {{ $component.service.loadBalancerIP }}
|
||||
{{- end }}
|
||||
{{- if $component.service.loadBalancerSourceRanges }}
|
||||
loadBalancerSourceRanges:
|
||||
{{- toYaml $component.service.loadBalancerSourceRanges | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
ports:
|
||||
- name: http
|
||||
port: {{ $component.service.port }}
|
||||
targetPort: http
|
||||
{{- if and (eq $component.service.type "NodePort") $component.service.nodePort }}
|
||||
nodePort: {{ $component.service.nodePort }}
|
||||
{{- end }}
|
||||
selector:
|
||||
{{- include (printf "skillhub.%s.selectorLabels" $name) $ | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- /* Scanner Service(固定 ClusterIP) */}}
|
||||
{{- if and .Values.scanner.enabled .Values.scanner.service }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "skillhub.fullname" . }}-scanner
|
||||
labels:
|
||||
{{- include "skillhub.scanner.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- name: http
|
||||
port: {{ .Values.scanner.service.port }}
|
||||
targetPort: http
|
||||
selector:
|
||||
{{- include "skillhub.scanner.selectorLabels" . | nindent 4 }}
|
||||
{{- end }}
|
||||
95
charts/skillhub/templates/validate.yaml
Normal file
95
charts/skillhub/templates/validate.yaml
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
{{- /* Cross-field validation that JSON Schema cannot express reliably. */ -}}
|
||||
{{- $absoluteHttpUrlPattern := "^https?://(\\[[0-9A-Fa-f:.]+\\]|[A-Za-z0-9]([A-Za-z0-9.-]*[A-Za-z0-9])?)(:[0-9]{1,5})?([/?#][^[:space:]]*)?$" -}}
|
||||
{{- 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|oauth2|login/oauth2|\\.well-known)(/|$)" $path.path -}}
|
||||
{{- fail "ingress.hosts[].paths reserves /api, /oauth2, /login/oauth2 and /.well-known 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 and .Values.s3.endpoint (not (regexMatch $absoluteHttpUrlPattern .Values.s3.endpoint)) -}}
|
||||
{{- fail "s3.endpoint must be an absolute HTTP(S) URL" -}}
|
||||
{{- end -}}
|
||||
{{- if and .Values.s3.publicEndpoint (not (regexMatch $absoluteHttpUrlPattern .Values.s3.publicEndpoint)) -}}
|
||||
{{- fail "s3.publicEndpoint must be an absolute HTTP(S) URL" -}}
|
||||
{{- 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 -}}
|
||||
74
charts/skillhub/templates/web-deployment.yaml
Normal file
74
charts/skillhub/templates/web-deployment.yaml
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "skillhub.fullname" . }}-web
|
||||
labels:
|
||||
{{- include "skillhub.web.labels" . | nindent 4 }}
|
||||
spec:
|
||||
{{- if not .Values.web.autoscaling.enabled }}
|
||||
replicas: {{ .Values.web.replicaCount }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "skillhub.web.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "skillhub.web.selectorLabels" . | nindent 8 }}
|
||||
annotations:
|
||||
checksum/config: {{ toYaml (dict "web" .Values.web "publicBaseUrl" .Values.publicBaseUrl "auth" .Values.auth) | sha256sum }}
|
||||
{{- range $key, $val := .Values.web.podAnnotations }}
|
||||
{{ $key }}: {{ $val }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- $secrets := .Values.web.imagePullSecrets }}
|
||||
{{- if $secrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml $secrets | nindent 8 }}
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: web
|
||||
image: {{ .Values.web.image.registry | default .Values.images.registry }}/skillhub-web:{{ .Values.web.image.tag | default .Values.images.tag | default (printf "v%s" .Chart.AppVersion) }}
|
||||
imagePullPolicy: {{ .Values.images.pullPolicy }}
|
||||
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 }}
|
||||
ports:
|
||||
- containerPort: {{ .Values.web.service.port }}
|
||||
name: http
|
||||
resources:
|
||||
{{- toYaml .Values.web.resources | nindent 12 }}
|
||||
readinessProbe:
|
||||
{{- toYaml .Values.web.probes.readiness | nindent 12 }}
|
||||
livenessProbe:
|
||||
{{- toYaml .Values.web.probes.liveness | nindent 12 }}
|
||||
{{- with .Values.web.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.web.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.web.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
254
charts/skillhub/tests/configuration-contracts.sh
Executable file
254
charts/skillhub/tests/configuration-contracts.sh
Executable file
|
|
@ -0,0 +1,254 @@
|
|||
#!/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 'bitnami/postgresql@sha256:db2312d9b243afa8c3b3f5496e478d17d0dff9791d06f3b93b9567abd86ae92f' "$TMP_DIR/default.yaml"
|
||||
grep -Fq 'bitnami/postgres-exporter@sha256:53ab72a1b940d7637e91619f1000da9ebef14bc7dad74321a78731d65c79f55b' "$TMP_DIR/default.yaml"
|
||||
grep -Fq 'bitnami/redis@sha256:08863c2c3f4e051fb6139b38fa223e9c13be5033326a59bead182860d899bf98' "$TMP_DIR/default.yaml"
|
||||
grep -Fq 'bitnami/redis-exporter@sha256:fb1dae6add1e1104989d086d9407f7d65f58968550aa5fddea20637a758c0773' "$TMP_DIR/default.yaml"
|
||||
if grep -Eq 'image:.*:latest([@"[:space:]]|$)' "$TMP_DIR/default.yaml"; then
|
||||
fail "default workloads must not use mutable latest image tags"
|
||||
fi
|
||||
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"
|
||||
grep -A1 -F 'name: SKILLHUB_REDIS_SENTINEL_CHECK_SENTINELS_LIST' "$TMP_DIR/sentinel.yaml" \
|
||||
| grep -Fq 'value: "false"'
|
||||
render sentinel-full "$CHART_DIR" \
|
||||
--set redis.architecture=replication \
|
||||
--set redis.sentinel.enabled=true >"$TMP_DIR/sentinel-full.yaml"
|
||||
grep -Fq 'bitnami/redis-sentinel@sha256:ae75dd69c192a632bdeb21baa6721080be5b12347e52add922036398b47631da' "$TMP_DIR/sentinel-full.yaml"
|
||||
if grep -Eq 'image:.*:latest([@"[:space:]]|$)' "$TMP_DIR/sentinel-full.yaml"; then
|
||||
fail "Sentinel workloads must not use mutable latest image tags"
|
||||
fi
|
||||
|
||||
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"
|
||||
if grep -Fq 'name: SKILLHUB_REDIS_SENTINEL_CHECK_SENTINELS_LIST' "$TMP_DIR/external-sentinel.yaml"; then
|
||||
fail "external Sentinel must preserve Redisson address consistency checks by default"
|
||||
fi
|
||||
|
||||
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 tls "$CHART_DIR" \
|
||||
--set ingress.enabled=true \
|
||||
--set-json 'ingress.tls=[{"hosts":["skills.example.com"],"secretName":"skills-tls"}]' \
|
||||
--show-only templates/ingress.yaml >"$TMP_DIR/tls-ingress.yaml"
|
||||
for server_path in /api /oauth2 /login/oauth2 /.well-known; do
|
||||
grep -Fq -- "- path: $server_path" "$TMP_DIR/tls-ingress.yaml"
|
||||
done
|
||||
if [[ $(grep -Fc 'name: tls-skillhub-server' "$TMP_DIR/tls-ingress.yaml") -ne 4 ]]; then
|
||||
fail "API and OAuth ingress paths must route directly to the SkillHub server"
|
||||
fi
|
||||
|
||||
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 reserved-oauth-ingress-path \
|
||||
--set ingress.enabled=true \
|
||||
--set-json 'ingress.hosts=[{"host":"skills.example.com","paths":[{"path":"/oauth2","pathType":"Prefix"}]}]'
|
||||
assert_rejected reserved-oauth-ingress-child-path \
|
||||
--set ingress.enabled=true \
|
||||
--set-json 'ingress.hosts=[{"host":"skills.example.com","paths":[{"path":"/login/oauth2/code/github","pathType":"Prefix"}]}]'
|
||||
assert_rejected invalid-s3-endpoint --set s3.endpoint=s3.amazonaws.com
|
||||
assert_rejected invalid-s3-public-endpoint --set s3.publicEndpoint=cdn.example.com
|
||||
assert_rejected invalid-s3-empty-authority --set-string 's3.endpoint=https://?'
|
||||
assert_rejected invalid-s3-whitespace-authority --set-string 's3.publicEndpoint=https:// '
|
||||
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"
|
||||
318
charts/skillhub/tests/install-upgrade-smoke.sh
Executable file
318
charts/skillhub/tests/install-upgrade-smoke.sh
Executable file
|
|
@ -0,0 +1,318 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
CHART_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
TEST_VALUES="$CHART_DIR/tests/test-values.yaml"
|
||||
SCENARIO=${HELM_SMOKE_SCENARIO:-default}
|
||||
NAMESPACE=${HELM_SMOKE_NAMESPACE:-skillhub-helm-smoke-$SCENARIO}
|
||||
RELEASE=${HELM_SMOKE_RELEASE:-skillhub-smoke}
|
||||
TIMEOUT=${HELM_SMOKE_TIMEOUT:-15m}
|
||||
KEEP_ENVIRONMENT=${KEEP_HELM_SMOKE:-false}
|
||||
TMP_DIR=$(mktemp -d)
|
||||
PORT_FORWARD_PID=""
|
||||
OWNS_NAMESPACE=false
|
||||
HELM_SCENARIO_ARGS=()
|
||||
|
||||
fail() {
|
||||
echo "ERROR: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
for command in helm kubectl curl jq sha256sum; do
|
||||
command -v "$command" >/dev/null 2>&1 || fail "$command is required"
|
||||
done
|
||||
|
||||
if kubectl get namespace "$NAMESPACE" >/dev/null 2>&1; then
|
||||
fail "namespace $NAMESPACE already exists; choose an unused HELM_SMOKE_NAMESPACE"
|
||||
fi
|
||||
|
||||
stop_port_forward() {
|
||||
if [[ -n "$PORT_FORWARD_PID" ]]; then
|
||||
kill "$PORT_FORWARD_PID" >/dev/null 2>&1 || true
|
||||
wait "$PORT_FORWARD_PID" >/dev/null 2>&1 || true
|
||||
PORT_FORWARD_PID=""
|
||||
fi
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
local exit_code=$?
|
||||
trap - EXIT
|
||||
stop_port_forward
|
||||
|
||||
if (( exit_code != 0 )) && kubectl get namespace "$NAMESPACE" >/dev/null 2>&1; then
|
||||
echo "Helm smoke failed; collecting non-secret diagnostics" >&2
|
||||
helm status "$RELEASE" --namespace "$NAMESPACE" >&2 || true
|
||||
kubectl get pods,pvc,deployments,statefulsets --namespace "$NAMESPACE" -o wide >&2 || true
|
||||
kubectl get events --namespace "$NAMESPACE" --sort-by=.lastTimestamp >&2 || true
|
||||
fi
|
||||
|
||||
if [[ "$KEEP_ENVIRONMENT" != "true" && "$OWNS_NAMESPACE" == "true" ]]; then
|
||||
helm uninstall "$RELEASE" --namespace "$NAMESPACE" --wait >/dev/null 2>&1 || true
|
||||
kubectl delete namespace "$NAMESPACE" --wait --timeout=5m >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
rm -rf "$TMP_DIR"
|
||||
exit "$exit_code"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
setup_scenario() {
|
||||
case "$SCENARIO" in
|
||||
default)
|
||||
;;
|
||||
sentinel)
|
||||
HELM_SCENARIO_ARGS+=(
|
||||
--set redis.architecture=replication
|
||||
--set redis.sentinel.enabled=true
|
||||
)
|
||||
;;
|
||||
s3)
|
||||
kubectl apply --namespace "$NAMESPACE" -f - <<'YAML'
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: minio
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: minio
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: minio
|
||||
spec:
|
||||
containers:
|
||||
- name: minio
|
||||
image: docker.io/minio/minio@sha256:14cea493d9a34af32f524e538b8346cf79f3321eff8e708c1e2960462bd8936e
|
||||
args:
|
||||
- server
|
||||
- /data
|
||||
env:
|
||||
- name: MINIO_ROOT_USER
|
||||
value: smoke-access-key
|
||||
- name: MINIO_ROOT_PASSWORD
|
||||
value: smoke-secret-key
|
||||
ports:
|
||||
- name: api
|
||||
containerPort: 9000
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /minio/health/ready
|
||||
port: api
|
||||
periodSeconds: 2
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: minio
|
||||
spec:
|
||||
selector:
|
||||
app: minio
|
||||
ports:
|
||||
- name: api
|
||||
port: 9000
|
||||
targetPort: api
|
||||
YAML
|
||||
kubectl rollout status deployment/minio \
|
||||
--namespace "$NAMESPACE" \
|
||||
--timeout=5m
|
||||
HELM_SCENARIO_ARGS+=(
|
||||
--set s3.enabled=true
|
||||
--set-string s3.endpoint=http://minio:9000
|
||||
--set-string s3.accessKey=smoke-access-key
|
||||
--set-string s3.secretKey=smoke-secret-key
|
||||
--set s3.autoCreateBucket=true
|
||||
)
|
||||
;;
|
||||
ingress-tls)
|
||||
command -v openssl >/dev/null 2>&1 || fail "openssl is required for ingress-tls"
|
||||
openssl req -x509 -newkey rsa:2048 -nodes \
|
||||
-keyout "$TMP_DIR/tls.key" \
|
||||
-out "$TMP_DIR/tls.crt" \
|
||||
-days 1 \
|
||||
-subj /CN=skillhub-smoke.local \
|
||||
-addext subjectAltName=DNS:skillhub-smoke.local >/dev/null 2>&1
|
||||
kubectl create secret tls skillhub-smoke-tls \
|
||||
--namespace "$NAMESPACE" \
|
||||
--cert "$TMP_DIR/tls.crt" \
|
||||
--key "$TMP_DIR/tls.key"
|
||||
HELM_SCENARIO_ARGS+=(
|
||||
--set ingress.enabled=true
|
||||
--set-json 'ingress.hosts=[{"host":"skillhub-smoke.local","paths":[{"path":"/","pathType":"Prefix"}]}]'
|
||||
--set-json 'ingress.tls=[{"hosts":["skillhub-smoke.local"],"secretName":"skillhub-smoke-tls"}]'
|
||||
)
|
||||
;;
|
||||
*)
|
||||
fail "unknown HELM_SMOKE_SCENARIO: $SCENARIO"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
assert_scenario_contract() {
|
||||
case "$SCENARIO" in
|
||||
default)
|
||||
;;
|
||||
sentinel)
|
||||
kubectl get deployment "$RELEASE-server" --namespace "$NAMESPACE" -o json \
|
||||
| jq -e '
|
||||
[.spec.template.spec.containers[]
|
||||
| select(.name == "server")
|
||||
| .env[]
|
||||
| select(.name == "SKILLHUB_REDIS_SENTINEL_CHECK_SENTINELS_LIST")
|
||||
| .value] == ["false"]
|
||||
' >/dev/null \
|
||||
|| fail "Sentinel scenario did not apply the Kubernetes-only address-check override"
|
||||
;;
|
||||
s3)
|
||||
local storage_provider
|
||||
storage_provider=$(kubectl get configmap "$RELEASE-config" \
|
||||
--namespace "$NAMESPACE" -o json | jq -r '.data["skillhub-storage-provider"]')
|
||||
[[ "$storage_provider" == "s3" ]] || fail "S3 scenario did not configure S3 storage"
|
||||
;;
|
||||
ingress-tls)
|
||||
kubectl get ingress "$RELEASE" --namespace "$NAMESPACE" -o json \
|
||||
| jq -e --arg server "$RELEASE-server" '
|
||||
.spec.tls[0].secretName == "skillhub-smoke-tls"
|
||||
and (
|
||||
[.spec.rules[].http.paths[]
|
||||
| select(
|
||||
.path == "/api"
|
||||
or .path == "/oauth2"
|
||||
or .path == "/login/oauth2"
|
||||
or .path == "/.well-known"
|
||||
)
|
||||
| .backend.service.name]
|
||||
| length == 4 and all(. == $server)
|
||||
)
|
||||
' >/dev/null \
|
||||
|| fail "TLS Ingress does not route every reserved path directly to the server"
|
||||
local cookie_secure
|
||||
cookie_secure=$(kubectl get configmap "$RELEASE-config" \
|
||||
--namespace "$NAMESPACE" -o json | jq -r '.data["session-cookie-secure"]')
|
||||
[[ "$cookie_secure" == "true" ]] || fail "TLS Ingress did not enable secure session cookies"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
probe_service() {
|
||||
local service=$1
|
||||
local service_port=$2
|
||||
local local_port=$3
|
||||
local path=$4
|
||||
local expected_status=${5:-200}
|
||||
local log_file="$TMP_DIR/${service}.port-forward.log"
|
||||
local status
|
||||
|
||||
stop_port_forward
|
||||
kubectl port-forward \
|
||||
--namespace "$NAMESPACE" \
|
||||
"service/$service" \
|
||||
"$local_port:$service_port" >"$log_file" 2>&1 &
|
||||
PORT_FORWARD_PID=$!
|
||||
|
||||
for _ in $(seq 1 60); do
|
||||
status=$(curl --silent --output /dev/null --write-out '%{http_code}' \
|
||||
"http://127.0.0.1:$local_port$path" 2>/dev/null || true)
|
||||
if [[ "$status" == "$expected_status" ]]; then
|
||||
stop_port_forward
|
||||
return 0
|
||||
fi
|
||||
if ! kill -0 "$PORT_FORWARD_PID" >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
cat "$log_file" >&2
|
||||
fail "$service$path did not return HTTP $expected_status"
|
||||
}
|
||||
|
||||
snapshot_secrets() {
|
||||
local output=$1
|
||||
: >"$output"
|
||||
for secret in "$RELEASE-secret" "$RELEASE-postgresql" "$RELEASE-redis"; do
|
||||
printf '%s ' "$secret" >>"$output"
|
||||
kubectl get secret "$secret" --namespace "$NAMESPACE" -o json \
|
||||
| jq -cS '.data' \
|
||||
| sha256sum \
|
||||
| awk '{print $1}' >>"$output"
|
||||
done
|
||||
}
|
||||
|
||||
snapshot_pvcs() {
|
||||
local output=$1
|
||||
kubectl get pvc --namespace "$NAMESPACE" -o json \
|
||||
| jq -r '.items[] | [.metadata.name, .metadata.uid, .spec.volumeName] | @tsv' \
|
||||
| sort >"$output"
|
||||
[[ -s "$output" ]] || fail "Helm install did not create any PVCs"
|
||||
}
|
||||
|
||||
assert_ready_and_healthy() {
|
||||
kubectl wait pod \
|
||||
--namespace "$NAMESPACE" \
|
||||
--all \
|
||||
--for=condition=Ready \
|
||||
--timeout="$TIMEOUT"
|
||||
|
||||
probe_service "$RELEASE-server" 8080 18081 /actuator/health
|
||||
probe_service "$RELEASE-web" 80 18080 /nginx-health
|
||||
probe_service "$RELEASE-web" 80 18080 /api/v1/auth/me 401
|
||||
probe_service "$RELEASE-scanner" 8000 18082 /health
|
||||
|
||||
local restarts
|
||||
restarts=$(kubectl get pods --namespace "$NAMESPACE" -o json \
|
||||
| jq '[.items[].status.containerStatuses[]?.restartCount] | add // 0')
|
||||
[[ "$restarts" == "0" ]] || fail "workloads restarted $restarts time(s)"
|
||||
}
|
||||
|
||||
helm dependency build "$CHART_DIR"
|
||||
kubectl create namespace "$NAMESPACE"
|
||||
OWNS_NAMESPACE=true
|
||||
setup_scenario
|
||||
|
||||
helm install "$RELEASE" "$CHART_DIR" \
|
||||
--namespace "$NAMESPACE" \
|
||||
--values "$TEST_VALUES" \
|
||||
--set-string fullnameOverride="$RELEASE" \
|
||||
--set-string publicBaseUrl=http://skillhub-smoke.local \
|
||||
"${HELM_SCENARIO_ARGS[@]}" \
|
||||
--wait \
|
||||
--timeout "$TIMEOUT"
|
||||
|
||||
assert_ready_and_healthy
|
||||
assert_scenario_contract
|
||||
snapshot_secrets "$TMP_DIR/secrets-before"
|
||||
snapshot_pvcs "$TMP_DIR/pvcs-before"
|
||||
revision_before=$(helm history "$RELEASE" --namespace "$NAMESPACE" -o json \
|
||||
| jq -r '.[-1].revision')
|
||||
|
||||
helm upgrade "$RELEASE" "$CHART_DIR" \
|
||||
--namespace "$NAMESPACE" \
|
||||
--reuse-values \
|
||||
--set-string publicBaseUrl=https://skillhub-smoke.local \
|
||||
--set-string server.podAnnotations.helm-smoke-revision=revision-2 \
|
||||
--wait \
|
||||
--timeout "$TIMEOUT"
|
||||
|
||||
assert_ready_and_healthy
|
||||
assert_scenario_contract
|
||||
snapshot_secrets "$TMP_DIR/secrets-after"
|
||||
snapshot_pvcs "$TMP_DIR/pvcs-after"
|
||||
revision_after=$(helm history "$RELEASE" --namespace "$NAMESPACE" -o json \
|
||||
| jq -r '.[-1].revision')
|
||||
|
||||
(( revision_after == revision_before + 1 )) \
|
||||
|| fail "Helm revision did not advance exactly once"
|
||||
cmp "$TMP_DIR/secrets-before" "$TMP_DIR/secrets-after" \
|
||||
|| fail "application or dependency Secret data changed during upgrade"
|
||||
cmp "$TMP_DIR/pvcs-before" "$TMP_DIR/pvcs-after" \
|
||||
|| fail "PVC identity or bound volume changed during upgrade"
|
||||
|
||||
public_base_url=$(kubectl get configmap "$RELEASE-config" \
|
||||
--namespace "$NAMESPACE" \
|
||||
-o json | jq -r '.data["public-base-url"]')
|
||||
[[ "$public_base_url" == "https://skillhub-smoke.local" ]] \
|
||||
|| fail "publicBaseUrl was not applied by the upgrade"
|
||||
|
||||
echo "Helm install/upgrade smoke passed for scenario: $SCENARIO"
|
||||
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
|
||||
462
charts/skillhub/values.schema.json
Normal file
462
charts/skillhub/values.schema.json
Normal 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", "pattern": "^(|https?://(\\[[0-9A-Fa-f:.]+\\]|[A-Za-z0-9]([A-Za-z0-9.-]*[A-Za-z0-9])?)(:[0-9]{1,5})?([/?#]\\S*)?)$" },
|
||||
"publicEndpoint": { "type": "string", "pattern": "^(|https?://(\\[[0-9A-Fa-f:.]+\\]|[A-Za-z0-9]([A-Za-z0-9.-]*[A-Za-z0-9])?)(:[0-9]{1,5})?([/?#]\\S*)?)$" },
|
||||
"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" }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
460
charts/skillhub/values.yaml
Normal file
460
charts/skillhub/values.yaml
Normal file
|
|
@ -0,0 +1,460 @@
|
|||
# ============================================================================
|
||||
# SkillHub Helm Chart 全局配置
|
||||
# ============================================================================
|
||||
|
||||
# ============================================================================
|
||||
# 镜像配置
|
||||
# ============================================================================
|
||||
images:
|
||||
registry: ghcr.io/iflytek
|
||||
tag: ""
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
|
||||
# 浏览器、CLI 和 OAuth 回调访问的公开地址(不带末尾斜杠)
|
||||
publicBaseUrl: ""
|
||||
deviceAuthVerificationUri: ""
|
||||
|
||||
auth:
|
||||
direct:
|
||||
enabled: true
|
||||
provider: local
|
||||
|
||||
builtinSkills:
|
||||
enabled: true
|
||||
|
||||
# ============================================================================
|
||||
# Ingress 配置
|
||||
# ============================================================================
|
||||
ingress:
|
||||
enabled: false
|
||||
className: nginx
|
||||
hosts:
|
||||
- host: skills.example.com
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
annotations: {}
|
||||
tls: []
|
||||
certManager:
|
||||
enabled: false
|
||||
issuerName: letsencrypt-prod
|
||||
issuerKind: ClusterIssuer
|
||||
|
||||
# ============================================================================
|
||||
# S3 对象存储配置
|
||||
# ============================================================================
|
||||
s3:
|
||||
enabled: false
|
||||
bucket: skillhub-storage
|
||||
endpoint: ""
|
||||
publicEndpoint: ""
|
||||
region: us-east-1
|
||||
forcePathStyle: true
|
||||
disableChunkedEncoding: false
|
||||
autoCreateBucket: false
|
||||
presignExpiry: PT10M
|
||||
accessKey: ""
|
||||
secretKey: ""
|
||||
|
||||
# ============================================================================
|
||||
# Session 配置
|
||||
# ============================================================================
|
||||
session:
|
||||
cookieSecure: false
|
||||
|
||||
# ============================================================================
|
||||
# Bootstrap 管理员
|
||||
# ============================================================================
|
||||
bootstrapAdmin:
|
||||
enabled: true
|
||||
userId: docker-admin
|
||||
username: admin
|
||||
displayName: "Platform Admin"
|
||||
email: admin@example.com
|
||||
password: ""
|
||||
|
||||
# ============================================================================
|
||||
# Spring Profiles
|
||||
# ============================================================================
|
||||
springProfilesActive: docker
|
||||
|
||||
# ============================================================================
|
||||
# Secret 配置
|
||||
# ============================================================================
|
||||
existingSecret: ""
|
||||
|
||||
secrets:
|
||||
# 默认禁止随机 Secret;仅在非 GitOps 临时环境中按需启用
|
||||
allowAutoGenerated: false
|
||||
bootstrapAdminPassword: ""
|
||||
downloadAnonCookieSecret: ""
|
||||
oauth2GithubClientId: ""
|
||||
oauth2GithubClientSecret: ""
|
||||
scannerLlmApiKey: ""
|
||||
scannerLlmBaseUrl: ""
|
||||
scannerLlmModel: ""
|
||||
|
||||
# ============================================================================
|
||||
# PostgreSQL 配置(Bitnami)
|
||||
# ============================================================================
|
||||
postgresql:
|
||||
enabled: true
|
||||
|
||||
architecture: standalone
|
||||
|
||||
# Bitnami's current chart defaults to a mutable latest tag. Pin the image
|
||||
# digest so installs and rollbacks remain reproducible.
|
||||
image:
|
||||
digest: "sha256:db2312d9b243afa8c3b3f5496e478d17d0dff9791d06f3b93b9567abd86ae92f"
|
||||
|
||||
auth:
|
||||
postgresPassword: ""
|
||||
database: skillhub
|
||||
username: skillhub
|
||||
password: ""
|
||||
|
||||
primary:
|
||||
persistence:
|
||||
enabled: true
|
||||
size: 10Gi
|
||||
storageClass: ""
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 1Gi
|
||||
extraEnvVars:
|
||||
- name: POSTGRESQL_MAX_CONNECTIONS
|
||||
value: "500"
|
||||
podAnnotations: {}
|
||||
podSecurityContext:
|
||||
enabled: true
|
||||
fsGroup: 1001
|
||||
containerSecurityContext:
|
||||
enabled: true
|
||||
runAsUser: 1001
|
||||
livenessProbe:
|
||||
enabled: true
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 20
|
||||
readinessProbe:
|
||||
enabled: true
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
|
||||
readReplicas:
|
||||
persistence:
|
||||
enabled: true
|
||||
size: 10Gi
|
||||
storageClass: ""
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
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
|
||||
image:
|
||||
digest: "sha256:53ab72a1b940d7637e91619f1000da9ebef14bc7dad74321a78731d65c79f55b"
|
||||
serviceMonitor:
|
||||
enabled: false
|
||||
|
||||
externalDatabase:
|
||||
host: ""
|
||||
port: 5432
|
||||
database: skillhub
|
||||
username: skillhub
|
||||
password: ""
|
||||
jdbcUrl: ""
|
||||
|
||||
# ============================================================================
|
||||
# Redis 配置(Bitnami)
|
||||
# ============================================================================
|
||||
redis:
|
||||
enabled: true
|
||||
|
||||
architecture: standalone
|
||||
|
||||
# Keep the bundled Redis runtime immutable for repeatable upgrades.
|
||||
image:
|
||||
digest: "sha256:08863c2c3f4e051fb6139b38fa223e9c13be5033326a59bead182860d899bf98"
|
||||
|
||||
auth:
|
||||
enabled: true
|
||||
password: ""
|
||||
|
||||
master:
|
||||
persistence:
|
||||
enabled: true
|
||||
size: 5Gi
|
||||
storageClass: ""
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 512Mi
|
||||
podAnnotations: {}
|
||||
podSecurityContext:
|
||||
enabled: true
|
||||
fsGroup: 1001
|
||||
containerSecurityContext:
|
||||
enabled: true
|
||||
runAsUser: 1001
|
||||
|
||||
replica:
|
||||
persistence:
|
||||
enabled: true
|
||||
size: 5Gi
|
||||
storageClass: ""
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 512Mi
|
||||
|
||||
sentinel:
|
||||
enabled: false
|
||||
masterSet: mymaster
|
||||
image:
|
||||
digest: "sha256:ae75dd69c192a632bdeb21baa6721080be5b12347e52add922036398b47631da"
|
||||
service:
|
||||
ports:
|
||||
sentinel: 26379
|
||||
containerPorts:
|
||||
sentinel: 26379
|
||||
|
||||
metrics:
|
||||
enabled: true
|
||||
image:
|
||||
digest: "sha256:fb1dae6add1e1104989d086d9407f7d65f58968550aa5fddea20637a758c0773"
|
||||
serviceMonitor:
|
||||
enabled: false
|
||||
|
||||
externalRedis:
|
||||
host: ""
|
||||
port: 6379
|
||||
password: ""
|
||||
sentinel:
|
||||
enabled: false
|
||||
masterSet: mymaster
|
||||
nodes: []
|
||||
password: ""
|
||||
|
||||
# ============================================================================
|
||||
# Server 配置
|
||||
# ============================================================================
|
||||
server:
|
||||
enabled: true
|
||||
replicaCount: 1
|
||||
|
||||
image:
|
||||
registry: ""
|
||||
tag: ""
|
||||
|
||||
dependencyWait:
|
||||
image:
|
||||
registry: docker.io
|
||||
repository: library/busybox
|
||||
tag: "1.37"
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
service:
|
||||
enabled: true
|
||||
type: ClusterIP
|
||||
port: 8080
|
||||
nodePort: ""
|
||||
loadBalancerIP: ""
|
||||
loadBalancerSourceRanges: []
|
||||
|
||||
storage:
|
||||
# 访问模式:留空时自动判断(单副本 RWO,多副本 RWX),或手动指定
|
||||
accessMode: ""
|
||||
size: 10Gi
|
||||
storageClassName: ""
|
||||
|
||||
# PVC 挂载会覆盖镜像内目录权限;使用镜像中 app 用户的组 ID 使本地存储可写
|
||||
podSecurityContext:
|
||||
fsGroup: 101
|
||||
fsGroupChangePolicy: OnRootMismatch
|
||||
|
||||
resources:
|
||||
requests:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
limits:
|
||||
cpu: 1000m
|
||||
memory: 1Gi
|
||||
|
||||
javaOpts: ""
|
||||
extraEnv: []
|
||||
podAnnotations: {}
|
||||
imagePullSecrets: []
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
|
||||
probes:
|
||||
startup:
|
||||
httpGet:
|
||||
path: /actuator/health
|
||||
port: http
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
failureThreshold: 30
|
||||
readiness:
|
||||
httpGet:
|
||||
path: /actuator/health
|
||||
port: http
|
||||
initialDelaySeconds: 20
|
||||
periodSeconds: 10
|
||||
liveness:
|
||||
httpGet:
|
||||
path: /actuator/health
|
||||
port: http
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 15
|
||||
|
||||
autoscaling:
|
||||
enabled: false
|
||||
minReplicas: 1
|
||||
maxReplicas: 10
|
||||
targetCPUUtilizationPercentage: 80
|
||||
targetMemoryUtilizationPercentage: 80
|
||||
|
||||
podDisruptionBudget:
|
||||
enabled: false
|
||||
minAvailable: 1
|
||||
|
||||
# ============================================================================
|
||||
# Web 配置
|
||||
# ============================================================================
|
||||
web:
|
||||
replicaCount: 1
|
||||
image:
|
||||
registry: ""
|
||||
tag: ""
|
||||
|
||||
service:
|
||||
enabled: true
|
||||
type: ClusterIP
|
||||
port: 80
|
||||
nodePort: ""
|
||||
loadBalancerIP: ""
|
||||
loadBalancerSourceRanges: []
|
||||
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 256Mi
|
||||
|
||||
extraEnv: []
|
||||
podAnnotations: {}
|
||||
imagePullSecrets: []
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
|
||||
probes:
|
||||
readiness:
|
||||
httpGet:
|
||||
path: /nginx-health
|
||||
port: http
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
liveness:
|
||||
httpGet:
|
||||
path: /nginx-health
|
||||
port: http
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 15
|
||||
|
||||
autoscaling:
|
||||
enabled: false
|
||||
minReplicas: 1
|
||||
maxReplicas: 5
|
||||
targetCPUUtilizationPercentage: 80
|
||||
targetMemoryUtilizationPercentage: 80
|
||||
|
||||
podDisruptionBudget:
|
||||
enabled: false
|
||||
minAvailable: 1
|
||||
|
||||
# ============================================================================
|
||||
# Scanner 配置
|
||||
# ============================================================================
|
||||
scanner:
|
||||
enabled: true
|
||||
replicaCount: 1
|
||||
image:
|
||||
registry: ""
|
||||
tag: ""
|
||||
|
||||
service:
|
||||
port: 8000
|
||||
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
|
||||
extraEnv: []
|
||||
podAnnotations: {}
|
||||
imagePullSecrets: []
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
|
||||
probes:
|
||||
readiness:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: http
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
liveness:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: http
|
||||
initialDelaySeconds: 20
|
||||
periodSeconds: 15
|
||||
|
||||
autoscaling:
|
||||
enabled: false
|
||||
minReplicas: 1
|
||||
maxReplicas: 5
|
||||
targetCPUUtilizationPercentage: 80
|
||||
targetMemoryUtilizationPercentage: 80
|
||||
|
||||
podDisruptionBudget:
|
||||
enabled: false
|
||||
minAvailable: 1
|
||||
|
|
@ -24,6 +24,7 @@ assert_pr_workflow_hardened() {
|
|||
|
||||
assert_pr_workflow_hardened "$REPO_ROOT/.github/workflows/pr-cli.yml"
|
||||
assert_pr_workflow_hardened "$REPO_ROOT/.github/workflows/pr-e2e.yml"
|
||||
assert_pr_workflow_hardened "$REPO_ROOT/.github/workflows/pr-helm-chart.yml"
|
||||
assert_pr_workflow_hardened "$REPO_ROOT/.github/workflows/pr-tests.yml"
|
||||
assert_pr_workflow_hardened "$PR_SCRIPTS_WORKFLOW"
|
||||
assert_pr_workflow_hardened "$SECURITY_WORKFLOW"
|
||||
|
|
@ -54,8 +55,12 @@ grep -Fq '.github/workflows/pr-cli.yml' "$PR_SCRIPTS_WORKFLOW" \
|
|||
|| fail "pr-scripts must run when PR CLI workflow changes"
|
||||
grep -Fq '.github/workflows/pr-e2e.yml' "$PR_SCRIPTS_WORKFLOW" \
|
||||
|| fail "pr-scripts must run when PR E2E workflow changes"
|
||||
grep -Fq '.github/workflows/pr-helm-chart.yml' "$PR_SCRIPTS_WORKFLOW" \
|
||||
|| fail "pr-scripts must run when PR Helm Chart workflow changes"
|
||||
grep -Fq '.github/workflows/pr-tests.yml' "$PR_SCRIPTS_WORKFLOW" \
|
||||
|| fail "pr-scripts must run when PR Tests workflow changes"
|
||||
grep -Fq '.github/workflows/publish-chart.yml' "$PR_SCRIPTS_WORKFLOW" \
|
||||
|| fail "pr-scripts must run when Chart publish workflow changes"
|
||||
grep -Fq "'**/*.py'" "$PR_SCRIPTS_WORKFLOW" \
|
||||
|| fail "pr-scripts must run when Python source changes"
|
||||
grep -Fq '.env.release.example' "$PR_SCRIPTS_WORKFLOW" \
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import org.redisson.api.RedissonClient;
|
|||
import org.redisson.config.Config;
|
||||
import org.redisson.config.SentinelServersConfig;
|
||||
import org.redisson.config.SingleServerConfig;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
|
@ -18,14 +19,20 @@ import java.util.List;
|
|||
public class RedissonConfig {
|
||||
|
||||
@Bean(destroyMethod = "shutdown")
|
||||
public RedissonClient redissonClient(RedisProperties redisProperties) {
|
||||
return Redisson.create(createConfig(redisProperties));
|
||||
public RedissonClient redissonClient(
|
||||
RedisProperties redisProperties,
|
||||
@Value("${skillhub.redis.sentinel.check-sentinels-list:true}") boolean checkSentinelsList) {
|
||||
return Redisson.create(createConfig(redisProperties, checkSentinelsList));
|
||||
}
|
||||
|
||||
static Config createConfig(RedisProperties redisProperties) {
|
||||
return createConfig(redisProperties, true);
|
||||
}
|
||||
|
||||
static Config createConfig(RedisProperties redisProperties, boolean checkSentinelsList) {
|
||||
Config config = new Config();
|
||||
if (hasSentinelConfiguration(redisProperties)) {
|
||||
configureSentinelServers(config, redisProperties);
|
||||
configureSentinelServers(config, redisProperties, checkSentinelsList);
|
||||
return config;
|
||||
}
|
||||
|
||||
|
|
@ -38,10 +45,14 @@ public class RedissonConfig {
|
|||
return config;
|
||||
}
|
||||
|
||||
private static void configureSentinelServers(Config config, RedisProperties redisProperties) {
|
||||
private static void configureSentinelServers(
|
||||
Config config,
|
||||
RedisProperties redisProperties,
|
||||
boolean checkSentinelsList) {
|
||||
SentinelServersConfig sentinelServersConfig = config.useSentinelServers()
|
||||
.setMasterName(redisProperties.getSentinel().getMaster())
|
||||
.setDatabase(redisProperties.getDatabase());
|
||||
.setDatabase(redisProperties.getDatabase())
|
||||
.setCheckSentinelsList(checkSentinelsList);
|
||||
List<String> nodes = redisProperties.getSentinel().getNodes();
|
||||
nodes.stream()
|
||||
.map(String::trim)
|
||||
|
|
@ -50,6 +61,9 @@ public class RedissonConfig {
|
|||
.forEach(sentinelServersConfig::addSentinelAddress);
|
||||
|
||||
applySharedSettings(sentinelServersConfig, redisProperties);
|
||||
if (StringUtils.hasText(redisProperties.getSentinel().getPassword())) {
|
||||
sentinelServersConfig.setSentinelPassword(redisProperties.getSentinel().getPassword());
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean hasSentinelConfiguration(RedisProperties redisProperties) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
spring:
|
||||
data:
|
||||
redis:
|
||||
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:}}
|
||||
password: ${SPRING_DATA_REDIS_SENTINEL_PASSWORD:${REDIS_SENTINEL_PASSWORD:}}
|
||||
|
|
@ -95,6 +95,9 @@ spring:
|
|||
skillhub:
|
||||
builtin-skills:
|
||||
enabled: ${SKILLHUB_BUILTIN_SKILLS_ENABLED:true}
|
||||
redis:
|
||||
sentinel:
|
||||
check-sentinels-list: ${SKILLHUB_REDIS_SENTINEL_CHECK_SENTINELS_LIST:true}
|
||||
auth:
|
||||
mock:
|
||||
enabled: ${SKILLHUB_AUTH_MOCK_ENABLED:false}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -105,6 +105,67 @@ class RedissonConfigTest {
|
|||
assertThat(sentinelConfig.getSentinelAddresses()).containsExactly("rediss://redis-sentinel-1:26379");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createConfig_appliesSentinelPasswordWhenSet() throws Exception {
|
||||
RedisProperties properties = new RedisProperties();
|
||||
RedisProperties.Sentinel sentinel = new RedisProperties.Sentinel();
|
||||
sentinel.setMaster("mymaster");
|
||||
sentinel.setNodes(List.of("redis-sentinel-1:26379"));
|
||||
sentinel.setPassword("sentinel-secret");
|
||||
properties.setSentinel(sentinel);
|
||||
properties.setPassword("master-secret");
|
||||
|
||||
Config config = RedissonConfig.createConfig(properties);
|
||||
SentinelServersConfig sentinelConfig = sentinelConfig(config);
|
||||
|
||||
assertThat(sentinelConfig.getSentinelPassword()).isEqualTo("sentinel-secret");
|
||||
assertThat(sentinelConfig.getPassword()).isEqualTo("master-secret");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createConfig_keepsSentinelMembershipCheckEnabledByDefault() throws Exception {
|
||||
RedisProperties properties = new RedisProperties();
|
||||
RedisProperties.Sentinel sentinel = new RedisProperties.Sentinel();
|
||||
sentinel.setMaster("mymaster");
|
||||
sentinel.setNodes(List.of("redis-sentinel-1:26379"));
|
||||
properties.setSentinel(sentinel);
|
||||
|
||||
Config config = RedissonConfig.createConfig(properties);
|
||||
SentinelServersConfig sentinelConfig = sentinelConfig(config);
|
||||
|
||||
assertThat(sentinelConfig.isCheckSentinelsList()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void createConfig_canDisableSentinelMembershipCheckForKubernetes() throws Exception {
|
||||
RedisProperties properties = new RedisProperties();
|
||||
RedisProperties.Sentinel sentinel = new RedisProperties.Sentinel();
|
||||
sentinel.setMaster("mymaster");
|
||||
sentinel.setNodes(List.of("redis-sentinel-1:26379"));
|
||||
properties.setSentinel(sentinel);
|
||||
|
||||
Config config = RedissonConfig.createConfig(properties, false);
|
||||
SentinelServersConfig sentinelConfig = sentinelConfig(config);
|
||||
|
||||
assertThat(sentinelConfig.isCheckSentinelsList()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void createConfig_doesNotSetSentinelPasswordWhenEmpty() throws Exception {
|
||||
RedisProperties properties = new RedisProperties();
|
||||
RedisProperties.Sentinel sentinel = new RedisProperties.Sentinel();
|
||||
sentinel.setMaster("mymaster");
|
||||
sentinel.setNodes(List.of("redis-sentinel-1:26379"));
|
||||
properties.setSentinel(sentinel);
|
||||
properties.setPassword("master-secret");
|
||||
|
||||
Config config = RedissonConfig.createConfig(properties);
|
||||
SentinelServersConfig sentinelConfig = sentinelConfig(config);
|
||||
|
||||
assertThat(sentinelConfig.getPassword()).isEqualTo("master-secret");
|
||||
assertThat(sentinelConfig.getSentinelPassword()).isNull();
|
||||
}
|
||||
|
||||
private SentinelServersConfig sentinelConfig(Config config) throws Exception {
|
||||
Method method = Config.class.getDeclaredMethod("getSentinelServersConfig");
|
||||
method.setAccessible(true);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue