@@ -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