6e0b32677a87708afcd6d85199ba2c6fa1a8f426
[arvados.git] / lib / config / load.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package config
6
7 import (
8         "bytes"
9         "encoding/json"
10         "errors"
11         "flag"
12         "fmt"
13         "io"
14         "io/ioutil"
15         "os"
16         "strings"
17
18         "git.curoverse.com/arvados.git/sdk/go/arvados"
19         "github.com/ghodss/yaml"
20         "github.com/imdario/mergo"
21         "github.com/sirupsen/logrus"
22 )
23
24 var ErrNoClustersDefined = errors.New("config does not define any clusters")
25
26 type Loader struct {
27         Stdin          io.Reader
28         Logger         logrus.FieldLogger
29         SkipDeprecated bool // Don't load legacy/deprecated config keys/files
30
31         Path          string
32         KeepstorePath string
33
34         configdata []byte
35 }
36
37 func NewLoader(stdin io.Reader, logger logrus.FieldLogger) *Loader {
38         ldr := &Loader{Stdin: stdin, Logger: logger}
39         ldr.SetupFlags(flag.NewFlagSet("", flag.ContinueOnError))
40         ldr.Path = "-"
41         return ldr
42 }
43
44 // SetupFlags configures a flagset so arguments like -config X can be
45 // used to change the loader's Path fields.
46 //
47 //      ldr := NewLoader(os.Stdin, logrus.New())
48 //      flagset := flag.NewFlagSet("", flag.ContinueOnError)
49 //      ldr.SetupFlags(flagset)
50 //      // ldr.Path == "/etc/arvados/config.yml"
51 //      flagset.Parse([]string{"-config", "/tmp/c.yaml"})
52 //      // ldr.Path == "/tmp/c.yaml"
53 func (ldr *Loader) SetupFlags(flagset *flag.FlagSet) {
54         flagset.StringVar(&ldr.Path, "config", arvados.DefaultConfigFile, "Site configuration `file` (default may be overridden by setting an ARVADOS_CONFIG environment variable)")
55         flagset.StringVar(&ldr.KeepstorePath, "legacy-keepstore-config", defaultKeepstoreConfigPath, "Legacy keepstore configuration `file`")
56 }
57
58 // MungeLegacyConfigArgs checks args for a -config flag whose argument
59 // is a regular file (or a symlink to one), but doesn't have a
60 // top-level "Clusters" key and therefore isn't a valid cluster
61 // configuration file. If it finds such a flag, it replaces -config
62 // with legacyConfigArg (e.g., "-legacy-keepstore-config").
63 //
64 // This is used by programs that still need to accept "-config" as a
65 // way to specify a per-component config file until their config has
66 // been migrated.
67 //
68 // If any errors are encountered while reading or parsing a config
69 // file, the given args are not munged. We presume the same errors
70 // will be encountered again and reported later on when trying to load
71 // cluster configuration from the same file, regardless of which
72 // struct we end up using.
73 func (ldr *Loader) MungeLegacyConfigArgs(lgr logrus.FieldLogger, args []string, legacyConfigArg string) []string {
74         munged := append([]string(nil), args...)
75         for i := 0; i < len(args); i++ {
76                 if !strings.HasPrefix(args[i], "-") || strings.SplitN(strings.TrimPrefix(args[i], "-"), "=", 2)[0] != "config" {
77                         continue
78                 }
79                 var operand string
80                 if strings.Contains(args[i], "=") {
81                         operand = strings.SplitN(args[i], "=", 2)[1]
82                 } else if i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") {
83                         i++
84                         operand = args[i]
85                 } else {
86                         continue
87                 }
88                 if fi, err := os.Stat(operand); err != nil || !fi.Mode().IsRegular() {
89                         continue
90                 }
91                 f, err := os.Open(operand)
92                 if err != nil {
93                         continue
94                 }
95                 defer f.Close()
96                 buf, err := ioutil.ReadAll(f)
97                 if err != nil {
98                         continue
99                 }
100                 var cfg arvados.Config
101                 err = yaml.Unmarshal(buf, &cfg)
102                 if err != nil {
103                         continue
104                 }
105                 if len(cfg.Clusters) == 0 {
106                         lgr.Warnf("%s is not a cluster config file -- interpreting %s as %s (please migrate your config!)", operand, "-config", legacyConfigArg)
107                         if operand == args[i] {
108                                 munged[i-1] = legacyConfigArg
109                         } else {
110                                 munged[i] = legacyConfigArg + "=" + operand
111                         }
112                 }
113         }
114         return munged
115 }
116
117 func (ldr *Loader) loadBytes(path string) ([]byte, error) {
118         if path == "-" {
119                 return ioutil.ReadAll(ldr.Stdin)
120         }
121         f, err := os.Open(path)
122         if err != nil {
123                 return nil, err
124         }
125         defer f.Close()
126         return ioutil.ReadAll(f)
127 }
128
129 func (ldr *Loader) Load() (*arvados.Config, error) {
130         if ldr.configdata == nil {
131                 buf, err := ldr.loadBytes(ldr.Path)
132                 if err != nil {
133                         return nil, err
134                 }
135                 ldr.configdata = buf
136         }
137
138         // Load the config into a dummy map to get the cluster ID
139         // keys, discarding the values; then set up defaults for each
140         // cluster ID; then load the real config on top of the
141         // defaults.
142         var dummy struct {
143                 Clusters map[string]struct{}
144         }
145         err := yaml.Unmarshal(ldr.configdata, &dummy)
146         if err != nil {
147                 return nil, err
148         }
149         if len(dummy.Clusters) == 0 {
150                 return nil, ErrNoClustersDefined
151         }
152
153         // We can't merge deep structs here; instead, we unmarshal the
154         // default & loaded config files into generic maps, merge
155         // those, and then json-encode+decode the result into the
156         // config struct type.
157         var merged map[string]interface{}
158         for id := range dummy.Clusters {
159                 var src map[string]interface{}
160                 err = yaml.Unmarshal(bytes.Replace(DefaultYAML, []byte(" xxxxx:"), []byte(" "+id+":"), -1), &src)
161                 if err != nil {
162                         return nil, fmt.Errorf("loading defaults for %s: %s", id, err)
163                 }
164                 err = mergo.Merge(&merged, src, mergo.WithOverride)
165                 if err != nil {
166                         return nil, fmt.Errorf("merging defaults for %s: %s", id, err)
167                 }
168         }
169         var src map[string]interface{}
170         err = yaml.Unmarshal(ldr.configdata, &src)
171         if err != nil {
172                 return nil, fmt.Errorf("loading config data: %s", err)
173         }
174         ldr.logExtraKeys(merged, src, "")
175         removeSampleKeys(merged)
176         err = mergo.Merge(&merged, src, mergo.WithOverride)
177         if err != nil {
178                 return nil, fmt.Errorf("merging config data: %s", err)
179         }
180
181         // map[string]interface{} => json => arvados.Config
182         var cfg arvados.Config
183         var errEnc error
184         pr, pw := io.Pipe()
185         go func() {
186                 errEnc = json.NewEncoder(pw).Encode(merged)
187                 pw.Close()
188         }()
189         err = json.NewDecoder(pr).Decode(&cfg)
190         if errEnc != nil {
191                 err = errEnc
192         }
193         if err != nil {
194                 return nil, fmt.Errorf("transcoding config data: %s", err)
195         }
196
197         if !ldr.SkipDeprecated {
198                 err = ldr.applyDeprecatedConfig(&cfg)
199                 if err != nil {
200                         return nil, err
201                 }
202                 for _, err := range []error{
203                         ldr.loadOldKeepstoreConfig(&cfg),
204                 } {
205                         if err != nil {
206                                 return nil, err
207                         }
208                 }
209         }
210
211         // Check for known mistakes
212         for id, cc := range cfg.Clusters {
213                 err = checkKeyConflict(fmt.Sprintf("Clusters.%s.PostgreSQL.Connection", id), cc.PostgreSQL.Connection)
214                 if err != nil {
215                         return nil, err
216                 }
217         }
218         return &cfg, nil
219 }
220
221 func checkKeyConflict(label string, m map[string]string) error {
222         saw := map[string]bool{}
223         for k := range m {
224                 k = strings.ToLower(k)
225                 if saw[k] {
226                         return fmt.Errorf("%s: multiple entries for %q (fix by using same capitalization as default/example file)", label, k)
227                 }
228                 saw[k] = true
229         }
230         return nil
231 }
232
233 func removeSampleKeys(m map[string]interface{}) {
234         delete(m, "SAMPLE")
235         for _, v := range m {
236                 if v, _ := v.(map[string]interface{}); v != nil {
237                         removeSampleKeys(v)
238                 }
239         }
240 }
241
242 func (ldr *Loader) logExtraKeys(expected, supplied map[string]interface{}, prefix string) {
243         if ldr.Logger == nil {
244                 return
245         }
246         allowed := map[string]interface{}{}
247         for k, v := range expected {
248                 allowed[strings.ToLower(k)] = v
249         }
250         for k, vsupp := range supplied {
251                 vexp, ok := allowed[strings.ToLower(k)]
252                 if !ok && expected["SAMPLE"] != nil {
253                         vexp = expected["SAMPLE"]
254                 } else if !ok {
255                         ldr.Logger.Warnf("deprecated or unknown config entry: %s%s", prefix, k)
256                         continue
257                 }
258                 if vsupp, ok := vsupp.(map[string]interface{}); !ok {
259                         // if vsupp is a map but vexp isn't map, this
260                         // will be caught elsewhere; see TestBadType.
261                         continue
262                 } else if vexp, ok := vexp.(map[string]interface{}); !ok {
263                         ldr.Logger.Warnf("unexpected object in config entry: %s%s", prefix, k)
264                 } else {
265                         ldr.logExtraKeys(vexp, vsupp, prefix+k+".")
266                 }
267         }
268 }