@@ -55,6 +55,76 @@ func Load(configFileName string, flagSet *pflag.FlagSet, into interface{}) error
5555 return nil
5656}
5757
58+// LoadYAML will load a YAML based configuration file into the options interface provided.
59+func LoadYAML(configFileName string, opts interface{}) error {
60+ buffer, err := loadAndSubstituteEnvs(configFileName)
61+ if err != nil {
62+ return err
63+ }
64+
65+ // Generic interface for loading arbitrary yaml structure
66+ var intermediate map[string]interface{}
67+
68+ if err := yaml.Unmarshal(buffer, &intermediate); err != nil {
69+ return fmt.Errorf("error unmarshalling config: %w", err)
70+ }
71+
72+ return Decode(intermediate, opts)
73+}
74+
75+func Decode(input interface{}, result interface{}) error {
76+ // Using mapstructure to decode arbitrary yaml structure into options and
77+ // merge with existing values instead of overwriting everything. This is especially
78+ // important as we have a lot of default values for boolean which are supposed to be
79+ // true by default. Normally by just parsing through yaml all booleans that aren't
80+ // referenced in the config file would be parsed as false and we cannot identify after
81+ // the fact if they have been explicitly set to false or have not been referenced.
82+ decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
83+ DecodeHook: mapstructure.ComposeDecodeHookFunc(toDurationHookFunc()),
84+ Metadata: nil, // Don't track any metadata
85+ Result: result, // Decode the result into the prefilled options
86+ TagName: "json", // Parse all fields that use the yaml tag
87+ ZeroFields: false, // Don't clean the default values from the result map (options)
88+ ErrorUnused: true, // Throw an error if keys have been used that aren't mapped to any struct fields
89+ IgnoreUntaggedFields: true, // Ignore fields in structures that aren't tagged with yaml
90+ })
91+
92+ if err != nil {
93+ return fmt.Errorf("error creating decoder for config: %w", err)
94+ }
95+
96+ if err := decoder.Decode(input); err != nil {
97+ return fmt.Errorf("error decoding config: %w", err)
98+ }
99+
100+ return nil
101+}
102+
103+// loadAndSubstituteEnvs reads the yaml config into a generic byte buffer and
104+// substitute env references
105+func loadAndSubstituteEnvs(configFileName string) ([]byte, error) {
106+ if configFileName == "" {
107+ return nil, errors.New("no configuration file provided")
108+ }
109+
110+ unparsedBuffer, err := os.ReadFile(configFileName)
111+ if err != nil {
112+ return nil, fmt.Errorf("unable to load config file: %w", err)
113+ }
114+
115+ modifiedBuffer, err := normalizeSubstitution(unparsedBuffer)
116+ if err != nil {
117+ return nil, fmt.Errorf("error normalizing substitution string : %w", err)
118+ }
119+
120+ buffer, err := envsubst.Bytes(modifiedBuffer)
121+ if err != nil {
122+ return nil, fmt.Errorf("error in substituting env variables : %w", err)
123+ }
124+
125+ return buffer, nil
126+}
127+
58128// registerFlags uses `cfg` and `flag` tags to associate flags in the flagSet
59129// to the fields in the options interface provided.
60130// Each exported field in the options must have a `cfg` tag otherwise an error will occur.
@@ -140,47 +210,6 @@ func isUnexported(name string) bool {
140210 return first == strings.ToLower(first)
141211}
142212
143−// LoadYAML will load a YAML based configuration file into the options interface provided.
144−func LoadYAML(configFileName string, into interface{}) error {
145− buffer, err := loadAndParseYaml(configFileName)
146− if err != nil {
147− return err
148− }
149−
150− // UnmarshalStrict will return an error if the config includes options that are
151− // not mapped to fields of the into struct
152− if err := yaml.UnmarshalStrict(buffer, into, yaml.DisallowUnknownFields); err != nil {
153− return fmt.Errorf("error unmarshalling config: %w", err)
154− }
155−
156− return nil
157−}
158−
159−// loadAndParseYaml reads the config from the filesystem and
160−// execute the environment variable substitution
161−func loadAndParseYaml(configFileName string) ([]byte, error) {
162− if configFileName == "" {
163− return nil, errors.New("no configuration file provided")
164− }
165−
166− unparsedBuffer, err := os.ReadFile(configFileName)
167− if err != nil {
168− return nil, fmt.Errorf("unable to load config file: %w", err)
169− }
170−
171− modifiedBuffer, err := normalizeSubstitution(unparsedBuffer)
172− if err != nil {
173− return nil, fmt.Errorf("error normalizing substitution string : %w", err)
174− }
175−
176− buffer, err := envsubst.Bytes(modifiedBuffer)
177− if err != nil {
178− return nil, fmt.Errorf("error in substituting env variables : %w", err)
179− }
180−
181− return buffer, nil
182−}
183−
184213// normalizeSubstitution normalizes dollar signs ($) with numerals like
185214// $1 or $2 properly by correctly escaping them
186215func normalizeSubstitution(unparsedBuffer []byte) ([]byte, error) {