mirror of
https://github.com/iflytek/skillhub.git
synced 2026-08-27 11:14:59 +00:00
fix(helm): resolve deployment review blockers
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
This commit is contained in:
parent
e72c17b09f
commit
2854b26b5d
14 changed files with 142 additions and 28 deletions
2
.github/workflows/pr-helm-chart.yml
vendored
2
.github/workflows/pr-helm-chart.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
2
.github/workflows/publish-chart.yml
vendored
2
.github/workflows/publish-chart.yml
vendored
|
|
@ -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
|
||||
|
||||
|
|
|
|||
14
README.md
14
README.md
|
|
@ -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:
|
||||
|
|
|
|||
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 集成
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
|
|
|
|||
|
|
@ -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 -}}
|
||||
|
|
|
|||
|
|
@ -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 \
|
||||
|
|
|
|||
|
|
@ -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" },
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue