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.
This commit is contained in:
Dominik Schmidt committed 2026-10-08 16:27:59 +02:00
1 parent 371e84ba68
commit 87fb14eb5d
4 files changed
+111 -90

No files matched your search

+53
View File
@@ -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
}
+53
View File
@@ -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, "")
}
+3 -44
View File
@@ -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
+2 -46
View File
@@ -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)
}