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
401 lines
11 KiB
Go
401 lines
11 KiB
Go
// 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
|
||
}
|