From b9fd10c202650955a549d69d047304d9756c1e3d Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Sat, 8 Aug 2026 01:26:35 +0200 Subject: [PATCH 1/2] refactor(config): extract BindSourcesToStructs into pkg/config/binder New leaf package (only gookit + pkg/config/defaults) so callers can bind a yaml config file without importing the aggregate service config. pkg/config keeps a backward-compatible re-export. --- pkg/config/binder/binder.go | 53 ++++++++++++++++++++++++++++++++ pkg/config/binder/binder_test.go | 53 ++++++++++++++++++++++++++++++++ pkg/config/helpers.go | 47 ++-------------------------- pkg/config/helpers_test.go | 48 ++--------------------------- 4 files changed, 111 insertions(+), 90 deletions(-) create mode 100644 pkg/config/binder/binder.go create mode 100644 pkg/config/binder/binder_test.go diff --git a/pkg/config/binder/binder.go b/pkg/config/binder/binder.go new file mode 100644 index 0000000000..28f6e11c25 --- /dev/null +++ b/pkg/config/binder/binder.go @@ -0,0 +1,53 @@ +// 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 +var 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 + } + _ = 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..e55c5d749b --- /dev/null +++ b/pkg/config/binder/binder_test.go @@ -0,0 +1,53 @@ +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 + 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 := BindSourcesToStructsFS(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 := 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..7a7fbcf031 100644 --- a/pkg/config/helpers.go +++ b/pkg/config/helpers.go @@ -1,55 +1,14 @@ 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`. -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 -} +// Backward-compatible re-export; the implementation lives in pkg/config/binder. +var BindSourcesToStructs = binder.BindSourcesToStructs // LocalEndpoint returns the local endpoint for a given protocol and address. // Use it when configuring the reva runtime to get a service endpoint in the same 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) } From eba5e870479b7e322c266bb017be1f8906585aaf Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Tue, 1 Sep 2026 09:45:03 +0200 Subject: [PATCH 2/2] refactor(config): keep the re-export a function, pin env expansion in the test --- pkg/config/binder/binder.go | 4 +++- pkg/config/binder/binder_test.go | 12 +++++++----- pkg/config/helpers.go | 6 ++++-- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/pkg/config/binder/binder.go b/pkg/config/binder/binder.go index 28f6e11c25..e59b210318 100644 --- a/pkg/config/binder/binder.go +++ b/pkg/config/binder/binder.go @@ -16,7 +16,7 @@ import ( // 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 -var decoderConfigTagName = "yaml" +const decoderConfigTagName = "yaml" // BindSourcesToStructs assigns any config value from a config file / env variable to struct `dst`. func BindSourcesToStructs(service string, dst any) error { @@ -42,6 +42,8 @@ func BindSourcesToStructsFS(fileSystem fs.FS, filePath, service string, dst any) 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) diff --git a/pkg/config/binder/binder_test.go b/pkg/config/binder/binder_test.go index e55c5d749b..07f76bc577 100644 --- a/pkg/config/binder/binder_test.go +++ b/pkg/config/binder/binder_test.go @@ -14,11 +14,13 @@ type TestConfig struct { } func TestBindSourcesToStructs(t *testing.T) { - // setup test env + // 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: "${FOO_VAR|no-foo}" -b: "${BAR_VAR|no-bar}" -c: "${CODE_VAR|code}" +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{ @@ -31,7 +33,7 @@ c: "${CODE_VAR|code}" t.Error(err) } - assert.Equal(t, c.A, "no-foo") + assert.Equal(t, c.A, "from-env") assert.Equal(t, c.B, "no-bar") assert.Equal(t, c.C, "code") } diff --git a/pkg/config/helpers.go b/pkg/config/helpers.go index 7a7fbcf031..576ecab5ee 100644 --- a/pkg/config/helpers.go +++ b/pkg/config/helpers.go @@ -7,8 +7,10 @@ import ( ) // BindSourcesToStructs assigns any config value from a config file / env variable to struct `dst`. -// Backward-compatible re-export; the implementation lives in pkg/config/binder. -var BindSourcesToStructs = binder.BindSourcesToStructs +// The implementation lives in pkg/config/binder. +func BindSourcesToStructs(service string, dst any) error { + return binder.BindSourcesToStructs(service, dst) +} // LocalEndpoint returns the local endpoint for a given protocol and address. // Use it when configuring the reva runtime to get a service endpoint in the same