@@ -3,12 +3,16 @@ package config
3
3
import (
4
4
"context"
5
5
"errors"
6
+ "fmt"
6
7
"log/slog"
8
+ "sync"
7
9
10
+ "github.com/go-playground/validator/v10"
8
11
"github.com/opentdf/platform/service/internal/server"
9
12
"github.com/opentdf/platform/service/logger"
10
13
"github.com/opentdf/platform/service/pkg/db"
11
14
"github.com/opentdf/platform/service/tracing"
15
+ "github.com/spf13/viper"
12
16
)
13
17
14
18
// ChangeHook is a function invoked when the configuration changes.
@@ -42,15 +46,17 @@ type Config struct {
42
46
SDKConfig SDKConfig `mapstructure:"sdk_config" json:"sdk_config"`
43
47
44
48
// Services represents the configuration settings for the services.
45
- Services ServicesMap `mapstructure:"services"`
49
+ Services ServicesMap `mapstructure:"services" json:"services" `
46
50
47
51
// Trace is for configuring open telemetry based tracing.
48
- Trace tracing.Config `mapstructure:"trace"`
52
+ Trace tracing.Config `mapstructure:"trace" json:"trace" `
49
53
50
54
// onConfigChangeHooks is a list of functions to call when the configuration changes.
51
55
onConfigChangeHooks []ChangeHook
52
56
// loaders is a list of configuration loaders.
53
57
loaders []Loader
58
+ // reloadMux ensures that the Reload function is thread-safe.
59
+ reloadMux * sync.Mutex
54
60
}
55
61
56
62
// SDKConfig represents the configuration for the SDK.
@@ -115,8 +121,23 @@ func (c *Config) Watch(ctx context.Context) error {
115
121
if len (c .loaders ) == 0 {
116
122
return nil
117
123
}
124
+
118
125
for _ , loader := range c .loaders {
119
- if err := loader .Watch (ctx , c , c .OnChange ); err != nil {
126
+ // onChangeCallback is the function that will be called by loaders when a change is detected.
127
+ // It orchestrates the reloading of the entire configuration and then triggers the registered hooks.
128
+ loaderName := loader .Name ()
129
+ onChangeCallback := func (ctx context.Context ) error {
130
+ slog .InfoContext (ctx , "configuration change detected, reloading..." , slog .String ("loader" , loaderName ))
131
+ if err := c .Reload (ctx ); err != nil {
132
+ slog .ErrorContext (ctx , "failed to reload configuration" , slog .Any ("error" , err ))
133
+ return err
134
+ }
135
+ slog .InfoContext (ctx , "configuration reloaded successfully" )
136
+
137
+ // Now call the user-provided hooks with the new configuration.
138
+ return c .OnChange (ctx )
139
+ }
140
+ if err := loader .Watch (ctx , c , onChangeCallback ); err != nil {
120
141
return err
121
142
}
122
143
}
@@ -138,10 +159,11 @@ func (c *Config) Close(ctx context.Context) error {
138
159
}
139
160
140
161
// OnChange invokes all registered onConfigChangeHooks after a configuration change.
141
- func (c * Config ) OnChange (_ context.Context ) error {
142
- if len (c .loaders ) == 0 {
162
+ func (c * Config ) OnChange (ctx context.Context ) error {
163
+ if len (c .onConfigChangeHooks ) == 0 {
143
164
return nil
144
165
}
166
+ slog .DebugContext (ctx , "executing configuration change hooks" )
145
167
for _ , hook := range c .onConfigChangeHooks {
146
168
if err := hook (c .Services ); err != nil {
147
169
return err
@@ -150,6 +172,87 @@ func (c *Config) OnChange(_ context.Context) error {
150
172
return nil
151
173
}
152
174
175
+ // Reload re-reads and merges configuration from all registered loaders.
176
+ // It is thread-safe and handles dependencies between loaders by iterating
177
+ // until the configuration stabilizes.
178
+ func (c * Config ) Reload (ctx context.Context ) error {
179
+ // Lock to ensure only one reload operation happens at a time.
180
+ c .reloadMux .Lock ()
181
+ defer c .reloadMux .Unlock ()
182
+
183
+ // This loop handles dependencies between loaders. It continues to iterate
184
+ // until a full pass over all loaders adds no new configuration values.
185
+ // This ensures that a loader can use configuration provided by another
186
+ // loader that appears later in the priority list.
187
+ var assigned map [string ]struct {}
188
+ for {
189
+ lastAssignedCount := len (assigned )
190
+ assigned = make (map [string ]struct {})
191
+ orderedViper := viper .NewWithOptions (viper .WithLogger (slog .Default ()))
192
+
193
+ // Loop through loaders in their order of priority.
194
+ for _ , loader := range c .loaders {
195
+ // The Load call allows the loader to refresh its internal state (e.g., re-read file, re-query DB).
196
+ // It uses the config `c` from the *previous* iteration to configure itself if needed.
197
+ if err := loader .Load (* c ); err != nil {
198
+ return fmt .Errorf ("loader %s failed to load: %w" , loader .Name (), err )
199
+ }
200
+
201
+ // Get all keys this loader knows about.
202
+ keys , err := loader .GetConfigKeys ()
203
+ if err != nil {
204
+ slog .WarnContext (
205
+ ctx ,
206
+ "loader failed to get config keys" ,
207
+ slog .String ("loader" , loader .Name ()),
208
+ slog .Any ("error" , err ),
209
+ )
210
+ continue
211
+ }
212
+
213
+ // Merge values from the current loader into Viper.
214
+ for _ , key := range keys {
215
+ // If a higher-priority loader already set this key, skip.
216
+ if _ , assignedAlready := assigned [key ]; assignedAlready {
217
+ continue
218
+ }
219
+ loaderValue , err := loader .Get (key )
220
+ if err != nil {
221
+ slog .WarnContext (
222
+ ctx ,
223
+ "loader.Get failed for a reported key" ,
224
+ slog .String ("loader" , loader .Name ()),
225
+ slog .String ("key" , key ),
226
+ slog .Any ("error" , err ),
227
+ )
228
+ continue
229
+ }
230
+ if loaderValue != nil {
231
+ orderedViper .Set (key , loaderValue )
232
+ assigned [key ] = struct {}{}
233
+ }
234
+ }
235
+ }
236
+
237
+ // Unmarshal the merged configuration into the main config struct `c`
238
+ // so it's available for the next iteration of the dependency loop.
239
+ if err := orderedViper .Unmarshal (c ); err != nil {
240
+ return errors .Join (err , ErrUnmarshallingConfig )
241
+ }
242
+
243
+ // If no new keys were assigned in this pass, the configuration has stabilized.
244
+ if len (assigned ) == lastAssignedCount {
245
+ break
246
+ }
247
+ }
248
+
249
+ // Final validation after the configuration has converged.
250
+ if err := validator .New ().Struct (c ); err != nil {
251
+ return errors .Join (err , ErrUnmarshallingConfig )
252
+ }
253
+ return nil
254
+ }
255
+
153
256
func (c SDKConfig ) LogValue () slog.Value {
154
257
return slog .GroupValue (
155
258
slog .Group ("core" ,
@@ -165,19 +268,36 @@ func (c SDKConfig) LogValue() slog.Value {
165
268
)
166
269
}
167
270
168
- // LoadConfig loads configuration using the provided loader or creates a default Viper loader
169
- func LoadConfig (_ context.Context , key , file string ) (* Config , error ) {
170
- config := & Config {}
171
-
172
- // Create default loader if none provided
173
- loader , err := NewEnvironmentLoader (key , file )
271
+ // Deprecated: Use the `Load` method with your preferred loaders
272
+ func LoadConfig (ctx context.Context , key , file string ) (* Config , error ) {
273
+ envLoader , err := NewEnvironmentValueLoader (key , nil )
174
274
if err != nil {
175
- return nil , err
275
+ return nil , fmt .Errorf ("could not load config: %w" , err )
276
+ }
277
+ configFileLoader , err := NewConfigFileLoader (key , file )
278
+ if err != nil {
279
+ return nil , fmt .Errorf ("could not load config: %w" , err )
280
+ }
281
+ defaultSettingsLoader , err := NewDefaultSettingsLoader ()
282
+ if err != nil {
283
+ return nil , fmt .Errorf ("could not load config: %w" , err )
284
+ }
285
+ return Load (
286
+ ctx ,
287
+ envLoader ,
288
+ configFileLoader ,
289
+ defaultSettingsLoader ,
290
+ )
291
+ }
292
+
293
+ // Load loads configuration using the provided loaders
294
+ func Load (ctx context.Context , loaders ... Loader ) (* Config , error ) {
295
+ config := & Config {
296
+ loaders : loaders ,
297
+ reloadMux : & sync.Mutex {},
176
298
}
177
299
178
- // Load initial configuration
179
- config .AddLoader (loader )
180
- if err := loader .Load (config ); err != nil {
300
+ if err := config .Reload (ctx ); err != nil {
181
301
return nil , err
182
302
}
183
303
0 commit comments