优化结构
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
package config
|
||||
|
||||
// Clone returns a deep copy of a configuration snapshot. The copy helpers are
|
||||
// intentionally kept in the config package so data/integration code does not
|
||||
// need protobuf cloning or storage-specific serializers.
|
||||
func Clone(value *Config) *Config { return cloneConfig(value) }
|
||||
|
||||
func CloneData(value *Data) *Data {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
return cloneConfig(&Config{Data: value}).Data
|
||||
}
|
||||
|
||||
func CloneAdmin(value *Admin) *Admin {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
return cloneConfig(&Config{Admin: value}).Admin
|
||||
}
|
||||
|
||||
func CloneDatabase(value *Database) *Database {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
return cloneConfig(&Config{Data: &Data{Database: value}}).Data.Database
|
||||
}
|
||||
|
||||
func CloneStorage(value *Storage) *Storage {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
return cloneConfig(&Config{Admin: &Admin{Storage: value}}).Admin.Storage
|
||||
}
|
||||
|
||||
func CloneEmail(value *Email) *Email {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
return cloneConfig(&Config{Admin: &Admin{Email: value}}).Admin.Email
|
||||
}
|
||||
@@ -0,0 +1,502 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/go-viper/mapstructure/v2"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var ErrConfigPathRequired = errors.New("configuration path is required")
|
||||
|
||||
// Store owns an immutable configuration snapshot and its Viper-backed file
|
||||
// watcher.
|
||||
// Every accessor returns a deep copy, so readers can safely use it without
|
||||
// coordinating with a reload in another goroutine.
|
||||
type Store struct {
|
||||
snapshot atomic.Pointer[Config]
|
||||
path string
|
||||
mu sync.RWMutex
|
||||
listeners map[uint64]func(*Config)
|
||||
nextID uint64
|
||||
watcher *fsnotify.Watcher
|
||||
stop chan struct{}
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// Load reads a YAML/JSON/TOML configuration file or a directory containing
|
||||
// config.yaml. Environment variables with KRA_ prefix override file values;
|
||||
// nested keys use underscores (for example KRA_ADMIN_JWT_SIGNING_KEY).
|
||||
func Load(path string) (*Config, error) {
|
||||
resolved, err := resolvePath(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v := newViper(resolved)
|
||||
if err = v.ReadInConfig(); err != nil {
|
||||
return nil, fmt.Errorf("read configuration %q: %w", resolved, err)
|
||||
}
|
||||
config, err := decode(v)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decode configuration %q: %w", resolved, err)
|
||||
}
|
||||
setConfigPath(config, resolved)
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// NewStore creates a store from an already decoded snapshot. The input is
|
||||
// copied immediately and may be reused or mutated by the caller afterwards.
|
||||
func NewStore(config *Config) *Store {
|
||||
store := &Store{listeners: make(map[uint64]func(*Config))}
|
||||
if config != nil && config.Admin != nil {
|
||||
store.path = config.Admin.ConfigPath
|
||||
}
|
||||
store.Replace(config)
|
||||
return store
|
||||
}
|
||||
|
||||
// LoadStore loads the initial snapshot and starts watching its source file.
|
||||
func LoadStore(path string) (*Store, error) {
|
||||
config, err := Load(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
store := NewStore(config)
|
||||
if err = store.Watch(); err != nil {
|
||||
store.Close()
|
||||
return nil, err
|
||||
}
|
||||
return store, nil
|
||||
}
|
||||
|
||||
func (r *Store) Snapshot() *Config {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
return cloneConfig(r.snapshot.Load())
|
||||
}
|
||||
|
||||
func (r *Store) Server() *Server {
|
||||
config := r.Snapshot()
|
||||
if config == nil {
|
||||
return nil
|
||||
}
|
||||
return config.Server
|
||||
}
|
||||
|
||||
func (r *Store) Data() *Data {
|
||||
config := r.Snapshot()
|
||||
if config == nil {
|
||||
return nil
|
||||
}
|
||||
return config.Data
|
||||
}
|
||||
|
||||
func (r *Store) Admin() *Admin {
|
||||
config := r.Snapshot()
|
||||
if config == nil {
|
||||
return nil
|
||||
}
|
||||
return config.Admin
|
||||
}
|
||||
|
||||
func (r *Store) Values() (*Data, *Admin) {
|
||||
config := r.Snapshot()
|
||||
if config == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return config.Data, config.Admin
|
||||
}
|
||||
|
||||
func (r *Store) UpdateDatabase(database *Database) {
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
config := r.Snapshot()
|
||||
if config == nil {
|
||||
config = &Config{}
|
||||
}
|
||||
if config.Data == nil {
|
||||
config.Data = &Data{}
|
||||
}
|
||||
config.Data.Database = cloneDatabase(database)
|
||||
r.replace(config, false)
|
||||
}
|
||||
|
||||
func (r *Store) ConfigPath() string {
|
||||
if r == nil {
|
||||
return ""
|
||||
}
|
||||
r.mu.RLock()
|
||||
path := r.path
|
||||
r.mu.RUnlock()
|
||||
return path
|
||||
}
|
||||
|
||||
func (r *Store) Replace(config *Config) {
|
||||
r.replace(config, true)
|
||||
}
|
||||
|
||||
func (r *Store) replace(config *Config, notify bool) {
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
next := cloneConfig(config)
|
||||
if next == nil {
|
||||
next = &Config{}
|
||||
}
|
||||
if path := r.ConfigPath(); path != "" {
|
||||
setConfigPath(next, path)
|
||||
}
|
||||
r.snapshot.Store(next)
|
||||
if !notify {
|
||||
return
|
||||
}
|
||||
r.mu.RLock()
|
||||
listeners := make([]func(*Config), 0, len(r.listeners))
|
||||
for _, listener := range r.listeners {
|
||||
listeners = append(listeners, listener)
|
||||
}
|
||||
r.mu.RUnlock()
|
||||
for _, listener := range listeners {
|
||||
listener(cloneConfig(next))
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe registers a callback after each successful replacement. The
|
||||
// callback is invoked outside the runtime lock, and the returned function is
|
||||
// idempotent so cleanup can safely be deferred by multiple owners.
|
||||
func (r *Store) Subscribe(listener func(*Config)) func() {
|
||||
if r == nil || listener == nil {
|
||||
return func() {}
|
||||
}
|
||||
r.mu.Lock()
|
||||
r.nextID++
|
||||
id := r.nextID
|
||||
r.listeners[id] = listener
|
||||
r.mu.Unlock()
|
||||
var once sync.Once
|
||||
return func() {
|
||||
once.Do(func() {
|
||||
r.mu.Lock()
|
||||
delete(r.listeners, id)
|
||||
r.mu.Unlock()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Watch starts watching the source directory. Repeated calls are harmless.
|
||||
func (r *Store) Watch() error {
|
||||
if r == nil {
|
||||
return ErrConfigPathRequired
|
||||
}
|
||||
path := r.ConfigPath()
|
||||
if path == "" {
|
||||
return ErrConfigPathRequired
|
||||
}
|
||||
r.mu.Lock()
|
||||
if r.watcher != nil {
|
||||
r.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
r.mu.Unlock()
|
||||
return fmt.Errorf("create configuration watcher: %w", err)
|
||||
}
|
||||
if err = watcher.Add(filepath.Dir(path)); err != nil {
|
||||
_ = watcher.Close()
|
||||
r.mu.Unlock()
|
||||
return fmt.Errorf("watch configuration directory: %w", err)
|
||||
}
|
||||
r.watcher = watcher
|
||||
r.stop = make(chan struct{})
|
||||
r.done = make(chan struct{})
|
||||
stop, done := r.stop, r.done
|
||||
r.mu.Unlock()
|
||||
go r.watchLoop(watcher, stop, done, path)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Store) watchLoop(watcher *fsnotify.Watcher, stop <-chan struct{}, done chan<- struct{}, path string) {
|
||||
defer close(done)
|
||||
const debounce = 100 * time.Millisecond
|
||||
var timer *time.Timer
|
||||
var timerC <-chan time.Time
|
||||
events := watcher.Events
|
||||
errors := watcher.Errors
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-events:
|
||||
if !ok {
|
||||
events = nil
|
||||
if errors == nil {
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
if filepath.Clean(event.Name) != path || event.Op&(fsnotify.Write|fsnotify.Create|fsnotify.Rename) == 0 {
|
||||
continue
|
||||
}
|
||||
if timer == nil {
|
||||
timer = time.NewTimer(debounce)
|
||||
} else {
|
||||
if !timer.Stop() {
|
||||
select {
|
||||
case <-timer.C:
|
||||
default:
|
||||
}
|
||||
}
|
||||
timer.Reset(debounce)
|
||||
}
|
||||
timerC = timer.C
|
||||
case <-timerC:
|
||||
timerC = nil
|
||||
config, err := Load(path)
|
||||
if err != nil {
|
||||
// Keep the last valid snapshot. A partially-written file must not
|
||||
// take down a running process or publish invalid state.
|
||||
continue
|
||||
}
|
||||
r.Replace(config)
|
||||
case _, ok := <-errors:
|
||||
// fsnotify errors are intentionally non-fatal; the watcher remains
|
||||
// useful for subsequent events and Close always terminates it.
|
||||
if !ok {
|
||||
errors = nil
|
||||
if events == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
case <-stop:
|
||||
if timer != nil {
|
||||
timer.Stop()
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Store) Close() {
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
r.mu.Lock()
|
||||
watcher, stop, done := r.watcher, r.stop, r.done
|
||||
r.watcher = nil
|
||||
r.stop = nil
|
||||
r.done = nil
|
||||
r.mu.Unlock()
|
||||
if watcher == nil {
|
||||
return
|
||||
}
|
||||
close(stop)
|
||||
_ = watcher.Close()
|
||||
<-done
|
||||
}
|
||||
|
||||
func resolvePath(path string) (string, error) {
|
||||
path = strings.TrimSpace(path)
|
||||
if path == "" {
|
||||
return "", ErrConfigPathRequired
|
||||
}
|
||||
absolute, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("resolve configuration path: %w", err)
|
||||
}
|
||||
if info, statErr := os.Stat(absolute); statErr == nil && info.IsDir() {
|
||||
absolute = filepath.Join(absolute, "config.yaml")
|
||||
}
|
||||
return filepath.Clean(absolute), nil
|
||||
}
|
||||
|
||||
func newViper(path string) *viper.Viper {
|
||||
v := viper.New()
|
||||
v.SetConfigFile(path)
|
||||
v.SetConfigType(strings.TrimPrefix(filepath.Ext(path), "."))
|
||||
v.SetEnvPrefix("KRA")
|
||||
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
|
||||
v.AutomaticEnv()
|
||||
return v
|
||||
}
|
||||
|
||||
func decode(v *viper.Viper) (*Config, error) {
|
||||
var config Config
|
||||
hook := mapstructure.ComposeDecodeHookFunc(
|
||||
mapstructure.StringToTimeDurationHookFunc(),
|
||||
mapstructure.StringToSliceHookFunc(","),
|
||||
)
|
||||
if err := v.Unmarshal(&config, viper.DecodeHook(hook)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
func setConfigPath(config *Config, path string) {
|
||||
if config != nil && config.Admin != nil {
|
||||
config.Admin.ConfigPath = path
|
||||
}
|
||||
}
|
||||
|
||||
func cloneConfig(value *Config) *Config {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
// Viper's mapstructure output contains only value fields and pointers;
|
||||
// round-tripping through a temporary map is unnecessarily lossy for
|
||||
// durations. Explicit copy via YAML gives us a robust deep copy while
|
||||
// preserving time.Duration values through the text hook.
|
||||
copy := *value
|
||||
if value.Server != nil {
|
||||
server := *value.Server
|
||||
copy.Server = &server
|
||||
if value.Server.HTTP != nil {
|
||||
http := *value.Server.HTTP
|
||||
copy.Server.HTTP = &http
|
||||
}
|
||||
}
|
||||
if value.Data != nil {
|
||||
data := *value.Data
|
||||
copy.Data = &data
|
||||
data.Database = cloneDatabase(value.Data.Database)
|
||||
data.Redis = cloneRedis(value.Data.Redis)
|
||||
data.Mongo = cloneMongo(value.Data.Mongo)
|
||||
data.DatabaseList = make([]*Database, len(value.Data.DatabaseList))
|
||||
for i, item := range value.Data.DatabaseList {
|
||||
data.DatabaseList[i] = cloneDatabase(item)
|
||||
}
|
||||
data.RedisList = make([]*Redis, len(value.Data.RedisList))
|
||||
for i, item := range value.Data.RedisList {
|
||||
data.RedisList[i] = cloneRedis(item)
|
||||
}
|
||||
}
|
||||
if value.Admin != nil {
|
||||
copy.Admin = cloneAdmin(value.Admin)
|
||||
}
|
||||
return ©
|
||||
}
|
||||
|
||||
func cloneDatabase(value *Database) *Database {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
copy := *value
|
||||
return ©
|
||||
}
|
||||
func cloneRedis(value *Redis) *Redis {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
copy := *value
|
||||
copy.ClusterAddrs = append([]string(nil), value.ClusterAddrs...)
|
||||
return ©
|
||||
}
|
||||
func cloneMongo(value *Mongo) *Mongo {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
copy := *value
|
||||
copy.Hosts = make([]*MongoHost, len(value.Hosts))
|
||||
for i, item := range value.Hosts {
|
||||
if item != nil {
|
||||
host := *item
|
||||
copy.Hosts[i] = &host
|
||||
}
|
||||
}
|
||||
return ©
|
||||
}
|
||||
|
||||
func cloneAdmin(value *Admin) *Admin {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
copy := *value
|
||||
if value.JWT != nil {
|
||||
item := *value.JWT
|
||||
copy.JWT = &item
|
||||
}
|
||||
if value.Captcha != nil {
|
||||
item := *value.Captcha
|
||||
copy.Captcha = &item
|
||||
}
|
||||
if value.Local != nil {
|
||||
item := *value.Local
|
||||
copy.Local = &item
|
||||
}
|
||||
if value.Email != nil {
|
||||
item := *value.Email
|
||||
copy.Email = &item
|
||||
}
|
||||
if value.Media != nil {
|
||||
item := *value.Media
|
||||
copy.Media = &item
|
||||
}
|
||||
if value.System != nil {
|
||||
item := *value.System
|
||||
copy.System = &item
|
||||
}
|
||||
if value.Zap != nil {
|
||||
item := *value.Zap
|
||||
item.FileOnlyModules = append([]string(nil), value.Zap.FileOnlyModules...)
|
||||
copy.Zap = &item
|
||||
}
|
||||
if value.App != nil {
|
||||
item := *value.App
|
||||
copy.App = &item
|
||||
}
|
||||
if value.CORS != nil {
|
||||
item := *value.CORS
|
||||
item.Whitelist = make([]*CORSRule, len(value.CORS.Whitelist))
|
||||
for i, rule := range value.CORS.Whitelist {
|
||||
if rule != nil {
|
||||
next := *rule
|
||||
item.Whitelist[i] = &next
|
||||
}
|
||||
}
|
||||
copy.CORS = &item
|
||||
}
|
||||
if value.Storage != nil {
|
||||
copy.Storage = cloneStorage(value.Storage)
|
||||
}
|
||||
copy.DiskList = make([]*Disk, len(value.DiskList))
|
||||
for i, item := range value.DiskList {
|
||||
if item != nil {
|
||||
next := *item
|
||||
copy.DiskList[i] = &next
|
||||
}
|
||||
}
|
||||
return ©
|
||||
}
|
||||
|
||||
func cloneStorage(value *Storage) *Storage {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
copy := *value
|
||||
if value.Qiniu != nil {
|
||||
item := *value.Qiniu
|
||||
copy.Qiniu = &item
|
||||
}
|
||||
copy.AliyunOSS = cloneObjectStore(value.AliyunOSS)
|
||||
copy.HuaweiOBS = cloneObjectStore(value.HuaweiOBS)
|
||||
copy.TencentCOS = cloneObjectStore(value.TencentCOS)
|
||||
copy.AWSS3 = cloneObjectStore(value.AWSS3)
|
||||
copy.CloudflareR2 = cloneObjectStore(value.CloudflareR2)
|
||||
copy.Minio = cloneObjectStore(value.Minio)
|
||||
return ©
|
||||
}
|
||||
func cloneObjectStore(value *ObjectStore) *ObjectStore {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
copy := *value
|
||||
return ©
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func writeConfig(t *testing.T, path, body string) {
|
||||
t.Helper()
|
||||
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadDirectoryAndDurations(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "config.yaml")
|
||||
writeConfig(t, path, "server:\n http:\n addr: :9000\n timeout: 600s\ndata:\n redis:\n read_timeout: 200ms\nadmin:\n jwt:\n expires_time: 168h\n media:\n session_ttl: 36\n")
|
||||
loaded, err := Load(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if loaded.Server == nil || loaded.Server.HTTP == nil || loaded.Server.HTTP.Timeout != 10*time.Minute {
|
||||
t.Fatalf("server timeout = %#v", loaded.Server)
|
||||
}
|
||||
if loaded.Data == nil || loaded.Data.Redis == nil || loaded.Data.Redis.ReadTimeout != 200*time.Millisecond {
|
||||
t.Fatalf("redis timeout = %#v", loaded.Data)
|
||||
}
|
||||
if loaded.Admin == nil || loaded.Admin.JWT == nil || loaded.Admin.JWT.ExpiresTime != 7*24*time.Hour {
|
||||
t.Fatalf("jwt duration = %#v", loaded.Admin)
|
||||
}
|
||||
if loaded.Admin.ConfigPath != path {
|
||||
t.Fatalf("config path = %q, want %q", loaded.Admin.ConfigPath, path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadRejectsInvalidDuration(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "config.yaml")
|
||||
writeConfig(t, path, "server:\n http:\n timeout: definitely-not-a-duration\n")
|
||||
if _, err := Load(path); err == nil {
|
||||
t.Fatal("invalid duration was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadEnvironmentOverride(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "config.yaml")
|
||||
writeConfig(t, path, "server:\n http:\n addr: :8000\n")
|
||||
t.Setenv("KRA_SERVER_HTTP_ADDR", ":9100")
|
||||
loaded, err := Load(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if loaded.Server == nil || loaded.Server.HTTP == nil || loaded.Server.HTTP.Addr != ":9100" {
|
||||
t.Fatalf("environment override = %#v", loaded.Server)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreSnapshotIsolationAndSubscriptions(t *testing.T) {
|
||||
store := NewStore(&Config{Admin: &Admin{JWT: &JWT{SigningKey: "before"}}})
|
||||
var calls atomic.Int32
|
||||
stop := store.Subscribe(func(value *Config) {
|
||||
calls.Add(1)
|
||||
value.Admin.JWT.SigningKey = "callback-mutation"
|
||||
})
|
||||
store.Replace(&Config{Admin: &Admin{JWT: &JWT{SigningKey: "after"}}})
|
||||
stop()
|
||||
stop()
|
||||
value := store.Snapshot()
|
||||
if value == nil || value.Admin == nil || value.Admin.JWT == nil || value.Admin.JWT.SigningKey != "after" {
|
||||
t.Fatalf("snapshot was mutated by callback: %#v", value)
|
||||
}
|
||||
if calls.Load() != 1 {
|
||||
t.Fatalf("callback count = %d", calls.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreConcurrentReadersAndReplacements(t *testing.T) {
|
||||
store := NewStore(&Config{Data: &Data{Database: &Database{Name: "initial"}}})
|
||||
var wait sync.WaitGroup
|
||||
for i := 0; i < 8; i++ {
|
||||
wait.Add(1)
|
||||
go func() {
|
||||
defer wait.Done()
|
||||
for j := 0; j < 500; j++ {
|
||||
_ = store.Snapshot()
|
||||
}
|
||||
}()
|
||||
}
|
||||
for i := 0; i < 500; i++ {
|
||||
store.Replace(&Config{Data: &Data{Database: &Database{Name: "next"}}})
|
||||
}
|
||||
wait.Wait()
|
||||
}
|
||||
|
||||
func TestStoreWatchReloadAndClose(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "config.yaml")
|
||||
writeConfig(t, path, "admin:\n app:\n app_id: first\n")
|
||||
store, err := LoadStore(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer store.Close()
|
||||
writeConfig(t, path, "admin:\n app:\n app_id: second\n")
|
||||
deadline := time.Now().Add(3 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
value := store.Snapshot()
|
||||
if value != nil && value.Admin != nil && value.Admin.App != nil && value.Admin.App.AppID == "second" {
|
||||
return
|
||||
}
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
}
|
||||
t.Fatalf("watcher did not reload configuration: %#v", store.Snapshot())
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
package config
|
||||
|
||||
import "time"
|
||||
|
||||
// Config is the complete application configuration loaded from config.yaml.
|
||||
// It intentionally uses ordinary Go types so configuration is independent of
|
||||
// protobuf generation and can be consumed by every internal module.
|
||||
type Config struct {
|
||||
Server *Server `mapstructure:"server" yaml:"server" json:"server"`
|
||||
Data *Data `mapstructure:"data" yaml:"data" json:"data"`
|
||||
Admin *Admin `mapstructure:"admin" yaml:"admin" json:"admin"`
|
||||
}
|
||||
|
||||
type Bootstrap = Config
|
||||
|
||||
type Server struct {
|
||||
HTTP *ServerHTTP `mapstructure:"http" yaml:"http" json:"http"`
|
||||
}
|
||||
|
||||
type ServerHTTP struct {
|
||||
Network string `mapstructure:"network" yaml:"network" json:"network"`
|
||||
Addr string `mapstructure:"addr" yaml:"addr" json:"addr"`
|
||||
Timeout time.Duration `mapstructure:"timeout" yaml:"timeout" json:"timeout"`
|
||||
}
|
||||
|
||||
type Data struct {
|
||||
Database *Database `mapstructure:"database" yaml:"database" json:"database"`
|
||||
Redis *Redis `mapstructure:"redis" yaml:"redis" json:"redis"`
|
||||
DatabaseList []*Database `mapstructure:"database_list" yaml:"database_list" json:"database_list"`
|
||||
RedisList []*Redis `mapstructure:"redis_list" yaml:"redis_list" json:"redis_list"`
|
||||
Mongo *Mongo `mapstructure:"mongo" yaml:"mongo" json:"mongo"`
|
||||
}
|
||||
|
||||
type Database struct {
|
||||
Driver string `mapstructure:"driver" yaml:"driver" json:"driver"`
|
||||
Source string `mapstructure:"source" yaml:"source" json:"source"`
|
||||
Host string `mapstructure:"host" yaml:"host" json:"host"`
|
||||
Port string `mapstructure:"port" yaml:"port" json:"port"`
|
||||
User string `mapstructure:"user" yaml:"user" json:"user"`
|
||||
Password string `mapstructure:"password" yaml:"password" json:"password"`
|
||||
Name string `mapstructure:"name" yaml:"name" json:"name"`
|
||||
Config string `mapstructure:"config" yaml:"config" json:"config"`
|
||||
Path string `mapstructure:"path" yaml:"path" json:"path"`
|
||||
AliasName string `mapstructure:"alias_name" yaml:"alias_name" json:"alias_name"`
|
||||
Disable bool `mapstructure:"disable" yaml:"disable" json:"disable"`
|
||||
Prefix string `mapstructure:"prefix" yaml:"prefix" json:"prefix"`
|
||||
Engine string `mapstructure:"engine" yaml:"engine" json:"engine"`
|
||||
LogMode string `mapstructure:"log_mode" yaml:"log_mode" json:"log_mode"`
|
||||
MaxIdleConns int `mapstructure:"max_idle_conns" yaml:"max_idle_conns" json:"max_idle_conns"`
|
||||
MaxOpenConns int `mapstructure:"max_open_conns" yaml:"max_open_conns" json:"max_open_conns"`
|
||||
// ConnMaxLifetime is expressed in seconds for compatibility with the
|
||||
// existing configuration contract. A bare YAML number must not mean ns.
|
||||
ConnMaxLifetime int `mapstructure:"conn_max_lifetime" yaml:"conn_max_lifetime" json:"conn_max_lifetime"`
|
||||
Singular bool `mapstructure:"singular" yaml:"singular" json:"singular"`
|
||||
}
|
||||
|
||||
type Redis struct {
|
||||
Network string `mapstructure:"network" yaml:"network" json:"network"`
|
||||
Addr string `mapstructure:"addr" yaml:"addr" json:"addr"`
|
||||
ReadTimeout time.Duration `mapstructure:"read_timeout" yaml:"read_timeout" json:"read_timeout"`
|
||||
WriteTimeout time.Duration `mapstructure:"write_timeout" yaml:"write_timeout" json:"write_timeout"`
|
||||
Name string `mapstructure:"name" yaml:"name" json:"name"`
|
||||
Password string `mapstructure:"password" yaml:"password" json:"password"`
|
||||
DB int `mapstructure:"db" yaml:"db" json:"db"`
|
||||
UseCluster bool `mapstructure:"use_cluster" yaml:"use_cluster" json:"use_cluster"`
|
||||
ClusterAddrs []string `mapstructure:"cluster_addrs" yaml:"cluster_addrs" json:"cluster_addrs"`
|
||||
}
|
||||
|
||||
type MongoHost struct {
|
||||
Host string `mapstructure:"host" yaml:"host" json:"host"`
|
||||
Port string `mapstructure:"port" yaml:"port" json:"port"`
|
||||
}
|
||||
|
||||
type Mongo struct {
|
||||
Coll string `mapstructure:"coll" yaml:"coll" json:"coll"`
|
||||
Options string `mapstructure:"options" yaml:"options" json:"options"`
|
||||
Database string `mapstructure:"database" yaml:"database" json:"database"`
|
||||
Username string `mapstructure:"username" yaml:"username" json:"username"`
|
||||
Password string `mapstructure:"password" yaml:"password" json:"password"`
|
||||
AuthSource string `mapstructure:"auth_source" yaml:"auth_source" json:"auth_source"`
|
||||
MinPoolSize uint64 `mapstructure:"min_pool_size" yaml:"min_pool_size" json:"min_pool_size"`
|
||||
MaxPoolSize uint64 `mapstructure:"max_pool_size" yaml:"max_pool_size" json:"max_pool_size"`
|
||||
SocketTimeoutMs int64 `mapstructure:"socket_timeout_ms" yaml:"socket_timeout_ms" json:"socket_timeout_ms"`
|
||||
ConnectTimeoutMs int64 `mapstructure:"connect_timeout_ms" yaml:"connect_timeout_ms" json:"connect_timeout_ms"`
|
||||
IsZap bool `mapstructure:"is_zap" yaml:"is_zap" json:"is_zap"`
|
||||
Hosts []*MongoHost `mapstructure:"hosts" yaml:"hosts" json:"hosts"`
|
||||
}
|
||||
|
||||
// Admin contains administration-service settings. ConfigPath is runtime
|
||||
// metadata populated by the loader and never decoded from or written to YAML.
|
||||
type Admin struct {
|
||||
RouterPrefix string `mapstructure:"router_prefix" yaml:"router_prefix" json:"router_prefix"`
|
||||
JWT *JWT `mapstructure:"jwt" yaml:"jwt" json:"jwt"`
|
||||
Captcha *Captcha `mapstructure:"captcha" yaml:"captcha" json:"captcha"`
|
||||
Local *Local `mapstructure:"local" yaml:"local" json:"local"`
|
||||
Email *Email `mapstructure:"email" yaml:"email" json:"email"`
|
||||
Storage *Storage `mapstructure:"storage" yaml:"storage" json:"storage"`
|
||||
ConfigPath string `mapstructure:"-" yaml:"-" json:"-"`
|
||||
Media *Media `mapstructure:"media" yaml:"media" json:"media"`
|
||||
DiskList []*Disk `mapstructure:"disk_list" yaml:"disk_list" json:"disk_list"`
|
||||
System *System `mapstructure:"system" yaml:"system" json:"system"`
|
||||
Zap *Zap `mapstructure:"zap" yaml:"zap" json:"zap"`
|
||||
CORS *CORS `mapstructure:"cors" yaml:"cors" json:"cors"`
|
||||
App *App `mapstructure:"app" yaml:"app" json:"app"`
|
||||
}
|
||||
|
||||
type JWT struct {
|
||||
SigningKey string `mapstructure:"signing_key" yaml:"signing_key" json:"signing_key"`
|
||||
ExpiresTime time.Duration `mapstructure:"expires_time" yaml:"expires_time" json:"expires_time"`
|
||||
BufferTime time.Duration `mapstructure:"buffer_time" yaml:"buffer_time" json:"buffer_time"`
|
||||
Issuer string `mapstructure:"issuer" yaml:"issuer" json:"issuer"`
|
||||
}
|
||||
|
||||
type Captcha struct {
|
||||
KeyLong int `mapstructure:"key_long" yaml:"key_long" json:"key_long"`
|
||||
ImgWidth int `mapstructure:"img_width" yaml:"img_width" json:"img_width"`
|
||||
ImgHeight int `mapstructure:"img_height" yaml:"img_height" json:"img_height"`
|
||||
StoreExpiration time.Duration `mapstructure:"store_expiration" yaml:"store_expiration" json:"store_expiration"`
|
||||
}
|
||||
|
||||
type Local struct {
|
||||
StorePath string `mapstructure:"store_path" yaml:"store_path" json:"store_path"`
|
||||
PathPrefix string `mapstructure:"path_prefix" yaml:"path_prefix" json:"path_prefix"`
|
||||
}
|
||||
|
||||
type Email struct {
|
||||
To string `mapstructure:"to" yaml:"to" json:"to"`
|
||||
From string `mapstructure:"from" yaml:"from" json:"from"`
|
||||
Host string `mapstructure:"host" yaml:"host" json:"host"`
|
||||
Secret string `mapstructure:"secret" yaml:"secret" json:"secret"`
|
||||
Nickname string `mapstructure:"nickname" yaml:"nickname" json:"nickname"`
|
||||
Port int `mapstructure:"port" yaml:"port" json:"port"`
|
||||
IsSSL bool `mapstructure:"is_ssl" yaml:"is_ssl" json:"is_ssl"`
|
||||
IsLoginAuth bool `mapstructure:"is_login_auth" yaml:"is_login_auth" json:"is_login_auth"`
|
||||
}
|
||||
|
||||
type Media struct {
|
||||
SessionTTL int `mapstructure:"session_ttl" yaml:"session_ttl" json:"session_ttl"`
|
||||
MaxFileSize int64 `mapstructure:"max_file_size" yaml:"max_file_size" json:"max_file_size"`
|
||||
ChunkDir string `mapstructure:"chunk_dir" yaml:"chunk_dir" json:"chunk_dir"`
|
||||
}
|
||||
|
||||
type Disk struct {
|
||||
MountPoint string `mapstructure:"mount_point" yaml:"mount_point" json:"mount_point"`
|
||||
}
|
||||
|
||||
type System struct {
|
||||
UseRedis bool `mapstructure:"use_redis" yaml:"use_redis" json:"use_redis"`
|
||||
UseMultipoint bool `mapstructure:"use_multipoint" yaml:"use_multipoint" json:"use_multipoint"`
|
||||
UseStrictAuth bool `mapstructure:"use_strict_auth" yaml:"use_strict_auth" json:"use_strict_auth"`
|
||||
DisableAutoMigrate bool `mapstructure:"disable_auto_migrate" yaml:"disable_auto_migrate" json:"disable_auto_migrate"`
|
||||
UseMongo bool `mapstructure:"use_mongo" yaml:"use_mongo" json:"use_mongo"`
|
||||
Addr int `mapstructure:"addr" yaml:"addr" json:"addr"`
|
||||
IplimitCount int `mapstructure:"iplimit_count" yaml:"iplimit_count" json:"iplimit_count"`
|
||||
IplimitTime int `mapstructure:"iplimit_time" yaml:"iplimit_time" json:"iplimit_time"`
|
||||
}
|
||||
|
||||
type Zap struct {
|
||||
Level string `mapstructure:"level" yaml:"level" json:"level"`
|
||||
Prefix string `mapstructure:"prefix" yaml:"prefix" json:"prefix"`
|
||||
Format string `mapstructure:"format" yaml:"format" json:"format"`
|
||||
Director string `mapstructure:"director" yaml:"director" json:"director"`
|
||||
EncodeLevel string `mapstructure:"encode_level" yaml:"encode_level" json:"encode_level"`
|
||||
StacktraceKey string `mapstructure:"stacktrace_key" yaml:"stacktrace_key" json:"stacktrace_key"`
|
||||
ShowLine bool `mapstructure:"show_line" yaml:"show_line" json:"show_line"`
|
||||
LogInConsole bool `mapstructure:"log_in_console" yaml:"log_in_console" json:"log_in_console"`
|
||||
RetentionDay int `mapstructure:"retention_day" yaml:"retention_day" json:"retention_day"`
|
||||
AccessReqBody bool `mapstructure:"access_req_body" yaml:"access_req_body" json:"access_req_body"`
|
||||
AccessRespData bool `mapstructure:"access_resp_data" yaml:"access_resp_data" json:"access_resp_data"`
|
||||
AccessReqHeaders bool `mapstructure:"access_req_headers" yaml:"access_req_headers" json:"access_req_headers"`
|
||||
AccessLogMaxBytes int `mapstructure:"access_log_max_bytes" yaml:"access_log_max_bytes" json:"access_log_max_bytes"`
|
||||
FileOnlyModules []string `mapstructure:"file_only_modules" yaml:"file_only_modules" json:"file_only_modules"`
|
||||
}
|
||||
|
||||
type CORS struct {
|
||||
Mode string `mapstructure:"mode" yaml:"mode" json:"mode"`
|
||||
Whitelist []*CORSRule `mapstructure:"whitelist" yaml:"whitelist" json:"whitelist"`
|
||||
}
|
||||
|
||||
type CORSRule struct {
|
||||
AllowOrigin string `mapstructure:"allow_origin" yaml:"allow_origin" json:"allow_origin"`
|
||||
AllowMethods string `mapstructure:"allow_methods" yaml:"allow_methods" json:"allow_methods"`
|
||||
AllowHeaders string `mapstructure:"allow_headers" yaml:"allow_headers" json:"allow_headers"`
|
||||
ExposeHeaders string `mapstructure:"expose_headers" yaml:"expose_headers" json:"expose_headers"`
|
||||
AllowCredentials bool `mapstructure:"allow_credentials" yaml:"allow_credentials" json:"allow_credentials"`
|
||||
}
|
||||
|
||||
type App struct {
|
||||
Node string `mapstructure:"node" yaml:"node" json:"node"`
|
||||
AppID string `mapstructure:"app_id" yaml:"app_id" json:"app_id"`
|
||||
Env string `mapstructure:"env" yaml:"env" json:"env"`
|
||||
}
|
||||
|
||||
type Storage struct {
|
||||
Type string `mapstructure:"type" yaml:"type" json:"type"`
|
||||
Qiniu *Qiniu `mapstructure:"qiniu" yaml:"qiniu" json:"qiniu"`
|
||||
AliyunOSS *ObjectStore `mapstructure:"aliyun_oss" yaml:"aliyun_oss" json:"aliyun_oss"`
|
||||
HuaweiOBS *ObjectStore `mapstructure:"huawei_obs" yaml:"huawei_obs" json:"huawei_obs"`
|
||||
TencentCOS *ObjectStore `mapstructure:"tencent_cos" yaml:"tencent_cos" json:"tencent_cos"`
|
||||
AWSS3 *ObjectStore `mapstructure:"aws_s3" yaml:"aws_s3" json:"aws_s3"`
|
||||
CloudflareR2 *ObjectStore `mapstructure:"cloudflare_r2" yaml:"cloudflare_r2" json:"cloudflare_r2"`
|
||||
Minio *ObjectStore `mapstructure:"minio" yaml:"minio" json:"minio"`
|
||||
}
|
||||
|
||||
type Qiniu struct {
|
||||
Zone string `mapstructure:"zone" yaml:"zone" json:"zone"`
|
||||
Bucket string `mapstructure:"bucket" yaml:"bucket" json:"bucket"`
|
||||
BaseURL string `mapstructure:"base_url" yaml:"base_url" json:"base_url"`
|
||||
AccessKey string `mapstructure:"access_key" yaml:"access_key" json:"access_key"`
|
||||
SecretKey string `mapstructure:"secret_key" yaml:"secret_key" json:"secret_key"`
|
||||
UseHTTPS bool `mapstructure:"use_https" yaml:"use_https" json:"use_https"`
|
||||
UseCDNDomains bool `mapstructure:"use_cdn_domains" yaml:"use_cdn_domains" json:"use_cdn_domains"`
|
||||
}
|
||||
|
||||
type ObjectStore struct {
|
||||
Endpoint string `mapstructure:"endpoint" yaml:"endpoint" json:"endpoint"`
|
||||
Region string `mapstructure:"region" yaml:"region" json:"region"`
|
||||
Bucket string `mapstructure:"bucket" yaml:"bucket" json:"bucket"`
|
||||
AccessKey string `mapstructure:"access_key" yaml:"access_key" json:"access_key"`
|
||||
SecretKey string `mapstructure:"secret_key" yaml:"secret_key" json:"secret_key"`
|
||||
BaseURL string `mapstructure:"base_url" yaml:"base_url" json:"base_url"`
|
||||
PathPrefix string `mapstructure:"path_prefix" yaml:"path_prefix" json:"path_prefix"`
|
||||
UseSSL bool `mapstructure:"use_ssl" yaml:"use_ssl" json:"use_ssl"`
|
||||
ForcePathStyle bool `mapstructure:"force_path_style" yaml:"force_path_style" json:"force_path_style"`
|
||||
AccountID string `mapstructure:"account_id" yaml:"account_id" json:"account_id"`
|
||||
}
|
||||
Reference in New Issue
Block a user