package config import ( "encoding/json" "fmt" "os" "path/filepath" "reflect" "strings" "time" ) // InstanceSettings 是实例持久化部署设置(KTD17 第五层): // 内置默认 → .env → 进程 env → CLI → instance-settings(本层最终生效)。 // 文件位于 /instance-settings.json;config_root 经显式 CLI/env // 定位,本文件不得自覆盖定位字段(bootstrap 自覆盖防护)。 type InstanceSettings struct { Revision int `json:"revision,omitempty"` Settings map[string]any `json:"settings"` } // bootstrapFields 是 instance-settings 永不生效的定位/身份字段 // (自覆盖会构成“用文件定位文件”的循环或变更实例身份)。 var bootstrapFields = map[string]bool{ "env": true, // 实例环境名属启动定位,仅 .env/进程 env/CLI 可定 } // DefaultConfigRoot 解析配置根:显式 CONFIG_ROOT 进程变量,否则工作目录。 // (后续 U10c 增加 instance_id 与独立配置目录布局时扩展。) func DefaultConfigRoot() string { if v := os.Getenv("CONFIG_ROOT"); strings.TrimSpace(v) != "" { return v } return "." } // InstanceSettingsPath 返回实例设置文件路径(相对 config_root)。 func InstanceSettingsPath(configRoot string) string { return filepath.Join(configRoot, "instance-settings.json") } // settingsModeIgnored 报告运维是否显式跳过实例设置层(恢复路径)。 func settingsModeIgnored() bool { return strings.TrimSpace(os.Getenv("SETTINGS_MODE")) == "ignore" } // loadInstanceSettings 读取并校验实例设置文件;缺失返回 nil。 func loadInstanceSettings(configRoot string) (*InstanceSettings, error) { path := InstanceSettingsPath(configRoot) fi, err := os.Stat(path) if err != nil { if os.IsNotExist(err) { return nil, nil } return nil, fmt.Errorf("instance settings stat %s: %w", path, err) } if fi.Mode().Perm()&0o022 != 0 { return nil, fmt.Errorf("instance settings %s must not be group/world writable (mode %o)", path, fi.Mode().Perm()) } raw, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("instance settings read %s: %w", path, err) } var is InstanceSettings if err := json.Unmarshal(raw, &is); err != nil { return nil, fmt.Errorf("instance settings parse %s: %w", path, err) } if is.Settings == nil { is.Settings = map[string]any{} } for k := range is.Settings { if bootstrapFields[k] { return nil, fmt.Errorf("instance settings %s: bootstrap field %q cannot be overridden here", path, k) } } return &is, nil } // applyInstanceSettings 将设置按字段 json 名覆盖到 cfg;类型不匹配即失败 // (整体校验原则:宁可拒绝启动,不猜测转换)。缺省键=继承上层,不做删除语义。 func applyInstanceSettings(cfg *Config, is *InstanceSettings) error { if cfg == nil || is == nil { return nil } v := reflect.ValueOf(cfg).Elem() t := v.Type() byJSON := map[string]int{} for i := 0; i < t.NumField(); i++ { name := jsonNameOf(t.Field(i)) if name != "" { byJSON[name] = i } } for key, val := range is.Settings { idx, ok := byJSON[key] if !ok { return fmt.Errorf("instance settings: unknown field %q", key) } fv := v.Field(idx) if !fv.CanSet() { return fmt.Errorf("instance settings: field %q not settable", key) } nv, err := coerceSettingValue(fv, val, key) if err != nil { return err } fv.Set(nv) } return nil } func jsonNameOf(f reflect.StructField) string { name := f.Tag.Get("json") if i := strings.IndexByte(name, ','); i >= 0 { name = name[:i] } if name == "" { return strings.ToLower(f.Name) } return name } func coerceSettingValue(fv reflect.Value, val any, key string) (reflect.Value, error) { switch fv.Type() { case reflect.TypeOf(""): s, ok := val.(string) if !ok { return fv, fmt.Errorf("instance settings: field %q wants string, got %T", key, val) } return reflect.ValueOf(s), nil case reflect.TypeOf(0), reflect.TypeOf(int64(0)): n, ok := val.(float64) // json.Number 默认解码为 float64 if !ok { return fv, fmt.Errorf("instance settings: field %q wants number, got %T", key, val) } iv := int64(n) if float64(iv) != n { return fv, fmt.Errorf("instance settings: field %q is not an integer: %v", key, n) } return reflect.ValueOf(iv).Convert(fv.Type()), nil case reflect.TypeOf(false): b, ok := val.(bool) if !ok { return fv, fmt.Errorf("instance settings: field %q wants bool, got %T", key, val) } return reflect.ValueOf(b), nil case reflect.TypeOf(time.Duration(0)): switch x := val.(type) { case string: d, err := time.ParseDuration(x) if err != nil { return fv, fmt.Errorf("instance settings: field %q duration: %w", key, err) } return reflect.ValueOf(d), nil case float64: // 数字按秒解释(部署配置直觉单位),必须整秒。 if float64(int64(x)) != x { return fv, fmt.Errorf("instance settings: field %q duration seconds not integer: %v", key, x) } return reflect.ValueOf(time.Duration(x) * time.Second), nil default: return fv, fmt.Errorf("instance settings: field %q wants duration string, got %T", key, val) } case reflect.TypeOf([]string(nil)): arr, ok := val.([]any) if !ok { return fv, fmt.Errorf("instance settings: field %q wants string array, got %T", key, val) } out := make([]string, 0, len(arr)) for _, e := range arr { s, ok := e.(string) if !ok { return fv, fmt.Errorf("instance settings: field %q array element must be string, got %T", key, e) } out = append(out, s) } return reflect.ValueOf(out), nil } return fv, fmt.Errorf("instance settings: field %q type %s not supported", key, fv.Type()) }