Skip to content

Commit 55da82b

Browse files
committed
app: Add viper based configuration loader
1 parent de5f0fd commit 55da82b

File tree

1 file changed

+315
-0
lines changed

1 file changed

+315
-0
lines changed

internal/app/config.go

Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
package app
2+
3+
import (
4+
"net/url"
5+
"os"
6+
"strings"
7+
"time"
8+
9+
"github.com/jeremywohl/flatten"
10+
"github.com/metal-toolbox/alloy/internal/model"
11+
"github.com/mitchellh/mapstructure"
12+
"github.com/pkg/errors"
13+
"go.hollow.sh/toolbox/events"
14+
)
15+
16+
var (
17+
ErrConfig = errors.New("configuration error")
18+
)
19+
20+
const (
21+
DefaultCollectInterval = 72 * time.Hour
22+
DefaultCollectSplay = 4 * time.Hour
23+
)
24+
25+
// Configuration holds application configuration read from a YAML or set by env variables.
26+
//
27+
// nolint:govet // prefer readability over field alignment optimization for this case.
28+
type Configuration struct {
29+
// LogLevel is the app verbose logging level.
30+
// one of - info, debug, trace
31+
LogLevel string `mapstructure:"log_level"`
32+
33+
// AppKind is either inband or outofband
34+
AppKind model.AppKind `mapstructure:"app_kind"`
35+
36+
// StoreKind declares the type of storage repository that holds asset inventory data.
37+
StoreKind model.StoreKind `mapstructure:"store_kind"`
38+
39+
// CSV file path when StoreKind is set to csv.
40+
CsvFile string `mapstructure:"csv_file"`
41+
42+
// ServerserviceOptions defines the serverservice client configuration parameters
43+
//
44+
// This parameter is required when StoreKind is set to serverservice.
45+
ServerserviceOptions *ServerserviceOptions `mapstructure:"serverservice"`
46+
47+
// Controller Out of band collector concurrency
48+
Concurrency int `mapstructure:"concurrency"`
49+
50+
CollectInterval time.Duration `mapstructure:"collect_interval"`
51+
52+
CollectIntervalSplay time.Duration `mapstructure:"collect_interval_splay"`
53+
54+
// EventsBrokerKind indicates the kind of event broker configuration to enable,
55+
//
56+
// Supported parameter value - nats
57+
EventsBorkerKind string `mapstructure:"events_broker_kind"`
58+
59+
// NatsOptions defines the NATs events broker configuration parameters.
60+
//
61+
// This parameter is required when EventsBrokerKind is set to nats.
62+
NatsOptions *events.NatsOptions `mapstructure:"nats"`
63+
}
64+
65+
// ServerserviceOptions defines configuration for the Serverservice client.
66+
// https://github.com/metal-toolbox/hollow-serverservice
67+
type ServerserviceOptions struct {
68+
EndpointURL *url.URL
69+
FacilityCode string `mapstructure:"facility_code"`
70+
Endpoint string `mapstructure:"endpoint"`
71+
OidcIssuerEndpoint string `mapstructure:"oidc_issuer_endpoint"`
72+
OidcAudienceEndpoint string `mapstructure:"oidc_audience_endpoint"`
73+
OidcClientSecret string `mapstructure:"oidc_client_secret"`
74+
OidcClientID string `mapstructure:"oidc_client_id"`
75+
OidcClientScopes []string `mapstructure:"oidc_client_scopes"`
76+
DisableOAuth bool `mapstructure:"disable_oauth"`
77+
}
78+
79+
// LoadConfiguration loads application configuration
80+
//
81+
// Reads in the cfgFile when available and overrides from environment variables.
82+
func (a *App) LoadConfiguration(cfgFile string, storeKind model.StoreKind) error {
83+
a.v.SetConfigType("yaml")
84+
a.v.SetEnvPrefix(model.AppName)
85+
a.v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
86+
a.v.AutomaticEnv()
87+
88+
// these are initialized here so viper can read in configuration from env vars
89+
// once https://github.com/spf13/viper/pull/1429 is merged, this can go.
90+
a.Config.ServerserviceOptions = &ServerserviceOptions{}
91+
a.Config.NatsOptions = &events.NatsOptions{
92+
Stream: &events.NatsStreamOptions{},
93+
Consumer: &events.NatsConsumerOptions{},
94+
}
95+
96+
if cfgFile != "" {
97+
fh, err := os.Open(cfgFile)
98+
if err != nil {
99+
return errors.Wrap(ErrConfig, err.Error())
100+
}
101+
102+
if err = a.v.ReadConfig(fh); err != nil {
103+
return errors.Wrap(ErrConfig, "ReadConfig error:"+err.Error())
104+
}
105+
}
106+
107+
a.v.SetDefault("log.level", "info")
108+
a.v.SetDefault("collect.interval", DefaultCollectInterval)
109+
a.v.SetDefault("collect.interval.splay", DefaultCollectSplay)
110+
111+
if err := a.envBindVars(a.Config); err != nil {
112+
return errors.Wrap(ErrConfig, "env var bind error:"+err.Error())
113+
}
114+
115+
if err := a.v.Unmarshal(a.Config); err != nil {
116+
return errors.Wrap(ErrConfig, "Unmarshal error: "+err.Error())
117+
}
118+
119+
a.envVarAppOverrides()
120+
121+
if a.Config.EventsBorkerKind == "nats" {
122+
if err := a.envVarNatsOverrides(); err != nil {
123+
return errors.Wrap(ErrConfig, "nats env overrides error:"+err.Error())
124+
}
125+
}
126+
127+
if storeKind == model.StoreKindServerservice {
128+
if err := a.envVarServerserviceOverrides(); err != nil {
129+
return errors.Wrap(ErrConfig, "serverservice env overrides error:"+err.Error())
130+
}
131+
}
132+
133+
return nil
134+
}
135+
136+
func (a *App) envVarAppOverrides() {
137+
if a.v.GetString("log.level") != "" {
138+
a.Config.LogLevel = a.v.GetString("log.level")
139+
}
140+
141+
if a.v.GetDuration("collect.interval") != 0 {
142+
a.Config.CollectInterval = a.v.GetDuration("collect.interval")
143+
}
144+
145+
if a.v.GetDuration("collect.interval.splay") != 0 {
146+
a.Config.CollectIntervalSplay = a.v.GetDuration("collect.interval.splay")
147+
}
148+
149+
if a.v.GetString("csv.file") != "" {
150+
a.Config.CsvFile = a.v.GetString("csv.file")
151+
}
152+
}
153+
154+
// envBindVars binds environment variables to the struct
155+
// without a configuration file being unmarshalled,
156+
// this is a workaround for a viper bug,
157+
//
158+
// This can be replaced by the solution in https://github.com/spf13/viper/pull/1429
159+
// once that PR is merged.
160+
func (a *App) envBindVars(cfg *Configuration) error {
161+
envKeysMap := map[string]interface{}{}
162+
if err := mapstructure.Decode(a.Config, &envKeysMap); err != nil {
163+
return err
164+
}
165+
166+
// Flatten nested conf map
167+
flat, err := flatten.Flatten(envKeysMap, "", flatten.DotStyle)
168+
if err != nil {
169+
return errors.Wrap(err, "Unable to flatten config")
170+
}
171+
172+
for k := range flat {
173+
if err := a.v.BindEnv(k); err != nil {
174+
return errors.Wrap(ErrConfig, "env var bind error: "+err.Error())
175+
}
176+
}
177+
178+
return nil
179+
}
180+
181+
// NATs streaming configuration
182+
var (
183+
defaultNatsConnectTimeout = 100 * time.Millisecond
184+
)
185+
186+
// nolint:gocyclo // nats env config load is cyclomatic
187+
func (a *App) envVarNatsOverrides() error {
188+
if a.Config.NatsOptions == nil {
189+
a.Config.NatsOptions = &events.NatsOptions{}
190+
}
191+
192+
if a.v.GetString("nats.url") != "" {
193+
a.Config.NatsOptions.URL = a.v.GetString("nats.url")
194+
}
195+
196+
if a.Config.NatsOptions.URL == "" {
197+
return errors.New("missing parameter: nats.url")
198+
}
199+
200+
if a.v.GetString("nats.stream.user") != "" {
201+
a.Config.NatsOptions.StreamUser = a.v.GetString("nats.stream.user")
202+
}
203+
204+
if a.v.GetString("nats.stream.pass") != "" {
205+
a.Config.NatsOptions.StreamPass = a.v.GetString("nats.stream.pass")
206+
}
207+
208+
if a.v.GetString("nats.creds.file") != "" {
209+
a.Config.NatsOptions.CredsFile = a.v.GetString("nats.creds.file")
210+
}
211+
212+
if a.v.GetString("nats.stream.name") != "" {
213+
if a.Config.NatsOptions.Stream == nil {
214+
a.Config.NatsOptions.Stream = &events.NatsStreamOptions{}
215+
}
216+
217+
a.Config.NatsOptions.Stream.Name = a.v.GetString("nats.stream.name")
218+
}
219+
220+
if a.Config.NatsOptions.Stream.Name == "" {
221+
return errors.New("A stream name is required")
222+
}
223+
224+
if a.v.GetString("nats.consumer.name") != "" {
225+
if a.Config.NatsOptions.Consumer == nil {
226+
a.Config.NatsOptions.Consumer = &events.NatsConsumerOptions{}
227+
}
228+
229+
a.Config.NatsOptions.Consumer.Name = a.v.GetString("nats.consumer.name")
230+
}
231+
232+
if a.Config.NatsOptions.ConnectTimeout == 0 {
233+
a.Config.NatsOptions.ConnectTimeout = defaultNatsConnectTimeout
234+
}
235+
236+
return nil
237+
}
238+
239+
// Server service configuration options
240+
241+
// nolint:gocyclo // parameter validation is cyclomatic
242+
func (a *App) envVarServerserviceOverrides() error {
243+
if a.Config.ServerserviceOptions == nil {
244+
a.Config.ServerserviceOptions = &ServerserviceOptions{}
245+
}
246+
247+
if a.v.GetString("serverservice.endpoint") != "" {
248+
a.Config.ServerserviceOptions.Endpoint = a.v.GetString("serverservice.endpoint")
249+
}
250+
251+
if a.v.GetString("serverservice.facility.code") != "" {
252+
a.Config.ServerserviceOptions.FacilityCode = a.v.GetString("serverservice.facility.code")
253+
}
254+
255+
if a.Config.ServerserviceOptions.FacilityCode == "" {
256+
return errors.New("serverservice facility code not defined")
257+
}
258+
259+
endpointURL, err := url.Parse(a.Config.ServerserviceOptions.Endpoint)
260+
if err != nil {
261+
return errors.New("serverservice endpoint URL error: " + err.Error())
262+
}
263+
264+
a.Config.ServerserviceOptions.EndpointURL = endpointURL
265+
266+
if a.v.GetString("serverservice.disable.oauth") != "" {
267+
a.Config.ServerserviceOptions.DisableOAuth = a.v.GetBool("serverservice.disable.oauth")
268+
}
269+
270+
if a.Config.ServerserviceOptions.DisableOAuth {
271+
return nil
272+
}
273+
274+
if a.v.GetString("serverservice.oidc.issuer.endpoint") != "" {
275+
a.Config.ServerserviceOptions.OidcIssuerEndpoint = a.v.GetString("serverservice.oidc.issuer.endpoint")
276+
}
277+
278+
if a.Config.ServerserviceOptions.OidcIssuerEndpoint == "" {
279+
return errors.New("serverservice oidc.issuer.endpoint not defined")
280+
}
281+
282+
if a.v.GetString("serverservice.oidc.audience.endpoint") != "" {
283+
a.Config.ServerserviceOptions.OidcAudienceEndpoint = a.v.GetString("serverservice.oidc.audience.endpoint")
284+
}
285+
286+
if a.Config.ServerserviceOptions.OidcAudienceEndpoint == "" {
287+
return errors.New("serverservice oidc.audience.endpoint not defined")
288+
}
289+
290+
if a.v.GetString("serverservice.oidc.client.secret") != "" {
291+
a.Config.ServerserviceOptions.OidcClientSecret = a.v.GetString("serverservice.oidc.client.secret")
292+
}
293+
294+
if a.Config.ServerserviceOptions.OidcClientSecret == "" {
295+
return errors.New("serverservice.oidc.client.secret not defined")
296+
}
297+
298+
if a.v.GetString("serverservice.oidc.client.id") != "" {
299+
a.Config.ServerserviceOptions.OidcClientID = a.v.GetString("serverservice.oidc.client.id")
300+
}
301+
302+
if a.Config.ServerserviceOptions.OidcClientID == "" {
303+
return errors.New("serverservice.oidc.client.id not defined")
304+
}
305+
306+
if a.v.GetString("serverservice.oidc.client.scopes") != "" {
307+
a.Config.ServerserviceOptions.OidcClientScopes = a.v.GetStringSlice("serverservice.oidc.client.scopes")
308+
}
309+
310+
if len(a.Config.ServerserviceOptions.OidcClientScopes) == 0 {
311+
return errors.New("serverservice oidc.client.scopes not defined")
312+
}
313+
314+
return nil
315+
}

0 commit comments

Comments
 (0)