skillhub/deploy/k8s
XiaoSeS 934cfa6ded
Some checks are pending
Deploy Docs / build (push) Waiting to run
Deploy Docs / Deploy (push) Blocked by required conditions
Security / Dependency Review (push) Waiting to run
Security / CodeQL (java-kotlin) (push) Waiting to run
Security / CodeQL (javascript-typescript) (push) Waiting to run
Security / CodeQL (python) (push) Waiting to run
feat(auth): add Feishu as a public login provider (R1-A2) (#877)
* feat(auth): let providers override OAuth userinfo loading

Some providers do not return a flat, standard userinfo payload, so
DefaultOAuth2UserService cannot read them. Add ProviderOAuth2UserService
so a provider can claim its own registration id and supply the loading
step, while everything after it stays shared.

The override runs inside the RemoteIdentityIoExecutor boundary added in
R1-A, so a provider's HTTP call does not hold the surrounding
transaction open. Registrations without an override keep using the
default user service unchanged.

Part of R1-A2 (public Provider adapters) per
openspec/changes/enterprise-identity-platform/rollout-plan.md.

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

* feat(auth): add Feishu as a public login provider

Adds Feishu (Lark) as a public sign-in option: it authenticates a
SkillHub platform account and nothing more. No Organization membership,
no directory sync, no Namespace grants.

Feishu deviates from standard OAuth in two ways this handles:
its userinfo response is wrapped in a {code, msg, data} envelope, and it
reports errors with HTTP 200. FeishuOAuth2UserService unwraps that
envelope into flat attributes; FeishuClaimsExtractor maps them to the
shared OAuthClaims, so account decisions still run through the unified
identity core added in R1-A.

Subject and email semantics, which decide whether a login can reach an
existing account:

- open_id is the only subject. union_id stays in extra rather than
  acting as a fallback: a subject that can change between logins would
  split one person across two platform accounts. Promoting union_id
  later needs an explicit alias migration.
- A blank or missing open_id fails the login instead of binding the
  literal string "null".
- emailVerified is always false. Feishu emails are imported by an
  organization admin and never confirmed with the user, so they carry no
  verification signal and cannot be used to join an existing account.

Operational bounds: the userinfo call has connect and read timeouts so an
unresponsive Feishu endpoint cannot hold a login thread, and the
OAuth2Error description carries only the provider error code, because an
upstream message can quote the request URI and with it the access token.
Like the GitHub and GitLab extractors, the claims extractor logs nothing.

The login button follows the existing config-driven catalog: with no
client id configured, /api/v1/auth/methods does not list Feishu and no
button renders. No frontend code change is needed; the icon resolves by
provider name.

Adapted from the implementation in #696 by @yhd4711499, re-extracted onto
current main with the subject, logging and timeout changes above.

Part of R1-A2 (public Provider adapters) per
openspec/changes/enterprise-identity-platform/rollout-plan.md.

Co-authored-by: yhd4711499 <yhd4711499@users.noreply.github.com>
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

* fix(auth): bound Feishu userinfo response and stop subject leaking into displayName

Three defects found reviewing this batch against the R1-A2 spec.

Response size limit. The spec's scope line asks for "远程 I/O 超时与响应大小
限制"; only the timeouts were implemented, so a misconfigured or hostile
OAUTH2_FEISHU_BASE_URI could stream an unbounded body into the parser.
Reads at most 64 KB before parsing, mirroring the 10 MB cap the shared
WebClientConfig already applies. Uses InputStream.readNBytes rather than
adding commons-io or guava, neither of which skillhub-auth declares.

Synthesized displayName. Falling back to "feishu-<open_id>" wrote the
external subject into UserAccount.displayName and into
UserActivatedEvent, carrying it somewhere event consumers may log it --
against the R1-A gate that logs must not contain the subject. Now stops
at name -> en_name like the GitHub and GitLab extractors.

Unused mobile attribute. A phone number was extracted into the principal
attributes and read by nothing. It is PII the spec did not ask for and it
widened the redaction surface for free.

Also drops a constructor overload that only passed List.of() through, and
a test that duplicated the blank-subject path.

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

* docs(auth): document the provider adapter contract and Feishu operator setup

AGENTS.md and CONTRIBUTING.md both require docs updates when auth flows or
deployment config change; this batch changed both and touched no docs.

03-authentication-design.md described adding a provider as "branch on
registrationId inside CustomOAuth2UserService", which the
ProviderOAuth2UserService strategy supersedes. Rewrites that recipe:
register an OAuthClaimsExtractor bean per provider, add a
ProviderOAuth2UserService only when the userinfo response is non-standard,
and note that the login page needs no code change. Also records the
provider-side obligations that are easy to get wrong -- stable subject with
no fallback, emailVerified only on proven ownership, bounded remote calls,
no subject in logs -- and un-comments the config example, which still
listed GitLab as a future possibility.

faq.md told operators to delete "the github and gitlab blocks" to hide SSO
buttons. That advice was already incomplete and gets worse per provider, so
it now explains the config-driven mechanism: an empty client id keeps the
entry off the login page, no file edit needed.

09-deployment.md listed only the GitHub credentials. Adds GitLab and Feishu,
and flags a deployment trap: Feishu emails are admin-imported so
emailVerified is always false, and skillhub.access-policy.mode=EMAIL_DOMAIN
denies every unverified email, which would reject all Feishu logins.

Squares the Feishu logo viewBox. It was 407.87x324.19 while login-button
renders it in a square w-5 h-5 box, so the mark was distorted.

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

* feat(deploy): wire Feishu credentials into the release surfaces

.env.release.example advertised OAUTH2_FEISHU_* knobs that no deployment
path could actually deliver. compose.release.yml has no env_file, so every
variable must be listed explicitly, and the Helm chart and k8s base only
mapped the GitHub secret keys. Setting the documented variables therefore
did nothing.

Adds Feishu to compose.release.yml, the Helm secret template and values,
the k8s deployment and its secret example. GitLab had the identical gap, so
it is wired at the same time rather than leaving the example file half true.

validate-release-config.sh only checked that GitHub's id and secret appear
together. A half-configured provider renders a login button whose exchange
then fails, so the check now loops over all three providers. Its test gained
both-directions cases per provider plus a fully configured pass; reverting
the loop to GitHub-only makes them fail.

Also adds the provider's only failure log. Nothing downstream records a
Feishu userinfo failure -- OAuth2LoginFailureHandler does not log either --
so the previous code was silent on error. Logs the exception class and
Feishu's own error code, never the upstream msg, which can quote the access
token; a test asserts the code is present and the token is not.

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

* fix(auth): use JSON token exchange for Feishu OAuth

Made-with: Proma
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

* fix(auth): preserve Feishu OAuth browser redirect

Add safe phase-level OAuth diagnostics and redact callback credentials from request logs.

Made-with: Proma
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

* docs(deploy): clarify Feishu OAuth configuration and validation

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

* fix(deploy): pass Feishu redirect URI through releases

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

* fix(deploy): pass S3 chunked encoding setting

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

* fix(deploy): preserve default Feishu callback derivation

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

---------

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
Co-authored-by: yhd4711499 <yhd4711499@users.noreply.github.com>
2026-09-21 14:39:22 +08:00
..
base feat(auth): add Feishu as a public login provider (R1-A2) (#877) 2026-09-21 14:39:22 +08:00
overlays fix(deploy): preserve PostgreSQL PVC data layout 2026-07-29 01:41:20 +08:00
README.md Merge pull request #367 from xring/fix/postgres-lostfound 2026-07-30 09:56:00 +08:00

Kubernetes 部署指南

本文档说明如何在 Kubernetes 集群中部署 SkillHub。

前置条件

  • Kubernetes 集群 (v1.24+)
  • kubectl 已配置并连接到集群
  • nginx ingress controller 已安装(可选,用于域名访问)
  • 默认 StorageClass 已配置(用于 PVC

目录结构

deploy/k8s/
├── base/                          # 基础配置(所有场景共用)
│   ├── kustomization.yaml
│   ├── configmap.yaml
│   ├── secret.yaml.example
│   ├── services.yaml
│   ├── backend-deployment.yaml
│   ├── frontend-deployment.yaml
│   ├── scanner-deployment.yaml
│   └── ingress.yaml
│
└── overlays/
    ├── with-infra/                # 完整部署(包含内置数据库)
    │   ├── kustomization.yaml
    │   ├── postgres-statefulset.yaml
    │   └── redis-statefulset.yaml
    │
    └── external/                  # 外部数据库
        └── kustomization.yaml

快速开始

1. 创建命名空间

kubectl create namespace skillhub

2. 配置 Secret

cd deploy/k8s/base

# 复制示例文件
cp secret.yaml.example secret.yaml

# 编辑 secret.yaml修改敏感配置

Secret 配置项

说明 必填
spring-datasource-url PostgreSQL 连接 URL
spring-datasource-username 数据库用户名
spring-datasource-password 数据库密码
redis-password Redis 数据节点密码
redis-sentinel-password Redis Sentinel 独立密码
bootstrap-admin-password 管理员密码
oauth2-github-client-id GitHub OAuth ID
oauth2-github-client-secret GitHub OAuth 密钥
skill-scanner-llm-api-key LLM API 密钥
skill-scanner-llm-base-url 本地/自定义 LLM 服务地址
skill-scanner-llm-model Scanner 使用的 LLM 模型名

3. 选择部署方式

方式一:完整部署(包含 PostgreSQL + Redis

适合全新环境,自动部署数据库:

kubectl apply -k overlays/with-infra/

方式二:使用外部数据库

适合已有 PostgreSQL 和 Redis 的环境:

  1. 修改 base/configmap.yaml 中的 Redis 配置:
redis-host: your-redis-host
redis-port: "6379"

连接外部 Redis Cluster 时改为配置节点列表。保留 redis-host/redis-port 不会影响 Cluster 选择:

redis-cluster-nodes: "redis-0.example.com:6379,redis-1.example.com:6379,redis-2.example.com:6379"
redis-cluster-max-redirects: "5"
redis-username: "skillhub"
redis-ssl-enabled: "true"
redis-connect-timeout: "5s"
redis-timeout: "3s"
redis-client-name: "skillhub"

所有 Cluster 节点通告的地址都必须能从 Server Pod 访问。Redis Cluster 只支持 数据库 0

连接外部 Redis Sentinel 时配置 master、节点和独立 ACLSentinel 配置优先于 Cluster 和单机 host/port

redis-username: "skillhub"
redis-sentinel-master: "mymaster"
redis-sentinel-nodes: "sentinel-0.example.com:26379,sentinel-1.example.com:26379,sentinel-2.example.com:26379"
redis-sentinel-username: "sentinel-user"
redis-sentinel-check-list: "true"
  1. 修改 base/secret.yaml 中的数据库和 Redis 凭据:
spring-datasource-url: jdbc:postgresql://your-postgres-host:5432/skillhub
redis-password: your-redis-password
redis-sentinel-password: your-sentinel-password
  1. 部署:
kubectl apply -k overlays/external/

4. 验证部署

# 检查 Pod 状态
kubectl get pods -n skillhub

# 等待所有 Pod 就绪
kubectl wait --for=condition=ready pod --all -n skillhub --timeout=300s

5. 访问服务

方式一:端口转发(推荐本地测试)

# 前端
kubectl port-forward svc/skillhub-web -n skillhub 8080:80

# 后端 API
kubectl port-forward svc/skillhub-server -n skillhub 8081:8080

访问 http://localhost:8080

方式二Ingress 域名访问

修改 base/ingress.yaml 中的域名:

spec:
  rules:
    - host: your-domain.com  # 修改为你的域名
kubectl apply -k overlays/with-infra/  # 或 overlays/external/

部署架构

┌─────────────────────────────────────────────────────────────┐
│                        skillhub namespace                    │
├─────────────────────────────────────────────────────────────┤
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐  │
│  │ skillhub-web│  │skillhub-    │  │ skillhub-scanner    │  │
│  │   (前端)    │  │  server     │  │    (扫描器)         │  │
│  │   :80       │  │  (后端)     │  │     :8000           │  │
│  └─────────────┘  │   :8080     │  └─────────────────────┘  │
│                   └──────┬──────┘                            │
│                          │                                   │
│         ┌────────────────┴────────────────┐                  │
│         │         with-infra only          │                 │
│         │  ┌─────────────┐  ┌───────────┐ │                 │
│         │  │  postgres-0 │  │  redis-0  │ │                 │
│         │  │   :5432     │  │   :6379   │ │                 │
│         │  └─────────────┘  └───────────┘ │                 │
│         └─────────────────────────────────┘                 │
│                                                              │
│  ┌─────────────────────────────────────────────────────────┐ │
│  │              PersistentVolumeClaims                      │ │
│  │  - skillhub-storage-pvc (10Gi)                          │ │
│  │  - postgres-data-0 (10Gi) - with-infra only             │ │
│  │  - redis-data-0 (5Gi) - with-infra only                 │ │
│  └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

配置说明

ConfigMap 配置项

默认值 说明
redis-host redis Redis 主机地址
redis-port 6379 Redis 端口
redis-cluster-nodes 未设置 外部 Redis Cluster 节点,逗号分隔的 host:port
redis-cluster-max-redirects 未设置 Cluster MOVED/ASK 最大重定向次数
redis-sentinel-master 未设置 Sentinel master set 名称
redis-sentinel-nodes 未设置 Sentinel 节点,逗号分隔的 host:port
redis-sentinel-username 未设置 Sentinel ACL 用户名
redis-sentinel-check-list true 是否校验 Sentinel 返回的节点列表
redis-username 未设置 Redis ACL 用户名
redis-ssl-enabled 未设置 是否使用 TLS
redis-connect-timeout 未设置 Redis 建连超时
redis-timeout 未设置 Redis 命令超时
redis-client-name 未设置 Redis 客户端名称
storage-base-path /var/lib/skillhub/storage 技能存储路径
skillhub-storage-provider local 存储类型local/s3
skill-scanner-enabled true 是否启用扫描器
skill-scanner-url http://skillhub-scanner:8000 扫描器地址
skill-scanner-mode upload 扫描模式
bootstrap-admin-enabled true 是否创建默认管理员
bootstrap-admin-user-id docker-admin 管理员用户 ID
bootstrap-admin-username admin 管理员用户名
bootstrap-admin-display-name Platform Admin 管理员显示名称
bootstrap-admin-email admin@example.com 管理员邮箱
session-cookie-secure false HTTPS 环境设为 true

Secret 配置项

说明 必填
spring-datasource-url PostgreSQL 连接 URL
spring-datasource-username 数据库用户名
spring-datasource-password 数据库密码
redis-password Redis 数据节点密码
redis-sentinel-password Redis Sentinel 独立密码
bootstrap-admin-password 管理员密码
oauth2-github-client-id GitHub OAuth ID
oauth2-github-client-secret GitHub OAuth 密钥
skill-scanner-llm-api-key LLM API 密钥
skill-scanner-llm-base-url 本地/自定义 LLM 服务地址
skill-scanner-llm-model LLM 模型名称

存储配置

本地存储(默认)

默认使用本地文件存储,数据保存在 PVC skillhub-storage-pvc 中。

S3/OSS 存储

生产环境建议使用 S3 兼容的对象存储:

  1. 修改 ConfigMap
skillhub-storage-provider: s3
  1. 在 Secret 中添加:
skillhub-storage-s3-access-key: your-access-key
skillhub-storage-s3-secret-key: your-secret-key
  1. 在 backend-deployment.yaml 中添加环境变量:
- name: SKILLHUB_STORAGE_S3_ENDPOINT
  value: https://oss-cn-shanghai.aliyuncs.com
- name: SKILLHUB_STORAGE_S3_BUCKET
  value: skillhub-prod
- name: SKILLHUB_STORAGE_S3_REGION
  value: cn-shanghai

持久化存储

PVC 大小 说明
skillhub-storage-pvc 10Gi 技能文件存储
postgres-data-0 10Gi PostgreSQL 数据with-infra only
redis-data-0 5Gi Redis 数据with-infra only

PostgreSQL 数据目录兼容性

with-infra 会在启动时检查 PostgreSQL PVC 根目录:如果已存在 PG_VERSION,继续使用根目录中的旧集群;否则在 pgdata/ 子目录初始化新集群,避免新 ext4 卷中的 lost+found 阻止 initdb。升级现有部署不需要移动数据库文件。

回滚到不包含该检测逻辑的旧清单时,如果集群位于 pgdata/,必须保留当前启动命令,或显式设置 PGDATA=/var/lib/postgresql/data/pgdata。不要把正在运行的数据库目录手动移动到 PVC 根目录。

镜像说明

组件 镜像
后端服务 ghcr.io/iflytek/skillhub-server:latest
前端服务 ghcr.io/iflytek/skillhub-web:latest
扫描器 ghcr.io/iflytek/skillhub-scanner:latest
PostgreSQL postgres:16-alpine
Redis redis:7-alpine

默认管理员

首次启动时,如果 bootstrap-admin-enabledtrue,系统会自动创建管理员账户:

  • 用户名:admin
  • 密码:在 secret.yamlbootstrap-admin-password 中配置

安全建议:首次登录后,请立即修改默认密码。

常见问题

Pod 一直 Pending

# 检查 PVC 是否绑定
kubectl get pvc -n skillhub

# 检查节点资源
kubectl describe node <node-name>

镜像拉取失败

如果镜像私有,需要创建拉取凭证:

kubectl create secret docker-registry ghcr-secret \
  --docker-server=ghcr.io \
  --docker-username=<GitHub用户名> \
  --docker-password=<GitHub Token> \
  -n skillhub

数据库连接失败

# 检查 PostgreSQL 是否就绪
kubectl logs postgres-0 -n skillhub

# 检查 Secret 配置
kubectl get secret skillhub-secret -n skillhub -o yaml

查看日志

# 后端日志
kubectl logs -l app.kubernetes.io/name=skillhub-server -n skillhub -f

# 前端日志
kubectl logs -l app.kubernetes.io/name=skillhub-web -n skillhub -f

# 扫描器日志
kubectl logs -l app.kubernetes.io/name=skillhub-scanner -n skillhub -f

清理

# 删除所有资源
kubectl delete -k overlays/with-infra/  # 或 overlays/external/

# 删除命名空间
kubectl delete namespace skillhub