mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-01 14:46:36 +08:00
feat(config): load manifest config.default.yaml with config.yaml override and clean root configs
This commit is contained in:
@@ -16,15 +16,14 @@ import (
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// DefaultFileName is the configuration file looked up when CONFIG_PATH is unset.
|
||||
const DefaultFileName = "config.yaml"
|
||||
// DefaultBaseFileName is the default base configuration file path looked up relative to the workspace.
|
||||
const DefaultBaseFileName = "manifest/config/config.default.yaml"
|
||||
|
||||
// DefaultCandidates defines the prioritized list of configuration file paths looked up
|
||||
// when CONFIG_PATH is unset.
|
||||
var DefaultCandidates = []string{
|
||||
"manifest/config/config.yaml",
|
||||
DefaultFileName,
|
||||
}
|
||||
// DefaultOverrideFileName is the default user override configuration file path looked up relative to the workspace.
|
||||
const DefaultOverrideFileName = "manifest/config/config.yaml"
|
||||
|
||||
// DefaultFileName is kept for backwards compatibility.
|
||||
const DefaultFileName = DefaultOverrideFileName
|
||||
|
||||
// EnvOnlyOrigin is reported by Describe when no configuration file was loaded.
|
||||
const EnvOnlyOrigin = "<env only>"
|
||||
@@ -40,47 +39,94 @@ type Option func(*Source)
|
||||
func WithPath(path string) Option {
|
||||
return func(s *Source) {
|
||||
s.path = path
|
||||
s.pinned = true
|
||||
}
|
||||
}
|
||||
|
||||
// WithDefaultPath pins the default base configuration file path.
|
||||
func WithDefaultPath(path string) Option {
|
||||
return func(s *Source) {
|
||||
s.defaultPath = path
|
||||
}
|
||||
}
|
||||
|
||||
// Source implements core.ConfigSource over a configuration file plus the process environment.
|
||||
type Source struct {
|
||||
v *viper.Viper
|
||||
path string
|
||||
found bool
|
||||
v *viper.Viper
|
||||
path string
|
||||
defaultPath string
|
||||
pinned bool
|
||||
defaultFound bool
|
||||
overrideFound bool
|
||||
found bool
|
||||
}
|
||||
|
||||
// NewSource loads the configuration file. A missing file is not an error: the source
|
||||
// then serves environment values only, matching the behaviour the previous pkg/config
|
||||
// loader had for deployments that configure everything through the environment.
|
||||
// NewSource loads configuration files. By default, it loads manifest/config/config.default.yaml
|
||||
func (s *Source) resolvePaths() {
|
||||
if s.pinned {
|
||||
return
|
||||
}
|
||||
if s.defaultPath == "" {
|
||||
s.defaultPath, _ = findExistingUpward(DefaultBaseFileName)
|
||||
}
|
||||
if s.path == "" {
|
||||
s.path = os.Getenv("CONFIG_PATH")
|
||||
}
|
||||
if s.path == "" {
|
||||
s.path, _ = findExistingUpward(DefaultOverrideFileName)
|
||||
}
|
||||
}
|
||||
|
||||
func readConfigFile(v *viper.Viper, path string, merge bool) (bool, error) {
|
||||
if path == "" {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
v.SetConfigFile(path)
|
||||
var err error
|
||||
if merge {
|
||||
err = v.MergeInConfig()
|
||||
} else {
|
||||
err = v.ReadInConfig()
|
||||
}
|
||||
|
||||
switch {
|
||||
case err == nil:
|
||||
return true, nil
|
||||
case isNotFound(err):
|
||||
return false, nil
|
||||
default:
|
||||
if _, statErr := os.Stat(path); statErr == nil { //nolint:gosec // path is vetted by bounded upward search or config options
|
||||
return false, fmt.Errorf("infra/config: read %s: %w", path, err)
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// NewSource loads configuration files. By default, it loads manifest/config/config.default.yaml
|
||||
// and merges manifest/config/config.yaml (or CONFIG_PATH) on top if present. A missing file is
|
||||
// not an error: the source then serves environment values only.
|
||||
func NewSource(opts ...Option) (*Source, error) {
|
||||
s := &Source{}
|
||||
for _, opt := range opts {
|
||||
opt(s)
|
||||
}
|
||||
|
||||
if s.path == "" {
|
||||
s.path = os.Getenv("CONFIG_PATH")
|
||||
}
|
||||
if s.path == "" {
|
||||
s.path = findConfigPath(DefaultCandidates...)
|
||||
}
|
||||
s.resolvePaths()
|
||||
|
||||
v := viper.New()
|
||||
v.SetConfigFile(s.path)
|
||||
|
||||
err := v.ReadInConfig()
|
||||
switch {
|
||||
case err == nil:
|
||||
s.found = true
|
||||
case isNotFound(err):
|
||||
// No file: fall through to environment-only lookups.
|
||||
default:
|
||||
if _, statErr := os.Stat(s.path); statErr == nil { //nolint:gosec // s.path comes from CONFIG_PATH or a bounded upward search
|
||||
return nil, fmt.Errorf("infra/config: read %s: %w", s.path, err)
|
||||
}
|
||||
var err error
|
||||
s.defaultFound, err = readConfigFile(v, s.defaultPath, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.overrideFound, err = readConfigFile(v, s.path, s.defaultFound)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.found = s.defaultFound || s.overrideFound
|
||||
s.v = v
|
||||
return s, nil
|
||||
}
|
||||
@@ -111,31 +157,25 @@ func (s *Source) Describe() string {
|
||||
if !s.found {
|
||||
return EnvOnlyOrigin
|
||||
}
|
||||
return s.path
|
||||
if s.overrideFound {
|
||||
return s.path
|
||||
}
|
||||
return s.defaultPath
|
||||
}
|
||||
|
||||
// findConfigPath searches upward from the working directory through candidate paths so
|
||||
// tests and binaries run from subdirectories still find configuration files in manifest/config/
|
||||
// or the repository root.
|
||||
func findConfigPath(candidates ...string) string {
|
||||
if len(candidates) == 0 {
|
||||
return DefaultFileName
|
||||
}
|
||||
for _, candidate := range candidates {
|
||||
if _, err := os.Stat(candidate); err == nil {
|
||||
return candidate
|
||||
}
|
||||
// findExistingUpward searches upward from the working directory for a relative file path.
|
||||
func findExistingUpward(relativeFilePath string) (string, bool) {
|
||||
if _, err := os.Stat(relativeFilePath); err == nil {
|
||||
return relativeFilePath, true
|
||||
}
|
||||
|
||||
dir := "."
|
||||
for range maxSearchDepth {
|
||||
dir += "/.."
|
||||
for _, candidate := range candidates {
|
||||
path := dir + "/" + candidate
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return path
|
||||
}
|
||||
path := dir + "/" + relativeFilePath
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return path, true
|
||||
}
|
||||
}
|
||||
return candidates[0]
|
||||
return "", false
|
||||
}
|
||||
|
||||
@@ -120,3 +120,88 @@ func TestSourceFindsManifestConfigInParentDirectory(t *testing.T) {
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, ":9200", value)
|
||||
}
|
||||
|
||||
func TestSourceDefaultConfigMergedWithOverride(t *testing.T) {
|
||||
tempRoot := t.TempDir()
|
||||
manifestConfigDir := filepath.Join(tempRoot, "manifest", "config")
|
||||
require.NoError(t, os.MkdirAll(manifestConfigDir, 0o755))
|
||||
|
||||
// 1. Write default configuration
|
||||
require.NoError(t, os.WriteFile(
|
||||
filepath.Join(manifestConfigDir, "config.default.yaml"),
|
||||
[]byte("app:\n addr: \":8000\"\n node_id: 1\n env: \"development\"\n"),
|
||||
0o600,
|
||||
))
|
||||
|
||||
// 2. Write override configuration (only overrides addr)
|
||||
require.NoError(t, os.WriteFile(
|
||||
filepath.Join(manifestConfigDir, "config.yaml"),
|
||||
[]byte("app:\n addr: \":9500\"\n"),
|
||||
0o600,
|
||||
))
|
||||
|
||||
subDir := filepath.Join(tempRoot, "backend")
|
||||
require.NoError(t, os.MkdirAll(subDir, 0o755))
|
||||
|
||||
origWd, err := os.Getwd()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, os.Chdir(subDir))
|
||||
t.Cleanup(func() {
|
||||
_ = os.Chdir(origWd)
|
||||
})
|
||||
|
||||
t.Setenv("CONFIG_PATH", "")
|
||||
|
||||
src, err := config.NewSource()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Overridden by config.yaml
|
||||
addr, ok := src.Lookup("app.addr")
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, ":9500", addr)
|
||||
|
||||
// Retained from config.default.yaml
|
||||
nodeID, ok := src.Lookup("app.node_id")
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, 1, nodeID)
|
||||
|
||||
envVal, ok := src.Lookup("app.env")
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "development", envVal)
|
||||
}
|
||||
|
||||
func TestSourceDefaultConfigUsedWhenOverrideAbsent(t *testing.T) {
|
||||
tempRoot := t.TempDir()
|
||||
manifestConfigDir := filepath.Join(tempRoot, "manifest", "config")
|
||||
require.NoError(t, os.MkdirAll(manifestConfigDir, 0o755))
|
||||
|
||||
// Write only default configuration
|
||||
require.NoError(t, os.WriteFile(
|
||||
filepath.Join(manifestConfigDir, "config.default.yaml"),
|
||||
[]byte("app:\n addr: \":8080\"\n env: \"testing\"\n"),
|
||||
0o600,
|
||||
))
|
||||
|
||||
subDir := filepath.Join(tempRoot, "backend")
|
||||
require.NoError(t, os.MkdirAll(subDir, 0o755))
|
||||
|
||||
origWd, err := os.Getwd()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, os.Chdir(subDir))
|
||||
t.Cleanup(func() {
|
||||
_ = os.Chdir(origWd)
|
||||
})
|
||||
|
||||
t.Setenv("CONFIG_PATH", "")
|
||||
|
||||
src, err := config.NewSource()
|
||||
require.NoError(t, err)
|
||||
|
||||
addr, ok := src.Lookup("app.addr")
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, ":8080", addr)
|
||||
|
||||
envVal, ok := src.Lookup("app.env")
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "testing", envVal)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user