Compare commits

...
Author SHA1 Message Date
deploy-promoter[bot] e1b552d8ce fix(k3s): stabilize platform probes and nginx dev image 2026-03-07 08:20:46 +01:00
deploy-promoter[bot] 840f50fdc0 feat(platform): add platform blueprint scaffolding 2026-03-07 08:12:05 +01:00
deploy-promoter[bot] 3237346d73 feat(gitops): align prod ownership and ingress contracts 2026-03-07 08:10:27 +01:00
deploy-promoter[bot] e56ab2b9db fix(gitops): pin ljwx-chat to verified harbor image 2026-03-07 05:29:54 +01:00
deploy-promoter[bot] e473eb8486 chore(gitops): prefer https openclaw gateway url 2026-03-07 05:03:00 +01:00
deploy-promoter[bot] 62a25e937c fix(gitops): configure ljwx-chat openclaw gateway 2026-03-07 02:32:05 +01:00
deploy-promoter[bot] ee7cf6df35 fix(gitops): use internal openclaw service for chat 2026-03-07 01:15:42 +01:00
deploy-promoter[bot] 199a20faf3 fix(gitops): configure ljwx-chat openclaw runtime 2026-03-07 00:53:39 +01:00
deploy-promoter[bot] 819a7dbee9 fix(gitops): standardize public ingress and onboarding 2026-03-06 17:53:37 +01:00
deploy-promoter[bot] 674a38015b feat(gitops): sync bookstore llm secret into k3s 2026-03-06 05:41:05 +01:00
deploy-promoter[bot] 1751f48477 feat(gitops): onboard ljwx-shiti dev deployment 2026-03-06 00:55:48 +01:00
deploy-promoter[bot] d24f75bb04 fix(stock): tune cronjob limits and align claude model alias 2026-03-05 16:14:05 +01:00
deploy-promoter[bot] 85a975edcd fix(gitops): isolate prod bootstrap on production cluster 2026-03-05 15:53:40 +01:00
deploy-promoter[bot] 26589b4ff1 feat(ljwx-dify): automate anthropic default llm bootstrap 2026-03-05 15:46:35 +01:00
deploy-promoter[bot] c9f4cd9ce8 fix(promoter): run latest repo script in cronjobs 2026-03-05 15:41:08 +01:00
deploy-promoter[bot] 192571e885 feat(promoter): support insecure harbor tls mode 2026-03-05 15:28:06 +01:00
deploy-promoter[bot] 5c3bec02c1 fix(promoter): probe harbor artifact api by digest 2026-03-05 15:25:35 +01:00
deploy-promoter[bot] 3e615f1614 fix(promoter): broaden harbor manifest media checks 2026-03-05 15:24:08 +01:00
deploy-promoter[bot] ce0015781f feat(smart-cs): enforce sprint gates and data contract 2026-03-05 15:23:17 +01:00
deploy-promoter[bot] 1b52e64914 feat(website): add prod gitops mapping and app 2026-03-05 15:09:59 +01:00
deploy-promoter[bot] be15bcddb1 fix(stock-etl): promote formal qlib bootstrap/predict images 2026-03-05 14:05:54 +01:00
deploy-promoter[bot] f04f1e9401 feat(gitops): automate prod gate with harbor tag relay 2026-03-05 14:01:19 +01:00
deploy-promoter[bot] 478cf86201 fix(ljwx-dify): add db presync migration and tls ingress 2026-03-05 09:31:21 +01:00
deploy-promoter[bot] 3b0a9441d0 chore(harbor): move service mirror namespace from app to ljwx 2026-03-05 09:03:06 +01:00
deploy-promoter[bot] caca119ed3 fix(k3s): use ghcr-pull and brunogao promoter image 2026-03-05 08:33:38 +01:00
deploy-promoter[bot] 1fcda8606d fix(gitops): align website chat dify to brunogao ghcr namespace 2026-03-05 08:15:04 +01:00
deploy-promoter[bot] 39fbf2e38d feat(gitops): onboard ljwx website chat dify services 2026-03-05 07:46:15 +01:00
deploy-promoter[bot] a86eabd4d2 chore(stock-agent): sync strategy weight env vars 2026-03-05 07:35:45 +01:00
174 changed files with 10126 additions and 436 deletions
@@ -0,0 +1,50 @@
---
name: Platform Execution
about: Track platform delivery work with explicit planning, evidence, and rollback fields
title: "[Platform] "
labels: platform
assignees: ""
---
## Summary
<!-- Describe the scope, expected outcome, and impacted behavior. -->
## Type
- [ ] Epic
- [ ] Issue
- [ ] Follow-up
## Priority
- [ ] P0
- [ ] P1
- [ ] P2
- [ ] P3
## Week
<!-- Use YYYY-Www, for example 2026-W10. -->
## Repos touched
- `ljwx-deploy`
## Files / Paths expected to change
- `platform/...`
## Dependencies
- None
## Acceptance Criteria
- [ ]
## Evidence Required
- [ ] PR link
- [ ] Manifest or config diff
- [ ] Runtime verification output
- [ ] Docs updated if contract or behavior changed
## Rollback Plan
- Revert target:
- Re-apply or resync step:
- Verification after rollback:
## Links
<!-- Optional milestone, epic, dashboard, ArgoCD, or runbook links. -->
+22
View File
@@ -57,6 +57,28 @@ jobs:
- name: Setup uv
uses: astral-sh/setup-uv@v4
- name: Smart CS contract gate
run: bash scripts/ci/check-smart-cs-contract.sh
- name: Smart CS smoke targets gate
run: bash scripts/ci/check-smart-cs-smoke-targets.sh
- name: Plaintext secret scan
run: bash scripts/ci/check-no-plain-secrets.sh .changed-files.txt
- name: Detect pgvector related changes
id: pgvector
run: |
if grep -Eq '^(apps/ljwx-dify/|apps/n8n/|config/dify/|cluster/.*postgres)' .changed-files.txt; then
echo "required=true" >> "$GITHUB_OUTPUT"
else
echo "required=false" >> "$GITHUB_OUTPUT"
fi
- name: Check pgvector baseline
if: steps.pgvector.outputs.required == 'true'
run: bash scripts/ops/check-pgvector.sh
- name: Validate release queue structure
run: uvx --with pyyaml python scripts/promoter/validate_queue.py
+40
View File
@@ -2,6 +2,19 @@
GitOps 配置仓库,使用 ArgoCD 管理 Kubernetes 集群。
## Start Here
- 新进程/新项目/老项目统一接入入口:`docs/START-HERE-GITOPS-ONBOARDING.md`
- 自动扫描当前仓库是否已接入:`bash scripts/ops/scan-gitops-context.sh --repo "$PWD"`
## Platform Blueprint
- 平台装配蓝图入口:`docs/platform/README.md`
- 平台配置与契约:`platform/`
- 集成版本清单:`release/platform-version.yaml`
- 平台 runbooks:`runbooks/`
- 新集群初始化脚本骨架:`scripts/bootstrap/cluster-init.sh`
## Bid-MVP Factory Control Plane
- 运行验证:`bash scripts/verify.sh`
@@ -9,18 +22,45 @@ GitOps 配置仓库,使用 ArgoCD 管理 Kubernetes 集群。
- Promoter/Argo/Smoke 运维:`docs/ops-runbook.md`
- 回滚演练:`docs/rollback-drill.md`
- Grafana 可观测看板 + TLS(GitOps):`apps/ljwx-platform-observability/`
- Public ingress + Let's Encrypt(Traefik HTTP01,GitOps):`argocd-apps/03-cert-manager-dev.yaml`、`argocd-apps/04-cert-manager-config-dev.yaml`、`apps/cert-manager-config/`
- 双 k3s 同代码部署规范:`docs/dual-k3s-deployment.md`
- CronJob secret 示例:`cluster/deploy-promoter-secret.example.yaml`
- 模板化批量接入:`docs/onboarding-automation.md`
- 服务仓 enqueue 工作流片段:`docs/service-repo-workflow-snippet.md`
- Argo dev 自动同步说明:`docs/argocd-dev-autosync.md`
## Harbor 同步基线(2026-03-05)
业务镜像统一命名空间:
- `harbor.eu.lingjingwanxiang.cn/ljwx/<service>`
当前复制策略(本地 Harbor API: `http://127.0.0.1:8080`):
1. `sync-ljwx-to-cn-event`(event_based)
- 源:本地 Harbor `ljwx/**`
- 目标:`https://harbor.omniverseai.net` 命名空间 `ljwx`
2. `sync-ljwx-to-cn-nightly`(scheduled)
- cron: `0 0 19 * * *`
- 源:本地 Harbor `ljwx/**`
- 目标:`https://harbor.omniverseai.net` 命名空间 `ljwx`
快速核验:
```bash
curl -u admin:Harbor12345 \
'http://127.0.0.1:8080/api/v2.0/replication/policies?page=1&page_size=100' \
| jq 'map({id,name,enabled,dest_namespace,trigger,speed})'
```
## 目录结构
- `infra/` - 基础设施组件(Gitea Runner 等)
- `argocd-apps/` - ArgoCD Application 定义
- `apps/` - 应用配置
- `cluster/` - 集群级别配置
- `platform/` - 平台装配、契约、路由、知识治理配置
- `runbooks/` - 平台级操作手册
## 组件
@@ -0,0 +1,16 @@
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: dnspod-letsencrypt-staging
annotations:
gitops.ljwx.io/issuer-implementation: http01-traefik
spec:
acme:
email: platform@lingjingwanxiang.cn
server: https://acme-staging-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: dnspod-letsencrypt-staging-account-key
solvers:
- http01:
ingress:
ingressClassName: traefik
@@ -0,0 +1,16 @@
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: dnspod-letsencrypt
annotations:
gitops.ljwx.io/issuer-implementation: http01-traefik
spec:
acme:
email: platform@lingjingwanxiang.cn
server: https://acme-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: dnspod-letsencrypt-account-key
solvers:
- http01:
ingress:
ingressClassName: traefik
@@ -0,0 +1,5 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- clusterissuer-dnspod-letsencrypt.yaml
- clusterissuer-dnspod-letsencrypt-staging.yaml
@@ -0,0 +1,4 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
+13 -1
View File
@@ -37,6 +37,10 @@ kubectl get crd servicemonitors.monitoring.coreos.com
#### 创建 Secret
```bash
# 推荐:在 ljwx-deploy 仓库根目录执行,自动从 infra-credentials 拉取密码
./scripts/create-app-secret.sh ljwx-bookstore /root/codes/.env
# 手动方式(如需逐项指定):
kubectl create secret generic ljwx-bookstore-secret \
--namespace=ljwx-bookstore \
--from-literal=DB_HOST='mysql-infra.infra.svc.cluster.local' \
@@ -55,6 +59,14 @@ kubectl create secret generic ljwx-bookstore-secret \
--from-literal=STORAGE_ALIYUN_ACCESS_KEY_SECRET='YOUR_SECRET'
```
如果 `/root/codes/.env` 中存在 Claude 代理配置,也可以单独同步可选的
`ljwx-bookstore-llm-secret`:
```bash
./scripts/sync-llm-secret.sh ljwx-bookstore /root/codes/.env
kubectl rollout restart deployment/ljwx-bookstore -n ljwx-bookstore
```
#### 部署应用
```bash
# 方式 1: Kustomize
@@ -81,7 +93,7 @@ kubectl exec -n ljwx-bookstore deployment/ljwx-bookstore -- \
kubectl get servicemonitor -n ljwx-bookstore
# 访问应用
curl http://book.omniverseai.net/fiction/index
curl https://bookstore.lingjingwanxiang.cn/fiction/index
```
### 📊 监控验证
+13 -1
View File
@@ -77,6 +77,10 @@ kubectl get svc -n tracing otel-collector-opentelemetry-collector
创建包含敏感信息的 Secret(**不要提交到 Git**):
```bash
# 推荐:在 ljwx-deploy 仓库根目录执行
./scripts/create-app-secret.sh ljwx-bookstore /root/codes/.env
# 手动方式:
kubectl create secret generic ljwx-bookstore-secret \
--namespace=ljwx-bookstore \
--from-literal=DB_HOST='mysql-infra.infra.svc.cluster.local' \
@@ -96,6 +100,14 @@ kubectl create secret generic ljwx-bookstore-secret \
--dry-run=client -o yaml | kubectl apply -f -
```
如果 `/root/codes/.env` 中存在 Claude 代理配置,可额外同步可选的
`ljwx-bookstore-llm-secret`,Deployment 会自动读取它覆盖默认 LLM 配置:
```bash
./scripts/sync-llm-secret.sh ljwx-bookstore /root/codes/.env
kubectl rollout restart deployment/ljwx-bookstore -n ljwx-bookstore
```
**生产环境建议**:使用 External Secrets Operator 或 Sealed Secrets 管理敏感信息。
### 步骤 2: 部署应用 (GitOps)
@@ -182,7 +194,7 @@ kubectl get servicemonitor -n ljwx-bookstore
kubectl get ingress -n ljwx-bookstore
# 访问应用
curl http://book.omniverseai.net/fiction/index
curl https://bookstore.lingjingwanxiang.cn/fiction/index
curl http://bookstore.192.168.1.83.nip.io/fiction/index
```
+37 -36
View File
@@ -2,17 +2,15 @@
## 域名信息
- **生产域名**: `book.omniverseai.net`
- **DNS提供商**: Cloudflare
- **SSL证书**: Let's Encrypt (自动通过cert-manager获取)
- **生产域名**: `bookstore.lingjingwanxiang.cn`
- **入口控制器**: Traefik
- **SSL证书**: Let's Encrypt (自动通过 cert-manager 获取)
## DNS记录配置
### 步骤1: 登录 Cloudflare
### 步骤1: 配置域名解析
1. 访问 https://dash.cloudflare.com/
2. 选择域名 `omniverseai.net`
3. 进入 DNS 管理页面
将 `bookstore.lingjingwanxiang.cn` 解析到 k3s 对外入口 IP。
### 步骤2: 添加 A 记录
@@ -20,7 +18,7 @@
| 类型 | 名称 | 内容 | 代理状态 | TTL |
|------|------|------|----------|-----|
| A | book | `<INGRESS_IP>` | 仅DNS(灰色云朵) | Auto |
| A | bookstore | `<INGRESS_IP>` | 仅DNS | Auto |
**获取 Ingress IP**:
```bash
@@ -34,20 +32,16 @@ kubectl get ingress ljwx-bookstore -n ljwx-bookstore \
192.168.194.177 # 示例,请使用实际IP
```
### 步骤3: Cloudflare 配置建议
### 步骤3: HTTPS 配置建议
#### SSL/TLS 设置
1. 进入 `SSL/TLS` → `概述`
2. 加密模式选择: **完全(严格)** 或 **完全**
3. 推荐: **完全(严格)** - 确保端到端加密
#### 代理状态
- **开发/测试**: 仅DNS(灰色云朵)- 直连Kubernetes
- **生产环境**: 代理(橙色云朵)- 使用Cloudflare CDN和DDoS防护
#### 其他推荐配置
- 启用 **自动HTTPS重写**
- 启用 **始终使用HTTPS**
- DNS 记录生效前不要切换业务入口
- 保证 `bookstore.lingjingwanxiang.cn` 已解析到 Traefik 对外 IP
- 配置 **最小TLS版本**: TLS 1.2
## 验证步骤
@@ -56,10 +50,10 @@ kubectl get ingress ljwx-bookstore -n ljwx-bookstore \
```bash
# 检查DNS记录
dig book.omniverseai.net +short
dig bookstore.lingjingwanxiang.cn +short
# 或使用nslookup
nslookup book.omniverseai.net
nslookup bookstore.lingjingwanxiang.cn
```
**期望输出**: 应该返回Ingress Controller的IP地址
@@ -73,10 +67,10 @@ nslookup book.omniverseai.net
kubectl get certificate -n ljwx-bookstore
# 查看证书详情
kubectl describe certificate ljwx-bookstore-tls -n ljwx-bookstore
kubectl describe certificate bookstore-lingjingwanxiang-cn-tls -n ljwx-bookstore
# 检查证书Secret
kubectl get secret ljwx-bookstore-tls -n ljwx-bookstore
kubectl get secret bookstore-lingjingwanxiang-cn-tls -n ljwx-bookstore
```
**期望状态**: `READY=True`
@@ -85,26 +79,35 @@ kubectl get secret ljwx-bookstore-tls -n ljwx-bookstore
```bash
# HTTP 自动重定向到 HTTPS
curl -I http://book.omniverseai.net
curl -I http://bookstore.lingjingwanxiang.cn
# HTTPS 访问
curl -I https://book.omniverseai.net
curl -I https://bookstore.lingjingwanxiang.cn
# 如果当前终端存在 DNS / 公网回环限制,改用 live Ingress 探测
python3 scripts/ops/public_ingress_access.py probe \
--host bookstore.lingjingwanxiang.cn \
--path /fiction/index
# 需要浏览器本机验收时,先生成推荐 hosts 映射
python3 scripts/ops/public_ingress_access.py hosts \
--host bookstore.lingjingwanxiang.cn
# 浏览器访问
open https://book.omniverseai.net/fiction/index
open https://bookstore.lingjingwanxiang.cn/fiction/index
```
### 4. SSL证书检查
```bash
# 使用 openssl 检查证书
echo | openssl s_client -servername book.omniverseai.net \
-connect book.omniverseai.net:443 2>/dev/null | \
echo | openssl s_client -servername bookstore.lingjingwanxiang.cn \
-connect bookstore.lingjingwanxiang.cn:443 2>/dev/null | \
openssl x509 -noout -dates -subject
# 在线检查
# 访问 https://www.ssllabs.com/ssltest/
# 输入: book.omniverseai.net
# 输入: bookstore.lingjingwanxiang.cn
```
## 故障排查
@@ -112,7 +115,7 @@ echo | openssl s_client -servername book.omniverseai.net \
### DNS未解析
```bash
# 检查Cloudflare DNS设置
# 检查 DNS 设置
# 1. 确认A记录存在
# 2. 确认IP地址正确
# 3. 清除本地DNS缓存: sudo dscacheutil -flushcache (macOS)
@@ -125,16 +128,16 @@ echo | openssl s_client -servername book.omniverseai.net \
kubectl logs -n cert-manager -l app=cert-manager --tail=50
# 检查 Certificate Events
kubectl describe certificate ljwx-bookstore-tls -n ljwx-bookstore
kubectl describe certificate bookstore-lingjingwanxiang-cn-tls -n ljwx-bookstore
# 检查 CertificateRequest
kubectl get certificaterequest -n ljwx-bookstore
kubectl describe certificaterequest -n ljwx-bookstore
# 常见问题:
# 1. Cloudflare API Token权限不足
# 2. DNS记录配置错误
# 3. ClusterIssuer配置问题
# 1. DNS记录配置错误
# 2. ClusterIssuer配置问题
# 3. Traefik 未接住 HTTP01 challenge
```
### HTTPS访问失败
@@ -144,8 +147,8 @@ kubectl describe certificaterequest -n ljwx-bookstore
kubectl get ingress ljwx-bookstore -n ljwx-bookstore
kubectl describe ingress ljwx-bookstore -n ljwx-bookstore
# 检查 Ingress Controller 日志
kubectl logs -n ingress-nginx -l app.kubernetes.io/component=controller --tail=50
# 检查 Traefik 日志
kubectl logs -n kube-system -l app.kubernetes.io/name=traefik --tail=50
```
## 生产环境清单
@@ -156,18 +159,16 @@ kubectl logs -n ingress-nginx -l app.kubernetes.io/component=controller --tail=5
- [ ] SSL证书自动获取成功(`kubectl get certificate -n ljwx-bookstore`)
- [ ] HTTPS强制重定向已启用
- [ ] 健康检查正常工作
- [ ] 应用可通过 https://book.omniverseai.net 访问
- [ ] 应用可通过 https://bookstore.lingjingwanxiang.cn 访问
- [ ] SSL证书A级评分(ssllabs.com测试)
- [ ] Cloudflare防火墙规则已配置(可选)
- [ ] 监控和告警已设置(可选)
## 相关资源
- Ingress配置: `ingress.yaml`
- cert-manager文档: https://cert-manager.io/docs/
- Cloudflare API: https://developers.cloudflare.com/api/
- Let's Encrypt文档: https://letsencrypt.org/docs/
## 更新历史
- 2026-01-01: 初始配置,添加 book.omniverseai.net 域名
- 2026-03-06: 切换到 bookstore.lingjingwanxiang.cn,并统一到 Traefik + cert-manager 基线
+33 -3
View File
@@ -15,6 +15,9 @@
./scripts/create-app-secret.sh ljwx-bookstore
```
如果 `/root/codes/.env` 中存在 Claude 代理配置,上面的脚本会额外同步
`ljwx-bookstore-llm-secret`,供 Deployment 以可选方式注入。
### 手动创建
如果自动化脚本不可用,使用以下命令手动创建:
@@ -43,10 +46,16 @@ kubectl create secret generic ljwx-bookstore-secret \
--from-literal=STORAGE_ALIYUN_ACCESS_KEY_SECRET="placeholder" \
--from-literal=STORAGE_TENCENT_SECRET_ID="placeholder" \
--from-literal=STORAGE_TENCENT_SECRET_KEY="placeholder" \
--from-literal=AI_CLOUD_API_KEY="" \
--namespace=ljwx-bookstore
```
如果需要让 `ljwx-bookstore` 使用 `/root/codes/.env` 中的 Claude 代理配置:
```bash
./scripts/sync-llm-secret.sh ljwx-bookstore /root/codes/.env
kubectl rollout restart deployment/ljwx-bookstore -n ljwx-bookstore
```
## Secret 架构
```
@@ -90,7 +99,12 @@ kubectl create secret generic ljwx-bookstore-secret \
./scripts/create-app-secret.sh ljwx-bookstore
```
3. **部署应用**(Argo CD自动同步)
3. **如需云端 LLM,创建可选 LLM Secret**
```bash
./scripts/sync-llm-secret.sh ljwx-bookstore /root/codes/.env
```
4. **部署应用**(Argo CD自动同步)
```bash
argocd app sync ljwx-bookstore
```
@@ -109,7 +123,10 @@ kubectl patch secret infra-credentials -n infra \
kubectl delete secret ljwx-bookstore-secret -n ljwx-bookstore
./scripts/create-app-secret.sh ljwx-bookstore
# 3. 重启应用
# 3. 如果修改了 /root/codes/.env,同步 LLM Secret
./scripts/sync-llm-secret.sh ljwx-bookstore /root/codes/.env
# 4. 重启应用
kubectl rollout restart deployment ljwx-bookstore -n ljwx-bookstore
```
@@ -167,6 +184,19 @@ kubectl delete secret ljwx-bookstore-secret -n ljwx-bookstore
./scripts/create-app-secret.sh ljwx-bookstore
```
### LLM 配置未生效
```bash
# 重新同步可选 LLM Secret
./scripts/sync-llm-secret.sh ljwx-bookstore /root/codes/.env
# 确认 Deployment 已注入 ljwx-bookstore-llm-secret
kubectl get deployment ljwx-bookstore -n ljwx-bookstore -o yaml | grep -n "ljwx-bookstore-llm-secret"
# 重启应用
kubectl rollout restart deployment/ljwx-bookstore -n ljwx-bookstore
```
## 相关文档
- 详细架构设计:`/docs/GITOPS_SECRET_MANAGEMENT.md`
+1 -1
View File
@@ -87,7 +87,7 @@ data:
# 微信支付配置(非敏感)
HGBOOK_WX_APP_ID: "wxe92bc58a93b04a87"
HGBOOK_WX_MCH_ID: "1623644809"
HGBOOK_WX_NOTIFY_URL: "http://bookstore.192.168.1.83.nip.io/wx/order/pay-notify"
HGBOOK_WX_NOTIFY_URL: "https://bookstore.lingjingwanxiang.cn/wx/order/pay-notify"
# 邮件配置(非敏感)
HGBOOK_NOTIFY_MAIL_ENABLE: "true"
+3
View File
@@ -70,6 +70,9 @@ spec:
envFrom:
- configMapRef:
name: ljwx-bookstore-config
- secretRef:
name: ljwx-bookstore-llm-secret
optional: true
# 环境变量 - 从 Secret(敏感信息)
env:
+13 -20
View File
@@ -1,7 +1,7 @@
---
# =============================================================================
# ljwx-bookstore Ingress Configuration
# 域名: book.omniverseai.net (HTTP only)
# 域名: bookstore.lingjingwanxiang.cn (HTTPS)
# =============================================================================
apiVersion: networking.k8s.io/v1
kind: Ingress
@@ -12,28 +12,21 @@ metadata:
app: ljwx-bookstore
component: backend
annotations:
# Nginx Ingress 配置
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/proxy-body-size: "50m"
nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "600"
# 禁用SSL重定向 - HTTP only
nginx.ingress.kubernetes.io/ssl-redirect: "false"
# 健康检查
nginx.ingress.kubernetes.io/health-check-path: "/fiction/actuator/health"
# CORS 配置(如果需要)
nginx.ingress.kubernetes.io/enable-cors: "true"
nginx.ingress.kubernetes.io/cors-allow-origin: "http://book.omniverseai.net"
cert-manager.io/cluster-issuer: dnspod-letsencrypt
traefik.ingress.kubernetes.io/router.entrypoints: web,websecure
traefik.ingress.kubernetes.io/router.middlewares: ljwx-bookstore-root-redirect@kubernetescrd
traefik.ingress.kubernetes.io/router.tls: "true"
spec:
ingressClassName: nginx
ingressClassName: traefik
tls:
- hosts:
- bookstore.lingjingwanxiang.cn
secretName: bookstore-lingjingwanxiang-cn-tls
# 路由规则
rules:
# 生产域名
- host: book.omniverseai.net
# 正式域名
- host: bookstore.lingjingwanxiang.cn
http:
paths:
- path: /
@@ -44,7 +37,7 @@ spec:
port:
number: 80
# 本地测试域名
# 历史本地调试域名
- host: bookstore.192.168.1.83.nip.io
http:
paths:
@@ -14,6 +14,7 @@ resources:
- configmap.yaml
- deployment.yaml
- service.yaml
- root-redirect-middleware.yaml
- ingress.yaml
- servicemonitor.yaml
- db-init-job.yaml
@@ -0,0 +1,17 @@
---
# =============================================================================
# Root path redirect for ljwx-bookstore public entrypoint
# =============================================================================
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: root-redirect
namespace: ljwx-bookstore
labels:
app: ljwx-bookstore
component: backend
spec:
redirectRegex:
regex: ^https?://([^/]+)/?$
replacement: https://${1}/fiction/index
permanent: true
@@ -0,0 +1,28 @@
# ljwx-bookstore capability contract (dev)
## Namespace baseline
- namespace: `ljwx-bookstore`
- namespace profile: `dev-default`
- registry profile: `harbor-only`
- runtime secret profile: `none`
- image pull secrets: `regcred`
- runtime contract secrets: `none`
## Capabilities
- ghcr_pull (namespace): Bind GHCR pull secret ghcr-pull to the namespace baseline.
- harbor_pull (namespace): Bind Harbor pull secret regcred to the namespace baseline.
- runtime_app_secret (application): Standardize service-specific runtime secret as runtime-app.
## Bootstrap commands
```bash
bash scripts/ops/sync-registry-pull-secrets.sh --target-namespace ljwx-bookstore --registry-profile harbor-only
```
## Notes
- `runtime-contract/*.secret.example.yaml` are placeholders only and must not contain real credentials.
- `cluster_bootstrap` is disabled for this entry; keep the existing cluster namespace/application manifests as the current source of truth.
- For legacy workloads, migrate deployment manifests gradually to `runtime-infra`, `runtime-llm`, and `runtime-app` instead of app-specific secret names.
@@ -0,0 +1,20 @@
service: ljwx-bookstore
environment: dev
namespace: ljwx-bookstore
cluster_bootstrap: false
ingress_profile: none
public_host: ''
generate_ingress: true
ingress_class_name: ''
cluster_issuer: ''
tls_secret_name: ''
namespace_profile: dev-default
registry_profile: harbor-only
runtime_secret_profile: none
capabilities:
- ghcr_pull
- harbor_pull
- runtime_app_secret
image_pull_secrets:
- regcred
contract_secret_names: []
+30
View File
@@ -0,0 +1,30 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: ljwx-chat-config
data:
NODE_ENV: "production"
PORT: "3210"
HOSTNAME: "0.0.0.0"
DATABASE_DRIVER: "node"
NEXT_PUBLIC_ENABLE_NEXT_AUTH: "0"
NEXT_PUBLIC_ENABLE_LOCAL_AUTH: "1"
NEXT_AUTH_SSO_SESSION_STRATEGY: "jwt"
NEXT_AUTH_SSO_PROVIDERS: ""
AUTH_TRUST_HOST: "true"
EMAIL_ENABLED: "false"
NEXT_PUBLIC_BASE_URL: "https://chat.lingjingwanxiang.cn"
NEXTAUTH_URL: "https://chat.lingjingwanxiang.cn"
S3_BUCKET: "ljwx-chat"
S3_ENABLE_PATH_STYLE: "true"
MIDDLEWARE_REWRITE_THROUGH_LOCAL: "1"
ENABLED_OLLAMA: "0"
OPENCLAW_GATEWAY_URL: "https://openclaw.lingjingwanxiang.cn/v1"
OPENCLAW_GATEWAY_ORIGIN: "https://openclaw.lingjingwanxiang.cn"
+58
View File
@@ -0,0 +1,58 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: ljwx-chat
spec:
replicas: 1
selector:
matchLabels:
app: ljwx-chat
template:
metadata:
labels:
app: ljwx-chat
spec:
imagePullSecrets:
- name: ghcr-pull
- name: regcred
containers:
- name: ljwx-chat
image: ghcr.io/brunogao/ljwx-chat:latest
imagePullPolicy: Always
ports:
- containerPort: 3210
name: http
protocol: TCP
envFrom:
- configMapRef:
name: ljwx-chat-config
- secretRef:
name: ljwx-chat-secrets
livenessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 20
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "2Gi"
cpu: "1000m"
securityContext:
runAsNonRoot: true
runAsUser: 1001
allowPrivilegeEscalation: false
readOnlyRootFilesystem: false
+29
View File
@@ -0,0 +1,29 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ljwx-chat
annotations:
cert-manager.io/cluster-issuer: dnspod-letsencrypt
traefik.ingress.kubernetes.io/router.entrypoints: web,websecure
traefik.ingress.kubernetes.io/router.tls: "true"
nginx.ingress.kubernetes.io/proxy-body-size: "100m"
nginx.ingress.kubernetes.io/proxy-connect-timeout: "600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "600"
nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
spec:
ingressClassName: traefik
tls:
- hosts:
- chat.lingjingwanxiang.cn
secretName: chat-lingjingwanxiang-cn-tls
rules:
- host: chat.lingjingwanxiang.cn
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: ljwx-chat
port:
number: 80
+12
View File
@@ -0,0 +1,12 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- configmap.yaml
- deployment.yaml
- service.yaml
- ingress.yaml
commonLabels:
app.kubernetes.io/name: ljwx-chat
app.kubernetes.io/part-of: ljwx-chat-suite
+17
View File
@@ -0,0 +1,17 @@
apiVersion: v1
kind: Service
metadata:
name: ljwx-chat
spec:
type: ClusterIP
selector:
app: ljwx-chat
ports:
- name: http
port: 80
targetPort: http
protocol: TCP
sessionAffinity: ClientIP
sessionAffinityConfig:
clientIP:
timeoutSeconds: 10800
@@ -0,0 +1,12 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: ljwx-chat-dev
resources:
- ../../base
images:
- name: ghcr.io/brunogao/ljwx-chat
newName: harbor.eu.lingjingwanxiang.cn/ljwx/ljwx-chat
newTag: openclaw-ws-20260307-0117
@@ -0,0 +1,34 @@
# ljwx-chat capability contract (dev)
## Namespace baseline
- namespace: `ljwx-chat-dev`
- namespace profile: `dev-default`
- registry profile: `ghcr-and-harbor`
- runtime secret profile: `app-runtime`
- image pull secrets: `ghcr-pull, regcred`
- runtime contract secrets: `runtime-app`
## Capabilities
- ghcr_pull (namespace): Bind GHCR pull secret ghcr-pull to the namespace baseline.
- harbor_pull (namespace): Bind Harbor pull secret regcred to the namespace baseline.
- runtime_app_secret (application): Standardize service-specific runtime secret as runtime-app.
## Bootstrap commands
```bash
bash scripts/ops/sync-registry-pull-secrets.sh --target-namespace ljwx-chat-dev --registry-profile ghcr-and-harbor
kubectl create secret generic runtime-app -n ljwx-chat-dev --from-literal=EXAMPLE_KEY='replace-me'
```
## Notes
- `runtime-contract/*.secret.example.yaml` are placeholders only and must not contain real credentials.
- Namespace baseline is generated into `cluster/namespace-<namespace>.yaml` and should remain the source of truth.
- For legacy workloads, migrate deployment manifests gradually to `runtime-infra`, `runtime-llm`, and `runtime-app` instead of app-specific secret names.
- The current `ljwx-chat` deployment still reads application secrets from the legacy `ljwx-chat-secrets` secret.
- When `lingjingwanxiang:32b` is routed through OpenClaw, `ljwx-chat-secrets` must include `OPENCLAW_GATEWAY_TOKEN`.
- `OPENAI_API_KEY` is still populated as a compatibility fallback for older builds.
- `OPENCLAW_GATEWAY_URL` is provided by the base ConfigMap and defaults to `https://openclaw.lingjingwanxiang.cn/v1`.
- `OPENCLAW_GATEWAY_ORIGIN` is provided by the base ConfigMap and defaults to `https://openclaw.lingjingwanxiang.cn`.
@@ -0,0 +1,22 @@
service: ljwx-chat
environment: dev
namespace: ljwx-chat-dev
cluster_bootstrap: true
ingress_profile: none
public_host: ''
generate_ingress: true
ingress_class_name: ''
cluster_issuer: ''
tls_secret_name: ''
namespace_profile: dev-default
registry_profile: ghcr-and-harbor
runtime_secret_profile: app-runtime
capabilities:
- ghcr_pull
- harbor_pull
- runtime_app_secret
image_pull_secrets:
- ghcr-pull
- regcred
contract_secret_names:
- runtime-app
@@ -0,0 +1,10 @@
apiVersion: v1
kind: Secret
metadata:
name: runtime-app
namespace: ljwx-chat-dev
annotations:
gitops.ljwx.io/generated-example: 'true'
type: Opaque
stringData:
EXAMPLE_KEY: <replace-with-app-secret>
+68
View File
@@ -0,0 +1,68 @@
apiVersion: v1
kind: Service
metadata:
name: api
spec:
selector:
app: ljwx-dify-api
ports:
- name: http
port: 5001
targetPort: 5001
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: ljwx-dify-api
spec:
replicas: 1
selector:
matchLabels:
app: ljwx-dify-api
template:
metadata:
labels:
app: ljwx-dify-api
spec:
imagePullSecrets:
- name: ghcr-pull
- name: regcred
containers:
- name: api
image: ghcr.io/brunogao/ljwx-dify-api:latest
ports:
- containerPort: 5001
name: http
envFrom:
- configMapRef:
name: ljwx-dify-config
- secretRef:
name: ljwx-dify-secrets
volumeMounts:
- name: storage
mountPath: /app/api/storage
resources:
requests:
memory: "1Gi"
cpu: "500m"
limits:
memory: "4Gi"
cpu: "2000m"
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 60
periodSeconds: 10
timeoutSeconds: 5
readinessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 30
periodSeconds: 5
timeoutSeconds: 5
volumes:
- name: storage
persistentVolumeClaim:
claimName: ljwx-dify-storage
+42
View File
@@ -0,0 +1,42 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: ljwx-dify-config
data:
MODE: "api"
LOG_LEVEL: "INFO"
DB_HOST: "postgres-lb.infra.svc.cluster.local"
DB_PORT: "5432"
DB_DATABASE: "ljwx_dify"
REDIS_HOST: "redis-lb.infra.svc.cluster.local"
REDIS_PORT: "6379"
REDIS_DB: "0"
VECTOR_STORE: "qdrant"
QDRANT_URL: "http://qdrant:6333"
STORAGE_TYPE: "s3"
S3_ENDPOINT: "http://minio.infra.svc.cluster.local:9000"
S3_USE_AWS_MANAGED_IAM: "false"
S3_USE_PATH_STYLE: "true"
S3_BUCKET_NAME: "ljwx-dify"
S3_REGION: "us-east-1"
NEXT_PUBLIC_COMPANY_NAME: "灵境万象"
NEXT_PUBLIC_SITE_NAME: "灵境万象知识库"
NEXT_PUBLIC_DEFAULT_LOCALE: "zh-Hans"
CONSOLE_API_URL: ""
APP_API_URL: ""
PLUGIN_DAEMON_URL: "http://plugin-daemon:5002"
PLUGIN_DAEMON_PORT: "5002"
DIFY_API_BASE_URL: "http://api:5001"
DIFY_CONSOLE_API_BASE: "http://api:5001/console/api"
DIFY_INIT_WAIT_SECONDS: "300"
DIFY_INIT_REQUEST_TIMEOUT_SECONDS: "15"
DIFY_DEFAULT_LLM_PROVIDER: "anthropic"
DIFY_DEFAULT_LLM_MODEL: "claude-sonnet-4-6"
+55
View File
@@ -0,0 +1,55 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ljwx-dify
annotations:
cert-manager.io/cluster-issuer: dnspod-letsencrypt
traefik.ingress.kubernetes.io/router.entrypoints: web,websecure
traefik.ingress.kubernetes.io/router.tls: "true"
nginx.ingress.kubernetes.io/proxy-body-size: "100m"
nginx.ingress.kubernetes.io/websocket-services: "api"
spec:
ingressClassName: traefik
tls:
- hosts:
- dify.lingjingwanxiang.cn
secretName: dify-lingjingwanxiang-cn-tls
rules:
- host: dify.lingjingwanxiang.cn
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: api
port:
number: 5001
- path: /console/api
pathType: Prefix
backend:
service:
name: api
port:
number: 5001
- path: /v1
pathType: Prefix
backend:
service:
name: api
port:
number: 5001
- path: /files
pathType: Prefix
backend:
service:
name: api
port:
number: 5001
- path: /
pathType: Prefix
backend:
service:
name: web
port:
number: 3000
@@ -0,0 +1,454 @@
apiVersion: batch/v1
kind: Job
metadata:
name: ljwx-dify-configure-default-llm
labels:
app.kubernetes.io/name: ljwx-dify
app.kubernetes.io/component: default-llm-config
annotations:
argocd.argoproj.io/hook: PostSync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
argocd.argoproj.io/sync-wave: "2"
spec:
ttlSecondsAfterFinished: 300
backoffLimit: 2
template:
metadata:
labels:
app.kubernetes.io/name: ljwx-dify
app.kubernetes.io/component: default-llm-config
spec:
restartPolicy: Never
imagePullSecrets:
- name: ghcr-pull
- name: regcred
containers:
- name: configure-default-llm
image: ghcr.io/brunogao/ljwx-dify-api:latest
command:
- /bin/sh
- -lc
- |
set -eu
/app/api/.venv/bin/python - <<'PY'
import base64
import json
import os
import sys
import time
import urllib.error
import urllib.parse
import urllib.request
from http.cookiejar import CookieJar
from typing import Any
API_BASE = os.getenv("DIFY_API_BASE_URL", "http://api:5001").rstrip("/")
CONSOLE_API_BASE = os.getenv("DIFY_CONSOLE_API_BASE", f"{API_BASE}/console/api").rstrip("/")
WAIT_SECONDS = int(os.getenv("DIFY_INIT_WAIT_SECONDS", "300"))
REQUEST_TIMEOUT_SECONDS = int(os.getenv("DIFY_INIT_REQUEST_TIMEOUT_SECONDS", "15"))
ADMIN_EMAIL = os.getenv("DIFY_ADMIN_EMAIL", "").strip()
ADMIN_NAME = os.getenv("DIFY_ADMIN_NAME", "Admin").strip() or "Admin"
ADMIN_PASSWORD = os.getenv("DIFY_ADMIN_PASSWORD", "").strip()
INIT_PASSWORD = os.getenv("DIFY_INIT_PASSWORD", "").strip()
PROVIDER_HINT = os.getenv("DIFY_DEFAULT_LLM_PROVIDER", "anthropic").strip() or "anthropic"
DEFAULT_MODEL = os.getenv("DIFY_DEFAULT_LLM_MODEL", "claude-sonnet-4-6").strip()
ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY", "").strip()
ANTHROPIC_BASE_URL = os.getenv("ANTHROPIC_BASE_URL", "https://api.anthropic.com").strip()
def fail(message: str) -> None:
print(f"[ERROR] {message}", file=sys.stderr)
sys.exit(1)
if not ADMIN_EMAIL:
fail("缺少 DIFY_ADMIN_EMAIL,无法登录 Dify 控制台。")
if not ADMIN_PASSWORD:
fail("缺少 DIFY_ADMIN_PASSWORD,无法登录 Dify 控制台。")
if not DEFAULT_MODEL:
fail("缺少 DIFY_DEFAULT_LLM_MODEL,无法设置默认模型。")
if not ANTHROPIC_API_KEY:
fail("缺少 ANTHROPIC_API_KEY,无法配置 Anthropic Provider。")
cookie_jar = CookieJar()
opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cookie_jar))
csrf_token: str | None = None
def _decode_response(raw: bytes) -> tuple[Any | None, str]:
text = raw.decode("utf-8", errors="replace")
if not text:
return None, ""
try:
return json.loads(text), text
except json.JSONDecodeError:
return None, text
def _error_text(body: Any | None, text: str) -> str:
if isinstance(body, dict):
code = body.get("code") or body.get("error_code")
message = body.get("message") or body.get("description") or body.get("error")
compact = json.dumps(body, ensure_ascii=False)
return f"code={code}, message={message}, body={compact}"
return text or "<empty>"
def request_json(
method: str,
path: str,
payload: dict[str, Any] | None = None,
expected_status: set[int] | None = None,
) -> tuple[int, Any | None, str]:
url = path if path.startswith("http") else f"{CONSOLE_API_BASE}{path}"
headers = {
"Accept": "application/json",
}
body_bytes = None
if payload is not None:
headers["Content-Type"] = "application/json"
body_bytes = json.dumps(payload).encode("utf-8")
if csrf_token and method.upper() not in {"GET", "HEAD", "OPTIONS"}:
headers["X-CSRF-Token"] = csrf_token
req = urllib.request.Request(url=url, data=body_bytes, headers=headers, method=method.upper())
try:
with opener.open(req, timeout=REQUEST_TIMEOUT_SECONDS) as resp:
status_code = resp.getcode()
body, text = _decode_response(resp.read())
except urllib.error.HTTPError as exc:
status_code = exc.code
body, text = _decode_response(exc.read())
if expected_status and status_code in expected_status:
return status_code, body, text
fail(
f"请求失败 {method.upper()} {url},status={status_code},{_error_text(body, text)}"
)
except Exception as exc: # noqa: BLE001
fail(f"请求异常 {method.upper()} {url},error={exc}")
if expected_status and status_code not in expected_status:
fail(
f"请求状态异常 {method.upper()} {url},status={status_code},{_error_text(body, text)}"
)
return status_code, body, text
def wait_for_api_ready() -> None:
health_url = f"{API_BASE}/health"
deadline = time.time() + WAIT_SECONDS
last_error = ""
while time.time() < deadline:
try:
with urllib.request.urlopen(health_url, timeout=REQUEST_TIMEOUT_SECONDS) as resp:
if resp.getcode() == 200:
print("[INFO] Dify API 健康检查通过。")
return
last_error = f"status={resp.getcode()}"
except Exception as exc: # noqa: BLE001
last_error = str(exc)
print("[INFO] 等待 Dify API 就绪...")
time.sleep(5)
fail(f"Dify API 未在 {WAIT_SECONDS}s 内就绪,最后错误: {last_error}")
def login_console() -> None:
global csrf_token
nonlocal_csrf_token: str | None = None
encoded_password = base64.b64encode(ADMIN_PASSWORD.encode("utf-8")).decode("utf-8")
request_json(
"POST",
"/login",
payload={
"email": ADMIN_EMAIL,
"password": encoded_password,
"remember_me": False,
},
expected_status={200},
)
for cookie in cookie_jar:
if cookie.name.endswith("csrf_token"):
nonlocal_csrf_token = cookie.value
break
if not nonlocal_csrf_token:
fail("登录成功但未获取 CSRF Token,无法继续调用写接口。")
csrf_token = nonlocal_csrf_token
print("[INFO] Dify 控制台登录成功。")
def ensure_setup() -> None:
status, body, text = request_json("GET", "/setup", expected_status={200})
if status != 200 or not isinstance(body, dict):
fail(f"读取 setup 状态失败,body={text}")
step = str(body.get("step", ""))
if step == "finished":
print("[INFO] Dify 已完成初始化。")
return
print("[INFO] Dify 尚未初始化,开始执行 setup。")
if INIT_PASSWORD:
init_status, init_body, init_text = request_json(
"POST",
"/init",
payload={"password": INIT_PASSWORD},
expected_status={201, 401, 403},
)
if init_status == 201:
print("[INFO] init 校验已完成。")
else:
code = init_body.get("code") if isinstance(init_body, dict) else None
if code == "already_setup":
print("[INFO] init 已完成,继续执行。")
else:
fail(
f"init 校验失败,status={init_status},{_error_text(init_body, init_text)}"
)
setup_status, setup_body, setup_text = request_json(
"POST",
"/setup",
payload={
"email": ADMIN_EMAIL,
"name": ADMIN_NAME,
"password": ADMIN_PASSWORD,
"language": "zh-Hans",
},
expected_status={201, 401, 403},
)
if setup_status == 201:
print("[INFO] Dify 初始化完成。")
return
setup_code = setup_body.get("code") if isinstance(setup_body, dict) else None
if setup_code == "already_setup":
print("[INFO] setup 已完成,继续执行后续配置。")
return
fail(
f"Dify setup 失败,status={setup_status},{_error_text(setup_body, setup_text)}"
)
def list_llm_providers() -> list[dict[str, Any]]:
_, body, text = request_json(
"GET",
"/workspaces/current/model-providers?model_type=llm",
expected_status={200},
)
if not isinstance(body, dict) or not isinstance(body.get("data"), list):
fail(f"读取 LLM provider 列表失败,body={text}")
return body["data"]
def pick_provider(
providers: list[dict[str, Any]],
provider_hint: str,
) -> dict[str, Any]:
lower_hint = provider_hint.lower()
for provider in providers:
name = str(provider.get("provider", ""))
if name.lower() == lower_hint:
return provider
for provider in providers:
name = str(provider.get("provider", ""))
if lower_hint in name.lower():
return provider
for provider in providers:
name = str(provider.get("provider", ""))
if name.split("/")[-1].lower() == lower_hint:
return provider
provider_names = [str(item.get("provider", "")) for item in providers]
fail(f"未找到 provider={provider_hint},当前可用: {provider_names}")
def build_provider_credentials(provider_item: dict[str, Any]) -> dict[str, str]:
schema = provider_item.get("provider_credential_schema") or {}
forms = schema.get("credential_form_schemas") if isinstance(schema, dict) else None
known_value_map = {
"anthropic_api_key": ANTHROPIC_API_KEY,
"api_key": ANTHROPIC_API_KEY,
"anthropic_api_url": ANTHROPIC_BASE_URL,
"api_base": ANTHROPIC_BASE_URL,
"base_url": ANTHROPIC_BASE_URL,
"endpoint": ANTHROPIC_BASE_URL,
}
credentials: dict[str, str] = {}
unresolved_required: list[str] = []
if isinstance(forms, list):
for form in forms:
if not isinstance(form, dict):
continue
variable = str(form.get("variable", "")).strip()
if not variable:
continue
lower_var = variable.lower()
required = bool(form.get("required"))
value = known_value_map.get(lower_var, "")
if not value:
if "key" in lower_var or "token" in lower_var:
value = ANTHROPIC_API_KEY
elif "url" in lower_var or "base" in lower_var or "endpoint" in lower_var:
value = ANTHROPIC_BASE_URL
if value:
credentials[variable] = value
elif required:
unresolved_required.append(variable)
if not credentials:
credentials["anthropic_api_key"] = ANTHROPIC_API_KEY
if ANTHROPIC_BASE_URL:
credentials["anthropic_api_url"] = ANTHROPIC_BASE_URL
if unresolved_required:
fail(
f"无法自动映射 provider 必填字段: {unresolved_required},请补充 Job 变量映射。"
)
return credentials
def quote_provider_path(provider_name: str) -> str:
return urllib.parse.quote(provider_name, safe="/")
def ensure_provider_credentials(provider_name: str, credentials: dict[str, str]) -> None:
providers = list_llm_providers()
provider_item = pick_provider(providers, provider_name)
provider_name = str(provider_item.get("provider", provider_name))
quoted_provider = quote_provider_path(provider_name)
custom_cfg = provider_item.get("custom_configuration") or {}
current_credential_id = custom_cfg.get("current_credential_id")
available_credentials = custom_cfg.get("available_credentials") or []
if not current_credential_id and available_credentials:
first_credential = available_credentials[0]
candidate_id = first_credential.get("credential_id")
if candidate_id:
request_json(
"POST",
f"/workspaces/current/model-providers/{quoted_provider}/credentials/switch",
payload={"credential_id": candidate_id},
expected_status={200},
)
current_credential_id = candidate_id
print(f"[INFO] 已切换 provider 凭据到 {candidate_id}。")
if current_credential_id:
request_json(
"PUT",
f"/workspaces/current/model-providers/{quoted_provider}/credentials",
payload={
"credential_id": current_credential_id,
"credentials": credentials,
},
expected_status={200},
)
print("[INFO] 已更新现有 Anthropic provider 凭据。")
else:
request_json(
"POST",
f"/workspaces/current/model-providers/{quoted_provider}/credentials",
payload={"credentials": credentials},
expected_status={201},
)
print("[INFO] 已创建 Anthropic provider 凭据。")
providers_after = list_llm_providers()
provider_item_after = pick_provider(providers_after, provider_name)
custom_cfg_after = provider_item_after.get("custom_configuration") or {}
current_after = custom_cfg_after.get("current_credential_id")
available_after = custom_cfg_after.get("available_credentials") or []
if not current_after and available_after:
candidate_id = available_after[0].get("credential_id")
if candidate_id:
request_json(
"POST",
f"/workspaces/current/model-providers/{quoted_provider}/credentials/switch",
payload={"credential_id": candidate_id},
expected_status={200},
)
print(f"[INFO] 已激活新创建的 provider 凭据 {candidate_id}。")
request_json(
"POST",
f"/workspaces/current/model-providers/{quoted_provider}/preferred-provider-type",
payload={"preferred_provider_type": "custom"},
expected_status={200},
)
print("[INFO] 已设置 provider 使用 custom 配置。")
def set_default_model(provider_name: str, model_name: str) -> None:
request_json(
"POST",
"/workspaces/current/default-model",
payload={
"model_settings": [
{
"model_type": "llm",
"provider": provider_name,
"model": model_name,
}
]
},
expected_status={200},
)
_, body, text = request_json(
"GET",
"/workspaces/current/default-model?model_type=llm",
expected_status={200},
)
if not isinstance(body, dict) or not isinstance(body.get("data"), dict):
fail(f"默认模型校验失败,body={text}")
current_model = body["data"].get("model")
current_provider = body["data"].get("provider", {}).get("provider")
if current_model != model_name or current_provider != provider_name:
fail(
f"默认模型未生效,期望 provider={provider_name}, model={model_name};"
f"实际 provider={current_provider}, model={current_model}"
)
print(
f"[INFO] 默认 LLM 已设置为 provider={provider_name}, model={model_name}。"
)
wait_for_api_ready()
ensure_setup()
login_console()
providers = list_llm_providers()
provider_item = pick_provider(providers, PROVIDER_HINT)
selected_provider_name = str(provider_item.get("provider", PROVIDER_HINT))
provider_credentials = build_provider_credentials(provider_item)
ensure_provider_credentials(selected_provider_name, provider_credentials)
set_default_model(selected_provider_name, DEFAULT_MODEL)
print("[INFO] Dify 默认 LLM 自动化配置完成。")
PY
envFrom:
- configMapRef:
name: ljwx-dify-config
- secretRef:
name: ljwx-dify-secrets
resources:
requests:
cpu: "100m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "1Gi"
+47
View File
@@ -0,0 +1,47 @@
apiVersion: batch/v1
kind: Job
metadata:
name: ljwx-dify-upgrade-db
labels:
app.kubernetes.io/name: ljwx-dify
app.kubernetes.io/component: db-migration
annotations:
argocd.argoproj.io/hook: PreSync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
argocd.argoproj.io/sync-wave: "-1"
spec:
ttlSecondsAfterFinished: 300
backoffLimit: 3
template:
metadata:
labels:
app.kubernetes.io/name: ljwx-dify
app.kubernetes.io/component: db-migration
spec:
restartPolicy: Never
imagePullSecrets:
- name: ghcr-pull
- name: regcred
containers:
- name: upgrade-db
image: ghcr.io/brunogao/ljwx-dify-api:latest
command:
- /bin/sh
- -lc
- |
set -e
echo "Starting Dify database migration..."
/app/api/.venv/bin/flask upgrade-db
echo "Dify database migration completed."
envFrom:
- configMapRef:
name: ljwx-dify-config
- secretRef:
name: ljwx-dify-secrets
resources:
requests:
cpu: "100m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "1Gi"
+18
View File
@@ -0,0 +1,18 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- configmap.yaml
- job-upgrade-db.yaml
- job-configure-default-llm.yaml
- pvc.yaml
- api.yaml
- worker.yaml
- web.yaml
- plugin-daemon.yaml
- qdrant.yaml
- ingress.yaml
commonLabels:
app.kubernetes.io/name: ljwx-dify
app.kubernetes.io/part-of: ljwx-chat-suite
+112
View File
@@ -0,0 +1,112 @@
apiVersion: v1
kind: Service
metadata:
name: plugin-daemon
spec:
selector:
app: ljwx-dify-plugin-daemon
ports:
- name: http
port: 5002
targetPort: 5002
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: ljwx-dify-plugin-daemon
spec:
replicas: 1
selector:
matchLabels:
app: ljwx-dify-plugin-daemon
template:
metadata:
labels:
app: ljwx-dify-plugin-daemon
spec:
imagePullSecrets:
- name: regcred
containers:
- name: plugin-daemon
image: langgenius/dify-plugin-daemon:0.5.1-local
ports:
- containerPort: 5002
name: http
env:
- name: PLUGIN_DAEMON_PORT
value: "5002"
- name: REDIS_HOST
value: "redis-lb.infra.svc.cluster.local"
- name: REDIS_PORT
value: "6379"
- name: REDIS_DB
value: "0"
- name: REDIS_USERNAME
value: ""
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: ljwx-dify-secrets
key: REDIS_PASSWORD
- name: DB_HOST
value: "postgres-lb.infra.svc.cluster.local"
- name: DB_PORT
value: "5432"
- name: DB_DATABASE
value: "ljwx_dify"
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: ljwx-dify-secrets
key: DB_USERNAME
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: ljwx-dify-secrets
key: DB_PASSWORD
- name: DIFY_INNER_API_URL
value: "http://api:5001"
- name: SERVER_KEY
valueFrom:
secretKeyRef:
name: ljwx-dify-secrets
key: PLUGIN_DAEMON_KEY
- name: DIFY_INNER_API_KEY
valueFrom:
secretKeyRef:
name: ljwx-dify-secrets
key: DIFY_INNER_API_KEY
- name: PLUGIN_REMOTE_INSTALLING_HOST
value: "https://dify.lingjingwanxiang.cn"
- name: PLUGIN_REMOTE_INSTALLING_PORT
value: "443"
- name: PLUGIN_WORKING_PATH
value: "/app/plugins"
envFrom:
- configMapRef:
name: ljwx-dify-config
- secretRef:
name: ljwx-dify-secrets
volumeMounts:
- name: plugins
mountPath: /app/plugins
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "1Gi"
cpu: "500m"
livenessProbe:
tcpSocket:
port: http
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
tcpSocket:
port: http
initialDelaySeconds: 10
periodSeconds: 5
volumes:
- name: plugins
emptyDir: {}
+10
View File
@@ -0,0 +1,10 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: ljwx-dify-storage
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
+75
View File
@@ -0,0 +1,75 @@
apiVersion: v1
kind: Service
metadata:
name: qdrant
spec:
selector:
app: qdrant
ports:
- name: http
port: 6333
targetPort: 6333
- name: grpc
port: 6334
targetPort: 6334
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: qdrant
spec:
serviceName: qdrant
replicas: 1
selector:
matchLabels:
app: qdrant
template:
metadata:
labels:
app: qdrant
spec:
containers:
- name: qdrant
image: qdrant/qdrant:v1.12.6
ports:
- containerPort: 6333
name: http
- containerPort: 6334
name: grpc
env:
- name: QDRANT__SERVICE__API_KEY
valueFrom:
secretKeyRef:
name: ljwx-dify-secrets
key: QDRANT_API_KEY
volumeMounts:
- name: qdrant-storage
mountPath: /qdrant/storage
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "2Gi"
cpu: "1000m"
livenessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 5
periodSeconds: 5
volumeClaimTemplates:
- metadata:
name: qdrant-storage
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
+57
View File
@@ -0,0 +1,57 @@
apiVersion: v1
kind: Service
metadata:
name: web
spec:
selector:
app: ljwx-dify-web
ports:
- name: http
port: 3000
targetPort: 3000
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: ljwx-dify-web
spec:
replicas: 1
selector:
matchLabels:
app: ljwx-dify-web
template:
metadata:
labels:
app: ljwx-dify-web
spec:
imagePullSecrets:
- name: ghcr-pull
- name: regcred
containers:
- name: web
image: ghcr.io/brunogao/ljwx-dify-web:latest
ports:
- containerPort: 3000
name: http
envFrom:
- configMapRef:
name: ljwx-dify-config
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "2Gi"
cpu: "1000m"
livenessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 10
periodSeconds: 5
+42
View File
@@ -0,0 +1,42 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: ljwx-dify-worker
spec:
replicas: 1
selector:
matchLabels:
app: ljwx-dify-worker
template:
metadata:
labels:
app: ljwx-dify-worker
spec:
imagePullSecrets:
- name: ghcr-pull
- name: regcred
containers:
- name: worker
image: ghcr.io/brunogao/ljwx-dify-api:latest
env:
- name: MODE
value: "worker"
envFrom:
- configMapRef:
name: ljwx-dify-config
- secretRef:
name: ljwx-dify-secrets
volumeMounts:
- name: storage
mountPath: /app/api/storage
resources:
requests:
memory: "1Gi"
cpu: "500m"
limits:
memory: "4Gi"
cpu: "2000m"
volumes:
- name: storage
persistentVolumeClaim:
claimName: ljwx-dify-storage
@@ -0,0 +1,17 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: ljwx-dify-dev
resources:
- ../../base
images:
- name: ghcr.io/brunogao/ljwx-dify-api
newName: ghcr.io/brunogao/ljwx-dify-api
newTag: latest
- name: ghcr.io/brunogao/ljwx-dify-web
newName: ghcr.io/brunogao/ljwx-dify-web
newTag: latest
- name: qdrant/qdrant
newTag: v1.12.6
@@ -0,0 +1,39 @@
# ljwx-dify capability contract (dev)
## Namespace baseline
- namespace: `ljwx-dify-dev`
- namespace profile: `dev-default`
- registry profile: `ghcr-and-harbor`
- runtime secret profile: `app-runtime`
- image pull secrets: `ghcr-pull, regcred`
- runtime contract secrets: `runtime-app`
## Capabilities
- ghcr_pull (namespace): Bind GHCR pull secret ghcr-pull to the namespace baseline.
- harbor_pull (namespace): Bind Harbor pull secret regcred to the namespace baseline.
- runtime_app_secret (application): Standardize service-specific runtime secret as runtime-app.
- db_init_job (application): Declare that the workload uses a database initialization or migration hook job.
## Public endpoint
- ingress profile: `traefik-letsencrypt-public`
- host: `https://dify.lingjingwanxiang.cn`
- ingress class: `traefik`
- cluster issuer: `dnspod-letsencrypt`
- tls secret: `dify-lingjingwanxiang-cn-tls`
- ingress artifact generation: `disabled (managed by existing manifests)`
## Bootstrap commands
```bash
bash scripts/ops/sync-registry-pull-secrets.sh --target-namespace ljwx-dify-dev --registry-profile ghcr-and-harbor
kubectl create secret generic runtime-app -n ljwx-dify-dev --from-literal=EXAMPLE_KEY='replace-me'
```
## Notes
- `runtime-contract/*.secret.example.yaml` are placeholders only and must not contain real credentials.
- Namespace baseline is generated into `cluster/namespace-<namespace>.yaml` and should remain the source of truth.
- For legacy workloads, migrate deployment manifests gradually to `runtime-infra`, `runtime-llm`, and `runtime-app` instead of app-specific secret names.
@@ -0,0 +1,23 @@
service: ljwx-dify
environment: dev
namespace: ljwx-dify-dev
cluster_bootstrap: true
ingress_profile: traefik-letsencrypt-public
public_host: dify.lingjingwanxiang.cn
generate_ingress: false
ingress_class_name: traefik
cluster_issuer: dnspod-letsencrypt
tls_secret_name: dify-lingjingwanxiang-cn-tls
namespace_profile: dev-default
registry_profile: ghcr-and-harbor
runtime_secret_profile: app-runtime
capabilities:
- ghcr_pull
- harbor_pull
- runtime_app_secret
- db_init_job
image_pull_secrets:
- ghcr-pull
- regcred
contract_secret_names:
- runtime-app
@@ -0,0 +1,10 @@
apiVersion: v1
kind: Secret
metadata:
name: runtime-app
namespace: ljwx-dify-dev
annotations:
gitops.ljwx.io/generated-example: 'true'
type: Opaque
stringData:
EXAMPLE_KEY: <replace-with-app-secret>
+10
View File
@@ -32,18 +32,28 @@ spec:
- name: http
containerPort: 8080
protocol: TCP
startupProbe:
httpGet:
path: /actuator/health/liveness
port: http
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 18
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: http
initialDelaySeconds: 30
periodSeconds: 15
timeoutSeconds: 2
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: http
initialDelaySeconds: 20
periodSeconds: 10
timeoutSeconds: 2
volumes:
- name: app-logs
emptyDir: {}
+14
View File
@@ -23,6 +23,13 @@ spec:
- host: platform-admin.lingjingwanxiang.cn
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: ljwx-platform
port:
number: 80
- path: /
pathType: Prefix
backend:
@@ -33,6 +40,13 @@ spec:
- host: platform-screen.lingjingwanxiang.cn
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: ljwx-platform
port:
number: 80
- path: /
pathType: Prefix
backend:
@@ -0,0 +1,29 @@
# ljwx-platform capability contract (prod)
## Namespace baseline
- namespace: `ljwx-platform`
- namespace profile: `prod-default`
- registry profile: `harbor-only`
- runtime secret profile: `app-runtime`
- image pull secrets: `regcred`
- runtime contract secrets: `runtime-app`
## Capabilities
- ghcr_pull (namespace): Bind GHCR pull secret ghcr-pull to the namespace baseline.
- harbor_pull (namespace): Bind Harbor pull secret regcred to the namespace baseline.
- runtime_app_secret (application): Standardize service-specific runtime secret as runtime-app.
## Bootstrap commands
```bash
bash scripts/ops/sync-registry-pull-secrets.sh --target-namespace ljwx-platform --registry-profile harbor-only
kubectl create secret generic runtime-app -n ljwx-platform --from-literal=EXAMPLE_KEY='replace-me'
```
## Notes
- `runtime-contract/*.secret.example.yaml` are placeholders only and must not contain real credentials.
- Namespace baseline is generated into `cluster-prod/namespace-<namespace>.yaml` and should remain the source of truth.
- For legacy workloads, migrate deployment manifests gradually to `runtime-infra`, `runtime-llm`, and `runtime-app` instead of app-specific secret names.
@@ -0,0 +1,21 @@
service: ljwx-platform
environment: prod
namespace: ljwx-platform
cluster_bootstrap: true
ingress_profile: none
public_host: ''
generate_ingress: true
ingress_class_name: ''
cluster_issuer: ''
tls_secret_name: ''
namespace_profile: prod-default
registry_profile: harbor-only
runtime_secret_profile: app-runtime
capabilities:
- ghcr_pull
- harbor_pull
- runtime_app_secret
image_pull_secrets:
- regcred
contract_secret_names:
- runtime-app
@@ -0,0 +1,10 @@
apiVersion: v1
kind: Secret
metadata:
name: runtime-app
namespace: ljwx-platform
annotations:
gitops.ljwx.io/generated-example: 'true'
type: Opaque
stringData:
EXAMPLE_KEY: <replace-with-app-secret>
+45
View File
@@ -0,0 +1,45 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: ljwx-shiti
labels:
app: ljwx-shiti
annotations: &id001
gitops.ljwx.io/namespace-profile: dev-default
gitops.ljwx.io/registry-profile: ghcr-and-harbor
gitops.ljwx.io/runtime-secret-profile: app-runtime
gitops.ljwx.io/capabilities: ghcr_pull,harbor_pull,runtime_app_secret
spec:
replicas: 1
selector:
matchLabels:
app: ljwx-shiti
template:
metadata:
labels:
app: ljwx-shiti
annotations: *id001
spec:
containers:
- name: ljwx-shiti
image: harbor.eu.lingjingwanxiang.cn/ljwx/ljwx-shiti:latest
ports:
- name: http
containerPort: 80
protocol: TCP
livenessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 5
periodSeconds: 5
envFrom:
- secretRef:
name: runtime-app
optional: true
+5
View File
@@ -0,0 +1,5 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
+15
View File
@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: ljwx-shiti
labels:
app: ljwx-shiti
spec:
type: ClusterIP
selector:
app: ljwx-shiti
ports:
- name: http
port: 80
targetPort: http
protocol: TCP
+25
View File
@@ -0,0 +1,25 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ljwx-shiti-ingress
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: web,websecure
traefik.ingress.kubernetes.io/router.tls: 'true'
cert-manager.io/cluster-issuer: dnspod-letsencrypt
spec:
ingressClassName: traefik
rules:
- host: shiti.lingjingwanxiang.cn
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: ljwx-shiti
port:
number: 80
tls:
- hosts:
- shiti.lingjingwanxiang.cn
secretName: shiti-lingjingwanxiang-cn-tls
@@ -0,0 +1,10 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: ljwx-shiti-dev
resources:
- ../../base
- ingress.yaml
images:
- name: ghcr.io/brunogao/ljwx-shiti
newName: harbor.eu.lingjingwanxiang.cn/ljwx/ljwx-shiti
newTag: latest
@@ -0,0 +1,38 @@
# ljwx-shiti capability contract (dev)
## Namespace baseline
- namespace: `ljwx-shiti-dev`
- namespace profile: `dev-default`
- registry profile: `ghcr-and-harbor`
- runtime secret profile: `app-runtime`
- image pull secrets: `ghcr-pull, regcred`
- runtime contract secrets: `runtime-app`
## Capabilities
- ghcr_pull (namespace): Bind GHCR pull secret ghcr-pull to the namespace baseline.
- harbor_pull (namespace): Bind Harbor pull secret regcred to the namespace baseline.
- runtime_app_secret (application): Standardize service-specific runtime secret as runtime-app.
## Public endpoint
- ingress profile: `traefik-letsencrypt-public`
- host: `https://shiti.lingjingwanxiang.cn`
- ingress class: `traefik`
- cluster issuer: `dnspod-letsencrypt`
- tls secret: `shiti-lingjingwanxiang-cn-tls`
- ingress artifact generation: `enabled`
## Bootstrap commands
```bash
bash scripts/ops/sync-registry-pull-secrets.sh --target-namespace ljwx-shiti-dev --registry-profile ghcr-and-harbor
kubectl create secret generic runtime-app -n ljwx-shiti-dev --from-literal=EXAMPLE_KEY='replace-me'
```
## Notes
- `runtime-contract/*.secret.example.yaml` are placeholders only and must not contain real credentials.
- Namespace baseline is generated into `cluster/namespace-<namespace>.yaml` and should remain the source of truth.
- For legacy workloads, migrate deployment manifests gradually to `runtime-infra`, `runtime-llm`, and `runtime-app` instead of app-specific secret names.
@@ -0,0 +1,22 @@
service: ljwx-shiti
environment: dev
namespace: ljwx-shiti-dev
cluster_bootstrap: true
ingress_profile: traefik-letsencrypt-public
public_host: shiti.lingjingwanxiang.cn
generate_ingress: true
ingress_class_name: traefik
cluster_issuer: dnspod-letsencrypt
tls_secret_name: shiti-lingjingwanxiang-cn-tls
namespace_profile: dev-default
registry_profile: ghcr-and-harbor
runtime_secret_profile: app-runtime
capabilities:
- ghcr_pull
- harbor_pull
- runtime_app_secret
image_pull_secrets:
- ghcr-pull
- regcred
contract_secret_names:
- runtime-app
@@ -0,0 +1,10 @@
apiVersion: v1
kind: Secret
metadata:
name: runtime-app
namespace: ljwx-shiti-dev
annotations:
gitops.ljwx.io/generated-example: 'true'
type: Opaque
stringData:
EXAMPLE_KEY: <replace-with-app-secret>
+4 -1
View File
@@ -16,9 +16,12 @@ spec:
app: ljwx-website
component: web
spec:
imagePullSecrets:
- name: ghcr-pull
- name: regcred
containers:
- name: ljwx-website
image: harbor.omniverseai.net/ljwx/ljwx-website:latest
image: ghcr.io/brunogao/ljwx-website:latest
ports:
- name: http
containerPort: 80
@@ -0,0 +1,25 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ljwx-website-ingress
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: web,websecure
traefik.ingress.kubernetes.io/router.tls: 'true'
cert-manager.io/cluster-issuer: dnspod-letsencrypt
spec:
ingressClassName: traefik
rules:
- host: website.lingjingwanxiang.cn
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: ljwx-website
port:
number: 80
tls:
- hosts:
- website.lingjingwanxiang.cn
secretName: website-lingjingwanxiang-cn-tls
@@ -3,13 +3,14 @@ kind: Kustomization
namespace: ljwx-website-dev
resources:
- ../../base
- ingress.yaml
patches:
- path: service-nodeport.yaml
- path: deployment-probe.yaml
images:
- name: harbor.omniverseai.net/ljwx/ljwx-website
newName: harbor.omniverseai.net/app/ljwx-website
digest: sha256:b6bee6292395deda36062a25cdbae5a7eb81477823736bd3dacbcdc5e6be1641
- name: ghcr.io/brunogao/ljwx-website
newName: ghcr.io/brunogao/ljwx-website
newTag: latest
replicas:
- name: ljwx-website
count: 1
@@ -0,0 +1,37 @@
# ljwx-website capability contract (dev)
## Namespace baseline
- namespace: `ljwx-website-dev`
- namespace profile: `dev-default`
- registry profile: `ghcr-and-harbor`
- runtime secret profile: `none`
- image pull secrets: `ghcr-pull, regcred`
- runtime contract secrets: `none`
## Capabilities
- ghcr_pull (namespace): Bind GHCR pull secret ghcr-pull to the namespace baseline.
- harbor_pull (namespace): Bind Harbor pull secret regcred to the namespace baseline.
- runtime_app_secret (application): Standardize service-specific runtime secret as runtime-app.
## Public endpoint
- ingress profile: `traefik-letsencrypt-public`
- host: `https://website.lingjingwanxiang.cn`
- ingress class: `traefik`
- cluster issuer: `dnspod-letsencrypt`
- tls secret: `website-lingjingwanxiang-cn-tls`
- ingress artifact generation: `enabled`
## Bootstrap commands
```bash
bash scripts/ops/sync-registry-pull-secrets.sh --target-namespace ljwx-website-dev --registry-profile ghcr-and-harbor
```
## Notes
- `runtime-contract/*.secret.example.yaml` are placeholders only and must not contain real credentials.
- `cluster_bootstrap` is disabled for this entry; keep the existing cluster namespace/application manifests as the current source of truth.
- For legacy workloads, migrate deployment manifests gradually to `runtime-infra`, `runtime-llm`, and `runtime-app` instead of app-specific secret names.
@@ -0,0 +1,21 @@
service: ljwx-website
environment: dev
namespace: ljwx-website-dev
cluster_bootstrap: false
ingress_profile: traefik-letsencrypt-public
public_host: website.lingjingwanxiang.cn
generate_ingress: true
ingress_class_name: traefik
cluster_issuer: dnspod-letsencrypt
tls_secret_name: website-lingjingwanxiang-cn-tls
namespace_profile: dev-default
registry_profile: ghcr-and-harbor
runtime_secret_profile: none
capabilities:
- ghcr_pull
- harbor_pull
- runtime_app_secret
image_pull_secrets:
- ghcr-pull
- regcred
contract_secret_names: []
@@ -0,0 +1,12 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: ljwx-website-prod
resources:
- ../../base
images:
- name: ghcr.io/brunogao/ljwx-website
newName: ghcr.io/brunogao/ljwx-website
newTag: latest
replicas:
- name: ljwx-website
count: 2
@@ -0,0 +1,28 @@
# ljwx-website capability contract (prod)
## Namespace baseline
- namespace: `ljwx-website-prod`
- namespace profile: `prod-default`
- registry profile: `ghcr-and-harbor`
- runtime secret profile: `none`
- image pull secrets: `ghcr-pull, regcred`
- runtime contract secrets: `none`
## Capabilities
- ghcr_pull (namespace): Bind GHCR pull secret ghcr-pull to the namespace baseline.
- harbor_pull (namespace): Bind Harbor pull secret regcred to the namespace baseline.
- runtime_app_secret (application): Standardize service-specific runtime secret as runtime-app.
## Bootstrap commands
```bash
bash scripts/ops/sync-registry-pull-secrets.sh --target-namespace ljwx-website-prod --registry-profile ghcr-and-harbor
```
## Notes
- `runtime-contract/*.secret.example.yaml` are placeholders only and must not contain real credentials.
- `cluster_bootstrap` is disabled for this entry; keep the existing cluster namespace/application manifests as the current source of truth.
- For legacy workloads, migrate deployment manifests gradually to `runtime-infra`, `runtime-llm`, and `runtime-app` instead of app-specific secret names.
@@ -0,0 +1,21 @@
service: ljwx-website
environment: prod
namespace: ljwx-website-prod
cluster_bootstrap: false
ingress_profile: none
public_host: ''
generate_ingress: true
ingress_class_name: ''
cluster_issuer: ''
tls_secret_name: ''
namespace_profile: prod-default
registry_profile: ghcr-and-harbor
runtime_secret_profile: none
capabilities:
- ghcr_pull
- harbor_pull
- runtime_app_secret
image_pull_secrets:
- ghcr-pull
- regcred
contract_secret_names: []
@@ -1,6 +1,2 @@
resources:
- ../../base
images:
- name: nginx
newName: harbor.omniverseai.net/dockerhub/library/nginx
newTag: "alpine"
+16 -1
View File
@@ -7,12 +7,27 @@ data:
ENVIRONMENT: "development"
RATE_LIMIT_RPM: "30"
MAX_TOKENS_PER_RUN: "50000"
DB_POOL_MIN_SIZE: "2"
DB_POOL_MAX_SIZE: "10"
DB_POOL_TIMEOUT_SECONDS: "5"
DB_COMMAND_TIMEOUT_SECONDS: "5"
KLINE_QUERY_TIMEOUT_SECONDS: "5"
KLINE_DEFAULT_LIMIT: "60"
KLINE_MAX_LIMIT: "500"
KLINE_BULK_PER_SYMBOL_LIMIT: "60"
KLINE_BULK_MAX_SYMBOLS: "500"
KLINE_BULK_MAX_ROWS: "20000"
CANDIDATE_POOL_SIZE: "300"
DISPLAY_TOP_N: "20"
MIN_AMOUNT_AVG: "10000000"
LOOKBACK_DAYS_CALENDAR: "150"
STRATEGY_WEIGHT_MOMENTUM_RULE_V1: "0.6"
STRATEGY_WEIGHT_TECHNICAL_PATTERN_V1: "0.4"
STRATEGY_WEIGHT_QLIB_RECO_READER_V1: "0.2"
WRITE_RECO: "true"
CODE_VERSION: "gitops"
LLM_PROVIDER: "claude"
ANTHROPIC_MODEL: "claude-sonnet-4-6-20260217"
ANTHROPIC_MODEL: "claude-sonnet-4-6"
LLM_TIMEOUT_SECONDS: "20"
LLM_MAX_OUTPUT_TOKENS: "1200"
MAX_USER_QUERY_LEN: "2000"
+75
View File
@@ -65,6 +65,56 @@ spec:
configMapKeyRef:
name: stock-agent-config
key: MAX_TOKENS_PER_RUN
- name: DB_POOL_MIN_SIZE
valueFrom:
configMapKeyRef:
name: stock-agent-config
key: DB_POOL_MIN_SIZE
- name: DB_POOL_MAX_SIZE
valueFrom:
configMapKeyRef:
name: stock-agent-config
key: DB_POOL_MAX_SIZE
- name: DB_POOL_TIMEOUT_SECONDS
valueFrom:
configMapKeyRef:
name: stock-agent-config
key: DB_POOL_TIMEOUT_SECONDS
- name: DB_COMMAND_TIMEOUT_SECONDS
valueFrom:
configMapKeyRef:
name: stock-agent-config
key: DB_COMMAND_TIMEOUT_SECONDS
- name: KLINE_QUERY_TIMEOUT_SECONDS
valueFrom:
configMapKeyRef:
name: stock-agent-config
key: KLINE_QUERY_TIMEOUT_SECONDS
- name: KLINE_DEFAULT_LIMIT
valueFrom:
configMapKeyRef:
name: stock-agent-config
key: KLINE_DEFAULT_LIMIT
- name: KLINE_MAX_LIMIT
valueFrom:
configMapKeyRef:
name: stock-agent-config
key: KLINE_MAX_LIMIT
- name: KLINE_BULK_PER_SYMBOL_LIMIT
valueFrom:
configMapKeyRef:
name: stock-agent-config
key: KLINE_BULK_PER_SYMBOL_LIMIT
- name: KLINE_BULK_MAX_SYMBOLS
valueFrom:
configMapKeyRef:
name: stock-agent-config
key: KLINE_BULK_MAX_SYMBOLS
- name: KLINE_BULK_MAX_ROWS
valueFrom:
configMapKeyRef:
name: stock-agent-config
key: KLINE_BULK_MAX_ROWS
- name: CANDIDATE_POOL_SIZE
valueFrom:
configMapKeyRef:
@@ -75,6 +125,31 @@ spec:
configMapKeyRef:
name: stock-agent-config
key: DISPLAY_TOP_N
- name: MIN_AMOUNT_AVG
valueFrom:
configMapKeyRef:
name: stock-agent-config
key: MIN_AMOUNT_AVG
- name: LOOKBACK_DAYS_CALENDAR
valueFrom:
configMapKeyRef:
name: stock-agent-config
key: LOOKBACK_DAYS_CALENDAR
- name: STRATEGY_WEIGHT_MOMENTUM_RULE_V1
valueFrom:
configMapKeyRef:
name: stock-agent-config
key: STRATEGY_WEIGHT_MOMENTUM_RULE_V1
- name: STRATEGY_WEIGHT_TECHNICAL_PATTERN_V1
valueFrom:
configMapKeyRef:
name: stock-agent-config
key: STRATEGY_WEIGHT_TECHNICAL_PATTERN_V1
- name: STRATEGY_WEIGHT_QLIB_RECO_READER_V1
valueFrom:
configMapKeyRef:
name: stock-agent-config
key: STRATEGY_WEIGHT_QLIB_RECO_READER_V1
- name: WRITE_RECO
valueFrom:
configMapKeyRef:
@@ -0,0 +1,25 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ljwx-stock-agent-ingress
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: web,websecure
traefik.ingress.kubernetes.io/router.tls: 'true'
cert-manager.io/cluster-issuer: dnspod-letsencrypt
spec:
ingressClassName: traefik
rules:
- host: stock-agent.lingjingwanxiang.cn
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: stock-agent
port:
number: 80
tls:
- hosts:
- stock-agent.lingjingwanxiang.cn
secretName: stock-agent-lingjingwanxiang-cn-tls
@@ -3,6 +3,7 @@ kind: Kustomization
namespace: ljwx-stock
resources:
- ../../base
- ingress.yaml
images:
- name: ghcr.io/brunogao/ljwx-stock/ljwx-stock-agent
newName: ghcr.io/brunogao/ljwx-stock/ljwx-stock-agent
@@ -0,0 +1,36 @@
# ljwx-stock-agent capability contract (dev)
## Namespace baseline
- namespace: `ljwx-stock`
- namespace profile: `dev-default`
- registry profile: `ghcr-and-harbor`
- runtime secret profile: `none`
- image pull secrets: `ghcr-pull, regcred`
- runtime contract secrets: `none`
## Capabilities
- ghcr_pull (namespace): Bind GHCR pull secret ghcr-pull to the namespace baseline.
- harbor_pull (namespace): Bind Harbor pull secret regcred to the namespace baseline.
## Public endpoint
- ingress profile: `traefik-letsencrypt-public`
- host: `https://stock-agent.lingjingwanxiang.cn`
- ingress class: `traefik`
- cluster issuer: `dnspod-letsencrypt`
- tls secret: `stock-agent-lingjingwanxiang-cn-tls`
- ingress artifact generation: `enabled`
## Bootstrap commands
```bash
bash scripts/ops/sync-registry-pull-secrets.sh --target-namespace ljwx-stock --registry-profile ghcr-and-harbor
```
## Notes
- `runtime-contract/*.secret.example.yaml` are placeholders only and must not contain real credentials.
- `cluster_bootstrap` is disabled for this entry; keep the existing cluster namespace/application manifests as the current source of truth.
- For legacy workloads, migrate deployment manifests gradually to `runtime-infra`, `runtime-llm`, and `runtime-app` instead of app-specific secret names.
@@ -0,0 +1,20 @@
service: ljwx-stock-agent
environment: dev
namespace: ljwx-stock
cluster_bootstrap: false
ingress_profile: traefik-letsencrypt-public
public_host: stock-agent.lingjingwanxiang.cn
generate_ingress: true
ingress_class_name: traefik
cluster_issuer: dnspod-letsencrypt
tls_secret_name: stock-agent-lingjingwanxiang-cn-tls
namespace_profile: dev-default
registry_profile: ghcr-and-harbor
runtime_secret_profile: none
capabilities:
- ghcr_pull
- harbor_pull
image_pull_secrets:
- ghcr-pull
- regcred
contract_secret_names: []
@@ -14,7 +14,7 @@ spec:
jobTemplate:
spec:
backoffLimit: 1
activeDeadlineSeconds: 21600
activeDeadlineSeconds: 43200
template:
spec:
restartPolicy: OnFailure
@@ -74,5 +74,5 @@ spec:
cpu: "1"
memory: 2Gi
limits:
cpu: "2"
memory: 4Gi
cpu: "4"
memory: 8Gi
@@ -14,7 +14,7 @@ spec:
jobTemplate:
spec:
backoffLimit: 2
activeDeadlineSeconds: 7200
activeDeadlineSeconds: 21600
template:
spec:
restartPolicy: OnFailure
@@ -74,5 +74,5 @@ spec:
cpu: 500m
memory: 1Gi
limits:
cpu: "1"
memory: 2Gi
cpu: "2"
memory: 4Gi
@@ -19,7 +19,7 @@ spec:
restartPolicy: OnFailure
containers:
- name: qlib-bootstrap
image: ghcr.io/brunogao/ljwx-stock/ljwx-stock-qlib-bootstrap:bootstrap
image: ghcr.io/brunogao/ljwx-stock/ljwx-stock-qlib-bootstrap:20260305-fix1
imagePullPolicy: IfNotPresent
command:
- python
@@ -64,7 +64,7 @@ spec:
- name: EXPORT_LOOKBACK_CALENDAR_DAYS
value: "4500"
- name: CODE_VERSION
value: p6a
value: p6a-fix20260305
volumeMounts:
- name: qlib-data
mountPath: /data/qlib
@@ -19,7 +19,7 @@ spec:
restartPolicy: OnFailure
initContainers:
- name: sync-qlib-from-minio
image: minio/mc:RELEASE.2024-06-13T22-53-53Z
image: minio/mc:RELEASE.2025-08-13T08-35-41Z
imagePullPolicy: IfNotPresent
command:
- /bin/sh
@@ -29,17 +29,22 @@ spec:
mc alias set qlib "${MINIO_ENDPOINT}" "${MINIO_ACCESS_KEY}" "${MINIO_SECRET_KEY}"
qdata_latest="$(mc cat "qlib/${MINIO_BUCKET}/${MINIO_PREFIX_QDATA}/LATEST" | tr -d '\r\n')"
if ! mc ls "qlib/${MINIO_BUCKET}" >/dev/null 2>&1; then
echo "MinIO bucket 不存在,跳过本次 qlib 同步: ${MINIO_BUCKET}"
exit 0
fi
qdata_latest="$(mc cat "qlib/${MINIO_BUCKET}/${MINIO_PREFIX_QDATA}/LATEST" 2>/dev/null | xargs || true)"
if [ -z "${qdata_latest}" ]; then
echo "qlib_data LATEST 为空" >&2
exit 1
echo "qlib_data LATEST 不存在或为空,跳过本次 qlib 同步"
exit 0
fi
model_family="qlib_lightgbm_alpha158"
model_latest="$(mc cat "qlib/${MINIO_BUCKET}/${MINIO_PREFIX_MODELS}/${model_family}/LATEST" | tr -d '\r\n')"
model_latest="$(mc cat "qlib/${MINIO_BUCKET}/${MINIO_PREFIX_MODELS}/${model_family}/LATEST" 2>/dev/null | xargs || true)"
if [ -z "${model_latest}" ]; then
echo "model LATEST 为空" >&2
exit 1
echo "model LATEST 不存在或为空,跳过本次 qlib 同步"
exit 0
fi
mkdir -p /data/qlib/qlib_data/cn
@@ -91,12 +96,38 @@ spec:
memory: 1Gi
containers:
- name: qlib-predict
image: ghcr.io/brunogao/ljwx-stock/ljwx-stock-qlib-predict:bootstrap
image: ghcr.io/brunogao/ljwx-stock/ljwx-stock-qlib-predict:20260305-fix1
imagePullPolicy: IfNotPresent
command:
- python
- -m
- qlib_predict.app.predict_to_pg
- /bin/sh
- -ec
- |
set -euo pipefail
MODEL_LATEST_FILE="/data/qlib/artifacts/models/qlib_lightgbm_alpha158/LATEST"
if [ ! -f "${MODEL_LATEST_FILE}" ]; then
echo "未检测到模型 LATEST,跳过本次预测"
exit 0
fi
MODEL_DATE="$(xargs < "${MODEL_LATEST_FILE}")"
if [ -z "${MODEL_DATE}" ]; then
echo "模型 LATEST 为空,跳过本次预测"
exit 0
fi
MODEL_DIR="/data/qlib/artifacts/models/qlib_lightgbm_alpha158/${MODEL_DATE}"
if [ ! -f "${MODEL_DIR}/model.pkl" ] || [ ! -f "${MODEL_DIR}/handler_config.yaml" ] || [ ! -f "${MODEL_DIR}/meta.json" ]; then
echo "模型产物不完整,跳过本次预测: ${MODEL_DIR}"
exit 0
fi
if [ ! -f "/data/qlib/qlib_data/cn/calendars/day.txt" ]; then
echo "未检测到 qlib provider 数据,跳过本次预测"
exit 0
fi
exec python -m qlib_predict.app.predict_to_pg
env:
- name: DATABASE_URL
valueFrom:
@@ -110,17 +141,17 @@ spec:
- name: CANDIDATE_POOL_SIZE
value: "300"
- name: CODE_VERSION
value: p3a
value: p3a-fix20260305
volumeMounts:
- name: qlib-data
mountPath: /data/qlib
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: "1"
memory: 2Gi
limits:
cpu: "2"
memory: 4Gi
volumes:
- name: qlib-data
persistentVolumeClaim:
+2 -2
View File
@@ -13,7 +13,7 @@ spec:
restartPolicy: Never
containers:
- name: qlib-bootstrap
image: ghcr.io/brunogao/ljwx-stock/ljwx-stock-qlib-bootstrap:bootstrap
image: ghcr.io/brunogao/ljwx-stock/ljwx-stock-qlib-bootstrap:20260305-fix1
imagePullPolicy: IfNotPresent
command:
- python
@@ -58,7 +58,7 @@ spec:
- name: EXPORT_LOOKBACK_CALENDAR_DAYS
value: "4500"
- name: CODE_VERSION
value: p6a
value: p6a-fix20260305
volumeMounts:
- name: qlib-data
mountPath: /data/qlib
@@ -6,10 +6,10 @@ resources:
images:
- name: ghcr.io/brunogao/ljwx-stock/ljwx-stock-qlib-predict
newName: ghcr.io/brunogao/ljwx-stock/ljwx-stock-qlib-predict
digest: sha256:9b310ac3e5f1079ac322b6ece4f532e43dc26fccfb9073bd8fce2fb697c0b606
digest: sha256:af130411bf6cffe0e8a58ad2aab6f6f30d45d8f8521f591e49074ca7f3a14118
- name: ghcr.io/brunogao/ljwx-stock/ljwx-stock-kline-etl
newName: ghcr.io/brunogao/ljwx-stock/ljwx-stock-kline-etl
digest: sha256:db529b61617eefd4b383e8f73668af1a272a883856c3e8cd32f5838ef5f03cc4
- name: ghcr.io/brunogao/ljwx-stock/ljwx-stock-qlib-bootstrap
newName: ghcr.io/brunogao/ljwx-stock/ljwx-stock-qlib-bootstrap
digest: sha256:6705bf8c0bb41bc36bbcdcfb021fb49c7b6bb3bb109f0ed37d8bf4118d755a51
digest: sha256:3179cf393e708ddc5ce3e3f62837e19f7c2c0d267cdb1bc93370edb170f72eed
@@ -0,0 +1,27 @@
# ljwx-stock-qlib-predict capability contract (dev)
## Namespace baseline
- namespace: `ljwx-stock`
- namespace profile: `dev-default`
- registry profile: `ghcr-and-harbor`
- runtime secret profile: `none`
- image pull secrets: `ghcr-pull, regcred`
- runtime contract secrets: `none`
## Capabilities
- ghcr_pull (namespace): Bind GHCR pull secret ghcr-pull to the namespace baseline.
- harbor_pull (namespace): Bind Harbor pull secret regcred to the namespace baseline.
## Bootstrap commands
```bash
bash scripts/ops/sync-registry-pull-secrets.sh --target-namespace ljwx-stock --registry-profile ghcr-and-harbor
```
## Notes
- `runtime-contract/*.secret.example.yaml` are placeholders only and must not contain real credentials.
- `cluster_bootstrap` is disabled for this entry; keep the existing cluster namespace/application manifests as the current source of truth.
- For legacy workloads, migrate deployment manifests gradually to `runtime-infra`, `runtime-llm`, and `runtime-app` instead of app-specific secret names.
@@ -0,0 +1,20 @@
service: ljwx-stock-qlib-predict
environment: dev
namespace: ljwx-stock
cluster_bootstrap: false
ingress_profile: none
public_host: ''
generate_ingress: true
ingress_class_name: ''
cluster_issuer: ''
tls_secret_name: ''
namespace_profile: dev-default
registry_profile: ghcr-and-harbor
runtime_secret_profile: none
capabilities:
- ghcr_pull
- harbor_pull
image_pull_secrets:
- ghcr-pull
- regcred
contract_secret_names: []
@@ -0,0 +1,20 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: cluster-prod-bootstrap
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/BrunoGaoSZ/ljwx-deploy.git
targetRevision: main
path: cluster-prod
destination:
server: https://kubernetes.default.svc
namespace: argocd
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
+27
View File
@@ -0,0 +1,27 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: cert-manager-dev
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "3"
spec:
project: default
source:
repoURL: quay.io/jetstack/charts
chart: cert-manager
targetRevision: v1.19.2
helm:
releaseName: cert-manager
values: |
crds:
enabled: true
destination:
server: https://kubernetes.default.svc
namespace: cert-manager
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
@@ -0,0 +1,22 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: cert-manager-config-dev
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "4"
spec:
project: default
source:
repoURL: https://github.com/BrunoGaoSZ/ljwx-deploy.git
targetRevision: main
path: apps/cert-manager-config/overlays/dev
destination:
server: https://kubernetes.default.svc
namespace: cert-manager
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
@@ -17,4 +17,4 @@ spec:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- CreateNamespace=true
+1 -1
View File
@@ -17,4 +17,4 @@ spec:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- CreateNamespace=true
+20
View File
@@ -0,0 +1,20 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: ljwx-dify-dev
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/BrunoGaoSZ/ljwx-deploy.git
targetRevision: main
path: apps/ljwx-dify/overlays/ljwx-dify-dev
destination:
server: https://kubernetes.default.svc
namespace: ljwx-dify-dev
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
+20
View File
@@ -0,0 +1,20 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: ljwx-chat-dev
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/BrunoGaoSZ/ljwx-deploy.git
targetRevision: main
path: apps/ljwx-chat/overlays/ljwx-chat-dev
destination:
server: https://kubernetes.default.svc
namespace: ljwx-chat-dev
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
+20
View File
@@ -0,0 +1,20 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: ljwx-shiti-dev
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/BrunoGaoSZ/ljwx-deploy.git
targetRevision: main
path: apps/ljwx-shiti/overlays/dev
destination:
server: https://kubernetes.default.svc
namespace: ljwx-shiti-dev
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
+8
View File
@@ -0,0 +1,8 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: default
namespace: dev
imagePullSecrets:
- name: ghcr-pull
- name: regcred
@@ -0,0 +1,145 @@
apiVersion: batch/v1
kind: CronJob
metadata:
name: deploy-promoter-prod
namespace: dev
labels:
app.kubernetes.io/name: deploy-promoter-prod
app.kubernetes.io/part-of: bid-mvp-factory
spec:
schedule: "5-59/10 * * * *"
timeZone: Asia/Tokyo
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 2
failedJobsHistoryLimit: 3
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: promoter-prod
image: ghcr.io/brunogao/ljwx-deploy-promoter:main
imagePullPolicy: Always
env:
# Production-only promoter: wait for artifacts in production Harbor
# (https://harbor.omniverseai.net/) and update prod overlays for the
# production ArgoCD instance to detect and deploy.
- name: GIT_BRANCH
value: main
- name: DEPLOY_REPO_URL
value: https://github.com/BrunoGaoSZ/ljwx-deploy.git
- name: SERVICE_MAP_PATH
value: release/services.orbstack-k3s-cn.yaml
- name: ENV_ALLOWLIST
value: "prod"
- name: SKIP_REGISTRY_CHECK
value: "0"
- name: SKIP_EVIDENCE_COLLECT
value: "0"
- name: RETRY_MAX
value: "10"
- name: HARBOR_INSECURE
value: "1"
- name: DEPLOY_REPO_TOKEN
valueFrom:
secretKeyRef:
name: deploy-promoter-secret
key: deploy_repo_token
optional: true
- name: GITHUB_TOKEN
valueFrom:
secretKeyRef:
name: deploy-promoter-secret
key: github_token
optional: true
- name: HARBOR_URL
valueFrom:
secretKeyRef:
name: deploy-promoter-secret
key: harbor_prod_url
optional: true
- name: HARBOR_URL_FALLBACK
valueFrom:
secretKeyRef:
name: deploy-promoter-secret
key: harbor_url
optional: true
- name: HARBOR_USER
valueFrom:
secretKeyRef:
name: deploy-promoter-secret
key: harbor_prod_username
optional: true
- name: HARBOR_USER_FALLBACK
valueFrom:
secretKeyRef:
name: deploy-promoter-secret
key: harbor_username
optional: true
- name: HARBOR_PASS
valueFrom:
secretKeyRef:
name: deploy-promoter-secret
key: harbor_prod_password
optional: true
- name: HARBOR_PASS_FALLBACK
valueFrom:
secretKeyRef:
name: deploy-promoter-secret
key: harbor_password
optional: true
command:
- /bin/bash
- -lc
- |
set -euo pipefail
if [ -z "${HARBOR_URL:-}" ] && [ -n "${HARBOR_URL_FALLBACK:-}" ]; then
export HARBOR_URL="${HARBOR_URL_FALLBACK}"
fi
if [ -z "${HARBOR_USER:-}" ] && [ -n "${HARBOR_USER_FALLBACK:-}" ]; then
export HARBOR_USER="${HARBOR_USER_FALLBACK}"
fi
if [ -z "${HARBOR_PASS:-}" ] && [ -n "${HARBOR_PASS_FALLBACK:-}" ]; then
export HARBOR_PASS="${HARBOR_PASS_FALLBACK}"
fi
if [ -z "${DEPLOY_REPO_TOKEN:-}" ] && [ -n "${GITHUB_TOKEN:-}" ]; then
export DEPLOY_REPO_TOKEN="${GITHUB_TOKEN}"
fi
if [ -z "${DEPLOY_REPO_TOKEN:-}" ]; then
echo "缺少 DEPLOY_REPO_TOKEN(请在 deploy-promoter-secret 中配置 deploy_repo_token)"
exit 1
fi
rm -rf /work/repo
git clone --depth=1 "https://x-access-token:${DEPLOY_REPO_TOKEN}@${DEPLOY_REPO_URL#https://}" /work/repo
cd /work/repo
cmd=(
python3 scripts/promoter/promote.py
--local-repo-dir /work/repo
--service-map "${SERVICE_MAP_PATH}"
--push-branch "${GIT_BRANCH}"
--retry-max "${RETRY_MAX}"
)
if [ -n "${ENV_ALLOWLIST:-}" ]; then
cmd+=(--env-allowlist "${ENV_ALLOWLIST}")
fi
if [ -n "${HARBOR_URL:-}" ]; then
cmd+=(--harbor-url "${HARBOR_URL}")
fi
if [ -n "${HARBOR_USER:-}" ]; then
cmd+=(--harbor-user "${HARBOR_USER}")
fi
if [ -n "${HARBOR_PASS:-}" ]; then
cmd+=(--harbor-pass "${HARBOR_PASS}")
fi
if [ "${SKIP_REGISTRY_CHECK:-0}" = "1" ] || [ "${SKIP_REGISTRY_CHECK:-}" = "true" ] || [ "${SKIP_REGISTRY_CHECK:-}" = "TRUE" ]; then
cmd+=(--skip-registry-check)
fi
if [ "${HARBOR_INSECURE:-0}" = "1" ] || [ "${HARBOR_INSECURE:-}" = "true" ] || [ "${HARBOR_INSECURE:-}" = "TRUE" ]; then
cmd+=(--harbor-insecure)
fi
if [ "${SKIP_EVIDENCE_COLLECT:-0}" = "1" ] || [ "${SKIP_EVIDENCE_COLLECT:-}" = "true" ] || [ "${SKIP_EVIDENCE_COLLECT:-}" = "TRUE" ]; then
cmd+=(--skip-evidence-collect)
fi
"${cmd[@]}"
+8
View File
@@ -0,0 +1,8 @@
resources:
- namespace-dev.yaml
- default-sa.yaml
- namespace-ljwx-platform.yaml
- ljwx-platform-prod-application.yaml
- namespace-ljwx-website-prod.yaml
- website-prod-application.yaml
- deploy-promoter-prod-cronjob.yaml
@@ -0,0 +1,20 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: ljwx-platform
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/BrunoGaoSZ/ljwx-deploy.git
targetRevision: main
path: apps/ljwx-platform/overlays/prod
destination:
server: https://kubernetes.default.svc
namespace: ljwx-platform
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
+4
View File
@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: dev
+62
View File
@@ -0,0 +1,62 @@
apiVersion: v1
kind: Namespace
metadata:
name: ljwx-platform
labels:
name: ljwx-platform
environment: prod
app: ljwx-platform
annotations:
gitops.ljwx.io/namespace-profile: prod-default
gitops.ljwx.io/registry-profile: harbor-only
gitops.ljwx.io/runtime-secret-profile: app-runtime
gitops.ljwx.io/capabilities: ghcr_pull,harbor_pull,runtime_app_secret
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: default
namespace: ljwx-platform
annotations:
gitops.ljwx.io/namespace-profile: prod-default
gitops.ljwx.io/registry-profile: harbor-only
gitops.ljwx.io/runtime-secret-profile: app-runtime
gitops.ljwx.io/capabilities: ghcr_pull,harbor_pull,runtime_app_secret
imagePullSecrets:
- name: regcred
---
apiVersion: v1
kind: ResourceQuota
metadata:
name: ljwx-platform-quota
namespace: ljwx-platform
spec:
hard:
requests.cpu: '2'
requests.memory: 4Gi
limits.cpu: '4'
limits.memory: 8Gi
persistentvolumeclaims: '10'
services: '10'
pods: '30'
---
apiVersion: v1
kind: LimitRange
metadata:
name: ljwx-platform-limits
namespace: ljwx-platform
spec:
limits:
- max:
cpu: '2'
memory: 4Gi
min:
cpu: 100m
memory: 256Mi
default:
cpu: 500m
memory: 1Gi
defaultRequest:
cpu: 250m
memory: 512Mi
type: Container
@@ -0,0 +1,44 @@
apiVersion: v1
kind: Namespace
metadata:
name: ljwx-website-prod
labels:
name: ljwx-website-prod
environment: prod
app: ljwx-website
---
apiVersion: v1
kind: ResourceQuota
metadata:
name: ljwx-website-quota
namespace: ljwx-website-prod
spec:
hard:
requests.cpu: "500m"
requests.memory: "512Mi"
limits.cpu: "1"
limits.memory: "1Gi"
persistentvolumeclaims: "5"
services: "5"
pods: "10"
---
apiVersion: v1
kind: LimitRange
metadata:
name: ljwx-website-limits
namespace: ljwx-website-prod
spec:
limits:
- max:
cpu: "500m"
memory: "512Mi"
min:
cpu: "10m"
memory: "32Mi"
default:
cpu: "100m"
memory: "128Mi"
defaultRequest:
cpu: "50m"
memory: "64Mi"
type: Container
@@ -0,0 +1,20 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: ljwx-website-prod
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/BrunoGaoSZ/ljwx-deploy.git
targetRevision: main
path: apps/ljwx-website/overlays/ljwx-website-prod
destination:
server: https://kubernetes.default.svc
namespace: ljwx-website-prod
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
+1
View File
@@ -4,4 +4,5 @@ metadata:
name: default
namespace: dev
imagePullSecrets:
- name: ghcr-pull
- name: regcred
+55 -5
View File
@@ -19,21 +19,26 @@ spec:
restartPolicy: OnFailure
containers:
- name: promoter
image: ghcr.io/brunogaosz/ljwx-deploy-promoter:main
image: ghcr.io/brunogao/ljwx-deploy-promoter:main
imagePullPolicy: Always
env:
# Dev promoter only: wait for local Harbor candidate images and update dev/demo overlays.
- name: GIT_BRANCH
value: main
- name: DEPLOY_REPO_URL
value: https://github.com/BrunoGaoSZ/ljwx-deploy.git
- name: SERVICE_MAP_PATH
value: release/services.local-k3s.yaml
- name: ENV_ALLOWLIST
value: "dev,demo"
- name: SKIP_REGISTRY_CHECK
value: "1"
value: "0"
- name: SKIP_EVIDENCE_COLLECT
value: "0"
- name: RETRY_MAX
value: "10"
- name: HARBOR_INSECURE
value: "1"
- name: DEPLOY_REPO_TOKEN
valueFrom:
secretKeyRef:
@@ -46,19 +51,38 @@ spec:
name: deploy-promoter-secret
key: github_token
optional: true
# Local Harbor is the dev gate; prod Harbor is handled only by cluster-prod/deploy-promoter-prod.
- name: HARBOR_URL
valueFrom:
secretKeyRef:
name: deploy-promoter-secret
key: harbor_local_url
optional: true
- name: HARBOR_URL_FALLBACK
valueFrom:
secretKeyRef:
name: deploy-promoter-secret
key: harbor_url
optional: true
- name: HARBOR_USER
valueFrom:
secretKeyRef:
name: deploy-promoter-secret
key: harbor_local_username
optional: true
- name: HARBOR_USER_FALLBACK
valueFrom:
secretKeyRef:
name: deploy-promoter-secret
key: harbor_username
optional: true
- name: HARBOR_PASS
valueFrom:
secretKeyRef:
name: deploy-promoter-secret
key: harbor_local_password
optional: true
- name: HARBOR_PASS_FALLBACK
valueFrom:
secretKeyRef:
name: deploy-promoter-secret
@@ -69,6 +93,15 @@ spec:
- -lc
- |
set -euo pipefail
if [ -z "${HARBOR_URL:-}" ] && [ -n "${HARBOR_URL_FALLBACK:-}" ]; then
export HARBOR_URL="${HARBOR_URL_FALLBACK}"
fi
if [ -z "${HARBOR_USER:-}" ] && [ -n "${HARBOR_USER_FALLBACK:-}" ]; then
export HARBOR_USER="${HARBOR_USER_FALLBACK}"
fi
if [ -z "${HARBOR_PASS:-}" ] && [ -n "${HARBOR_PASS_FALLBACK:-}" ]; then
export HARBOR_PASS="${HARBOR_PASS_FALLBACK}"
fi
if [ -z "${DEPLOY_REPO_TOKEN:-}" ] && [ -n "${GITHUB_TOKEN:-}" ]; then
export DEPLOY_REPO_TOKEN="${GITHUB_TOKEN}"
fi
@@ -76,18 +109,35 @@ spec:
echo "缺少 DEPLOY_REPO_TOKEN(请在 deploy-promoter-secret 中配置 deploy_repo_token)"
exit 1
fi
rm -rf /work/repo
git clone --depth=1 "https://x-access-token:${DEPLOY_REPO_TOKEN}@${DEPLOY_REPO_URL#https://}" /work/repo
cd /work/repo
cmd=(
python3 /app/scripts/promoter/promote.py
--deploy-repo-url "${DEPLOY_REPO_URL}"
--deploy-repo-token "${DEPLOY_REPO_TOKEN}"
python3 scripts/promoter/promote.py
--local-repo-dir /work/repo
--service-map "${SERVICE_MAP_PATH}"
--push-branch "${GIT_BRANCH}"
--retry-max "${RETRY_MAX}"
)
if [ -n "${ENV_ALLOWLIST:-}" ]; then
cmd+=(--env-allowlist "${ENV_ALLOWLIST}")
fi
if [ -n "${HARBOR_URL:-}" ]; then
cmd+=(--harbor-url "${HARBOR_URL}")
fi
if [ -n "${HARBOR_USER:-}" ]; then
cmd+=(--harbor-user "${HARBOR_USER}")
fi
if [ -n "${HARBOR_PASS:-}" ]; then
cmd+=(--harbor-pass "${HARBOR_PASS}")
fi
if [ "${SKIP_REGISTRY_CHECK:-0}" = "1" ] || [ "${SKIP_REGISTRY_CHECK:-}" = "true" ] || [ "${SKIP_REGISTRY_CHECK:-}" = "TRUE" ]; then
cmd+=(--skip-registry-check)
fi
if [ "${HARBOR_INSECURE:-0}" = "1" ] || [ "${HARBOR_INSECURE:-}" = "true" ] || [ "${HARBOR_INSECURE:-}" = "TRUE" ]; then
cmd+=(--harbor-insecure)
fi
if [ "${SKIP_EVIDENCE_COLLECT:-0}" = "1" ] || [ "${SKIP_EVIDENCE_COLLECT:-}" = "true" ] || [ "${SKIP_EVIDENCE_COLLECT:-}" = "TRUE" ]; then
cmd+=(--skip-evidence-collect)
fi
@@ -10,6 +10,15 @@ stringData:
deploy_repo_token: "<github_app_or_fine_grained_token>"
# Backward compatible key (optional, can be omitted after migration).
github_token: "<legacy_token_optional>"
# Local Harbor credentials (dev/demos deploy gate).
harbor_local_url: "https://harbor.eu.lingjingwanxiang.cn"
harbor_local_username: "admin"
harbor_local_password: "<local_harbor_password>"
# Production Harbor credentials (prod deploy gate).
harbor_prod_url: "https://harbor.omniverseai.net"
harbor_prod_username: "admin"
harbor_prod_password: "<prod_harbor_password>"
# Legacy single Harbor keys (fallback only).
harbor_url: "https://harbor.example.com"
harbor_username: "<harbor_user>"
harbor_password: "<harbor_password>"
+17 -11
View File
@@ -1,12 +1,18 @@
resources:
- namespace-dev.yaml
- namespace-ljwx-website-dev.yaml
- namespace-ljwx-bookstore.yaml
- website-dev-application.yaml
- ljwx-platform-observability-application.yaml
- deploy-promoter-cronjob.yaml
- smoke-runner-cronjob.yaml
- regcred.yaml
- default-sa.yaml
- stock-etl-qlib-predict-application.yaml
- stock-agent-application.yaml
- namespace-dev.yaml
- namespace-ljwx-website-dev.yaml
- namespace-ljwx-dify-dev.yaml
- namespace-ljwx-chat-dev.yaml
- namespace-ljwx-bookstore.yaml
- website-dev-application.yaml
- ljwx-dify-dev-application.yaml
- ljwx-chat-dev-application.yaml
- ljwx-platform-observability-application.yaml
- deploy-promoter-cronjob.yaml
- smoke-runner-cronjob.yaml
- regcred.yaml
- default-sa.yaml
- stock-etl-qlib-predict-application.yaml
- stock-agent-application.yaml
- namespace-ljwx-shiti-dev.yaml
- ljwx-shiti-dev-application.yaml
+20
View File
@@ -0,0 +1,20 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: ljwx-chat-dev
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/BrunoGaoSZ/ljwx-deploy.git
targetRevision: main
path: apps/ljwx-chat/overlays/ljwx-chat-dev
destination:
server: https://kubernetes.default.svc
namespace: ljwx-chat-dev
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
+20
View File
@@ -0,0 +1,20 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: ljwx-dify-dev
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/BrunoGaoSZ/ljwx-deploy.git
targetRevision: main
path: apps/ljwx-dify/overlays/ljwx-dify-dev
destination:
server: https://kubernetes.default.svc
namespace: ljwx-dify-dev
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true

Some files were not shown because too many files have changed in this diff Show More