/
config.go
318 lines (258 loc) · 9.48 KB
/
config.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
package conf
/*
This is a package that wraps viper, a package designed to handle configuration
files, for BCDA and BCDAWORKER. This package will go through a number of stages.
1. Local env looks primarily at conf package for variables, but will also look
in the environment for any variables it is not tracking. PROD/TEST/DEV will
only look in the environment. (WE ARE HERE NOW)
2. Local env will only look at conf package. PROD/TEST/DEV still the same.
3. All env will look at the conf package.
4. Make conf package less ubiquitous by implementing some solution that removes
all the repeated calls to the conf package for information.
Assumptions:
1. The configuration file is a env file (can be changed if needed).
2. The configuration variables, once it has been ingested by the conf package,
will stay immutable during the uptime of the application (exception is test).
*/
import (
"errors"
"fmt"
"go/build"
"os"
"reflect"
"strings"
"testing"
"github.com/mitchellh/mapstructure"
log "github.com/sirupsen/logrus"
"github.com/spf13/viper"
)
// nullValue allows us to "unset" a parameter within the viper config
// we need to set a non-nil value when using the Set function to ensure
// that the value is returned.
type nullValue struct{}
var null = nullValue{}
// Private global variable:
// config is a conf package struct that wraps the viper struct with one other field
type config struct {
viper.Viper
}
// envVars is an uninitialized config struct at this point, and it is private.
// It is initialized by the init func when the configuration file is found.
var envVars config
// Implementing a state machine that tracks the status of the configuration loading.
// This state machine should go away when in stage 3.
type configStatus uint8
const (
configGood configStatus = 0
configBad configStatus = 1
noConfigFound configStatus = 2
structtag = "conf"
defaulttag = structtag + "_default"
)
// if configuration file found and loaded, state doesn't changed
var state configStatus = configGood
// init: First thing to run when this package is loaded by the binary.
// Even if multiple packages import conf, this will be called and ran ONLY once.
func init() {
apiConfigPath := os.Getenv("BCDA_API_CONFIG_PATH")
workerConfigPath := os.Getenv("BCDA_WORKER_CONFIG_PATH")
env := os.Getenv("DEPLOYMENT_TARGET")
gopath := os.Getenv("GOPATH")
if gopath == "" {
gopath = build.Default.GOPATH
}
// might be better suited to pull the config file path from Docker, then we can use
// a test-specific file in the future instead of using 'fakes' in the same dir as the real ones
envPath := gopath + fmt.Sprintf("/src/github.com/CMSgov/bcda-app/conf/configs/%s.env", env)
configPaths := configPaths(envPath, apiConfigPath, workerConfigPath)
// TODO: if a config file is not found, this should panic. Because of the use of
// init(), this cannot be done because it breaks tests. We should load in the configs
// in main.go for the worker and api instead.
envVars, state = loadConfigs(configPaths...)
}
// This is the private helper function that sets up viper. This function is
// called by the init() function only once during initialization of the package.
func loadConfigs(locations ...string) (config, configStatus) {
status := noConfigFound
var v = viper.New()
v.AutomaticEnv()
// Allows environment variables explicitly set to
// an empty string and be considered as valid
v.AllowEmptyEnv(true)
for _, loc := range locations {
if _, err := os.Stat(loc); err == nil {
v.SetConfigFile(loc)
if err := v.MergeInConfig(); err != nil {
log.Warnf("Failed to read in config from %s %s", loc, err.Error())
if status != configGood {
status = configBad
}
} else {
log.Debugf("Successfully loaded config from %s.", loc)
status = configGood
}
}
}
return config{*v}, status
}
func configPaths(envFilePath string, apiPath string, workerPath string) (configPaths []string) {
configPaths = []string{}
if apiPath != "" {
configPaths = append(configPaths, apiPath)
}
if workerPath != "" {
configPaths = append(configPaths, workerPath)
}
configPaths = append(configPaths, envFilePath)
return configPaths
}
/*
findEnv is a helper function that will determine what environment the application
is running in: local or PROD/TEST/DEV env. Each environment should have a distinct
path where the configuration file is located. First it check the local path,
then the PROD/DEV/TEST. If both not found, defaults to just using environment vars.
Later iterations of this package will phase out this "defaulting" behavior.
*/
func findEnv(location []string) (bool, string) {
for _, el := range location {
// Check if the configuration file exists
if _, err := os.Stat(el + "/local.env"); err == nil {
return true, el
}
}
return false, ""
}
// GetEnv() is a public function that retrieves values stored in conf. If it does not
// exist, an empty string (i.e., "") is returned.
// This function will be phased out in later versions of the package.
func GetEnv(key string) string {
val, _ := LookupEnv(key)
return val
}
// LookupEnv is a public function, like GetEnv, designed to replace os.LookupEnv() in code-base.
// This function will most likely become a private function in later versions of the package.
func LookupEnv(key string) (string, bool) {
if state == configGood {
value := envVars.Get(key)
if value == nil {
return "", false
} else if _, ok := value.(nullValue); ok {
// key was explicitly unset via UnsetEnv
return "", false
} else {
return value.(string), true
}
}
return os.LookupEnv(key)
}
// SetEnv is a public function that adds key values into conf. This function should only be used
// either in this package itself or testing. Protect parameter is type *testing.T, and it's there
// to ensure developers knowingly use it in the appropriate circumstances.
// This function will most likely become a private function in later versions of the package.
func SetEnv(protect *testing.T, key string, value string) error {
var err error
// If configuration file is good, change key value in conf struct
if state == configGood {
envVars.Set(key, value) // This doesn't return anything...
} else {
// Configuration file is bad, change the EV
err = os.Setenv(key, value)
}
return err
}
// UnsetEnv is a public function that "unsets" a variable. Like SetEnv, this should only be used
// either in this package itself or testing.
// This function will most likely become a private function in later versions of the package.
func UnsetEnv(protect *testing.T, key string) error {
// If configuration file is good
if state == configGood {
envVars.Set(key, null)
}
// Unset environment variable too to ensure that viper does not attempt
// to retrieve it from the os.
return os.Unsetenv(key)
}
/***********************************************************************************************
CODE BELOW THIS BLOCK IS EXPERIMENTAL AND ONLY BEING USED INTERNALLY BY CONF PACKAGE
***********************************************************************************************/
// Gopath function exposes the gopath of the application while keeping it immutable. Golang does
// not allow const to be strings, and a var would make it mutable if made public. This could be
// something the config / viper struct keeps track. For now it's separate.
//func Gopath() string {
//return envVars.gopath
//}
/*
Checkout function takes a reference to a struct or a slice of string. It will traverse both
data structures and look up key value pairs in the conf / viper struct by the name of the field
for structs and string values for the slice. The function works with pointers so no value is
returned. An error is returned if the wrong data structure is provided.
*/
func Checkout(v interface{}) error {
// Check if the data type provided is supported
switch v := v.(type) {
// If it's a slice of strings
case []string:
for n, key := range v {
if val, exists := LookupEnv(key); exists {
v[n] = val
} else {
v[n] = ""
}
}
return nil
// Checking the rest of data types through reflection
default:
// Get the concrete value from the interface
check := reflect.ValueOf(v)
// Is it a pointer?
if check.Kind() == reflect.Ptr {
// Dereference the pointer
el := check.Elem()
// Is the data type a struct?
if el.Kind() == reflect.Struct {
if err := bindenvs(el); err != nil {
return fmt.Errorf("failed to bind env vars to viper struct: %w", err)
}
return envVars.Unmarshal(v, func(dc *mapstructure.DecoderConfig) { dc.TagName = structtag })
}
}
}
return errors.New("The data type provided to Checkout func is not supported.")
}
// bindenv: workaround to make the unmarshal work with environment variables
// Inspired from solution found here : https://github.com/spf13/viper/issues/188#issuecomment-399884438
func bindenvs(field reflect.Value, parts ...string) error {
if field.Kind() == reflect.Ptr {
return nil
}
for i := 0; i < field.NumField(); i++ {
v := field.Field(i)
t := field.Type().Field(i)
tv, ok := t.Tag.Lookup(structtag)
if !ok {
continue
}
dv, hasDefault := t.Tag.Lookup(defaulttag)
if tv == ",squash" {
if err := bindenvs(v, parts...); err != nil {
return err
}
continue
}
var err error
switch v.Kind() {
case reflect.Struct:
err = bindenvs(v, append(parts, tv)...)
default:
key := strings.Join(append(parts, tv), ".")
err = envVars.BindEnv(key)
if hasDefault {
envVars.SetDefault(key, dv)
}
}
if err != nil {
return err
}
}
return nil
}