mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-29 22:06:38 +08:00
182 lines
4.8 KiB
Go
182 lines
4.8 KiB
Go
// Copyright 2026 Arctel.net
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
// Package config adapts viper to the kernel configuration source contract. It is a
|
|
// runtime adapter rather than a core.Plugin: it owns no routes, services or tasks and
|
|
// therefore never appears in app.Use. Keeping viper here preserves the micro-kernel
|
|
// rule against importing concrete runtime dependencies.
|
|
package config
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
|
|
"github.com/spf13/viper"
|
|
)
|
|
|
|
// DefaultBaseFileName is the default base configuration file path looked up relative to the workspace.
|
|
const DefaultBaseFileName = "manifest/config/config.default.yaml"
|
|
|
|
// 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>"
|
|
|
|
// maxSearchDepth bounds the upward directory walk so a misconfigured working directory
|
|
// cannot make the loader scan the whole filesystem.
|
|
const maxSearchDepth = 5
|
|
|
|
// Option configures a Source.
|
|
type Option func(*Source)
|
|
|
|
// WithPath pins the configuration file, bypassing CONFIG_PATH and the upward search.
|
|
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
|
|
defaultPath string
|
|
pinned bool
|
|
defaultFound bool
|
|
overrideFound bool
|
|
found bool
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
s.resolvePaths()
|
|
|
|
v := viper.New()
|
|
|
|
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
|
|
}
|
|
|
|
// isNotFound reports whether the loader failed only because the file is absent.
|
|
func isNotFound(err error) bool {
|
|
var notFound viper.ConfigFileNotFoundError
|
|
return errors.As(err, ¬Found) || errors.Is(err, fs.ErrNotExist)
|
|
}
|
|
|
|
// Lookup returns the raw value stored at a dotted path, or false when the file was not
|
|
// loaded or the path is absent. Declared defaults therefore stay distinguishable from
|
|
// values explicitly set to a zero.
|
|
func (s *Source) Lookup(path string) (any, bool) {
|
|
if !s.found || !s.v.IsSet(path) {
|
|
return nil, false
|
|
}
|
|
return s.v.Get(path), true
|
|
}
|
|
|
|
// LookupEnv reads a process environment variable.
|
|
func (s *Source) LookupEnv(name string) (string, bool) {
|
|
return os.LookupEnv(name)
|
|
}
|
|
|
|
// Describe returns the loaded file path, or EnvOnlyOrigin when running on environment values.
|
|
func (s *Source) Describe() string {
|
|
if !s.found {
|
|
return EnvOnlyOrigin
|
|
}
|
|
if s.overrideFound {
|
|
return s.path
|
|
}
|
|
return s.defaultPath
|
|
}
|
|
|
|
// 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 += "/.."
|
|
path := dir + "/" + relativeFilePath
|
|
if _, err := os.Stat(path); err == nil {
|
|
return path, true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|