Skip to content

Commit 47691c8

Browse files
committed
[config] migrate to koanf
1 parent 6837461 commit 47691c8

File tree

5 files changed

+541
-15
lines changed

5 files changed

+541
-15
lines changed

config/config.go

Lines changed: 134 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,152 @@
11
package config
22

33
import (
4+
"encoding/json"
45
"errors"
56
"fmt"
67
"os"
8+
"strings"
79

8-
"github.com/joho/godotenv"
9-
"github.com/kelseyhightower/envconfig"
10+
"github.com/knadh/koanf/parsers/dotenv"
11+
"github.com/knadh/koanf/parsers/yaml"
12+
"github.com/knadh/koanf/providers/env/v2"
13+
"github.com/knadh/koanf/providers/file"
14+
"github.com/knadh/koanf/providers/s3"
15+
"github.com/knadh/koanf/v2"
1016
)
1117

12-
func Load[T any](c *T) error {
13-
err := godotenv.Load()
18+
var ErrMissingAWSCreds = errors.New("AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION must be set")
19+
20+
// Load reads configuration from various sources and unmarshals it into a given struct.
21+
//
22+
// It looks for configuration in the following order (later overrides earlier):
23+
// 1. S3, if `WithS3YAML` is provided.
24+
// 2. Local file, if `WithLocalYAML` is provided.
25+
// 3. `.env` file in the current working directory.
26+
// 4. Environment variables.
27+
//
28+
// If any of the above sources result in an error (other than `os.ErrNotExist`), it will be returned.
29+
//
30+
// If a source results in `os.ErrNotExist`, it will be skipped.
31+
//
32+
// The final configuration will be unmarshaled into the given struct. If unmarshaling fails, an error will be returned.
33+
func Load[T any](c *T, opts ...Option) error {
34+
options := new(options)
35+
options.apply(opts...)
36+
37+
k := koanf.New(".")
38+
39+
if err := loadFromS3(options, k); err != nil {
40+
return err
41+
}
42+
43+
if err := loadFromYAML(options.withYaml, k); err != nil {
44+
return err
45+
}
46+
47+
if err := loadDotenv(k); err != nil {
48+
return err
49+
}
50+
51+
if err := loadEnv(k); err != nil {
52+
return err
53+
}
54+
55+
if err := k.Unmarshal("", c); err != nil {
56+
return fmt.Errorf("unmarshal: %w", err)
57+
}
58+
59+
return nil
60+
}
61+
62+
func loadFromS3(options *options, k *koanf.Koanf) error {
63+
if options.withS3Bucket == "" || options.withS3ObjectKey == "" {
64+
return nil
65+
}
66+
67+
accessKey := os.Getenv("AWS_ACCESS_KEY_ID")
68+
secretKey := os.Getenv("AWS_SECRET_ACCESS_KEY")
69+
region := os.Getenv("AWS_REGION")
70+
endpoint := os.Getenv("AWS_ENDPOINT")
71+
72+
if accessKey == "" || secretKey == "" || region == "" {
73+
return ErrMissingAWSCreds
74+
}
75+
76+
s3Config := s3.Config{
77+
AccessKey: accessKey,
78+
SecretKey: secretKey,
79+
Region: region,
80+
Bucket: options.withS3Bucket,
81+
ObjectKey: options.withS3ObjectKey,
82+
Endpoint: endpoint,
83+
}
84+
85+
err := k.Load(s3.Provider(s3Config), yaml.Parser())
86+
if err != nil && !strings.Contains(err.Error(), "404") {
87+
return fmt.Errorf("load s3: %w", err)
88+
}
89+
90+
return nil
91+
}
92+
93+
func loadFromYAML(path string, k *koanf.Koanf) error {
94+
if path == "" {
95+
return nil
96+
}
97+
98+
err := k.Load(file.Provider(path), yaml.Parser())
1499
if err != nil && !errors.Is(err, os.ErrNotExist) {
15-
return fmt.Errorf("failed to load .env file: %w", err)
100+
return fmt.Errorf("load yaml: %w", err)
16101
}
17102

18-
return loadFromEnv(c)
103+
return nil
19104
}
20105

21-
func loadFromEnv[T any](c *T) error {
22-
if err := envconfig.Process("", c); err != nil {
23-
return fmt.Errorf("failed to load envconfig: %w", err)
106+
func loadDotenv(k *koanf.Koanf) error {
107+
err := k.Load(file.Provider(".env"), dotenv.ParserEnvWithValue("", "__", envTransform))
108+
if err != nil && !errors.Is(err, os.ErrNotExist) {
109+
return fmt.Errorf("load dotenv: %w", err)
24110
}
25111

26112
return nil
27113
}
114+
115+
func loadEnv(k *koanf.Koanf) error {
116+
if err := k.Load(env.Provider("__", env.Opt{
117+
Prefix: "",
118+
TransformFunc: envTransform,
119+
EnvironFunc: nil,
120+
}), nil); err != nil {
121+
return fmt.Errorf("load env: %w", err)
122+
}
123+
124+
return nil
125+
}
126+
127+
func envTransform(k, v string) (string, any) {
128+
k = strings.ToLower(k)
129+
// JSON object -> map
130+
if strings.HasPrefix(v, "{") && strings.HasSuffix(v, "}") {
131+
var m map[string]any
132+
if err := json.Unmarshal([]byte(v), &m); err == nil {
133+
return k, m
134+
}
135+
}
136+
// JSON array -> []any
137+
if strings.HasPrefix(v, "[") && strings.HasSuffix(v, "]") {
138+
var a []any
139+
if err := json.Unmarshal([]byte(v), &a); err == nil {
140+
return k, a
141+
}
142+
}
143+
// CSV -> []string
144+
if strings.Contains(v, ",") {
145+
parts := strings.Split(v, ",")
146+
for i := range parts {
147+
parts[i] = strings.TrimSpace(parts[i])
148+
}
149+
return k, parts
150+
}
151+
return k, v
152+
}

0 commit comments

Comments
 (0)