fix(helm): resolve deployment review blockers

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
This commit is contained in:
XiaoSeS 2026-07-29 14:04:26 +08:00
parent e72c17b09f
commit 2854b26b5d
14 changed files with 142 additions and 28 deletions

View file

@ -106,7 +106,7 @@ jobs:
args: >-
--set s3.enabled=true
--set s3.bucket=test-bucket
--set s3.endpoint=s3.amazonaws.com
--set s3.endpoint=https://s3.amazonaws.com
--set s3.region=us-east-1
- name: external-secret
description: 外部 Secret

View file

@ -6,7 +6,7 @@ on:
workflow_dispatch:
inputs:
version:
description: Chart and application version (for example, 0.2.13)
description: Chart and application version (for example, 0.2.14)
required: true
type: string

View file

@ -343,6 +343,20 @@ Basic Kubernetes manifests are available under [`deploy/k8s/`](./deploy/k8s):
- `backend-deployment.yaml`
- `frontend-deployment.yaml`
- `services.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.
- `ingress.yaml`
Apply them after creating your own secret:

View file

@ -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 集成

View file

@ -3,7 +3,7 @@ name: skillhub
description: Self-hosted, open-source agent skill registry for enterprises.
type: application
version: 0.1.0
appVersion: 0.2.13
appVersion: 0.2.14
keywords:
- skillhub
- ai

View file

@ -181,7 +181,7 @@ global:
images:
registry: registry.example.com/library
tag: v0.2.13
tag: v0.2.14
pullPolicy: IfNotPresent
server:
@ -271,7 +271,9 @@ standalone → replication、Redis standalone/replication → Sentinel 等切换
### Redis Sentinel
内置 Sentinel 使用 Bitnami Redis 的同一份密码同时保护 Redis 数据节点和
Sentinel。节点地址由副本数自动生成不需要手动配置
Sentinel。节点地址由副本数自动生成不需要手动配置。由于 Bitnami Sentinel
上报的 Pod 地址可能与客户端连接的 Headless Service FQDN 不同Chart 仅在该
内置模式下关闭 Redisson 的 Sentinel 地址一致性检查:
```bash
helm -n skillhub upgrade -i skillhub ./charts/skillhub \
@ -282,7 +284,9 @@ helm -n skillhub upgrade -i skillhub ./charts/skillhub \
外部 Sentinel 必须提供至少一个 `host:port` 节点。Redis 数据密码和 Sentinel
密码可以不同;使用 `existingSecret` 时分别对应 `redis-password`
`redis-sentinel-password`
`redis-sentinel-password`。外部 Sentinel 默认保留 Redisson 地址一致性检查;
只有已确认服务发现会改写节点地址时,才通过 `server.extraEnv` 显式设置
`SKILLHUB_REDIS_SENTINEL_CHECK_SENTINELS_LIST=false`
```bash
helm -n skillhub upgrade -i skillhub ./charts/skillhub \
@ -294,6 +298,15 @@ helm -n skillhub upgrade -i skillhub ./charts/skillhub \
--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"
```
### 存储配置
| 参数 | 描述 | 默认值 |
@ -327,8 +340,8 @@ helm -n skillhub upgrade -i skillhub ./charts/skillhub \
|------|------|--------|
| `s3.enabled` | 启用 S3 | `false` |
| `s3.bucket` | Bucket 名称 | `skillhub-storage` |
| `s3.endpoint` | S3 端点 | `""` |
| `s3.publicEndpoint` | S3 公网访问端点 | `""` |
| `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` |
@ -341,7 +354,7 @@ helm -n skillhub upgrade -i skillhub ./charts/skillhub \
-f values-production.yaml \
--set s3.enabled=true \
--set s3.bucket=your-bucket \
--set s3.endpoint=s3.amazonaws.com \
--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
@ -374,9 +387,11 @@ ingress:
alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":6443}]'
```
`hosts` 是至少包含一个条目的对象数组。Chart 自动将 `/api` 转发给 Server
`hosts[].paths` 中的路径转发给 Web因此 `/api` 是保留路径。`tls` 同样是数组,
可为不同证书分别配置域名TLS 域名会写入 cert-manager Certificate SAN
`hosts` 是至少包含一个条目的对象数组。Chart 自动将 `/api``/oauth2`
`/login/oauth2``/.well-known` 直接转发给 Server确保 TLS 终止后的 OAuth
回调协议保持正确;`hosts[].paths` 中的其他路径转发给 Web因此上述四个前缀
均为保留路径。`tls` 同样是数组可为不同证书分别配置域名TLS 域名会写入
cert-manager Certificate SAN
```yaml
ingress:
@ -410,6 +425,13 @@ helm -n skillhub upgrade -i skillhub ./charts/skillhub \
每个 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

View file

@ -30,6 +30,27 @@ spec:
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 }}

View file

@ -100,6 +100,10 @@ spec:
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 }}

View file

@ -17,8 +17,8 @@
{{- end -}}
{{- range $host := .Values.ingress.hosts -}}
{{- range $path := $host.paths -}}
{{- if regexMatch "^/api(?:/|$)" $path.path -}}
{{- fail "ingress.hosts[].paths reserves /api for the SkillHub server" -}}
{{- 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 -}}
@ -61,6 +61,12 @@
{{- 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 "^https?://.+" .Values.s3.endpoint)) -}}
{{- fail "s3.endpoint must be an absolute HTTP(S) URL" -}}
{{- end -}}
{{- if and .Values.s3.publicEndpoint (not (regexMatch "^https?://.+" .Values.s3.publicEndpoint)) -}}
{{- fail "s3.publicEndpoint must be an absolute HTTP(S) URL" -}}
{{- end -}}
{{- if not .Values.secrets.allowAutoGenerated -}}
{{- if not .Values.existingSecret -}}

View file

@ -102,6 +102,8 @@ 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 external-sentinel "$CHART_DIR" \
--set postgresql.enabled=false \
@ -115,6 +117,9 @@ render external-sentinel "$CHART_DIR" \
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' \
@ -131,6 +136,16 @@ render tls "$CHART_DIR" \
--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 \
@ -203,6 +218,11 @@ assert_rejected old-sentinel-service-switch --set redis.sentinel.service.enabled
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 invalid-s3-endpoint --set s3.endpoint=s3.amazonaws.com
assert_rejected invalid-s3-public-endpoint --set s3.publicEndpoint=cdn.example.com
assert_rejected empty-ingress-hosts --set-json 'ingress.hosts=[]'
assert_rejected cert-manager-without-tls \
--set ingress.enabled=true \

View file

@ -114,8 +114,8 @@
"properties": {
"enabled": { "type": "boolean" },
"bucket": { "type": "string", "minLength": 1 },
"endpoint": { "type": "string" },
"publicEndpoint": { "type": "string" },
"endpoint": { "type": "string", "pattern": "^(|https?://.+)$" },
"publicEndpoint": { "type": "string", "pattern": "^(|https?://.+)$" },
"region": { "type": "string", "minLength": 1 },
"forcePathStyle": { "type": "boolean" },
"disableChunkedEncoding": { "type": "boolean" },

View file

@ -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,13 +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())
// K8s headless DNS 场景下客户端通过 pod FQDN 连接 sentinel
// sentinel 自身上报的地址格式不同跳过地址一致性检查避免误报连接失败
.setCheckSentinelsList(false);
.setCheckSentinelsList(checkSentinelsList);
List<String> nodes = redisProperties.getSentinel().getNodes();
nodes.stream()
.map(String::trim)

View file

@ -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}

View file

@ -123,7 +123,7 @@ class RedissonConfigTest {
}
@Test
void createConfig_sentinelCheckSentinelsListDisabled() throws Exception {
void createConfig_keepsSentinelMembershipCheckEnabledByDefault() throws Exception {
RedisProperties properties = new RedisProperties();
RedisProperties.Sentinel sentinel = new RedisProperties.Sentinel();
sentinel.setMaster("mymaster");
@ -133,6 +133,20 @@ class RedissonConfigTest {
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();
}