xfy 6967957299 feat(config): add ${ENV_VAR} interpolation in YAML configuration
Enable environment variable substitution in configuration files using
${VAR} syntax. Supports 12-factor app deployment patterns without
hardcoding secrets or environment-specific values.

Syntax:
- Only ${VAR} with curly braces (avoids conflict with $variable system)
- Missing variables preserved as-is (${MISSING} stays unchanged)
- Multiple variables per line supported
- Adjacent variables ${A}${B} handled correctly

Integration:
- Applied in config.Load() after os.ReadFile, before yaml.Unmarshal
- Applied in processIncludes() for each included file
- 12 unit tests covering single/multiple/missing/empty variables
2026-06-11 23:41:52 +08:00

401 lines
11 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package config 提供 YAML 配置文件的解析、验证和默认配置生成功能。
//
// 该文件包含根配置结构体和加载/保存功能,包括:
// - 根配置结构体
// - 配置文件的加载、保存和验证方法
//
// 主要用途:
//
// 用于定义和管理服务器的完整配置,支持单服务器和多虚拟主机两种模式。
//
// 注意事项:
// - 配置文件使用 YAML 格式
// - 所有配置项都有合理的默认值
// - 配置加载后会自动验证
//
// 作者xfy
package config
import (
"errors"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"gopkg.in/yaml.v3"
)
// 默认配置常量。
const (
// DefaultPprofPath pprof 端点的默认路径。
DefaultPprofPath = "/debug/pprof"
)
// ServerMode 服务器运行模式类型。
//
// 定义服务器的工作模式,支持显式配置或自动推断。
type ServerMode string
// ServerMode 枚举值。
const (
// ServerModeSingle 单服务器模式 - 只运行一个服务器实例。
ServerModeSingle ServerMode = "single"
// ServerModeVHost 虚拟主机模式 - 多个服务器共享相同的监听地址。
ServerModeVHost ServerMode = "vhost"
// ServerModeMultiServer 多服务器模式 - 多个服务器监听不同的地址。
ServerModeMultiServer ServerMode = "multi_server"
// ServerModeAuto 自动模式 - 根据配置自动推断运行模式。
ServerModeAuto ServerMode = "auto"
)
// Config 根配置结构,支持单服务器和多虚拟主机两种模式。
//
// 包含服务器配置、日志配置、性能配置和监控配置等模块。
// 是配置文件的顶级结构体,所有其他配置都作为其子结构。
//
// 注意事项:
// - 必须配置 servers 列表中的至少一个
// - 加载后会自动进行配置验证
// - Stream 配置为可选,用于 TCP/UDP 层代理
// - HTTP/3 配置为可选,需 SSL 配置配合才能生效
//
// 使用示例:
//
// cfg, err := config.Load("config.yaml")
// if err != nil {
// log.Fatal(err)
// }
// // 使用多虚拟主机模式
// for _, s := range cfg.Servers {
// // 处理每个服务器配置
// }
type Config struct {
Mode ServerMode `yaml:"mode"`
Variables VariablesConfig `yaml:"variables"`
Logging LoggingConfig `yaml:"logging"`
Servers []ServerConfig `yaml:"servers"`
Stream []StreamConfig `yaml:"stream"`
Monitoring MonitoringConfig `yaml:"monitoring"`
HTTP3 HTTP3Config `yaml:"http3"`
Resolver ResolverConfig `yaml:"resolver"`
Performance PerformanceConfig `yaml:"performance"`
Shutdown ShutdownConfig `yaml:"shutdown"`
Include []IncludeConfig `yaml:"include"` // 配置引入,支持从其他文件引入配置片段
}
// parseSize 解析大小字符串(支持 k, m 单位)。
func parseSize(s string) (int, error) {
s = strings.TrimSpace(s)
if s == "" {
return 0, strconv.ErrSyntax
}
// 提取单位
unit := strings.ToLower(s[len(s)-1:])
multiplier := 1
numStr := s
switch unit {
case "k":
multiplier = 1024
numStr = s[:len(s)-1]
case "m":
multiplier = 1024 * 1024
numStr = s[:len(s)-1]
}
value, err := strconv.Atoi(numStr)
if err != nil {
return 0, err
}
return value * multiplier, nil
}
// Load 从文件加载配置。
//
// 读取指定路径的 YAML 配置文件,解析并验证配置内容。
//
// 参数:
// - path: 配置文件路径
//
// 返回值:
// - *Config: 解析后的配置对象
// - error: 读取、解析或验证失败时的错误信息
//
// 注意事项:
// - 加载后会自动调用 Validate 进行配置验证
// - 文件不存在或格式错误都会返回错误
func Load(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("读取配置文件失败: %w", err)
}
data = ExpandEnv(data)
// 从默认值开始YAML 只覆盖显式配置的字段。
// 注意yaml.v3 对 slice 会整体替换,因此用户显式配置的 Servers[]
// 元素不会继承 server-level 默认值;但顶层 struct 字段Performance、
// Monitoring、Resolver的默认值会被保留。
cfg := DefaultConfig()
if err := yaml.Unmarshal(data, cfg); err != nil {
return nil, fmt.Errorf("解析配置文件失败: %w", err)
}
if len(cfg.Include) > 0 {
absPath, err := filepath.Abs(path)
if err != nil {
return nil, fmt.Errorf("获取配置文件绝对路径失败: %w", err)
}
visited := map[string]bool{absPath: true}
if err := processIncludes(cfg, filepath.Dir(path), 0, visited); err != nil {
return nil, fmt.Errorf("处理配置引入失败: %w", err)
}
}
if err := Validate(cfg); err != nil {
return nil, fmt.Errorf("配置验证失败: %w", err)
}
return cfg, nil
}
const maxIncludeDepth = 10
func processIncludes(cfg *Config, baseDir string, depth int, visited map[string]bool) error {
if depth >= maxIncludeDepth {
return fmt.Errorf("配置引入嵌套深度超过 %d 层", maxIncludeDepth)
}
for _, inc := range cfg.Include {
pattern := inc.Path
if !filepath.IsAbs(pattern) {
pattern = filepath.Join(baseDir, pattern)
}
matches, err := filepath.Glob(pattern)
if err != nil {
return fmt.Errorf("展开引入路径 %q 失败: %w", inc.Path, err)
}
if len(matches) == 0 {
return fmt.Errorf("引入路径 %q 未匹配到任何文件", inc.Path)
}
for _, match := range matches {
absMatch, err := filepath.Abs(match)
if err != nil {
return fmt.Errorf("获取引入文件绝对路径失败 %q: %w", match, err)
}
if visited[absMatch] {
return fmt.Errorf("检测到循环引入: %s", absMatch)
}
visited[absMatch] = true
info, err := os.Stat(match)
if err != nil {
return fmt.Errorf("读取引入文件 %q 失败: %w", match, err)
}
if info.IsDir() {
delete(visited, absMatch)
continue
}
data, err := os.ReadFile(match)
if err != nil {
return fmt.Errorf("读取引入文件 %q 失败: %w", match, err)
}
data = ExpandEnv(data)
var included Config
if err := yaml.Unmarshal(data, &included); err != nil {
return fmt.Errorf("解析引入文件 %q 失败: %w", match, err)
}
if len(included.Include) > 0 {
if err := processIncludes(&included, filepath.Dir(match), depth+1, visited); err != nil {
return err
}
}
cfg.Servers = append(cfg.Servers, included.Servers...)
cfg.Stream = append(cfg.Stream, included.Stream...)
for k, v := range included.Variables.Set {
if _, exists := cfg.Variables.Set[k]; !exists {
if cfg.Variables.Set == nil {
cfg.Variables.Set = make(map[string]string)
}
cfg.Variables.Set[k] = v
}
}
delete(visited, absMatch)
}
}
cfg.Include = nil
return nil
}
// HasServers 检查是否为多虚拟主机模式。
//
// 返回值:
// - bool: 如果配置了 servers 列表且非空,返回 true
func (c *Config) HasServers() bool {
return len(c.Servers) > 0
}
// GetDefaultServerFromList 从 servers 列表中获取默认服务器配置。
//
// 遍历 servers 列表,返回第一个 Default 标记为 true 的服务器。
// 用于在虚拟主机模式下获取默认服务器的配置作为 fallback。
//
// 返回值:
// - *ServerConfig: 默认服务器配置,如无则返回 nil
func (c *Config) GetDefaultServerFromList() *ServerConfig {
for i := range c.Servers {
if c.Servers[i].Default {
return &c.Servers[i]
}
}
return nil
}
// GetMode 获取服务器运行模式。
//
// 如果 Mode 显式设置(非 auto返回设置的值。
// 如果 Mode 是 auto 或未设置,根据配置自动推断:
// - servers 数量 == 1 → single
// - servers 数量 > 1 且所有 listen 地址相同 → vhost
// - servers 数量 > 1 且 listen 地址不同 → multi_server
//
// 返回值:
// - ServerMode: 推断后的服务器运行模式
func (c *Config) GetMode() ServerMode {
// 如果显式设置了非 auto 模式,直接返回
if c.Mode != "" && c.Mode != ServerModeAuto {
return c.Mode
}
// 自动推断模式
serverCount := len(c.Servers)
// servers 为空 → auto配置验证会确保至少有一个服务器
if serverCount == 0 {
return ServerModeAuto
}
// servers 数量 == 1 → single
if serverCount == 1 {
return ServerModeSingle
}
// servers 数量 > 1检查 listen 地址
firstListen := c.Servers[0].Listen
allSameListen := true
for i := 1; i < serverCount; i++ {
if c.Servers[i].Listen != firstListen {
allSameListen = false
break
}
}
// 所有 listen 地址相同 → vhost否则 → multi_server
if allSameListen {
return ServerModeVHost
}
return ServerModeMultiServer
}
// Validate 配置验证入口。
//
// 验证配置的完整性和有效性,检查是否至少配置了一个服务器,
// 并递归验证所有服务器配置。
//
// 参数:
// - cfg: 配置对象
//
// 返回值:
// - error: 验证失败时的错误信息,包含具体字段路径
//
// 验证规则:
// - 必须配置 servers 数组且至少包含一个服务器
// - 所有服务器配置必须通过 validateServer 验证
func Validate(cfg *Config) error {
// 必须配置 servers 且至少包含一个服务器
if !cfg.HasServers() {
return errors.New("必须配置 servers 且至少包含一个服务器")
}
// 验证模式
if err := validateMode(cfg.Mode); err != nil {
return err
}
// 验证监听地址冲突multi_server 模式)
if err := validateListenConflicts(cfg.Servers, cfg.GetMode()); err != nil {
return err
}
// 验证 default 服务器唯一性
if err := validateDefaultServer(cfg.Servers); err != nil {
return err
}
// 验证所有服务器
for i := range cfg.Servers {
if err := validateServer(&cfg.Servers[i], false); err != nil {
return fmt.Errorf("servers[%d]: %w", i, err)
}
}
// 验证 Stream 配置
for i := range cfg.Stream {
if err := validateStream(&cfg.Stream[i]); err != nil {
return fmt.Errorf("stream[%d]: %w", i, err)
}
}
// 验证日志配置
if err := validateLogging(&cfg.Logging); err != nil {
return err
}
// 验证性能配置
if err := validatePerformance(&cfg.Performance); err != nil {
return fmt.Errorf("performance: %w", err)
}
// 验证 Resolver 配置
if err := cfg.Resolver.Validate(); err != nil {
return fmt.Errorf("resolver: %w", err)
}
// 验证变量配置
if err := validateVariables(&cfg.Variables); err != nil {
return fmt.Errorf("variables: %w", err)
}
// 验证关闭配置
if err := validateShutdown(&cfg.Shutdown); err != nil {
return err
}
return nil
}
// validateShutdown 验证关闭配置。
func validateShutdown(cfg *ShutdownConfig) error {
if cfg.GracefulTimeout < 0 {
return errors.New("shutdown.graceful_timeout 不能为负数")
}
if cfg.FastTimeout < 0 {
return errors.New("shutdown.fast_timeout 不能为负数")
}
// 0 值表示使用默认值,在应用层处理
return nil
}