Merge branch 'master' into 14930-arvput-trash-at
[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         "fmt"
12         "io"
13         "io/ioutil"
14         "os"
15         "strings"
16
17         "git.curoverse.com/arvados.git/sdk/go/arvados"
18         "github.com/ghodss/yaml"
19         "github.com/imdario/mergo"
20 )
21
22 type logger interface {
23         Warnf(string, ...interface{})
24 }
25
26 func LoadFile(path string, log logger) (*arvados.Config, error) {
27         f, err := os.Open(path)
28         if err != nil {
29                 return nil, err
30         }
31         defer f.Close()
32         return Load(f, log)
33 }
34
35 func Load(rdr io.Reader, log logger) (*arvados.Config, error) {
36         return load(rdr, log, true)
37 }
38
39 func load(rdr io.Reader, log logger, useDeprecated bool) (*arvados.Config, error) {
40         buf, err := ioutil.ReadAll(rdr)
41         if err != nil {
42                 return nil, err
43         }
44
45         // Load the config into a dummy map to get the cluster ID
46         // keys, discarding the values; then set up defaults for each
47         // cluster ID; then load the real config on top of the
48         // defaults.
49         var dummy struct {
50                 Clusters map[string]struct{}
51         }
52         err = yaml.Unmarshal(buf, &dummy)
53         if err != nil {
54                 return nil, err
55         }
56         if len(dummy.Clusters) == 0 {
57                 return nil, errors.New("config does not define any clusters")
58         }
59
60         // We can't merge deep structs here; instead, we unmarshal the
61         // default & loaded config files into generic maps, merge
62         // those, and then json-encode+decode the result into the
63         // config struct type.
64         var merged map[string]interface{}
65         for id := range dummy.Clusters {
66                 var src map[string]interface{}
67                 err = yaml.Unmarshal(bytes.Replace(DefaultYAML, []byte(" xxxxx:"), []byte(" "+id+":"), -1), &src)
68                 if err != nil {
69                         return nil, fmt.Errorf("loading defaults for %s: %s", id, err)
70                 }
71                 err = mergo.Merge(&merged, src, mergo.WithOverride)
72                 if err != nil {
73                         return nil, fmt.Errorf("merging defaults for %s: %s", id, err)
74                 }
75         }
76         var src map[string]interface{}
77         err = yaml.Unmarshal(buf, &src)
78         if err != nil {
79                 return nil, fmt.Errorf("loading config data: %s", err)
80         }
81         logExtraKeys(log, merged, src, "")
82         removeSampleKeys(merged)
83         err = mergo.Merge(&merged, src, mergo.WithOverride)
84         if err != nil {
85                 return nil, fmt.Errorf("merging config data: %s", err)
86         }
87
88         // map[string]interface{} => json => arvados.Config
89         var cfg arvados.Config
90         var errEnc error
91         pr, pw := io.Pipe()
92         go func() {
93                 errEnc = json.NewEncoder(pw).Encode(merged)
94                 pw.Close()
95         }()
96         err = json.NewDecoder(pr).Decode(&cfg)
97         if errEnc != nil {
98                 err = errEnc
99         }
100         if err != nil {
101                 return nil, fmt.Errorf("transcoding config data: %s", err)
102         }
103
104         if useDeprecated {
105                 err = applyDeprecatedConfig(&cfg, buf, log)
106                 if err != nil {
107                         return nil, err
108                 }
109         }
110
111         // Check for known mistakes
112         for id, cc := range cfg.Clusters {
113                 err = checkKeyConflict(fmt.Sprintf("Clusters.%s.PostgreSQL.Connection", id), cc.PostgreSQL.Connection)
114                 if err != nil {
115                         return nil, err
116                 }
117         }
118         return &cfg, nil
119 }
120
121 func checkKeyConflict(label string, m map[string]string) error {
122         saw := map[string]bool{}
123         for k := range m {
124                 k = strings.ToLower(k)
125                 if saw[k] {
126                         return fmt.Errorf("%s: multiple entries for %q (fix by using same capitalization as default/example file)", label, k)
127                 }
128                 saw[k] = true
129         }
130         return nil
131 }
132
133 func removeSampleKeys(m map[string]interface{}) {
134         delete(m, "SAMPLE")
135         for _, v := range m {
136                 if v, _ := v.(map[string]interface{}); v != nil {
137                         removeSampleKeys(v)
138                 }
139         }
140 }
141
142 func logExtraKeys(log logger, expected, supplied map[string]interface{}, prefix string) {
143         if log == nil {
144                 return
145         }
146         allowed := map[string]interface{}{}
147         for k, v := range expected {
148                 allowed[strings.ToLower(k)] = v
149         }
150         for k, vsupp := range supplied {
151                 vexp, ok := allowed[strings.ToLower(k)]
152                 if !ok && expected["SAMPLE"] != nil {
153                         vexp = expected["SAMPLE"]
154                 } else if !ok {
155                         log.Warnf("deprecated or unknown config entry: %s%s", prefix, k)
156                         continue
157                 }
158                 if vsupp, ok := vsupp.(map[string]interface{}); !ok {
159                         // if vsupp is a map but vexp isn't map, this
160                         // will be caught elsewhere; see TestBadType.
161                         continue
162                 } else if vexp, ok := vexp.(map[string]interface{}); !ok {
163                         log.Warnf("unexpected object in config entry: %s%s", prefix, k)
164                 } else {
165                         logExtraKeys(log, vexp, vsupp, prefix+k+".")
166                 }
167         }
168 }