15003: Add config-check command.
[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         err = mergo.Merge(&merged, src, mergo.WithOverride)
82         if err != nil {
83                 return nil, fmt.Errorf("merging config data: %s", err)
84         }
85
86         // map[string]interface{} => json => arvados.Config
87         var cfg arvados.Config
88         var errEnc error
89         pr, pw := io.Pipe()
90         go func() {
91                 errEnc = json.NewEncoder(pw).Encode(merged)
92                 pw.Close()
93         }()
94         err = json.NewDecoder(pr).Decode(&cfg)
95         if errEnc != nil {
96                 err = errEnc
97         }
98         if err != nil {
99                 return nil, fmt.Errorf("transcoding config data: %s", err)
100         }
101
102         if useDeprecated {
103                 err = applyDeprecatedConfig(&cfg, buf, log)
104                 if err != nil {
105                         return nil, err
106                 }
107         }
108
109         // Check for known mistakes
110         for id, cc := range cfg.Clusters {
111                 err = checkKeyConflict(fmt.Sprintf("Clusters.%s.PostgreSQL.Connection", id), cc.PostgreSQL.Connection)
112                 if err != nil {
113                         return nil, err
114                 }
115         }
116         return &cfg, nil
117 }
118
119 func checkKeyConflict(label string, m map[string]string) error {
120         saw := map[string]bool{}
121         for k := range m {
122                 k = strings.ToLower(k)
123                 if saw[k] {
124                         return fmt.Errorf("%s: multiple keys with tolower(key)==%q (use same case as defaults)", label, k)
125                 }
126                 saw[k] = true
127         }
128         return nil
129 }