Merge branch '15227-apiserver-properties-bugfix'
[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         err = mergo.Merge(&merged, src, mergo.WithOverride)
83         if err != nil {
84                 return nil, fmt.Errorf("merging config data: %s", err)
85         }
86
87         // map[string]interface{} => json => arvados.Config
88         var cfg arvados.Config
89         var errEnc error
90         pr, pw := io.Pipe()
91         go func() {
92                 errEnc = json.NewEncoder(pw).Encode(merged)
93                 pw.Close()
94         }()
95         err = json.NewDecoder(pr).Decode(&cfg)
96         if errEnc != nil {
97                 err = errEnc
98         }
99         if err != nil {
100                 return nil, fmt.Errorf("transcoding config data: %s", err)
101         }
102
103         if useDeprecated {
104                 err = applyDeprecatedConfig(&cfg, buf, log)
105                 if err != nil {
106                         return nil, err
107                 }
108         }
109
110         // Check for known mistakes
111         for id, cc := range cfg.Clusters {
112                 err = checkKeyConflict(fmt.Sprintf("Clusters.%s.PostgreSQL.Connection", id), cc.PostgreSQL.Connection)
113                 if err != nil {
114                         return nil, err
115                 }
116         }
117         return &cfg, nil
118 }
119
120 func checkKeyConflict(label string, m map[string]string) error {
121         saw := map[string]bool{}
122         for k := range m {
123                 k = strings.ToLower(k)
124                 if saw[k] {
125                         return fmt.Errorf("%s: multiple entries for %q (fix by using same capitalization as default/example file)", label, k)
126                 }
127                 saw[k] = true
128         }
129         return nil
130 }
131
132 func logExtraKeys(log logger, expected, supplied map[string]interface{}, prefix string) {
133         if log == nil {
134                 return
135         }
136         for k, vsupp := range supplied {
137                 if vexp, ok := expected[k]; !ok {
138                         log.Warnf("deprecated or unknown config entry: %s%s", prefix, k)
139                 } else if vsupp, ok := vsupp.(map[string]interface{}); !ok {
140                         // if vsupp is a map but vexp isn't map, this
141                         // will be caught elsewhere; see TestBadType.
142                         continue
143                 } else if vexp, ok := vexp.(map[string]interface{}); !ok {
144                         log.Warnf("unexpected object in config entry: %s%s", prefix, k)
145                 } else {
146                         logExtraKeys(log, vexp, vsupp, prefix+k+".")
147                 }
148         }
149 }