1 package env2config
2
3 import (
4 "os"
5 "path/filepath"
6 "sort"
7 "strconv"
8 "strings"
9 "unicode"
10
11 "github.com/kelseyhightower/envconfig"
12 "github.com/pkg/errors"
13 )
14
15 type Config struct {
16 Name string
17 Opts Opts
18 Values Values
19
20 registry *registry
21 }
22
23 type Opts struct {
24 File string `required:"true"`
25 Format string `required:"true"`
26 TemplateFile string `split_words:"true"`
27 TemplateDeleteKeys []string `split_words:"true"`
28
29 Inputs Values
30 }
31
32 type Values map[string]string
33
34 var _ envconfig.Setter = Values{}
35
36 func (v Values) Set(value string) error { return nil }
37
38 func New(name string) (Config, error) {
39 c, err := newConfig(name, parseEnv(os.Environ()), defaultRegistry)
40 if name != "" {
41 err = errors.Wrap(err, strings.ToLower(name))
42 }
43 return c, err
44 }
45
46 func newConfig(name string, env map[string]string, registry *registry) (Config, error) {
47 name = strings.ToLower(name)
48 if name == "" {
49 return Config{}, errors.New("Config name is required")
50 }
51 trimmed := name
52 trimmed = strings.TrimFunc(trimmed, unicode.IsLetter)
53 trimmed = strings.TrimFunc(trimmed, unicode.IsNumber)
54 if trimmed != "" {
55 return Config{}, errors.Errorf("Config names must only use letters or numbers: %q", name)
56 }
57 config := Config{
58 registry: registry,
59 }
60 err := envconfig.Process(name, &config)
61 if err != nil {
62 return Config{}, err
63 }
64 config.Name = name
65 config.Opts.Inputs = filterEnvPrefix(name+"_opts_in", env)
66 config.Values = configEnvValues(name, env)
67
68 var missingInputs []string
69 for dest, src := range config.Opts.Inputs {
70 value, isSet := env[src]
71 if !isSet {
72 missingInputs = append(missingInputs, src)
73 }
74 config.Values[dest] = value
75 }
76 if len(missingInputs) > 0 {
77 sort.Strings(missingInputs)
78 return Config{}, errors.Errorf("Missing required environment variables: %s", strings.Join(missingInputs, ", "))
79 }
80 return config, nil
81 }
82
83 func (c Config) Write() error {
84 var template map[string]interface{}
85 if c.Opts.TemplateFile != "" {
86 err := os.MkdirAll(filepath.Dir(c.Opts.TemplateFile), 0755)
87 if err != nil {
88 return err
89 }
90 f, err := os.Open(c.Opts.TemplateFile)
91 if err != nil {
92 return err
93 }
94 defer f.Close()
95 err = c.registry.UnmarshalFormat(c.Opts.Format, f, &template)
96 if err != nil {
97 return err
98 }
99 sortTemplateDeleteKeys(c.Opts.TemplateDeleteKeys)
100 for _, deleteKey := range c.Opts.TemplateDeleteKeys {
101 templateInt, _ := deleteKeyPath(template, parseKeyPath(deleteKey))
102 template = templateInt.(map[string]interface{})
103 }
104 }
105 values := c.writableValues(template)
106 f, err := os.OpenFile(c.Opts.File, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
107 if err != nil {
108 return err
109 }
110 defer f.Close()
111 return c.registry.MarshalFormat(c.Opts.Format, f, values)
112 }
113
114 func (c Config) writableValues(template map[string]interface{}) interface{} {
115 result := template
116 if result == nil {
117 result = make(map[string]interface{})
118 }
119 for key, value := range c.Values {
120 current := result
121 keys := parseKeyPath(key)
122 for i := 0; i < len(keys)-1; i++ {
123 key := keys[i]
124 _, exists := current[key]
125 if !exists {
126 current[key] = make(map[string]interface{})
127 }
128 switch next := current[key].(type) {
129 case map[string]interface{}:
130 current = next
131 case []interface{}:
132 nextMap := arrayToMap(next)
133 current[key] = nextMap
134 current = nextMap
135 default:
136
137 nextMap := make(map[string]interface{})
138 current[key] = nextMap
139 current = nextMap
140 }
141 }
142 lastKey := keys[len(keys)-1]
143 current[lastKey] = value
144 }
145
146 return mapsToArrays(result)
147 }
148
149 func mapsToArrays(m map[string]interface{}) interface{} {
150 isArray := true
151 for key, value := range m {
152 if mapValue, isMap := value.(map[string]interface{}); isMap {
153 m[key] = mapsToArrays(mapValue)
154 }
155 if strings.TrimFunc(key, unicode.IsNumber) != "" {
156 isArray = false
157 }
158 }
159 if !isArray {
160 return m
161 }
162
163
164 values := make([]interface{}, len(m))
165 for key, value := range m {
166 index, err := strconv.ParseInt(key, 10, 64)
167 if err != nil {
168 panic(err)
169 }
170 values[index] = value
171 }
172 return values
173 }
174
175 func arrayToMap(a []interface{}) map[string]interface{} {
176 m := make(map[string]interface{}, len(a))
177 for index, value := range a {
178 m[strconv.FormatInt(int64(index), 10)] = value
179 }
180 return m
181 }
182