-
Notifications
You must be signed in to change notification settings - Fork 0
/
configset.go
164 lines (147 loc) · 4.54 KB
/
configset.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
package configset
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"github.com/spf13/afero"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
"sigs.k8s.io/yaml"
)
var cs configSet
// Load loads the config set from all *.yaml files under the given directory.
// If there are environment variables set such as CONFIGSET.{path}={value},
// the config set will be overwritten according to {paths} and {values}.
func Load(dirPath string) error { return cs.Load(afero.NewOsFs(), dirPath, os.Environ()) }
// MustLoad likes Load but panics when an error occurs.
func MustLoad(dirPath string) {
if err := Load(dirPath); err != nil {
panic(fmt.Sprintf("load config set: %v", err))
}
}
// ReadValue finds the value for the given path from the config set and
// unmarshals the given config from that value in form of JSON.
// If no value can be found by the path, ErrValueNotFound is returned.
func ReadValue(path string, config interface{}) error { return cs.ReadValue(path, config) }
// MustReadValue likes ReadValue but panics when an error occurs.
func MustReadValue(path string, config interface{}) {
if err := ReadValue(path, config); err != nil {
panic(fmt.Sprintf("read value: %v", err))
}
}
// Dump returns the config set in form of JSON.
func Dump(prefix string, indention string) json.RawMessage { return cs.Dump(prefix, indention) }
type configSet struct {
raw json.RawMessage
}
func (cs *configSet) Load(fs afero.Fs, dirPath string, environment []string) error {
raw, err := aggregateConfigs(fs, dirPath)
if err != nil {
return err
}
raw, err = overwriteConfigSet(raw, environment)
if err != nil {
return err
}
cs.raw = raw
return nil
}
func aggregateConfigs(fs afero.Fs, dirPath string) (json.RawMessage, error) {
fileInfoSet, err := afero.ReadDir(fs, dirPath)
if err != nil {
return nil, fmt.Errorf("read dir; dirPath=%q: %w", dirPath, err)
}
rawConfigs := make(map[string]json.RawMessage)
for _, fileInfo := range fileInfoSet {
if fileInfo.IsDir() {
continue
}
fileName := fileInfo.Name()
configName := strings.TrimSuffix(fileName, ".yaml")
if len(configName) == len(fileName) {
continue
}
filePath := filepath.Join(dirPath, fileName)
rawConfig, err := afero.ReadFile(fs, filePath)
if err != nil {
return nil, fmt.Errorf("read file; filePath=%q: %w", filePath, err)
}
rawConfig, err = yaml.YAMLToJSONStrict(rawConfig)
if err != nil {
return nil, fmt.Errorf("convert yaml to json; filePath=%q: %w", filePath, err)
}
rawConfigs[configName] = rawConfig
}
rawConfigSet, err := json.Marshal(rawConfigs)
if err != nil {
return nil, fmt.Errorf("marshal to json: %w", err)
}
return rawConfigSet, nil
}
func overwriteConfigSet(rawConfigSet json.RawMessage, environment []string) (json.RawMessage, error) {
kvs := extractKVs(environment)
for _, kv := range kvs {
key, value := kv[0], kv[1]
data, err := yaml.YAMLToJSONStrict([]byte(value))
if err != nil {
return nil, fmt.Errorf("convert yaml to json; key=%q value=%q: %w", key, value, err)
}
path := key[len(keyPrefix):]
rawConfigSet, err = sjson.SetRawBytesOptions(rawConfigSet, path, data, &sjson.Options{
Optimistic: true,
ReplaceInPlace: true,
})
if err != nil {
return nil, fmt.Errorf("set json value; path=%q: %w", path, err)
}
}
return rawConfigSet, nil
}
const keyPrefix = "CONFIGSET."
func extractKVs(environment []string) [][2]string {
var kvs [][2]string
for _, rawKV := range environment {
if !strings.HasPrefix(rawKV, keyPrefix) {
continue
}
i := strings.IndexByte(rawKV, '=')
if i < 0 {
continue
}
kv := [2]string{rawKV[:i], rawKV[i+1:]}
kvs = append(kvs, kv)
}
sort.Slice(kvs, func(i, j int) bool {
return kvs[i][0] < kvs[j][0]
})
return kvs
}
func (cs *configSet) ReadValue(path string, config interface{}) error {
value := gjson.GetBytes(cs.raw, path).Raw
if value == "" {
return fmt.Errorf("%w; path=%q", ErrValueNotFound, path)
}
if err := json.Unmarshal([]byte(value), config); err != nil {
return fmt.Errorf("unmarshal from json; path=%q configType=\"%T\": %w", path, config, err)
}
return nil
}
func (cs *configSet) Dump(prefix string, indention string) json.RawMessage {
if len(prefix)+len(indention) == 0 {
raw := make(json.RawMessage, len(cs.raw))
copy(raw, cs.raw)
return raw
}
var buffer bytes.Buffer
json.Indent(&buffer, cs.raw, prefix, indention)
buffer.WriteByte('\n')
raw := buffer.Bytes()
return raw
}
// ErrValueNotFound is returned when the JSON value does not exist.
var ErrValueNotFound = errors.New("configset: value not found")