15003: Error out if PostgreSQL connection map is ambiguous.
[arvados.git] / lib / config / load.go
index 159dd65dc0d021c0d482589efa09021c1043dbc2..ec5ce636a0eb520115be4d8301f7ea4adfd95ebd 100644 (file)
@@ -6,6 +6,7 @@ package config
 
 import (
        "bytes"
+       "encoding/json"
        "errors"
        "fmt"
        "io"
@@ -15,18 +16,13 @@ import (
 
        "git.curoverse.com/arvados.git/sdk/go/arvados"
        "github.com/ghodss/yaml"
+       "github.com/imdario/mergo"
 )
 
 type logger interface {
        Warnf(string, ...interface{})
 }
 
-type deprecatedConfig struct {
-       Clusters map[string]struct {
-               NodeProfiles map[string]arvados.NodeProfile
-       }
-}
-
 func LoadFile(path string, log logger) (*arvados.Config, error) {
        f, err := os.Open(path)
        if err != nil {
@@ -37,7 +33,6 @@ func LoadFile(path string, log logger) (*arvados.Config, error) {
 }
 
 func Load(rdr io.Reader, log logger) (*arvados.Config, error) {
-       var cfg arvados.Config
        buf, err := ioutil.ReadAll(rdr)
        if err != nil {
                return nil, err
@@ -57,66 +52,70 @@ func Load(rdr io.Reader, log logger) (*arvados.Config, error) {
        if len(dummy.Clusters) == 0 {
                return nil, errors.New("config does not define any clusters")
        }
+
+       // We can't merge deep structs here; instead, we unmarshal the
+       // default & loaded config files into generic maps, merge
+       // those, and then json-encode+decode the result into the
+       // config struct type.
+       var merged map[string]interface{}
        for id := range dummy.Clusters {
-               err = yaml.Unmarshal(bytes.Replace(DefaultYAML, []byte("xxxxx"), []byte(id), -1), &cfg)
+               var src map[string]interface{}
+               err = yaml.Unmarshal(bytes.Replace(DefaultYAML, []byte(" xxxxx:"), []byte(" "+id+":"), -1), &src)
                if err != nil {
                        return nil, fmt.Errorf("loading defaults for %s: %s", id, err)
                }
+               err = mergo.Merge(&merged, src, mergo.WithOverride)
+               if err != nil {
+                       return nil, fmt.Errorf("merging defaults for %s: %s", id, err)
+               }
        }
-       err = yaml.Unmarshal(buf, &cfg)
+       var src map[string]interface{}
+       err = yaml.Unmarshal(buf, &src)
        if err != nil {
-               return nil, err
+               return nil, fmt.Errorf("loading config data: %s", err)
        }
-
-       // Check for deprecated config values, and apply them to cfg.
-       var dc deprecatedConfig
-       err = yaml.Unmarshal(buf, &dc)
+       err = mergo.Merge(&merged, src, mergo.WithOverride)
        if err != nil {
-               return nil, err
+               return nil, fmt.Errorf("merging config data: %s", err)
+       }
+
+       // map[string]interface{} => json => arvados.Config
+       var cfg arvados.Config
+       var errEnc error
+       pr, pw := io.Pipe()
+       go func() {
+               errEnc = json.NewEncoder(pw).Encode(merged)
+               pw.Close()
+       }()
+       err = json.NewDecoder(pr).Decode(&cfg)
+       if errEnc != nil {
+               err = errEnc
        }
-       err = applyDeprecatedConfig(&cfg, &dc, log)
        if err != nil {
-               return nil, err
+               return nil, fmt.Errorf("transcoding config data: %s", err)
        }
-       return &cfg, nil
-}
 
-func applyDeprecatedConfig(cfg *arvados.Config, dc *deprecatedConfig, log logger) error {
-       hostname, err := os.Hostname()
+       err = applyDeprecatedConfig(&cfg, buf, log)
        if err != nil {
-               return err
+               return nil, err
        }
-       for id, dcluster := range dc.Clusters {
-               cluster, ok := cfg.Clusters[id]
-               if !ok {
-                       return fmt.Errorf("can't load legacy config %q that is not present in current config", id)
-               }
-               for name, np := range dcluster.NodeProfiles {
-                       if name == "*" || name == os.Getenv("ARVADOS_NODE_PROFILE") || name == hostname {
-                               applyDeprecatedNodeProfile(hostname, np.RailsAPI, &cluster.Services.RailsAPI)
-                               applyDeprecatedNodeProfile(hostname, np.Controller, &cluster.Services.Controller)
-                               applyDeprecatedNodeProfile(hostname, np.DispatchCloud, &cluster.Services.DispatchCloud)
-                       }
+       for id, cc := range cfg.Clusters {
+               err = checkKeyConflict(fmt.Sprintf("Clusters.%s.PostgreSQL.Connection", id), cc.PostgreSQL.Connection)
+               if err != nil {
+                       return nil, err
                }
-               cfg.Clusters[id] = cluster
        }
-       return nil
+       return &cfg, nil
 }
 
-func applyDeprecatedNodeProfile(hostname string, ssi arvados.SystemServiceInstance, svc *arvados.Service) {
-       scheme := "https"
-       if !ssi.TLS {
-               scheme = "http"
-       }
-       if svc.InternalURLs == nil {
-               svc.InternalURLs = map[arvados.URL]arvados.ServiceInstance{}
-       }
-       host := ssi.Listen
-       if host == "" {
-               return
-       }
-       if strings.HasPrefix(host, ":") {
-               host = hostname + host
+func checkKeyConflict(label string, m map[string]string) error {
+       saw := map[string]bool{}
+       for k := range m {
+               k = strings.ToLower(k)
+               if saw[k] {
+                       return fmt.Errorf("%s: multiple keys with tolower(key)==%q (use same case as defaults)", label, k)
+               }
+               saw[k] = true
        }
-       svc.InternalURLs[arvados.URL{Scheme: scheme, Host: host}] = arvados.ServiceInstance{}
+       return nil
 }