添加qiming-rcoder模块

This commit is contained in:
Codex
2026-06-01 13:54:52 +08:00
parent 8092c4b1f8
commit 4b1a580132
539 changed files with 151650 additions and 0 deletions

390
qiming-rcoder/k8s/README.md Normal file
View File

@@ -0,0 +1,390 @@
# RCoder K8s 部署配置
本目录提供 **三种部署方式**,按场景选择:
| 场景 | 方式 | 入口 |
|------|------|------|
| 日常开发 / 在线环境 | **Kustomize** | `deploy-dev.sh` / `deploy-prod.sh` |
| 参数化部署 / 对外交付 | **Helm** (双栈并存) | `helm install ... k8s/helm/rcoder` |
| 政企客户 / 完全断网 | **离线 Bundle** | `make k8s-offline-bundle``install.sh` |
另外还有可选的 **本地镜像加速**`register2/`),面向多节点 k3s / 频繁构建场景,详见下文"本地镜像加速"一节。
## 目录结构
```
k8s/
├── manifests/ # Kustomize manifests (日常开发 + 在线部署)
│ ├── base/ # 基础配置 (不可直接部署, 需走 overlay)
│ │ ├── storage/ # postgresql / minio / juicefs-init / juicefs-pvc
│ │ └── rcoder/ # rcoder Deployment/Service/NetworkPolicy/PDB/SA+ClusterRole
│ └── overlays/
│ ├── dev/ # 开发环境 (nuwax-rcoder-dev, NodePort 30080)
│ └── prod/ # 生产环境 (nuwax-rcoder-prod, NodePort 30081)
├── helm/ # Helm chart (对外交付 + 离线部署源)
│ └── rcoder/
│ ├── Chart.yaml
│ ├── values.yaml # 默认值 (共享)
│ ├── values-dev.yaml # dev 覆盖
│ ├── values-prod.yaml # prod 覆盖
│ ├── values-offline.yaml # 离线 registry 覆盖
│ └── templates/ # K8s 模板
├── offline/ # 离线 bundle 素材
│ ├── images.txt # 镜像清单 (单一数据源)
│ ├── install.sh # 离线安装脚本 (direct / registry 双模式)
│ ├── rewrite-registry.sh # 重打 tag + 推送私有 registry
│ └── README.md # 交付给政企客户的部署手册
├── register2/ # 本地私有镜像仓库 (docker compose)
│ ├── docker-compose.yml # registry:2 容器 + 数据卷
│ └── README.md # 作 k3s mirror / 离线 push 目标的用法
├── deploy-dev.sh # [在线] Kustomize 开发环境一键部署
├── deploy-prod.sh # [在线] Kustomize 生产环境一键部署 (含占位符密码守卫)
├── undeploy.sh # [在线] 清理部署 (ENV=dev|prod)
├── scripts/ # 辅助脚本
│ ├── deploy-juicefs.sh # 仅部署存储层 (ENV=dev|prod)
│ ├── test-chat.sh # 功能冒烟测试 (NAMESPACE 可覆盖)
│ ├── install-k3s-registry-mirrors-cn.sh # K3s 节点镜像加速配置 (支持 REGISTRY_HOST)
│ └── k3s-registries-cn.yaml # 镜像加速配置模板
└── README.md
```
dev 和 prod 可以并存于同一个集群:
- 集群级资源StorageClass / ClusterRoleBinding命名独立`juicefs-sc-dev` / `juicefs-sc-prod``rcoder-pods-crb-dev` / `rcoder-pods-crb-prod`
- NodePort 错开dev=30080 / prod=30081
- ClusterRole `rcoder-pods-clusterrole` 是唯一共享的集群级资源(两个 overlay 写入完全一致,幂等)
---
## 快速开始
### 开发环境
```bash
# 首次完整部署(含 Longhorn、JuiceFS CSI、open-iscsi 检查)
./k8s/deploy-dev.sh
# 常用
kubectl apply -k k8s/manifests/overlays/dev # 部署/更新
kubectl delete -k k8s/manifests/overlays/dev # 删除
kubectl get all -n nuwax-rcoder-dev # 查看状态
k8s/scripts/test-chat.sh # 冒烟测试
```
### 生产环境
> **重要**:首次部署前必须替换 `k8s/manifests/overlays/prod/credentials.yaml` 和 `juicefs-secret.yaml` 里的 `CHANGE-ME-BEFORE-DEPLOY` 占位符;`deploy-prod.sh` 有守卫检查,发现占位符会拒绝部署。若使用 SealedSecret / ExternalSecret 在集群外注入,设置 `FORCE_PROD_DEPLOY=1` 跳过。
```bash
# 首次完整部署
./k8s/deploy-prod.sh
# 跳过占位符检查(仅当通过外部密钥注入方案时)
FORCE_PROD_DEPLOY=1 ./k8s/deploy-prod.sh
# 常用
kubectl apply -k k8s/manifests/overlays/prod
kubectl get all -n nuwax-rcoder-prod
NAMESPACE=nuwax-rcoder-prod k8s/scripts/test-chat.sh
```
### 清理
```bash
ENV=dev ./k8s/undeploy.sh # 清理 dev
ENV=prod ./k8s/undeploy.sh # 清理 prod
```
---
## Makefile 快捷命令
在项目根目录执行:
```bash
# 镜像
make dev-build-k8s # 构建并推送 K8s 镜像 (含 k3s 本地 import + rollout restart)
# 部署/清理(默认走 overlay
make deploy-dev # kubectl apply -k overlays/dev
make deploy-prod # kubectl apply -k overlays/prod
make undeploy-dev # 清理 dev
make undeploy-prod # 清理 prod
# 开发闭环
make dev-up-k8s # 部署 dev
make dev-restart-k8s # 重新构建镜像 + 重部署 dev
make dev-down-k8s # 清理 dev
make dev-logs-k8s # 跟随 rcoder 日志
# 本地验证(不部署)
make kustomize-build # 分别 build base / dev / prod
# 离线部署 (政企内网)
make k8s-offline-bundle # 打包完整离线包 (images + helm + manifests)
make k8s-offline-import BUNDLE=<tgz> INSTALL_ARGS="--mode=direct --env=dev"
make k8s-offline-images-list # 打印所有离线依赖镜像清单
make k8s-offline-clean # 清理构建产物
```
---
## Helm 部署 (参数化 / 对外交付)
Helm chart 与 Kustomize **双栈并存、长期维护**。修改 `manifests/base/*.yaml` 时请同步更新 `helm/rcoder/templates/*` 对应模板。
```bash
# dev (本地或在线环境)
helm install rcoder-dev k8s/helm/rcoder \
--namespace nuwax-rcoder-dev --create-namespace \
-f k8s/helm/rcoder/values-dev.yaml
# 同集群已有 Kustomize 部署的话, 加 --set rcoder.clusterRole.create=false
# (避免 Helm 尝试接管 Kustomize 已创建的 ClusterRole)
helm install rcoder-helm-test k8s/helm/rcoder \
--namespace nuwax-rcoder-helm-test --create-namespace \
-f k8s/helm/rcoder/values-dev.yaml \
--set rcoder.service.nodePort=30082 \
--set juicefs.storageClass.name=juicefs-sc-helm-test \
--set rcoder.clusterRole.create=false
# prod (密码通过 --set 或 external secret 注入)
helm install rcoder-prod k8s/helm/rcoder \
--namespace nuwax-rcoder-prod --create-namespace \
-f k8s/helm/rcoder/values-prod.yaml \
--set credentials.postgresql.password=<real> \
--set credentials.minio.rootPassword=<real>
# 升级
helm upgrade rcoder-dev k8s/helm/rcoder -f k8s/helm/rcoder/values-dev.yaml
# 卸载
helm uninstall rcoder-dev --namespace nuwax-rcoder-dev
```
### 渲染对比 (开发调试)
```bash
helm template rcoder-dev k8s/helm/rcoder -f k8s/helm/rcoder/values-dev.yaml > /tmp/helm-dev.yaml
kubectl kustomize k8s/manifests/overlays/dev > /tmp/kustomize-dev.yaml
diff <(grep -E 'image:|nodePort:|storageClassName:' /tmp/helm-dev.yaml | sort -u) \
<(grep -E 'image:|nodePort:|storageClassName:' /tmp/kustomize-dev.yaml | sort -u)
```
### 集群级资源命名
Helm 为每个 release 自动生成独立的集群级资源名:
| 资源 | dev release (`rcoder-dev`) | prod release (`rcoder-prod`) |
|------|---------------------------|------------------------------|
| StorageClass | `juicefs-sc-dev` (显式 override) | `juicefs-sc-prod` (显式 override) |
| ClusterRoleBinding | `rcoder-dev-pods-crb` (自动) | `rcoder-prod-pods-crb` (自动) |
| ClusterRole | `rcoder-pods-clusterrole` (共享, `resource-policy: keep`) |
---
## 离线部署 (政企内网, 完全断网)
当客户内网无公网访问时,使用 **Bundle 模式**在有网的构建机打包所有镜像、helm charts、manifests产出单个 `tar.gz`;离线机器上解压运行 `install.sh` 完成部署。
### 构建 bundle (有网构建机)
```bash
# 拉取所有镜像 + 打包 helm charts + 下载 Longhorn/JuiceFS CSI manifest + 压缩
make k8s-offline-bundle
# 输出: dist/rcoder-offline-<version>-<arch>.tar.gz (约 1.7-2 GB, 共 27 个镜像)
ls -lh dist/rcoder-offline-*.tar.gz
# 查看镜像清单
make k8s-offline-images-list
```
### 离线机器部署
```bash
# 方式 1: 用 make 一键跑完
make k8s-offline-import BUNDLE=/path/to/rcoder-offline-xxx.tar.gz \
INSTALL_ARGS="--mode=direct --env=dev"
# 方式 2: 手工解压 + 跑 install.sh
tar xzf rcoder-offline-xxx.tar.gz -C /opt/rcoder-offline
cd /opt/rcoder-offline
sudo bash install.sh --mode=direct --env=dev
```
### 两种导入模式
| 模式 | 镜像落盘方式 | 适用场景 |
|------|------------|----------|
| `--mode=direct` | `ctr image import` 到节点 containerd | 单节点 / 小集群 / 无私有 registry |
| `--mode=registry` | `docker load` + re-tag + push | 多节点 / 已有 Harbor/Nexus / 客户自建 registry |
详见 [`offline/README.md`](./offline/README.md)(客户交付文档)。
---
## 本地镜像加速(多节点 / 频繁构建场景)
当本地 k3s 有多个节点,或者你频繁 push 测试镜像又不想每次走公网时,启用 `register2/` 作本地 registry mirror所有节点直接拉局域网镜像千兆
### 架构
```
k3s/containerd (所有节点)
├── (1) http://<你的机器>:5000 ← register2 (本目录, 私有推送)
├── (2) http://<你的机器>:5002 ← registry-cache (可选 pull-through)
├── (3) https://docker.m.daocloud.io / ... ← 公网 cn mirror 回退
└── (4) 默认源 (docker.io / registry.k8s.io) 兜底
```
containerd 按顺序尝试 endpoint命中200即返回失败404/超时)自动回退下一个。
### 启动本地 registry
```bash
cd k8s/register2
docker compose up -d
# 验证:
curl -s http://<你的机器>:5000/v2/_catalog
```
### 一键配置 k3s 节点
```bash
# 在每个 k3s 节点上执行 (REGISTRY_HOST 按实际填)
sudo REGISTRY_HOST=192.168.32.228:5000 \
k8s/scripts/install-k3s-registry-mirrors-cn.sh
# 多个 registry 串联 (比如同时有 registry2 和 pull-through cache)
sudo REGISTRY_HOST=192.168.32.228:5000,192.168.32.228:5002 \
k8s/scripts/install-k3s-registry-mirrors-cn.sh
```
脚本会:
- 备份现有 `/etc/rancher/k3s/registries.yaml`
- 写入包含本地 registry + 公网 cn mirror 回退的完整配置(含 `registry.k8s.io`/`ghcr.io`/`quay.io` 全套)
- 自动加 "自指 mirror" + `insecure_skip_verify` 允许 `crictl pull <host>/foo` 走 plain HTTP
- 自动 `systemctl restart k3s``k3s-agent`
-`crictl pull docker.io/rancher/mirrored-pause:3.6` 验证
### 本机 docker daemon 配置(允许 push 到 HTTP registry
```bash
sudo tee /etc/docker/daemon.json <<EOF
{
"insecure-registries": ["192.168.32.228:5000"]
}
EOF
sudo systemctl restart docker
```
详见 [`register2/README.md`](./register2/README.md)。
### 与离线 bundle 的联动
配好本地 registry 后,在本机就能测通 bundle 的 `registry 模式`
```bash
tar xzf dist/rcoder-offline-*.tar.gz -C /tmp/offline-test
cd /tmp/offline-test
docker load -i images/all-images.tar
bash rewrite-registry.sh \
--registry 192.168.32.228:5000/rcoder \
--thirdparty-registry 192.168.32.228:5000/thirdparty \
--images-file images/images.txt
# 之后 helm install 时 k3s 自动从 192.168.32.228:5000 拉
```
---
## 存储架构
```
┌─────────────────────────────────────────────────────────────┐
│ Kubernetes 集群 │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ JuiceFS CSI Driver (kube-system) │ │
│ └──────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌────────────────┐ ┌────────────────┐ │
│ │ rcoder 主服务 │ │ Agent Runner │ │
│ │ (Deployment) │ │ (动态创建 Pod) │ │
│ └────────┬───────┘ └────────┬───────┘ │
│ └──────────┬───────────┘ │
│ ↓ │
│ ┌──────────────┐ │
│ │ JuiceFS 卷 │ ← ReadWriteMany 共享 │
│ └───────┬──────┘ │
└──────────────────────┼──────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────┐
│ MinIO (S3) ← JuiceFS 数据存储per-namespace
│ PostgreSQL ← JuiceFS 元数据存储per-namespace
└───────────────────────────────────────────────────────────────┘
```
每个 namespace 独立拥有 PostgreSQL 和 MinIO 实例metadata 和 object 存储互不干扰。
---
## 环境要求
- Kubernetes 1.14+(本仓库在 K3s 1.34.x 上测试)
- `kubectl` 已配置 kubeconfig
- `helm` 3.x用于首次安装 JuiceFS CSI Driver + Helm 部署路径)
- Longhorn 存储(为 PostgreSQL / MinIO 提供 PV`deploy-*.sh` 会自动安装)
- `open-iscsi`Longhorn 依赖;`deploy-*.sh` 会自动安装)
- `docker`(仅在有网构建机 `make k8s-offline-bundle` 或本地 push 到 register2 时需要)
国内节点建议先配置 k3s 镜像加速(纯公网 cn mirror 版):
```bash
sudo k8s/scripts/install-k3s-registry-mirrors-cn.sh
```
有本地 registry 时用 `REGISTRY_HOST=...`(见 "本地镜像加速" 一节)。
---
## 故障排查
```bash
# Pod 状态
kubectl get pods -n nuwax-rcoder-dev # 或 -prod / -helm-test / <your-ns>
# 事件
kubectl get events -n nuwax-rcoder-dev --sort-by='.lastTimestamp'
# RCoder 日志
kubectl logs -n nuwax-rcoder-dev -l app=rcoder --tail=200 -f
# PVC 绑定情况
kubectl get pvc -n nuwax-rcoder-dev
kubectl describe pvc rcoder-workspace -n nuwax-rcoder-dev
# StorageClass
kubectl get sc
# 镜像加速是否生效 (与 kubelet 同链路验证, 不要用 k3s ctr images pull)
sudo crictl --runtime-endpoint unix:///run/k3s/containerd/containerd.sock \
pull docker.io/rancher/mirrored-pause:3.6
# 本地 registry 命中情况
curl -s http://192.168.32.228:5000/v2/_catalog
curl -s http://192.168.32.228:5002/v2/_catalog # pull-through 缓存
```
---
## 历史说明
此前版本曾提供 `k8s/helm/rcoder/` Helm chart 作为另一条部署路径;**2025 年后改为 Kustomize + Helm 双栈并存**
- Kustomize 用于开发和已有部署,保持轻量;
- Helm 用于参数化对外交付和离线 bundle
- 两者共享同一份 K8s 资源定义(修改时需同步)。

View File

@@ -0,0 +1,331 @@
#!/bin/bash
# ============================================================
# RCoder K8s 开发环境部署脚本 (Kustomize)
# 用于日常开发测试验证
# ============================================================
set -e
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# 路径解析:支持从任意 cwd 调用脚本
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
NAMESPACE="${NAMESPACE:-nuwax-rcoder-dev}"
KUSTOMIZE_DIR="${KUSTOMIZE_DIR:-${SCRIPT_DIR}/manifests/overlays/dev}"
JUICEFS_CE_MOUNT_IMAGE="${JUICEFS_CE_MOUNT_IMAGE:-juicedata/mount:ce-v1.3.1}"
DNS_CHECK_IMAGE="${DNS_CHECK_IMAGE:-busybox:1.36}"
echo -e "${GREEN}=========================================="
echo " RCoder K8s 开发环境部署 (Kustomize)"
echo "==========================================${NC}"
ensure_juicefs_ce_image() {
echo -e "${YELLOW}配置 JuiceFS CSI 使用 CE Mount 镜像: ${JUICEFS_CE_MOUNT_IMAGE}${NC}"
local node_ds=""
if kubectl get daemonset/juicefs-csi-node -n kube-system &> /dev/null; then
node_ds="juicefs-csi-node"
elif kubectl get daemonset/juicefs-csi-driver-node -n kube-system &> /dev/null; then
node_ds="juicefs-csi-driver-node"
fi
# 兼容不同版本 CSI 驱动:同时设置旧变量与 CE 专用变量
if [ -n "$node_ds" ]; then
kubectl -n kube-system set env "daemonset/${node_ds}" -c juicefs-plugin \
JUICEFS_CE_MOUNT_IMAGE="${JUICEFS_CE_MOUNT_IMAGE}" \
JUICEFS_MOUNT_IMAGE="${JUICEFS_CE_MOUNT_IMAGE}" >/dev/null
fi
kubectl -n kube-system set env statefulset/juicefs-csi-controller -c juicefs-plugin \
JUICEFS_CE_MOUNT_IMAGE="${JUICEFS_CE_MOUNT_IMAGE}" \
JUICEFS_MOUNT_IMAGE="${JUICEFS_CE_MOUNT_IMAGE}" >/dev/null
# 同步更新驱动 ConfigMap部分版本从这里读取 mountImage
kubectl -n kube-system patch configmap juicefs-csi-driver-config --type merge \
-p "{\"data\":{\"config.yaml\":\"enableNodeSelector: false\\nmountImage: ${JUICEFS_CE_MOUNT_IMAGE}\\nmountPodPatch:\\n\"}}" >/dev/null || true
}
check_cluster_dns() {
echo -e "${GREEN}检查集群 DNS 基线...${NC}"
local check_name="rcoder-dns-check-$(date +%s)"
if kubectl run "${check_name}" --restart=Never --rm -i -n "${NAMESPACE}" \
--image="${DNS_CHECK_IMAGE}" --command -- \
sh -lc "nslookup kubernetes.default.svc.cluster.local >/dev/null && nslookup postgresql.${NAMESPACE}.svc.cluster.local >/dev/null && nslookup minio-service.${NAMESPACE}.svc.cluster.local >/dev/null" >/dev/null 2>&1; then
echo -e "${GREEN}✅ 集群 DNS 解析正常${NC}"
else
echo -e "${RED}❌ 集群 DNS 解析异常:无法解析 Kubernetes/业务 Service 域名${NC}"
echo -e "${YELLOW}建议先修复 CoreDNS 后再继续(例如重启 coredns / 检查 coredns addon${NC}"
exit 1
fi
}
# ============================================================
# 步骤 1: 检查 K8s 集群
# ============================================================
echo ""
echo -e "${GREEN}[1/6] 检查 K8s 集群...${NC}"
if ! command -v kubectl &> /dev/null; then
echo -e "${RED}Error: kubectl not found${NC}"
echo "请安装 kubectl: https://kubernetes.io/docs/tasks/tools/"
exit 1
fi
if ! kubectl cluster-info &> /dev/null; then
echo -e "${RED}Error: 无法连接到 K8s 集群${NC}"
echo ""
echo -e "${YELLOW}请先部署 K3s 集群 (中国镜像):${NC}"
echo ""
echo " curl -sfL https://rancher-mirror.rancher.cn/k3s/k3s-install.sh | INSTALL_K3S_MIRROR=cn sh -"
echo ""
echo " # 安装完成后, 配置 kubectl:"
echo " mkdir -p ~/.kube"
echo " sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config"
echo " chmod 600 ~/.kube/config"
echo ""
echo " # 重新运行此脚本"
exit 1
fi
echo -e "${GREEN}✅ K8s 集群连接正常${NC}"
kubectl get nodes
# ============================================================
# 步骤 1.5: 检查/安装 open-iscsi (Longhorn 依赖)
# ============================================================
echo ""
echo -e "${GREEN}[1.5/6] 检查 open-iscsi 依赖...${NC}"
install_open_iscsi() {
echo -e "${YELLOW}正在检测操作系统...${NC}"
# 检测操作系统类型
if [ -f /etc/os-release ]; then
. /etc/os-release
OS_ID="$ID"
OS_VERSION="$VERSION_ID"
else
OS_ID="unknown"
fi
echo -e "${YELLOW}检测到操作系统: $OS_ID${NC}"
case "$OS_ID" in
ubuntu|debian)
echo -e "${YELLOW}安装 open-iscsi...${NC}"
apt-get update -qq
apt-get install -y -qq open-iscsi > /dev/null 2>&1
;;
centos|rhel|rocky|almalinux)
echo -e "${YELLOW}安装 iscsi-initiator-utils...${NC}"
yum install -y -q iscsi-initiator-utils > /dev/null 2>&1
;;
sles|opensuse-leap|opensuse-tumbleweed)
echo -e "${YELLOW}安装 open-iscsi...${NC}"
zypper install -y -q open-iscsi > /dev/null 2>&1
;;
*)
echo -e "${RED}不支持的操作系统: $OS_ID${NC}"
echo -e "${YELLOW}请手动安装 open-iscsi 或 iscsi-initiator-utils${NC}"
return 1
;;
esac
echo -e "${YELLOW}启动 iscsid 服务...${NC}"
systemctl enable --now iscsid 2>/dev/null || true
systemctl enable --now iscsid.socket 2>/dev/null || true
return 0
}
# 检查 iscsiadm 是否存在
if command -v iscsiadm &> /dev/null; then
echo -e "${GREEN}✅ open-iscsi 已安装: $(iscsiadm --version 2>&1 | head -1)${NC}"
else
echo -e "${YELLOW}⚠️ open-iscsi 未安装${NC}"
echo -e "${YELLOW}Longhorn 需要 open-iscsi 来提供 iSCSI 存储${NC}"
# 尝试自动安装
if [ "$EUID" -eq 0 ]; then
install_open_iscsi
if [ $? -eq 0 ]; then
echo -e "${GREEN}✅ open-iscsi 安装成功${NC}"
fi
else
echo -e "${YELLOW}请使用 sudo 运行此脚本,或手动安装:${NC}"
echo -e " ${CYAN}Ubuntu/Debian: sudo apt install open-iscsi${NC}"
echo -e " ${CYAN}CentOS/RHEL: sudo yum install iscsi-initiator-utils${NC}"
echo -e " ${CYAN}SUSE: sudo zypper install open-iscsi${NC}"
echo ""
echo -e "${YELLOW}安装完成后,重新运行此脚本${NC}"
exit 1
fi
fi
# 验证 iscsiadm 可用
if ! command -v iscsiadm &> /dev/null; then
echo -e "${RED}❌ open-iscsi 安装失败${NC}"
exit 1
fi
echo -e "${GREEN}✅ open-iscsi 检查完成${NC}"
# ============================================================
# 步骤 2: 部署 Longhorn 存储
# ============================================================
echo ""
echo -e "${GREEN}[2/6] 检查/部署 Longhorn 存储...${NC}"
if kubectl get sc longhorn &> /dev/null; then
echo -e "${GREEN}✅ Longhorn 已安装${NC}"
else
echo -e "${YELLOW}Longhorn 未安装,正在部署...${NC}"
kubectl apply -f https://raw.githubusercontent.com/longhorn/longhorn/master/deploy/longhorn.yaml
echo -e "${GREEN}⏳ 等待 Longhorn 就绪...${NC}"
# 等待 Longhorn Manager 就绪
kubectl wait --for=condition=ready pod -l app=longhorn-manager \
-n longhorn-system --timeout=300s 2>/dev/null || true
# 等待 Longhorn UI 就绪
kubectl wait --for=condition=ready pod -l app=longhorn-ui \
-n longhorn-system --timeout=300s 2>/dev/null || true
echo -e "${GREEN}✅ Longhorn 部署完成${NC}"
fi
kubectl get sc | grep longhorn || echo -e "${YELLOW}Warning: Longhorn StorageClass 未就绪${NC}"
# ============================================================
# 步骤 3: 部署 JuiceFS CSI
# ============================================================
echo ""
echo -e "${GREEN}[3/6] 检查/部署 JuiceFS CSI Driver...${NC}"
if kubectl get ds juicefs-csi-driver-node -n kube-system &> /dev/null || kubectl get ds juicefs-csi-node -n kube-system &> /dev/null; then
echo -e "${GREEN}✅ JuiceFS CSI Driver 已安装${NC}"
ensure_juicefs_ce_image
else
echo -e "${YELLOW}JuiceFS CSI Driver 未安装,正在部署...${NC}"
if ! command -v helm &> /dev/null; then
echo -e "${YELLOW}Helm 未安装,跳过 JuiceFS CSI 部署${NC}"
echo "请手动安装: helm repo add juicedata https://juicedata.github.io/charts"
else
helm repo add juicedata https://juicedata.github.io/charts 2>/dev/null || true
helm repo update
helm install juicefs-csi-driver juicedata/juicefs-csi-driver \
--namespace kube-system \
--set webhook.enabled=false
echo -e "${GREEN}⏳ 等待 JuiceFS CSI Driver 就绪...${NC}"
kubectl wait --for=condition=ready pod -l app=juicefs-csi-driver-node \
-n kube-system --timeout=120s 2>/dev/null || true
ensure_juicefs_ce_image
echo -e "${GREEN}✅ JuiceFS CSI Driver 部署完成${NC}"
fi
fi
# ============================================================
# 步骤 4: 使用 Kustomize 部署 RCoder
# ============================================================
echo ""
echo -e "${GREEN}[4/6] 部署 RCoder 到 namespace: $NAMESPACE${NC}"
# 检查 kubectl kustomize 插件
if ! kubectl kustomize --help &> /dev/null; then
echo -e "${RED}Error: kubectl kustomize 插件未找到 (需要 kubectl 1.14+)${NC}"
exit 1
fi
# 部署 (分两阶段避免 JuiceFS CSI mount 期间 postgresql 未就绪导致 60s retry 噪音)
echo -e "${YELLOW}阶段 1/2: 先部署存储层 + RBAC + Secret...${NC}"
kubectl apply -k "$KUSTOMIZE_DIR"
echo -e "${YELLOW}等待 PostgreSQL 就绪 (JuiceFS CSI 依赖)...${NC}"
kubectl wait --for=condition=ready pod -l app=postgresql -n "$NAMESPACE" --timeout=180s 2>&1 || {
echo -e "${RED}❌ PostgreSQL 未在 180s 内就绪${NC}"
kubectl get pods -n "$NAMESPACE"
exit 1
}
echo -e "${YELLOW}等待 MinIO 就绪...${NC}"
kubectl wait --for=condition=ready pod -l app=minio -n "$NAMESPACE" --timeout=180s 2>&1 || {
echo -e "${RED}❌ MinIO 未在 180s 内就绪${NC}"
exit 1
}
# 如果 rcoder pod 已经创建但还卡在 mount retry删掉让它重建 (clean mount)
RCODER_POD_READY=$(kubectl get pod -n "$NAMESPACE" -l app=rcoder -o jsonpath='{.items[*].status.conditions[?(@.type=="Ready")].status}' 2>/dev/null | grep -c True)
if [ "${RCODER_POD_READY:-0}" -eq 0 ]; then
echo -e "${YELLOW}阶段 2/2: 重建 rcoder pod 以触发干净 mount...${NC}"
kubectl delete pod -n "$NAMESPACE" -l app=rcoder --ignore-not-found --wait=false
fi
echo -e "${YELLOW}等待 rcoder Deployment 就绪...${NC}"
kubectl rollout status deploy/rcoder -n "$NAMESPACE" --timeout=180s
check_cluster_dns
echo -e "${GREEN}✅ RCoder 部署完成${NC}"
# ============================================================
# 步骤 5: 验证部署
# ============================================================
echo ""
echo -e "${GREEN}[5/6] 验证部署状态...${NC}"
echo ""
echo "--- Pods ---"
kubectl get pods -n "$NAMESPACE"
echo ""
echo "--- Services ---"
kubectl get svc -n "$NAMESPACE"
echo ""
echo "--- StorageClass ---"
kubectl get sc | grep -E "longhorn|juicefs"
echo ""
echo "--- PVC ---"
kubectl get pvc -n "$NAMESPACE"
# 获取访问信息
NODE_IP=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}' 2>/dev/null || echo "localhost")
SVC_TYPE=$(kubectl get svc rcoder -n "$NAMESPACE" -o jsonpath='{.spec.type}' 2>/dev/null)
echo ""
echo -e "${GREEN}=========================================="
echo " 部署完成!"
echo "==========================================${NC}"
echo ""
echo -e "访问方式:"
if [ "$SVC_TYPE" = "NodePort" ]; then
NODE_PORT=$(kubectl get svc rcoder -n "$NAMESPACE" -o jsonpath='{.spec.ports[0].nodePort}')
echo -e " ${GREEN}http://${NODE_IP}:${NODE_PORT}/health${NC}"
echo -e " ${GREEN}http://${NODE_IP}:${NODE_PORT}/chat${NC}"
elif [ "$SVC_TYPE" = "LoadBalancer" ]; then
EXT_IP=$(kubectl get svc rcoder -n "$NAMESPACE" -o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null || echo "<pending>")
echo -e " ${GREEN}http://${EXT_IP}:8087/health${NC}"
else
echo -e " ${GREEN}kubectl port-forward svc/rcoder 8087:8087 -n $NAMESPACE${NC}"
echo -e " 然后访问: ${GREEN}http://localhost:8087/health${NC}"
fi
echo ""
echo -e "Kustomize 管理命令:"
echo -e " ${YELLOW}kubectl apply -k $KUSTOMIZE_DIR${NC} # 部署/更新"
echo -e " ${YELLOW}kubectl delete -k $KUSTOMIZE_DIR${NC} # 删除"
echo -e " ${YELLOW}kubectl get all -n $NAMESPACE${NC} # 查看状态"
echo ""
echo -e "Longhorn 控制台:"
echo -e " ${YELLOW}kubectl port-forward svc/longhorn-frontend 8080:80 -n longhorn-system${NC}"
echo -e " 访问: ${GREEN}http://localhost:8080${NC}"
echo ""

View File

@@ -0,0 +1,353 @@
#!/bin/bash
# ============================================================
# RCoder K8s 生产环境部署脚本 (Kustomize)
# 部署目标: namespace = nuwax-rcoder-prod, overlay = manifests/overlays/prod
# 支持 K3s / 标准 K8s 集群
# ============================================================
set -e
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# 路径解析:支持从任意 cwd 调用脚本
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
NAMESPACE="${NAMESPACE:-nuwax-rcoder-prod}"
KUSTOMIZE_DIR="${KUSTOMIZE_DIR:-${SCRIPT_DIR}/manifests/overlays/prod}"
JUICEFS_CE_MOUNT_IMAGE="${JUICEFS_CE_MOUNT_IMAGE:-juicedata/mount:ce-v1.3.1}"
DNS_CHECK_IMAGE="${DNS_CHECK_IMAGE:-busybox:1.36}"
echo -e "${GREEN}=========================================="
echo " RCoder K8s 生产环境部署 (Kustomize)"
echo "==========================================${NC}"
echo -e " namespace: ${NAMESPACE}"
echo -e " overlay: ${KUSTOMIZE_DIR}"
echo ""
# ============================================================
# 生产环境安全闸门: 拒绝使用占位符密码部署
# 允许用 FORCE_PROD_DEPLOY=1 绕过(用于 CI 在外部注入 Secret 的场景)
# ============================================================
check_prod_credentials() {
local overlay_dir="$1"
local files=("${overlay_dir}/credentials.yaml" "${overlay_dir}/juicefs-secret.yaml")
local found=0
for f in "${files[@]}"; do
if [ -f "$f" ] && grep -q "CHANGE-ME-BEFORE-DEPLOY" "$f"; then
echo -e "${RED}❌ 检测到占位符密码未替换: $f${NC}" >&2
found=1
fi
done
if [ "$found" -eq 1 ]; then
echo "" >&2
echo -e "${YELLOW}生产环境禁止使用 CHANGE-ME-BEFORE-DEPLOY 占位符部署。${NC}" >&2
echo -e "${YELLOW}处理方式(二选一):${NC}" >&2
echo -e " 1) 把上述文件里的占位符替换为真实强密码后再部署" >&2
echo -e " 2) 如果你用 SealedSecret / ExternalSecret 从外部注入密钥," >&2
echo -e " 请设置 FORCE_PROD_DEPLOY=1 并确保上线后真正的 Secret 已就绪" >&2
exit 1
fi
}
if [ "${FORCE_PROD_DEPLOY:-0}" != "1" ]; then
check_prod_credentials "${KUSTOMIZE_DIR}"
fi
ensure_juicefs_ce_image() {
echo -e "${YELLOW}配置 JuiceFS CSI 使用 CE Mount 镜像: ${JUICEFS_CE_MOUNT_IMAGE}${NC}"
local node_ds=""
if kubectl get daemonset/juicefs-csi-node -n kube-system &> /dev/null; then
node_ds="juicefs-csi-node"
elif kubectl get daemonset/juicefs-csi-driver-node -n kube-system &> /dev/null; then
node_ds="juicefs-csi-driver-node"
fi
if [ -n "$node_ds" ]; then
kubectl -n kube-system set env "daemonset/${node_ds}" -c juicefs-plugin \
JUICEFS_CE_MOUNT_IMAGE="${JUICEFS_CE_MOUNT_IMAGE}" \
JUICEFS_MOUNT_IMAGE="${JUICEFS_CE_MOUNT_IMAGE}" >/dev/null
fi
kubectl -n kube-system set env statefulset/juicefs-csi-controller -c juicefs-plugin \
JUICEFS_CE_MOUNT_IMAGE="${JUICEFS_CE_MOUNT_IMAGE}" \
JUICEFS_MOUNT_IMAGE="${JUICEFS_CE_MOUNT_IMAGE}" >/dev/null
kubectl -n kube-system patch configmap juicefs-csi-driver-config --type merge \
-p "{\"data\":{\"config.yaml\":\"enableNodeSelector: false\\nmountImage: ${JUICEFS_CE_MOUNT_IMAGE}\\nmountPodPatch:\\n\"}}" >/dev/null || true
}
check_cluster_dns() {
echo -e "${GREEN}检查集群 DNS 基线...${NC}"
local check_name="rcoder-dns-check-$(date +%s)"
if kubectl run "${check_name}" --restart=Never --rm -i -n "${NAMESPACE}" \
--image="${DNS_CHECK_IMAGE}" --command -- \
sh -lc "nslookup kubernetes.default.svc.cluster.local >/dev/null && nslookup postgresql.${NAMESPACE}.svc.cluster.local >/dev/null && nslookup minio-service.${NAMESPACE}.svc.cluster.local >/dev/null" >/dev/null 2>&1; then
echo -e "${GREEN}✅ 集群 DNS 解析正常${NC}"
else
echo -e "${RED}❌ 集群 DNS 解析异常:无法解析 Kubernetes/业务 Service 域名${NC}"
echo -e "${YELLOW}建议先修复 CoreDNS 后再继续(例如重启 coredns / 检查 coredns addon${NC}"
exit 1
fi
}
# ============================================================
# 步骤 1: 检查 K8s 集群
# ============================================================
echo ""
echo -e "${GREEN}[1/6] 检查 K8s 集群...${NC}"
if ! command -v kubectl &> /dev/null; then
echo -e "${RED}Error: kubectl not found${NC}"
echo "请安装 kubectl: https://kubernetes.io/docs/tasks/tools/"
exit 1
fi
if ! kubectl cluster-info &> /dev/null; then
echo -e "${RED}Error: 无法连接到 K8s 集群${NC}"
echo ""
echo -e "${YELLOW}请先部署 K3s 集群 (中国镜像):${NC}"
echo ""
echo " curl -sfL https://rancher-mirror.rancher.cn/k3s/k3s-install.sh | INSTALL_K3S_MIRROR=cn sh -"
echo ""
echo " # 安装完成后, 配置 kubectl:"
echo " mkdir -p ~/.kube"
echo " sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config"
echo " chmod 600 ~/.kube/config"
echo ""
echo " # 重新运行此脚本"
exit 1
fi
echo -e "${GREEN}✅ K8s 集群连接正常${NC}"
kubectl get nodes
# ============================================================
# 步骤 1.5: 检查/安装 open-iscsi (Longhorn 依赖)
# ============================================================
echo ""
echo -e "${GREEN}[1.5/6] 检查 open-iscsi 依赖...${NC}"
install_open_iscsi() {
echo -e "${YELLOW}正在检测操作系统...${NC}"
if [ -f /etc/os-release ]; then
. /etc/os-release
OS_ID="$ID"
OS_VERSION="$VERSION_ID"
else
OS_ID="unknown"
fi
echo -e "${YELLOW}检测到操作系统: $OS_ID${NC}"
case "$OS_ID" in
ubuntu|debian)
echo -e "${YELLOW}安装 open-iscsi...${NC}"
apt-get update -qq
apt-get install -y -qq open-iscsi > /dev/null 2>&1
;;
centos|rhel|rocky|almalinux)
echo -e "${YELLOW}安装 iscsi-initiator-utils...${NC}"
yum install -y -q iscsi-initiator-utils > /dev/null 2>&1
;;
sles|opensuse-leap|opensuse-tumbleweed)
echo -e "${YELLOW}安装 open-iscsi...${NC}"
zypper install -y -q open-iscsi > /dev/null 2>&1
;;
*)
echo -e "${RED}不支持的操作系统: $OS_ID${NC}"
echo -e "${YELLOW}请手动安装 open-iscsi 或 iscsi-initiator-utils${NC}"
return 1
;;
esac
echo -e "${YELLOW}启动 iscsid 服务...${NC}"
systemctl enable --now iscsid 2>/dev/null || true
systemctl enable --now iscsid.socket 2>/dev/null || true
return 0
}
if command -v iscsiadm &> /dev/null; then
echo -e "${GREEN}✅ open-iscsi 已安装: $(iscsiadm --version 2>&1 | head -1)${NC}"
else
echo -e "${YELLOW}⚠️ open-iscsi 未安装${NC}"
echo -e "${YELLOW}Longhorn 需要 open-iscsi 来提供 iSCSI 存储${NC}"
if [ "$EUID" -eq 0 ]; then
install_open_iscsi
if [ $? -eq 0 ]; then
echo -e "${GREEN}✅ open-iscsi 安装成功${NC}"
fi
else
echo -e "${YELLOW}请使用 sudo 运行此脚本,或手动安装:${NC}"
echo -e " Ubuntu/Debian: sudo apt install open-iscsi"
echo -e " CentOS/RHEL: sudo yum install iscsi-initiator-utils"
echo -e " SUSE: sudo zypper install open-iscsi"
echo ""
echo -e "${YELLOW}安装完成后,重新运行此脚本${NC}"
exit 1
fi
fi
if ! command -v iscsiadm &> /dev/null; then
echo -e "${RED}❌ open-iscsi 安装失败${NC}"
exit 1
fi
echo -e "${GREEN}✅ open-iscsi 检查完成${NC}"
# ============================================================
# 步骤 2: 部署 Longhorn 存储
# ============================================================
echo ""
echo -e "${GREEN}[2/6] 检查/部署 Longhorn 存储...${NC}"
if kubectl get sc longhorn &> /dev/null; then
echo -e "${GREEN}✅ Longhorn 已安装${NC}"
else
echo -e "${YELLOW}Longhorn 未安装,正在部署...${NC}"
kubectl apply -f https://raw.githubusercontent.com/longhorn/longhorn/master/deploy/longhorn.yaml
echo -e "${GREEN}⏳ 等待 Longhorn 就绪...${NC}"
kubectl wait --for=condition=ready pod -l app=longhorn-manager \
-n longhorn-system --timeout=300s 2>/dev/null || true
kubectl wait --for=condition=ready pod -l app=longhorn-ui \
-n longhorn-system --timeout=300s 2>/dev/null || true
echo -e "${GREEN}✅ Longhorn 部署完成${NC}"
fi
kubectl get sc | grep longhorn || echo -e "${YELLOW}Warning: Longhorn StorageClass 未就绪${NC}"
# ============================================================
# 步骤 3: 部署 JuiceFS CSI
# ============================================================
echo ""
echo -e "${GREEN}[3/6] 检查/部署 JuiceFS CSI Driver...${NC}"
if kubectl get ds juicefs-csi-driver-node -n kube-system &> /dev/null || kubectl get ds juicefs-csi-node -n kube-system &> /dev/null; then
echo -e "${GREEN}✅ JuiceFS CSI Driver 已安装${NC}"
ensure_juicefs_ce_image
else
echo -e "${YELLOW}JuiceFS CSI Driver 未安装,正在部署...${NC}"
if ! command -v helm &> /dev/null; then
echo -e "${RED}Error: Helm not found安装 JuiceFS CSI Driver 需要 Helm${NC}"
echo "请安装 Helm: https://helm.sh/docs/intro/install/"
exit 1
else
helm repo add juicedata https://juicedata.github.io/charts 2>/dev/null || true
helm repo update
helm install juicefs-csi-driver juicedata/juicefs-csi-driver \
--namespace kube-system \
--set webhook.enabled=false
echo -e "${GREEN}⏳ 等待 JuiceFS CSI Driver 就绪...${NC}"
kubectl wait --for=condition=ready pod -l app=juicefs-csi-driver-node \
-n kube-system --timeout=120s 2>/dev/null || true
ensure_juicefs_ce_image
echo -e "${GREEN}✅ JuiceFS CSI Driver 部署完成${NC}"
fi
fi
# ============================================================
# 步骤 4: 使用 Kustomize 部署 RCoder (prod overlay)
# ============================================================
echo ""
echo -e "${GREEN}[4/6] 部署 RCoder 到 namespace: $NAMESPACE${NC}"
if ! kubectl kustomize --help &> /dev/null; then
echo -e "${RED}Error: kubectl kustomize 插件未找到 (需要 kubectl 1.14+)${NC}"
exit 1
fi
# 分两阶段部署:等 PostgreSQL/MinIO 就绪再让 rcoder 起,避免 JuiceFS CSI mount 的 60s retry 噪音
echo -e "${YELLOW}阶段 1/2: apply manifests...${NC}"
kubectl apply -k "$KUSTOMIZE_DIR"
echo -e "${YELLOW}等待 PostgreSQL 就绪 (JuiceFS CSI 依赖)...${NC}"
kubectl wait --for=condition=ready pod -l app=postgresql -n "$NAMESPACE" --timeout=180s 2>&1 || {
echo -e "${RED}❌ PostgreSQL 未在 180s 内就绪${NC}"
kubectl get pods -n "$NAMESPACE"
exit 1
}
echo -e "${YELLOW}等待 MinIO 就绪...${NC}"
kubectl wait --for=condition=ready pod -l app=minio -n "$NAMESPACE" --timeout=180s 2>&1 || {
echo -e "${RED}❌ MinIO 未在 180s 内就绪${NC}"
exit 1
}
RCODER_POD_READY=$(kubectl get pod -n "$NAMESPACE" -l app=rcoder -o jsonpath='{.items[*].status.conditions[?(@.type=="Ready")].status}' 2>/dev/null | grep -c True)
if [ "${RCODER_POD_READY:-0}" -eq 0 ]; then
echo -e "${YELLOW}阶段 2/2: 重建 rcoder pod 以触发干净 mount...${NC}"
kubectl delete pod -n "$NAMESPACE" -l app=rcoder --ignore-not-found --wait=false
fi
echo -e "${YELLOW}等待 rcoder Deployment 就绪...${NC}"
kubectl rollout status deploy/rcoder -n "$NAMESPACE" --timeout=180s
check_cluster_dns
echo -e "${GREEN}✅ RCoder 部署完成${NC}"
# ============================================================
# 步骤 5: 验证部署
# ============================================================
echo ""
echo -e "${GREEN}[5/6] 验证部署状态...${NC}"
echo ""
echo "--- Pods ---"
kubectl get pods -n "$NAMESPACE"
echo ""
echo "--- Services ---"
kubectl get svc -n "$NAMESPACE"
echo ""
echo "--- StorageClass ---"
kubectl get sc | grep -E "longhorn|juicefs"
echo ""
echo "--- PVC ---"
kubectl get pvc -n "$NAMESPACE"
NODE_IP=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}' 2>/dev/null || echo "localhost")
SVC_TYPE=$(kubectl get svc rcoder -n "$NAMESPACE" -o jsonpath='{.spec.type}' 2>/dev/null)
echo ""
echo -e "${GREEN}=========================================="
echo " 部署完成!"
echo "==========================================${NC}"
echo ""
echo -e "访问方式:"
if [ "$SVC_TYPE" = "NodePort" ]; then
NODE_PORT=$(kubectl get svc rcoder -n "$NAMESPACE" -o jsonpath='{.spec.ports[0].nodePort}')
echo -e " ${GREEN}http://${NODE_IP}:${NODE_PORT}/health${NC}"
echo -e " ${GREEN}http://${NODE_IP}:${NODE_PORT}/chat${NC}"
elif [ "$SVC_TYPE" = "LoadBalancer" ]; then
EXT_IP=$(kubectl get svc rcoder -n "$NAMESPACE" -o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null || echo "<pending>")
echo -e " ${GREEN}http://${EXT_IP}:8087/health${NC}"
else
echo -e " ${GREEN}kubectl port-forward svc/rcoder 8087:8087 -n $NAMESPACE${NC}"
echo -e " 然后访问: ${GREEN}http://localhost:8087/health${NC}"
fi
echo ""
echo -e "Kustomize 管理命令:"
echo -e " ${YELLOW}kubectl apply -k $KUSTOMIZE_DIR${NC} # 部署/更新"
echo -e " ${YELLOW}kubectl delete -k $KUSTOMIZE_DIR${NC} # 删除"
echo -e " ${YELLOW}kubectl get all -n $NAMESPACE${NC} # 查看状态"
echo ""
echo -e "Longhorn 控制台:"
echo -e " ${YELLOW}kubectl port-forward svc/longhorn-frontend 8080:80 -n longhorn-system${NC}"
echo -e " 访问: ${GREEN}http://localhost:8080${NC}"
echo ""

View File

@@ -0,0 +1,617 @@
# RCoder K8s 部署方案 —— 架构详解
> 本文档详细说明 `rcoder` 项目在 Kubernetes 上的完整部署架构。
> 代码仓库: `/home/swufe/gitworkspace/rcoder/k8s/`
---
## 1. 整体架构
```
┌──────────────────────────────────────────────────────────────────────────────┐
│ Kubernetes 集群 │
│ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ Ingress Controller (可选) │ │
│ │ className 可配置 (nginx/traefik/alb) │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ rcoder Service (NodePort 30080) │ │
│ │ Deployment / Pod (rcoder 主服务) │ │
│ │ │ │
│ │ • REST API :8087 (health / chat / agent 管理) │ │
│ │ • Pingora :8088 (内部反向代理,给动态 agent-runner Pod 用) │ │
│ │ │ │
│ │ 动态创建 Agent Runner Pod ←── K8s API (不挂 docker.sock) │ │
│ └──────────────────────────────┬───────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ JuiceFS StorageClass (RWX, 跨节点共享) │ │
│ │ juicefs-sc-{release} │ │
│ │ │ │
│ │ PVC: rcoder-workspace (50Gi) │ │
│ │ ├── /app/project_workspace ← rcoder 主服务读写 │ │
│ │ └── /app/computer-project-workspace ← computer agent runner 用 │ │
│ └───────────────────────────────┬──────────────────────────────────────┘ │
└──────────────────────────────────┼──────────────────────────────────────────┘
┌───────────────────┼────────────────────┐
│ │ │
▼ ▼ ▼
┌──────────────────────────┐ ┌────────────────┐ ┌─────────────────────────────┐
│ PostgreSQL │ │ MinIO │ │ JuiceFS CSI Driver │
│ StatefulSet (RWO) │ │ StatefulSet │ │ (kube-system) │
│ │ │ (RWO) │ │ │
│ • JuiceFS 元数据存储 │ │ │ │ • csi-provisioner │
│ • DB: juicefs │ │ bucket: juicefs│ │ • csi-node-driver-registrar │
│ • Longhorn / local-path │ │ │ │ • juicefs-plugin (DaemonSet) │
│ • 10Gi PVC │ │ Longhorn / │ │ │
│ │ │ local-path │ │ │
└──────────────────────────┘ │ 30Gi PVC │ └─────────────────────────────┘
└────────────────┘
▲ │
│ │
└───────────────────────┬─────────────────┘
┌────────▼────────┐
│ Longhorn / │
│ local-path │
│ (块存储 RWO) │
└─────────────────┘
```
---
## 2. 存储架构详解
### 2.1 存储分层
| 层次 | 存储类型 | 访问模式 | 使用场景 | 典型大小 |
|------|---------|---------|---------|---------|
| **共享文件系统** | JuiceFS (CSI) | RWX 跨节点 | rcoder workspace, project_workspace, computer_workspace | 50Gi |
| **对象存储** | MinIO (S3) | RWO 单节点 | JuiceFS 数据块后端 + 应用文件存储 | 30Gi+ |
| **元数据库** | PostgreSQL | RWO 单节点 | JuiceFS 文件元数据 (inode/权限/目录结构) | 10Gi |
| **块存储** | Longhorn / local-path | RWO 单节点 | PostgreSQL 数据盘, MinIO 数据盘 | 按需 |
### 2.2 JuiceFS 数据流
```
用户/应用 Pod ──写入──▶ JuiceFS FUSE Mount (CSI Driver)
PostgreSQL (元数据)
• 文件名 / inode 编号
• 权限 / 所有者
• 目录结构
• 硬链接 / 符号链接
MinIO S3 (数据块)
• chunk-xxx 文件
• juicefs.db 内部索引
```
**为什么这样分层:**
- **JuiceFS = 共享层**:提供 POSIX RWX多节点 rcoder Pod 和动态创建的 agent-runner Pod 可以同时读写同一份项目文件
- **PostgreSQL = 元数据**:文件系统"目录结构/文件名/权限"存这里JuiceFS mount pod 重启不丢文件索引
- **MinIO = 数据块后端**:文件内容 chunk 存在 MinIO bucket不存在节点本地JuiceFS mount pod 重启不丢数据
- **Longhorn/local-path = 数据库盘**MySQL/Redis/Milvus/ES 不能用 JuiceFS延迟太高强制走块存储
### 2.3 Longhorn vs local-path
| 维度 | local-path (k3s 内置) | Longhorn |
|------|----------------------|---------|
| 多节点副本 | ❌ 单节点独占 | ✅ 3 副本同步分布不同节点 |
| 扩容 | ❌ 手动迁数据 | ✅ UI/CLI 秒扩 |
| 快照/回滚 | ❌ | ✅ |
| 备份到 S3 | ❌ | ✅ |
| UI 管理 | ❌ | ✅ (:9000) |
| 资源开销 | ~0 | ~500MB agent/node |
| 适用场景 | 单节点 dev | 多节点 prod |
**扩展策略**
- 当前 `values.yaml``storageClass: local-path`dev够用
- 未来多节点集群装好 Longhorn 后,只需把 `storageClass` 改为 `longhorn`,所有 StatefulSet 的 PVC 自动用 Longhorn
### 2.4 JuiceFS Secret 关键配置
```yaml
# juicefs-secret (渲染后)
name: "rcoder-juicefs"
metaurl: "postgres://juicefs:<pass>@postgresql.<ns>:5432/juicefs"
storage: "minio"
bucket: "http://minio-service.<ns>:9000/juicefs"
access-key: "<minio-root-user>"
secret-key: "<minio-root-password>"
```
---
## 3. 核心组件
### 3.1 rcoder 主服务 (Deployment)
**关键环境变量**
| 环境变量 | 值 | 作用 |
|---------|---|------|
| `CONTAINER_RUNTIME` | `kubernetes` | 激活 K8s API 模式,创建动态 Pod 而非 Docker 容器 |
| `RCODER_K8S_NAMESPACE` | `{{ .Release.Namespace }}` | rcoder 通过此 namespace 创建 agent-runner Pod |
| `RCODER_K8S_STORAGE_CLASS` | `juicefs-sc-{{ .Release.Name }}` | 动态 Pod 挂 JuiceFS PVC 用的 SC 名 |
| `RCODER_DOCKER_IMAGE` | `rcoder-k8s:latest` | agent-runner Pod 的基础镜像 |
| `RCODER_DOCKER_IMAGE_COMPUTER` | `rcoder-computer-agent-runner:latest` | computer agent runner 镜像 |
**挂载的卷**
| 卷名 | 来源 | 挂载路径 | 用途 |
|------|------|---------|------|
| rcoder-config | ConfigMap | `/app/config.yml` | rcoder 运行时配置 |
| rcoder-workspace | JuiceFS PVC (RWX) | `/app/project_workspace` (subPath: `project_workspace`) | 项目工作区 |
| rcoder-workspace | JuiceFS PVC (RWX) | `/app/computer-project-workspace` (subPath: `computer_workspace`) | Computer agent 工作区 |
**权限模型**
- ServiceAccount: `rcoder-pods-sa`
- ClusterRole: `rcoder-pods-clusterrole` (集群级,多 release 共享)
- `pods`: create / delete / get / list / watch / patch / update
- `pods/log`: get / list
- `pods/exec`: create
- `pods/status`: get
- `persistentvolumeclaims`: get / list / watch / create / delete
### 3.2 动态 Agent Runner Pod
rcoder 主服务通过 K8s API 动态创建的临时 Pod
```
rcoder 主服务 (Deployment)
├── 收到 /chat 请求
├── 调用 K8s API 创建 Pod:
│ • name: rcoder-agent-runner-<session-id>
│ • image: rcoder-computer-agent-runner
│ • env: RCODER_K8S_NAMESPACE / RCODER_DOCKER_IMAGE_* 等
│ • volumes:
│ - project_workspace (JuiceFS RWX, subPath)
│ - computer_workspace (JuiceFS RWX, subPath)
│ - emptyDir cache (可重建)
└── Pod 内 gRPC 服务rcoder 主服务通过 serviceDNS:50051 连接
```
**与 docker-compose 模式的区别**
| 维度 | docker-compose (Docker) | K8s |
|------|------------------------|-----|
| 容器创建 | docker CLI / Docker API | K8s API |
| 通信方式 | Docker 内部网络 + 端口映射 | K8s ClusterIP Service DNS |
| 工作空间 | hostPath bind-mount | JuiceFS RWX PVC |
| 清理 | docker stop + rm | K8s Pod delete |
| socket | /var/run/docker.sock | 不需要 |
### 3.3 PostgreSQL (StatefulSet)
- **用途**JuiceFS 元数据存储
- **数据库**`juicefs` (JuiceFS 自动建)
- **用户**`juicefs` (来自 credentials Secret)
- **存储**Longhorn / local-path 块存储 PVC (RWO)
- **initContainers**: 清理 `lost+found` 目录避免 PG 初始化失败
### 3.4 MinIO (StatefulSet)
- **用途**JuiceFS S3 数据块后端
- **bucket**`juicefs` (由 minio-init Job 创建)
- **凭据**:来自 `rcoder-credentials` Secret (`MINIO_ROOT_USER` / `MINIO_ROOT_PASSWORD`)
- **存储**Longhorn / local-path 块存储 PVC (RWO)
- **健康检查**
- Liveness: `/minio/health/live`
- Readiness: `/minio/health/ready`
### 3.5 minio-init Job
- **触发时机**`helm.sh/hook: post-install,post-upgrade`
- **作用**:确保 `juicefs` bucket 存在
- **策略**`helm.sh/hook-delete-policy: before-hook-creation` 每次 upgrade 重新运行
- **镜像**`minio/mc` (MinIO Client)
---
## 4. 部署方式
### 4.1 三种部署方式对比
| 方式 | 入口 | 适用场景 | 维护者 |
|------|------|---------|--------|
| **Kustomize** | `deploy-dev.sh` / `deploy-prod.sh` | 日常开发 / 在线环境快速迭代 | 开发团队 |
| **Helm** | `helm install ... k8s/helm/rcoder` | 参数化部署 / 对外交付 | 交付/运维 |
| **Offline Bundle** | `make k8s-offline-bundle``install.sh` | 政企客户完全断网 | 交付 |
### 4.2 Kustomize 部署流程
```
deploy-dev.sh 顺序执行:
[1/6] 检查 K8s 集群 + kubectl
[1.5/6] 检查/安装 open-iscsi (Longhorn 依赖)
[2/6] 部署 Longhorn (若未安装)
│ └── kubectl apply -f https://raw.githubusercontent.com/longhorn/longhorn/master/deploy/longhorn.yaml
[3/6] 部署 JuiceFS CSI Driver (若未安装)
│ └── helm install juicefs-csi-driver juicedata/juicefs-csi-driver --namespace kube-system
[4/6] Kustomize 部署应用 (manifests/overlays/dev)
│ └── kubectl apply -k manifests/overlays/dev
│ 顺序:
│ 1. namespace.yaml
│ 2. storage/ (postgresql → minio → minio-init → juicefs-secret → juicefs-sc → juicefs-pvc)
│ 3. rcoder/ (SA → ClusterRole/CRB → ConfigMap → Deployment → Service → NetworkPolicy → PDB)
[5/6] 验证部署状态
[6/6] 输出访问信息
```
### 4.3 Helm 部署
```bash
# dev
helm install rcoder-dev k8s/helm/rcoder \
--namespace nuwax-rcoder-dev --create-namespace \
-f k8s/helm/rcoder/values-dev.yaml
# prod (密码通过 --set 注入)
helm install rcoder-prod k8s/helm/rcoder \
--namespace nuwax-rcoder-prod --create-namespace \
-f k8s/helm/rcoder/values-prod.yaml \
--set credentials.postgresql.password=<real> \
--set credentials.minio.rootPassword=<real>
# 同集群 Kustomize + Helm 并存 (Helm 接管 ClusterRole)
helm install rcoder-helm k8s/helm/rcoder \
--set rcoder.clusterRole.create=false
```
### 4.4 离线部署
**构建 (有网机器)**
```bash
make k8s-offline-bundle
# 产出: dist/rcoder-offline-<ver>-<arch>.tar.gz (~1.7-2GB, 27个镜像)
```
**安装 (客户内网, 两种模式)**
| 模式 | 镜像导入方式 | 适用场景 |
|------|------------|---------|
| `--mode=direct` | `ctr image import` 到节点 containerd | 单节点 / 小集群, 无私有 registry |
| `--mode=registry` | re-tag + push 到客户 Harbor/Nexus/ACR | 多节点, 已有私有 registry |
**跳过选项**
| 参数 | 何时使用 |
|------|---------|
| `--skip-longhorn` | 集群已有 Ceph/NFS/CSI-local-path |
| `--skip-juicefs-csi` | 集群已有 JuiceFS CSI 或用其他 RWX 方案 |
| `--skip-image-import` | 镜像已手工导入完成 |
---
## 5. 多环境共存
同一集群可同时跑 dev / test / prod通过 release 名字 + namespace 天然隔离:
| 资源 | dev (nuwax-rcoder-dev) | prod (nuwax-rcoder-prod) |
|------|----------------------|--------------------------|
| Namespace | `nuwax-rcoder-dev` | `nuwax-rcoder-prod` |
| StorageClass | `juicefs-sc-dev` | `juicefs-sc-prod` |
| ClusterRoleBinding | `rcoder-dev-pods-crb` | `rcoder-prod-pods-crb` |
| ClusterRole | `rcoder-pods-clusterrole` (共享, `helm.sh/resource-policy: keep`) | 同 |
| PostgreSQL | `postgresql` StatefulSet | 同名 (不同 namespace) |
| MinIO | `minio` StatefulSet | 同名 (不同 namespace) |
| rcoder Deployment | `rcoder` Deployment | 同名 (不同 namespace) |
| NodePort | 30080 | 30081 |
---
## 6. 镜像清单
**离线包共 27 个镜像,分 5 类**
### 6.1 RCoder 自有镜像
| 镜像 | 用途 | 架构 |
|------|------|------|
| `rcoder` | rcoder 主服务 (docker-compose 用, 不进 K8s) | amd64 + arm64 |
| `rcoder-k8s` | rcoder K8s 变体 (`CARGO_FEATURES=kubernetes`) | amd64 + arm64 |
| `rcoder-agent-runner` | 动态 agent-runner 基础镜像 | amd64 + arm64 |
### 6.2 存储层
| 镜像 | 用途 | 版本 |
|------|------|------|
| `postgres:16-alpine` | JuiceFS 元数据 | 16-alpine |
| `minio/minio` | S3 对象存储 + JuiceFS bucket | RELEASE.2024-12-18T13-15-44Z |
| `minio/mc` | MinIO Client (init Job 用) | RELEASE.2024-11-21T17-21-54Z |
| `busybox:1.36` | init container / DNS check | 1.36 |
### 6.3 JuiceFS CE
| 镜像 | 用途 |
|------|------|
| `juicedata/mount:ce-v1.3.1` | JuiceFS FUSE Mount Pod 镜像 |
| `juicedata/juicefs-csi-driver:v0.31.3` | CSI Driver 主镜像 |
| `juicedata/csi-dashboard:v0.31.3` | CSI Dashboard |
### 6.4 JuiceFS CSI Sidecars
| 镜像 | 版本 |
|------|------|
| `registry.k8s.io/sig-storage/csi-node-driver-registrar:v2.9.0` |
| `registry.k8s.io/sig-storage/csi-provisioner:v3.6.0` |
| `registry.k8s.io/sig-storage/csi-resizer:v1.9.0` |
| `registry.k8s.io/sig-storage/livenessprobe:v2.11.0` |
### 6.5 Longhorn v1.7.2
| 镜像 | 用途 |
|------|------|
| `longhornio/longhorn-manager:v1.7.2` | Longhorn 控制平面 |
| `longhornio/longhorn-engine:v1.7.2` | 存储引擎 |
| `longhornio/longhorn-ui:v1.7.2` | Web UI |
| `longhornio/longhorn-instance-manager:v1.7.2` | 实例管理 |
| `longhornio/longhorn-share-manager:v1.7.2` | NFS / iSCSI 共享 |
| `longhornio/backing-image-manager:v1.7.2` | 镜像管理 |
| `longhornio/longhorn-cli:v1.7.2` | CLI |
| `longhornio/support-bundle-kit:v0.0.45` | 诊断工具 |
| `longhornio/csi-attacher:v4.7.0` | CSI attacher |
| `longhornio/csi-provisioner:v4.0.1-20241007` | CSI provisioner |
| `longhornio/csi-resizer:v1.12.0` | CSI resizer |
| `longhornio/csi-snapshotter:v7.0.2-20241007` | CSI snapshotter |
| `longhornio/csi-node-driver-registrar:v2.12.0` | CSI node driver |
| `longhornio/livenessprobe:v2.14.0` | Liveness probe |
---
## 7. 文件结构
```
k8s/
├── docs/
│ └── ARCHITECTURE.md ← 本文档
├── README.md ← 总览 + 快速开始
├── nuwax-platform-k8s-plan.md ← 迁移计划 (build-agent-docker 视角)
├── deploy-dev.sh ← Kustomize dev 部署 (一键)
├── deploy-prod.sh ← Kustomize prod 部署 (含密码守卫)
├── undeploy.sh ← 清理脚本
├── helm/ ← Helm chart (对外交付 + 离线源)
│ └── rcoder/
│ ├── Chart.yaml
│ ├── .helmignore
│ ├── values.yaml ← 默认值 (dev + prod 共享)
│ ├── values-dev.yaml ← dev 覆盖 (NodePort 30080, local-path)
│ ├── values-prod.yaml ← prod 覆盖 (密码 CHANGE-ME)
│ ├── values-offline.yaml ← 离线 registry 覆盖
│ └── templates/
│ ├── _helpers.tpl ← 镜像拼装/SC名/ClusterRole名 helpers
│ ├── NOTES.txt
│ ├── storage/
│ │ ├── credentials-secret.yaml
│ │ ├── juicefs-secret.yaml
│ │ ├── juicefs-storageclass.yaml
│ │ ├── juicefs-pvc.yaml ← RWX PVC (JuiceFS SC)
│ │ ├── postgresql-statefulset.yaml
│ │ ├── postgresql-service.yaml
│ │ ├── minio-statefulset.yaml
│ │ ├── minio-service.yaml
│ │ └── minio-init-job.yaml ← 创建 juicefs bucket
│ └── rcoder/
│ ├── clusterrole.yaml ← 集群级, helm.sh/resource-policy: keep
│ ├── clusterrolebinding.yaml
│ ├── serviceaccount.yaml
│ ├── deployment.yaml ← CONTAINER_RUNTIME=kubernetes
│ ├── service.yaml
│ ├── configmap.yaml ← config.yml
│ ├── networkpolicy.yaml
│ └── pdb.yaml
├── manifests/ ← Kustomize (日常开发用)
│ ├── base/
│ │ ├── kustomization.yaml
│ │ ├── namespace.yaml
│ │ ├── storage/
│ │ │ ├── kustomization.yaml
│ │ │ ├── juicefs-pvc.yaml
│ │ │ ├── postgresql-deployment.yaml
│ │ │ ├── minio-deployment.yaml
│ │ │ └── minio-init-job.yaml
│ │ └── rcoder/
│ │ ├── kustomization.yaml
│ │ ├── rcoder-deployment.yaml
│ │ ├── rcoder-service.yaml
│ │ ├── rcoder-configmap.yaml
│ │ ├── rcoder-networkpolicy.yaml
│ │ ├── rcoder-pdb.yaml
│ │ └── serviceaccount.yaml
│ ├── overlays/
│ │ ├── dev/
│ │ │ ├── kustomization.yaml
│ │ │ ├── clusterrolebinding.yaml
│ │ │ ├── credentials.yaml
│ │ │ ├── juicefs-secret.yaml
│ │ │ ├── juicefs-storageclass.yaml
│ │ │ └── rcoder-configmap.yaml
│ │ └── prod/
│ │ ├── kustomization.yaml
│ │ ├── clusterrolebinding.yaml
│ │ ├── credentials.yaml
│ │ ├── juicefs-secret.yaml
│ │ ├── juicefs-storageclass.yaml
│ │ └── rcoder-configmap.yaml
│ ├── _deprecated/
│ │ └── nfs/
│ │ ├── nfs-server.yaml
│ │ └── nfs-subdir-provisioner.yaml
│ └── JUICEFS_DEPLOYMENT.md ← JuiceFS CSI 手动部署指南
├── offline/ ← 离线部署工具
│ ├── images.txt ← 27 个镜像清单 (版本 pin)
│ ├── install.sh ← 主安装脚本 (direct / registry 双模式)
│ ├── rewrite-registry.sh ← registry 模式 re-tag + push
│ └── README.md ← 客户交付手册
├── register2/ ← 本地私有镜像仓库 (可选)
│ ├── docker-compose.yml ← registry:2 + 数据卷
│ ├── registry-config.yml
│ └── README.md
└── scripts/
├── deploy-juicefs.sh ← 仅部署 JuiceFS CSI (不装 Longhorn)
├── test-chat.sh ← 冒烟测试脚本
├── install-k3s-registry-mirrors-cn.sh ← K3s 镜像加速配置
└── k3s-registries-cn.yaml ← 镜像加速配置模板
```
---
## 8. 关键设计决策
### 8.1 rcoder-k8s 独立镜像
**方式 A采用**:新增 `rcoder-k8s` 独立镜像 tag
- `Dockerfile``ARG CARGO_FEATURES`
- `make build-rcoder-k8s``CARGO_FEATURES=kubernetes`
- 不挂 docker.sock通过 K8s API 创建动态 agent-runner Pod
- docker-compose 用的 `rcoder` 镜像不受影响
**未采用方式 B**:让 `rcoder` 镜像始终含 kubernetes feature
- docker-compose 老用户也会带上 K8s 代码(轻微浪费)
### 8.2 集群级资源命名策略
| 资源类型 | 命名策略 | 理由 |
|---------|---------|------|
| StorageClass | `juicefs-sc-{release}` | 每个 release 独立,避免多环境冲突 |
| ClusterRole | `rcoder-pods-clusterrole` (固定) | 规则相同,共享最简;`helm.sh/resource-policy: keep` 防误删 |
| ClusterRoleBinding | `{release}-pods-crb` | release 独立 |
### 8.3 密码管理
| 环境 | 策略 |
|------|------|
| dev | 明文默认值(`CHANGE-ME`),开箱即用 |
| prod | `CHANGE-ME-BEFORE-DEPLOY` 占位符,`deploy-prod.sh` 守卫拒绝部署 |
| 进阶 | 推荐 `--set` / SealedSecret / ExternalSecret 注入 |
### 8.4 JuiceFS vs 其他 RWX 方案
| 方案 | 优势 | 劣势 |
|------|------|------|
| JuiceFS + MinIO + PG | K8s 原生,跨节点 POSIXCSI 集成 | 需要 MinIO + PG 两个有状态组件 |
| NFS Server | 简单 | 单点,无副本,无 CSI 原生集成 |
| CephFS | 成熟,副本分布 | 需要 Ceph 集群,运维复杂 |
| Longhorn NFS | Longhorn 一个组件搞定 | Longhorn NFS 共享盘性能一般 |
|阿里云 NAS/EFS | 云上托管 | 绑定云厂商 |
选择 JuiceFS 的核心原因RCoder 的核心价值是跨 agent-runner Pod 共享项目文件JuiceFS 提供 POSIX RWX + K8s CSI 原生支持,是最轻量的自建方案。
---
## 9. 环境要求
| 组件 | 版本要求 | 说明 |
|------|---------|------|
| Kubernetes | 1.19+ | 已在 K3s 1.34.x 测试通过 |
| kubectl | 与集群匹配 | `kubectl cluster-info` 能通 |
| helm | 3.x | 用于 Helm 部署路径 |
| docker | 最新 | 仅 `make k8s-offline-bundle` 或 push 到 register2 时需要 |
| k3s / nerdctl / ctr | 最新 | 仅 direct 模式离线部署需要 |
| open-iscsi | 任意 | Longhorn 依赖 (`apt install open-iscsi`) |
---
## 10. 运维常用命令
```bash
# 查看所有资源
kubectl get all -n nuwax-rcoder-dev
# 查看 Pod 日志
kubectl logs -n nuwax-rcoder-dev -l app=rcoder --tail=200 -f
# 查看 JuiceFS 挂载
kubectl exec -n nuwax-rcoder-dev deploy/rcoder -- df -h | grep juicefs
# 进入 Pod 调试
kubectl exec -it -n nuwax-rcoder-dev deploy/rcoder -- sh
# 查看 PVC 状态
kubectl get pvc -n nuwax-rcoder-dev
# 查看 StorageClass
kubectl get sc | grep -E "longhorn|juicefs"
# 重启 rcoder deployment
kubectl rollout restart deploy/rcoder -n nuwax-rcoder-dev
kubectl rollout status deploy/rcoder -n nuwax-rcoder-dev
# Longhorn UI
kubectl port-forward svc/longhorn-frontend 8080:80 -n longhorn-system
# 清理 dev 环境
ENV=dev ./undeploy.sh
```
---
## 11. 故障排查
### Pod 一直 Pending
```bash
kubectl describe pod -n nuwax-rcoder-dev <pod-name>
# 常见原因:
# - ImagePullBackOff: 节点 containerd 没有镜像 (direct 模式漏了某节点)
# - ContainerCreating: Longhorn / JuiceFS CSI 未就绪
```
### JuiceFS 挂载失败
```bash
# CSI Driver 日志
kubectl logs -n kube-system -l app=juicefs-csi-driver-node --tail=50
# 检查 PostgreSQL / MinIO 是否就绪
kubectl get pods -n nuwax-rcoder-dev -l app=postgresql
kubectl get pods -n nuwax-rcoder-dev -l app=minio
# JuiceFS mount pod 是否 Running
kubectl get pods -n nuwax-rcoder-dev | grep juicefs
```
### Longhorn 无法启动
```bash
# 检查 open-iscsi
ssh <node> "sudo apt install -y open-iscsi && sudo systemctl enable --now iscsid"
kubectl delete pod -n longhorn-system -l app=longhorn-manager --force
```
### rcoder 健康检查失败
```bash
# 查看 rcoder 日志 (RUST_LOG=info)
kubectl logs -n nuwax-rcoder-dev -l app=rcoder --tail=100
# 验证 K8s API 连通性 (rcoder pod 内)
kubectl exec -it -n nuwax-rcoder-dev deploy/rcoder -- sh
kubectl auth can-i create pods -n nuwax-rcoder-dev --as=system:serviceaccount:nuwax-rcoder-dev:rcoder-pods-sa
```

View File

@@ -0,0 +1,15 @@
# Patterns to ignore when building packages.
.DS_Store
.git/
.gitignore
.vscode/
.idea/
*.tmproj
*.bak
*.swp
*.orig
*~
# OWNERS file
OWNERS
# Helm
.helmignore

View File

@@ -0,0 +1,18 @@
apiVersion: v2
name: rcoder
description: RCoder AI 驱动开发平台 (ACP-based, Rust) —— 含 RCoder 主服务 + PostgreSQL + MinIO + JuiceFS StorageClass
type: application
version: 1.0.0
appVersion: "latest"
kubeVersion: ">=1.19.0-0"
keywords:
- rcoder
- acp
- ai
- agent
- juicefs
- rust
maintainers:
- name: RCoder Team
annotations:
category: AI

View File

@@ -0,0 +1,39 @@
RCoder {{ .Chart.AppVersion }} 已安装到 namespace "{{ .Release.Namespace }}" (release: {{ .Release.Name }}).
环境: {{ .Values.environment }}
副本数: {{ .Values.rcoder.replicas }}
NodePort: {{ .Values.rcoder.service.nodePort }}
StorageClass: {{ include "rcoder.storageClassName" . }}
1. 查看部署状态:
kubectl get pods -n {{ .Release.Namespace }}
kubectl get svc -n {{ .Release.Namespace }}
kubectl get pvc -n {{ .Release.Namespace }}
2. 等待就绪(首次部署 PostgreSQL + MinIO + JuiceFS mount 约需 2-5 分钟):
kubectl rollout status deploy/rcoder -n {{ .Release.Namespace }} --timeout=300s
3. 访问服务 (NodePort):
{{- if eq .Values.rcoder.service.type "NodePort" }}
NODE_IP=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}')
curl http://${NODE_IP}:{{ .Values.rcoder.service.nodePort }}/health
{{- else }}
kubectl port-forward svc/rcoder {{ .Values.rcoder.service.port }}:{{ .Values.rcoder.service.port }} -n {{ .Release.Namespace }}
curl http://localhost:{{ .Values.rcoder.service.port }}/health
{{- end }}
4. 查看日志:
kubectl logs -n {{ .Release.Namespace }} -l app=rcoder -f
{{- if eq .Values.environment "prod" }}
⚠️ 生产环境安全提醒:
- credentials.postgresql.password / credentials.minio.rootPassword 不应使用默认值
- 建议使用 --set 动态注入或 SealedSecret / ExternalSecret 方案
- 当前密码: {{ .Values.credentials.postgresql.password }} (如为 CHANGE-ME-BEFORE-DEPLOY 请立即替换)
{{- end }}
升级: helm upgrade {{ .Release.Name }} k8s/helm/rcoder -f <values>
卸载: helm uninstall {{ .Release.Name }} --namespace {{ .Release.Namespace }}
注意StorageClass 与 ClusterRoleBinding 是集群级资源,会随 release 删除;
PostgreSQL / MinIO 的 PVC 由 reclaimPolicy 决定是否保留数据)

View File

@@ -0,0 +1,140 @@
{{/*
RCoder chart helper
*/}}
{{/* (Release + chart) */}}
{{- define "rcoder.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- if contains $name .Release.Name -}}
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{/* Chart */}}
{{- define "rcoder.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/* () */}}
{{- define "rcoder.labels" -}}
helm.sh/chart: {{ include "rcoder.chart" . }}
app.kubernetes.io/name: rcoder
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
environment: {{ .Values.environment | quote }}
{{- end -}}
{{/* selector label (app=rcoder Kustomize , Service pod) */}}
{{- define "rcoder.selectorLabels" -}}
app: rcoder
component: rcoder-main
{{- end -}}
{{/* ============================================================
rcoder-own image -> {{ .Values.global.imageRegistry }}/{repo}:{tag}
third-party image -> {{ .Values.global.thirdPartyRegistry }}/{repo}:{tag}
( thirdPartyRegistry , repo)
============================================================ */}}
{{/* : dict {repository, tag, chartContext} */}}
{{- define "rcoder.ownImage" -}}
{{- $repo := .repository -}}
{{- $tag := .tag -}}
{{- $registry := .ctx.Values.global.imageRegistry -}}
{{- if empty $tag -}}{{- $tag = .ctx.Chart.AppVersion -}}{{- end -}}
{{- if $registry -}}
{{- printf "%s/%s:%s" $registry $repo $tag -}}
{{- else -}}
{{- printf "%s:%s" $repo $tag -}}
{{- end -}}
{{- end -}}
{{/* : dict {repository, tag, ctx} */}}
{{- define "rcoder.thirdPartyImage" -}}
{{- $repo := .repository -}}
{{- $tag := .tag -}}
{{- $registry := .ctx.Values.global.thirdPartyRegistry -}}
{{- if $registry -}}
{{- printf "%s/%s:%s" $registry $repo $tag -}}
{{- else -}}
{{- printf "%s:%s" $repo $tag -}}
{{- end -}}
{{- end -}}
{{/* : rcoder image */}}
{{- define "rcoder.image" -}}
{{- include "rcoder.ownImage" (dict "repository" .Values.rcoder.image.repository "tag" .Values.rcoder.image.tag "ctx" .) -}}
{{- end -}}
{{/* : agent-runner image ( rcoder env var 使) */}}
{{- define "rcoder.agentRunnerImage" -}}
{{- include "rcoder.ownImage" (dict "repository" .Values.agentRunner.image.repository "tag" .Values.agentRunner.image.tag "ctx" .) -}}
{{- end -}}
{{/* (config.yml docker_config.multi_image_config.global_defaults.registry_prefix 使) */}}
{{- define "rcoder.registryPrefix" -}}
{{- default "" .Values.global.imageRegistry -}}
{{- end -}}
{{/* ============================================================
StorageClass (, Release )
- juicefs.storageClass.name ->
- 退 "juicefs-sc-{release}"
============================================================ */}}
{{- define "rcoder.storageClassName" -}}
{{- if .Values.juicefs.storageClass.name -}}
{{- .Values.juicefs.storageClass.name -}}
{{- else -}}
{{- printf "juicefs-sc-%s" .Release.Name -}}
{{- end -}}
{{- end -}}
{{/* ============================================================
ClusterRoleBinding (, )
============================================================ */}}
{{- define "rcoder.clusterRoleBindingName" -}}
{{- printf "%s-pods-crb" .Release.Name -}}
{{- end -}}
{{/* ClusterRole release ClusterRole, */}}
{{- define "rcoder.clusterRoleName" -}}
rcoder-pods-clusterrole
{{- end -}}
{{/* imagePullSecrets */}}
{{- define "rcoder.imagePullSecrets" -}}
{{- if .Values.global.imagePullSecrets }}
imagePullSecrets:
{{- range .Values.global.imagePullSecrets }}
- name: {{ .name }}
{{- end }}
{{- end -}}
{{- end -}}
{{/* PostgreSQL service (namespace ) */}}
{{- define "rcoder.postgresqlHost" -}}
{{- printf "postgresql.%s.svc.cluster.local" .Release.Namespace -}}
{{- end -}}
{{/* MinIO service */}}
{{- define "rcoder.minioHost" -}}
{{- printf "minio-service.%s.svc.cluster.local" .Release.Namespace -}}
{{- end -}}
{{/* JuiceFS metaurl */}}
{{- define "rcoder.juicefsMetaurl" -}}
{{- printf "postgres://%s:%s@postgresql.%s:5432/juicefs" .Values.credentials.postgresql.user .Values.credentials.postgresql.password .Release.Namespace -}}
{{- end -}}
{{/* JuiceFS bucket URL */}}
{{- define "rcoder.juicefsBucketUrl" -}}
{{- printf "http://minio-service.%s:9000/%s" .Release.Namespace .Values.juicefs.bucket -}}
{{- end -}}

View File

@@ -0,0 +1,29 @@
{{- if .Values.rcoder.clusterRole.create }}
# 集群级 ClusterRole —— 多 release 共享同一个 (规则与 namespace 无关, 重复 apply 幂等)
# 与 Kustomize 并存时, 把 .Values.rcoder.clusterRole.create 置 false, 让 Kustomize 持有所有权
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ include "rcoder.clusterRoleName" . }}
labels:
{{- include "rcoder.labels" . | nindent 4 }}
annotations:
# 避免多 release 互相 helm uninstall 时删除共享 ClusterRole
"helm.sh/resource-policy": keep
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["create", "delete", "get", "list", "watch", "patch", "update"]
- apiGroups: [""]
resources: ["pods/log"]
verbs: ["get", "list"]
- apiGroups: [""]
resources: ["pods/exec"]
verbs: ["create"]
- apiGroups: [""]
resources: ["pods/status"]
verbs: ["get"]
- apiGroups: [""]
resources: ["persistentvolumeclaims"]
verbs: ["get", "list", "watch", "create", "delete"]
{{- end }}

View File

@@ -0,0 +1,15 @@
# 集群级 ClusterRoleBinding —— 每个 release 独立, 解决同集群多 release 冲突
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: {{ include "rcoder.clusterRoleBindingName" . }}
labels:
{{- include "rcoder.labels" . | nindent 4 }}
subjects:
- kind: ServiceAccount
name: rcoder-pods-sa
namespace: {{ .Release.Namespace }}
roleRef:
kind: ClusterRole
name: {{ include "rcoder.clusterRoleName" . }}
apiGroup: rbac.authorization.k8s.io

View File

@@ -0,0 +1,70 @@
# ConfigMap: 渲染 config.yml —— 保持与 k8s/manifests/overlays 下 rcoder-configmap.yaml 结构对齐
apiVersion: v1
kind: ConfigMap
metadata:
name: rcoder-config
namespace: {{ .Release.Namespace }}
labels:
{{- include "rcoder.labels" . | nindent 4 }}
data:
config.yml: |
default_agent: {{ .Values.config.defaultAgent | quote }}
projects_dir: {{ .Values.config.projectsDir | quote }}
port: {{ .Values.config.port }}
cleanup_config:
enabled: {{ .Values.config.cleanup.enabled }}
idle_timeout_seconds: {{ .Values.config.cleanup.idleTimeoutSeconds }}
cleanup_interval_seconds: {{ .Values.config.cleanup.cleanupIntervalSeconds }}
docker_stop_timeout_seconds: {{ .Values.config.cleanup.dockerStopTimeoutSeconds }}
proxy_config:
listen_port: {{ .Values.config.proxy.listenPort }}
default_backend_port: {{ .Values.config.proxy.defaultBackendPort }}
backend_host: {{ .Values.config.proxy.backendHost | quote }}
port_param: {{ .Values.config.proxy.portParam | quote }}
health_check:
enabled: {{ .Values.config.proxy.healthCheck.enabled }}
interval_seconds: {{ .Values.config.proxy.healthCheck.intervalSeconds }}
timeout_seconds: {{ .Values.config.proxy.healthCheck.timeoutSeconds }}
healthy_threshold: {{ .Values.config.proxy.healthCheck.healthyThreshold }}
unhealthy_threshold: {{ .Values.config.proxy.healthCheck.unhealthyThreshold }}
docker_config:
multi_image_config:
global_defaults:
registry_prefix: {{ include "rcoder.registryPrefix" . | quote }}
services:
rcoder:
service_type: "RCoder"
arm64_image: {{ include "rcoder.image" . | quote }}
amd64_image: {{ include "rcoder.image" . | quote }}
default_image: {{ include "rcoder.image" . | quote }}
image_tag_prefix: "rcoder"
enabled: true
computer-agent-runner:
service_type: "ComputerAgentRunner"
arm64_image: {{ include "rcoder.agentRunnerImage" . | quote }}
amd64_image: {{ include "rcoder.agentRunnerImage" . | quote }}
default_image: {{ include "rcoder.agentRunnerImage" . | quote }}
image_tag_prefix: "rcoder-computer-agent-runner"
enabled: true
selection_strategy: "ServiceOnly"
cache_config:
enabled: {{ .Values.config.dockerConfig.cache.enabled }}
ttl_seconds: {{ .Values.config.dockerConfig.cache.ttlSeconds }}
max_entries: {{ .Values.config.dockerConfig.cache.maxEntries }}
network_base_name: {{ .Values.config.dockerConfig.networkBaseName | quote }}
network_mode: {{ .Values.config.dockerConfig.networkMode | quote }}
work_dir: {{ .Values.config.dockerConfig.workDir | quote }}
auto_cleanup: {{ .Values.config.dockerConfig.autoCleanup }}
container_ttl_seconds: {{ .Values.config.dockerConfig.containerTtlSeconds }}
api_key_auth:
enabled: {{ .Values.config.apiKeyAuth.enabled }}
api_key: {{ .Values.config.apiKeyAuth.apiKey | quote }}
kubernetes_config:
storage_class: {{ include "rcoder.storageClassName" . | quote }}

View File

@@ -0,0 +1,65 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: rcoder
namespace: {{ .Release.Namespace }}
labels:
{{- include "rcoder.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.rcoder.replicas }}
selector:
matchLabels:
{{- include "rcoder.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "rcoder.labels" . | nindent 8 }}
{{- include "rcoder.selectorLabels" . | nindent 8 }}
spec:
terminationGracePeriodSeconds: {{ .Values.rcoder.terminationGracePeriodSeconds }}
serviceAccountName: rcoder-pods-sa
{{- include "rcoder.imagePullSecrets" . | nindent 6 }}
containers:
- name: rcoder
image: {{ include "rcoder.image" . }}
imagePullPolicy: {{ .Values.global.imagePullPolicy }}
command: ["/app/bin/rcoder"]
args: ["--port", "{{ .Values.rcoder.service.port }}"]
ports:
- containerPort: {{ .Values.rcoder.service.port }}
env:
- name: CONTAINER_RUNTIME
value: "kubernetes"
- name: RCODER_K8S_NAMESPACE
value: {{ .Release.Namespace | quote }}
- name: RUST_LOG
value: {{ .Values.rcoder.rustLog | quote }}
- name: RCODER_K8S_STORAGE_CLASS
value: {{ include "rcoder.storageClassName" . | quote }}
- name: RCODER_DOCKER_IMAGE
value: {{ include "rcoder.image" . | quote }}
- name: RCODER_DOCKER_IMAGE_COMPUTER
value: {{ include "rcoder.agentRunnerImage" . | quote }}
resources:
{{- toYaml .Values.rcoder.resources | nindent 12 }}
livenessProbe:
{{- toYaml .Values.rcoder.livenessProbe | nindent 12 }}
readinessProbe:
{{- toYaml .Values.rcoder.readinessProbe | nindent 12 }}
volumeMounts:
- name: rcoder-config
mountPath: /app/config.yml
subPath: config.yml
- name: project-workspace
mountPath: /app/project_workspace
subPath: project_workspace
- name: project-workspace
mountPath: /app/computer-project-workspace
subPath: computer_workspace
volumes:
- name: rcoder-config
configMap:
name: rcoder-config
- name: project-workspace
persistentVolumeClaim:
claimName: rcoder-workspace

View File

@@ -0,0 +1,62 @@
{{- if .Values.rcoder.networkPolicy.enabled }}
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: rcoder-network-policy
namespace: {{ .Release.Namespace }}
labels:
{{- include "rcoder.labels" . | nindent 4 }}
spec:
podSelector:
matchLabels:
app: rcoder
policyTypes:
- Ingress
- Egress
ingress:
# 同 namespace 入站流量
- from:
- podSelector: {}
ports:
- { protocol: TCP, port: {{ .Values.rcoder.service.port }} }
- { protocol: TCP, port: {{ .Values.config.proxy.listenPort }} }
egress:
# DNS
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- { protocol: UDP, port: 53 }
# K8s API Server
- to:
- namespaceSelector: {}
podSelector:
matchLabels:
kubernetes.io/service-name: kubernetes
ports:
- { protocol: TCP, port: 443 }
# PostgreSQL
- to:
- podSelector:
matchLabels:
app: postgresql
ports:
- { protocol: TCP, port: {{ .Values.postgresql.service.port }} }
# MinIO
- to:
- podSelector:
matchLabels:
app: minio
ports:
- { protocol: TCP, port: {{ .Values.minio.service.apiPort }} }
- { protocol: TCP, port: {{ .Values.minio.service.consolePort }} }
# 动态 agent pod (rcoder -> agent gRPC/HTTP)
- to:
- podSelector:
matchLabels:
managed-by: rcoder-runtime
ports:
- { protocol: TCP, port: 50051 }
- { protocol: TCP, port: 8086 }
{{- end }}

View File

@@ -0,0 +1,14 @@
{{- if .Values.rcoder.pdb.enabled }}
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: rcoder-pdb
namespace: {{ .Release.Namespace }}
labels:
{{- include "rcoder.labels" . | nindent 4 }}
spec:
maxUnavailable: {{ .Values.rcoder.pdb.maxUnavailable }}
selector:
matchLabels:
app: rcoder
{{- end }}

View File

@@ -0,0 +1,19 @@
apiVersion: v1
kind: Service
metadata:
name: rcoder
namespace: {{ .Release.Namespace }}
labels:
{{- include "rcoder.labels" . | nindent 4 }}
spec:
type: {{ .Values.rcoder.service.type }}
# component=rcoder-main 把动态创建的 rcoder-agent / computer-agent-runner pod
# (它们也带 app=rcoder 标签) 排除在外, 避免 NodePort 把流量路由到不监听 8087 的 agent pod
selector:
{{- include "rcoder.selectorLabels" . | nindent 4 }}
ports:
- port: {{ .Values.rcoder.service.port }}
targetPort: {{ .Values.rcoder.service.port }}
{{- if eq .Values.rcoder.service.type "NodePort" }}
nodePort: {{ .Values.rcoder.service.nodePort }}
{{- end }}

View File

@@ -0,0 +1,7 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: rcoder-pods-sa
namespace: {{ .Release.Namespace }}
labels:
{{- include "rcoder.labels" . | nindent 4 }}

View File

@@ -0,0 +1,13 @@
apiVersion: v1
kind: Secret
metadata:
name: rcoder-credentials
namespace: {{ .Release.Namespace }}
labels:
{{- include "rcoder.labels" . | nindent 4 }}
type: Opaque
stringData:
POSTGRES_USER: {{ .Values.credentials.postgresql.user | quote }}
POSTGRES_PASSWORD: {{ .Values.credentials.postgresql.password | quote }}
MINIO_ROOT_USER: {{ .Values.credentials.minio.rootUser | quote }}
MINIO_ROOT_PASSWORD: {{ .Values.credentials.minio.rootPassword | quote }}

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: rcoder-workspace
namespace: {{ .Release.Namespace }}
labels:
app: rcoder
{{- include "rcoder.labels" . | nindent 4 }}
spec:
# JuiceFS 原生支持 RWX, 多节点 rcoder pod 共享工作空间
accessModes: ["ReadWriteMany"]
storageClassName: {{ include "rcoder.storageClassName" . | quote }}
resources:
requests:
storage: {{ .Values.workspace.size | quote }}

View File

@@ -0,0 +1,26 @@
apiVersion: v1
kind: Secret
metadata:
name: juicefs-secret
namespace: {{ .Release.Namespace }}
labels:
{{- include "rcoder.labels" . | nindent 4 }}
type: Opaque
stringData:
# JuiceFS CE 必需字段 (小写键名)
name: "rcoder-juicefs"
metaurl: {{ include "rcoder.juicefsMetaurl" . | quote }}
storage: "minio"
bucket: {{ include "rcoder.juicefsBucketUrl" . | quote }}
access-key: {{ .Values.credentials.minio.rootUser | quote }}
secret-key: {{ .Values.credentials.minio.rootPassword | quote }}
# 兼容字段 (保留给脚本/日志使用)
METAURL: {{ include "rcoder.juicefsMetaurl" . | quote }}
MINIO_ACCESS_KEY: {{ .Values.credentials.minio.rootUser | quote }}
MINIO_SECRET_KEY: {{ .Values.credentials.minio.rootPassword | quote }}
MINIO_ADDRESS: {{ printf "minio-service.%s:%d" .Release.Namespace (int .Values.minio.service.apiPort) | quote }}
# 其他配置
APPEND_FSYNC: {{ .Values.juicefs.meta.appendFsync | quote }}
CACHE_SIZE: {{ .Values.juicefs.meta.cacheSize | quote }}

View File

@@ -0,0 +1,25 @@
{{- if .Values.juicefs.storageClass.create }}
# JuiceFS StorageClass —— 集群级资源, 每个 release 独立命名
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: {{ include "rcoder.storageClassName" . }}
labels:
{{- include "rcoder.labels" . | nindent 4 }}
annotations:
storageclass.kubernetes.io/is-default-class: "false"
provisioner: csi.juicefs.com
parameters:
csi.storage.k8s.io/provisioner-secret-name: juicefs-secret
csi.storage.k8s.io/provisioner-secret-namespace: {{ .Release.Namespace }}
csi.storage.k8s.io/controller-expand-secret-name: juicefs-secret
csi.storage.k8s.io/controller-expand-secret-namespace: {{ .Release.Namespace }}
csi.storage.k8s.io/node-publish-secret-name: juicefs-secret
csi.storage.k8s.io/node-publish-secret-namespace: {{ .Release.Namespace }}
juicefs/controllerQuotaSet: "false"
juicefs/mountImage: {{ include "rcoder.thirdPartyImage" (dict "repository" .Values.juicefs.mountImage.repository "tag" .Values.juicefs.mountImage.tag "ctx" .) | quote }}
juicefs/mountOptions: {{ .Values.juicefs.storageClass.mountOptions | quote }}
reclaimPolicy: {{ .Values.juicefs.storageClass.reclaimPolicy }}
allowVolumeExpansion: true
volumeBindingMode: Immediate
{{- end }}

View File

@@ -0,0 +1,47 @@
apiVersion: batch/v1
kind: Job
metadata:
name: minio-init
namespace: {{ .Release.Namespace }}
labels:
{{- include "rcoder.labels" . | nindent 4 }}
annotations:
# 每次 helm upgrade 强制重新运行 (确保 bucket 一定存在, 即使 Job 已清理)
helm.sh/hook: post-install,post-upgrade
helm.sh/hook-weight: "5"
helm.sh/hook-delete-policy: before-hook-creation
spec:
ttlSecondsAfterFinished: 300
template:
spec:
restartPolicy: OnFailure
{{- include "rcoder.imagePullSecrets" . | nindent 6 }}
containers:
- name: mc
image: {{ include "rcoder.thirdPartyImage" (dict "repository" .Values.minio.initImage.repository "tag" .Values.minio.initImage.tag "ctx" .) }}
imagePullPolicy: {{ .Values.global.imagePullPolicy }}
command:
- /bin/sh
- -c
- |
echo "Waiting for MinIO to be ready..."
until mc alias set myminio http://minio:{{ .Values.minio.service.apiPort }} ${MINIO_ROOT_USER} ${MINIO_ROOT_PASSWORD}; do
echo "MinIO not ready, retrying..."
sleep 2
done
echo "MinIO is ready, creating bucket..."
mc mb myminio/{{ .Values.minio.bucket }} --ignore-existing
mc ls myminio/
echo "MinIO initialization completed!"
env:
- name: MINIO_ROOT_USER
valueFrom:
secretKeyRef:
name: rcoder-credentials
key: MINIO_ROOT_USER
- name: MINIO_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: rcoder-credentials
key: MINIO_ROOT_PASSWORD

View File

@@ -0,0 +1,41 @@
---
# Headless Service for StatefulSet
apiVersion: v1
kind: Service
metadata:
name: minio
namespace: {{ .Release.Namespace }}
labels:
app: minio
{{- include "rcoder.labels" . | nindent 4 }}
spec:
clusterIP: None
selector:
app: minio
ports:
- name: api
port: {{ .Values.minio.service.apiPort }}
targetPort: {{ .Values.minio.service.apiPort }}
- name: console
port: {{ .Values.minio.service.consolePort }}
targetPort: {{ .Values.minio.service.consolePort }}
---
# ClusterIP Service for internal access
apiVersion: v1
kind: Service
metadata:
name: minio-service
namespace: {{ .Release.Namespace }}
labels:
app: minio
{{- include "rcoder.labels" . | nindent 4 }}
spec:
selector:
app: minio
ports:
- name: api
port: {{ .Values.minio.service.apiPort }}
targetPort: {{ .Values.minio.service.apiPort }}
- name: console
port: {{ .Values.minio.service.consolePort }}
targetPort: {{ .Values.minio.service.consolePort }}

View File

@@ -0,0 +1,83 @@
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: minio-pvc
namespace: {{ .Release.Namespace }}
labels:
app: minio
{{- include "rcoder.labels" . | nindent 4 }}
spec:
accessModes: ["ReadWriteOnce"]
{{- if .Values.minio.storage.storageClass }}
storageClassName: {{ .Values.minio.storage.storageClass | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.minio.storage.size | quote }}
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: minio
namespace: {{ .Release.Namespace }}
labels:
app: minio
{{- include "rcoder.labels" . | nindent 4 }}
spec:
serviceName: minio
replicas: 1
selector:
matchLabels:
app: minio
template:
metadata:
labels:
app: minio
{{- include "rcoder.labels" . | nindent 8 }}
spec:
{{- include "rcoder.imagePullSecrets" . | nindent 6 }}
containers:
- name: minio
image: {{ include "rcoder.thirdPartyImage" (dict "repository" .Values.minio.image.repository "tag" .Values.minio.image.tag "ctx" .) }}
imagePullPolicy: {{ .Values.global.imagePullPolicy }}
ports:
- containerPort: {{ .Values.minio.service.apiPort }}
name: api
- containerPort: {{ .Values.minio.service.consolePort }}
name: console
env:
- name: MINIO_ROOT_USER
valueFrom:
secretKeyRef:
name: rcoder-credentials
key: MINIO_ROOT_USER
- name: MINIO_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: rcoder-credentials
key: MINIO_ROOT_PASSWORD
args:
- server
- /data
- --console-address
- ":{{ .Values.minio.service.consolePort }}"
livenessProbe:
httpGet:
path: /minio/health/live
port: {{ .Values.minio.service.apiPort }}
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /minio/health/ready
port: {{ .Values.minio.service.apiPort }}
initialDelaySeconds: 5
periodSeconds: 5
volumeMounts:
- name: minio-data
mountPath: /data
volumes:
- name: minio-data
persistentVolumeClaim:
claimName: minio-pvc

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: postgresql
namespace: {{ .Release.Namespace }}
labels:
app: postgresql
{{- include "rcoder.labels" . | nindent 4 }}
spec:
selector:
app: postgresql
ports:
- name: postgres
port: {{ .Values.postgresql.service.port }}
targetPort: {{ .Values.postgresql.service.port }}

View File

@@ -0,0 +1,84 @@
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgresql-pvc
namespace: {{ .Release.Namespace }}
labels:
app: postgresql
{{- include "rcoder.labels" . | nindent 4 }}
spec:
accessModes: ["ReadWriteOnce"]
{{- if .Values.postgresql.storage.storageClass }}
storageClassName: {{ .Values.postgresql.storage.storageClass | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.postgresql.storage.size | quote }}
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgresql
namespace: {{ .Release.Namespace }}
labels:
app: postgresql
{{- include "rcoder.labels" . | nindent 4 }}
spec:
serviceName: postgresql
replicas: 1
selector:
matchLabels:
app: postgresql
template:
metadata:
labels:
app: postgresql
{{- include "rcoder.labels" . | nindent 8 }}
spec:
{{- include "rcoder.imagePullSecrets" . | nindent 6 }}
initContainers:
- name: cleanup
image: {{ include "rcoder.thirdPartyImage" (dict "repository" .Values.postgresql.image.repository "tag" .Values.postgresql.image.tag "ctx" .) }}
imagePullPolicy: {{ .Values.global.imagePullPolicy }}
command: ["rm", "-rf", "/var/lib/postgresql/data/lost+found"]
volumeMounts:
- name: postgres-data
mountPath: /var/lib/postgresql/data
containers:
- name: postgresql
image: {{ include "rcoder.thirdPartyImage" (dict "repository" .Values.postgresql.image.repository "tag" .Values.postgresql.image.tag "ctx" .) }}
imagePullPolicy: {{ .Values.global.imagePullPolicy }}
ports:
- containerPort: {{ .Values.postgresql.service.port }}
name: postgres
env:
- name: POSTGRES_DB
value: "juicefs"
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: rcoder-credentials
key: POSTGRES_USER
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: rcoder-credentials
key: POSTGRES_PASSWORD
livenessProbe:
exec:
command: ["pg_isready", "-U", {{ .Values.credentials.postgresql.user | quote }}]
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
exec:
command: ["pg_isready", "-U", {{ .Values.credentials.postgresql.user | quote }}]
initialDelaySeconds: 5
periodSeconds: 5
volumeMounts:
- name: postgres-data
mountPath: /var/lib/postgresql/data
volumes:
- name: postgres-data
persistentVolumeClaim:
claimName: postgresql-pvc

View File

@@ -0,0 +1,25 @@
# ============================================================================
# Dev 环境覆盖
# 使用: helm install rcoder-dev k8s/helm/rcoder -f k8s/helm/rcoder/values-dev.yaml \
# --namespace nuwax-rcoder-dev --create-namespace
# ============================================================================
environment: dev
rcoder:
replicas: 1
service:
nodePort: 30080
juicefs:
storageClass:
name: juicefs-sc-dev
# Dev 默认值 —— 方便本地联调,生产部署前必须改
credentials:
postgresql:
user: juicefs
password: juicefs_password
minio:
rootUser: minioadmin
rootPassword: minioadmin

View File

@@ -0,0 +1,31 @@
# ============================================================================
# 离线部署覆盖
#
# 使用场景:政企内网,无公网访问,所有镜像已 re-tag 推送到客户私有 registry。
#
# 如果走 "ctr image import 到每个节点" 模式,节点 containerd 里会以原始 repo:tag
# 保存镜像,此时不需要改 global.imageRegistry —— 保留默认值即可,配合
# imagePullPolicy: IfNotPresent 直接用本地缓存。
#
# 如果走 "re-tag + push 到私有 registry" 模式,把下面两个字段覆盖为实际地址:
#
# global:
# imageRegistry: harbor.internal/rcoder # RCoder 自有镜像仓库
# thirdPartyRegistry: harbor.internal/thirdparty # 第三方镜像 (postgres/minio/juicedata)
#
# 使用:
# helm install rcoder k8s/helm/rcoder \
# -f k8s/helm/rcoder/values-dev.yaml \
# -f k8s/helm/rcoder/values-offline.yaml \
# --set global.imageRegistry=harbor.internal/rcoder \
# --set global.thirdPartyRegistry=harbor.internal/thirdparty
# ============================================================================
global:
# 示例值, 客户按需改
imageRegistry: harbor.internal/rcoder
thirdPartyRegistry: harbor.internal/thirdparty
imagePullPolicy: IfNotPresent
# 如果私有 registry 需要认证,在集群里创建 Secret 后填:
# imagePullSecrets: [{name: harbor-pull-secret}]
imagePullSecrets: []

View File

@@ -0,0 +1,30 @@
# ============================================================================
# Prod 环境覆盖
#
# ⚠️ 上线前必须替换 credentials.* 里的 CHANGE-ME-BEFORE-DEPLOY 占位符。
# 推荐做法:不把真实密码写进这里,改用 --set 或 SealedSecret/ExternalSecret。
#
# 使用: helm install rcoder-prod k8s/helm/rcoder -f k8s/helm/rcoder/values-prod.yaml \
# --namespace nuwax-rcoder-prod --create-namespace \
# --set credentials.postgresql.password=<real> \
# --set credentials.minio.rootPassword=<real>
# ============================================================================
environment: prod
rcoder:
replicas: 2
service:
nodePort: 30081
juicefs:
storageClass:
name: juicefs-sc-prod
credentials:
postgresql:
user: juicefs
password: CHANGE-ME-BEFORE-DEPLOY
minio:
rootUser: CHANGE-ME-BEFORE-DEPLOY
rootPassword: CHANGE-ME-BEFORE-DEPLOY

View File

@@ -0,0 +1,176 @@
# ============================================================================
# RCoder Helm values - 默认(共享)配置
#
# 使用方式:
# helm install rcoder-dev k8s/helm/rcoder -f k8s/helm/rcoder/values-dev.yaml
# helm install rcoder-prod k8s/helm/rcoder -f k8s/helm/rcoder/values-prod.yaml
#
# 离线部署叠加 values-offline.yaml 覆盖 global.imageRegistry。
# ============================================================================
global:
# 所有镜像的仓库前缀。离线部署时覆盖为 harbor.internal/rcoder 等私有地址。
# 应用到: rcoder / rcoder-agent-runner / postgres / minio / mc / juicedata/mount
# 说明: 为空时使用每个镜像的原始 repo非空时会被拼到 repo 前面形成 "{registry}/{repo}"。
imageRegistry: "nuwax-docker-images-registry.cn-hangzhou.cr.aliyuncs.com/dev"
imagePullPolicy: IfNotPresent
imagePullSecrets: []
# 第三方镜像的 registry 前缀。离线/私有场景用于把 postgres / minio/minio / minio/mc /
# juicedata/mount 这些上游镜像指向客户私有仓库。为空时使用原始 repo。
thirdPartyRegistry: ""
# ----------------------------------------------------------------------------
# 应用部署
# ----------------------------------------------------------------------------
environment: dev # dev | prod —— 仅作为 label 和默认后缀
rcoder:
image:
repository: rcoder
tag: "" # 空时继承 .Chart.appVersion
replicas: 1
resources:
requests:
memory: 512Mi
cpu: 500m
limits:
memory: 2Gi
cpu: 2000m
service:
type: NodePort
port: 8087
nodePort: 30080
terminationGracePeriodSeconds: 60
rustLog: info
livenessProbe:
httpGet: { path: /health, port: 8087 }
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet: { path: /health, port: 8087 }
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 2
# NetworkPolicy 是否启用 (cluster 不支持 NetworkPolicy 时置 false)
networkPolicy:
enabled: true
# PodDisruptionBudget
pdb:
enabled: true
maxUnavailable: 0
# 是否创建共享的 ClusterRole (rcoder-pods-clusterrole)
# - true (默认): chart 拥有该 ClusterRole
# - false: 当集群里已有其他方式创建的同名 ClusterRole (如 Kustomize 并存) 时置 false
clusterRole:
create: true
# 动态 agent pod 使用的镜像 (通过 env var 注入,不会直接创建 pod)
agentRunner:
image:
repository: rcoder-agent-runner
tag: "" # 空时继承 .Chart.appVersion
# ----------------------------------------------------------------------------
# 存储层
# ----------------------------------------------------------------------------
postgresql:
image:
repository: postgres
tag: 16-alpine
storage:
size: 10Gi
# 默认 local-path与 Kustomize base 保持一致, k3s 自带)。
# Longhorn / 离线集群可按需改为 longhorn / "" (空 => 集群默认)。
storageClass: local-path
service:
port: 5432
minio:
image:
repository: minio/minio
tag: latest
initImage:
repository: minio/mc
tag: latest
storage:
size: 30Gi
storageClass: local-path
service:
apiPort: 9000
consolePort: 9001
bucket: juicefs # 需与 juicefs.bucket 对应
juicefs:
mountImage:
repository: juicedata/mount
tag: ce-v1.3.1
# StorageClass 是集群级资源; Helm release 之间必须独立命名避免冲突
storageClass:
create: true
# 留空则自动使用 "juicefs-sc-{release}"
name: ""
reclaimPolicy: Retain
mountOptions: "cache-size=1024,allow-other"
# juicefs-secret 里会使用到, 决定 mount 时的元数据连接 / 对象存储地址
meta:
appendFsync: "1"
cacheSize: "1024"
# bucket URL 指向 namespace 内的 minio-service (自动拼装, 无需修改)
bucket: juicefs
# ----------------------------------------------------------------------------
# 凭据 (Secret rcoder-credentials)
# ----------------------------------------------------------------------------
credentials:
postgresql:
user: juicefs
password: CHANGE-ME
minio:
rootUser: minioadmin
rootPassword: minioadmin
# ----------------------------------------------------------------------------
# 工作空间 PVC (ReadWriteMany via JuiceFS)
# ----------------------------------------------------------------------------
workspace:
size: 50Gi
# ----------------------------------------------------------------------------
# RCoder 应用配置 (渲染为 ConfigMap 里的 config.yml)
# ----------------------------------------------------------------------------
config:
defaultAgent: Claude
projectsDir: /app/project_workspace
port: 8087
cleanup:
enabled: true
idleTimeoutSeconds: 600
cleanupIntervalSeconds: 300
dockerStopTimeoutSeconds: 30
proxy:
listenPort: 8088
defaultBackendPort: 8087
backendHost: 127.0.0.1
portParam: port
healthCheck:
enabled: true
intervalSeconds: 5
timeoutSeconds: 1
healthyThreshold: 2
unhealthyThreshold: 3
dockerConfig:
networkBaseName: agent-network
networkMode: bridge
workDir: /app
autoCleanup: true
containerTtlSeconds: 3600
cache:
enabled: true
ttlSeconds: 3600
maxEntries: 50
apiKeyAuth:
enabled: false
apiKey: YOUR_API_KEY_HERE

View File

@@ -0,0 +1,282 @@
# JuiceFS + MinIO + PostgreSQL 部署指南
## 架构
```
┌─────────────────────────────────────────────────────────────┐
│ Kubernetes 集群 │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ JuiceFS CSI Driver │ │
│ │ (StorageClass: juicefs-sc) │ │
│ └──────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌────────────────┐ ┌────────────────┐ │
│ │ rcoder 主服务 │ │ Agent Runner │ │
│ │ Pod (Deployment)│ │ Pod (动态创建) │ │
│ └────────┬───────┘ └────────┬───────┘ │
│ │ │ │
│ └──────────┬───────────┘ │
│ ↓ │
│ ┌──────────────┐ │
│ │ JuiceFS 卷 │ ← 共享文件系统 (RWX) │
│ └───────┬──────┘ │
└──────────────────────┼────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────┐
│ MinIO (S3) ←──── 数据存储 │
│ PostgreSQL ← 元数据存储 │
└───────────────────────────────────────────────────────────────┘
```
## 文件结构
```
manifests/
├── 00-namespace.yaml # Namespace
├── 01-storage/ # 存储层
│ ├── postgresql-deployment.yaml
│ ├── minio-deployment.yaml
│ ├── minio-init-job.yaml
│ ├── juicefs-secret.yaml
│ ├── juicefs-storageclass.yaml
│ └── juicefs-pvc.yaml
├── 02-rcoder/ # 应用层
│ ├── serviceaccount.yaml
│ ├── rcoder-configmap.yaml
│ ├── rcoder-deployment.yaml
│ ├── rcoder-service.yaml
│ ├── rcoder-networkpolicy.yaml
│ └── rcoder-pdb.yaml
└── _deprecated/nfs/ # 废弃的 NFS 配置
```
## 部署顺序
### 第一步:部署 JuiceFS CSI Driver
```bash
# 使用 Helm 部署
helm repo add juicefs https://juicefs.github.io/charts
helm repo update
helm install juicefs-csi-driver juicefs/juicefs-csi-driver \
--namespace kube-system \
--set webhook.enabled=false \
--set csiSidecarImage.registry=docker.io \
--set csiSidecarImage.repository=juicedata/csi-sidecar \
--set csiSidecarImage.tag=v1.3.8 \
--set image.registry=docker.io \
--set image.repository=juicedata/juicefs-csi-driver \
--set image.tag=v1.1.0 \
--set nodeDaemonImage.registry=docker.io \
--set nodeDaemonImage.repository=juicedata/juicefs-csi-driver \
--set nodeDaemonImage.tag=v1.1.0
```
验证:
```bash
kubectl get pods -n kube-system | grep juicefs
# 预期juicefs-csi-driver-xxx 运行正常
```
---
### 第二步:部署 PostgreSQL
```bash
kubectl apply -f manifests/01-storage/postgresql-deployment.yaml
```
验证:
```bash
kubectl get pods -n nuwax-rcoder | grep postgresql
kubectl logs -n nuwax-rcoder -l app=postgresql --tail=50
```
---
### 第三步:部署 MinIO
```bash
kubectl apply -f manifests/01-storage/minio-deployment.yaml
```
验证:
```bash
kubectl get pods -n nuwax-rcoder | grep minio
# 访问 MinIO Console: http://<minio-ip>:9001
```
---
### 第四步:初始化 MinIO Bucket
```bash
kubectl apply -f manifests/01-storage/minio-init-job.yaml
# 等待完成
kubectl wait --for=condition=complete job/minio-init -n nuwax-rcoder --timeout=120s
# 验证
kubectl exec -it -n nuwax-rcoder deploy/minio -- mc ls myminio/
```
---
### 第五步:创建 JuiceFS Secret
```bash
kubectl apply -f manifests/01-storage/juicefs-secret.yaml
```
---
### 第六步:创建 JuiceFS StorageClass
```bash
kubectl apply -f manifests/01-storage/juicefs-storageclass.yaml
```
验证:
```bash
kubectl get sc | grep juicefs
```
---
### 第七步:部署 RCoder 相关资源
```bash
# 部署 rcoder PVC (使用 JuiceFS)
kubectl apply -f manifests/01-storage/juicefs-pvc.yaml
# 验证 PVC
kubectl get pvc -n nuwax-rcoder
# 状态应为 Bound
# 部署其他 rcoder 资源
kubectl apply -f manifests/00-namespace.yaml
kubectl apply -f manifests/02-rcoder/serviceaccount.yaml
kubectl apply -f manifests/02-rcoder/rcoder-configmap.yaml
kubectl apply -f manifests/02-rcoder/rcoder-deployment.yaml
kubectl apply -f manifests/02-rcoder/rcoder-service.yaml
kubectl apply -f manifests/02-rcoder/rcoder-networkpolicy.yaml
kubectl apply -f manifests/02-rcoder/rcoder-pdb.yaml
```
---
### 第八步:验证整个系统
```bash
# 检查所有 Pod
kubectl get pods -n nuwax-rcoder
# 查看 JuiceFS 挂载情况
kubectl exec -it -n nuwax-rcoder deploy/rcoder -- df -h | grep juicefs
# 测试写入
kubectl exec -it -n nuwax-rcoder deploy/rcoder -- sh -c "echo 'test' > /app/project_workspace/test.txt"
# 查看日志
kubectl logs -n nuwax-rcoder -l app=rcoder --tail=50
```
---
## 存储结构
部署后,数据存储结构如下:
```
PostgreSQL (元数据):
├── juicefs database
│ ├── inodes
│ ├── chunks
│ ├── del-files
│ └── sessions
MinIO (S3 数据):
├── juicefs bucket
│ └── juicefs/
│ ├── juicefs.db ← JuiceFS 内部数据库
│ └── chunk-xxx ← 数据块
```
---
## 卸载顺序
```bash
# 1. 删除 rcoder 资源
kubectl delete -f manifests/02-rcoder/rcoder-deployment.yaml
kubectl delete -f manifests/02-rcoder/rcoder-configmap.yaml
kubectl delete -f manifests/01-storage/juicefs-pvc.yaml
# 2. 删除 JuiceFS 相关
kubectl delete -f manifests/01-storage/juicefs-storageclass.yaml
kubectl delete -f manifests/01-storage/juicefs-secret.yaml
# 3. 删除存储层
kubectl delete -f manifests/01-storage/minio-deployment.yaml
kubectl delete -f manifests/01-storage/postgresql-deployment.yaml
# 4. 删除 JuiceFS CSI Driver (可选)
helm uninstall juicefs-csi-driver -n kube-system
```
---
## 常见问题
### 1. PVC 一直 Pending
```bash
# 检查 JuiceFS CSI Driver 状态
kubectl get pods -n kube-system | grep juicefs
# 检查 StorageClass
kubectl get sc juicefs-sc -o yaml
# 检查 Secret
kubectl get secret juicefs-secret -n nuwax-rcoder -o yaml
```
### 2. MinIO Bucket 创建失败
```bash
# 确认 MinIO 运行正常
kubectl logs -n nuwax-rcoder -l app=minio --tail=20
# 手动进入 Pod 测试
kubectl exec -it -n nuwax-rcoder deploy/minio -- sh
mc alias set myminio http://localhost:9000 minioadmin minioadmin
mc mb myminio/juicefs
```
### 3. JuiceFS 挂载失败
```bash
# 查看 Node Pod 日志
kubectl get pods -n kube-system -o wide | grep juicefs
# 查看挂载详情
kubectl describe pod -n kube-system -l app=juicefs-csi-driver-node
```
---
## 资源需求
| 组件 | CPU | Memory | Storage |
|------|-----|--------|---------|
| PostgreSQL | 500m | 1Gi | 20Gi |
| MinIO | 500m | 1Gi | 100Gi+ |
| JuiceFS CSI | 100m | 128Mi | - |
总计:约 2 核 + 2.5Gi + 120Gi 存储

View File

@@ -0,0 +1,106 @@
# ============================================================
# NFS Server (内建模式)
# 集群内自建 NFS Server数据持久化到 local-path PV
#
# ⚠️ 注意: OrbStack/Mac 桌面 K8s 环境不支持此方式,请使用外部 NFS Server
#
# 如使用外部 NFS Server跳过此文件直接部署 nfs-subdir-provisioner.yaml
# ============================================================
---
apiVersion: v1
kind: Namespace
metadata:
name: nfs-storage
---
apiVersion: v1
kind: Service
metadata:
name: nfs-server
namespace: nuwax-nfs-storage
spec:
clusterIP: None
selector:
app: nfs-server
ports:
- name: nfs
port: 2049
targetPort: 2049
- name: mountd
port: 20048
targetPort: 20048
- name: rpcbind
port: 111
targetPort: 111
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: nfs-server
namespace: nuwax-nfs-storage
spec:
serviceName: nfs-server
replicas: 1
selector:
matchLabels:
app: nfs-server
template:
metadata:
labels:
app: nfs-server
spec:
# 亲和性: NFS Server 调度到有 local-path 存储的节点
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: Exists
containers:
- name: nfs-server
image: itsthenetwork/nfs-server-alpine:latest
env:
- name: SHARED_DIRECTORY
value: "/exports"
- name: SYNC
value: "true"
ports:
- name: nfs
containerPort: 2049
protocol: TCP
- name: mountd
containerPort: 20048
protocol: TCP
- name: rpcbind
containerPort: 111
protocol: TCP
securityContext:
runAsUser: 0
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "512Mi"
volumeMounts:
- name: nfs-data
mountPath: /exports
volumes:
- name: nfs-data
persistentVolumeClaim:
claimName: nfs-server-pvc
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nfs-server-pvc
namespace: nuwax-nfs-storage
labels:
app: nfs-server
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: local-path
resources:
requests:
storage: 100Gi

View File

@@ -0,0 +1,135 @@
# ============================================================
# NFS Subdir External Provisioner + StorageClass
#
# 两种使用方式:
# 模式一(内建 NFS): nfs-server.yaml 部署后,使用此文件
# 模式二(外部 NFS): 直接修改 NFS_SERVER/NFS_PATH 环境变量指向外部 NFS
#
# 参考: https://github.com/kubernetes-sigs/nfs-subdir-external-provisioner
# ============================================================
---
apiVersion: v1
kind: Namespace
metadata:
name: nfs-storage
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: nfs-client-provisioner
namespace: nuwax-nfs-storage
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: nfs-client-provisioner-cluster-runner
rules:
- apiGroups: [""]
resources: ["nodes"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["persistentvolumes"]
verbs: ["get", "list", "watch", "create", "delete"]
- apiGroups: [""]
resources: ["persistentvolumeclaims"]
verbs: ["get", "list", "watch", "update"]
- apiGroups: ["storage.k8s.io"]
resources: ["storageclasses"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["events"]
verbs: ["create", "update", "patch"]
- apiGroups: ["storage.k8s.io"]
resources: ["csinodeinfos"]
verbs: ["get", "list", "watch"]
- apiGroups: ["coordination.k8s.io"]
resources: ["leases"]
verbs: ["get", "create", "update"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: nfs-client-provisioner-cluster-runner-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: nfs-client-provisioner-cluster-runner
subjects:
- kind: ServiceAccount
name: nfs-client-provisioner
namespace: nuwax-nfs-storage
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nfs-client-provisioner
namespace: nuwax-nfs-storage
spec:
replicas: 1
selector:
matchLabels:
app: nfs-client-provisioner
template:
metadata:
labels:
app: nfs-client-provisioner
spec:
serviceAccountName: nfs-client-provisioner
containers:
- name: nfs-client-provisioner
image: registry.k8s.io/sig-storage/nfs-subdir-external-provisioner:v4.0.2
volumeMounts:
- name: nfs-provisioner
mountPath: /persistentvolumes
env:
- name: PROVISIONER_NAME
value: rcoder-nfs
# ⚠️ 修改此处指向外部 NFS Server
# 内建模式: nfs-server.nfs-storage.svc.cluster.local (nfs-server.yaml 部署后)
# 外部模式: 替换为客户提供的 NFS 地址,如 192.168.1.100
- name: NFS_SERVER
value: nfs-server.nfs-storage.svc.cluster.local
- name: NFS_PATH
value: /exports
- name: NFS_MOUNT_OPTIONS
value: "vers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2"
- name: ON_DELETE_POLICY
value: "delete"
- name: ARCHIVE_ON_DELETE
value: "false"
resources:
requests:
cpu: "50m"
memory: "64Mi"
limits:
cpu: "200m"
memory: "256Mi"
volumes:
- name: nfs-provisioner
nfs:
# ⚠️ 修改此处指向外部 NFS Server
server: nfs-server.nfs-storage.svc.cluster.local
path: /exports
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: rcoder-nfs
annotations:
storageclass.kubernetes.io/is-default-class: "false"
provisioner: rcoder-nfs
parameters:
pathPattern: "${.PVC.namespace}/${.PVC.name}"
onDelete: "delete"
archiveOnDelete: "false"
mountOptions:
- vers=4.1
- rsize=1048576
- wsize=1048576
- hard
- timeo=600
- retrans=2
- noresvport
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true

View File

@@ -0,0 +1,10 @@
# Kustomize base - 基础配置
# 使用方式: kubectl apply -k manifests/base
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- storage
- rcoder

View File

@@ -0,0 +1,5 @@
# Namespace for RCoder
apiVersion: v1
kind: Namespace
metadata:
name: nuwax-rcoder

View File

@@ -0,0 +1,15 @@
# RCoder application layer - 应用层配置
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
# 注意:
# - rcoder-configmap.yaml 由各环境 overlay 提供storage_class 需随环境变化)
# - ClusterRoleBinding 由各环境 overlay 提供(避免集群级命名冲突)
resources:
- serviceaccount.yaml
- rcoder-deployment.yaml
- rcoder-service.yaml
- rcoder-networkpolicy.yaml
- rcoder-pdb.yaml

View File

@@ -0,0 +1,85 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: rcoder
namespace: nuwax-rcoder
spec:
replicas: 1
selector:
matchLabels:
app: rcoder
component: rcoder-main
template:
metadata:
labels:
app: rcoder
component: rcoder-main
spec:
# Graceful shutdown: allow 60s for containers to clean up
terminationGracePeriodSeconds: 60
serviceAccountName: rcoder-pods-sa
containers:
- name: rcoder
image: nuwax-docker-images-registry.cn-hangzhou.cr.aliyuncs.com/dev/rcoder:latest
# IfNotPresent: 节点已缓存就直接用,避免每次启动都去 registry 验 token/manifest
# 缺点: latest tag 内容更新后需 kubectl rollout restart 才会拉新 image
imagePullPolicy: IfNotPresent
command: ["/app/bin/rcoder"]
args: ["--port", "8087"]
ports:
- containerPort: 8087
env:
- name: CONTAINER_RUNTIME
value: "kubernetes"
- name: RCODER_K8S_NAMESPACE
value: "nuwax-rcoder"
- name: RUST_LOG
value: "info"
# JuiceFS 存储配置 (StorageClass 名称由各环境 overlay 覆盖)
- name: RCODER_K8S_STORAGE_CLASS
value: "juicefs-sc-placeholder"
- name: RCODER_DOCKER_IMAGE
value: "nuwax-docker-images-registry.cn-hangzhou.cr.aliyuncs.com/dev/rcoder:latest"
- name: RCODER_DOCKER_IMAGE_COMPUTER
value: "nuwax-docker-images-registry.cn-hangzhou.cr.aliyuncs.com/dev/rcoder-agent-runner:latest"
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "2Gi"
cpu: "2000m"
# Health probes for K8s liveness/readiness detection
livenessProbe:
httpGet:
path: /health
port: 8087
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /health
port: 8087
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 2
volumeMounts:
- name: rcoder-config
mountPath: /app/config.yml
subPath: config.yml
- name: project-workspace
mountPath: /app/project_workspace
subPath: project_workspace
- name: project-workspace
mountPath: /app/computer-project-workspace
subPath: computer_workspace
volumes:
- name: rcoder-config
configMap:
name: rcoder-config
- name: project-workspace
persistentVolumeClaim:
claimName: rcoder-workspace

View File

@@ -0,0 +1,70 @@
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: rcoder-network-policy
namespace: nuwax-rcoder
spec:
podSelector:
matchLabels:
app: rcoder
policyTypes:
- Ingress
- Egress
ingress:
# 允许来自同 namespace 的流量
- from:
- podSelector: {}
ports:
- protocol: TCP
port: 8087
- protocol: TCP
port: 8088
egress:
# DNS 解析
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
# K8s API Server
- to:
- namespaceSelector: {}
podSelector:
matchLabels:
kubernetes.io/service-name: kubernetes
ports:
- protocol: TCP
port: 443
# PostgreSQL 访问
- to:
- podSelector:
matchLabels:
app: postgresql
ports:
- protocol: TCP
port: 5432
# MinIO 访问
- to:
- podSelector:
matchLabels:
app: minio
ports:
- protocol: TCP
port: 9000
- protocol: TCP
port: 9001
# Agent Pod gRPC 访问 (rcoder -> agent 通信)
# 用 managed-by: rcoder-runtime 标签同时匹配两类动态创建的 pod
# - service_type=rcoder (legacy /chat 创建的 rcoder-agent pod)
# - service_type=computer-agent-runner (/computer/chat 创建的 computer-agent-runner pod)
- to:
- podSelector:
matchLabels:
managed-by: rcoder-runtime
ports:
- protocol: TCP
port: 50051
- protocol: TCP
port: 8086

View File

@@ -0,0 +1,10 @@
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: rcoder-pdb
namespace: nuwax-rcoder
spec:
maxUnavailable: 0
selector:
matchLabels:
app: rcoder

View File

@@ -0,0 +1,16 @@
apiVersion: v1
kind: Service
metadata:
name: rcoder
namespace: nuwax-rcoder
spec:
type: NodePort
# 用 component=rcoder-main 把动态创建的 rcoder-agent / computer-agent-runner pod
# (它们也带 app=rcoder 标签)排除在外,避免 NodePort 把流量路由到不监听 8087 的 agent pod
selector:
app: rcoder
component: rcoder-main
ports:
# nodePort 由各环境 overlay 指定(避免同集群下 dev/prod 冲突)
- port: 8087
targetPort: 8087

View File

@@ -0,0 +1,35 @@
# ServiceAccount + ClusterRole for RCoder to manage Pods
# 注意: ClusterRoleBinding 由各环境 overlay 提供(避免集群级命名冲突)
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: rcoder-pods-sa
namespace: nuwax-rcoder
---
# ClusterRole for Pod management集群共享dev/prod 可复用同一个 ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: rcoder-pods-clusterrole
rules:
# Pod 管理
- apiGroups: [""]
resources: ["pods"]
verbs: ["create", "delete", "get", "list", "watch", "patch", "update"]
# Pod 日志
- apiGroups: [""]
resources: ["pods/log"]
verbs: ["get", "list"]
# Pod 执行命令(用于调试)
- apiGroups: [""]
resources: ["pods/exec"]
verbs: ["create"]
# Pod 状态
- apiGroups: [""]
resources: ["pods/status"]
verbs: ["get"]
# PVC 管理(动态创建/删除工作空间存储)
- apiGroups: [""]
resources: ["persistentvolumeclaims"]
verbs: ["get", "list", "watch", "create", "delete"]

View File

@@ -0,0 +1,19 @@
# ============================================================
# RCoder JuiceFS PVC
# 替代原来的 NFS PVC
# ============================================================
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: rcoder-workspace
namespace: nuwax-rcoder
labels:
app: rcoder
spec:
# ReadWriteMany: 支持 K8s 多节点同时读写 (JuiceFS 特性)
accessModes: ["ReadWriteMany"]
# storageClassName 由各环境 overlay 覆盖dev: juicefs-sc-dev, prod: juicefs-sc-prod
storageClassName: juicefs-sc-placeholder
resources:
requests:
storage: 50Gi

View File

@@ -0,0 +1,15 @@
# Storage layer - 存储层配置
# 注意:
# - juicefs-secret 由各环境 overlay 提供 (dev/prod)
# - juicefs-storageclass 由各环境 overlay 提供(集群级资源需按环境独立命名,避免冲突)
# - 不再使用 juicefs-init JobJuiceFS CSI driver 首次挂载时会用 Secret 里的
# name/metaurl/storage/bucket/access-key/secret-key 自动执行 formatJob 是冗余的
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- postgresql-deployment.yaml
- minio-deployment.yaml
- minio-init-job.yaml
- juicefs-pvc.yaml

View File

@@ -0,0 +1,112 @@
# ============================================================
# MinIO for JuiceFS Data Storage
# JuiceFS S3 兼容对象存储
# ============================================================
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: minio-pvc
namespace: nuwax-rcoder
labels:
app: minio
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: "local-path" # 单节点环境使用 local-path
resources:
requests:
storage: 30Gi # 单节点环境使用较小存储
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: minio
namespace: nuwax-rcoder
spec:
serviceName: minio
replicas: 1
selector:
matchLabels:
app: minio
template:
metadata:
labels:
app: minio
spec:
containers:
- name: minio
image: minio/minio:latest
ports:
- containerPort: 9000
name: api
- containerPort: 9001
name: console
env:
- name: MINIO_ROOT_USER
valueFrom:
secretKeyRef:
name: rcoder-credentials
key: MINIO_ROOT_USER
- name: MINIO_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: rcoder-credentials
key: MINIO_ROOT_PASSWORD
args:
- server
- /data
- --console-address
- ":9001"
livenessProbe:
httpGet:
path: /minio/health/live
port: 9000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /minio/health/ready
port: 9000
initialDelaySeconds: 5
periodSeconds: 5
volumeMounts:
- name: minio-data
mountPath: /data
volumes:
- name: minio-data
persistentVolumeClaim:
claimName: minio-pvc
---
apiVersion: v1
kind: Service
metadata:
name: minio
namespace: nuwax-rcoder
spec:
selector:
app: minio
ports:
- name: api
port: 9000
targetPort: 9000
- name: console
port: 9001
targetPort: 9001
clusterIP: None # Headless service for StatefulSet
---
# Service for K8s internal access (ClusterIP)
apiVersion: v1
kind: Service
metadata:
name: minio-service
namespace: nuwax-rcoder
spec:
selector:
app: minio
ports:
- name: api
port: 9000
targetPort: 9000
- name: console
port: 9001
targetPort: 9001

View File

@@ -0,0 +1,47 @@
# ============================================================
# MinIO Bucket 初始化 Job
# 创建 JuiceFS 所需的 bucket
# ============================================================
apiVersion: batch/v1
kind: Job
metadata:
name: minio-init
namespace: nuwax-rcoder
spec:
ttlSecondsAfterFinished: 300 # Job 完成后 5 分钟自动清理
template:
spec:
restartPolicy: OnFailure
containers:
- name: mc
image: minio/mc:latest
command:
- /bin/sh
- -c
- |
# 等待 MinIO 就绪
echo "Waiting for MinIO to be ready..."
until mc alias set myminio http://minio:9000 ${MINIO_ROOT_USER} ${MINIO_ROOT_PASSWORD}; do
echo "MinIO not ready, retrying..."
sleep 2
done
echo "MinIO is ready, creating bucket..."
# 创建 JuiceFS 专用 bucket
mc mb myminio/juicefs --ignore-existing
# 验证
mc ls myminio/
echo "MinIO initialization completed!"
env:
- name: MINIO_ROOT_USER
valueFrom:
secretKeyRef:
name: rcoder-credentials
key: MINIO_ROOT_USER
- name: MINIO_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: rcoder-credentials
key: MINIO_ROOT_PASSWORD

View File

@@ -0,0 +1,97 @@
# ============================================================
# PostgreSQL for JuiceFS Metadata
# JuiceFS 元数据存储
# ============================================================
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgresql-pvc
namespace: nuwax-rcoder
labels:
app: postgresql
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: "local-path" # 单节点环境使用 local-path
resources:
requests:
storage: 10Gi
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgresql
namespace: nuwax-rcoder
spec:
serviceName: postgresql
replicas: 1
selector:
matchLabels:
app: postgresql
template:
metadata:
labels:
app: postgresql
spec:
initContainers:
- name: cleanup
image: postgres:16-alpine
command: ["rm", "-rf", "/var/lib/postgresql/data/lost+found"]
volumeMounts:
- name: postgres-data
mountPath: /var/lib/postgresql/data
containers:
- name: postgresql
image: postgres:16-alpine
ports:
- containerPort: 5432
name: postgres
env:
- name: POSTGRES_DB
value: "juicefs"
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: rcoder-credentials
key: POSTGRES_USER
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: rcoder-credentials
key: POSTGRES_PASSWORD
livenessProbe:
exec:
command:
- pg_isready
- -U
- juicefs
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
exec:
command:
- pg_isready
- -U
- juicefs
initialDelaySeconds: 5
periodSeconds: 5
volumeMounts:
- name: postgres-data
mountPath: /var/lib/postgresql/data
volumes:
- name: postgres-data
persistentVolumeClaim:
claimName: postgresql-pvc
---
apiVersion: v1
kind: Service
metadata:
name: postgresql
namespace: nuwax-rcoder
spec:
selector:
app: postgresql
ports:
- name: postgres
port: 5432
targetPort: 5432

View File

@@ -0,0 +1,17 @@
# ============================================================
# ClusterRoleBinding - Dev 环境专用
# 把 base 里共享的 ClusterRole 绑定到 dev namespace 的 ServiceAccount
# 集群级资源独立命名,避免与 prod 冲突
# ============================================================
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: rcoder-pods-crb-dev
subjects:
- kind: ServiceAccount
name: rcoder-pods-sa
namespace: nuwax-rcoder-dev
roleRef:
kind: ClusterRole
name: rcoder-pods-clusterrole
apiGroup: rbac.authorization.k8s.io

View File

@@ -0,0 +1,16 @@
# ============================================================
# 存储层凭据 Secret - Dev 环境
# 维护 PostgreSQL / MinIO 账号密码
# 注意:同步维护 juicefs-secret.yaml 里的 metaurl / MINIO_ACCESS_KEY 等字段
# ============================================================
apiVersion: v1
kind: Secret
metadata:
name: rcoder-credentials
namespace: nuwax-rcoder-dev
type: Opaque
stringData:
POSTGRES_USER: "juicefs"
POSTGRES_PASSWORD: "juicefs_password"
MINIO_ROOT_USER: "minioadmin"
MINIO_ROOT_PASSWORD: "minioadmin"

View File

@@ -0,0 +1,28 @@
# ============================================================
# JuiceFS Secret - Dev 环境专用
# 覆盖 base 中的 juicefs-secret
# ============================================================
apiVersion: v1
kind: Secret
metadata:
name: juicefs-secret
namespace: nuwax-rcoder-dev
type: Opaque
stringData:
# JuiceFS CE 必需字段(小写键名)
name: "rcoder-juicefs"
metaurl: "postgres://juicefs:juicefs_password@postgresql.nuwax-rcoder-dev:5432/juicefs"
storage: "minio"
bucket: "http://minio-service.nuwax-rcoder-dev:9000/juicefs"
access-key: "minioadmin"
secret-key: "minioadmin"
# 兼容字段(保留给现有脚本/日志使用)
METAURL: "postgres://juicefs:juicefs_password@postgresql.nuwax-rcoder-dev:5432/juicefs"
MINIO_ACCESS_KEY: "minioadmin"
MINIO_SECRET_KEY: "minioadmin"
MINIO_ADDRESS: "minio-service.nuwax-rcoder-dev:9000"
# 其他配置
APPEND_FSYNC: "1"
CACHE_SIZE: "1024"

View File

@@ -0,0 +1,24 @@
# ============================================================
# JuiceFS StorageClass - Dev 环境专用
# 集群级资源独立命名,避免与 prod 冲突
# ============================================================
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: juicefs-sc-dev
annotations:
storageclass.kubernetes.io/is-default-class: "false"
provisioner: csi.juicefs.com
parameters:
csi.storage.k8s.io/provisioner-secret-name: juicefs-secret
csi.storage.k8s.io/provisioner-secret-namespace: nuwax-rcoder-dev
csi.storage.k8s.io/controller-expand-secret-name: juicefs-secret
csi.storage.k8s.io/controller-expand-secret-namespace: nuwax-rcoder-dev
csi.storage.k8s.io/node-publish-secret-name: juicefs-secret
csi.storage.k8s.io/node-publish-secret-namespace: nuwax-rcoder-dev
juicefs/controllerQuotaSet: "false"
juicefs/mountImage: "juicedata/mount:ce-v1.3.1"
juicefs/mountOptions: "cache-size=1024,allow-other"
reclaimPolicy: Retain
allowVolumeExpansion: true
volumeBindingMode: Immediate

View File

@@ -0,0 +1,90 @@
# Dev environment overlay
# 使用方式: kubectl apply -k manifests/overlays/dev
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: nuwax-rcoder-dev
resources:
- ../../base
# dev 独立的集群级资源(避免与 prod 冲突)
- juicefs-storageclass.yaml
- clusterrolebinding.yaml
# dev 独立的 namespace 内资源
- rcoder-configmap.yaml
- credentials.yaml
- juicefs-secret.yaml
# 开发环境标签
# - includeSelectors 必须保持 false否则 environment label 会被注入到
# NetworkPolicy 的 egress podSelector误伤 K8s API Server 和动态创建的 Agent Runner Pod。
# - includeTemplates: true 让 Pod 模板也带上 environment label方便 kubectl 按环境筛选。
labels:
- pairs:
environment: dev
includeSelectors: false
includeTemplates: true
# 开发环境副本数
replicas:
- name: rcoder
count: 1
# 开发环境镜像 tag
images:
- name: nuwax-docker-images-registry.cn-hangzhou.cr.aliyuncs.com/dev/rcoder
newName: nuwax-docker-images-registry.cn-hangzhou.cr.aliyuncs.com/nuwax-test/rcoder-k8s
newTag: latest
- name: nuwax-docker-images-registry.cn-hangzhou.cr.aliyuncs.com/dev/rcoder-agent-runner
newName: nuwax-docker-images-registry.cn-hangzhou.cr.aliyuncs.com/nuwax-test/rcoder-computer-agent-runner
newTag: latest
patches:
# NodePort: 30080dev 专用)
- patch: |-
apiVersion: v1
kind: Service
metadata:
name: rcoder
spec:
ports:
- port: 8087
targetPort: 8087
nodePort: 30080
target:
kind: Service
name: rcoder
# PVC 指向 dev 专属 StorageClass
- patch: |-
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: rcoder-workspace
spec:
storageClassName: juicefs-sc-dev
target:
kind: PersistentVolumeClaim
name: rcoder-workspace
# rcoder deployment 指向 dev 环境的 namespace 和 StorageClass
- patch: |-
apiVersion: apps/v1
kind: Deployment
metadata:
name: rcoder
spec:
template:
spec:
containers:
- name: rcoder
env:
- name: RCODER_K8S_NAMESPACE
value: nuwax-rcoder-dev
- name: RCODER_K8S_STORAGE_CLASS
value: juicefs-sc-dev
target:
kind: Deployment
name: rcoder

View File

@@ -0,0 +1,71 @@
# ============================================================
# RCoder ConfigMap - Dev 环境专用
# storage_class 指向 juicefs-sc-dev
# ============================================================
apiVersion: v1
kind: ConfigMap
metadata:
name: rcoder-config
data:
config.yml: |
# RCoder 配置文件
default_agent: "Claude"
projects_dir: "/app/project_workspace"
port: 8087
cleanup_config:
enabled: true
idle_timeout_seconds: 600
cleanup_interval_seconds: 300
docker_stop_timeout_seconds: 30
proxy_config:
listen_port: 8088
default_backend_port: 8087
backend_host: "127.0.0.1"
port_param: "port"
health_check:
enabled: true
interval_seconds: 5
timeout_seconds: 1
healthy_threshold: 2
unhealthy_threshold: 3
docker_config:
multi_image_config:
global_defaults:
registry_prefix: "nuwax-docker-images-registry.cn-hangzhou.cr.aliyuncs.com/dev"
services:
rcoder:
service_type: "RCoder"
arm64_image: "nuwax-docker-images-registry.cn-hangzhou.cr.aliyuncs.com/dev/rcoder:latest"
amd64_image: "nuwax-docker-images-registry.cn-hangzhou.cr.aliyuncs.com/dev/rcoder:latest"
default_image: "nuwax-docker-images-registry.cn-hangzhou.cr.aliyuncs.com/dev/rcoder:latest"
image_tag_prefix: "rcoder"
enabled: true
computer-agent-runner:
service_type: "ComputerAgentRunner"
arm64_image: "nuwax-docker-images-registry.cn-hangzhou.cr.aliyuncs.com/dev/rcoder-agent-runner:latest"
amd64_image: "nuwax-docker-images-registry.cn-hangzhou.cr.aliyuncs.com/dev/rcoder-agent-runner:latest"
default_image: "nuwax-docker-images-registry.cn-hangzhou.cr.aliyuncs.com/dev/rcoder-agent-runner:latest"
image_tag_prefix: "rcoder-computer-agent-runner"
enabled: true
selection_strategy: "ServiceOnly"
cache_config:
enabled: true
ttl_seconds: 3600
max_entries: 50
network_base_name: "agent-network"
network_mode: "bridge"
work_dir: "/app"
auto_cleanup: true
container_ttl_seconds: 3600
api_key_auth:
enabled: false
api_key: "YOUR_API_KEY_HERE"
kubernetes_config:
storage_class: "juicefs-sc-dev"

View File

@@ -0,0 +1,14 @@
# ClusterRoleBinding - 本地测试环境
# 把 base 里共享的 ClusterRole 绑定到 local namespace 的 ServiceAccount
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: rcoder-pods-crb-local
subjects:
- kind: ServiceAccount
name: rcoder-pods-sa
namespace: rcoder-local
roleRef:
kind: ClusterRole
name: rcoder-pods-clusterrole
apiGroup: rbac.authorization.k8s.io

View File

@@ -0,0 +1,12 @@
# 存储层凭据 Secret - 本地测试环境
apiVersion: v1
kind: Secret
metadata:
name: rcoder-credentials
namespace: rcoder-local
type: Opaque
stringData:
POSTGRES_USER: "juicefs"
POSTGRES_PASSWORD: "juicefs_password"
MINIO_ROOT_USER: "minioadmin"
MINIO_ROOT_PASSWORD: "minioadmin"

View File

@@ -0,0 +1,91 @@
# Local overlay - OrbStack 本地测试
# 用法: kubectl apply -k k8s/manifests/overlays/local
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: rcoder-local
resources:
- ../../base
- clusterrolebinding.yaml
- credentials.yaml
- rcoder-configmap.yaml
labels:
- pairs:
environment: local
includeSelectors: false
includeTemplates: true
replicas:
- name: rcoder
count: 1
images:
- name: nuwax-docker-images-registry.cn-hangzhou.cr.aliyuncs.com/dev/rcoder
newName: rcoder-local
newTag: latest
- name: nuwax-docker-images-registry.cn-hangzhou.cr.aliyuncs.com/dev/rcoder-agent-runner
newName: rcoder-agent-runner-local
newTag: latest
patches:
# PVC 使用 local-pathOrbStack 默认 StorageClass
# local-path 只支持 ReadWriteOnce本地测试改为单节点读写
- patch: |-
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: rcoder-workspace
spec:
storageClassName: local-path
accessModes:
- ReadWriteOnce
target:
kind: PersistentVolumeClaim
name: rcoder-workspace
# Deployment 环境变量适配
- patch: |-
apiVersion: apps/v1
kind: Deployment
metadata:
name: rcoder
spec:
template:
spec:
containers:
- name: rcoder
env:
- name: RCODER_K8S_NAMESPACE
value: rcoder-local
- name: RCODER_K8S_STORAGE_CLASS
value: local-path
- name: RCODER_K8S_PVC_ACCESS_MODE
value: ReadWriteOnce
- name: RUST_LOG
value: "debug"
- name: RCODER_DOCKER_IMAGE
value: "rcoder-local:latest"
- name: RCODER_DOCKER_IMAGE_COMPUTER
value: "rcoder-agent-runner-local:latest"
target:
kind: Deployment
name: rcoder
# Service 使用 NodePort本地访问
- patch: |-
apiVersion: v1
kind: Service
metadata:
name: rcoder
spec:
type: NodePort
ports:
- port: 8087
targetPort: 8087
nodePort: 30087
target:
kind: Service
name: rcoder

View File

@@ -0,0 +1,50 @@
# RCoder ConfigMap - 本地测试环境
apiVersion: v1
kind: ConfigMap
metadata:
name: rcoder-config
data:
config.yml: |
default_agent: "Claude"
projects_dir: "/app/project_workspace"
port: 8087
cleanup_config:
enabled: true
idle_timeout_seconds: 600
cleanup_interval_seconds: 300
docker_stop_timeout_seconds: 30
docker_config:
multi_image_config:
global_defaults:
registry_prefix: ""
services:
rcoder:
service_type: "RCoder"
arm64_image: "rcoder-local:latest"
amd64_image: "rcoder-local:latest"
default_image: "rcoder-local:latest"
image_tag_prefix: "rcoder"
enabled: true
computer-agent-runner:
service_type: "ComputerAgentRunner"
arm64_image: "rcoder-agent-runner-local:latest"
amd64_image: "rcoder-agent-runner-local:latest"
default_image: "rcoder-agent-runner-local:latest"
image_tag_prefix: "rcoder-agent-runner"
enabled: true
selection_strategy: "ServiceOnly"
network_base_name: "agent-network"
network_mode: "bridge"
work_dir: "/app"
auto_cleanup: true
container_ttl_seconds: 3600
api_key_auth:
enabled: false
kubernetes_config:
storage_class: "local-path"

View File

@@ -0,0 +1,17 @@
# ============================================================
# ClusterRoleBinding - Prod 环境专用
# 把 base 里共享的 ClusterRole 绑定到 prod namespace 的 ServiceAccount
# 集群级资源独立命名,避免与 dev 冲突
# ============================================================
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: rcoder-pods-crb-prod
subjects:
- kind: ServiceAccount
name: rcoder-pods-sa
namespace: nuwax-rcoder-prod
roleRef:
kind: ClusterRole
name: rcoder-pods-clusterrole
apiGroup: rbac.authorization.k8s.io

View File

@@ -0,0 +1,16 @@
# ============================================================
# 存储层凭据 Secret - Prod 环境
# ⚠️ 上线前必须替换为真实强密码,并同步修改 juicefs-secret.yaml 的 metaurl / MINIO_ACCESS_KEY 等字段
# 推荐做法:不把真实密码提交到 git改用 SealedSecret / ExternalSecret / 部署时注入
# ============================================================
apiVersion: v1
kind: Secret
metadata:
name: rcoder-credentials
namespace: nuwax-rcoder-prod
type: Opaque
stringData:
POSTGRES_USER: "juicefs"
POSTGRES_PASSWORD: "prodPg2024"
MINIO_ROOT_USER: "minioadmin"
MINIO_ROOT_PASSWORD: "prodMinio2024"

View File

@@ -0,0 +1,29 @@
# ============================================================
# JuiceFS Secret - Prod 环境专用
# ⚠️ 上线前必须把 metaurl/access-key/secret-key/MINIO_* 的明文密码
# 替换为 credentials.yaml 中设置的真实密码,保持两处一致。
# ============================================================
apiVersion: v1
kind: Secret
metadata:
name: juicefs-secret
namespace: nuwax-rcoder-prod
type: Opaque
stringData:
# JuiceFS CE 必需字段(小写键名)
name: "rcoder-juicefs"
metaurl: "postgres://juicefs:prodPg2024@postgresql.nuwax-rcoder-prod:5432/juicefs"
storage: "minio"
bucket: "http://minio-service.nuwax-rcoder-prod:9000/juicefs"
access-key: "minioadmin"
secret-key: "prodMinio2024"
# 兼容字段(保留给现有脚本/日志使用)
METAURL: "postgres://juicefs:prodPg2024@postgresql.nuwax-rcoder-prod:5432/juicefs"
MINIO_ACCESS_KEY: "minioadmin"
MINIO_SECRET_KEY: "prodMinio2024"
MINIO_ADDRESS: "minio-service.nuwax-rcoder-prod:9000"
# 其他配置
APPEND_FSYNC: "1"
CACHE_SIZE: "1024"

View File

@@ -0,0 +1,24 @@
# ============================================================
# JuiceFS StorageClass - Prod 环境专用
# 集群级资源独立命名,避免与 dev 冲突
# ============================================================
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: juicefs-sc-prod
annotations:
storageclass.kubernetes.io/is-default-class: "false"
provisioner: csi.juicefs.com
parameters:
csi.storage.k8s.io/provisioner-secret-name: juicefs-secret
csi.storage.k8s.io/provisioner-secret-namespace: nuwax-rcoder-prod
csi.storage.k8s.io/controller-expand-secret-name: juicefs-secret
csi.storage.k8s.io/controller-expand-secret-namespace: nuwax-rcoder-prod
csi.storage.k8s.io/node-publish-secret-name: juicefs-secret
csi.storage.k8s.io/node-publish-secret-namespace: nuwax-rcoder-prod
juicefs/controllerQuotaSet: "false"
juicefs/mountImage: "juicedata/mount:ce-v1.3.1"
juicefs/mountOptions: "cache-size=1024,allow-other"
reclaimPolicy: Retain
allowVolumeExpansion: true
volumeBindingMode: Immediate

View File

@@ -0,0 +1,87 @@
# Production environment overlay
# 使用方式: kubectl apply -k manifests/overlays/prod
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: nuwax-rcoder-prod
resources:
- ../../base
# prod 独立的集群级资源(避免与 dev 冲突)
- juicefs-storageclass.yaml
- clusterrolebinding.yaml
# prod 独立的 namespace 内资源
- rcoder-configmap.yaml
- credentials.yaml
- juicefs-secret.yaml
# 生产环境标签(含义同 dev 注释)
labels:
- pairs:
environment: production
includeSelectors: false
includeTemplates: true
# 生产环境副本数
replicas:
- name: rcoder
count: 2
# 生产环境镜像 tag
images:
- name: nuwax-docker-images-registry.cn-hangzhou.cr.aliyuncs.com/dev/rcoder
newName: nuwax-docker-images-registry.cn-hangzhou.cr.aliyuncs.com/nuwax-test/rcoder-k8s
newTag: latest
- name: nuwax-docker-images-registry.cn-hangzhou.cr.aliyuncs.com/dev/rcoder-agent-runner
newName: nuwax-docker-images-registry.cn-hangzhou.cr.aliyuncs.com/nuwax-test/rcoder-computer-agent-runner
newTag: latest
patches:
# NodePort: 30081prod 专用,与 dev 30080 错开)
- patch: |-
apiVersion: v1
kind: Service
metadata:
name: rcoder
spec:
ports:
- port: 8087
targetPort: 8087
nodePort: 30081
target:
kind: Service
name: rcoder
# PVC 指向 prod 专属 StorageClass
- patch: |-
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: rcoder-workspace
spec:
storageClassName: juicefs-sc-prod
target:
kind: PersistentVolumeClaim
name: rcoder-workspace
# rcoder deployment 指向 prod 环境的 namespace 和 StorageClass
- patch: |-
apiVersion: apps/v1
kind: Deployment
metadata:
name: rcoder
spec:
template:
spec:
containers:
- name: rcoder
env:
- name: RCODER_K8S_NAMESPACE
value: nuwax-rcoder-prod
- name: RCODER_K8S_STORAGE_CLASS
value: juicefs-sc-prod
target:
kind: Deployment
name: rcoder

View File

@@ -0,0 +1,71 @@
# ============================================================
# RCoder ConfigMap - Prod 环境专用
# storage_class 指向 juicefs-sc-prod
# ============================================================
apiVersion: v1
kind: ConfigMap
metadata:
name: rcoder-config
data:
config.yml: |
# RCoder 配置文件
default_agent: "Claude"
projects_dir: "/app/project_workspace"
port: 8087
cleanup_config:
enabled: true
idle_timeout_seconds: 600
cleanup_interval_seconds: 300
docker_stop_timeout_seconds: 30
proxy_config:
listen_port: 8088
default_backend_port: 8087
backend_host: "127.0.0.1"
port_param: "port"
health_check:
enabled: true
interval_seconds: 5
timeout_seconds: 1
healthy_threshold: 2
unhealthy_threshold: 3
docker_config:
multi_image_config:
global_defaults:
registry_prefix: "nuwax-docker-images-registry.cn-hangzhou.cr.aliyuncs.com/dev"
services:
rcoder:
service_type: "RCoder"
arm64_image: "nuwax-docker-images-registry.cn-hangzhou.cr.aliyuncs.com/dev/rcoder:latest"
amd64_image: "nuwax-docker-images-registry.cn-hangzhou.cr.aliyuncs.com/dev/rcoder:latest"
default_image: "nuwax-docker-images-registry.cn-hangzhou.cr.aliyuncs.com/dev/rcoder:latest"
image_tag_prefix: "rcoder"
enabled: true
computer-agent-runner:
service_type: "ComputerAgentRunner"
arm64_image: "nuwax-docker-images-registry.cn-hangzhou.cr.aliyuncs.com/dev/rcoder-agent-runner:latest"
amd64_image: "nuwax-docker-images-registry.cn-hangzhou.cr.aliyuncs.com/dev/rcoder-agent-runner:latest"
default_image: "nuwax-docker-images-registry.cn-hangzhou.cr.aliyuncs.com/dev/rcoder-agent-runner:latest"
image_tag_prefix: "rcoder-computer-agent-runner"
enabled: true
selection_strategy: "ServiceOnly"
cache_config:
enabled: true
ttl_seconds: 3600
max_entries: 50
network_base_name: "agent-network"
network_mode: "bridge"
work_dir: "/app"
auto_cleanup: true
container_ttl_seconds: 3600
api_key_auth:
enabled: false
api_key: "YOUR_API_KEY_HERE"
kubernetes_config:
storage_class: "juicefs-sc-prod"

View File

@@ -0,0 +1,417 @@
# Nuwax Platform K8s 部署方案
本文档记录 **`build-agent-docker` 项目** 从 docker-compose 迁移到 K8s 部署的完整方案。rcoder 项目的 Helm chart 是其中一个组件的**复制来源**,不做二次开发。
> 代码仓库:`/home/swufe/gitworkspace/build-agent-docker`
> 分支:`dev-k8s`
> 对应 rcoder Helm chart:`agents-a24a3ab5f6/k8s/helm/rcoder/`(模板复制基础)
---
## 1. 背景 / 为什么迁移
### 1.1 docker-compose 的两个痛点
1. **hostPath 限制扩容节点**
- 所有数据卷 bind-mount 到宿主机目录
- 新加节点时文件系统不共享,rcoder 动态创建的 agent-runner 容器只能跑在同一台机器
- 单点故障、IO 瓶颈、无弹性
2. **政企客户需要 K8s 原生部署**
- 很多客户已有自建 K8s 集群
- 方案必须兼容**标准 K8s 1.25+**(k3s 只作 dev 便利)
- 必须支持离线部署
### 1.2 目标
- **全栈 Helm chart** (`k8s/helm/nuwax-platform/`),支持 dev / test / prod / offline 四环境
- **JuiceFS + PostgreSQL + MinIO** K8s 原生文件服务栈(替代 hostPath)
- **保留现有 workflow**:`make dev``make push-test``make push-prod` 不变,新增 `make k8s-deploy-*` 对接
- **政企离线 bundle**(照抄 rcoder offline 模式,自包含所有镜像)
- **双栈并存**:docker-compose 继续可用,不 break 现有流程
---
## 2. 架构
```
┌──────────────────────────────────────────────────────────────┐
│ Ingress (可选, className 可配) │
│ └── / → frontend svc:80 │
│ │
│ NodePort (dev 便利 / 未配域名时兜底): │
│ frontend :30080 MinIO API :30900 Console :30901 │
│ │
│ 集群内部 (ClusterIP): │
│ backend:8080(主 REST API) │
│ +18082(Netty 代理 /page/) │
│ +18085(computer 路由,noVNC/websockify/audio) │
│ +18086(模型代理) │
│ +18087(沙盒代理) │
│ mcp-proxy:8089 / rcoder:8086 │
│ mysql:3306 / redis:6379 / milvus:19530 / es:9200 │
│ │
│ 文件服务(K8s 原生,跨节点): │
│ ├── JuiceFS StorageClass (juicefs-sc-<release>, RWX) │
│ │ 元数据: PostgreSQL StatefulSet (块存储 PVC) │
│ │ 数据块: MinIO StatefulSet (块存储 PVC) │
│ │ bucket juicefs (JuiceFS chunks) │
│ │ bucket nuwax-app (应用对象存储) │
│ └── RWX PVC (挂 JuiceFS SC): │
│ backend-upload / rcoder-workspace / │
│ rcoder-computer-workspace │
│ 动态 agent-runner Pod 也挂同一组 │
│ │
│ 数据库自有块存储 (global.blockStorageClass / 集群默认): │
│ mysql / redis / milvus / elasticsearch-ik StatefulSet │
└──────────────────────────────────────────────────────────────┘
```
**分档原则**: 数据库类用**块存储**(RWO,低延迟),共享文件类用 **JuiceFS**(RWX,跨节点)。两档都可通过 values 切换到客户自有 SC。
---
## 3. 关键决策 + 理由
### 3.1 Helm chart 大一统(不用 Kustomize)
| 维度 | 选择 | 理由 |
|------|------|------|
| 部署方式 | **单一 Helm chart**,4 个 values 文件 | 8+ 组件 + 多环境用 Kustomize overlay 会很臃肿;Helm values 参数化更友好,对外交付好用 |
| 已有 K8s 资产 | 推倒重建(tekton / deploy / overlays / 旧 helm 全删) | 现有都是半成品,有硬编码密码/hostPath/和 GitHub Actions 重复的 Tekton |
### 3.2 rcoder:新增 `rcoder-k8s` 镜像(feature flag 编译)
rcoder 源码里 `kubernetes` feature 已经存在:
```toml
# crates/rcoder/Cargo.toml
[features]
default = []
kubernetes = ["docker_manager/kubernetes"] # CONTAINER_RUNTIME=kubernetes 生效
```
**方式 A(采用)**: 新增独立 image tag `rcoder-k8s`
- `Dockerfile``ARG CARGO_FEATURES`,`make build-rcoder-k8s``CARGO_FEATURES=kubernetes`
- 不挂 docker.sock,通过 K8s API 创建动态 agent-runner Pod
- docker-compose 用的 `rcoder` 镜像不受影响
**方式 B(未采用)**: 让 `rcoder` 镜像始终含 kubernetes feature
- 镜像多几 MB,但逻辑简单
- docker-compose 老用户场景也会带上 K8s 代码(轻微浪费)
### 3.3 Elasticsearch IK 分词插件:定制镜像
docker-compose 通过挂载 `docker/config/elasticsearch/plugins/` 使用 IK 插件,K8s 不能挂主机目录,所以:
- 新建 `build_config/elasticsearch-ik/Dockerfile`:4 行 `FROM elasticsearch:9.2.1` + `COPY analysis-ik-9.2.1/`
- 复制 docker-compose 已用的 IK 发行包到 `build_config/elasticsearch-ik/`(维护时双份同步,IK 本身 ~1.5MB)
- K8s values `elasticsearch.image.repository: elasticsearch-ik`
### 3.4 文件服务:**JuiceFS + PG + MinIO,不用 Longhorn**
rcoder chart 有 Longhorn,但 nuwax-platform 不带:
- 数据库用集群默认块存储 SC 就够(用户自有 longhorn/ceph-rbd/local-path 都行)
- 共享文件用 JuiceFS 完全覆盖跨节点读写
- 少装一个 Longhorn 减少离线 bundle 体积(~5GB → ~3GB)
**MinIO 只起一个实例**,开**两个 bucket**:
- `juicefs` — JuiceFS 数据块
- `nuwax-app` — 应用对象存储(backend 的文件等)
### 3.5 兼容标准 K8s 1.25+
明确 **不依赖 k3s 特有机制**:
- StorageClass 空串 → 走集群默认,不强制 `local-path`
- Ingress Controller 由用户提供(`className` 参数化,默认 `nginx`,可改 `traefik` / `alb` / `istio`)
- 镜像拉取走公共或客户私有 registry,不依赖 k3s registries.yaml
k3s 只作为 dev 便利路径(开发者自己本地起一套测),生产一律走标准 K8s。
### 3.6 对外暴露:只 frontend + MinIO API
| 服务 | 是否对外 | 方式 |
|-----|--------|------|
| frontend | ✓ | Ingress(配域名时)或 NodePort(dev) |
| **MinIO API** | **✓** | NodePort 30900(应用要直连 S3 协议) |
| MinIO Console | (可选) | NodePort 30901(管理员) |
| backend / mcp-proxy / rcoder | ✗ | ClusterIP 内部 |
| mysql / redis / milvus / es | ✗ | ClusterIP 内部 |
| postgresql(JuiceFS 元数据) | ✗ | ClusterIP 内部 |
### 3.7 多环境同集群共存
同一 K8s 集群可同时跑 dev / test / prod 三个 release,天然隔离:
- **namespace 级资源** → 各自 namespace 隔离
- **集群级资源**(冲突风险):
- `ClusterRole` 名字固定 `nuwax-rcoder-pods-clusterrole`(规则相同,多 release 共享,`helm.sh/resource-policy: keep`)
- `ClusterRoleBinding` 名字 `{release}-rcoder-pods-crb`(Release 独立)
- `StorageClass` 名字 `juicefs-sc-{release}`(Release 独立)
### 3.8 不在范围内的
| 组件 | 为什么不做 |
|-----|-----------|
| Doris | 未正式使用,后续用到再加 |
| log-platform | 废弃 |
| video-analysis | 废弃 |
| subapp-deployer | 容器里 npm install 安装,不需要独立部署 |
| agent-client | 不需要 K8s 部署 |
---
## 4. 实现(3 个 PR + 1 个 fix commit)
Git 提交历史(`build-agent-docker` 仓库 `dev-k8s` 分支):
```
1563052 refactor(build): remove dev-parallel target, rely on serial make dev
15ca818 fix(k8s): post-review fixes to make deployment actually work
77d33af feat(k8s): offline bundle tooling + production hardening (PR3)
138b313 feat(k8s): complete nuwax-platform Helm chart with K8s-native file service (PR2)
edf1f7d refactor(k8s): rebuild k8s/ directory with Helm chart skeleton (PR1)
```
### PR1 — 清理 + Helm 骨架 (`edf1f7d`)
- 删除 `k8s/{tekton,deploy,overlays,helm/nuwax-platform}`(195 文件)
- 新建 `k8s/helm/nuwax-platform/` 骨架:Chart.yaml + values + `_helpers.tpl` + NOTES + 空目录占位
- 重写 `makefiles/10-k8s.mk`:`k8s-lint` / `k8s-render-*`
- **验证**:helm lint 4 环境通过,render 空骨架
### PR2 — 全量模板 + 构建系统 (`138b313`)
**构建系统**:
- `build_config/rcoder/Dockerfile``ARG CARGO_FEATURES`
- 新建 `build_config/elasticsearch-ik/Dockerfile` + 复制 IK 插件
- `makefiles/02-build.mk``build-rcoder-k8s-{amd64,arm64}` / `build-elasticsearch-ik-{amd64,arm64}`,挂到现有 `build-{amd64,arm64}-only`(→ `make dev` 自动覆盖)
- `makefiles/05-push.mk``push-rcoder-k8s` / `push-elasticsearch-ik`,挂到 `push-custom-images`(→ `make push-test`/`push-prod` 自动覆盖)
**Helm 模板**(33 个):
- `rcoder/` 8 个(复制自 rcoder chart,去 docker.sock + 换 image 为 rcoder-k8s)
- `frontend/` 4 个(nginx.conf 里 `backend:` 通过 replace 自动变 `{release}-backend:`)
- `backend/` 4 个(5 个端口 + ConfigMap 化 application-external.yml + entrypoint.sh)
- `mcp-proxy/` 3 个
- `storage/` 15 个(MySQL/Redis/Milvus/ES/PG/MinIO + JuiceFS Secret/SC + credentials)
- `shared-pvcs/` 3 个 RWX PVC
- `ingress.yaml` 1 个(条件渲染)
**Make 部署目标**:
- `k8s-install-juicefs-csi`(集群级,一次性)
- `k8s-deploy-{dev,test,prod}` + preflight
- `k8s-undeploy-*` / `k8s-purge-*` / `k8s-status-*` / `k8s-logs-<env>-<component>`
### PR3 — 离线 bundle + 生产加固 (`77d33af`)
**离线**:
- `k8s/offline/images.txt` — ~20 个镜像 pin 版本
- `k8s/offline/install.sh` — direct / registry 双模式,direct 自动探测 k3s ctr / nerdctl / ctr
- `k8s/offline/rewrite-registry.sh` — 重 tag 推到客户 registry
- `k8s/offline/README.md` — 客户交付手册
- `makefiles/10-k8s.mk``k8s-offline-bundle` / `k8s-offline-import`
**加固**:
- `templates/common/network-policy.yaml` — prod 默认 deny-all + 显式放行
- values-prod 开启 NetworkPolicy + Ingress + TLS + PDB
**文档**:`k8s/README.md` 覆盖 dev/test/prod/offline 四场景
### Fix commit — 部署前自查发现的 5 处 bug (`15ca818`)
| # | 问题 | 修法 |
|---|-----|-----|
| 1 | backend 等 Redis 时 `redis-cli ping` 未带 `-a $REDIS_PASSWORD` → NOAUTH,entrypoint exit 1 | `configmap.yaml` replace 加专项规则 |
| 2 | `manage_jwt_secret()` 覆盖 env,Pod 重启换 JWT → session 失效 | Secret 投影为 `/app/config/jwt/jwt_secret_key.txt`,脚本走"读文件"分支 |
| 3 | rcoder 缺 nginx 子应用 conf + 404/502 + 3 个 workspace 子目录 + 脚手架模板 + agent-runner 覆盖目录 | 加 2 个 ConfigMap(nginx conf / binaryData 模板 zip)+ subPath 切分 workspace PVC + emptyDir 给 agent-runner |
| 4 | ES readinessProbe 硬编码 Basic base64 → 改密码后 401 | 改 exec curl -u `elastic:$ELASTIC_PASSWORD`,从 Secret 动态注入 |
| 5 | values-prod.yaml 6 处 CHANGE-ME 需人工替换 | 改成和 `docker/.env` 一致的默认密码(`root` / `admin123` / `123456` / `elastic123` / `minioadmin`)开箱即用 |
| 6 | 文档补漏 | prod namespace PSS ≤ baseline(Milvus 需要 seccomp Unconfined)+ CNI 支持矩阵 |
### 精简 commit — 移除 `dev-parallel` (`1563052`)
- 并行 docker buildx 会偶发 BuildKit cache 竞态,生产/复现难
- `make dev` 串行路径已覆盖所有 K8s 镜像,移除 `dev-parallel` 及其子目标
- 保留 `setup-parallel`(git clone/pull 无竞态)
---
## 5. 使用流程
### 5.1 本地 dev(k3s / kind)
```bash
cd build-agent-docker
# 1. 构建全部镜像 + 推到测试仓库(含 rcoder-k8s + elasticsearch-ik)
make dev
# 2. 装 JuiceFS CSI Driver(集群级,一次性)
make k8s-install-juicefs-csi
# 3. 部署到 nuwax-dev namespace
make k8s-deploy-dev
make k8s-status-dev
# 4. 访问
curl http://<node-ip>:30080/ # frontend
curl http://<node-ip>:30080/api/health # 经 nginx 到 backend
# 验证 JuiceFS 跨 Pod 共享
kubectl exec -n nuwax-dev deploy/nuwax-dev-backend -- sh -c 'echo test > /app/upload/x'
kubectl exec -n nuwax-dev deploy/nuwax-dev-rcoder -- cat /app/upload/x # test
```
### 5.2 test / prod
```bash
# 改 values-test.yaml / values-prod.yaml(blockStorageClass / ingress hosts / 域名)
kubectl config use-context <test-cluster>
make k8s-install-juicefs-csi
make k8s-deploy-test
# prod(镜像切生产仓库)
make push-prod
kubectl config use-context <prod-cluster>
make k8s-install-juicefs-csi
make k8s-deploy-prod
```
### 5.3 政企离线
```bash
# 构建机(有网)
make push-prod # 先把镜像推到 /nuwax
make k8s-offline-bundle # dist/nuwax-offline-<ver>-<arch>.tar.gz (~3GB)
# 客户机器(断网)
scp dist/nuwax-offline-*.tar.gz offline-node:/tmp/
ssh offline-node
cd /tmp && tar xzf nuwax-offline-*.tar.gz -C nuwax-offline && cd nuwax-offline
bash install.sh --mode=direct --env=prod # 单节点
# 或多节点:
bash install.sh --mode=registry --registry=harbor.internal/nuwax --env=prod
```
---
## 6. 镜像清单 + 用途
| 镜像 | docker-compose | K8s | 说明 |
|-----|:---:|:---:|------|
| agent-platform-front | ✓ | ✓ | 前端 nginx |
| agent-platform-backend | ✓ | ✓ | Spring Boot 后端 |
| mcp-proxy | ✓ | ✓ | Rust MCP 代理 |
| rcoder | ✓ | ✗ | docker-compose 用,挂 docker.sock |
| **rcoder-k8s** | ✗ | ✓ | **K8s 用,feature=kubernetes 编译,K8s API 起 Pod** |
| rcoder-computer-agent-runner | ✓ | ✓ | 动态 agent-runner 基础镜像 |
| agent-client | ✓ | ✗ | 不纳入 K8s |
| elasticsearch(官方) | ✓ | ✗ | docker-compose 用 |
| **elasticsearch-ik** | ✗ | ✓ | **K8s 用,含 IK 中文分词插件** |
| mysql:8.0 | ✓ | ✓ | 第三方 |
| redis:7.0 | ✓ | ✓ | 第三方 |
| milvusdb/milvus:v2.5.8 | ✓ | ✓ | 第三方 |
| minio:RELEASE-xxx | ✓ | ✓ | JuiceFS 后端 + 应用对象存储(2 bucket) |
| postgres:16-alpine | ✗ | ✓ | JuiceFS 元数据(K8s 专用) |
| busybox:1.36 | ✓ | ✓ | init container |
| juicedata/juicefs-csi-driver:v0.31.3 | ✗ | ✓ | K8s 专用,集群级组件 |
| juicedata/mount:ce-v1.3.1 | ✗ | ✓ | JuiceFS CSI 运行时 |
| registry.k8s.io/sig-storage/csi-* 4 个 | ✗ | ✓ | JuiceFS CSI sidecars |
---
## 7. 文件结构
```
build-agent-docker/
├── build_config/
│ ├── rcoder/
│ │ └── Dockerfile # 加 ARG CARGO_FEATURES,rcoder-k8s 传 kubernetes
│ └── elasticsearch-ik/ # 【新增】
│ ├── Dockerfile # 4 行 FROM + COPY
│ └── elasticsearch-analysis-ik-9.2.1/
├── makefiles/
│ ├── 00-vars.mk # + RCODER_K8S_IMAGE / ELASTICSEARCH_IK_IMAGE
│ ├── 02-build.mk # + build-rcoder-k8s-* / build-elasticsearch-ik-*
│ ├── 05-push.mk # + push-rcoder-k8s / push-elasticsearch-ik
│ └── 10-k8s.mk # 整体重写: k8s-lint/render/deploy/offline
└── k8s/ # 【整体重建】
├── README.md # 四场景操作指南 + 故障排查
├── helm/
│ └── nuwax-platform/
│ ├── Chart.yaml
│ ├── values.yaml # 默认
│ ├── values-dev.yaml # 本地 k3s / NodePort / 小资源
│ ├── values-test.yaml # 自建 K8s / 中等规模
│ ├── values-prod.yaml # 副本 2+ / Ingress+TLS / NetworkPolicy
│ ├── values-offline.yaml # 私有 registry 占位
│ ├── configs/ # .Files.Get 读的配置文件
│ │ ├── nginx.conf # (backend: 主机自动替换)
│ │ ├── application-external.yml
│ │ ├── docker-entrypoint.sh
│ │ ├── mcp_config.yml
│ │ ├── mysql.cnf / redis.conf / milvus/*.yaml / elasticsearch/*
│ │ ├── sub_app_multi_apps.conf # rcoder 内置 nginx 子应用路由
│ │ ├── init_mysql{,_data}.sql
│ │ ├── nginx/custom/{404,502}.html
│ │ └── rcoder-templates/ # react-vite-template.zip, vue3-vite-template.zip
│ └── templates/
│ ├── _helpers.tpl
│ ├── NOTES.txt
│ ├── ingress.yaml
│ ├── frontend/ (4) # Deploy + Service + nginx CM + 404/502 CM
│ ├── backend/ (4) # Deploy + Service + config CM + pdb
│ ├── mcp-proxy/ (3)
│ ├── rcoder/ (10) # 8 复制自 rcoder chart + nginx CM + 模板 CM
│ ├── storage/ (17) # 6 组中间件 + PG + MinIO + JuiceFS
│ ├── shared-pvcs/ (3) # backend-upload / rcoder-workspace ×2
│ └── common/ (1) # network-policy (prod)
└── offline/ # 离线 bundle 源素材
├── images.txt # 20+ 镜像清单
├── install.sh # direct / registry 双模式
├── rewrite-registry.sh
└── README.md # 客户交付手册
```
---
## 8. 已知限制 / 运维注意
| 项 | 详情 |
|----|------|
| K3s flannel 不支持 NetworkPolicy | prod 启用 NetworkPolicy 需用 Calico / Cilium / Antrea / kube-router |
| prod namespace PSS 级别 | 必须 ≤ baseline(Milvus 需要 seccompProfile: Unconfined,restricted 会拒) |
| rcoder chart 双份维护 | `build-agent-docker/.../templates/rcoder/` 和 rcoder 仓库 `k8s/helm/rcoder/` 同步更新 |
| ES IK 插件目录双份 | `docker/config/elasticsearch/plugins/``build_config/elasticsearch-ik/` 同步 |
| JuiceFS 上的 DB 性能 | MySQL/Redis/Milvus/ES 严禁用 JuiceFS,模板里已强制块存储 PVC |
| 密码管理 | dev/prod values 里是明文默认值(开箱即用);生产环境建议 External Secrets / sealed-secrets |
| direct 模式镜像导入 | 多节点集群每个节点都要运行 install.sh,或推荐 registry 模式一次推送全集群可用 |
---
## 9. 验证矩阵
| 检查项 | 命令 | 预期 |
|-------|------|------|
| Helm chart 语法 | `make k8s-lint` | 4 环境都 pass |
| 模板渲染 | `make k8s-render-all` | 4 环境输出有效 YAML |
| dev 资源数 | `make k8s-render-dev \| grep -c "^kind:"` | 46 |
| prod 资源数 | `make k8s-render-prod \| grep -c "^kind:"` | 54(含 5 NetworkPolicy + Ingress + 2 PDB) |
| 镜像构建全量 | `make -n dev \| grep -c "docker buildx build"` | 20(9 镜像 × 2 架构 + 基础镜像) |
| K8s 集群端到端 | `make k8s-deploy-dev && curl http://<ip>:30080/` | HTTP 200 |
| JuiceFS 跨 Pod 共享 | 见 §5.1 最后两条 kubectl exec | `test` |
| 离线 bundle 构建 | `make k8s-offline-bundle` | `dist/nuwax-offline-*.tar.gz` 产出 |
| 离线 bundle 内容 | `tar tzf dist/nuwax-offline-*.tar.gz \| head` | install.sh + images + charts + values |
---
## 10. 术语
- **release name**: Helm 安装时的名字,和 namespace 同名,形如 `nuwax-dev` / `nuwax-test` / `nuwax-prod`
- **block storage**: 块存储(RWO, 单 Pod 读写),用于数据库
- **shared storage**: 共享文件存储(RWX, 多 Pod 读写),用于上传文件 / workspace
- **JuiceFS CSI**: 集群级 CSI driver,不在 nuwax-platform chart 里,单独 `make k8s-install-juicefs-csi`
- **direct 模式**: 离线部署时把镜像直接导入每个节点 containerd,单节点 / 小集群用
- **registry 模式**: 离线部署时把镜像推到客户私有 registry,多节点推荐

View File

@@ -0,0 +1,215 @@
# RCoder 离线部署指南
本文档面向政企/内网客户:**目标机器无公网访问,所有镜像与依赖必须随部署包交付**。
---
## 目录内容(解压 `rcoder-offline-*.tar.gz` 后)
```
.
├── install.sh # 主安装脚本
├── rewrite-registry.sh # registry 模式: re-tag + push 辅助脚本
├── README.md # 本文件
├── values-dev.yaml # RCoder dev 环境 values
├── values-prod.yaml # RCoder prod 环境 values
├── values-offline.yaml # 离线 registry 覆盖模板
├── images/
│ ├── all-images.tar # 所有镜像的 docker save 产物
│ └── images.txt # 镜像清单
├── charts/
│ └── rcoder-*.tgz # RCoder Helm chart
├── longhorn/
│ └── longhorn-v*.yaml # Longhorn 全量 manifest
├── juicefs-csi/
│ └── juicefs-csi-v*.yaml # JuiceFS CSI Driver 官方 manifest
└── manifests/ # 原始 Kustomize 资产 (参考, 不必使用)
```
---
## 前置条件
在离线机器上,提前准备好:
| 组件 | 版本 | 说明 |
|------|------|------|
| K8s | 1.19+ | 已部署, kubeconfig 已配置 |
| kubectl | 与集群匹配 | `kubectl cluster-info` 能通 |
| helm | 3.x | 用于 `helm upgrade --install` |
| docker | 最新 | **仅 registry 模式需要** |
| k3s / nerdctl / ctr | 最新 | **仅 direct 模式需要** |
| open-iscsi | 任意 | Longhorn 依赖 (Ubuntu: `apt install open-iscsi`) |
---
## 两种部署模式
### 模式 A: direct推荐单节点 / 小集群)
直接把镜像 import 到节点 containerd**无需私有 registry**。适合 k3s 单节点或 3-5 节点小集群。
多节点集群需要**在每个节点上依次执行**镜像导入。
```bash
# 1. 解压 bundle (可通过 make k8s-offline-import 一步完成)
tar xzf rcoder-offline-<version>-<arch>.tar.gz -C /opt/rcoder-offline
cd /opt/rcoder-offline
# 2. 一键安装
sudo ./install.sh --mode=direct --env=dev
# 生产环境
sudo ./install.sh --mode=direct --env=prod \
--set credentials.postgresql.password='<prod-pg-pass>' \
--set credentials.minio.rootPassword='<prod-minio-pass>'
```
> **多节点注意**: 每个节点都要执行 `sudo k3s ctr -n k8s.io image import images/all-images.tar`
> 或把 bundle scp 到每个节点各跑一次 `--skip-juicefs-csi --skip-longhorn` 只做镜像导入。
### 模式 B: registry推荐多节点 / 生产)
把镜像重新打 tag 推送到客户的私有 registryHarbor / Nexus / 阿里云 ACR 内网实例K8s 按常规流程拉取。
**前置: 集群节点能访问私有 registry并配置好拉取凭据如需要**
```bash
# 1. 解压
tar xzf rcoder-offline-<version>-<arch>.tar.gz -C /opt/rcoder-offline
cd /opt/rcoder-offline
# 2. 如果私有 registry 需要认证, 先登录 docker daemon
docker login harbor.internal
# 3. 安装 (一条命令完成推送+部署)
./install.sh \
--mode=registry \
--registry=harbor.internal/rcoder \
--thirdparty-registry=harbor.internal/thirdparty \
--env=prod
```
推送完成后,镜像会以如下命名出现在 registry
| 原镜像 | 推送目标 |
|--------|---------|
| `nuwax-docker-images-registry.cn-hangzhou.cr.aliyuncs.com/dev/rcoder:latest` | `harbor.internal/rcoder/rcoder:latest` |
| `postgres:16-alpine` | `harbor.internal/thirdparty/postgres:16-alpine` |
| `longhornio/longhorn-manager:v1.7.2` | `harbor.internal/thirdparty/longhornio/longhorn-manager:v1.7.2` |
| `registry.k8s.io/sig-storage/csi-provisioner:v5.1.0` | `harbor.internal/thirdparty/sig-storage/csi-provisioner:v5.1.0` |
如果集群需要 imagePullSecret:
```bash
kubectl -n nuwax-rcoder-prod create secret docker-registry harbor-pull \
--docker-server=harbor.internal \
--docker-username=<user> \
--docker-password=<pass>
# 重新安装时:
./install.sh ... --set 'global.imagePullSecrets[0].name=harbor-pull'
```
---
## 常见操作
### 查看部署状态
```bash
kubectl get pods -n nuwax-rcoder-dev
kubectl get svc -n nuwax-rcoder-dev
kubectl rollout status deploy/rcoder -n nuwax-rcoder-dev
```
### 访问服务
```bash
# NodePort
NODE_IP=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}')
curl http://${NODE_IP}:30080/health # dev
curl http://${NODE_IP}:30081/health # prod
# 或 port-forward
kubectl port-forward -n nuwax-rcoder-dev svc/rcoder 8087:8087
curl http://localhost:8087/health
```
### 卸载
```bash
helm uninstall rcoder-dev --namespace nuwax-rcoder-dev
# 彻底删除 (包含 PVC)
kubectl delete pvc --all -n nuwax-rcoder-dev
kubectl delete namespace nuwax-rcoder-dev
```
### 升级
收到新 bundle 后直接重新跑 `install.sh``helm upgrade --install` 会自动升级):
```bash
./install.sh --mode=registry --registry=harbor.internal/rcoder --env=prod
```
---
## 跳过选项
| 参数 | 用途 |
|------|------|
| `--skip-longhorn` | 集群已有其他 CSI (Ceph/NFS/CSI-local-path),不装 Longhorn |
| `--skip-juicefs-csi` | 集群已有 JuiceFS CSI 或用其他 RWX 方案 |
| `--skip-image-import` | 镜像已手工导入,调试用 |
---
## 多节点镜像批量导入direct 模式)
`install.sh` 只在执行节点导入镜像。多节点集群建议写一个辅助脚本循环每个节点:
```bash
NODES=(node1 node2 node3)
for n in "${NODES[@]}"; do
scp images/all-images.tar root@$n:/tmp/
ssh root@$n 'sudo k3s ctr -n k8s.io image import /tmp/all-images.tar && rm /tmp/all-images.tar'
done
# 然后在控制平面节点跑:
./install.sh --mode=direct --env=prod --skip-image-import
```
---
## 故障排查
### Pod 长时间 Pending
```bash
kubectl describe pod -n nuwax-rcoder-dev <pod-name>
# 常见原因:
# - ImagePullBackOff -> 节点 containerd 没有镜像 (direct 模式漏了某节点)
# - 长时间 ContainerCreating -> Longhorn / JuiceFS CSI 未就绪
```
### JuiceFS 挂载失败
```bash
kubectl logs -n kube-system -l app=juicefs-csi-driver-node --tail=50
# 检查 postgresql / minio 是否就绪
kubectl get pods -n nuwax-rcoder-dev -l app=postgresql
kubectl get pods -n nuwax-rcoder-dev -l app=minio
```
### Longhorn 无法启动
```bash
# 常见原因: open-iscsi 未安装
ssh <node> "sudo apt install -y open-iscsi && sudo systemctl enable --now iscsid"
kubectl delete pod -n longhorn-system -l app=longhorn-manager --force
```
---
## 版本信息
本 bundle 固定了以下版本:
- RCoder: (由构建时的 git tag 决定, 见 `images/images.txt`)
- PostgreSQL: 16-alpine
- MinIO: 固定 release tag
- JuiceFS Mount: ce-v1.3.1
- JuiceFS CSI: v0.31.3 (官方 deploy/k8s.yaml; helm 仓库已停更)
- Longhorn: v1.7.2
升级任意组件需要**重新构建 bundle**`make k8s-offline-bundle`),本离线包不能在线更新。

View File

@@ -0,0 +1,52 @@
# ============================================================================
# 离线部署镜像清单 —— 所有 make k8s-offline-bundle 会拉取 + 打包的镜像
#
# 格式: 一行一个 <repo>:<tag>; # 开头为注释, 空行忽略
# 原则: 版本必须 pin, 避免离线包随时间漂移
#
# 升级 Longhorn / JuiceFS CSI 时, 同步更新本文件 + longhorn-$VERSION.yaml
# ============================================================================
# ==== RCoder 自有镜像 ====
nuwax-docker-images-registry.cn-hangzhou.cr.aliyuncs.com/dev/rcoder:latest
nuwax-docker-images-registry.cn-hangzhou.cr.aliyuncs.com/dev/rcoder-agent-runner:latest
# ==== 存储层 ====
postgres:16-alpine
minio/minio:RELEASE.2024-12-18T13-15-44Z
minio/mc:RELEASE.2024-11-21T17-21-54Z
busybox:1.36
# ==== JuiceFS CE Mount ====
juicedata/mount:ce-v1.3.1
# ==== JuiceFS CSI Driver v0.31.3 (通过官方 deploy/k8s.yaml 安装) ====
# 清单来源: curl https://raw.githubusercontent.com/juicedata/juicefs-csi-driver/v0.31.3/deploy/k8s.yaml | grep image:
juicedata/juicefs-csi-driver:v0.31.3
juicedata/csi-dashboard:v0.31.3
# ==== JuiceFS CSI sidecars (版本由 juicefs-csi k8s.yaml 决定) ====
registry.k8s.io/sig-storage/csi-node-driver-registrar:v2.9.0
registry.k8s.io/sig-storage/csi-provisioner:v3.6.0
registry.k8s.io/sig-storage/csi-resizer:v1.9.0
registry.k8s.io/sig-storage/livenessprobe:v2.11.0
# ==== Longhorn v1.7.2 核心组件 ====
# 清单来源: curl https://raw.githubusercontent.com/longhorn/longhorn/v1.7.2/deploy/longhorn-images.txt
# 升级时必须重新拉取 longhorn-images.txt 并同步 tag
longhornio/longhorn-manager:v1.7.2
longhornio/longhorn-engine:v1.7.2
longhornio/longhorn-ui:v1.7.2
longhornio/longhorn-instance-manager:v1.7.2
longhornio/longhorn-share-manager:v1.7.2
longhornio/backing-image-manager:v1.7.2
longhornio/longhorn-cli:v1.7.2
longhornio/support-bundle-kit:v0.0.45
# ==== Longhorn CSI sidecars (独立于 Longhorn 主镜像, 版本随 Longhorn 升级变化) ====
longhornio/csi-attacher:v4.7.0
longhornio/csi-provisioner:v4.0.1-20241007
longhornio/csi-resizer:v1.12.0
longhornio/csi-snapshotter:v7.0.2-20241007
longhornio/csi-node-driver-registrar:v2.12.0
longhornio/livenessprobe:v2.14.0

View File

@@ -0,0 +1,277 @@
#!/bin/bash
# ============================================================================
# RCoder 离线部署脚本
#
# 使用场景: 政企内网, 无外网访问
#
# 运行流程:
# 1. 将镜像导入到节点 containerd (direct 模式) 或推到私有 registry (registry 模式)
# 2. 安装 Longhorn 存储 (可跳过)
# 3. 安装 JuiceFS CSI Driver (可跳过)
# 4. 安装 RCoder Helm chart
#
# 依赖 (离线机器上必须预装):
# - kubectl (配置好 kubeconfig)
# - helm 3.x
# - docker (registry 模式) 或 k3s/nerdctl (direct 模式)
# ============================================================================
set -e
# ---- 默认参数 ----
MODE="direct" # direct | registry
REGISTRY="" # registry 模式下必填, 例: harbor.internal
THIRDPARTY_REGISTRY="" # 第三方镜像目标 registry, 留空同 REGISTRY
ENV="dev" # dev | prod
RELEASE_NAME="" # 留空 => rcoder-$ENV
SKIP_LONGHORN=0
SKIP_JUICEFS_CSI=0
SKIP_IMAGE_IMPORT=0 # 调试用: 跳过镜像导入
NAMESPACE="" # 留空 => nuwax-rcoder-$ENV
# ---- 颜色 ----
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
usage() {
cat <<EOF
用法: $0 [选项]
选项:
--mode=MODE direct (默认) 或 registry
--registry=URL 私有 registry 地址 (registry 模式必填)
--thirdparty-registry=URL 第三方镜像目标 (留空同 --registry)
--env=ENV dev (默认) 或 prod
--release=NAME Helm release 名 (默认 rcoder-\$ENV)
--namespace=NS 目标 namespace (默认 nuwax-rcoder-\$ENV)
--skip-longhorn 跳过 Longhorn 安装 (集群已有 CSI 时用)
--skip-juicefs-csi 跳过 JuiceFS CSI 安装
--skip-image-import 跳过镜像导入 (调试/镜像已就位时用)
-h|--help 显示本帮助
示例:
# 最常见: 断网机器直接用节点 containerd 本地缓存
$0 --mode=direct --env=dev
# 有私有 registry: 先推送再部署
$0 --mode=registry --registry=harbor.internal/rcoder --env=prod
# 集群已有存储, 只装 RCoder 本身
$0 --mode=direct --env=dev --skip-longhorn --skip-juicefs-csi
EOF
exit 0
}
# ---- 参数解析 ----
while [ $# -gt 0 ]; do
case "$1" in
--mode=*) MODE="${1#*=}" ;;
--registry=*) REGISTRY="${1#*=}" ;;
--thirdparty-registry=*) THIRDPARTY_REGISTRY="${1#*=}" ;;
--env=*) ENV="${1#*=}" ;;
--release=*) RELEASE_NAME="${1#*=}" ;;
--namespace=*) NAMESPACE="${1#*=}" ;;
--skip-longhorn) SKIP_LONGHORN=1 ;;
--skip-juicefs-csi) SKIP_JUICEFS_CSI=1 ;;
--skip-image-import) SKIP_IMAGE_IMPORT=1 ;;
-h|--help) usage ;;
*) echo -e "${RED}未知参数: $1${NC}"; usage ;;
esac
shift
done
[ -z "$RELEASE_NAME" ] && RELEASE_NAME="rcoder-$ENV"
[ -z "$NAMESPACE" ] && NAMESPACE="nuwax-rcoder-$ENV"
[ -z "$THIRDPARTY_REGISTRY" ] && THIRDPARTY_REGISTRY="$REGISTRY"
if [ "$MODE" = "registry" ] && [ -z "$REGISTRY" ]; then
echo -e "${RED}错误: registry 模式必须指定 --registry=URL${NC}"; exit 1
fi
echo -e "${GREEN}=========================================="
echo " RCoder 离线部署"
echo "==========================================${NC}"
echo " 模式: $MODE"
echo " 环境: $ENV"
echo " Release: $RELEASE_NAME"
echo " Namespace: $NAMESPACE"
[ "$MODE" = "registry" ] && echo " Registry: $REGISTRY"
[ "$MODE" = "registry" ] && echo " 3rd-party: $THIRDPARTY_REGISTRY"
echo " Longhorn: $([ $SKIP_LONGHORN = 1 ] && echo '跳过' || echo '安装')"
echo " JuiceFS CSI: $([ $SKIP_JUICEFS_CSI = 1 ] && echo '跳过' || echo '安装')"
echo ""
# ---- 前置检查 ----
for cmd in kubectl helm; do
if ! command -v $cmd &>/dev/null; then
echo -e "${RED}错误: 未安装 $cmd${NC}"; exit 1
fi
done
if ! kubectl cluster-info &>/dev/null; then
echo -e "${RED}错误: 无法连接 K8s 集群, 检查 kubeconfig${NC}"; exit 1
fi
# ============================================================================
# 步骤 1: 导入镜像
# ============================================================================
if [ "$SKIP_IMAGE_IMPORT" = "0" ]; then
echo -e "${GREEN}[1/4] 导入镜像...${NC}"
IMAGES_TAR="$SCRIPT_DIR/images/all-images.tar"
if [ ! -f "$IMAGES_TAR" ]; then
echo -e "${RED}错误: 未找到 $IMAGES_TAR${NC}"; exit 1
fi
case "$MODE" in
direct)
# 优先使用 k3s ctr, 回退到 nerdctl, 再回退到 ctr
if command -v k3s &>/dev/null; then
echo " 使用 k3s ctr 导入 (namespace: k8s.io)..."
sudo k3s ctr -n k8s.io image import "$IMAGES_TAR"
elif command -v nerdctl &>/dev/null; then
echo " 使用 nerdctl 导入..."
sudo nerdctl --namespace k8s.io load -i "$IMAGES_TAR"
elif command -v ctr &>/dev/null; then
echo " 使用 ctr 导入 (namespace: k8s.io)..."
sudo ctr -n k8s.io image import "$IMAGES_TAR"
else
echo -e "${RED}错误: direct 模式需要 k3s / nerdctl / ctr 其中之一${NC}"; exit 1
fi
echo -e "${GREEN} ✅ 镜像已导入节点 containerd${NC}"
echo -e "${YELLOW} ⚠️ 如果是多节点集群, 需要在每个节点上运行 ctr image import${NC}"
;;
registry)
if ! command -v docker &>/dev/null; then
echo -e "${RED}错误: registry 模式需要 docker${NC}"; exit 1
fi
echo " 加载到 docker daemon..."
docker load -i "$IMAGES_TAR"
echo " 重打 tag 并推送到 $REGISTRY..."
bash "$SCRIPT_DIR/rewrite-registry.sh" \
--registry "$REGISTRY" \
--thirdparty-registry "$THIRDPARTY_REGISTRY" \
--images-file "$SCRIPT_DIR/images/images.txt"
;;
esac
else
echo -e "${YELLOW}[1/4] 跳过镜像导入 (--skip-image-import)${NC}"
fi
# ============================================================================
# 步骤 2: 安装 Longhorn
# ============================================================================
echo -e "${GREEN}[2/4] Longhorn...${NC}"
if [ "$SKIP_LONGHORN" = "1" ]; then
echo " 跳过 (--skip-longhorn)"
elif kubectl get sc longhorn &>/dev/null; then
echo -e "${GREEN} ✅ 已安装${NC}"
else
LONGHORN_YAML=$(ls "$SCRIPT_DIR/longhorn/longhorn-"*.yaml 2>/dev/null | head -1)
if [ -z "$LONGHORN_YAML" ]; then
echo -e "${RED}错误: 未找到 longhorn/longhorn-*.yaml${NC}"; exit 1
fi
# registry 模式: 先把 manifest 里的 longhornio/* 改成 $THIRDPARTY_REGISTRY/longhornio/*
APPLY_YAML="$LONGHORN_YAML"
if [ "$MODE" = "registry" ]; then
APPLY_YAML="/tmp/longhorn-rewritten.yaml"
sed -E "s#(image:\s*)longhornio/#\1${THIRDPARTY_REGISTRY}/longhornio/#g" \
"$LONGHORN_YAML" > "$APPLY_YAML"
fi
echo " 部署 Longhorn..."
kubectl apply -f "$APPLY_YAML"
echo " 等待 Longhorn manager 就绪 (最长 5 分钟)..."
kubectl wait --for=condition=ready pod -l app=longhorn-manager \
-n longhorn-system --timeout=300s || true
echo -e "${GREEN} ✅ Longhorn 已安装${NC}"
fi
# ============================================================================
# 步骤 3: 安装 JuiceFS CSI Driver (使用官方 deploy/k8s.yaml, 不走 helm)
# ============================================================================
echo -e "${GREEN}[3/4] JuiceFS CSI Driver...${NC}"
if [ "$SKIP_JUICEFS_CSI" = "1" ]; then
echo " 跳过 (--skip-juicefs-csi)"
elif kubectl get ds -n kube-system juicefs-csi-driver-node &>/dev/null \
|| kubectl get ds -n kube-system juicefs-csi-node &>/dev/null; then
echo -e "${GREEN} ✅ 已安装${NC}"
else
JUICEFS_YAML=$(ls "$SCRIPT_DIR/juicefs-csi/juicefs-csi-"*.yaml 2>/dev/null | head -1)
if [ -z "$JUICEFS_YAML" ]; then
echo -e "${RED}错误: 未找到 juicefs-csi/juicefs-csi-*.yaml${NC}"; exit 1
fi
# registry 模式: manifest 里所有 image 前面加上私有 registry 前缀
APPLY_YAML="$JUICEFS_YAML"
if [ "$MODE" = "registry" ]; then
APPLY_YAML="/tmp/juicefs-csi-rewritten.yaml"
sed -E \
-e "s#(image:\s*)juicedata/#\1${REGISTRY}/juicedata/#g" \
-e "s#(image:\s*)registry\.k8s\.io/sig-storage/#\1${THIRDPARTY_REGISTRY}/sig-storage/#g" \
"$JUICEFS_YAML" > "$APPLY_YAML"
fi
echo " 部署 JuiceFS CSI ($(basename $JUICEFS_YAML))..."
kubectl apply -f "$APPLY_YAML"
# 设置 mount 镜像 (CE 版本, 与 RCoder 使用的 juicefs 客户端对齐)
MOUNT_IMG="${THIRDPARTY_REGISTRY:+$THIRDPARTY_REGISTRY/}juicedata/mount:ce-v1.3.1"
for ds in juicefs-csi-driver-node juicefs-csi-node; do
kubectl -n kube-system get daemonset/$ds &>/dev/null && \
kubectl -n kube-system set env daemonset/$ds -c juicefs-plugin \
JUICEFS_CE_MOUNT_IMAGE="$MOUNT_IMG" \
JUICEFS_MOUNT_IMAGE="$MOUNT_IMG" >/dev/null 2>&1 || true
done
kubectl -n kube-system get statefulset/juicefs-csi-controller &>/dev/null && \
kubectl -n kube-system set env statefulset/juicefs-csi-controller -c juicefs-plugin \
JUICEFS_CE_MOUNT_IMAGE="$MOUNT_IMG" \
JUICEFS_MOUNT_IMAGE="$MOUNT_IMG" >/dev/null 2>&1 || true
echo " 等待 CSI 就绪 (最长 2 分钟)..."
for label in "app=juicefs-csi-driver-node" "app=juicefs-csi-node"; do
kubectl wait --for=condition=ready pod -l "$label" -n kube-system --timeout=120s 2>/dev/null && break || true
done
echo -e "${GREEN} ✅ JuiceFS CSI 已安装${NC}"
fi
# ============================================================================
# 步骤 4: 安装 RCoder
# ============================================================================
echo -e "${GREEN}[4/4] 安装 RCoder...${NC}"
RCODER_CHART=$(ls "$SCRIPT_DIR/charts/rcoder-"*.tgz 2>/dev/null | head -1)
if [ -z "$RCODER_CHART" ]; then
echo -e "${RED}错误: 未找到 charts/rcoder-*.tgz${NC}"; exit 1
fi
VALUES_ARGS=(-f "$SCRIPT_DIR/values-$ENV.yaml")
if [ "$MODE" = "registry" ]; then
VALUES_ARGS+=(
--set "global.imageRegistry=$REGISTRY"
--set "global.thirdPartyRegistry=$THIRDPARTY_REGISTRY"
)
fi
echo " helm upgrade --install $RELEASE_NAME ..."
helm upgrade --install "$RELEASE_NAME" "$RCODER_CHART" \
--namespace "$NAMESPACE" \
--create-namespace \
"${VALUES_ARGS[@]}"
echo -e "${GREEN} 等待 rcoder deployment 就绪 (最长 5 分钟)...${NC}"
kubectl rollout status deploy/rcoder -n "$NAMESPACE" --timeout=300s || true
# ============================================================================
# 收尾
# ============================================================================
echo ""
echo -e "${GREEN}=========================================="
echo " 部署完成!"
echo "==========================================${NC}"
kubectl get pods -n "$NAMESPACE"
echo ""
NODE_IP=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}' 2>/dev/null || echo "<node-ip>")
NODE_PORT=$(kubectl get svc rcoder -n "$NAMESPACE" -o jsonpath='{.spec.ports[0].nodePort}' 2>/dev/null || echo "<nodeport>")
echo "访问: curl http://${NODE_IP}:${NODE_PORT}/health"
echo "日志: kubectl logs -n $NAMESPACE -l app=rcoder -f"
echo "卸载: helm uninstall $RELEASE_NAME --namespace $NAMESPACE"

View File

@@ -0,0 +1,73 @@
#!/bin/bash
# ============================================================================
# 把 docker daemon 里已加载的镜像 re-tag 并推送到私有 registry
#
# 输入: images.txt (一行一个镜像)
# 行为:
# - RCoder 自有镜像 (nuwax-docker-images-registry.cn-hangzhou.cr.aliyuncs.com/dev/*)
# -> 推到 $REGISTRY/{name}:{tag}
# - 其他 (postgres / minio / juicedata / sig-storage / longhornio 等)
# -> 推到 $THIRDPARTY_REGISTRY/{repo}:{tag}
#
# 依赖: docker (镜像已 load 在本地 daemon)
# ============================================================================
set -e
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
REGISTRY=""
THIRDPARTY_REGISTRY=""
IMAGES_FILE=""
while [ $# -gt 0 ]; do
case "$1" in
--registry) REGISTRY="$2"; shift 2 ;;
--thirdparty-registry) THIRDPARTY_REGISTRY="$2"; shift 2 ;;
--images-file) IMAGES_FILE="$2"; shift 2 ;;
*) echo "未知参数: $1"; exit 1 ;;
esac
done
[ -z "$REGISTRY" ] && { echo -e "${RED}--registry 必填${NC}"; exit 1; }
[ -z "$THIRDPARTY_REGISTRY" ] && THIRDPARTY_REGISTRY="$REGISTRY"
[ -z "$IMAGES_FILE" ] && { echo -e "${RED}--images-file 必填${NC}"; exit 1; }
[ ! -f "$IMAGES_FILE" ] && { echo -e "${RED}未找到 $IMAGES_FILE${NC}"; exit 1; }
# RCoder 自有镜像的 registry 前缀 (用于识别 + 剥离)
RCODER_OWN_PREFIX="nuwax-docker-images-registry.cn-hangzhou.cr.aliyuncs.com/dev/"
echo -e "${GREEN}Re-tag + push 镜像到私有 registry${NC}"
echo " 自有镜像 -> $REGISTRY/"
echo " 第三方 -> $THIRDPARTY_REGISTRY/"
while IFS= read -r line; do
# 跳过注释和空行
line="${line%%#*}"; line="${line##[[:space:]]}"; line="${line%%[[:space:]]}"
[ -z "$line" ] && continue
SRC="$line"
DST=""
if [[ "$SRC" == "$RCODER_OWN_PREFIX"* ]]; then
# 自有镜像: 剥离前缀, 推到 REGISTRY
STRIPPED="${SRC#$RCODER_OWN_PREFIX}"
DST="$REGISTRY/$STRIPPED"
else
# 第三方: 保留完整 repo 路径, 推到 THIRDPARTY_REGISTRY
# 处理 registry.k8s.io/sig-storage/xxx —— 去掉 registry.k8s.io/ 保留 sig-storage/xxx
REPO="$SRC"
case "$REPO" in
registry.k8s.io/*) REPO="${REPO#registry.k8s.io/}" ;;
docker.io/*) REPO="${REPO#docker.io/}" ;;
quay.io/*) REPO="${REPO#quay.io/}" ;;
esac
DST="$THIRDPARTY_REGISTRY/$REPO"
fi
echo -e "${YELLOW} $SRC${NC}"
echo " => $DST"
docker tag "$SRC" "$DST"
docker push "$DST"
done < "$IMAGES_FILE"
echo -e "${GREEN}✅ 所有镜像已推送到私有 registry${NC}"

View File

@@ -0,0 +1,110 @@
## registry2本地私有镜像仓库
本目录用 docker compose 起一个 `registry:2` 容器,作为局域网内的镜像中转站——既供 docker push 测试用,也可配到 k3s 节点作首选 mirror。
### 地址
- **私有仓库**`192.168.32.228:5000`(本机启动后局域网可达)
- **Web UI**`http://192.168.32.228:5080`(浏览器访问;用于查看仓库/镜像/Tag 等)
### 启动/停止
```bash
cd /home/swufe/gitworkspace/rcoder/k8s/register2
docker compose up -d
docker compose ps
docker compose logs -f
docker compose down
```
数据落地在同目录下的 `./data/` compose 已声明 volume
---
## 作为 k3s 的镜像 mirror
### 架构
```
k3s/containerd (两个节点)
├──(1st) http://192.168.32.228:5000 ← registry2 (本目录, 私有推送)
├──(2nd) http://192.168.32.228:5002 ← registry-cache (pull-through, 可选)
├──(3rd) https://docker.m.daocloud.io / ... ← 公网 cn mirror 回退
└──(4th) https://*.docker.io / registry.k8s.io (默认兜底)
```
containerd 按顺序尝试 endpoint404 自动回退到下一个。
### 一键配置两个节点
```bash
# 1. 启动 registry2
cd k8s/register2 && docker compose up -d
# 2. 给每个 k3s 节点配 mirror (用 REGISTRY_HOST 环境变量)
sudo REGISTRY_HOST=192.168.32.228:5000 \
k8s/scripts/install-k3s-registry-mirrors-cn.sh
# 脚本会:
# - 备份已有 /etc/rancher/k3s/registries.yaml (如有)
# - 写入新配置 (本地 registry 作首选, 公网 cn mirror 兜底)
# - 自动 restart k3s 或 k3s-agent
# - 用 crictl pull 验证 mirror 是否生效
# 3. 配置本机 docker daemon 允许 push 到 HTTP registry
# 编辑 /etc/docker/daemon.json:
# {
# "insecure-registries": ["192.168.32.228:5000"]
# }
sudo systemctl restart docker
```
### 多个 registry 串联
如果同时有 registry2 (5000, 私有存储) 和 registry-cache (5002, pull-through 缓存)
```bash
sudo REGISTRY_HOST=192.168.32.228:5000,192.168.32.228:5002 \
k8s/scripts/install-k3s-registry-mirrors-cn.sh
```
### 验证
```bash
# 1) 推个镜像进去看 5000 是否可用
docker tag busybox:1.36 192.168.32.228:5000/busybox:test
docker push 192.168.32.228:5000/busybox:test
curl -s http://192.168.32.228:5000/v2/_catalog
# 2) k3s 节点上: 用 crictl 拉 (与 kubelet 一致的链路)
sudo crictl --runtime-endpoint unix:///run/k3s/containerd/containerd.sock \
pull 192.168.32.228:5000/busybox:test
# 3) 验证 mirror 路由 (通过 docker.io 间接拉)
sudo crictl --runtime-endpoint unix:///run/k3s/containerd/containerd.sock \
pull docker.io/rancher/mirrored-pause:3.6
```
---
## 与离线 bundle 的联动
配好 registry mirror 后bundle 的 `--mode=registry` 可以直接推到 5000
```bash
tar xzf dist/rcoder-offline-*.tar.gz -C /tmp/offline-test
cd /tmp/offline-test
docker load -i images/all-images.tar
bash rewrite-registry.sh \
--registry 192.168.32.228:5000/rcoder \
--thirdparty-registry 192.168.32.228:5000/thirdparty \
--images-file images/images.txt
# 之后 helm install 时 k3s 会自动从本地 registry 拉, 走内网千兆
helm install rcoder-offline k8s/helm/rcoder \
-f k8s/helm/rcoder/values-dev.yaml \
-f k8s/helm/rcoder/values-offline.yaml \
--set global.imageRegistry=192.168.32.228:5000/rcoder \
--set global.thirdPartyRegistry=192.168.32.228:5000/thirdparty \
--namespace nuwax-rcoder-offline-test --create-namespace
```

View File

@@ -0,0 +1,26 @@
services:
registry2:
image: registry:2
container_name: registry2
restart: unless-stopped
ports:
- "5000:5000"
environment:
REGISTRY_STORAGE_DELETE_ENABLED: "true"
volumes:
- ./data:/var/lib/registry
- ./registry-config.yml:/etc/docker/registry/config.yml:ro
registry-ui:
image: joxit/docker-registry-ui:latest
container_name: registry2-ui
restart: unless-stopped
ports:
- "5080:80"
environment:
SINGLE_REGISTRY: "true"
REGISTRY_TITLE: "registry2"
# UI 在浏览器端直接请求 registry这里必须是浏览器可达的地址
REGISTRY_URL: "http://192.168.32.228:5000"
depends_on:
- registry2

View File

@@ -0,0 +1,14 @@
version: 0.1
log:
fields:
service: registry2
storage:
filesystem:
rootdirectory: /var/lib/registry
http:
addr: :5000
headers:
Access-Control-Allow-Origin: ["*"]
Access-Control-Allow-Methods: ["HEAD", "GET", "OPTIONS", "DELETE"]
Access-Control-Allow-Headers: ["Authorization", "Accept", "Cache-Control", "Content-Type", "Origin", "User-Agent"]
Access-Control-Expose-Headers: ["Docker-Content-Digest"]

View File

@@ -0,0 +1,101 @@
#!/bin/bash
# ============================================================
# JuiceFS + MinIO + PostgreSQL 部署脚本
# 使用 Kustomize overlay (dev / prod) 部署,避免集群级资源冲突
# 用法 (可在任意目录运行):
# ENV=dev k8s/scripts/deploy-juicefs.sh # 部署到 nuwax-rcoder-dev默认
# ENV=prod k8s/scripts/deploy-juicefs.sh # 部署到 nuwax-rcoder-prod
# ============================================================
set -e
ENV="${ENV:-dev}"
case "$ENV" in
dev|prod) ;;
*)
echo "Error: ENV 必须是 dev 或 prod当前值: $ENV"
exit 1
;;
esac
# 路径解析:脚本位于 k8s/scripts/manifests 位于 k8s/manifests/
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
K8S_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
NAMESPACE="${NAMESPACE:-nuwax-rcoder-${ENV}}"
MANIFESTS_DIR="${K8S_ROOT}/manifests"
OVERLAY_DIR="${MANIFESTS_DIR}/overlays/${ENV}"
echo "=========================================="
echo " RCoder - JuiceFS 存储部署 (${ENV})"
echo " namespace: ${NAMESPACE}"
echo " overlay: ${OVERLAY_DIR}"
echo "=========================================="
# 检查 kubectl
if ! command -v kubectl &> /dev/null; then
echo "Error: kubectl not found"
exit 1
fi
# 检查 kubectl kustomize 插件
if ! kubectl kustomize --help &> /dev/null; then
echo "Error: kubectl kustomize not found (需要 kubectl 1.14+)"
exit 1
fi
# 部署 JuiceFS CSI Driver
echo ""
echo "[1/6] 检查 JuiceFS CSI Driver..."
if ! kubectl get daemonset juicefs-csi-driver-node -n kube-system &> /dev/null; then
echo "JuiceFS CSI Driver 未部署,是否部署? (y/n)"
read -r response
if [[ "$response" =~ ^[Yy]$ ]]; then
helm repo add juicefs https://juicefs.github.io/charts || true
helm repo update
helm install juicefs-csi-driver juicefs/juicefs-csi-driver \
--namespace kube-system \
--set webhook.enabled=false
echo "等待 JuiceFS CSI Driver 就绪..."
kubectl rollout status daemonset/juicefs-csi-driver-node -n kube-system --timeout=120s
fi
fi
# 使用 Kustomize overlay 部署
echo ""
echo "[2/6] 使用 Kustomize overlay 部署..."
kubectl apply -k "${OVERLAY_DIR}"
# 等待存储层就绪
echo ""
echo "[3/6] 等待 PostgreSQL 就绪..."
kubectl wait --for=condition=ready pod -l app=postgresql -n ${NAMESPACE} --timeout=180s 2>/dev/null || echo "⚠️ PostgreSQL 等待超时"
echo ""
echo "[4/6] 等待 MinIO 就绪..."
kubectl wait --for=condition=ready pod -l app=minio -n ${NAMESPACE} --timeout=180s 2>/dev/null || echo "⚠️ MinIO 等待超时"
echo ""
echo "[5/6] 等待 MinIO Bucket 初始化..."
kubectl wait --for=condition=complete job/minio-init -n ${NAMESPACE} --timeout=120s 2>/dev/null || echo "⚠️ MinIO 初始化等待超时"
# 验证部署
echo ""
echo "[6/6] 验证部署状态..."
echo ""
echo "StorageClass:"
kubectl get sc | grep juicefs || echo " (未找到)"
echo ""
echo "PVC:"
kubectl get pvc -n ${NAMESPACE}
echo ""
echo "Pods:"
kubectl get pods -n ${NAMESPACE}
echo ""
echo "=========================================="
echo " 部署完成!"
echo "=========================================="

View File

@@ -0,0 +1,192 @@
#!/usr/bin/env bash
# 为 K3s 配置中国大陆常用的 containerd 镜像加速docker.io / registry.k8s.io / ghcr.io / quay.io
# 用法(在 K3s 节点上):
# sudo k8s/scripts/install-k3s-registry-mirrors-cn.sh
#
# 可选: 本地内网 registry 作首选 mirror (命中时走千兆内网)
# sudo REGISTRY_HOST=192.168.32.228:5000 k8s/scripts/install-k3s-registry-mirrors-cn.sh
# # 多个 registry 用逗号隔开, 按顺序作为 endpoint:
# sudo REGISTRY_HOST=192.168.32.228:5000,192.168.32.228:5002 k8s/scripts/install-k3s-registry-mirrors-cn.sh
#
# 可选:禁止回退到 Docker Hub / GHCR 官方地址(对已配置 mirrors 的仓库不再走官方 endpoint
# sudo env INSTALL_DISABLE_REGISTRY_FALLBACK=1 k8s/scripts/install-k3s-registry-mirrors-cn.sh
#
# 完成后会重启 k3s短暂影响调度中的 Pod。
set -euo pipefail
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
if [[ "${EUID:-}" -ne 0 ]]; then
echo -e "${RED}请使用 root 运行: sudo $0${NC}"
exit 1
fi
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SRC="${SCRIPT_DIR}/k3s-registries-cn.yaml"
DEST="/etc/rancher/k3s/registries.yaml"
if [[ ! -f "$SRC" ]]; then
echo -e "${RED}找不到模板: $SRC${NC}"
exit 1
fi
install -d /etc/rancher/k3s
# 备份现有配置 (如果有)
if [[ -f "$DEST" ]]; then
BACKUP="${DEST}.bak.$(date +%s)"
cp -a "$DEST" "$BACKUP"
echo -e "${YELLOW}已备份现有配置:${NC} $BACKUP"
fi
# ---- 可选: 生成带本地 registry 的配置 ----
if [[ -n "${REGISTRY_HOST:-}" ]]; then
echo -e "${GREEN}使用本地 registry 前缀:${NC} $REGISTRY_HOST"
IFS=',' read -ra LOCAL_REGS <<< "$REGISTRY_HOST"
# 纯粹用 printf 累加真实换行 (不依赖命令替换保留 \n)
_emit_endpoints() {
for r in "${LOCAL_REGS[@]}"; do
printf ' - "http://%s"\n' "$r"
done
}
_emit_self_mirrors() {
for r in "${LOCAL_REGS[@]}"; do
printf ' "%s":\n endpoint:\n - "http://%s"\n' "$r" "$r"
done
}
_emit_self_configs() {
for r in "${LOCAL_REGS[@]}"; do
printf ' "%s":\n tls:\n insecure_skip_verify: true\n' "$r"
done
}
{
cat <<EOF
# 由 install-k3s-registry-mirrors-cn.sh 生成 (REGISTRY_HOST=${REGISTRY_HOST})
# 文档: https://docs.k3s.io/installation/private-registry
mirrors:
"docker.io":
endpoint:
EOF
_emit_endpoints
cat <<'EOF'
- "https://docker.m.daocloud.io"
- "https://docker.1ms.run"
"registry.k8s.io":
endpoint:
EOF
_emit_endpoints
cat <<'EOF'
- "https://registry.aliyuncs.com/google_containers"
- "https://k8s.m.daocloud.io"
"ghcr.io":
endpoint:
EOF
_emit_endpoints
cat <<'EOF'
- "https://ghcr.m.daocloud.io"
"quay.io":
endpoint:
EOF
_emit_endpoints
cat <<'EOF'
- "https://quay.m.daocloud.io"
"nuwax-docker-images-registry.cn-hangzhou.cr.aliyuncs.com":
endpoint:
EOF
_emit_endpoints
cat <<'EOF'
- "https://nuwax-docker-images-registry.cn-hangzhou.cr.aliyuncs.com"
# 自指: 允许 "crictl pull <host>/foo" 走 plain HTTP
EOF
_emit_self_mirrors
cat <<'EOF'
configs:
EOF
_emit_self_configs
} > "$DEST"
else
cp -a "$SRC" "$DEST"
fi
chmod 0644 "$DEST"
echo -e "${GREEN}已写入:${NC} $DEST"
echo -e "${YELLOW}内容预览:${NC}"
cat "$DEST"
# 可选:对已在 registries.yaml 中声明的仓库,不再回退到官方默认 endpoint见 K3s 文档 Default Endpoint Fallback
if [[ "${INSTALL_DISABLE_REGISTRY_FALLBACK:-0}" == "1" ]]; then
install -d /etc/rancher/k3s/config.yaml.d
DROPIN="/etc/rancher/k3s/config.yaml.d/99-rcoder-disable-default-registry-endpoint.yaml"
cat >"$DROPIN" <<'EOF'
disable-default-registry-endpoint: true
EOF
chmod 0644 "$DROPIN"
echo ""
echo -e "${GREEN}已写入 K3s 配置片段:${NC} $DROPIN"
echo -e "${YELLOW}(仅对 registries.yaml 里配置过 mirrors 的仓库生效;避免回退到被墙地址导致长时间卡住)${NC}"
fi
if systemctl is-active --quiet k3s 2>/dev/null; then
echo ""
echo -e "${YELLOW}正在重启 k3s 使镜像配置生效...${NC}"
systemctl restart k3s
echo -e "${GREEN}k3s 已重启。${NC}"
elif systemctl is-active --quiet k3s-agent 2>/dev/null; then
echo ""
echo -e "${YELLOW}正在重启 k3s-agent...${NC}"
systemctl restart k3s-agent
echo -e "${GREEN}k3s-agent 已重启。${NC}"
else
echo -e "${YELLOW}未检测到 k3s / k3s-agent 服务处于 active请手动重启:${NC}"
echo " systemctl restart k3s"
echo " # 或 agent 节点:"
echo " systemctl restart k3s-agent"
fi
echo ""
echo -e "${YELLOW}说明:${NC} ${GREEN}k3s ctr images pull${NC} 往往 ${RED}不会${NC} 使用 /etc/rancher/k3s/registries.yaml"
echo " 仍会直连 registry-1.docker.io因此在国内容易误报「镜像加速没生效」。"
echo -e " 与 kubelet/CRI 一致的正确验证方式是用 ${GREEN}crictl pull${NC}"
RUNTIME_SOCK=""
for s in /run/k3s/containerd/containerd.sock /var/run/k3s/containerd/containerd.sock; do
if [[ -S "$s" ]]; then
RUNTIME_SOCK="$s"
break
fi
done
if [[ -n "$RUNTIME_SOCK" ]]; then
echo ""
echo -e "${GREEN}验证拉取(推荐):${NC}"
echo " sudo crictl --runtime-endpoint unix://${RUNTIME_SOCK} pull docker.io/rancher/mirrored-pause:3.6"
if command -v crictl &>/dev/null; then
echo ""
echo -e "${YELLOW}正在尝试自动拉取 pause 镜像(失败则忽略,你可手动执行上一行)...${NC}"
if crictl --runtime-endpoint "unix://${RUNTIME_SOCK}" pull docker.io/rancher/mirrored-pause:3.6; then
echo -e "${GREEN}crictl 拉取成功:镜像加速已对 CRI/kubelet 生效。${NC}"
else
echo -e "${RED}crictl 拉取失败,请把完整终端输出保存后排查(勿用 k3s ctr 判断)。${NC}"
fi
else
echo -e "${YELLOW}未安装 crictl可安装后再验证。${NC}"
fi
else
echo ""
echo -e "${GREEN}验证拉取(推荐):${NC}"
echo " sudo crictl --runtime-endpoint unix:///run/k3s/containerd/containerd.sock pull docker.io/rancher/mirrored-pause:3.6"
fi

View File

@@ -0,0 +1,63 @@
# 复制到节点: /etc/rancher/k3s/registries.yaml 后执行: sudo systemctl restart k3s
#
# 注意: 「k3s ctr images pull」通常不走本文件验证请用 crictl与 kubelet 一致),例如:
# sudo crictl --runtime-endpoint unix:///run/k3s/containerd/containerd.sock pull docker.io/rancher/mirrored-pause:3.6
#
# 文档: https://docs.k3s.io/installation/private-registry
#
# ============================================================================
# 可选: 内网本地 registry 加速
# ----------------------------------------------------------------------------
# 如果你在局域网起了 registry:2 (见 k8s/register2/docker-compose.yml), 建议把它
# 作为首选 mirror —— 命中时走内网 (~千兆), 未命中自动回退到公网 cn mirror。
#
# 使用 install-k3s-registry-mirrors-cn.sh 时, 设置 REGISTRY_HOST=<ip>:<port> 会
# 自动生成包含本地 registry 的完整配置; 或者手动把下方注释掉的 `- "http://..."`
# 行取消注释并改成你的 registry 地址。
# ============================================================================
mirrors:
"docker.io":
endpoint:
# - "http://192.168.32.228:5000" # 本地私有 (按需 push 加速专用镜像)
# - "http://192.168.32.228:5002" # 本地 pull-through 缓存
- "https://docker.m.daocloud.io"
- "https://docker.1ms.run"
"registry.k8s.io":
endpoint:
# - "http://192.168.32.228:5000"
- "https://registry.aliyuncs.com/google_containers"
- "https://k8s.m.daocloud.io"
"ghcr.io":
endpoint:
# - "http://192.168.32.228:5000"
- "https://ghcr.m.daocloud.io"
"quay.io":
endpoint:
# - "http://192.168.32.228:5000"
- "https://quay.m.daocloud.io"
# RCoder 自有阿里云镜像 —— 本地 registry 命中时走内网
# "nuwax-docker-images-registry.cn-hangzhou.cr.aliyuncs.com":
# endpoint:
# - "http://192.168.32.228:5000"
# - "https://nuwax-docker-images-registry.cn-hangzhou.cr.aliyuncs.com"
# 自指 mirror: 允许 "crictl pull 192.168.32.228:5000/foo" 直接走 plain HTTP
# "192.168.32.228:5000":
# endpoint:
# - "http://192.168.32.228:5000"
# "192.168.32.228:5002":
# endpoint:
# - "http://192.168.32.228:5002"
# configs:
# "192.168.32.228:5000":
# tls:
# insecure_skip_verify: true
# "192.168.32.228:5002":
# tls:
# insecure_skip_verify: true

View File

@@ -0,0 +1,57 @@
#!/bin/bash
set -e
NAMESPACE="${NAMESPACE:-nuwax-rcoder-dev}"
echo "=== Testing RCoder in K8s ==="
# 获取 NodePort
NODE_PORT=$(kubectl get svc rcoder -n "${NAMESPACE}" -o jsonpath='{.spec.ports[0].nodePort}')
echo "NodePort: $NODE_PORT"
# 获取节点 IP优先使用第一个 IPv4避免 dual-stack 返回多个地址导致 URL 拼接失败)
NODE_IP=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}' | awk '{print $1}')
echo "Node IP: $NODE_IP"
BASE_URL="http://${NODE_IP}:${NODE_PORT}"
echo ""
echo "Testing /health endpoint..."
curl -s "${BASE_URL}/health" | jq .
echo ""
echo "Testing /chat endpoint..."
CHAT_RESP=$(curl -s -X POST "${BASE_URL}/chat" \
-H "Content-Type: application/json" \
-d '{"prompt": "hello"}')
echo "${CHAT_RESP}" | jq .
PROJECT_ID=$(echo "${CHAT_RESP}" | jq -r '.data.project_id // empty')
echo ""
echo "Testing /computer/chat endpoint..."
USER_ID="k8s-test-user"
COMPUTER_RESP=$(curl -s -X POST "${BASE_URL}/computer/chat" \
-H "Content-Type: application/json" \
-d "{\"user_id\":\"${USER_ID}\",\"prompt\":\"hello from k8s\"}")
echo "${COMPUTER_RESP}" | jq .
COMPUTER_PROJECT_ID=$(echo "${COMPUTER_RESP}" | jq -r '.data.project_id // empty')
echo ""
echo "Verifying created pods by labels..."
if [ -n "${PROJECT_ID}" ]; then
kubectl get pods -n "${NAMESPACE}" -l "project_id=${PROJECT_ID}" -o wide
fi
kubectl get pods -n "${NAMESPACE}" -l "user_id=${USER_ID}" -o wide
echo ""
echo "Verifying pod status API..."
curl -s "${BASE_URL}/computer/pod/status?user_id=${USER_ID}" | jq .
if [ -n "${PROJECT_ID}" ]; then
curl -s "${BASE_URL}/computer/pod/status?project_id=${PROJECT_ID}" | jq .
fi
if [ -n "${COMPUTER_PROJECT_ID}" ]; then
curl -s "${BASE_URL}/computer/pod/status?project_id=${COMPUTER_PROJECT_ID}" | jq .
fi
echo ""
echo "=== Test complete ==="

View File

@@ -0,0 +1,30 @@
#!/bin/bash
# ============================================================
# K8s 资源清理脚本 (Kustomize)
# 用法:
# ENV=dev ./undeploy.sh # 清理开发环境 (默认)
# ENV=prod ./undeploy.sh # 清理生产环境
# ============================================================
set -e
ENV="${ENV:-dev}"
case "$ENV" in
dev|prod) ;;
*)
echo "Error: ENV 必须是 dev 或 prod当前值: $ENV"
exit 1
;;
esac
# 路径解析:支持从任意 cwd 调用
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
NAMESPACE="${NAMESPACE:-nuwax-rcoder-${ENV}}"
OVERLAY_DIR="${SCRIPT_DIR}/manifests/overlays/${ENV}"
echo "=== 使用 Kustomize 清理 RCoder 资源 (${ENV} 环境, namespace: ${NAMESPACE}) ==="
kubectl delete -k "${OVERLAY_DIR}" --ignore-not-found
echo "=== RCoder 资源已清理 ==="