diff --git a/.github/workflows/pr-helm-chart.yml b/.github/workflows/pr-helm-chart.yml index c980d3ec..c24b0377 100644 --- a/.github/workflows/pr-helm-chart.yml +++ b/.github/workflows/pr-helm-chart.yml @@ -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 diff --git a/.github/workflows/publish-chart.yml b/.github/workflows/publish-chart.yml index 823524d5..5264d84c 100644 --- a/.github/workflows/publish-chart.yml +++ b/.github/workflows/publish-chart.yml @@ -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 diff --git a/README.md b/README.md index 4c36db58..2d5dec57 100644 --- a/README.md +++ b/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: diff --git a/README_zh.md b/README_zh.md index 7923daae..694bdc27 100644 --- a/README_zh.md +++ b/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 集成 diff --git a/charts/skillhub/Chart.yaml b/charts/skillhub/Chart.yaml index 3f125dd3..2bf90a60 100644 --- a/charts/skillhub/Chart.yaml +++ b/charts/skillhub/Chart.yaml @@ -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 diff --git a/charts/skillhub/README.md b/charts/skillhub/README.md index 445873d4..592a0319 100644 --- a/charts/skillhub/README.md +++ b/charts/skillhub/README.md @@ -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 diff --git a/charts/skillhub/templates/ingress.yaml b/charts/skillhub/templates/ingress.yaml index 60512116..dd5f28d2 100644 --- a/charts/skillhub/templates/ingress.yaml +++ b/charts/skillhub/templates/ingress.yaml @@ -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 }} diff --git a/charts/skillhub/templates/server-deployment.yaml b/charts/skillhub/templates/server-deployment.yaml index 84e7ac9d..b1bd6393 100644 --- a/charts/skillhub/templates/server-deployment.yaml +++ b/charts/skillhub/templates/server-deployment.yaml @@ -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 }} diff --git a/charts/skillhub/templates/validate.yaml b/charts/skillhub/templates/validate.yaml index e5a34aa2..5b1862f6 100644 --- a/charts/skillhub/templates/validate.yaml +++ b/charts/skillhub/templates/validate.yaml @@ -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 -}} diff --git a/charts/skillhub/tests/configuration-contracts.sh b/charts/skillhub/tests/configuration-contracts.sh index acf671a6..36348f32 100755 --- a/charts/skillhub/tests/configuration-contracts.sh +++ b/charts/skillhub/tests/configuration-contracts.sh @@ -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 \ diff --git a/charts/skillhub/values.schema.json b/charts/skillhub/values.schema.json index b1c6f3a0..a151d663 100644 --- a/charts/skillhub/values.schema.json +++ b/charts/skillhub/values.schema.json @@ -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" }, diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/RedissonConfig.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/RedissonConfig.java index 09123c37..9f4de0b3 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/RedissonConfig.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/RedissonConfig.java @@ -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 nodes = redisProperties.getSentinel().getNodes(); nodes.stream() .map(String::trim) diff --git a/server/skillhub-app/src/main/resources/application.yml b/server/skillhub-app/src/main/resources/application.yml index 286ff05f..7d5133c2 100644 --- a/server/skillhub-app/src/main/resources/application.yml +++ b/server/skillhub-app/src/main/resources/application.yml @@ -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} diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/config/RedissonConfigTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/config/RedissonConfigTest.java index 1095f16e..9f290ede 100644 --- a/server/skillhub-app/src/test/java/com/iflytek/skillhub/config/RedissonConfigTest.java +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/config/RedissonConfigTest.java @@ -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(); }