13647: Return recognizable error when no clusters are defined.
[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
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`")
55         flagset.StringVar(&ldr.KeepstorePath, "legacy-keepstore-config", defaultKeepstoreConfigPath, "Legacy keepstore configuration `file`")
56 }
57
58 func (ldr *Loader) loadBytes(path string) ([]byte, error) {
59         if path == "-" {
60                 return ioutil.ReadAll(ldr.Stdin)
61         }
62         f, err := os.Open(path)
63         if err != nil {
64                 return nil, err
65         }
66         defer f.Close()
67         return ioutil.ReadAll(f)
68 }
69
70 func (ldr *Loader) Load() (*arvados.Config, error) {
71         if ldr.configdata == nil {
72                 buf, err := ldr.loadBytes(ldr.Path)
73                 if err != nil {
74                         return nil, err
75                 }
76                 ldr.configdata = buf
77         }
78
79         // Load the config into a dummy map to get the cluster ID
80         // keys, discarding the values; then set up defaults for each
81         // cluster ID; then load the real config on top of the
82         // defaults.
83         var dummy struct {
84                 Clusters map[string]struct{}
85         }
86         err := yaml.Unmarshal(ldr.configdata, &dummy)
87         if err != nil {
88                 return nil, err
89         }
90         if len(dummy.Clusters) == 0 {
91                 return nil, ErrNoClustersDefined
92         }
93
94         // We can't merge deep structs here; instead, we unmarshal the
95         // default & loaded config files into generic maps, merge
96         // those, and then json-encode+decode the result into the
97         // config struct type.
98         var merged map[string]interface{}
99         for id := range dummy.Clusters {
100                 var src map[string]interface{}
101                 err = yaml.Unmarshal(bytes.Replace(DefaultYAML, []byte(" xxxxx:"), []byte(" "+id+":"), -1), &src)
102                 if err != nil {
103                         return nil, fmt.Errorf("loading defaults for %s: %s", id, err)
104                 }
105                 err = mergo.Merge(&merged, src, mergo.WithOverride)
106                 if err != nil {
107                         return nil, fmt.Errorf("merging defaults for %s: %s", id, err)
108                 }
109         }
110         var src map[string]interface{}
111         err = yaml.Unmarshal(ldr.configdata, &src)
112         if err != nil {
113                 return nil, fmt.Errorf("loading config data: %s", err)
114         }
115         ldr.logExtraKeys(merged, src, "")
116         removeSampleKeys(merged)
117         err = mergo.Merge(&merged, src, mergo.WithOverride)
118         if err != nil {
119                 return nil, fmt.Errorf("merging config data: %s", err)
120         }
121
122         // map[string]interface{} => json => arvados.Config
123         var cfg arvados.Config
124         var errEnc error
125         pr, pw := io.Pipe()
126         go func() {
127                 errEnc = json.NewEncoder(pw).Encode(merged)
128                 pw.Close()
129         }()
130         err = json.NewDecoder(pr).Decode(&cfg)
131         if errEnc != nil {
132                 err = errEnc
133         }
134         if err != nil {
135                 return nil, fmt.Errorf("transcoding config data: %s", err)
136         }
137
138         if !ldr.SkipDeprecated {
139                 err = ldr.applyDeprecatedConfig(&cfg)
140                 if err != nil {
141                         return nil, err
142                 }
143                 for _, err := range []error{
144                         ldr.loadOldKeepstoreConfig(&cfg),
145                 } {
146                         if err != nil {
147                                 return nil, err
148                         }
149                 }
150         }
151
152         // Check for known mistakes
153         for id, cc := range cfg.Clusters {
154                 err = checkKeyConflict(fmt.Sprintf("Clusters.%s.PostgreSQL.Connection", id), cc.PostgreSQL.Connection)
155                 if err != nil {
156                         return nil, err
157                 }
158         }
159         return &cfg, nil
160 }
161
162 func checkKeyConflict(label string, m map[string]string) error {
163         saw := map[string]bool{}
164         for k := range m {
165                 k = strings.ToLower(k)
166                 if saw[k] {
167                         return fmt.Errorf("%s: multiple entries for %q (fix by using same capitalization as default/example file)", label, k)
168                 }
169                 saw[k] = true
170         }
171         return nil
172 }
173
174 func removeSampleKeys(m map[string]interface{}) {
175         delete(m, "SAMPLE")
176         for _, v := range m {
177                 if v, _ := v.(map[string]interface{}); v != nil {
178                         removeSampleKeys(v)
179                 }
180         }
181 }
182
183 func (ldr *Loader) logExtraKeys(expected, supplied map[string]interface{}, prefix string) {
184         if ldr.Logger == nil {
185                 return
186         }
187         allowed := map[string]interface{}{}
188         for k, v := range expected {
189                 allowed[strings.ToLower(k)] = v
190         }
191         for k, vsupp := range supplied {
192                 vexp, ok := allowed[strings.ToLower(k)]
193                 if !ok && expected["SAMPLE"] != nil {
194                         vexp = expected["SAMPLE"]
195                 } else if !ok {
196                         ldr.Logger.Warnf("deprecated or unknown config entry: %s%s", prefix, k)
197                         continue
198                 }
199                 if vsupp, ok := vsupp.(map[string]interface{}); !ok {
200                         // if vsupp is a map but vexp isn't map, this
201                         // will be caught elsewhere; see TestBadType.
202                         continue
203                 } else if vexp, ok := vexp.(map[string]interface{}); !ok {
204                         ldr.Logger.Warnf("unexpected object in config entry: %s%s", prefix, k)
205                 } else {
206                         ldr.logExtraKeys(vexp, vsupp, prefix+k+".")
207                 }
208         }
209 }