# funops
**Repository Path**: uprain/funops
## Basic Information
- **Project Name**: funops
- **Description**: No description available
- **Primary Language**: Unknown
- **License**: AGPL-3.0
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2026-09-22
- **Last Updated**: 2026-09-30
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# FunOps

**一个轻量、务实的 CI/CD 与主机运维平台,为需要简单构建部署自动化的团队而生。**
## 简介
FunOps 是一个聚焦日常需求的 CI/CD 平台:项目管理、环境配置、可复用凭证、参数化构建计划、构建记录、实时日志、通知机器人与 GitLab Webhook 触发。目标是在保留 Jenkins 自由风格任务那种直观工作流的同时,让团队对构建状态、执行日志和部署结果有清晰的可见性。
在此基础上,FunOps 扩展了**主机管理**能力:Linux 主机台账、Web 终端(xterm.js + WebSocket)、敏感命令管控(高危命令拒绝/告警 + 全量审计),覆盖「构建 → 部署 → 上机操作」的完整链路。
构建计划支持 **Jenkins 风格流水线**:用 `pipeline.yml` 描述 `agent / stages / when / env / triggers / notify`,编辑器内置语法校验与错误定位,可实时预览流水线流程图;执行位置可选本机运行或指定远程主机运行。项目根目录附带 `demo-gin-app/` 示例项目,可直接用于验证两条执行路径。
## 功能特性
**CI/CD**
- 项目与服务管理
- 环境管理(开发 / 测试 / 预发 / 生产)
- 构建计划配置:分支、参数、阶段、外部脚本仓库、通知
- Jenkins 风格流水线:`pipeline.yml` 编辑(语法校验、错误定位)、流程图实时预览
- 执行位置可选:本机运行,或构建目录上传到指定远程主机执行
- 手动构建与 GitLab Webhook 自动触发
- 构建日志实时流式输出、阶段级日志视图
- 构建记录、仪表盘统计、构建趋势
- 凭证管理:GitLab Token、SSH 密钥、通用 Git 用户名密码、kubeconfig
- 多代码仓库支持:GitLab / GitHub / Gitee / 通用 Git(Codeup 等)
- 通知机器人配置与测试发送(钉钉 / 企业微信 / 飞书 / WebHook)
**主机管理**
- Linux 主机台账:环境分组、负责人、K/V 标签、配置信息采集、批量导入
- Web 终端:浏览器内直接 SSH 到目标主机(xterm.js + WebSocket)
- 敏感命令管控:内置高危命令规则(格式化磁盘、递归强删、关机重启等),
按主机 / 标签 / 角色范围生效,命中后拒绝或告警
- 全量命令审计与终端连接审计
**证书管理**
- SSL 证书监控:到期时间跟踪、周期巡检、异常日志、批量导入、模板下载
**系统管理**
- 用户、角色、权限、LDAP、登录日志、操作日志、安全配置(水印 / 会话超时 / 登录保护)
## 技术栈
| 端 | 技术 |
|---|---|
| 前端 | Vue 3、Ant Design Vue 4.x、Vue Router、Axios、ECharts、Vite、xterm.js |
| 后端 | Python 3.9+、Django 4.2、Uvicorn / ASGI(WebSocket 原生实现)、paramiko、GitPython、python-gitlab、PyJWT、ldap3 |
| 存储 | MySQL 8(唯一必需的外部中间件,不依赖 Redis / 消息队列 / 对象存储) |
## 目录结构
```text
.
|-- funops-server/ # Django 后端与 API(内层 Django 工程包:funops/)
| |-- Dockerfile # 后端镜像(python:3.9-slim + uvicorn)
| |-- conf/config.yml # 后端非敏感默认配置(敏感项全部走环境变量)
| `-- requirements.txt # Python 依赖
|-- funops-web/ # Vue 3 前端应用
| |-- Dockerfile # 前端镜像(Node 构建 → Nginx 托管)
| |-- nginx.conf # 前端容器 Nginx 配置(SPA + /api + /ws 代理)
| `-- vite.config.js # 开发服务器 / 代理配置
|-- demo-gin-app/ # Go Gin 示例项目(流水线测试用)
|-- docs/
| |-- sql/ # funops_init.sql(幂等初始化)+ 增量迁移脚本
| `-- DESIGN.md # 设计文档(各模块设计原理与实现方式)
|-- docker-compose.yml # 方式二:MySQL + 后端 + 前端全容器一键拉起
|-- start.sh # 方式一:本地一键启动 / 停止(MySQL + 后端 + 前端)
|-- .env.example # 环境变量示例(复制为 .env 使用,.env 不入仓库)
`-- README.md
```
## 快速开始
两种启动方式,**任选其一**(不要同时启动,两者都占用后端 `8900` 端口):
| 方式 | 组件位置 | 适用场景 | 入口 |
|---|---|---|---|
| 方式一:本地部署 | MySQL 容器/本机实例 + 后端/前端跑在宿主机 | 源码开发、断点调试、前后端热更新 | `./start.sh` |
| 方式二:全容器部署 | MySQL、后端、前端全部在容器 | 快速体验、演示、部署验证 | `docker compose up -d` |
默认登录账号:**admin / admin123**(首次登录请立即修改密码)
### 方式一:本地一键启动(推荐用于开发)
#### 前置要求
- Python 3.9+
- Node.js 18+
- Docker / OrbStack(**可选**:仅当本机没有 MySQL 时才需要,脚本用它自动拉起 MySQL 容器)
- Git
#### 启动
```bash
git clone https://gitee.com/uprain/funops.git
cd funops
chmod +x start.sh
./start.sh
```
脚本会自动完成:
1. **MySQL**:优先复用本机 `3306` 上已有的 MySQL;端口空闲时用 `docker compose` 拉起 `funops-mysql` 容器(数据存在命名卷 `funops-mysql-data` 里),并等待就绪;检测到没有业务表时自动导入 `docs/sql/funops_init.sql`
2. **后端**:不存在 `funops-server/.venv` 时自动创建,依赖缺失或 `requirements.txt` 变更时自动安装,然后以 uvicorn 启动
3. **前端**:`funops-web/node_modules` 缺失时自动 `npm install`,然后启动 Vite dev server
4. 逐项做真实健康检查(后端 HTTP 探活、前端 HTTP 探活),全部就绪后才提示启动成功
停止 / 查看状态:
```bash
./start.sh --stop # 停止后端与前端(MySQL 容器若由脚本拉起则一并停止,数据保留)
./start.sh --status # 查看三个组件的运行状态
./start.sh --restart # 先停再起
./start.sh --help # 查看全部参数
```
#### 访问地址
```text
前端界面 http://localhost:8001
后端 API http://localhost:8900/api/
MySQL 127.0.0.1:3306 (库名 funops)
```
#### 可覆盖的环境变量
```bash
FUNOPS_ENV=dev # 运行环境 dev / test / prod
BACKEND_PORT=8900 # 后端端口
WEB_PORT=8001 # 前端端口
MYSQL_PORT=3306 # MySQL 端口
FUNOPS_MYSQL_PASSWORD=funopspasswd123 # MySQL root 密码(测试默认值)
FUNOPS_SECRET_KEY=... # JWT 密钥(test/prod 必填;dev 缺省用内置开发密钥)
PIP_INDEX=https://mirrors.aliyun.com/pypi/simple/ # pip 镜像
NPM_REGISTRY=https://registry.npmmirror.com # npm 镜像
```
例:`WEB_PORT=9000 BACKEND_PORT=9100 ./start.sh`
#### 手动分步启动(不用脚本)
```bash
# 1) MySQL(已有 MySQL 可跳过,只需导入一次表结构)
docker compose up -d mysql
docker exec -i funops-mysql mysql --default-character-set=utf8mb4 -uroot -pfunopspasswd123 funops < docs/sql/funops_init.sql
# 2) 后端
cd funops-server
python3 -m venv .venv
.venv/bin/python -m pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/
.venv/bin/python -m uvicorn funops.asgi:application --host 0.0.0.0 --port 8900
# 3) 前端(另开一个终端)
cd funops-web
npm install --registry=https://registry.npmmirror.com
npm run dev
```
### 方式二:Docker Compose 全容器启动
#### 前置要求
- Docker / OrbStack(内存建议 ≥ 4G)
#### 启动
```bash
git clone https://gitee.com/uprain/funops.git
cd funops
docker compose up -d # 首次会构建前后端镜像,约需几分钟
docker compose ps # 三个容器都应为 running / healthy
```
首次启动时 MySQL 数据卷为空,会自动执行 `docs/sql/funops_init.sql` 完成建库建表与默认数据写入。
#### 访问地址
```text
前端界面 http://localhost:8080 (WEB_PORT 可覆盖)
后端 API http://localhost:8900 (BACKEND_PORT 可覆盖)
MySQL 127.0.0.1:3306 (MYSQL_PORT 可覆盖)
```
前端容器内的 Nginx 同时托管静态文件并把 `/api/`、`/ws/` 反向代理到后端容器,因此浏览器只访问 `8080` 一个端口,**没有跨域配置**。
#### 常用命令
```bash
docker compose logs -f funops-server # 后端日志(含构建任务执行日志)
docker compose logs -f funops-web # Nginx 访问日志
docker compose exec funops-server tail -f /app/logs/django.log
docker compose restart funops-server # 重启后端
docker compose down # 停止并删除容器(数据保留)
docker compose up -d --build # 代码更新后重新构建并启动
```
> ⚠️ 不要用 `docker compose down -v`:`-v` 会删除 `funops-mysql-data` 卷,数据库需重新导入。
#### 容器部署的功能边界
- 后端容器内**没有** `dockerd` / `kubectl` / `maven`。构建计划若勾选「本机执行」且阶段里用到这些命令,会失败;
此时请把执行位置设为「远程主机」,或改用方式一(本地部署,直接用宿主机工具链)。
- 构建产物保存在命名卷 `funops-build-data`(容器内 `/data`),容器重建不丢。
## 国内部署与镜像源
仓库里的部署脚本与 Dockerfile **默认已全部切换到国内源**,国内服务器可直接构建,无需改任何文件:
| 环节 | 默认源 | 位置 |
| --- | --- | --- |
| Python 依赖 | 阿里云 PyPI `mirrors.aliyun.com/pypi/simple` | `funops-server/Dockerfile`、`start.sh` |
| Node 依赖 | npmmirror `registry.npmmirror.com`(`package-lock.json` 的 resolved 同为该源) | `funops-web/Dockerfile`、`start.sh` |
| Debian apt | 阿里云 `mirrors.aliyun.com`(自动替换 `deb.debian.org` **和** `security.debian.org`) | `funops-server/Dockerfile` |
| Alpine apk / Go 模块 | 阿里云 Alpine、`goproxy.cn` | `demo-gin-app/Dockerfile` |
### 想换源
```bash
# 单独构建时
docker build --build-arg APT_MIRROR=mirrors.tuna.tsinghua.edu.cn \
--build-arg PIP_INDEX=https://pypi.tuna.tsinghua.edu.cn/simple \
./funops-server
# 或用 .env / 环境变量覆盖(compose 会自动读取)
# PIP_INDEX=... NPM_REGISTRY=... APT_MIRROR=...
```
### 构建中断:`Temporary failure resolving 'mirrors.aliyun.com'`
这是**容器里的 DNS 解析失败**,不是镜像源的问题——日志里通常会同时看到另一个域名也解析不了
(比如 `security.debian.org`)。先确认宿主自己是好的:
```bash
# 1) 宿主能否解析(正常应返回 IP)
getent hosts mirrors.aliyun.com
# 2) 容器里能否解析(失败=容器网络 DNS 有问题,此时换任何源都没用)
docker run --rm alpine getent hosts mirrors.aliyun.com
# 或 alpine 也没拉取成功时:docker run --rm busybox nslookup mirrors.aliyun.com
```
修复二选一:
**① daemon 级指定 DNS(推荐,对所有容器与构建同时生效)**
```bash
sudo tee /etc/docker/daemon.json <<'EOF'
{
"dns": ["223.5.5.5", "119.29.29.29", "114.114.114.114"]
}
EOF
sudo systemctl daemon-reload && sudo systemctl restart docker
```
**② 让构建直接用宿主网络**(源文件不用改,只是换个启动方式):
```bash
docker build --network host -t funops/server:latest ./funops-server
```
或在 `docker-compose.yml` 每个 `build:` 下加一行 `network: host`(**需要 Compose v2 的 `docker compose`**,
已停止维护的 `docker-compose` v1 不支持该字段):
```yaml
funops-server:
build:
context: ./funops-server
network: host
```
> 根因通常是 daemon 默认的 DNS 转发链路断了(宿主 `/etc/resolv.conf` 指向某个容器访问不到的
> 地址,或 firewalld/iptables 规则把桥接网络的 DNS 挡掉了)。这类问题只改 Dockerfile 治不了。
### 拉不动 Docker Hub 基础镜像
国内服务器常见现象:`python:3.9-slim`、`node:22-alpine`、`mysql:8` 拉取超时。
两种解法,选一个即可:
**① 给 Docker daemon 配镜像加速(推荐,对所有镜像生效)**
```bash
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json <<'EOF'
{
"registry-mirrors": ["https://<你的加速地址>"]
}
EOF
sudo systemctl restart docker
```
加速地址优先用**云厂商给的个人专属地址**(阿里云:容器镜像服务控制台 → 镜像加速器,
形如 `https://xxxxxx.mirror.aliyuncs.com`),也可以配公共加速服务。**这类地址生命周期普遍很短,
下面这份清单会过期,请务必先验证再用**:
```bash
for m in docker.1ms.run docker.m.daocloud.io docker.xuanyuan.me docker.1panel.live \
docker.nju.edu.cn mirror.baidubce.com hub-mirror.c.163.com mirror.ccs.tencentyun.com; do
printf "%-30s %s\n" "$m" "$(curl -s -o /dev/null -m 8 -w '%{http_code}' https://$m/v2/)"
done
```
返回 `401` 即健康(registry v2 的鉴权挑战),`000` / `403` / 超时说明在你这条网络上不可用。
挑 2~3 个可用的填进 `registry-mirrors`,Docker 会按顺序重试:
```bash
sudo tee /etc/docker/daemon.json <<'EOF'
{
"dns": ["223.5.5.5", "119.29.29.29"],
"registry-mirrors": [
"https://docker.1ms.run",
"https://docker.m.daocloud.io",
"https://docker.xuanyuan.me"
]
}
EOF
sudo systemctl daemon-reload && sudo systemctl restart docker
docker info | grep -A4 "Registry Mirrors" # 确认生效
docker pull hello-world # 实测一次
```
配好后本仓库无需任何改动,直接 `docker compose up -d --build`。
**② 只覆盖本项目用到的镜像(不想动 daemon 配置时)**
在 `.env` 里写完整镜像地址即可:
```bash
PYTHON_BASE_IMAGE=<加速前缀>/python:3.9-slim
NODE_BASE_IMAGE=<加速前缀>/node:22-alpine
NGINX_BASE_IMAGE=<加速前缀>/nginx:1.27-alpine
MYSQL_IMAGE=<加速前缀>/mysql:8
```
> 之所以不把这些前缀写死在 Dockerfile 里:第三方公共加速地址生命周期普遍很短,
> 写死就是给半年后的自己埋雷。默认值保持官方镜像名,需要时用一个环境变量切换,改回来也只是一行。
## 配置说明
**敏感项全部走环境变量,仓库内不存放任何密钥/密码。**
### 多环境(FUNOPS_ENV)
| 环境 | debug / CORS 放开 | SECRET_KEY |
|---|---|---|
| `dev`(默认) | 开 | 未设置时用内置开发密钥(禁止用于生产) |
| `test` | 开 | **必须显式设置** `FUNOPS_SECRET_KEY`,否则后端拒绝启动 |
| `prod` | 关 | **必须显式设置** `FUNOPS_SECRET_KEY`,否则后端拒绝启动 |
### 环境变量清单
复制仓库根目录 `.env.example` 为 `.env` 后按需修改(`.env` 已被 git 忽略;`./start.sh` 与 `docker compose` 都会自动读取,已 export 的变量优先):
```text
FUNOPS_ENV # dev / test / prod(默认 dev)
FUNOPS_SECRET_KEY # JWT 签名密钥(test/prod 必填)
FUNOPS_DEBUG # 调试开关(默认按环境决定)
FUNOPS_ALLOWED_HOSTS # 允许的 Host,逗号分隔(默认 '*')
FUNOPS_CORS_ALLOW_ALL # CORS 放开全部来源(默认按环境决定)
FUNOPS_MYSQL_HOST # 默认 127.0.0.1(容器内为 mysql)
FUNOPS_MYSQL_PORT # 默认 3306
FUNOPS_MYSQL_DB # 默认 funops
FUNOPS_MYSQL_USER # 默认 root
FUNOPS_MYSQL_PASSWORD # 默认 funopspasswd123(仅测试环境;生产务必修改)
FUNOPS_BUILD_ROOT # 构建产物根目录,默认 funops-server/../data
```
`funops-server/conf/config.yml` 只保留**非敏感默认值**(host/port/库名/字符集等),环境变量优先级高于它。新增中间件(如 Redis)时照此模式:config.yml 加一节 + `FUNOPS__*` 环境变量。
> 注意:MySQL root 密码在**数据卷首次初始化时**确定。已有 `funops-mysql-data` 卷的环境修改 `FUNOPS_MYSQL_PASSWORD` 不会改变卷内 root 密码,需要进容器改密或删卷重建。
## 数据库初始化
表结构以 SQL 为准,**项目不使用 Django migration**:
- `docs/sql/funops_init.sql`:幂等初始化脚本。全新环境自动建库建表并写入默认数据(管理员账号、角色权限、内置高危命令规则、菜单权限等);已有环境重复导入不会丢数据,缺失的表 / 列 / 默认数据会自动补齐。
- `docs/sql/2026-*.sql`:增量迁移脚本,按文件名日期顺序执行。导入时建议显式指定字符集,否则中文会变 `????`:
```bash
docker exec -i funops-mysql mysql --default-character-set=utf8mb4 -uroot -pfunopspasswd123 funops < docs/sql/xxx.sql
```
修改 `funops-server/apps/models.py` 后,需要同步修改 SQL(或在库里手工建表),否则运行期会报表不存在。
## 二次开发说明
- **前端接口地址**:开发环境下 `vite.config.js` 已把 `/api`、`/ws` 代理到后端(默认 `127.0.0.1:8900`),前端代码统一使用相对路径调用接口即可,无需配置 `VITE_API_URL`、无跨域问题。后端不在本机时用环境变量调整:
```bash
BACKEND_HOST=192.168.1.10 BACKEND_PORT=8900 npm run dev
```
- **构建任务执行位置**:
- 「本机」指 FunOps 后端所在机器(本地部署即宿主机,容器部署即 funops-server 容器)
- 「远程主机」通过 `agent_hosts` 指定主机名或 `agent_label` 指定标签,多台按顺序逐台执行,走 SSH 上传构建目录后流式执行
- **流水线定义**:数据库中的结构化字段是唯一事实源,`pipeline.yml` 是它的编辑视图,两者由 `funops-server/apps/utils/pipeline_yml.py` 双向转换。
- **前端开发规范**:主题令牌集中在 `funops-web/src/assets/css/theme.css`(`--fp-*`),页面样式优先复用令牌,避免硬编码颜色。
## 常见问题排查
**端口被占用**
```bash
lsof -nP -iTCP:8900 -sTCP:LISTEN # 后端
lsof -nP -iTCP:8001 -sTCP:LISTEN # 前端
# 或直接换端口
WEB_PORT=9000 BACKEND_PORT=9100 ./start.sh
```
**前端页面能打开但接口全 404 / 502**
说明 `/api` 没代理到后端。本地部署检查后端是否在 `BACKEND_PORT` 上监听;容器部署检查 `docker compose ps` 里 funops-server 是否 healthy。
**后端启动报数据库连接失败**
先确认 MySQL 已就绪,再核对 `.env`(参考 `.env.example`)或 `FUNOPS_MYSQL_*` 环境变量。容器内连的是服务名 `mysql`,宿主机连的是 `127.0.0.1`。
**导入 SQL 后中文变成问号**
没指定字符集,重导一次并加上 `--default-character-set=utf8mb4`。
**Web 终端连不上**
Web 终端依赖 WebSocket,需要 `/ws/` 的代理支持 Upgrade。本地部署由 Vite 代理处理;容器部署由 `funops-web/nginx.conf` 的 `/ws/` 段处理。若前面还挂了外层 Nginx,记得同样透传 `Upgrade` / `Connection` 头。
**日志位置**
```text
本地部署 logs/backend.log、logs/web.log(脚本输出);funops-server/logs/django.log
容器部署 docker compose logs -f funops-server;容器内 /app/logs/
```
## 项目状态
FunOps 仍在积极开发中,核心工作流已经可用,功能与细节持续完善。欢迎在使用中反馈问题与建议。
## License
本项目基于 [LICENSE](LICENSE) 协议开源。