15003: Error out if PostgreSQL connection map is ambiguous.
[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         buf, err := ioutil.ReadAll(rdr)
37         if err != nil {
38                 return nil, err
39         }
40
41         // Load the config into a dummy map to get the cluster ID
42         // keys, discarding the values; then set up defaults for each
43         // cluster ID; then load the real config on top of the
44         // defaults.
45         var dummy struct {
46                 Clusters map[string]struct{}
47         }
48         err = yaml.Unmarshal(buf, &dummy)
49         if err != nil {
50                 return nil, err
51         }
52         if len(dummy.Clusters) == 0 {
53                 return nil, errors.New("config does not define any clusters")
54         }
55
56         // We can't merge deep structs here; instead, we unmarshal the
57         // default & loaded config files into generic maps, merge
58         // those, and then json-encode+decode the result into the
59         // config struct type.
60         var merged map[string]interface{}
61         for id := range dummy.Clusters {
62                 var src map[string]interface{}
63                 err = yaml.Unmarshal(bytes.Replace(DefaultYAML, []byte(" xxxxx:"), []byte(" "+id+":"), -1), &src)
64                 if err != nil {
65                         return nil, fmt.Errorf("loading defaults for %s: %s", id, err)
66                 }
67                 err = mergo.Merge(&merged, src, mergo.WithOverride)
68                 if err != nil {
69                         return nil, fmt.Errorf("merging defaults for %s: %s", id, err)
70                 }
71         }
72         var src map[string]interface{}
73         err = yaml.Unmarshal(buf, &src)
74         if err != nil {
75                 return nil, fmt.Errorf("loading config data: %s", err)
76         }
77         err = mergo.Merge(&merged, src, mergo.WithOverride)
78         if err != nil {
79                 return nil, fmt.Errorf("merging config data: %s", err)
80         }
81
82         // map[string]interface{} => json => arvados.Config
83         var cfg arvados.Config
84         var errEnc error
85         pr, pw := io.Pipe()
86         go func() {
87                 errEnc = json.NewEncoder(pw).Encode(merged)
88                 pw.Close()
89         }()
90         err = json.NewDecoder(pr).Decode(&cfg)
91         if errEnc != nil {
92                 err = errEnc
93         }
94         if err != nil {
95                 return nil, fmt.Errorf("transcoding config data: %s", err)
96         }
97
98         err = applyDeprecatedConfig(&cfg, buf, log)
99         if err != nil {
100                 return nil, err
101         }
102         for id, cc := range cfg.Clusters {
103                 err = checkKeyConflict(fmt.Sprintf("Clusters.%s.PostgreSQL.Connection", id), cc.PostgreSQL.Connection)
104                 if err != nil {
105                         return nil, err
106                 }
107         }
108         return &cfg, nil
109 }
110
111 func checkKeyConflict(label string, m map[string]string) error {
112         saw := map[string]bool{}
113         for k := range m {
114                 k = strings.ToLower(k)
115                 if saw[k] {
116                         return fmt.Errorf("%s: multiple keys with tolower(key)==%q (use same case as defaults)", label, k)
117                 }
118                 saw[k] = true
119         }
120         return nil
121 }