diff --git a/pkg/config/binder/binder.go b/pkg/config/binder/binder.go new file mode 100644 index 0000000000..e59b210318 --- /dev/null +++ b/pkg/config/binder/binder.go @@ -0,0 +1,55 @@ +// Package binder binds OpenCloud yaml config files to config structs, as a leaf +// package that avoids importing the aggregate service config in pkg/config. +package binder + +import ( + "io/fs" + "os" + "path" + "strings" + + gofig "github.com/gookit/config/v2" + gooyaml "github.com/gookit/config/v2/yaml" + "github.com/opencloud-eu/opencloud/pkg/config/defaults" +) + +// decoderConfigTagName sets the tag name to be used from the config structs +// currently we only support "yaml" because we only support config loading +// from yaml files and the yaml parser has no simple way to set a custom tag name to use +const decoderConfigTagName = "yaml" + +// BindSourcesToStructs assigns any config value from a config file / env variable to struct `dst`. +func BindSourcesToStructs(service string, dst any) error { + fileSystem := os.DirFS("/") + filePath := strings.TrimLeft(path.Join(defaults.BaseConfigPath(), service+".yaml"), "/") + return BindSourcesToStructsFS(fileSystem, filePath, service, dst) +} + +// BindSourcesToStructsFS is like BindSourcesToStructs but reads from the given fs.FS and path. +func BindSourcesToStructsFS(fileSystem fs.FS, filePath, service string, dst any) error { + cnf := gofig.NewWithOptions(service) + cnf.WithOptions(func(options *gofig.Options) { + options.ParseEnv = true + options.DecoderConfig.TagName = decoderConfigTagName + }) + cnf.AddDriver(gooyaml.Driver) + + yamlContent, err := fs.ReadFile(fileSystem, filePath) + if err != nil { + if os.IsNotExist(err) { + return nil + } + + return err + } + // the error is ignored on purpose, matching the pre-extraction behavior: + // an unparseable yaml file binds nothing instead of failing the startup + _ = cnf.LoadSources("yaml", yamlContent) + + err = cnf.BindStruct("", &dst) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/config/binder/binder_test.go b/pkg/config/binder/binder_test.go new file mode 100644 index 0000000000..07f76bc577 --- /dev/null +++ b/pkg/config/binder/binder_test.go @@ -0,0 +1,55 @@ +package binder + +import ( + "testing" + "testing/fstest" + + "gotest.tools/v3/assert" +) + +type TestConfig struct { + A string `yaml:"a"` + B string `yaml:"b"` + C string `yaml:"c"` +} + +func TestBindSourcesToStructs(t *testing.T) { + // setup test env: one var set to pin env expansion, two deliberately + // unset to pin the defaults + t.Setenv("BINDER_TEST_SET_VAR", "from-env") + yaml := ` +a: "${BINDER_TEST_SET_VAR|no-foo}" +b: "${BINDER_TEST_UNSET_VAR|no-bar}" +c: "${BINDER_TEST_OTHER_UNSET_VAR|code}" +` + filePath := "etc/opencloud/foo.yaml" + fs := fstest.MapFS{ + filePath: {Data: []byte(yaml)}, + } + // perform test + c := TestConfig{} + err := BindSourcesToStructsFS(fs, filePath, "foo", &c) + if err != nil { + t.Error(err) + } + + assert.Equal(t, c.A, "from-env") + assert.Equal(t, c.B, "no-bar") + assert.Equal(t, c.C, "code") +} + +func TestBindSourcesToStructs_UnknownFile(t *testing.T) { + // setup test env + filePath := "etc/opencloud/foo.yaml" + fs := fstest.MapFS{} + // perform test + c := TestConfig{} + err := BindSourcesToStructsFS(fs, filePath, "foo", &c) + if err != nil { + t.Error(err) + } + + assert.Equal(t, c.A, "") + assert.Equal(t, c.B, "") + assert.Equal(t, c.C, "") +} diff --git a/pkg/config/helpers.go b/pkg/config/helpers.go index b3affd8dd0..576ecab5ee 100644 --- a/pkg/config/helpers.go +++ b/pkg/config/helpers.go @@ -1,54 +1,15 @@ package config import ( - "io/fs" - "os" - "path" "strings" - gofig "github.com/gookit/config/v2" - gooyaml "github.com/gookit/config/v2/yaml" - "github.com/opencloud-eu/opencloud/pkg/config/defaults" -) - -var ( - // decoderConfigTagName sets the tag name to be used from the config structs - // currently we only support "yaml" because we only support config loading - // from yaml files and the yaml parser has no simple way to set a custom tag name to use - decoderConfigTagName = "yaml" + "github.com/opencloud-eu/opencloud/pkg/config/binder" ) // BindSourcesToStructs assigns any config value from a config file / env variable to struct `dst`. +// The implementation lives in pkg/config/binder. func BindSourcesToStructs(service string, dst any) error { - fileSystem := os.DirFS("/") - filePath := strings.TrimLeft(path.Join(defaults.BaseConfigPath(), service+".yaml"), "/") - return bindSourcesToStructs(fileSystem, filePath, service, dst) -} - -func bindSourcesToStructs(fileSystem fs.FS, filePath, service string, dst any) error { - cnf := gofig.NewWithOptions(service) - cnf.WithOptions(func(options *gofig.Options) { - options.ParseEnv = true - options.DecoderConfig.TagName = decoderConfigTagName - }) - cnf.AddDriver(gooyaml.Driver) - - yamlContent, err := fs.ReadFile(fileSystem, filePath) - if err != nil { - if os.IsNotExist(err) { - return nil - } - - return err - } - _ = cnf.LoadSources("yaml", yamlContent) - - err = cnf.BindStruct("", &dst) - if err != nil { - return err - } - - return nil + return binder.BindSourcesToStructs(service, dst) } // LocalEndpoint returns the local endpoint for a given protocol and address. diff --git a/pkg/config/helpers_test.go b/pkg/config/helpers_test.go index d31782a141..dfcb8193ff 100644 --- a/pkg/config/helpers_test.go +++ b/pkg/config/helpers_test.go @@ -4,54 +4,10 @@ import ( "testing" "testing/fstest" + "github.com/opencloud-eu/opencloud/pkg/config/binder" "gotest.tools/v3/assert" ) -type TestConfig struct { - A string `yaml:"a"` - B string `yaml:"b"` - C string `yaml:"c"` -} - -func TestBindSourcesToStructs(t *testing.T) { - // setup test env - yaml := ` -a: "${FOO_VAR|no-foo}" -b: "${BAR_VAR|no-bar}" -c: "${CODE_VAR|code}" -` - filePath := "etc/opencloud/foo.yaml" - fs := fstest.MapFS{ - filePath: {Data: []byte(yaml)}, - } - // perform test - c := TestConfig{} - err := bindSourcesToStructs(fs, filePath, "foo", &c) - if err != nil { - t.Error(err) - } - - assert.Equal(t, c.A, "no-foo") - assert.Equal(t, c.B, "no-bar") - assert.Equal(t, c.C, "code") -} - -func TestBindSourcesToStructs_UnknownFile(t *testing.T) { - // setup test env - filePath := "etc/opencloud/foo.yaml" - fs := fstest.MapFS{} - // perform test - c := TestConfig{} - err := bindSourcesToStructs(fs, filePath, "foo", &c) - if err != nil { - t.Error(err) - } - - assert.Equal(t, c.A, "") - assert.Equal(t, c.B, "") - assert.Equal(t, c.C, "") -} - func TestBindSourcesToStructs_NoEnvVar(t *testing.T) { // setup test env yaml := ` @@ -180,7 +136,7 @@ clientlog: } // perform test c := Config{} - err := bindSourcesToStructs(fs, filePath, "foo", &c) + err := binder.BindSourcesToStructsFS(fs, filePath, "foo", &c) if err != nil { t.Error(err) }