diff --git a/agent/Makefile b/agent/Makefile index 9d9be4c..7e0071e 100644 --- a/agent/Makefile +++ b/agent/Makefile @@ -10,14 +10,14 @@ LDFLAGS := -s -w -X main.version=$(VERSION) -X main.buildTime=$(BUILD_TIME) # 静态编译:关闭 CGO 并使用静态链接 LINUX_LDFLAGS := -extldflags "-static" -.PHONY: build cross build-linux-amd64 build-linux-arm64 tidy run fmt vet clean help +.PHONY: build cross build-linux-amd64 build-linux-arm64 build-darwin-arm64 tidy run fmt vet clean help ## build: 本地构建当前平台二进制 build: go build -ldflags "$(LDFLAGS)" -o $(APP) . -## cross: 交叉编译 Linux amd64 与 arm64(静态链接,适合容器/嵌入式部署) -cross: build-linux-amd64 build-linux-arm64 +## cross: 交叉编译 Linux amd64/arm64 + macOS arm64 +cross: build-linux-amd64 build-linux-arm64 build-darwin-arm64 ## build-linux-amd64: 交叉编译 linux/amd64 build-linux-amd64: @@ -29,6 +29,11 @@ build-linux-arm64: CGO_ENABLED=0 GOOS=linux GOARCH=arm64 \ go build -ldflags "$(LDFLAGS) $(LINUX_LDFLAGS)" -o bin/$(APP)-linux-arm64 . +## build-darwin-arm64: 编译 macOS/arm64 (Apple Silicon) // Logan.Li +build-darwin-arm64: + CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 \ + go build -ldflags "$(LDFLAGS)" -o bin/$(APP)-darwin-arm64 . + ## tidy: 整理依赖 tidy: go mod tidy diff --git a/agent/deploy/install.sh b/agent/deploy/install.sh index e74e6b0..96d2562 100644 --- a/agent/deploy/install.sh +++ b/agent/deploy/install.sh @@ -1,6 +1,6 @@ #!/bin/bash -# AIOps Agent 一键安装脚本 — 支持 RHEL/CentOS/Rocky/Alma/Amazon/Aliyun/Debian/Ubuntu 全系列 -# 用法: curl -fsSL http://YOUR_SERVER/agent/install.sh | bash -s -- --server http://YOUR_SERVER:8080 --token TOKEN +# AIOps Agent 一键安装脚本 — 支持 Linux(systemd) 和 macOS(launchd) +# 用法: curl -fsSL http://YOUR_SERVER/agent/install.sh | sudo bash -s -- --server http://YOUR_SERVER:8080 --token TOKEN # 署名 Logan.Li set -e @@ -10,8 +10,24 @@ AIOPS_SERVER="" AGENT_TOKEN="" INSTALL_DIR="/usr/local/bin" SERVICE_NAME="aiops-agent" +PLIST_LABEL="com.ezops.aiops-agent" ARCH=$(uname -m) -OS=$(cat /etc/os-release 2>/dev/null | grep '^ID=' | cut -d'"' -f2 || echo "unknown") +KERNEL=$(uname -s) # Linux 或 Darwin + +# 检测 OS 显示名 +detect_os() { + if [ "$KERNEL" = "Darwin" ]; then + echo "$(sw_vers -productName 2>/dev/null || echo 'macOS') $(sw_vers -productVersion 2>/dev/null || echo '')" + elif [ -f /etc/os-release ]; then + . /etc/os-release 2>/dev/null && echo "${PRETTY_NAME:-${NAME:-Linux}}" || echo "Linux" + else + echo "Linux" + fi +} +OS_DISPLAY=$(detect_os) + +# OS 小写 (linux / darwin) +OS_LOWER=$(echo "$KERNEL" | tr '[:upper:]' '[:lower:]') # 解析命令行参数 while [[ $# -gt 0 ]]; do @@ -30,14 +46,17 @@ echo " ╔═══════════════════════ echo " ║ AIOps Agent 一键安装 ║" echo " ╚══════════════════════════════════════════╝" echo "" -echo " 系统: ${OS} (${ARCH})" +echo " 系统: ${OS_DISPLAY} (${ARCH})" +echo " 内核: ${KERNEL}" echo " 版本: ${AGENT_VERSION}" echo " 后端: ${AIOPS_SERVER:-未指定}" echo "" # 检查 root 权限 -if [ "$EUID" -ne 0 ]; then - echo "❌ 请使用 root 用户或 sudo 执行此脚本" +if [ "$(id -u)" -ne 0 ]; then + echo "❌ 请使用 root 或 sudo 执行此脚本" + echo " 推荐用法: curl -fsSL | sudo bash -s -- --server --token " + echo " (sudo 放在 bash 前,不是 curl 前)" exit 1 fi @@ -50,10 +69,10 @@ case "$ARCH" in exit 1 ;; esac -echo " 匹配: linux/${DOWNLOAD_ARCH}" +echo " 匹配: ${OS_LOWER}/${DOWNLOAD_ARCH}" echo "" -# 安装依赖工具(curl 在大多数发行版自带,这里确保有) +# 安装依赖工具 if command -v curl &> /dev/null; then echo "✓ curl 已安装" else @@ -64,6 +83,8 @@ else dnf install -y curl elif command -v apt-get &> /dev/null; then apt-get update -qq && apt-get install -y curl + elif command -v brew &> /dev/null; then + brew install curl else echo "❌ 无法确定包管理器,请手动安装 curl" exit 1 @@ -76,11 +97,15 @@ if [ -z "$AIOPS_SERVER" ]; then exit 1 fi -BINARY_URL="${AIOPS_SERVER}/agent/aiops-agent-linux-${DOWNLOAD_ARCH}" +BINARY_URL="${AIOPS_SERVER}/agent/aiops-agent-${OS_LOWER}-${DOWNLOAD_ARCH}" echo "→ 下载 Agent: ${BINARY_URL}" curl -fSL -o /tmp/aiops-agent "${BINARY_URL}" || { - echo "❌ 下载失败,请检查后端地址和网络" + echo "❌ 下载失败: ${BINARY_URL}" + echo " 请检查: 1) 后端地址是否正确 2) 该平台是否有对应二进制" + if [ "$OS_LOWER" = "darwin" ]; then + echo " 提示: macOS 二进制需要后端编译 darwin 版本 (make build-darwin-arm64)" + fi exit 1 } chmod +x /tmp/aiops-agent @@ -88,13 +113,79 @@ mv /tmp/aiops-agent "${INSTALL_DIR}/aiops-agent" echo "✓ 安装到 ${INSTALL_DIR}/aiops-agent" -# 配置 systemd 服务 if [ -z "$AGENT_TOKEN" ]; then echo "⚠ 未提供 --token,使用空令牌" AGENT_TOKEN="none" fi -cat > /etc/systemd/system/${SERVICE_NAME}.service << EOF +# ─── 平台分支: Linux(systemd) / macOS(launchd) ─── + +if [ "$OS_LOWER" = "darwin" ]; then + # ===== macOS: launchd 服务 ===== + PLIST_PATH="/Library/LaunchDaemons/${PLIST_LABEL}.plist" + + mkdir -p "$(dirname "$PLIST_PATH")" + + cat > "$PLIST_PATH" << EOF + + + + + Label + ${PLIST_LABEL} + ProgramArguments + + ${INSTALL_DIR}/aiops-agent + -server + ${AIOPS_SERVER} + -interval + 10 + -token + ${AGENT_TOKEN} + + RunAtLoad + + KeepAlive + + StandardOutPath + /var/log/aiops-agent.log + StandardErrorPath + /var/log/aiops-agent.err.log + + +EOF + + echo "✓ 创建 launchd 服务 ($PLIST_PATH)" + + # 停止旧实例(如果存在) + launchctl unload "$PLIST_PATH" 2>/dev/null || true + # 加载并启动 + launchctl load -w "$PLIST_PATH" + + sleep 2 + + # 检查运行状态 + if launchctl list | grep -q "$PLIST_LABEL"; then + echo "" + echo "✅ 安装成功!Agent 已启动并设置为开机自启" + echo "" + echo " 查看状态: launchctl list | grep aiops" + echo " 查看日志: tail -f /var/log/aiops-agent.log" + echo " 手动停止: launchctl unload $PLIST_PATH" + echo " 手动启动: launchctl load -w $PLIST_PATH" + echo " 卸载: launchctl unload $PLIST_PATH && rm -f ${INSTALL_DIR}/aiops-agent $PLIST_PATH" + echo "" + else + echo "❌ Agent 启动失败,请检查日志:" + echo " cat /var/log/aiops-agent.err.log" + exit 1 + fi + +else + # ===== Linux: systemd 服务 ===== + SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service" + + cat > "$SERVICE_FILE" << EOF [Unit] Description=AIOps Monitoring Agent After=network-online.target @@ -111,25 +202,26 @@ LimitNOFILE=65536 WantedBy=multi-user.target EOF -echo "✓ 创建 systemd 服务" + echo "✓ 创建 systemd 服务" -# 启动服务 -systemctl daemon-reload -systemctl enable ${SERVICE_NAME} -systemctl restart ${SERVICE_NAME} + # 启动服务 + systemctl daemon-reload + systemctl enable ${SERVICE_NAME} + systemctl restart ${SERVICE_NAME} -sleep 2 + sleep 2 -if systemctl is-active --quiet ${SERVICE_NAME}; then - echo "" - echo "✅ 安装成功!Agent 已启动并设置为开机自启" - echo "" - echo " 查看状态: systemctl status ${SERVICE_NAME}" - echo " 查看日志: journalctl -u ${SERVICE_NAME} -f" - echo " 卸载: ${0} --uninstall || systemctl stop ${SERVICE_NAME} && rm -f ${INSTALL_DIR}/aiops-agent /etc/systemd/system/${SERVICE_NAME}.service && systemctl daemon-reload" - echo "" -else - echo "❌ Agent 启动失败,请检查日志:" - echo " journalctl -u ${SERVICE_NAME} -n 20" - exit 1 + if systemctl is-active --quiet ${SERVICE_NAME}; then + echo "" + echo "✅ 安装成功!Agent 已启动并设置为开机自启" + echo "" + echo " 查看状态: systemctl status ${SERVICE_NAME}" + echo " 查看日志: journalctl -u ${SERVICE_NAME} -f" + echo " 卸载: systemctl stop ${SERVICE_NAME} && rm -f ${INSTALL_DIR}/aiops-agent ${SERVICE_FILE} && systemctl daemon-reload" + echo "" + else + echo "❌ Agent 启动失败,请检查日志:" + echo " journalctl -u ${SERVICE_NAME} -n 20" + exit 1 + fi fi diff --git a/backend/agent/bin/aiops-agent-darwin-arm64 b/backend/agent/bin/aiops-agent-darwin-arm64 new file mode 100755 index 0000000..4d2aa08 Binary files /dev/null and b/backend/agent/bin/aiops-agent-darwin-arm64 differ diff --git a/backend/agent/deploy/install.sh b/backend/agent/deploy/install.sh index e74e6b0..96d2562 100755 --- a/backend/agent/deploy/install.sh +++ b/backend/agent/deploy/install.sh @@ -1,6 +1,6 @@ #!/bin/bash -# AIOps Agent 一键安装脚本 — 支持 RHEL/CentOS/Rocky/Alma/Amazon/Aliyun/Debian/Ubuntu 全系列 -# 用法: curl -fsSL http://YOUR_SERVER/agent/install.sh | bash -s -- --server http://YOUR_SERVER:8080 --token TOKEN +# AIOps Agent 一键安装脚本 — 支持 Linux(systemd) 和 macOS(launchd) +# 用法: curl -fsSL http://YOUR_SERVER/agent/install.sh | sudo bash -s -- --server http://YOUR_SERVER:8080 --token TOKEN # 署名 Logan.Li set -e @@ -10,8 +10,24 @@ AIOPS_SERVER="" AGENT_TOKEN="" INSTALL_DIR="/usr/local/bin" SERVICE_NAME="aiops-agent" +PLIST_LABEL="com.ezops.aiops-agent" ARCH=$(uname -m) -OS=$(cat /etc/os-release 2>/dev/null | grep '^ID=' | cut -d'"' -f2 || echo "unknown") +KERNEL=$(uname -s) # Linux 或 Darwin + +# 检测 OS 显示名 +detect_os() { + if [ "$KERNEL" = "Darwin" ]; then + echo "$(sw_vers -productName 2>/dev/null || echo 'macOS') $(sw_vers -productVersion 2>/dev/null || echo '')" + elif [ -f /etc/os-release ]; then + . /etc/os-release 2>/dev/null && echo "${PRETTY_NAME:-${NAME:-Linux}}" || echo "Linux" + else + echo "Linux" + fi +} +OS_DISPLAY=$(detect_os) + +# OS 小写 (linux / darwin) +OS_LOWER=$(echo "$KERNEL" | tr '[:upper:]' '[:lower:]') # 解析命令行参数 while [[ $# -gt 0 ]]; do @@ -30,14 +46,17 @@ echo " ╔═══════════════════════ echo " ║ AIOps Agent 一键安装 ║" echo " ╚══════════════════════════════════════════╝" echo "" -echo " 系统: ${OS} (${ARCH})" +echo " 系统: ${OS_DISPLAY} (${ARCH})" +echo " 内核: ${KERNEL}" echo " 版本: ${AGENT_VERSION}" echo " 后端: ${AIOPS_SERVER:-未指定}" echo "" # 检查 root 权限 -if [ "$EUID" -ne 0 ]; then - echo "❌ 请使用 root 用户或 sudo 执行此脚本" +if [ "$(id -u)" -ne 0 ]; then + echo "❌ 请使用 root 或 sudo 执行此脚本" + echo " 推荐用法: curl -fsSL | sudo bash -s -- --server --token " + echo " (sudo 放在 bash 前,不是 curl 前)" exit 1 fi @@ -50,10 +69,10 @@ case "$ARCH" in exit 1 ;; esac -echo " 匹配: linux/${DOWNLOAD_ARCH}" +echo " 匹配: ${OS_LOWER}/${DOWNLOAD_ARCH}" echo "" -# 安装依赖工具(curl 在大多数发行版自带,这里确保有) +# 安装依赖工具 if command -v curl &> /dev/null; then echo "✓ curl 已安装" else @@ -64,6 +83,8 @@ else dnf install -y curl elif command -v apt-get &> /dev/null; then apt-get update -qq && apt-get install -y curl + elif command -v brew &> /dev/null; then + brew install curl else echo "❌ 无法确定包管理器,请手动安装 curl" exit 1 @@ -76,11 +97,15 @@ if [ -z "$AIOPS_SERVER" ]; then exit 1 fi -BINARY_URL="${AIOPS_SERVER}/agent/aiops-agent-linux-${DOWNLOAD_ARCH}" +BINARY_URL="${AIOPS_SERVER}/agent/aiops-agent-${OS_LOWER}-${DOWNLOAD_ARCH}" echo "→ 下载 Agent: ${BINARY_URL}" curl -fSL -o /tmp/aiops-agent "${BINARY_URL}" || { - echo "❌ 下载失败,请检查后端地址和网络" + echo "❌ 下载失败: ${BINARY_URL}" + echo " 请检查: 1) 后端地址是否正确 2) 该平台是否有对应二进制" + if [ "$OS_LOWER" = "darwin" ]; then + echo " 提示: macOS 二进制需要后端编译 darwin 版本 (make build-darwin-arm64)" + fi exit 1 } chmod +x /tmp/aiops-agent @@ -88,13 +113,79 @@ mv /tmp/aiops-agent "${INSTALL_DIR}/aiops-agent" echo "✓ 安装到 ${INSTALL_DIR}/aiops-agent" -# 配置 systemd 服务 if [ -z "$AGENT_TOKEN" ]; then echo "⚠ 未提供 --token,使用空令牌" AGENT_TOKEN="none" fi -cat > /etc/systemd/system/${SERVICE_NAME}.service << EOF +# ─── 平台分支: Linux(systemd) / macOS(launchd) ─── + +if [ "$OS_LOWER" = "darwin" ]; then + # ===== macOS: launchd 服务 ===== + PLIST_PATH="/Library/LaunchDaemons/${PLIST_LABEL}.plist" + + mkdir -p "$(dirname "$PLIST_PATH")" + + cat > "$PLIST_PATH" << EOF + + + + + Label + ${PLIST_LABEL} + ProgramArguments + + ${INSTALL_DIR}/aiops-agent + -server + ${AIOPS_SERVER} + -interval + 10 + -token + ${AGENT_TOKEN} + + RunAtLoad + + KeepAlive + + StandardOutPath + /var/log/aiops-agent.log + StandardErrorPath + /var/log/aiops-agent.err.log + + +EOF + + echo "✓ 创建 launchd 服务 ($PLIST_PATH)" + + # 停止旧实例(如果存在) + launchctl unload "$PLIST_PATH" 2>/dev/null || true + # 加载并启动 + launchctl load -w "$PLIST_PATH" + + sleep 2 + + # 检查运行状态 + if launchctl list | grep -q "$PLIST_LABEL"; then + echo "" + echo "✅ 安装成功!Agent 已启动并设置为开机自启" + echo "" + echo " 查看状态: launchctl list | grep aiops" + echo " 查看日志: tail -f /var/log/aiops-agent.log" + echo " 手动停止: launchctl unload $PLIST_PATH" + echo " 手动启动: launchctl load -w $PLIST_PATH" + echo " 卸载: launchctl unload $PLIST_PATH && rm -f ${INSTALL_DIR}/aiops-agent $PLIST_PATH" + echo "" + else + echo "❌ Agent 启动失败,请检查日志:" + echo " cat /var/log/aiops-agent.err.log" + exit 1 + fi + +else + # ===== Linux: systemd 服务 ===== + SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service" + + cat > "$SERVICE_FILE" << EOF [Unit] Description=AIOps Monitoring Agent After=network-online.target @@ -111,25 +202,26 @@ LimitNOFILE=65536 WantedBy=multi-user.target EOF -echo "✓ 创建 systemd 服务" + echo "✓ 创建 systemd 服务" -# 启动服务 -systemctl daemon-reload -systemctl enable ${SERVICE_NAME} -systemctl restart ${SERVICE_NAME} + # 启动服务 + systemctl daemon-reload + systemctl enable ${SERVICE_NAME} + systemctl restart ${SERVICE_NAME} -sleep 2 + sleep 2 -if systemctl is-active --quiet ${SERVICE_NAME}; then - echo "" - echo "✅ 安装成功!Agent 已启动并设置为开机自启" - echo "" - echo " 查看状态: systemctl status ${SERVICE_NAME}" - echo " 查看日志: journalctl -u ${SERVICE_NAME} -f" - echo " 卸载: ${0} --uninstall || systemctl stop ${SERVICE_NAME} && rm -f ${INSTALL_DIR}/aiops-agent /etc/systemd/system/${SERVICE_NAME}.service && systemctl daemon-reload" - echo "" -else - echo "❌ Agent 启动失败,请检查日志:" - echo " journalctl -u ${SERVICE_NAME} -n 20" - exit 1 + if systemctl is-active --quiet ${SERVICE_NAME}; then + echo "" + echo "✅ 安装成功!Agent 已启动并设置为开机自启" + echo "" + echo " 查看状态: systemctl status ${SERVICE_NAME}" + echo " 查看日志: journalctl -u ${SERVICE_NAME} -f" + echo " 卸载: systemctl stop ${SERVICE_NAME} && rm -f ${INSTALL_DIR}/aiops-agent ${SERVICE_FILE} && systemctl daemon-reload" + echo "" + else + echo "❌ Agent 启动失败,请检查日志:" + echo " journalctl -u ${SERVICE_NAME} -n 20" + exit 1 + fi fi diff --git a/backend/internal/api/handlers/k8s.go b/backend/internal/api/handlers/k8s.go index 9e8e023..c80a28e 100644 --- a/backend/internal/api/handlers/k8s.go +++ b/backend/internal/api/handlers/k8s.go @@ -349,3 +349,39 @@ func (h *K8sHandler) ListIngresses(c *gin.Context) { Data: ingresses, }) } + +// GetTopology 获取集群拓扑布局 // Logan.Li +func (h *K8sHandler) GetTopology(c *gin.Context) { + clusterID := c.Param("id") + var layout models.TopologyLayout + result := h.db.Where("cluster_id = ?", clusterID).First(&layout) + if result.Error != nil { + c.JSON(http.StatusOK, models.Response{Code: 200, Data: &models.TopologyLayout{}}) + return + } + c.JSON(http.StatusOK, models.Response{Code: 200, Data: layout}) +} + +// SaveTopology 保存集群拓扑布局 // Logan.Li +func (h *K8sHandler) SaveTopology(c *gin.Context) { + clusterID, _ := strconv.Atoi(c.Param("id")) + var req struct { + LayoutData string `json:"layout_data"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误"}) + return + } + + var layout models.TopologyLayout + h.db.Where("cluster_id = ?", clusterID).First(&layout) + layout.ClusterID = uint(clusterID) + layout.LayoutData = req.LayoutData + if layout.ID == 0 { + h.db.Create(&layout) + } else { + h.db.Save(&layout) + } + + c.JSON(http.StatusOK, models.Response{Code: 200, Message: "保存成功"}) +} diff --git a/backend/internal/api/handlers/role.go b/backend/internal/api/handlers/role.go new file mode 100644 index 0000000..f9f791b --- /dev/null +++ b/backend/internal/api/handlers/role.go @@ -0,0 +1,168 @@ +package handlers + +import ( + "net/http" + "strconv" + + "ezops/internal/models" + + "github.com/gin-gonic/gin" +) + +// CreateRole 创建角色 // Logan.Li +// POST /api/v1/admin/roles +func (h *UserHandler) CreateRole(c *gin.Context) { + var req models.CreateRoleRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, models.Response{ + Code: 400, + Message: "请求参数错误: " + err.Error(), + }) + return + } + + // 名称 / 编码唯一性校验 + var existing models.Role + if err := h.db.Where("name = ? OR code = ?", req.Name, req.Code).First(&existing).Error; err == nil { + c.JSON(http.StatusBadRequest, models.Response{ + Code: 400, + Message: "角色名称或编码已存在", + }) + return + } + + role := models.Role{ + Name: req.Name, + Code: req.Code, + Description: req.Description, + Status: 1, + } + if err := h.db.Create(&role).Error; err != nil { + c.JSON(http.StatusInternalServerError, models.Response{ + Code: 500, + Message: "创建角色失败", + }) + return + } + + // 关联权限 + if len(req.PermissionIDs) > 0 { + var perms []models.Permission + h.db.Find(&perms, req.PermissionIDs) + h.db.Model(&role).Association("Permissions").Append(perms) + } + + h.db.Preload("Permissions").First(&role, role.ID) + c.JSON(http.StatusOK, models.Response{ + Code: 200, + Message: "创建成功", + Data: role, + }) +} + +// UpdateRole 更新角色 // Logan.Li +// PUT /api/v1/admin/roles/:id +func (h *UserHandler) UpdateRole(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "无效的角色 ID"}) + return + } + + var req models.UpdateRoleRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, models.Response{ + Code: 400, + Message: "请求参数错误: " + err.Error(), + }) + return + } + + var role models.Role + if err := h.db.First(&role, id).Error; err != nil { + c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "角色不存在"}) + return + } + + updates := map[string]interface{}{} + if req.Name != "" { + // 名称 / 编码唯一性校验(排除自身) + var dup models.Role + if err := h.db.Where("(name = ? OR code = ?) AND id != ?", req.Name, req.Code, id).First(&dup).Error; err == nil { + c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "角色名称或编码已存在"}) + return + } + updates["name"] = req.Name + } + if req.Code != "" { + updates["code"] = req.Code + } + if req.Description != nil { + updates["description"] = *req.Description + } + if req.Status != nil { + updates["status"] = *req.Status + } + + if len(updates) > 0 { + if err := h.db.Model(&role).Updates(updates).Error; err != nil { + c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "更新失败"}) + return + } + } + + // 替换权限关联 + if req.PermissionIDs != nil { + var perms []models.Permission + if len(req.PermissionIDs) > 0 { + h.db.Find(&perms, req.PermissionIDs) + } + h.db.Model(&role).Association("Permissions").Replace(perms) + } + + h.db.Preload("Permissions").First(&role, id) + c.JSON(http.StatusOK, models.Response{ + Code: 200, + Message: "更新成功", + Data: role, + }) +} + +// DeleteRole 删除角色 // Logan.Li +// DELETE /api/v1/admin/roles/:id +func (h *UserHandler) DeleteRole(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "无效的角色 ID"}) + return + } + + var role models.Role + if err := h.db.First(&role, id).Error; err != nil { + c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "角色不存在"}) + return + } + + // 清空权限关联后再删除(避免 join 表残留) + h.db.Model(&role).Association("Permissions").Clear() + if err := h.db.Delete(&role).Error; err != nil { + c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除失败"}) + return + } + + c.JSON(http.StatusOK, models.Response{ + Code: 200, + Message: "删除成功", + }) +} + +// ListPermissions 获取权限列表(供前端角色权限选择) // Logan.Li +// GET /api/v1/admin/permissions +func (h *UserHandler) ListPermissions(c *gin.Context) { + var perms []models.Permission + h.db.Where("status = ?", 1).Order("sort ASC, id ASC").Find(&perms) + c.JSON(http.StatusOK, models.Response{ + Code: 200, + Data: models.ListResponse{List: perms, Total: int64(len(perms))}, + }) +} diff --git a/backend/internal/api/handlers/user_admin.go b/backend/internal/api/handlers/user_admin.go index fd6003e..defc65d 100644 --- a/backend/internal/api/handlers/user_admin.go +++ b/backend/internal/api/handlers/user_admin.go @@ -341,7 +341,7 @@ func (h *UserHandler) BatchImportUsers(c *gin.Context) { // ListRoles 获取角色列表(供前端下拉选择) // Logan.Li func (h *UserHandler) ListRoles(c *gin.Context) { var roles []models.Role - h.db.Where("status = ?", 1).Order("id ASC").Find(&roles) + h.db.Where("status = ?", 1).Preload("Permissions").Order("id ASC").Find(&roles) c.JSON(http.StatusOK, models.Response{ Code: 200, Data: models.ListResponse{List: roles, Total: int64(len(roles))}, }) diff --git a/backend/internal/api/routes/routes.go b/backend/internal/api/routes/routes.go index e437aea..7b6c7ca 100644 --- a/backend/internal/api/routes/routes.go +++ b/backend/internal/api/routes/routes.go @@ -26,14 +26,16 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config) { r.StaticFile("/agent/install.sh", "./agent/deploy/install.sh") r.StaticFile("/agent/aiops-agent-linux-amd64", "./agent/bin/aiops-agent-linux-amd64") r.StaticFile("/agent/aiops-agent-linux-arm64", "./agent/bin/aiops-agent-linux-arm64") + r.StaticFile("/agent/aiops-agent-darwin-arm64", "./agent/bin/aiops-agent-darwin-arm64") // macOS Apple Silicon // Logan.Li r.GET("/agent/arch", func(c *gin.Context) { c.JSON(200, gin.H{ "version": "1.0.0", "downloads": gin.H{ - "amd64": "/agent/aiops-agent-linux-amd64", - "arm64": "/agent/aiops-agent-linux-arm64", + "linux-amd64": "/agent/aiops-agent-linux-amd64", + "linux-arm64": "/agent/aiops-agent-linux-arm64", + "darwin-arm64": "/agent/aiops-agent-darwin-arm64", }, - "install": "curl -fsSL http://:8080/agent/install.sh | bash -s -- --server http://:8080 --token ", + "install": "curl -fsSL http://:8080/agent/install.sh | sudo bash -s -- --server http://:8080 --token ", }) }) @@ -70,6 +72,15 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config) { admin.DELETE("/users/:id", authHandler.DeleteUser) admin.POST("/users/batch-import", authHandler.BatchImportUsers) admin.GET("/users/import-template", authHandler.ImportTemplate) + + // 角色管理路由 // Logan.Li + admin.GET("/roles", authHandler.ListRoles) + admin.POST("/roles", authHandler.CreateRole) + admin.PUT("/roles/:id", authHandler.UpdateRole) + admin.DELETE("/roles/:id", authHandler.DeleteRole) + + // 权限列表(供角色权限选择) + admin.GET("/permissions", authHandler.ListPermissions) } // LDAP 配置路由 @@ -198,6 +209,8 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config) { clusters.GET("/:id/deployments", k8sHandler.ListDeployments) clusters.GET("/:id/services", k8sHandler.ListServices) clusters.GET("/:id/ingresses", k8sHandler.ListIngresses) + clusters.GET("/:id/topology", k8sHandler.GetTopology) // 获取拓扑布局 // Logan.Li + clusters.PUT("/:id/topology", k8sHandler.SaveTopology) // 保存拓扑布局 // Logan.Li } // 告警路由 diff --git a/backend/internal/models/k8s.go b/backend/internal/models/k8s.go index f2e3953..b073d91 100644 --- a/backend/internal/models/k8s.go +++ b/backend/internal/models/k8s.go @@ -193,3 +193,12 @@ type K8sClusterStats struct { TotalPods int64 `json:"total_pods"` TotalServices int64 `json:"total_services"` } + +// TopologyLayout 拓扑布局(保存节点位置 + 自定义节点 + 自定义连线) // Logan.Li +type TopologyLayout struct { + ID uint `gorm:"primarykey" json:"id"` + ClusterID uint `gorm:"uniqueIndex;not null" json:"cluster_id"` + LayoutData string `gorm:"type:text" json:"layout_data"` // JSON: {positions, customNodes, customEdges} + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} diff --git a/backend/internal/models/user.go b/backend/internal/models/user.go index 4f37f87..c1e8e3b 100644 --- a/backend/internal/models/user.go +++ b/backend/internal/models/user.go @@ -105,6 +105,23 @@ type UpdateUserRequest struct { RoleIDs []uint `json:"role_ids"` } +// CreateRoleRequest 创建角色请求 // Logan.Li +type CreateRoleRequest struct { + Name string `json:"name" binding:"required,min=1,max=64"` + Code string `json:"code" binding:"required,min=1,max=64"` + Description string `json:"description"` + PermissionIDs []uint `json:"permission_ids"` +} + +// UpdateRoleRequest 更新角色请求 // Logan.Li +type UpdateRoleRequest struct { + Name string `json:"name" binding:"omitempty,min=1,max=64"` + Code string `json:"code" binding:"omitempty,min=1,max=64"` + Description *string `json:"description"` + Status *int `json:"status"` + PermissionIDs []uint `json:"permission_ids"` +} + // ChangePasswordRequest 修改密码请求 type ChangePasswordRequest struct { OldPassword string `json:"old_password" binding:"required"` diff --git a/backend/pkg/database/database.go b/backend/pkg/database/database.go index 30f7ffa..13df5a1 100644 --- a/backend/pkg/database/database.go +++ b/backend/pkg/database/database.go @@ -102,5 +102,6 @@ func AutoMigrate(db *gorm.DB) error { &models.LogEntry{}, &models.SQLAuditRecord{}, &models.SQLAuditRule{}, + &models.TopologyLayout{}, // 拓扑布局保存 // Logan.Li ) } diff --git a/frontend/src/components/cloud/RegionManager.tsx b/frontend/src/components/cloud/RegionManager.tsx index b592139..fa599a7 100644 --- a/frontend/src/components/cloud/RegionManager.tsx +++ b/frontend/src/components/cloud/RegionManager.tsx @@ -284,7 +284,7 @@ export function RegionManager() { {r.location || '-'} - {r.endpoint || '-'} + {r.endpoint || '-'} {r.status === 'active' ? ( diff --git a/frontend/src/components/deploy/AppRelease.tsx b/frontend/src/components/deploy/AppRelease.tsx index 227b0f6..e5c5573 100644 --- a/frontend/src/components/deploy/AppRelease.tsx +++ b/frontend/src/components/deploy/AppRelease.tsx @@ -1,4 +1,5 @@ import { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; import { cn } from '@/lib/utils'; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; @@ -30,6 +31,8 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; +import { Textarea } from '@/components/ui/textarea'; +import { Label } from '@/components/ui/label'; import { Rocket, Plus, @@ -53,14 +56,26 @@ import { Terminal, } from 'lucide-react'; import { releaseApi } from '@/services/api'; +import { toast } from 'sonner'; export function AppReleaseManager() { + const navigate = useNavigate(); const [searchQuery, setSearchQuery] = useState(''); const [envFilter, setEnvFilter] = useState('all'); const [statusFilter, setStatusFilter] = useState('all'); const [releases, setReleases] = useState([]); const [loading, setLoading] = useState(true); const [selectedRelease, setSelectedRelease] = useState(null); + const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); + const [submitting, setSubmitting] = useState(false); + const [newRelease, setNewRelease] = useState({ + appName: '', + version: '', + environment: 'dev', + commitId: '', + commitMsg: '', + description: '', + }); const loadData = async () => { try { @@ -165,6 +180,33 @@ export function AppReleaseManager() { } }; + // 新建发布提交(复用 releaseApi.createRelease) // Logan.Li + const handleCreateRelease = async () => { + if (!newRelease.appName.trim() || !newRelease.version.trim()) { + toast.error('请填写应用名称和版本号'); + return; + } + setSubmitting(true); + try { + await releaseApi.createRelease({ + app_name: newRelease.appName.trim(), + version: newRelease.version.trim(), + environment: newRelease.environment, + commit_id: newRelease.commitId.trim(), + commit_msg: newRelease.commitMsg.trim(), + description: newRelease.description.trim(), + }); + toast.success('发布申请已提交'); + setIsCreateDialogOpen(false); + setNewRelease({ appName: '', version: '', environment: 'dev', commitId: '', commitMsg: '', description: '' }); + loadData(); + } catch { + toast.error('创建失败'); + } finally { + setSubmitting(false); + } + }; + // 模拟流水线步骤 const pipelineSteps = [ { name: '代码检出', status: 'success', duration: '15s' }, @@ -186,11 +228,18 @@ export function AppReleaseManager() {

CI/CD流水线管理与发布历史

- - @@ -485,6 +534,93 @@ export function AppReleaseManager() { + + {/* Create Release Dialog */} + + + + 新建发布 + 创建新的应用版本发布申请 + +
+
+ + setNewRelease({ ...newRelease, appName: e.target.value })} + placeholder="如:用户中心服务" + className="mt-1 bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-500" + /> +
+
+
+ + setNewRelease({ ...newRelease, version: e.target.value })} + placeholder="v1.0.0" + className="mt-1 bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-500" + /> +
+
+ + +
+
+
+
+ + setNewRelease({ ...newRelease, commitId: e.target.value })} + placeholder="abc1234" + className="mt-1 bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-500" + /> +
+
+ + setNewRelease({ ...newRelease, commitMsg: e.target.value })} + placeholder="feat: 新增功能" + className="mt-1 bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-500" + /> +
+
+
+ +