签入版本
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
// Package config 提供配置读取的环境变量覆盖接线
|
||||
//
|
||||
// 覆盖规则遵循 12-Factor 标准优先级:GF_ 前缀环境变量 > 配置文件。
|
||||
// 键名规则:配置路径大写、点换下划线并加 GF_ 前缀
|
||||
// (database.default.link → GF_DATABASE_DEFAULT_LINK)。
|
||||
// 实现方式为包装默认文件适配器(gcfg.Adapter 是框架官方扩展点),
|
||||
// 注册后全部 Get/MustGet 读取点零改动地获得覆盖能力。
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/os/gcfg"
|
||||
"github.com/gogf/gf/v2/os/genv"
|
||||
)
|
||||
|
||||
// adapterEnvFirst 环境变量优先的配置适配器
|
||||
//
|
||||
// 仅覆盖配置文件中已存在的键:环境变量名由配置路径正向推导(无歧义),
|
||||
// 而反向拆分下划线无法还原 uploadDir 这类驼峰键名,故不做补键注入。
|
||||
// 文件返回的 map 会被就地叠加环境变量值,进程内环境恒定,重复叠加幂等。
|
||||
type adapterEnvFirst struct {
|
||||
base gcfg.Adapter // 默认文件适配器,环境未命中时回落
|
||||
}
|
||||
|
||||
// RegisterEnvOverride 注册环境变量优先的配置适配器(包装并替换默认文件适配器)
|
||||
//
|
||||
// 须在首次读取配置之前调用(服务启动入口最前,含缓存/数据库/Redis 初始化);
|
||||
// 未设置任何 GF_ 环境变量时行为与默认文件适配器完全一致。
|
||||
func RegisterEnvOverride() {
|
||||
gcfg.Instance().SetAdapter(&adapterEnvFirst{base: gcfg.Instance().GetAdapter()})
|
||||
}
|
||||
|
||||
// Available 委托文件适配器
|
||||
func (a *adapterEnvFirst) Available(ctx context.Context, resource ...string) bool {
|
||||
return a.base.Available(ctx, resource...)
|
||||
}
|
||||
|
||||
// Get 先查 GF_ 前缀环境变量,未命中回落文件适配器;
|
||||
// 文件返回 map(如 database 整节点)时对已有键逐叶叠加环境变量覆盖
|
||||
func (a *adapterEnvFirst) Get(ctx context.Context, pattern string) (any, error) {
|
||||
if v := envValue(pattern); v != nil {
|
||||
return v, nil
|
||||
}
|
||||
value, err := a.base.Get(ctx, pattern)
|
||||
if err != nil || value == nil {
|
||||
return value, err
|
||||
}
|
||||
if m, ok := value.(map[string]any); ok {
|
||||
overlayMap(m, pattern)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// Data 返回全量配置并叠加环境变量覆盖
|
||||
func (a *adapterEnvFirst) Data(ctx context.Context) (map[string]any, error) {
|
||||
data, err := a.base.Data(ctx)
|
||||
if err != nil || data == nil {
|
||||
return data, err
|
||||
}
|
||||
overlayMap(data, "")
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// envValue 按键路径取 GF_ 前缀环境变量;未设置或空值返回 nil(空串视为未设置,
|
||||
// 避免容器编排注入的空占位把配置文件中的有效值清空)
|
||||
func envValue(pattern string) any {
|
||||
if pattern == "" || pattern == "." {
|
||||
return nil
|
||||
}
|
||||
v := genv.Get("GF_" + strings.ToUpper(strings.ReplaceAll(pattern, ".", "_")))
|
||||
if v == nil || v.String() == "" {
|
||||
return nil
|
||||
}
|
||||
return v.Val()
|
||||
}
|
||||
|
||||
// overlayMap 对 map 中已存在的键递归叠加环境变量覆盖
|
||||
//
|
||||
// prefix 为当前层级的配置路径(顶层为空);仅处理标量覆盖,
|
||||
// 切片/数组不支持环境变量替换(本项目可覆盖键均为标量)
|
||||
func overlayMap(m map[string]any, prefix string) {
|
||||
for k, val := range m {
|
||||
path := k
|
||||
if prefix != "" && prefix != "." {
|
||||
path = prefix + "." + k
|
||||
}
|
||||
if env := envValue(path); env != nil {
|
||||
m[k] = env
|
||||
continue
|
||||
}
|
||||
if child, ok := val.(map[string]any); ok {
|
||||
overlayMap(child, path)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/gogf/gf/v2/os/gcfg"
|
||||
)
|
||||
|
||||
// fakeBase 模拟文件适配器,返回固定配置树
|
||||
type fakeBase struct {
|
||||
data map[string]any
|
||||
}
|
||||
|
||||
func (f *fakeBase) Available(ctx context.Context, resource ...string) bool { return true }
|
||||
|
||||
func (f *fakeBase) Get(ctx context.Context, pattern string) (any, error) {
|
||||
if pattern == "" || pattern == "." {
|
||||
return f.data, nil
|
||||
}
|
||||
var cur any = f.data
|
||||
for _, part := range splitPattern(pattern) {
|
||||
m, ok := cur.(map[string]any)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
cur = m[part]
|
||||
}
|
||||
return cur, nil
|
||||
}
|
||||
|
||||
func (f *fakeBase) Data(ctx context.Context) (map[string]any, error) { return f.data, nil }
|
||||
|
||||
func splitPattern(pattern string) []string {
|
||||
var parts []string
|
||||
start := 0
|
||||
for i := 0; i < len(pattern); i++ {
|
||||
if pattern[i] == '.' {
|
||||
parts = append(parts, pattern[start:i])
|
||||
start = i + 1
|
||||
}
|
||||
}
|
||||
return append(parts, pattern[start:])
|
||||
}
|
||||
|
||||
// TestAdapterEnvFirst 覆盖优先级核心行为:
|
||||
// - 设置 GF_ 环境变量 → 覆盖文件值(含 map 整节点逐叶叠加)
|
||||
// - 未设置 → 回落文件值
|
||||
// - 空值环境变量 → 视为未设置,不清空文件值
|
||||
func TestAdapterEnvFirst(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
base := &fakeBase{data: map[string]any{
|
||||
"database": map[string]any{
|
||||
"default": map[string]any{
|
||||
"type": "mysql",
|
||||
"link": "file-link",
|
||||
},
|
||||
},
|
||||
"jwt": map[string]any{"secret": "file-secret"},
|
||||
}}
|
||||
a := &adapterEnvFirst{base: base}
|
||||
|
||||
// 1. 未设置环境变量:文件值原样返回
|
||||
if v, _ := a.Get(ctx, "database.default.link"); v != "file-link" {
|
||||
t.Fatalf("未设环境变量时应返回文件值,得到: %v", v)
|
||||
}
|
||||
|
||||
// 2. 标量覆盖
|
||||
os.Setenv("GF_JWT_SECRET", "env-secret")
|
||||
defer os.Unsetenv("GF_JWT_SECRET")
|
||||
if v, _ := a.Get(ctx, "jwt.secret"); v != "env-secret" {
|
||||
t.Fatalf("环境变量应覆盖文件值,得到: %v", v)
|
||||
}
|
||||
|
||||
// 3. map 整节点逐叶叠加(gdb 读 database 整节点的场景)
|
||||
os.Setenv("GF_DATABASE_DEFAULT_LINK", "env-link")
|
||||
defer os.Unsetenv("GF_DATABASE_DEFAULT_LINK")
|
||||
v, _ := a.Get(ctx, "database")
|
||||
m, ok := v.(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("database 节点应返回 map,得到: %T", v)
|
||||
}
|
||||
def := m["default"].(map[string]any)
|
||||
if def["link"] != "env-link" {
|
||||
t.Fatalf("整节点读取时 link 应被环境变量覆盖,得到: %v", def["link"])
|
||||
}
|
||||
if def["type"] != "mysql" {
|
||||
t.Fatalf("未覆盖的键应保持文件值,得到: %v", def["type"])
|
||||
}
|
||||
|
||||
// 4. 空值环境变量视为未设置(不清空文件值)
|
||||
os.Setenv("GF_DATABASE_DEFAULT_TYPE", "")
|
||||
defer os.Unsetenv("GF_DATABASE_DEFAULT_TYPE")
|
||||
if v, _ := a.Get(ctx, "database.default.type"); v != "mysql" {
|
||||
t.Fatalf("空环境变量应视为未设置,得到: %v", v)
|
||||
}
|
||||
|
||||
// 5. Data 全量叠加
|
||||
data, _ := a.Data(ctx)
|
||||
defData := data["database"].(map[string]any)["default"].(map[string]any)
|
||||
if defData["link"] != "env-link" {
|
||||
t.Fatalf("Data 应叠加环境变量覆盖,得到: %v", defData["link"])
|
||||
}
|
||||
}
|
||||
|
||||
// TestRegisterEnvOverride 注册后 g.Cfg() 读取链路获得覆盖能力
|
||||
func TestRegisterEnvOverride(t *testing.T) {
|
||||
adapter, err := gcfg.NewAdapterContent(`database:
|
||||
default:
|
||||
link: file-link
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatalf("构造内容适配器失败: %v", err)
|
||||
}
|
||||
gcfg.Instance().SetAdapter(adapter)
|
||||
RegisterEnvOverride()
|
||||
ctx := context.Background()
|
||||
|
||||
os.Setenv("GF_DATABASE_DEFAULT_LINK", "env-link")
|
||||
defer os.Unsetenv("GF_DATABASE_DEFAULT_LINK")
|
||||
if v := gcfg.Instance().MustGet(ctx, "database.default.link").String(); v != "env-link" {
|
||||
t.Fatalf("注册后 g.Cfg 读取应获得环境变量覆盖,得到: %v", v)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user