mirror of
https://github.com/Awuqing/BackupX.git
synced 2026-09-06 16:06:45 +08:00
feat: 优化集群部署与堡垒机接入 (#106)
支持受限网络、正向代理、私有 CA 与 SSH 堡垒机部署 Agent。 加固 Docker、systemd、Nginx、安装器、Release 校验与可信代理边界,并完善命令队列索引、前端安装向导及中英文运维文档。
This commit is contained in:
@@ -10,8 +10,10 @@ package agent
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
@@ -22,16 +24,34 @@ type Config struct {
|
||||
Master string `yaml:"master"`
|
||||
// Token 节点认证令牌(在 Master 创建节点时生成)
|
||||
Token string `yaml:"token"`
|
||||
// TokenFile 从文件读取节点认证令牌;适合 systemd 凭据和容器 secret。
|
||||
// Token 与 TokenFile 同时设置时优先使用 Token。
|
||||
TokenFile string `yaml:"tokenFile"`
|
||||
// HeartbeatInterval 心跳间隔,默认 15s
|
||||
HeartbeatInterval string `yaml:"heartbeatInterval"`
|
||||
// PollInterval 命令轮询间隔,默认 5s
|
||||
PollInterval string `yaml:"pollInterval"`
|
||||
// TempDir 备份临时目录,默认 /var/lib/backupx-agent/tmp
|
||||
TempDir string `yaml:"tempDir"`
|
||||
// ProxyURL Agent 访问 Master 使用的显式代理。留空时遵循
|
||||
// HTTP_PROXY、HTTPS_PROXY 与 NO_PROXY;支持 http(s) 和 socks5(h)。
|
||||
ProxyURL string `yaml:"proxyUrl"`
|
||||
// CACertFile 私有 CA 的 PEM 文件路径,用于安全连接内网 HTTPS Master。
|
||||
CACertFile string `yaml:"caCertFile"`
|
||||
// InsecureSkipTLSVerify 测试环境允许跳过 TLS 证书校验
|
||||
InsecureSkipTLSVerify bool `yaml:"insecureSkipTlsVerify"`
|
||||
}
|
||||
|
||||
// Overrides 表示命令行显式提供的 Agent 配置覆盖项。
|
||||
type Overrides struct {
|
||||
Master string
|
||||
Token string
|
||||
TokenFile string
|
||||
TempDir string
|
||||
ProxyURL string
|
||||
CACertFile string
|
||||
}
|
||||
|
||||
// LoadConfigFile 从 YAML 文件加载 Agent 配置。
|
||||
func LoadConfigFile(path string) (*Config, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
@@ -50,42 +70,109 @@ func LoadConfigFile(path string) (*Config, error) {
|
||||
// 支持的环境变量:
|
||||
// - BACKUPX_AGENT_MASTER Master URL
|
||||
// - BACKUPX_AGENT_TOKEN 节点认证令牌
|
||||
// - BACKUPX_AGENT_TOKEN_FILE 节点认证令牌文件
|
||||
// - BACKUPX_AGENT_HEARTBEAT 心跳间隔(如 15s)
|
||||
// - BACKUPX_AGENT_POLL 命令轮询间隔(如 5s)
|
||||
// - BACKUPX_AGENT_TEMP_DIR 临时目录
|
||||
// - BACKUPX_AGENT_PROXY_URL 显式 HTTP(S)/SOCKS5 代理
|
||||
// - BACKUPX_AGENT_CA_CERT_FILE 私有 CA PEM 文件
|
||||
// - BACKUPX_AGENT_INSECURE_TLS true / 1 跳过 TLS 校验
|
||||
func LoadConfigFromEnv() (*Config, error) {
|
||||
cfg := &Config{
|
||||
Master: strings.TrimSpace(os.Getenv("BACKUPX_AGENT_MASTER")),
|
||||
Token: strings.TrimSpace(os.Getenv("BACKUPX_AGENT_TOKEN")),
|
||||
TokenFile: strings.TrimSpace(os.Getenv("BACKUPX_AGENT_TOKEN_FILE")),
|
||||
HeartbeatInterval: strings.TrimSpace(os.Getenv("BACKUPX_AGENT_HEARTBEAT")),
|
||||
PollInterval: strings.TrimSpace(os.Getenv("BACKUPX_AGENT_POLL")),
|
||||
TempDir: strings.TrimSpace(os.Getenv("BACKUPX_AGENT_TEMP_DIR")),
|
||||
ProxyURL: strings.TrimSpace(os.Getenv("BACKUPX_AGENT_PROXY_URL")),
|
||||
CACertFile: strings.TrimSpace(os.Getenv("BACKUPX_AGENT_CA_CERT_FILE")),
|
||||
InsecureSkipTLSVerify: strings.EqualFold(os.Getenv("BACKUPX_AGENT_INSECURE_TLS"), "true") || os.Getenv("BACKUPX_AGENT_INSECURE_TLS") == "1",
|
||||
}
|
||||
return applyConfigDefaults(cfg)
|
||||
}
|
||||
|
||||
// MergeWithFlags 把命令行覆盖值合并入配置(非空覆盖)。
|
||||
func (c *Config) MergeWithFlags(master, token, tempDir string) {
|
||||
if strings.TrimSpace(master) != "" {
|
||||
c.Master = master
|
||||
// ApplyOverrides 把命令行覆盖值合并入配置(非空覆盖)。
|
||||
func (c *Config) ApplyOverrides(overrides Overrides) {
|
||||
if strings.TrimSpace(overrides.Master) != "" {
|
||||
c.Master = strings.TrimSpace(overrides.Master)
|
||||
}
|
||||
if strings.TrimSpace(token) != "" {
|
||||
c.Token = token
|
||||
tokenProvided := strings.TrimSpace(overrides.Token) != ""
|
||||
if strings.TrimSpace(overrides.TokenFile) != "" {
|
||||
c.TokenFile = strings.TrimSpace(overrides.TokenFile)
|
||||
if !tokenProvided {
|
||||
// An explicit --token-file must override a token inherited from YAML.
|
||||
c.Token = ""
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(tempDir) != "" {
|
||||
c.TempDir = tempDir
|
||||
if tokenProvided {
|
||||
c.Token = strings.TrimSpace(overrides.Token)
|
||||
}
|
||||
if strings.TrimSpace(overrides.TempDir) != "" {
|
||||
c.TempDir = strings.TrimSpace(overrides.TempDir)
|
||||
}
|
||||
if strings.TrimSpace(overrides.ProxyURL) != "" {
|
||||
c.ProxyURL = strings.TrimSpace(overrides.ProxyURL)
|
||||
}
|
||||
if strings.TrimSpace(overrides.CACertFile) != "" {
|
||||
c.CACertFile = strings.TrimSpace(overrides.CACertFile)
|
||||
}
|
||||
}
|
||||
|
||||
// ResolveToken 在所有配置源合并完成后读取 token 文件。
|
||||
func (c *Config) ResolveToken() error {
|
||||
if strings.TrimSpace(c.Token) != "" || strings.TrimSpace(c.TokenFile) == "" {
|
||||
c.Token = strings.TrimSpace(c.Token)
|
||||
return nil
|
||||
}
|
||||
data, err := os.ReadFile(strings.TrimSpace(c.TokenFile))
|
||||
if err != nil {
|
||||
return fmt.Errorf("read agent token file: %w", err)
|
||||
}
|
||||
c.Token = strings.TrimSpace(string(data))
|
||||
if c.Token == "" {
|
||||
return errors.New("agent token file is empty")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate 校验必填字段。
|
||||
func (c *Config) Validate() error {
|
||||
masterURL, err := url.Parse(strings.TrimSpace(c.Master))
|
||||
if strings.TrimSpace(c.Master) == "" {
|
||||
return errors.New("master url is required (set via --master, BACKUPX_AGENT_MASTER or config file)")
|
||||
}
|
||||
if err != nil || (masterURL.Scheme != "http" && masterURL.Scheme != "https") || masterURL.Host == "" || masterURL.User != nil || masterURL.RawQuery != "" || masterURL.Fragment != "" {
|
||||
return errors.New("master url must be an absolute http(s) URL without credentials, query or fragment")
|
||||
}
|
||||
if strings.TrimSpace(c.Token) == "" {
|
||||
return errors.New("token is required (set via --token, BACKUPX_AGENT_TOKEN or config file)")
|
||||
return errors.New("token is required (set via --token, --token-file, environment or config file)")
|
||||
}
|
||||
if c.ProxyURL != "" {
|
||||
proxyURL, proxyErr := url.Parse(c.ProxyURL)
|
||||
if proxyErr != nil || proxyURL.Host == "" {
|
||||
return errors.New("proxy url must be an absolute URL")
|
||||
}
|
||||
switch proxyURL.Scheme {
|
||||
case "http", "https", "socks5", "socks5h":
|
||||
default:
|
||||
return errors.New("proxy url scheme must be http, https, socks5 or socks5h")
|
||||
}
|
||||
if proxyURL.RawQuery != "" || proxyURL.Fragment != "" || (proxyURL.Path != "" && proxyURL.Path != "/") {
|
||||
return errors.New("proxy url must not contain a path, query or fragment")
|
||||
}
|
||||
}
|
||||
if c.CACertFile != "" && c.InsecureSkipTLSVerify {
|
||||
return errors.New("ca cert file and insecure TLS cannot be enabled together")
|
||||
}
|
||||
for name, value := range map[string]string{
|
||||
"heartbeat interval": c.HeartbeatInterval,
|
||||
"poll interval": c.PollInterval,
|
||||
} {
|
||||
duration, durationErr := time.ParseDuration(value)
|
||||
if durationErr != nil || duration <= 0 {
|
||||
return fmt.Errorf("%s must be a positive duration", name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -101,5 +188,9 @@ func applyConfigDefaults(cfg *Config) (*Config, error) {
|
||||
cfg.TempDir = "/var/lib/backupx-agent/tmp"
|
||||
}
|
||||
cfg.Master = strings.TrimRight(strings.TrimSpace(cfg.Master), "/")
|
||||
cfg.Token = strings.TrimSpace(cfg.Token)
|
||||
cfg.TokenFile = strings.TrimSpace(cfg.TokenFile)
|
||||
cfg.ProxyURL = strings.TrimSpace(cfg.ProxyURL)
|
||||
cfg.CACertFile = strings.TrimSpace(cfg.CACertFile)
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user