feat(parser): support multiple -C config files, merged left-to-right
Allow passing -C multiple times to load and merge several configuration files in order. Slice fields are appended; scalar fields (TLS, Log, API, Metrics, Profiling) are overridden by later files — the same semantics as the existing mergeConfig() used for CLI+config merging. - Change Args.CfgFile string to Args.CfgFiles []string - Extract readConfig() helper from Parse() for single-source loading - Loop over CfgFiles in Parse(), merging each into the accumulator - Add Quotas to mergeConfig (was missing from the slice list) - Add TestParse_MultiFile integration test for two-file merge Closes go-gost/gost#150
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
package parser
|
package parser
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/go-gost/x/config"
|
"github.com/go-gost/x/config"
|
||||||
@@ -47,6 +49,7 @@ func TestMergeConfig_AppendsAllSlices(t *testing.T) {
|
|||||||
SDs: []*config.SDConfig{{Name: "sd1"}},
|
SDs: []*config.SDConfig{{Name: "sd1"}},
|
||||||
Recorders: []*config.RecorderConfig{{Name: "rec1"}},
|
Recorders: []*config.RecorderConfig{{Name: "rec1"}},
|
||||||
Limiters: []*config.LimiterConfig{{Name: "l1"}},
|
Limiters: []*config.LimiterConfig{{Name: "l1"}},
|
||||||
|
Quotas: []*config.QuotaConfig{{Name: "q1"}},
|
||||||
CLimiters: []*config.LimiterConfig{{Name: "cl1"}},
|
CLimiters: []*config.LimiterConfig{{Name: "cl1"}},
|
||||||
RLimiters: []*config.LimiterConfig{{Name: "rl1"}},
|
RLimiters: []*config.LimiterConfig{{Name: "rl1"}},
|
||||||
Loggers: []*config.LoggerConfig{{Name: "log1"}},
|
Loggers: []*config.LoggerConfig{{Name: "log1"}},
|
||||||
@@ -66,6 +69,7 @@ func TestMergeConfig_AppendsAllSlices(t *testing.T) {
|
|||||||
SDs: []*config.SDConfig{{Name: "sd2"}},
|
SDs: []*config.SDConfig{{Name: "sd2"}},
|
||||||
Recorders: []*config.RecorderConfig{{Name: "rec2"}},
|
Recorders: []*config.RecorderConfig{{Name: "rec2"}},
|
||||||
Limiters: []*config.LimiterConfig{{Name: "l2"}},
|
Limiters: []*config.LimiterConfig{{Name: "l2"}},
|
||||||
|
Quotas: []*config.QuotaConfig{{Name: "q2"}},
|
||||||
CLimiters: []*config.LimiterConfig{{Name: "cl2"}},
|
CLimiters: []*config.LimiterConfig{{Name: "cl2"}},
|
||||||
RLimiters: []*config.LimiterConfig{{Name: "rl2"}},
|
RLimiters: []*config.LimiterConfig{{Name: "rl2"}},
|
||||||
Loggers: []*config.LoggerConfig{{Name: "log2"}},
|
Loggers: []*config.LoggerConfig{{Name: "log2"}},
|
||||||
@@ -111,6 +115,9 @@ func TestMergeConfig_AppendsAllSlices(t *testing.T) {
|
|||||||
if len(got.Limiters) != 2 {
|
if len(got.Limiters) != 2 {
|
||||||
t.Fatal("limiters not properly appended")
|
t.Fatal("limiters not properly appended")
|
||||||
}
|
}
|
||||||
|
if len(got.Quotas) != 2 {
|
||||||
|
t.Fatal("quotas not properly appended")
|
||||||
|
}
|
||||||
if len(got.CLimiters) != 2 {
|
if len(got.CLimiters) != 2 {
|
||||||
t.Fatal("climiters not properly appended")
|
t.Fatal("climiters not properly appended")
|
||||||
}
|
}
|
||||||
@@ -194,7 +201,7 @@ func TestMergeConfig_ScalarKeepsCfg1WhenCfg2Nil(t *testing.T) {
|
|||||||
|
|
||||||
func TestInit(t *testing.T) {
|
func TestInit(t *testing.T) {
|
||||||
Init(Args{
|
Init(Args{
|
||||||
CfgFile: "test.yml",
|
CfgFiles: []string{"test.yml"},
|
||||||
Services: []string{"s1"},
|
Services: []string{"s1"},
|
||||||
Nodes: []string{"n1"},
|
Nodes: []string{"n1"},
|
||||||
Debug: true,
|
Debug: true,
|
||||||
@@ -203,8 +210,8 @@ func TestInit(t *testing.T) {
|
|||||||
MetricsAddr: ":9090",
|
MetricsAddr: ":9090",
|
||||||
})
|
})
|
||||||
|
|
||||||
if defaultParser.args.CfgFile != "test.yml" {
|
if len(defaultParser.args.CfgFiles) != 1 || defaultParser.args.CfgFiles[0] != "test.yml" {
|
||||||
t.Fatalf("CfgFile not set: got %q", defaultParser.args.CfgFile)
|
t.Fatalf("CfgFiles not set: got %v", defaultParser.args.CfgFiles)
|
||||||
}
|
}
|
||||||
if len(defaultParser.args.Services) != 1 || defaultParser.args.Services[0] != "s1" {
|
if len(defaultParser.args.Services) != 1 || defaultParser.args.Services[0] != "s1" {
|
||||||
t.Fatal("Services not set")
|
t.Fatal("Services not set")
|
||||||
@@ -225,3 +232,56 @@ func TestInit(t *testing.T) {
|
|||||||
t.Fatal("MetricsAddr not set")
|
t.Fatal("MetricsAddr not set")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParse_MultiFile(t *testing.T) {
|
||||||
|
// Create two temporary config files.
|
||||||
|
cfg1 := &config.Config{
|
||||||
|
Services: []*config.ServiceConfig{{Name: "s1", Addr: ":8080"}},
|
||||||
|
}
|
||||||
|
cfg2 := &config.Config{
|
||||||
|
Services: []*config.ServiceConfig{{Name: "s2", Addr: ":8081"}},
|
||||||
|
Log: &config.LogConfig{Level: "debug"},
|
||||||
|
Quotas: []*config.QuotaConfig{{Name: "q1"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
file1 := filepath.Join(tmpDir, "cfg1.yml")
|
||||||
|
file2 := filepath.Join(tmpDir, "cfg2.yml")
|
||||||
|
|
||||||
|
for _, entry := range []struct {
|
||||||
|
path string
|
||||||
|
cfg *config.Config
|
||||||
|
}{
|
||||||
|
{file1, cfg1},
|
||||||
|
{file2, cfg2},
|
||||||
|
} {
|
||||||
|
f, err := os.Create(entry.path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := entry.cfg.Write(f, "yaml"); err != nil {
|
||||||
|
f.Close()
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
f.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
Init(Args{
|
||||||
|
CfgFiles: []string{file1, file2},
|
||||||
|
})
|
||||||
|
|
||||||
|
got, err := Parse()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Parse(): %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(got.Services) != 2 {
|
||||||
|
t.Fatalf("expected 2 services, got %d", len(got.Services))
|
||||||
|
}
|
||||||
|
if got.Log == nil || got.Log.Level != "debug" {
|
||||||
|
t.Fatal("Log.Level not set from cfg2")
|
||||||
|
}
|
||||||
|
if len(got.Quotas) != 1 || got.Quotas[0].Name != "q1" {
|
||||||
|
t.Fatal("Quotas not merged from cfg2")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -35,9 +35,9 @@ func Parse() (*config.Config, error) {
|
|||||||
// Args holds the raw CLI flags and positional arguments that Init will merge
|
// Args holds the raw CLI flags and positional arguments that Init will merge
|
||||||
// into the final configuration.
|
// into the final configuration.
|
||||||
type Args struct {
|
type Args struct {
|
||||||
// CfgFile is the path to a YAML or JSON config file, "-" for stdin, or
|
// CfgFiles is the list of YAML or JSON config file paths, "-" for stdin, or
|
||||||
// an inline JSON string.
|
// an inline JSON string. Multiple files are merged left-to-right.
|
||||||
CfgFile string
|
CfgFiles []string
|
||||||
|
|
||||||
// Services is the list of service definitions from -L flags.
|
// Services is the list of service definitions from -L flags.
|
||||||
Services []string
|
Services []string
|
||||||
@@ -65,31 +65,16 @@ type parser struct {
|
|||||||
func (p *parser) Parse() (*config.Config, error) {
|
func (p *parser) Parse() (*config.Config, error) {
|
||||||
cfg := &config.Config{}
|
cfg := &config.Config{}
|
||||||
|
|
||||||
if cfgFile := strings.TrimSpace(p.args.CfgFile); cfgFile != "" {
|
for _, cfgFile := range p.args.CfgFiles {
|
||||||
if cfgFile == "-" { // stdin
|
cfgFile = strings.TrimSpace(cfgFile)
|
||||||
br := bufio.NewReader(os.Stdin)
|
if cfgFile == "" {
|
||||||
b, err := br.Peek(1)
|
continue
|
||||||
|
}
|
||||||
|
fcfg, err := readConfig(cfgFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if b[0] == '{' {
|
cfg = mergeConfig(cfg, fcfg)
|
||||||
if err := cfg.Read(br, "json"); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err := cfg.Read(br, "yaml"); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if strings.HasPrefix(cfgFile, "{") && strings.HasSuffix(cfgFile, "}") { // inline
|
|
||||||
if err := json.Unmarshal([]byte(cfgFile), cfg); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err := cfg.ReadFile(cfgFile); err != nil { // file
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cmdCfg, err := cmd.BuildConfigFromCmd(p.args.Services, p.args.Nodes)
|
cmdCfg, err := cmd.BuildConfigFromCmd(p.args.Services, p.args.Nodes)
|
||||||
@@ -193,6 +178,39 @@ func (p *parser) Parse() (*config.Config, error) {
|
|||||||
return cfg, nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// readConfig reads a single configuration source, which may be "-" (stdin),
|
||||||
|
// an inline JSON string starting with "{", or a file path.
|
||||||
|
func readConfig(cfgFile string) (*config.Config, error) {
|
||||||
|
cfg := &config.Config{}
|
||||||
|
|
||||||
|
if cfgFile == "-" { // stdin
|
||||||
|
br := bufio.NewReader(os.Stdin)
|
||||||
|
b, err := br.Peek(1)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if b[0] == '{' {
|
||||||
|
if err := cfg.Read(br, "json"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := cfg.Read(br, "yaml"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if strings.HasPrefix(cfgFile, "{") && strings.HasSuffix(cfgFile, "}") { // inline
|
||||||
|
if err := json.Unmarshal([]byte(cfgFile), cfg); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := cfg.ReadFile(cfgFile); err != nil { // file
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
func mergeConfig(cfg1, cfg2 *config.Config) *config.Config {
|
func mergeConfig(cfg1, cfg2 *config.Config) *config.Config {
|
||||||
if cfg1 == nil {
|
if cfg1 == nil {
|
||||||
return cfg2
|
return cfg2
|
||||||
@@ -214,6 +232,7 @@ func mergeConfig(cfg1, cfg2 *config.Config) *config.Config {
|
|||||||
SDs: append(cfg1.SDs, cfg2.SDs...),
|
SDs: append(cfg1.SDs, cfg2.SDs...),
|
||||||
Recorders: append(cfg1.Recorders, cfg2.Recorders...),
|
Recorders: append(cfg1.Recorders, cfg2.Recorders...),
|
||||||
Limiters: append(cfg1.Limiters, cfg2.Limiters...),
|
Limiters: append(cfg1.Limiters, cfg2.Limiters...),
|
||||||
|
Quotas: append(cfg1.Quotas, cfg2.Quotas...),
|
||||||
CLimiters: append(cfg1.CLimiters, cfg2.CLimiters...),
|
CLimiters: append(cfg1.CLimiters, cfg2.CLimiters...),
|
||||||
RLimiters: append(cfg1.RLimiters, cfg2.RLimiters...),
|
RLimiters: append(cfg1.RLimiters, cfg2.RLimiters...),
|
||||||
Loggers: append(cfg1.Loggers, cfg2.Loggers...),
|
Loggers: append(cfg1.Loggers, cfg2.Loggers...),
|
||||||
|
|||||||
Reference in New Issue
Block a user