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
This commit is contained in:
parent
f605ef3b44
commit
6967957299
@ -134,6 +134,8 @@ func Load(path string) (*Config, error) {
|
|||||||
return nil, fmt.Errorf("读取配置文件失败: %w", err)
|
return nil, fmt.Errorf("读取配置文件失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data = ExpandEnv(data)
|
||||||
|
|
||||||
// 从默认值开始,YAML 只覆盖显式配置的字段。
|
// 从默认值开始,YAML 只覆盖显式配置的字段。
|
||||||
// 注意:yaml.v3 对 slice 会整体替换,因此用户显式配置的 Servers[]
|
// 注意:yaml.v3 对 slice 会整体替换,因此用户显式配置的 Servers[]
|
||||||
// 元素不会继承 server-level 默认值;但顶层 struct 字段(Performance、
|
// 元素不会继承 server-level 默认值;但顶层 struct 字段(Performance、
|
||||||
@ -206,6 +208,8 @@ func processIncludes(cfg *Config, baseDir string, depth int, visited map[string]
|
|||||||
return fmt.Errorf("读取引入文件 %q 失败: %w", match, err)
|
return fmt.Errorf("读取引入文件 %q 失败: %w", match, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data = ExpandEnv(data)
|
||||||
|
|
||||||
var included Config
|
var included Config
|
||||||
if err := yaml.Unmarshal(data, &included); err != nil {
|
if err := yaml.Unmarshal(data, &included); err != nil {
|
||||||
return fmt.Errorf("解析引入文件 %q 失败: %w", match, err)
|
return fmt.Errorf("解析引入文件 %q 失败: %w", match, err)
|
||||||
|
|||||||
22
internal/config/env.go
Normal file
22
internal/config/env.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var envPattern = regexp.MustCompile(`\$\{([^}]+)\}`)
|
||||||
|
|
||||||
|
// ExpandEnv replaces ${VAR} patterns with environment variable values.
|
||||||
|
func ExpandEnv(data []byte) []byte {
|
||||||
|
return envPattern.ReplaceAllFunc(data, func(match []byte) []byte {
|
||||||
|
name := string(match[2 : len(match)-1])
|
||||||
|
if name == "" {
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
if value, ok := os.LookupEnv(name); ok {
|
||||||
|
return []byte(value)
|
||||||
|
}
|
||||||
|
return match
|
||||||
|
})
|
||||||
|
}
|
||||||
121
internal/config/env_test.go
Normal file
121
internal/config/env_test.go
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestExpandEnv(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected string
|
||||||
|
setup func(*testing.T)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "single variable",
|
||||||
|
input: "${VAR}",
|
||||||
|
expected: "value",
|
||||||
|
setup: func(t *testing.T) {
|
||||||
|
t.Setenv("VAR", "value")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple variables",
|
||||||
|
input: "${HOST}:${PORT}",
|
||||||
|
expected: "localhost:8080",
|
||||||
|
setup: func(t *testing.T) {
|
||||||
|
t.Setenv("HOST", "localhost")
|
||||||
|
t.Setenv("PORT", "8080")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "variable with prefix and suffix",
|
||||||
|
input: "prefix_${VAR}_suffix",
|
||||||
|
expected: "prefix_value_suffix",
|
||||||
|
setup: func(t *testing.T) {
|
||||||
|
t.Setenv("VAR", "value")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing variable unchanged",
|
||||||
|
input: "${MISSING}",
|
||||||
|
expected: "${MISSING}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mixed existing and missing",
|
||||||
|
input: "${VAR}/${MISSING}",
|
||||||
|
expected: "value/${MISSING}",
|
||||||
|
setup: func(t *testing.T) {
|
||||||
|
t.Setenv("VAR", "value")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty input",
|
||||||
|
input: "",
|
||||||
|
expected: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no variables",
|
||||||
|
input: "just a plain string",
|
||||||
|
expected: "just a plain string",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty variable name",
|
||||||
|
input: "${}",
|
||||||
|
expected: "${}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "adjacent variables",
|
||||||
|
input: "${VAR1}${VAR2}",
|
||||||
|
expected: "value1value2",
|
||||||
|
setup: func(t *testing.T) {
|
||||||
|
t.Setenv("VAR1", "value1")
|
||||||
|
t.Setenv("VAR2", "value2")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "variable with empty value",
|
||||||
|
input: "${EMPTY_VAR}",
|
||||||
|
expected: "",
|
||||||
|
setup: func(t *testing.T) {
|
||||||
|
t.Setenv("EMPTY_VAR", "")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "same variable multiple times",
|
||||||
|
input: "${VAR}-${VAR}-${VAR}",
|
||||||
|
expected: "val-val-val",
|
||||||
|
setup: func(t *testing.T) {
|
||||||
|
t.Setenv("VAR", "val")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "full yaml-like input",
|
||||||
|
input: `server:
|
||||||
|
host: ${HOST}
|
||||||
|
port: ${PORT}
|
||||||
|
name: ${APP_NAME}`,
|
||||||
|
expected: `server:
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 9090
|
||||||
|
name: lolly`,
|
||||||
|
setup: func(t *testing.T) {
|
||||||
|
t.Setenv("HOST", "127.0.0.1")
|
||||||
|
t.Setenv("PORT", "9090")
|
||||||
|
t.Setenv("APP_NAME", "lolly")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if tt.setup != nil {
|
||||||
|
tt.setup(t)
|
||||||
|
}
|
||||||
|
result := ExpandEnv([]byte(tt.input))
|
||||||
|
assert.Equal(t, tt.expected, string(result))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user