package appconfig import ( "flag" "fmt" "strings" ) // Field sources, for logging only — never for values. const ( SourceFlag = "flag" SourceEnv = "env" SourceFile = "file" SourceDefault = "default" ) // Defaults are the built-in defaults, i.e. exactly what the flags declare. type Defaults struct { Addr string User string Password string WorkDir string } // Values is the effective runtime configuration after merging. type Values struct { Addr string User string Password string WorkDir string Bin string FRPC string Peers []string JoinKey string } // Sources records where each resolved field came from ("" = not configured). type Sources struct { Addr string User string Password string WorkDir string Bin string FRPC string Peers string JoinKey string } // ExplicitSet returns the flag names the operator actually typed. // // flag.Visit walks only flags that were set on the command line, which is // precisely the test required here: an unset flag must NOT override a config // file value with its default. func ExplicitSet(fs *flag.FlagSet) map[string]bool { set := map[string]bool{} fs.Visit(func(f *flag.Flag) { set[f.Name] = true }) return set } // Resolve merges per field: explicit flag > config file (non-empty) > // built-in default. func Resolve(def Defaults, flags Values, explicit map[string]bool, f *File) (Values, Sources) { var out Values var src Sources var file File if f != nil { file = *f } // pick covers the four fields that have a built-in default. pick := func(flagName, flagVal, defVal, fileVal string) (string, string) { if explicit[flagName] { return flagVal, SourceFlag } if fileVal != "" { return fileVal, SourceFile } return defVal, SourceDefault } out.Addr, src.Addr = pick("addr", flags.Addr, def.Addr, file.Addr) out.User, src.User = pick("user", flags.User, def.User, file.User) out.Password, src.Password = pick("password", flags.Password, def.Password, file.Password) out.WorkDir, src.WorkDir = pick("workdir", flags.WorkDir, def.WorkDir, file.WorkDir) // No built-in default for these: empty means "not configured". pickOpt := func(flagName, flagVal, fileVal string) (string, string) { if explicit[flagName] { return flagVal, SourceFlag } if fileVal != "" { return fileVal, SourceFile } return "", "" } out.Bin, src.Bin = pickOpt("bin", flags.Bin, file.Bin) out.FRPC, src.FRPC = pickOpt("frpc", flags.FRPC, file.FRPC) // join-key is special: its flag default comes from $W4F_JOIN_KEY, i.e. a // per-process override that is more specific than a value baked into the // config file. Dropping it here would silently disable env-based cluster // joining whenever a file happened to be present. switch { case explicit["join-key"]: out.JoinKey, src.JoinKey = flags.JoinKey, SourceFlag case flags.JoinKey != "": out.JoinKey, src.JoinKey = flags.JoinKey, SourceEnv case file.JoinKey != "": out.JoinKey, src.JoinKey = file.JoinKey, SourceFile } // peers is repeatable (-peer a -peer b). An explicit flag REPLACES the // file's list instead of appending: appending would resurrect peers the // operator deliberately dropped on the command line. if explicit["peer"] { out.Peers, src.Peers = flags.Peers, SourceFlag } else if len(file.Peers) > 0 { out.Peers, src.Peers = file.Peers, SourceFile } return out, src } // Summary renders the resolved configuration for the startup log with every // secret redacted. Using this instead of ad-hoc formatting means an accidental // log.Printf("%v", values) still cannot leak the basic-auth password or the // cluster join key. func (v Values) Summary() string { return fmt.Sprintf("addr=%s user=%s password= workdir=%s bin=%s frpc=%s peers=%s join-key=", v.Addr, v.User, v.WorkDir, orDash(v.Bin), orDash(v.FRPC), orDash(strings.Join(v.Peers, ","))) } func orDash(s string) string { if s == "" { return "-" } return s } // String implements fmt.Stringer so %v/%s on Values is always redacted. func (v Values) String() string { return v.Summary() }