11// Package config provides utilities for configuration parsing and loading.
2- // It includes functionality for handling command-line flags and loading configuration from YAML files,
2+ // It includes functionality for handling command-line flags and
3+ // loading configuration from YAML files and environment variables,
34// with additional support for setting default values and validation.
45// Additionally, it provides a struct that defines common settings for a TLS client.
5- //
6- // Example usage:
7- //
8- // type Config struct {
9- // ServerAddress string `yaml:"server_address" default:"localhost:8080"`
10- // TLS config.TLS `yaml:",inline"`
11- // }
12- //
13- // // Validate implements the Validator interface.
14- // func (c *Config) Validate() error {
15- // if _, _, err := net.SplitHostPort(c.ServerAddress); err != nil {
16- // return errors.Wrapf(err, "invalid server address: %s", c.ServerAddress)
17- // }
18- //
19- // return nil
20- // }
21- //
22- // type Flags struct {
23- // Config string `short:"c" long:"config" description:"Path to config file" required:"true"`
24- // }
25- //
26- // func main() {
27- // var flags Flags
28- // if err := config.ParseFlags(&flags); err != nil {
29- // log.Fatalf("error parsing flags: %v", err)
30- // }
31- //
32- // var cfg Config
33- // if err := config.FromYAMLFile(flags.Config, &cfg); err != nil {
34- // log.Fatalf("error loading config: %v", err)
35- // }
36- //
37- // tlsCfg, err := cfg.TLS.MakeConfig("icinga.com")
38- // if err != nil {
39- // log.Fatalf("error creating TLS config: %v", err)
40- // }
41- //
42- // // ...
43- // }
446package config
457
468import (
@@ -51,16 +13,17 @@ import (
5113 "github.com/goccy/go-yaml"
5214 "github.com/jessevdk/go-flags"
5315 "github.com/pkg/errors"
16+ "io/fs"
5417 "os"
5518 "reflect"
5619)
5720
58- // ErrInvalidArgument is the error returned by [ParseFlags] or [FromYAMLFile] if
59- // its parsing result cannot be stored in the value pointed to by the designated passed argument which
60- // must be a non-nil struct pointer.
21+ // ErrInvalidArgument is the error returned by any function that loads configuration if
22+ // the parsing result cannot be stored in the value pointed to by the specified argument,
23+ // which must be a non-nil struct pointer.
6124var ErrInvalidArgument = stderrors .New ("invalid argument" )
6225
63- // ErrInvalidConfiguration is attached to errors returned by [FromYAMLFile] or [FromEnv] when
26+ // ErrInvalidConfiguration is attached to errors returned by any function that loads configuration when
6427// the configuration is invalid,
6528// i.e. if the Validate method of the provided [Validator] interface returns an error,
6629// which is then propagated by these functions.
@@ -71,7 +34,9 @@ var ErrInvalidConfiguration = stderrors.New("invalid configuration")
7134// FromYAMLFile parses the given YAML file and stores the result
7235// in the value pointed to by v. If v is nil or not a struct pointer,
7336// FromYAMLFile returns an [ErrInvalidArgument] error.
37+ //
7438// It is possible to define default values via the struct tag `default`.
39+ //
7540// The function also validates the configuration using the Validate method
7641// of the provided [Validator] interface.
7742// Any error returned from Validate is propagated with [ErrInvalidConfiguration] attached,
@@ -82,14 +47,18 @@ var ErrInvalidConfiguration = stderrors.New("invalid configuration")
8247//
8348// type Config struct {
8449// ServerAddress string `yaml:"server_address" default:"localhost:8080"`
50+ // TLS config.TLS `yaml:",inline"`
8551// }
8652//
87- // // Validate implements the Validator interface.
8853// func (c *Config) Validate() error {
8954// if _, _, err := net.SplitHostPort(c.ServerAddress); err != nil {
9055// return errors.Wrapf(err, "invalid server address: %s", c.ServerAddress)
9156// }
9257//
58+ // if err := c.TLS.Validate(); err != nil {
59+ // return errors.WithStack(err)
60+ // }
61+ //
9362// return nil
9463// }
9564//
@@ -99,6 +68,11 @@ var ErrInvalidConfiguration = stderrors.New("invalid configuration")
9968// log.Fatalf("error loading config: %v", err)
10069// }
10170//
71+ // tlsCfg, err := cfg.TLS.MakeConfig("icinga.com")
72+ // if err != nil {
73+ // log.Fatalf("error creating TLS config: %v", err)
74+ // }
75+ //
10276// // ...
10377// }
10478func FromYAMLFile (name string , v Validator ) error {
@@ -136,9 +110,47 @@ type EnvOptions = env.Options
136110
137111// FromEnv parses environment variables and stores the result in the value pointed to by v.
138112// If v is nil or not a struct pointer, FromEnv returns an [ErrInvalidArgument] error.
113+ //
114+ // It is possible to define default values via the struct tag `default`.
115+ //
116+ // The function also validates the configuration using the Validate method
117+ // of the provided [Validator] interface.
139118// Any error returned from Validate is propagated with [ErrInvalidConfiguration] attached,
140119// allowing errors.Is() checks on the returned errors to recognize both ErrInvalidConfiguration and
141120// the original errors returned from Validate.
121+ //
122+ // Example usage:
123+ //
124+ // type Config struct {
125+ // ServerAddress string `env:"SERVER_ADDRESS" default:"localhost:8080"`
126+ // TLS config.TLS
127+ // }
128+ //
129+ // func (c *Config) Validate() error {
130+ // if _, _, err := net.SplitHostPort(c.ServerAddress); err != nil {
131+ // return errors.Wrapf(err, "invalid server address: %s", c.ServerAddress)
132+ // }
133+ //
134+ // if err := c.TLS.Validate(); err != nil {
135+ // return errors.WithStack(err)
136+ // }
137+ //
138+ // return nil
139+ // }
140+ //
141+ // func main() {
142+ // var cfg Config
143+ // if err := config.FromEnv(cfg, config.EnvOptions{}); err != nil {
144+ // log.Fatalf("error loading config: %v", err)
145+ // }
146+ //
147+ // tlsCfg, err := cfg.TLS.MakeConfig("icinga.com")
148+ // if err != nil {
149+ // log.Fatalf("error creating TLS config: %v", err)
150+ // }
151+ //
152+ // // ...
153+ // }
142154func FromEnv (v Validator , options EnvOptions ) error {
143155 if err := validateNonNilStructPointer (v ); err != nil {
144156 return errors .WithStack (err )
@@ -159,13 +171,131 @@ func FromEnv(v Validator, options EnvOptions) error {
159171 return nil
160172}
161173
174+ // LoadOptions contains options for loading configuration from both files and environment variables.
175+ type LoadOptions struct {
176+ // Flags provides access to specific command line flag values.
177+ Flags Flags
178+
179+ // EnvOptions contains options for loading configuration from environment variables.
180+ EnvOptions EnvOptions
181+ }
182+
183+ // Load loads configuration from both YAML files and environment variables and
184+ // stores the result in the value pointed to by v.
185+ // If v is nil or not a struct pointer,
186+ // Load returns an [ErrInvalidArgument] error.
187+ //
188+ // It is possible to define default values via the struct tag `default`.
189+ //
190+ // The function also validates the configuration using the Validate method
191+ // of the provided [Validator] interface.
192+ // Any error returned from Validate is propagated with [ErrInvalidConfiguration] attached,
193+ // allowing errors.Is() checks on the returned errors to recognize both ErrInvalidConfiguration and
194+ // the original errors returned from Validate.
195+ //
196+ // This function handles configuration loading in three scenarios:
197+ //
198+ // 1. Load configuration exclusively from YAML files when no applicable environment variables are set.
199+ // 2. Combine YAML file and environment variable configurations, allowing environment variables to
200+ // supplement or override possible incomplete YAML data.
201+ // 3. Load entirely from environment variables if the default YAML config file is missing and
202+ // no specific config path is provided.
203+ //
204+ // Example usage:
205+ //
206+ // const DefaultConfigPath = "/path/to/config.yml"
207+ //
208+ // type Flags struct {
209+ // Config string `short:"c" long:"config" description:"Path to config file"`
210+ // }
211+ //
212+ // func (f Flags) GetConfigPath() string {
213+ // if f.Config == "" {
214+ // return DefaultConfigPath
215+ // }
216+ //
217+ // return f.Config
218+ // }
219+ //
220+ // func (f Flags) IsExplicitConfigPath() bool {
221+ // return f.Config != ""
222+ // }
223+ //
224+ // type Config struct {
225+ // ServerAddress string `yaml:"server_address" env:"SERVER_ADDRESS" default:"localhost:8080"`
226+ // TLS config.TLS `yaml:",inline"`
227+ // }
228+ //
229+ // func (c *Config) Validate() error {
230+ // if _, _, err := net.SplitHostPort(c.ServerAddress); err != nil {
231+ // return errors.Wrapf(err, "invalid server address: %s", c.ServerAddress)
232+ // }
233+ //
234+ // if err := c.TLS.Validate(); err != nil {
235+ // return errors.WithStack(err)
236+ // }
237+ //
238+ // return nil
239+ // }
240+ //
241+ // func main() {
242+ // var flags Flags
243+ // if err := config.ParseFlags(&flags); err != nil {
244+ // log.Fatalf("error parsing flags: %v", err)
245+ // }
246+ //
247+ // var cfg Config
248+ // if err := config.Load(&cfg, config.LoadOptions{Flags: flags, EnvOptions: config.EnvOptions{}}); err != nil {
249+ // log.Fatalf("error loading config: %v", err)
250+ // }
251+ //
252+ // tlsCfg, err := cfg.TLS.MakeConfig("icinga.com")
253+ // if err != nil {
254+ // log.Fatalf("error creating TLS config: %v", err)
255+ // }
256+ //
257+ // // ...
258+ // }
259+ func Load (v Validator , options LoadOptions ) error {
260+ if err := validateNonNilStructPointer (v ); err != nil {
261+ return errors .WithStack (err )
262+ }
263+
264+ if err := FromYAMLFile (options .Flags .GetConfigPath (), v ); err != nil {
265+ // Allow continuation with FromEnv by handling:
266+ //
267+ // - ErrInvalidConfiguration:
268+ // The configuration may be incomplete and will be revalidated in FromEnv.
269+ //
270+ // - Non-existent file errors:
271+ // If no explicit config path is set, fallback to environment variables is allowed.
272+ configIsInvalid := errors .Is (err , ErrInvalidConfiguration )
273+ configFileDoesNotExist := errors .Is (err , fs .ErrNotExist ) && ! options .Flags .IsExplicitConfigPath ()
274+ if ! (configIsInvalid || configFileDoesNotExist ) {
275+ return errors .WithStack (err )
276+ }
277+ }
278+
279+ // Call FromEnv regardless of the outcome from FromYAMLFile.
280+ // If no environment variables are set, configuration relies entirely on YAML.
281+ // Otherwise, environment variables can supplement, override YAML settings, or serve as the sole source.
282+ // FromEnv also includes validation, ensuring completeness after considering both sources.
283+ if err := FromEnv (v , options .EnvOptions ); err != nil {
284+ return errors .WithStack (err )
285+ }
286+
287+ return nil
288+ }
289+
162290// ParseFlags parses CLI flags and stores the result
163291// in the value pointed to by v. If v is nil or not a struct pointer,
164292// ParseFlags returns an [ErrInvalidArgument] error.
293+ //
165294// ParseFlags adds a default Help Options group,
166295// which contains the options -h and --help.
167296// If either option is specified on the command line,
168297// ParseFlags prints the help message to [os.Stdout] and exits.
298+ //
169299// Note that errors are not printed automatically,
170300// so error handling is the sole responsibility of the caller.
171301//
0 commit comments