package config import ( "crypto/rand" "encoding/hex" "encoding/json" "errors" "fmt" "os" "strconv" "time" ) // DefaultConfigFileName 是未指定 -config 时在工作目录下使用的默认配置文件名。 const DefaultConfigFileName = "config.json" // Config 包含运行时、上报、采集与服务注册相关的全部配置项。 type Config struct { // HostID 是对外上报和查询时使用的机器标识;为空时采集器会尽力回退为主机名。 HostID string `json:"host_id"` // HTTP 控制本地查询 API 的监听与 token 鉴权。 HTTP HTTPConfig `json:"http"` // Collect 控制后台快照采集周期与单轮超时。 Collect CollectConfig `json:"collect"` // Report 控制可选的中心端主动上报流程。 Report ReportConfig `json:"report"` // Service 控制 Linux systemd unit 与 Windows 服务注册元数据。 Service ServiceConfig `json:"service"` } // HTTPConfig 控制本地 HTTP API 的监听地址与查询参数鉴权 token。 type HTTPConfig struct { // ListenAddress 是 HTTP API 监听地址,默认 0.0.0.0:8080 会暴露到所有网卡。 ListenAddress string `json:"listen_address"` // Token 用于 GET 请求查询参数 ?token= 的鉴权,禁止为空。 Token string `json:"token"` } // CollectConfig 控制周期性主机状态采集。 type CollectConfig struct { // Interval 是后台刷新 HostStatus 缓存的周期。 Interval Duration `json:"interval"` // Timeout 是单轮聚合采集的整体截止时间。 Timeout Duration `json:"timeout"` } // ReportConfig 控制可选的周期性状态 POST 上报。 type ReportConfig struct { // Enabled 控制是否启动主动上报循环。 Enabled bool `json:"enabled"` // Endpoint 是中心端接收 HostStatus JSON 的 HTTP URL,启用上报时必填。 Endpoint string `json:"endpoint"` // Interval 是两次上报尝试之间的周期。 Interval Duration `json:"interval"` // Timeout 是单次 POST 请求的 HTTP 客户端超时。 Timeout Duration `json:"timeout"` } // ServiceConfig 控制操作系统服务注册时使用的元数据。 type ServiceConfig struct { // Name 是 systemd unit 名称和 Windows 服务名。 Name string `json:"name"` // DisplayName 是 Windows 服务管理器中显示的名称。 DisplayName string `json:"display_name"` // Description 是 systemd unit 与 Windows 服务的说明文本。 Description string `json:"description"` } // Duration 封装 time.Duration,使 JSON 配置可使用 Go 时长字符串(如 "30s")。 type Duration time.Duration // Duration 返回标准库的 time.Duration 值。 func (d Duration) Duration() time.Duration { return time.Duration(d) } // MarshalJSON 将时长序列化为 Go 时长字符串,例如 "30s"。 func (d Duration) MarshalJSON() ([]byte, error) { return json.Marshal(time.Duration(d).String()) } // UnmarshalJSON 从 JSON 解析 Go 时长字符串。 func (d *Duration) UnmarshalJSON(data []byte) error { var raw string if err := json.Unmarshal(data, &raw); err != nil { return err } parsed, err := time.ParseDuration(raw) if err != nil { return err } *d = Duration(parsed) return nil } // Default 返回内存中的默认配置(不含 token,需由配置文件或 LoadOrCreate 提供)。 func Default() Config { return Config{ HTTP: HTTPConfig{ListenAddress: "0.0.0.0:8080"}, Collect: CollectConfig{ Interval: Duration(30 * time.Second), Timeout: Duration(10 * time.Second), }, Report: ReportConfig{ Enabled: false, Interval: Duration(60 * time.Second), Timeout: Duration(5 * time.Second), }, Service: ServiceConfig{ Name: "mfd-monitor-agent", DisplayName: "MFD Monitor Agent", Description: "Collects host status and exposes it over HTTP.", }, } } // ResolvePath 将 CLI 传入的配置路径解析为实际文件路径。 func ResolvePath(explicit string) string { if explicit != "" { return explicit } return DefaultConfigFileName } // LoadOrCreate 在配置文件不存在时写入含随机 token 的默认配置,再加载。 // 仅供 agent run 使用;服务安装等场景应使用 Load。 func LoadOrCreate(path string) (Config, error) { if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) { cfg := Default() // 自动生成配置时必须创建强随机 token,避免默认空 token 暴露主机状态 API。 token, err := generateToken() if err != nil { return cfg, fmt.Errorf("generate http token: %w", err) } cfg.HTTP.Token = token if err := writeConfigFile(path, cfg); err != nil { return cfg, err } } return Load(path) } // Load 读取 JSON 配置文件,应用环境变量覆盖,并校验必填项。 func Load(path string) (Config, error) { cfg := Default() if path == "" { return cfg, errors.New("config path is required") } data, err := os.ReadFile(path) if err != nil { return cfg, err } if err := json.Unmarshal(data, &cfg); err != nil { return cfg, err } applyEnv(&cfg) if err := validate(&cfg); err != nil { return cfg, err } return cfg, nil } // validate 检查配置中的必填与安全约束。 func validate(cfg *Config) error { if cfg.HTTP.Token == "" { return errors.New("http.token is required") } if cfg.Report.Enabled && cfg.Report.Endpoint == "" { return errors.New("report endpoint is required when reporting is enabled") } return nil } // writeConfigFile 将配置以缩进 JSON 写入 path,权限 0600。 func writeConfigFile(path string, cfg Config) error { data, err := json.MarshalIndent(cfg, "", " ") if err != nil { return fmt.Errorf("marshal config: %w", err) } data = append(data, '\n') // 配置内包含 HTTP token,默认只允许当前用户读写。 if err := os.WriteFile(path, data, 0o600); err != nil { return fmt.Errorf("write config %s: %w", path, err) } return nil } // generateToken 生成用于 HTTP 查询参数鉴权的随机十六进制字符串。 func generateToken() (string, error) { buf := make([]byte, 32) if _, err := rand.Read(buf); err != nil { return "", err } return hex.EncodeToString(buf), nil } // applyEnv 将支持的 MFD_AGENT_* 环境变量覆盖到 cfg 上。 func applyEnv(cfg *Config) { if value := os.Getenv("MFD_AGENT_HOST_ID"); value != "" { cfg.HostID = value } if value := os.Getenv("MFD_AGENT_LISTEN"); value != "" { cfg.HTTP.ListenAddress = value } if value := os.Getenv("MFD_AGENT_TOKEN"); value != "" { cfg.HTTP.Token = value } if value := os.Getenv("MFD_AGENT_REPORT_ENABLED"); value != "" { // 非法环境变量值保持文件配置不变,避免拼写错误让 agent 无法启动。 if parsed, err := strconv.ParseBool(value); err == nil { cfg.Report.Enabled = parsed } } if value := os.Getenv("MFD_AGENT_REPORT_ENDPOINT"); value != "" { cfg.Report.Endpoint = value } if value := os.Getenv("MFD_AGENT_REPORT_INTERVAL"); value != "" { if parsed, err := time.ParseDuration(value); err == nil { cfg.Report.Interval = Duration(parsed) } } if value := os.Getenv("MFD_AGENT_REPORT_TIMEOUT"); value != "" { if parsed, err := time.ParseDuration(value); err == nil { cfg.Report.Timeout = Duration(parsed) } } if value := os.Getenv("MFD_AGENT_COLLECT_INTERVAL"); value != "" { if parsed, err := time.ParseDuration(value); err == nil { cfg.Collect.Interval = Duration(parsed) } } if value := os.Getenv("MFD_AGENT_COLLECT_TIMEOUT"); value != "" { if parsed, err := time.ParseDuration(value); err == nil { cfg.Collect.Timeout = Duration(parsed) } } }