Skip to content

Commit ed6b36e

Browse files
committed
better handling of nested structs and add tests
1 parent a81c6ba commit ed6b36e

2 files changed

Lines changed: 144 additions & 2 deletions

File tree

cmd/dependency/dependency.go

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,10 @@ func InitCommandAndConfig(cmd *cobra.Command, useConfigFile bool, config any) {
9696
// bindEnvsFromConfig binds an env for every config key, so AutomaticEnv can
9797
// override keys that are not present in the config file.
9898
func bindEnvsFromConfig(config any) {
99-
// Marshal the default config and let viper flatten it into keys.
100-
b, err := yaml.Marshal(config)
99+
schema := reflect.New(reflect.TypeOf(config).Elem()).Interface()
100+
materializeStructPtrs(reflect.ValueOf(schema).Elem())
101+
102+
b, err := yaml.Marshal(schema)
101103
if err != nil {
102104
panic(fmt.Errorf("marshal config for env binding: %w", err))
103105
}
@@ -113,6 +115,29 @@ func bindEnvsFromConfig(config any) {
113115
}
114116
}
115117

118+
// materializeStructPtrs allocates nil struct-pointers so their nested keys serialize.
119+
func materializeStructPtrs(v reflect.Value) {
120+
switch v.Kind() {
121+
case reflect.Pointer:
122+
if v.Type().Elem().Kind() != reflect.Struct {
123+
return
124+
}
125+
if v.IsNil() {
126+
if !v.CanSet() {
127+
return
128+
}
129+
v.Set(reflect.New(v.Type().Elem()))
130+
}
131+
materializeStructPtrs(v.Elem())
132+
case reflect.Struct:
133+
for i := 0; i < v.NumField(); i++ {
134+
if f := v.Field(i); f.CanSet() {
135+
materializeStructPtrs(f)
136+
}
137+
}
138+
}
139+
}
140+
116141
// InitMonitor initialize monitor and return final handler.
117142
func InitMonitor(ctx context.Context, pprofPort int, tracingConfig base.TracingConfig) func() {
118143
var shutdowns = make(chan func(), 2)

cmd/dependency/dependency_test.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* Copyright 2020 The Dragonfly Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package dependency
18+
19+
import (
20+
"strings"
21+
"testing"
22+
23+
"github.com/spf13/viper"
24+
"github.com/stretchr/testify/assert"
25+
"github.com/stretchr/testify/require"
26+
27+
schedulerconfig "d7y.io/dragonfly/v2/scheduler/config"
28+
)
29+
30+
// tlsConfig mirrors the optional, pointer-typed TLS sections of the real
31+
// configs (e.g. scheduler/config.Config.Server.TLS), which are left nil by the
32+
// default config and therefore never appear in a marshalled value snapshot.
33+
type tlsConfig struct {
34+
CACert string `yaml:"caCert" mapstructure:"caCert"`
35+
}
36+
37+
// testConfig mirrors the real config shape: populated scalar defaults plus an
38+
// optional nested section behind a nil pointer.
39+
type testConfig struct {
40+
Name string `yaml:"name" mapstructure:"name"`
41+
TLS *tlsConfig `yaml:"tls" mapstructure:"tls"`
42+
}
43+
44+
// newTestConfig returns the default config: scalar defaults set, optional TLS
45+
// section left nil (as config.New() does for the real configs).
46+
func newTestConfig() *testConfig {
47+
return &testConfig{Name: "default-name"}
48+
}
49+
50+
// setupViper resets and configures the package-global viper the same way
51+
// InitCommandAndConfig does, with the given env prefix.
52+
func setupViper(prefix string) {
53+
viper.Reset()
54+
viper.SetEnvPrefix(prefix)
55+
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
56+
viper.AutomaticEnv()
57+
}
58+
59+
func TestBindEnvsFromConfig_TopLevelOverride(t *testing.T) {
60+
setupViper("test")
61+
t.Setenv("TEST_NAME", "from-env")
62+
63+
cfg := newTestConfig()
64+
bindEnvsFromConfig(cfg)
65+
require.NoError(t, viper.Unmarshal(cfg, initDecoderConfig))
66+
67+
assert.Equal(t, "from-env", cfg.Name)
68+
}
69+
70+
// TestBindEnvsFromConfig_NestedNilPointerOverride is the regression guard: the
71+
// TLS section is nil in the default config, so the previous marshal-of-value
72+
// implementation never bound TEST_TLS_CACERT and this override was silently
73+
// dropped. Enumerating keys from the type closes that gap.
74+
func TestBindEnvsFromConfig_NestedNilPointerOverride(t *testing.T) {
75+
setupViper("test")
76+
t.Setenv("TEST_TLS_CACERT", "/etc/ssl/ca.crt")
77+
78+
cfg := newTestConfig()
79+
require.Nil(t, cfg.TLS, "TLS should start nil to model the default config")
80+
81+
bindEnvsFromConfig(cfg)
82+
require.NoError(t, viper.Unmarshal(cfg, initDecoderConfig))
83+
84+
require.NotNil(t, cfg.TLS, "nested env override should materialize the TLS section")
85+
assert.Equal(t, "/etc/ssl/ca.crt", cfg.TLS.CACert)
86+
}
87+
88+
func TestBindEnvsFromConfig_NoEnvKeepsDefaults(t *testing.T) {
89+
setupViper("test")
90+
91+
cfg := newTestConfig()
92+
bindEnvsFromConfig(cfg)
93+
require.NoError(t, viper.Unmarshal(cfg, initDecoderConfig))
94+
95+
assert.Equal(t, "default-name", cfg.Name)
96+
assert.Nil(t, cfg.TLS, "no env set should leave the optional section nil")
97+
}
98+
99+
// TestBindEnvsFromConfig_RealSchedulerConfig exercises the actual production
100+
// config type end-to-end, including a nested key (Server.TLS.CACert) that lives
101+
// under a pointer left nil by config.New() — the regression the fix targets.
102+
func TestBindEnvsFromConfig_RealSchedulerConfig(t *testing.T) {
103+
setupViper("scheduler")
104+
t.Setenv("SCHEDULER_SERVER_HOST", "override-host")
105+
t.Setenv("SCHEDULER_SERVER_TLS_CACERT", "/etc/ssl/ca.crt")
106+
107+
cfg := schedulerconfig.New()
108+
require.Nil(t, cfg.Server.TLS, "default scheduler config leaves Server.TLS nil")
109+
110+
bindEnvsFromConfig(cfg)
111+
require.NoError(t, viper.Unmarshal(cfg, initDecoderConfig))
112+
113+
assert.Equal(t, "override-host", cfg.Server.Host, "env should override a populated default")
114+
115+
require.NotNil(t, cfg.Server.TLS, "nested env override should materialize the TLS section")
116+
assert.Equal(t, "/etc/ssl/ca.crt", cfg.Server.TLS.CACert)
117+
}

0 commit comments

Comments
 (0)