feat: run.sh四模式(dev/docker/build/binary) + CLI flags + Redka内嵌Redis + Agent复制修复 + 服务器编辑IP字段 + 安装Agent按钮
This commit is contained in:
1 parent
d0b94d39c0
commit
2354959811
14 files changed
+1355
-575
No files matched your search
+101
-82
@@ -1,82 +1,101 @@
|
||||
# ============================================================
|
||||
# AIOps Backend + Agent 多阶段 Dockerfile
|
||||
# Stage 1: 编译后端 (Go 1.21)
|
||||
# Stage 2: 交叉编译 Agent (Go 1.24, linux-amd64/arm64 + darwin-arm64)
|
||||
# Stage 3: 运行时 (Alpine)
|
||||
# 署名 Logan.Li
|
||||
# ============================================================
|
||||
|
||||
# ==================== Stage 1: Backend Builder ====================
|
||||
FROM golang:1.21-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add --no-cache git ca-certificates tzdata
|
||||
|
||||
ENV TZ=Asia/Shanghai
|
||||
ENV GOPROXY=https://goproxy.cn,direct
|
||||
ENV GOSUMDB=off
|
||||
|
||||
# 复制后端 go.mod(利用 Docker 层缓存)
|
||||
COPY backend/go.mod backend/go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# 复制后端源码
|
||||
COPY backend/ ./
|
||||
|
||||
# 构建后端二进制
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
|
||||
-ldflags="-w -s" \
|
||||
-o ezops ./cmd/
|
||||
|
||||
# ==================== Stage 2: Agent Builder ====================
|
||||
FROM golang:1.24-alpine AS agent-builder
|
||||
|
||||
WORKDIR /agent
|
||||
|
||||
ENV GOPROXY=https://goproxy.cn,direct
|
||||
ENV GOSUMDB=off
|
||||
|
||||
# 复制 agent go.mod
|
||||
COPY agent/go.mod agent/go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# 复制 agent 源码
|
||||
COPY agent/ ./
|
||||
|
||||
# 交叉编译三个平台的 agent 二进制
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o bin/aiops-agent-linux-amd64 . && \
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o bin/aiops-agent-linux-arm64 . && \
|
||||
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w" -o bin/aiops-agent-darwin-arm64 .
|
||||
|
||||
# ==================== Stage 3: Runtime ====================
|
||||
FROM alpine:latest
|
||||
|
||||
# 安装运行时依赖(curl=健康检查,openssh-client=SSH远程安装agent)
|
||||
RUN apk --no-cache add ca-certificates tzdata curl openssh-client
|
||||
|
||||
ENV TZ=Asia/Shanghai
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 复制后端二进制
|
||||
COPY --from=builder /app/ezops .
|
||||
|
||||
# 创建运行时目录
|
||||
RUN mkdir -p /app/logs /app/uploads /app/agent/bin /app/agent/deploy
|
||||
|
||||
# 复制 agent 二进制(三个平台)
|
||||
COPY --from=agent-builder /agent/bin/aiops-agent-linux-amd64 /app/agent/bin/
|
||||
COPY --from=agent-builder /agent/bin/aiops-agent-linux-arm64 /app/agent/bin/
|
||||
COPY --from=agent-builder /agent/bin/aiops-agent-darwin-arm64 /app/agent/bin/
|
||||
|
||||
# 复制 agent 安装脚本和 systemd service 模板
|
||||
COPY agent/deploy/install.sh /app/agent/deploy/
|
||||
COPY agent/deploy/aiops-agent.service /app/agent/deploy/
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
|
||||
CMD curl -f http://localhost:8080/health || exit 1
|
||||
|
||||
CMD ["./ezops"]
|
||||
1|# ============================================================
|
||||
2|# AIOps Backend + Agent 多阶段 Dockerfile
|
||||
3|# Stage 1: 编译后端 (Go 1.21)
|
||||
4|# Stage 2: 交叉编译 Agent (Go 1.24, linux-amd64/arm64 + darwin-arm64)
|
||||
5|# Stage 3: 运行时 (Alpine)
|
||||
6|# 署名 Logan.Li
|
||||
7|# ============================================================
|
||||
8|
|
||||
9|# ==================== Stage 1: Backend Builder ====================
|
||||
10|FROM golang:1.21-alpine AS builder
|
||||
11|
|
||||
12|WORKDIR /app
|
||||
13|
|
||||
14|<<<<<<< HEAD
|
||||
15|=======
|
||||
16|# 使用国内 Alpine 镜像源加速
|
||||
17|RUN sed -i 's|dl-cdn.alpinelinux.org|mirrors.aliyun.com|g' /etc/apk/repositories
|
||||
18|
|
||||
19|# 安装构建依赖
|
||||
20|>>>>>>> ca18ead (feat: run.sh四模式(dev/docker/build/binary) + CLI flags + Redka内嵌Redis + Agent复制修复 + 服务器编辑IP字段 + 安装Agent按钮)
|
||||
21|RUN apk add --no-cache git ca-certificates tzdata
|
||||
22|
|
||||
23|ENV TZ=Asia/Shanghai
|
||||
24|ENV GOPROXY=https://goproxy.cn,direct
|
||||
25|# 跳过 toolchain 校验(GOTOOLCHAIN 自动下载 Go 1.22)
|
||||
26|ENV GONOSUMCHECK=*
|
||||
27|# 自动下载 Go 1.22 工具链(Redka 需要)
|
||||
28|ENV GOTOOLCHAIN=go1.22.2
|
||||
29|
|
||||
30|# 复制后端 go.mod(利用 Docker 层缓存)
|
||||
31|COPY backend/go.mod backend/go.sum ./
|
||||
32|RUN go mod download
|
||||
33|
|
||||
34|# 复制后端源码
|
||||
35|COPY backend/ ./
|
||||
36|
|
||||
37|# 构建后端二进制
|
||||
38|RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
|
||||
39| -ldflags="-w -s" \
|
||||
40| -o ezops ./cmd/
|
||||
41|
|
||||
42|# ==================== Stage 2: Agent Builder ====================
|
||||
43|FROM golang:1.24-alpine AS agent-builder
|
||||
44|
|
||||
45|WORKDIR /agent
|
||||
46|
|
||||
47|ENV GOPROXY=https://goproxy.cn,direct
|
||||
48|ENV GOSUMDB=off
|
||||
49|
|
||||
50|# 复制 agent go.mod
|
||||
51|COPY agent/go.mod agent/go.sum ./
|
||||
52|RUN go mod download
|
||||
53|
|
||||
54|# 复制 agent 源码
|
||||
55|COPY agent/ ./
|
||||
56|
|
||||
57|# 交叉编译三个平台的 agent 二进制
|
||||
58|RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o bin/aiops-agent-linux-amd64 . && \
|
||||
59| CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o bin/aiops-agent-linux-arm64 . && \
|
||||
60| CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w" -o bin/aiops-agent-darwin-arm64 .
|
||||
61|
|
||||
62|# ==================== Stage 3: Runtime ====================
|
||||
63|FROM alpine:latest
|
||||
64|
|
||||
65|<<<<<<< HEAD
|
||||
66|# 安装运行时依赖(curl=健康检查,openssh-client=SSH远程安装agent)
|
||||
67|RUN apk --no-cache add ca-certificates tzdata curl openssh-client
|
||||
68|=======
|
||||
69|# 使用国内 Alpine 镜像源加速
|
||||
70|RUN sed -i 's|dl-cdn.alpinelinux.org|mirrors.aliyun.com|g' /etc/apk/repositories
|
||||
71|
|
||||
72|# 安装运行时依赖
|
||||
73|RUN apk --no-cache add ca-certificates tzdata curl
|
||||
74|>>>>>>> ca18ead (feat: run.sh四模式(dev/docker/build/binary) + CLI flags + Redka内嵌Redis + Agent复制修复 + 服务器编辑IP字段 + 安装Agent按钮)
|
||||
75|
|
||||
76|ENV TZ=Asia/Shanghai
|
||||
77|
|
||||
78|WORKDIR /app
|
||||
79|
|
||||
80|# 复制后端二进制
|
||||
81|COPY --from=builder /app/ezops .
|
||||
82|
|
||||
83|# 创建运行时目录
|
||||
84|RUN mkdir -p /app/logs /app/uploads /app/agent/bin /app/agent/deploy
|
||||
85|
|
||||
86|# 复制 agent 二进制(三个平台)
|
||||
87|COPY --from=agent-builder /agent/bin/aiops-agent-linux-amd64 /app/agent/bin/
|
||||
88|COPY --from=agent-builder /agent/bin/aiops-agent-linux-arm64 /app/agent/bin/
|
||||
89|COPY --from=agent-builder /agent/bin/aiops-agent-darwin-arm64 /app/agent/bin/
|
||||
90|
|
||||
91|# 复制 agent 安装脚本和 systemd service 模板
|
||||
92|COPY agent/deploy/install.sh /app/agent/deploy/
|
||||
93|COPY agent/deploy/aiops-agent.service /app/agent/deploy/
|
||||
94|
|
||||
95|EXPOSE 8080
|
||||
96|
|
||||
97|HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
|
||||
98| CMD curl -f http://localhost:8080/health || exit 1
|
||||
99|
|
||||
100|CMD ["./ezops"]
|
||||
101|
|
||||
+189
-13
@@ -2,6 +2,8 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
@@ -12,18 +14,50 @@ import (
|
||||
"ezops/internal/config"
|
||||
"ezops/pkg/database"
|
||||
"ezops/pkg/logger"
|
||||
ezredis "ezops/pkg/redis"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/spf13/viper"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// 版本信息(通过编译时 ldflags 注入)
|
||||
// 版本信息(通过编译时 ldflags 注入)// Logan.Li
|
||||
var (
|
||||
Version = "dev"
|
||||
BuildTime = "unknown"
|
||||
GitCommit = "unknown"
|
||||
)
|
||||
|
||||
// CLI 参数定义 // Logan.Li
|
||||
var (
|
||||
flagPort = flag.String("port", "", "服务端口 (默认: 8080)")
|
||||
flagConfig = flag.String("config", "", "配置文件路径 (默认: config.yaml)")
|
||||
flagMode = flag.String("mode", "", "运行模式: development, production, release")
|
||||
flagLogLevel = flag.String("log-level", "", "日志级别: debug, info, warn, error")
|
||||
|
||||
// 数据库参数
|
||||
flagDBDriver = flag.String("db-driver", "", "数据库驱动: sqlite, mysql, postgres (默认: sqlite)")
|
||||
flagDBHost = flag.String("db-host", "", "数据库主机")
|
||||
flagDBPort = flag.Int("db-port", 0, "数据库端口")
|
||||
flagDBUser = flag.String("db-user", "", "数据库用户名")
|
||||
flagDBPassword = flag.String("db-password", "", "数据库密码")
|
||||
flagDBName = flag.String("db-name", "", "数据库名 / SQLite 文件路径")
|
||||
flagDBCharset = flag.String("db-charset", "", "数据库字符集")
|
||||
|
||||
// Redis 参数
|
||||
flagRedisHost = flag.String("redis-host", "", "Redis 主机")
|
||||
flagRedisPort = flag.Int("redis-port", 0, "Redis 端口")
|
||||
flagRedisPassword = flag.String("redis-password", "", "Redis 密码")
|
||||
flagRedisDB = flag.Int("redis-db", 0, "Redis 数据库编号")
|
||||
|
||||
// JWT 参数
|
||||
flagJWTSecret = flag.String("jwt-secret", "", "JWT 密钥")
|
||||
flagJWTExpire = flag.Int("jwt-expire", 0, "JWT 过期时间 (小时)")
|
||||
|
||||
// 显示版本
|
||||
flagVersion = flag.Bool("version", false, "显示版本信息")
|
||||
)
|
||||
|
||||
// @title AIOps运维平台 API
|
||||
// @version 1.0
|
||||
// @description AIOps智能运维平台后端API文档
|
||||
@@ -45,32 +79,67 @@ var (
|
||||
// @description Type "Bearer" followed by a space and JWT token.
|
||||
|
||||
func main() {
|
||||
// 加载配置
|
||||
cfg := config.Load()
|
||||
// 解析 CLI 参数 // Logan.Li
|
||||
flag.Parse()
|
||||
|
||||
// 显示版本信息 // Logan.Li
|
||||
if *flagVersion {
|
||||
fmt.Printf("AIOps 智能运维平台\n")
|
||||
fmt.Printf(" 版本: %s\n", Version)
|
||||
fmt.Printf(" 构建: %s\n", BuildTime)
|
||||
fmt.Printf(" 提交: %s\n", GitCommit)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// 加载配置 // Logan.Li
|
||||
cfg := loadConfig()
|
||||
|
||||
// 初始化日志
|
||||
log := logger.New(cfg.LogLevel)
|
||||
defer log.Sync()
|
||||
|
||||
log.Info("AIOps 启动中",
|
||||
zap.String("version", Version),
|
||||
zap.String("mode", cfg.Mode),
|
||||
zap.String("port", cfg.Port),
|
||||
)
|
||||
|
||||
// 设置Gin模式
|
||||
if cfg.Mode == "production" {
|
||||
if cfg.Mode == "production" || cfg.Mode == "release" {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
|
||||
// 初始化数据库
|
||||
db, err := database.New(cfg.Database)
|
||||
if err != nil {
|
||||
log.Fatal("failed to connect database", zap.Error(err))
|
||||
log.Fatal("数据库连接失败", zap.Error(err))
|
||||
}
|
||||
|
||||
// 自动迁移
|
||||
if err := database.AutoMigrate(db); err != nil {
|
||||
log.Fatal("failed to migrate database", zap.Error(err))
|
||||
log.Fatal("数据库迁移失败", zap.Error(err))
|
||||
}
|
||||
|
||||
// 初始化种子数据
|
||||
seedData(db)
|
||||
|
||||
// 初始化缓存(Redka 内嵌 / Redis 外部)// Logan.Li
|
||||
cache, err := ezredis.New(ezredis.Config{
|
||||
Driver: "redka", // 默认内嵌 Redka,零依赖
|
||||
Host: cfg.Redis.Host,
|
||||
Port: cfg.Redis.Port,
|
||||
Password: cfg.Redis.Password,
|
||||
DB: cfg.Redis.DB,
|
||||
DataDir: "data/redka",
|
||||
})
|
||||
if err != nil {
|
||||
log.Warn("缓存初始化失败,继续启动", zap.Error(err))
|
||||
cache = nil
|
||||
} else {
|
||||
log.Info("缓存已就绪", zap.String("driver", "redka"))
|
||||
defer cache.Close()
|
||||
}
|
||||
|
||||
// 创建Gin实例
|
||||
r := gin.New()
|
||||
|
||||
@@ -85,11 +154,13 @@ func main() {
|
||||
"version": Version,
|
||||
"build_time": BuildTime,
|
||||
"git_commit": GitCommit,
|
||||
"mode": cfg.Mode,
|
||||
"db_driver": cfg.Database.Driver,
|
||||
})
|
||||
})
|
||||
|
||||
// 注册路由
|
||||
routes.Register(r, db, log, cfg)
|
||||
routes.Register(r, db, log, cfg, cache)
|
||||
|
||||
// 创建HTTP服务器
|
||||
srv := &http.Server{
|
||||
@@ -99,28 +170,133 @@ func main() {
|
||||
|
||||
// 优雅关闭
|
||||
go func() {
|
||||
log.Info("服务已启动", zap.String("addr", srv.Addr))
|
||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatal("failed to start server", zap.Error(err))
|
||||
log.Fatal("服务启动失败", zap.Error(err))
|
||||
}
|
||||
}()
|
||||
|
||||
log.Info("server started", zap.String("port", cfg.Port))
|
||||
|
||||
// 等待中断信号
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
|
||||
log.Info("shutting down server...")
|
||||
log.Info("正在关闭服务...")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := srv.Shutdown(ctx); err != nil {
|
||||
log.Fatal("server forced to shutdown", zap.Error(err))
|
||||
log.Fatal("强制关闭服务", zap.Error(err))
|
||||
}
|
||||
|
||||
log.Info("server exited")
|
||||
log.Info("服务已退出")
|
||||
}
|
||||
|
||||
// loadConfig 加载配置(CLI flags 优先于 config.yaml)// Logan.Li
|
||||
func loadConfig() *config.Config {
|
||||
viper.SetConfigName("config")
|
||||
viper.SetConfigType("yaml")
|
||||
viper.AddConfigPath("./configs")
|
||||
viper.AddConfigPath(".")
|
||||
|
||||
// 如果指定了配置文件路径,使用它 // Logan.Li
|
||||
if *flagConfig != "" {
|
||||
viper.SetConfigFile(*flagConfig)
|
||||
}
|
||||
|
||||
// 默认值(独立二进制模式:SQLite + 无 Redis)// Logan.Li
|
||||
viper.SetDefault("mode", "release")
|
||||
viper.SetDefault("port", "8080")
|
||||
viper.SetDefault("log_level", "info")
|
||||
|
||||
viper.SetDefault("database.driver", "sqlite")
|
||||
viper.SetDefault("database.host", "localhost")
|
||||
viper.SetDefault("database.port", 3306)
|
||||
viper.SetDefault("database.user", "")
|
||||
viper.SetDefault("database.password", "")
|
||||
viper.SetDefault("database.dbname", "data/aiops.db")
|
||||
viper.SetDefault("database.charset", "utf8mb4")
|
||||
viper.SetDefault("database.max_conn", 100)
|
||||
viper.SetDefault("database.max_idle", 10)
|
||||
|
||||
viper.SetDefault("redis.host", "localhost")
|
||||
viper.SetDefault("redis.port", 6379)
|
||||
viper.SetDefault("redis.password", "")
|
||||
viper.SetDefault("redis.db", 0)
|
||||
|
||||
viper.SetDefault("jwt.secret", "aiops-jwt-secret-change-in-production")
|
||||
viper.SetDefault("jwt.expire_hour", 24)
|
||||
|
||||
viper.SetDefault("websocket.read_buffer_size", 1024)
|
||||
viper.SetDefault("websocket.write_buffer_size", 1024)
|
||||
|
||||
// 读取配置文件(不存在也不报错)// Logan.Li
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
// 配置文件不存在是正常的(独立二进制模式)
|
||||
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
|
||||
fmt.Fprintf(os.Stderr, "警告: 读取配置文件失败: %v,使用默认配置\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 环境变量覆盖 // Logan.Li
|
||||
viper.AutomaticEnv()
|
||||
|
||||
// CLI flags 覆盖(最高优先级)// Logan.Li
|
||||
applyFlags()
|
||||
|
||||
// 解析配置 // Logan.Li
|
||||
var cfg config.Config
|
||||
if err := viper.Unmarshal(&cfg); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "配置解析失败: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return &cfg
|
||||
}
|
||||
|
||||
// applyFlags 将 CLI 参数映射到 viper(覆盖 config.yaml 和默认值)// Logan.Li
|
||||
func applyFlags() {
|
||||
// 检查是否有任何 flag 被显式设置 // Logan.Li
|
||||
flag.Visit(func(f *flag.Flag) {
|
||||
switch f.Name {
|
||||
case "port":
|
||||
viper.Set("port", *flagPort)
|
||||
case "mode":
|
||||
viper.Set("mode", *flagMode)
|
||||
case "log-level":
|
||||
viper.Set("log_level", *flagLogLevel)
|
||||
|
||||
case "db-driver":
|
||||
viper.Set("database.driver", *flagDBDriver)
|
||||
case "db-host":
|
||||
viper.Set("database.host", *flagDBHost)
|
||||
case "db-port":
|
||||
viper.Set("database.port", *flagDBPort)
|
||||
case "db-user":
|
||||
viper.Set("database.user", *flagDBUser)
|
||||
case "db-password":
|
||||
viper.Set("database.password", *flagDBPassword)
|
||||
case "db-name":
|
||||
viper.Set("database.dbname", *flagDBName)
|
||||
case "db-charset":
|
||||
viper.Set("database.charset", *flagDBCharset)
|
||||
|
||||
case "redis-host":
|
||||
viper.Set("redis.host", *flagRedisHost)
|
||||
case "redis-port":
|
||||
viper.Set("redis.port", *flagRedisPort)
|
||||
case "redis-password":
|
||||
viper.Set("redis.password", *flagRedisPassword)
|
||||
case "redis-db":
|
||||
viper.Set("redis.db", *flagRedisDB)
|
||||
|
||||
case "jwt-secret":
|
||||
viper.Set("jwt.secret", *flagJWTSecret)
|
||||
case "jwt-expire":
|
||||
viper.Set("jwt.expire_hour", *flagJWTExpire)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// CORS 跨域中间件
|
||||
|
||||
+8
-2
@@ -1,12 +1,17 @@
|
||||
module ezops
|
||||
|
||||
go 1.21
|
||||
go 1.22
|
||||
|
||||
toolchain go1.22.2
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/go-ldap/ldap/v3 v3.4.10
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/nalgeon/redka v0.2.0
|
||||
github.com/redis/go-redis/v9 v9.7.0
|
||||
github.com/spf13/viper v1.18.2
|
||||
go.uber.org/zap v1.26.0
|
||||
golang.org/x/crypto v0.31.0
|
||||
@@ -19,7 +24,9 @@ require (
|
||||
require (
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||
github.com/bytedance/sonic v1.9.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
@@ -29,7 +36,6 @@ require (
|
||||
github.com/go-playground/validator/v10 v10.16.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
|
||||
@@ -2,9 +2,15 @@ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
||||
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
@@ -12,6 +18,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
@@ -102,11 +110,15 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/nalgeon/redka v0.2.0 h1:9Hjp5aHPHPLE44T1m41LKzx+b/MquMXt8ggw69eVu6U=
|
||||
github.com/nalgeon/redka v0.2.0/go.mod h1:vLxjY3XS9IwBID2YEFWeeMiN4Ar/DtKd4JW62JTAxuU=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
|
||||
github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||
@@ -207,6 +219,7 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
|
||||
@@ -236,6 +236,9 @@ func (h *ServerHandler) UpdateServer(c *gin.Context) {
|
||||
if req.Hostname != "" {
|
||||
updates["hostname"] = req.Hostname
|
||||
}
|
||||
if req.IP != "" {
|
||||
updates["ip"] = req.IP
|
||||
}
|
||||
if req.PublicIP != "" {
|
||||
updates["public_ip"] = req.PublicIP
|
||||
}
|
||||
|
||||
@@ -12,8 +12,9 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Register 注册所有路由
|
||||
func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config) {
|
||||
// Register 注册所有路由 // Logan.Li
|
||||
// cache 参数为可选 Redis 缓存实例(支持 redka/redis 双后端)
|
||||
func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, cache interface{}) {
|
||||
// API版本前缀
|
||||
api := r.Group("/api/v1")
|
||||
|
||||
|
||||
@@ -155,6 +155,7 @@ type CreateServerRequest struct {
|
||||
// UpdateServerRequest 更新服务器请求
|
||||
type UpdateServerRequest struct {
|
||||
Hostname string `json:"hostname"`
|
||||
IP string `json:"ip" binding:"omitempty,ip"`
|
||||
PublicIP string `json:"public_ip" binding:"omitempty,ip"`
|
||||
Port int `json:"port" binding:"omitempty,min=1,max=65535"`
|
||||
Username string `json:"username"`
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
// go-redis 外部 Redis 服务器实现
|
||||
// 用于 Docker Compose / 生产环境部署 // Logan.Li
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
goredis "github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// goRedisCache 基于 go-redis 的缓存实现 // Logan.Li
|
||||
type goRedisCache struct {
|
||||
client *goredis.Client
|
||||
}
|
||||
|
||||
func newGoRedis(cfg Config) (Cache, error) {
|
||||
client := goredis.NewClient(&goredis.Options{
|
||||
Addr: fmt.Sprintf("%s:%d", cfg.Host, cfg.Port),
|
||||
Password: cfg.Password,
|
||||
DB: cfg.DB,
|
||||
})
|
||||
|
||||
// 连接测试
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := client.Ping(ctx).Err(); err != nil {
|
||||
return nil, fmt.Errorf("redis: 连接失败 %s:%d: %w", cfg.Host, cfg.Port, err)
|
||||
}
|
||||
|
||||
return &goRedisCache{client: client}, nil
|
||||
}
|
||||
|
||||
func (r *goRedisCache) Get(ctx context.Context, key string) (string, error) {
|
||||
val, err := r.client.Get(ctx, key).Result()
|
||||
if err == goredis.Nil {
|
||||
return "", nil
|
||||
}
|
||||
return val, err
|
||||
}
|
||||
|
||||
func (r *goRedisCache) Set(ctx context.Context, key string, value string, expiration time.Duration) error {
|
||||
return r.client.Set(ctx, key, value, expiration).Err()
|
||||
}
|
||||
|
||||
func (r *goRedisCache) Del(ctx context.Context, keys ...string) error {
|
||||
return r.client.Del(ctx, keys...).Err()
|
||||
}
|
||||
|
||||
func (r *goRedisCache) Exists(ctx context.Context, keys ...string) (int64, error) {
|
||||
return r.client.Exists(ctx, keys...).Result()
|
||||
}
|
||||
|
||||
func (r *goRedisCache) Expire(ctx context.Context, key string, expiration time.Duration) error {
|
||||
return r.client.Expire(ctx, key, expiration).Err()
|
||||
}
|
||||
|
||||
func (r *goRedisCache) Ping(ctx context.Context) error {
|
||||
return r.client.Ping(ctx).Err()
|
||||
}
|
||||
|
||||
func (r *goRedisCache) Close() error {
|
||||
return r.client.Close()
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Package redis 提供统一的 Redis 缓存抽象层
|
||||
// 支持两种后端:
|
||||
// - redka: 内嵌 Redis 兼容存储(零依赖,独立二进制模式)// Logan.Li
|
||||
// - redis: 外部 Redis 服务器(Docker / 生产环境)
|
||||
//
|
||||
// 使用方式:
|
||||
//
|
||||
// cache, err := redis.New(redis.Config{
|
||||
// Driver: "redka", // 或 "redis"
|
||||
// Host: "localhost",
|
||||
// Port: 6379,
|
||||
// Password: "",
|
||||
// DB: 0,
|
||||
// DataDir: "data/redka", // redka 数据目录(仅 redka 驱动使用)
|
||||
// })
|
||||
//
|
||||
// cache.Set(ctx, "key", "value", 0)
|
||||
// val, _ := cache.Get(ctx, "key")
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Config Redis 配置 // Logan.Li
|
||||
type Config struct {
|
||||
Driver string // "redka" 或 "redis"
|
||||
Host string
|
||||
Port int
|
||||
Password string
|
||||
DB int
|
||||
DataDir string // redka 数据目录
|
||||
}
|
||||
|
||||
// Cache Redis 缓存接口 // Logan.Li
|
||||
type Cache interface {
|
||||
Get(ctx context.Context, key string) (string, error)
|
||||
Set(ctx context.Context, key string, value string, expiration time.Duration) error
|
||||
Del(ctx context.Context, keys ...string) error
|
||||
Exists(ctx context.Context, keys ...string) (int64, error)
|
||||
Expire(ctx context.Context, key string, expiration time.Duration) error
|
||||
Ping(ctx context.Context) error
|
||||
Close() error
|
||||
}
|
||||
|
||||
// New 创建缓存实例(根据驱动自动选择后端)// Logan.Li
|
||||
func New(cfg Config) (Cache, error) {
|
||||
switch cfg.Driver {
|
||||
case "redka", "":
|
||||
return newRedka(cfg)
|
||||
case "redis":
|
||||
return newGoRedis(cfg)
|
||||
default:
|
||||
return nil, fmt.Errorf("redis: 不支持的驱动: %s (支持: redka, redis)", cfg.Driver)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// redka 内嵌 Redis 兼容存储实现(v0.2.x API)
|
||||
// 使用 github.com/nalgeon/redka — 基于 SQLite 的 Redis 替代方案 // Logan.Li
|
||||
// 零外部依赖,数据存在本地文件中,非常适合独立二进制部署
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/nalgeon/redka"
|
||||
)
|
||||
|
||||
// redkaCache 基于 Redka v0.2.x 的缓存实现 // Logan.Li
|
||||
type redkaCache struct {
|
||||
db *redka.DB
|
||||
}
|
||||
|
||||
func newRedka(cfg Config) (Cache, error) {
|
||||
dataDir := cfg.DataDir
|
||||
if dataDir == "" {
|
||||
dataDir = "data/redka"
|
||||
}
|
||||
|
||||
// Redka v0.2.x: Open(path, opts) // Logan.Li
|
||||
db, err := redka.Open(dataDir+"/redka.db", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &redkaCache{db: db}, nil
|
||||
}
|
||||
|
||||
func (r *redkaCache) Get(ctx context.Context, key string) (string, error) {
|
||||
// Redka v0.2.x: Str().Get() 返回 core.Value // Logan.Li
|
||||
val, err := r.db.Str().Get(key)
|
||||
if err == redka.ErrNotFound {
|
||||
return "", nil
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return val.String(), nil
|
||||
}
|
||||
|
||||
func (r *redkaCache) Set(ctx context.Context, key string, value string, expiration time.Duration) error {
|
||||
// Redka v0.2.x: Str().Set() 返回 error // Logan.Li
|
||||
if err := r.db.Str().Set(key, value); err != nil {
|
||||
return err
|
||||
}
|
||||
if expiration > 0 {
|
||||
_, err := r.db.Key().Expire(key, expiration)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *redkaCache) Del(ctx context.Context, keys ...string) error {
|
||||
for _, key := range keys {
|
||||
if _, err := r.db.Key().Delete(key); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *redkaCache) Exists(ctx context.Context, keys ...string) (int64, error) {
|
||||
var count int64
|
||||
for _, key := range keys {
|
||||
ok, err := r.db.Key().Exists(key)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if ok {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (r *redkaCache) Expire(ctx context.Context, key string, expiration time.Duration) error {
|
||||
ok, err := r.db.Key().Expire(key, expiration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return redka.ErrNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *redkaCache) Ping(ctx context.Context) error {
|
||||
// Redka 始终可用(内嵌数据库)// Logan.Li
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *redkaCache) Close() error {
|
||||
return r.db.Close()
|
||||
}
|
||||
Reference in new issue
Block a user