15003: Load config over defaults.
authorTom Clegg <tclegg@veritasgenetics.com>
Tue, 16 Apr 2019 18:05:53 +0000 (14:05 -0400)
committerTom Clegg <tclegg@veritasgenetics.com>
Tue, 23 Apr 2019 14:21:17 +0000 (10:21 -0400)
Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg@veritasgenetics.com>

lib/config/load.go [new file with mode: 0644]
lib/config/load_test.go [new file with mode: 0644]
sdk/go/arvados/config.go

diff --git a/lib/config/load.go b/lib/config/load.go
new file mode 100644 (file)
index 0000000..e16a6e4
--- /dev/null
@@ -0,0 +1,113 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package config
+
+import (
+       "bytes"
+       "errors"
+       "fmt"
+       "io"
+       "io/ioutil"
+       "os"
+       "strings"
+
+       "git.curoverse.com/arvados.git/sdk/go/arvados"
+       "github.com/ghodss/yaml"
+)
+
+type logger interface {
+       Warnf(string, ...interface{})
+}
+
+type deprecatedConfig struct {
+       Clusters map[string]struct {
+               NodeProfiles map[string]arvados.NodeProfile
+       }
+}
+
+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
+       }
+
+       // Load the config into a dummy map to get the cluster ID
+       // keys, discarding the values; then set up defaults for each
+       // cluster ID; then load the real config on top of the
+       // defaults.
+       var dummy struct {
+               Clusters map[string]struct{}
+       }
+       err = yaml.Unmarshal(buf, &dummy)
+       if err != nil {
+               return nil, err
+       }
+       if len(dummy.Clusters) == 0 {
+               return nil, errors.New("config does not define any clusters")
+       }
+       for id := range dummy.Clusters {
+               err = yaml.Unmarshal(bytes.Replace(DefaultYAML, []byte("xxxxx"), []byte(id), -1), &cfg)
+               if err != nil {
+                       return nil, fmt.Errorf("loading defaults for %s: %s", id, err)
+               }
+       }
+       err = yaml.Unmarshal(buf, &cfg)
+       if err != nil {
+               return nil, err
+       }
+
+       // Check for deprecated config values, and apply them to cfg.
+       var dc deprecatedConfig
+       err = yaml.Unmarshal(buf, &dc)
+       if err != nil {
+               return nil, err
+       }
+       err = applyDeprecatedConfig(&cfg, &dc, log)
+       if err != nil {
+               return nil, err
+       }
+       return &cfg, nil
+}
+
+func applyDeprecatedConfig(cfg *arvados.Config, dc *deprecatedConfig, log logger) error {
+       hostname, err := os.Hostname()
+       if err != nil {
+               return 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)
+                       }
+               }
+               cfg.Clusters[id] = cluster
+       }
+       return 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
+       }
+       svc.InternalURLs[arvados.URL{Scheme: scheme, Host: host}] = arvados.ServiceInstance{}
+}
diff --git a/lib/config/load_test.go b/lib/config/load_test.go
new file mode 100644 (file)
index 0000000..f00ce33
--- /dev/null
@@ -0,0 +1,116 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package config
+
+import (
+       "bytes"
+       "io"
+       "os"
+       "os/exec"
+       "testing"
+
+       "git.curoverse.com/arvados.git/sdk/go/ctxlog"
+       "github.com/ghodss/yaml"
+       check "gopkg.in/check.v1"
+)
+
+// Gocheck boilerplate
+func Test(t *testing.T) {
+       check.TestingT(t)
+}
+
+var _ = check.Suite(&LoadSuite{})
+
+type LoadSuite struct{}
+
+func (s *LoadSuite) TestEmpty(c *check.C) {
+       cfg, err := Load(&bytes.Buffer{}, ctxlog.TestLogger(c))
+       c.Check(cfg, check.IsNil)
+       c.Assert(err, check.ErrorMatches, `config does not define any clusters`)
+}
+
+func (s *LoadSuite) TestNoConfigs(c *check.C) {
+       cfg, err := Load(bytes.NewBufferString(`Clusters: {"z1111": {}}`), ctxlog.TestLogger(c))
+       c.Assert(err, check.IsNil)
+       c.Assert(cfg.Clusters, check.HasLen, 1)
+       cc, err := cfg.GetCluster("z1111")
+       c.Assert(err, check.IsNil)
+       c.Check(cc.ClusterID, check.Equals, "z1111")
+}
+
+func (s *LoadSuite) TestMultipleClusters(c *check.C) {
+       cfg, err := Load(bytes.NewBufferString(`{"Clusters":{"z1111":{},"z2222":{}}}`), ctxlog.TestLogger(c))
+       c.Assert(err, check.IsNil)
+       c1, err := cfg.GetCluster("z1111")
+       c.Assert(err, check.IsNil)
+       c.Check(c1.ClusterID, check.Equals, "z1111")
+       c2, err := cfg.GetCluster("z2222")
+       c.Assert(err, check.IsNil)
+       c.Check(c2.ClusterID, check.Equals, "z2222")
+}
+
+func (s *LoadSuite) TestNodeProfilesToServices(c *check.C) {
+       hostname, err := os.Hostname()
+       c.Assert(err, check.IsNil)
+       s.checkEquivalent(c, `
+Clusters:
+ z1111:
+  NodeProfiles:
+   "*":
+    arvados-dispatch-cloud:
+     listen: ":9006"
+    arvados-controller:
+     listen: ":9004"
+   `+hostname+`:
+    arvados-api-server:
+     listen: ":8000"
+`, `
+Clusters:
+ z1111:
+  Services:
+   RailsAPI:
+    InternalURLs:
+     "http://`+hostname+`:8000": {}
+   Controller:
+    InternalURLs:
+     "http://`+hostname+`:9004": {}
+   DispatchCloud:
+    InternalURLs:
+     "http://`+hostname+`:9006": {}
+  NodeProfiles:
+   "*":
+    arvados-dispatch-cloud:
+     listen: ":9006"
+    arvados-controller:
+     listen: ":9004"
+   `+hostname+`:
+    arvados-api-server:
+     listen: ":8000"
+`)
+}
+
+func (s *LoadSuite) checkEquivalent(c *check.C, goty, expectedy string) {
+       got, err := Load(bytes.NewBufferString(goty), ctxlog.TestLogger(c))
+       c.Assert(err, check.IsNil)
+       expected, err := Load(bytes.NewBufferString(expectedy), ctxlog.TestLogger(c))
+       c.Assert(err, check.IsNil)
+       if !c.Check(got, check.DeepEquals, expected) {
+               cmd := exec.Command("diff", "-u", "--label", "got", "--label", "expected", "/dev/fd/3", "/dev/fd/4")
+               for _, obj := range []interface{}{got, expected} {
+                       y, _ := yaml.Marshal(obj)
+                       pr, pw, err := os.Pipe()
+                       c.Assert(err, check.IsNil)
+                       defer pr.Close()
+                       go func() {
+                               io.Copy(pw, bytes.NewBuffer(y))
+                               pw.Close()
+                       }()
+                       cmd.ExtraFiles = append(cmd.ExtraFiles, pr)
+               }
+               diff, err := cmd.CombinedOutput()
+               c.Log(string(diff))
+               c.Check(err, check.IsNil)
+       }
+}
index 2965d5ecb0dc8aa89da2354eea231464d9fa202f..32763acd18ad833497dd208c8c17a84096633347 100644 (file)
@@ -105,6 +105,10 @@ func (su *URL) UnmarshalText(text []byte) error {
        return err
 }
 
+func (su URL) MarshalText() ([]byte, error) {
+       return []byte(fmt.Sprintf("%s", (*url.URL)(&su).String())), nil
+}
+
 type ServiceInstance struct{}
 
 type Logging struct {