cmd: change how default configuration values are built

For the orchestrator, we need to build default values for slice of
configurations. We introduce a Reset() method that will be called by
mapstructure.
This commit is contained in:
Vincent Bernat
2022-07-07 20:55:33 +02:00
parent dff8773c7f
commit 22eab774a4
8 changed files with 158 additions and 25 deletions

View File

@@ -11,6 +11,7 @@ import (
"net/http"
"net/url"
"os"
"reflect"
"sort"
"strconv"
"strings"
@@ -77,6 +78,7 @@ func (c ConfigRelatedOptions) Parse(out io.Writer, component string, config inte
}
// Parse provided configuration
defaultHook, disableDefaultHook := DefaultHook()
var metadata mapstructure.Metadata
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
Result: &config,
@@ -89,6 +91,7 @@ func (c ConfigRelatedOptions) Parse(out io.Writer, component string, config inte
return key == field
},
DecodeHook: mapstructure.ComposeDecodeHookFunc(
defaultHook,
flow.ConfigurationUnmarshalerHook(),
clickhouse.NetworkNamesUnmarshalerHook(),
mapstructure.TextUnmarshallerHookFunc(),
@@ -102,6 +105,7 @@ func (c ConfigRelatedOptions) Parse(out io.Writer, component string, config inte
if err := decoder.Decode(rawConfig); err != nil {
return fmt.Errorf("unable to parse configuration: %w", err)
}
disableDefaultHook()
// Override with environment variables
for _, keyval := range os.Environ() {
@@ -171,3 +175,42 @@ func (c ConfigRelatedOptions) Parse(out io.Writer, component string, config inte
return nil
}
// DefaultHook will reset the destination value to its default using
// the Reset() method if present.
func DefaultHook() (mapstructure.DecodeHookFunc, func()) {
disabled := false
hook := func(from, to reflect.Value) (interface{}, error) {
if disabled {
return from.Interface(), nil
}
if to.Kind() == reflect.Ptr {
// We already have a pointer
method, ok := to.Type().MethodByName("Reset")
if !ok {
return from.Interface(), nil
}
if to.IsNil() {
new := reflect.New(to.Type().Elem())
method.Func.Call([]reflect.Value{new})
to.Set(new)
return from.Interface(), nil
}
method.Func.Call([]reflect.Value{to})
return from.Interface(), nil
}
// Not a pointer, let's check if we take a pointer
method, ok := reflect.PointerTo(to.Type()).MethodByName("Reset")
if !ok {
return from.Interface(), nil
}
method.Func.Call([]reflect.Value{to.Addr()})
// Resume decoding
return from.Interface(), nil
}
disable := func() {
disabled = true
}
return hook, disable
}

View File

@@ -47,8 +47,8 @@ type dummyModule2DetailsConfiguration struct {
IntervalValue time.Duration
}
func dummyDefaultConfiguration() dummyConfiguration {
return dummyConfiguration{
func (c *dummyConfiguration) Reset() {
*c = dummyConfiguration{
Module1: dummyModule1Configuration{
Listen: "127.0.0.1:8080",
Topic: "nothingness",
@@ -79,7 +79,7 @@ module1:
Path: configFile,
}
parsed := dummyDefaultConfiguration()
parsed := dummyConfiguration{}
out := bytes.NewBuffer([]byte{})
if err := c.Parse(out, "dummy", &parsed); err == nil {
t.Fatal("Parse() didn't error")
@@ -113,7 +113,7 @@ module2:
Dump: true,
}
parsed := dummyDefaultConfiguration()
parsed := dummyConfiguration{}
out := bytes.NewBuffer([]byte{})
if err := c.Parse(out, "dummy", &parsed); err != nil {
t.Fatalf("Parse() error:\n%+v", err)
@@ -213,7 +213,7 @@ module2:
Dump: true,
}
parsed := dummyDefaultConfiguration()
parsed := dummyConfiguration{}
out := bytes.NewBuffer([]byte{})
if err := c.Parse(out, "dummy", &parsed); err != nil {
t.Fatalf("Parse() error:\n%+v", err)
@@ -267,7 +267,7 @@ module2:
Dump: true,
}
parsed := dummyDefaultConfiguration()
parsed := dummyConfiguration{}
out := bytes.NewBuffer([]byte{})
if err := c.Parse(out, "dummy", &parsed); err != nil {
t.Fatalf("Parse() error:\n%+v", err)
@@ -312,7 +312,7 @@ module1:
c := cmd.ConfigRelatedOptions{Path: configFile}
parsed := dummyDefaultConfiguration()
parsed := dummyConfiguration{}
out := bytes.NewBuffer([]byte{})
if err := c.Parse(out, "dummy", &parsed); err != nil {
t.Fatalf("Parse() error:\n%+v", err)
@@ -332,7 +332,7 @@ module1:
c := cmd.ConfigRelatedOptions{Path: configFile}
parsed := dummyDefaultConfiguration()
parsed := dummyConfiguration{}
out := bytes.NewBuffer([]byte{})
if err := c.Parse(out, "dummy", &parsed); err == nil {
t.Fatal("Parse() didn't error")
@@ -343,3 +343,87 @@ invalid key "unused"`); diff != "" {
}
})
}
func TestDefaultInSlice(t *testing.T) {
try := func(t *testing.T, parse func(cmd.ConfigRelatedOptions, *bytes.Buffer) interface{}) {
// Configuration file
config := `---
modules:
- module1:
topic: flows1
- module1:
topic: flows2
`
configFile := filepath.Join(t.TempDir(), "config.yaml")
ioutil.WriteFile(configFile, []byte(config), 0644)
c := cmd.ConfigRelatedOptions{
Path: configFile,
Dump: true,
}
out := bytes.NewBuffer([]byte{})
parsed := parse(c, out)
// Expected configuration
expected := map[string][]dummyConfiguration{
"Modules": {
{
Module1: dummyModule1Configuration{
Listen: "127.0.0.1:8080",
Topic: "flows1",
Workers: 100,
},
Module2: dummyModule2Configuration{
MoreDetails: MoreDetails{
Stuff: "hello",
},
Details: dummyModule2DetailsConfiguration{
Workers: 1,
IntervalValue: time.Minute,
},
},
}, {
Module1: dummyModule1Configuration{
Listen: "127.0.0.1:8080",
Topic: "flows2",
Workers: 100,
},
Module2: dummyModule2Configuration{
MoreDetails: MoreDetails{
Stuff: "hello",
},
Details: dummyModule2DetailsConfiguration{
Workers: 1,
IntervalValue: time.Minute,
},
},
},
},
}
if diff := helpers.Diff(parsed, expected); diff != "" {
t.Errorf("Parse() (-got, +want):\n%s", diff)
}
}
t.Run("without pointer", func(t *testing.T) {
try(t, func(c cmd.ConfigRelatedOptions, out *bytes.Buffer) interface{} {
parsed := struct {
Modules []dummyConfiguration
}{}
if err := c.Parse(out, "dummy", &parsed); err != nil {
t.Fatalf("Parse() error:\n%+v", err)
}
return parsed
})
})
t.Run("with pointer", func(t *testing.T) {
try(t, func(c cmd.ConfigRelatedOptions, out *bytes.Buffer) interface{} {
parsed := struct {
Modules []*dummyConfiguration
}{}
if err := c.Parse(out, "dummy", &parsed); err != nil {
t.Fatalf("Parse() error:\n%+v", err)
}
return parsed
})
})
}

View File

@@ -27,9 +27,9 @@ type ConsoleConfiguration struct {
Database database.Configuration
}
// DefaultConsoleConfiguration is the default configuration for the console command.
func DefaultConsoleConfiguration() ConsoleConfiguration {
return ConsoleConfiguration{
// Reset resets the console configuration to its default value.
func (c *ConsoleConfiguration) Reset() {
*c = ConsoleConfiguration{
HTTP: http.DefaultConfiguration(),
Reporting: reporter.DefaultConfiguration(),
Console: console.DefaultConfiguration(),
@@ -55,7 +55,7 @@ var consoleCmd = &cobra.Command{
manage collected flows.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
config := DefaultConsoleConfiguration()
config := ConsoleConfiguration{}
ConsoleOptions.Path = args[0]
if err := ConsoleOptions.Parse(cmd.OutOrStdout(), "console", &config); err != nil {
return err

View File

@@ -11,7 +11,9 @@ import (
func TestConsoleStart(t *testing.T) {
r := reporter.NewMock(t)
if err := consoleStart(r, DefaultConsoleConfiguration(), true); err != nil {
config := ConsoleConfiguration{}
config.Reset()
if err := consoleStart(r, config, true); err != nil {
t.Fatalf("consoleStart() error:\n%+v", err)
}
}

View File

@@ -29,9 +29,9 @@ type InletConfiguration struct {
Core core.Configuration
}
// DefaultInletConfiguration is the default configuration for the inlet command.
func DefaultInletConfiguration() InletConfiguration {
return InletConfiguration{
// Reset resets the configuration for the inlet command to its default value.
func (c *InletConfiguration) Reset() {
*c = InletConfiguration{
HTTP: http.DefaultConfiguration(),
Reporting: reporter.DefaultConfiguration(),
Flow: flow.DefaultConfiguration(),
@@ -58,7 +58,7 @@ var inletCmd = &cobra.Command{
hydration and export to Kafka.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
config := DefaultInletConfiguration()
config := InletConfiguration{}
InletOptions.Path = args[0]
if err := InletOptions.Parse(cmd.OutOrStdout(), "inlet", &config); err != nil {
return err

View File

@@ -11,7 +11,9 @@ import (
func TestInletStart(t *testing.T) {
r := reporter.NewMock(t)
if err := inletStart(r, DefaultInletConfiguration(), true); err != nil {
config := InletConfiguration{}
config.Reset()
if err := inletStart(r, config, true); err != nil {
t.Fatalf("inletStart() error:\n%+v", err)
}
}

View File

@@ -30,9 +30,9 @@ type OrchestratorConfiguration struct {
Console []ConsoleConfiguration
}
// DefaultOrchestratorConfiguration is the default configuration for the orchestrator command.
func DefaultOrchestratorConfiguration() OrchestratorConfiguration {
return OrchestratorConfiguration{
// Reset resets the configuration of the orchestrator command to its default value.
func (c *OrchestratorConfiguration) Reset() {
*c = OrchestratorConfiguration{
Reporting: reporter.DefaultConfiguration(),
HTTP: http.DefaultConfiguration(),
ClickHouseDB: clickhousedb.DefaultConfiguration(),
@@ -40,8 +40,8 @@ func DefaultOrchestratorConfiguration() OrchestratorConfiguration {
Kafka: kafka.DefaultConfiguration(),
Orchestrator: orchestrator.DefaultConfiguration(),
// Other service configurations
Inlet: []InletConfiguration{DefaultInletConfiguration()},
Console: []ConsoleConfiguration{DefaultConsoleConfiguration()},
Inlet: []InletConfiguration{},
Console: []ConsoleConfiguration{},
}
}
@@ -61,7 +61,7 @@ var orchestratorCmd = &cobra.Command{
components and centralizes configuration of the various other components.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
config := DefaultOrchestratorConfiguration()
config := OrchestratorConfiguration{}
OrchestratorOptions.Path = args[0]
OrchestratorOptions.BeforeDump = func() {
// Override some parts of the configuration

View File

@@ -11,7 +11,9 @@ import (
func TestOrchestratorStart(t *testing.T) {
r := reporter.NewMock(t)
if err := orchestratorStart(r, DefaultOrchestratorConfiguration(), true); err != nil {
config := OrchestratorConfiguration{}
config.Reset()
if err := orchestratorStart(r, config, true); err != nil {
t.Fatalf("orchestratorStart() error:\n%+v", err)
}
}