Skip to content

Commit a0213d2

Browse files
committed
Support loading configurations from both YAML and env
This commit adds the `config#Load` function to load configurations from both YAML files and environment variables. It handles scenarios where configurations are loaded exclusively from YAML, combined with environment variables, or entirely from environment variables if the YAML file is missing and no specific configuration file is provided via command line flags.
1 parent 430ab9b commit a0213d2

File tree

1 file changed

+117
-0
lines changed

1 file changed

+117
-0
lines changed

config/config.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import (
5151
"github.com/goccy/go-yaml"
5252
"github.com/jessevdk/go-flags"
5353
"github.com/pkg/errors"
54+
"io/fs"
5455
"os"
5556
"reflect"
5657
)
@@ -159,6 +160,122 @@ func FromEnv(v Validator, options EnvOptions) error {
159160
return nil
160161
}
161162

163+
// LoadOptions contains options for loading configuration from both files and environment variables.
164+
type LoadOptions struct {
165+
// Flags provides access to specific command line flag values.
166+
Flags Flags
167+
168+
// EnvOptions contains options for loading configuration from environment variables.
169+
EnvOptions EnvOptions
170+
}
171+
172+
// Load loads configuration from both YAML files and environment variables and
173+
// stores the result in the value pointed to by v.
174+
// If v is nil or not a struct pointer,
175+
// Load returns an [ErrInvalidArgument] error.
176+
//
177+
// It is possible to define default values via the struct tag `default`.
178+
//
179+
// The function also validates the configuration using the Validate method
180+
// of the provided [Validator] interface.
181+
// Any error returned from Validate is propagated with [ErrInvalidConfiguration] attached,
182+
// allowing errors.Is() checks on the returned errors to recognize both ErrInvalidConfiguration and
183+
// the original errors returned from Validate.
184+
//
185+
// This function handles configuration loading in three scenarios:
186+
//
187+
// 1. Load configuration exclusively from YAML files when no applicable environment variables are set.
188+
// 2. Combine YAML file and environment variable configurations, allowing environment variables to
189+
// supplement or override possible incomplete YAML data.
190+
// 3. Load entirely from environment variables if the default YAML config file is missing and
191+
// no specific config path is provided.
192+
//
193+
// Example usage:
194+
//
195+
// const DefaultConfigPath = "/path/to/config.yml"
196+
//
197+
// type Flags struct {
198+
// Config string `short:"c" long:"config" description:"Path to config file"`
199+
// }
200+
//
201+
// func (f Flags) GetConfigPath() string {
202+
// if f.Config == "" {
203+
// return DefaultConfigPath
204+
// }
205+
//
206+
// return f.Config
207+
// }
208+
//
209+
// func (f Flags) IsExplicitConfigPath() bool {
210+
// return f.Config != ""
211+
// }
212+
//
213+
// type Config struct {
214+
// ServerAddress string `yaml:"server_address" env:"SERVER_ADDRESS" default:"localhost:8080"`
215+
// TLS config.TLS `yaml:",inline"`
216+
// }
217+
//
218+
// func (c *Config) Validate() error {
219+
// if _, _, err := net.SplitHostPort(c.ServerAddress); err != nil {
220+
// return errors.Wrapf(err, "invalid server address: %s", c.ServerAddress)
221+
// }
222+
//
223+
// if err := c.TLS.Validate(); err != nil {
224+
// return errors.WithStack(err)
225+
// }
226+
//
227+
// return nil
228+
// }
229+
//
230+
// func main() {
231+
// var flags Flags
232+
// if err := config.ParseFlags(&flags); err != nil {
233+
// log.Fatalf("error parsing flags: %v", err)
234+
// }
235+
//
236+
// var cfg Config
237+
// if err := config.Load(&cfg, config.LoadOptions{Flags: flags, EnvOptions: config.EnvOptions{}}); err != nil {
238+
// log.Fatalf("error loading config: %v", err)
239+
// }
240+
//
241+
// tlsCfg, err := cfg.TLS.MakeConfig("icinga.com")
242+
// if err != nil {
243+
// log.Fatalf("error creating TLS config: %v", err)
244+
// }
245+
//
246+
// // ...
247+
// }
248+
func Load(v Validator, options LoadOptions) error {
249+
if err := validateNonNilStructPointer(v); err != nil {
250+
return errors.WithStack(err)
251+
}
252+
253+
if err := FromYAMLFile(options.Flags.GetConfigPath(), v); err != nil {
254+
// Allow continuation with FromEnv by handling:
255+
//
256+
// - ErrInvalidConfiguration:
257+
// The configuration may be incomplete and will be revalidated in FromEnv.
258+
//
259+
// - Non-existent file errors:
260+
// If no explicit config path is set, fallback to environment variables is allowed.
261+
configIsInvalid := errors.Is(err, ErrInvalidConfiguration)
262+
configFileDoesNotExist := errors.Is(err, fs.ErrNotExist) && !options.Flags.IsExplicitConfigPath()
263+
if !(configIsInvalid || configFileDoesNotExist) {
264+
return errors.WithStack(err)
265+
}
266+
}
267+
268+
// Call FromEnv regardless of the outcome from FromYAMLFile.
269+
// If no environment variables are set, configuration relies entirely on YAML.
270+
// Otherwise, environment variables can supplement, override YAML settings, or serve as the sole source.
271+
// FromEnv also includes validation, ensuring completeness after considering both sources.
272+
if err := FromEnv(v, options.EnvOptions); err != nil {
273+
return errors.WithStack(err)
274+
}
275+
276+
return nil
277+
}
278+
162279
// ParseFlags parses CLI flags and stores the result
163280
// in the value pointed to by v. If v is nil or not a struct pointer,
164281
// ParseFlags returns an [ErrInvalidArgument] error.

0 commit comments

Comments
 (0)