-
Notifications
You must be signed in to change notification settings - Fork 5
/
config.go
190 lines (156 loc) · 4.37 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
package internal
import (
"fmt"
"os"
"path"
"reflect"
"runtime"
"strconv"
"strings"
"sync"
)
var (
configLock = &sync.Mutex{}
// Config returns the app global config initialized from environment variables.
// [Read] locks not needed if there are no writes involved. Config is only populated at startup so there won't be any more writes.
Config *AppConfig
)
type OIDCConfig struct {
ClientID string `env:"OIDC_CLIENT_ID"`
ClientSecret string `env:"OIDC_CLIENT_SECRET"`
Issuer string `env:"OIDC_ISSUER"`
Scopes string `env:"OIDC_SCOPES"`
Domain string `env:"OIDC_DOMAIN"`
}
type PostgresConfig struct {
// Port represents the db port to use in the application, depending on setup (dockerized or not).
Port int `env:"DB_PORT"`
User string `env:"POSTGRES_USER"`
Password string `env:"POSTGRES_PASSWORD"`
Server string `env:"POSTGRES_SERVER"`
DB string `env:"POSTGRES_DB"`
TraceEnabled bool `env:"POSTGRES_TRACE,false"`
}
type RedisConfig struct {
DB int `env:"REDIS_DB"`
Host string `env:"REDIS_HOST"`
}
type SuperAdminConfig struct {
DefaultEmail string `env:"DEFAULT_SUPERADMIN_EMAIL"`
}
// AppConfig contains app settings.
type AppConfig struct {
Postgres PostgresConfig
Redis RedisConfig
OIDC OIDCConfig
SuperAdmin SuperAdminConfig
Domain string `env:"DOMAIN"`
APIPort string `env:"API_PORT"`
APIVersion string `env:"API_VERSION"`
APIPrefix string `env:"API_PREFIX"`
AppEnv string `env:"APP_ENV"`
SigningKey string `env:"SIGNING_KEY"`
BuildVersion string `env:"BUILD_VERSION"`
}
// NewAppConfig initializes app config from current environment variables.
// config can be replaced with subsequent calls.
func NewAppConfig() error {
configLock.Lock()
defer configLock.Unlock()
cfg := &AppConfig{}
if err := loadEnvToConfig(cfg); err != nil {
return fmt.Errorf("loadEnvToConfig: %w", err)
}
Config = cfg
return nil
}
// loadEnvToConfig loads env vars to a given struct based on an `env` tag.
func loadEnvToConfig(config any) error {
cfg := reflect.ValueOf(config)
if cfg.Kind() == reflect.Pointer {
cfg = cfg.Elem()
}
for idx := 0; idx < cfg.NumField(); idx++ {
fType := cfg.Type().Field(idx)
fld := cfg.Field(idx)
if fld.Kind() == reflect.Struct {
if !fld.CanInterface() { // unexported
continue
}
if err := loadEnvToConfig(fld.Addr().Interface()); err != nil {
return fmt.Errorf("nested struct %q env loading: %w", cfg.Type().Field(idx).Name, err)
}
}
if !fld.CanSet() {
continue
}
if env, ok := fType.Tag.Lookup("env"); ok && len(env) > 0 {
err := setEnvToField(env, fld)
if err != nil {
return fmt.Errorf("could not set %q to %q: %w", env, cfg.Type().Field(idx).Name, err)
}
}
}
return nil
}
func splitEnvTag(s string) (string, string) {
x := strings.Split(s, ",")
if len(x) == 1 {
return x[0], ""
}
return x[0], x[1]
}
func setEnvToField(envTag string, field reflect.Value) error {
envvar, defaultVal := splitEnvTag(envTag)
val, present := os.LookupEnv(envvar)
if !present && field.Kind() != reflect.Pointer && defaultVal == "" {
return fmt.Errorf("%s is not set but required", envvar)
}
if !present && field.Kind() != reflect.Pointer && defaultVal != "" {
val = defaultVal
}
var isPtr bool
kind := field.Kind()
if kind == reflect.Pointer {
kind = field.Type().Elem().Kind()
isPtr = true
}
if val == "" && isPtr && kind != reflect.String {
return nil
}
switch kind {
case reflect.String:
if !present && isPtr {
setVal[*string](false, field, nil) // since default val is always ""
return nil
}
setVal(isPtr, field, val)
case reflect.Int:
v, err := strconv.Atoi(val)
if err != nil {
return fmt.Errorf("could not convert %s to int: %w", envvar, err)
}
setVal(isPtr, field, v)
case reflect.Bool:
v, err := strconv.ParseBool(val)
if err != nil {
return fmt.Errorf("could not convert %s to bool: %w", envvar, err)
}
setVal(isPtr, field, v)
default:
return fmt.Errorf("unsupported type for env tag %q: %T", envvar, field.Interface())
}
return nil
}
func setVal[T any](isPtr bool, field reflect.Value, v T) {
if isPtr {
field.Set(reflect.ValueOf(&v))
} else {
field.Set(reflect.ValueOf(v))
}
}
// Returns the directory of the file this function lives in.
func getFileRuntimeDirectory() string {
_, b, _, _ := runtime.Caller(0)
return path.Join(path.Dir(b))
}