Merge branch '13647-load-old-config'
authorTom Clegg <tclegg@veritasgenetics.com>
Fri, 12 Jul 2019 19:05:06 +0000 (15:05 -0400)
committerTom Clegg <tclegg@veritasgenetics.com>
Fri, 12 Jul 2019 19:05:06 +0000 (15:05 -0400)
refs #13647

Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg@veritasgenetics.com>

13 files changed:
build/run-tests.sh
cmd/arvados-client/cmd.go
cmd/arvados-server/cmd.go
lib/cloud/cloudtest/cmd.go
lib/cmd/cmd.go
lib/config/cmd.go
lib/config/cmd_test.go
lib/config/deprecated.go
lib/config/export_test.go
lib/config/load.go
lib/config/load_test.go
lib/service/cmd.go
sdk/go/arvados/config.go

index 5c71ea8e354dfca33d376b0b79bc565864ca0525..85504db0360e3d1921398a09b756e53f619fa2c2 100755 (executable)
@@ -772,7 +772,7 @@ do_test_once() {
         # before trying "go test". Otherwise, coverage-reporting
         # mode makes Go show the wrong line numbers when reporting
         # compilation errors.
-        go get -ldflags "-X main.version=${ARVADOS_VERSION:-$(git log -n1 --format=%H)-dev}" -t "git.curoverse.com/arvados.git/$1" && \
+        go get -ldflags "-X git.curoverse.com/arvados.git/lib/cmd.version=${ARVADOS_VERSION:-$(git log -n1 --format=%H)-dev}" -t "git.curoverse.com/arvados.git/$1" && \
             cd "$GOPATH/src/git.curoverse.com/arvados.git/$1" && \
             if [[ -n "${testargs[$1]}" ]]
         then
@@ -838,7 +838,7 @@ do_install_once() {
         result=1
     elif [[ "$2" == "go" ]]
     then
-        go get -ldflags "-X main.version=${ARVADOS_VERSION:-$(git log -n1 --format=%H)-dev}" -t "git.curoverse.com/arvados.git/$1"
+        go get -ldflags "-X git.curoverse.com/arvados.git/lib/cmd.version=${ARVADOS_VERSION:-$(git log -n1 --format=%H)-dev}" -t "git.curoverse.com/arvados.git/$1"
     elif [[ "$2" == "pip" ]]
     then
         # $3 can name a path directory for us to use, including trailing
index 4550ae53aced128d0698891c76d95a1730cae316..589e05c8a14c92a84de4ba2ae2fe586d72bc3a3d 100644 (file)
@@ -12,12 +12,11 @@ import (
 )
 
 var (
-       version = "dev"
        handler = cmd.Multi(map[string]cmd.Handler{
-               "-e":        cmd.Version(version),
-               "version":   cmd.Version(version),
-               "-version":  cmd.Version(version),
-               "--version": cmd.Version(version),
+               "-e":        cmd.Version,
+               "version":   cmd.Version,
+               "-version":  cmd.Version,
+               "--version": cmd.Version,
 
                "copy":     cli.Copy,
                "create":   cli.Create,
index dd34eff7d44855826cdaaf13efa59b6091a79060..14cd63e46de5fcf6c7e85d6566b79a10cccf26ca 100644 (file)
@@ -15,11 +15,10 @@ import (
 )
 
 var (
-       version = "dev"
        handler = cmd.Multi(map[string]cmd.Handler{
-               "version":   cmd.Version(version),
-               "-version":  cmd.Version(version),
-               "--version": cmd.Version(version),
+               "version":   cmd.Version,
+               "-version":  cmd.Version,
+               "--version": cmd.Version,
 
                "cloudtest":       cloudtest.Command,
                "config-check":    config.CheckCommand,
index 35a78695fdb6b2ebf6de195ed5ef7954bc53656f..9c3fc46c0c754d467c3997c2895f5bd8ea7ba1cc 100644 (file)
@@ -63,7 +63,9 @@ func (command) RunCommand(prog string, args []string, stdin io.Reader, stdout, s
                logger.Info("exiting")
        }()
 
-       cfg, err := config.LoadFile(*configFile, logger)
+       loader := config.NewLoader(stdin, logger)
+       loader.Path = *configFile
+       cfg, err := loader.Load()
        if err != nil {
                return 1
        }
index 9292ef7e5ff5b3afb6012833299d9f89a7ea346c..24b69f0cc5c529fe45b5be6f4ae6698cff607ff9 100644 (file)
@@ -28,11 +28,18 @@ func (f HandlerFunc) RunCommand(prog string, args []string, stdin io.Reader, std
        return f(prog, args, stdin, stdout, stderr)
 }
 
-type Version string
+// Version is a Handler that prints the package version (set at build
+// time using -ldflags) and Go runtime version to stdout, and returns
+// 0.
+var Version versionCommand
 
-func (v Version) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
+var version = "dev"
+
+type versionCommand struct{}
+
+func (versionCommand) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
        prog = regexp.MustCompile(` -*version$`).ReplaceAllLiteralString(prog, "")
-       fmt.Fprintf(stdout, "%s %s (%s)\n", prog, v, runtime.Version())
+       fmt.Fprintf(stdout, "%s %s (%s)\n", prog, version, runtime.Version())
        return 0
 }
 
index 0351ad02a1923d0a58e8d894885edcbe494ac902..5cb76fc35d63bd331f92e7ec02fccc1084a0c0e6 100644 (file)
@@ -9,13 +9,12 @@ import (
        "flag"
        "fmt"
        "io"
-       "io/ioutil"
        "os"
        "os/exec"
 
-       "git.curoverse.com/arvados.git/sdk/go/arvados"
        "git.curoverse.com/arvados.git/sdk/go/ctxlog"
        "github.com/ghodss/yaml"
+       "github.com/sirupsen/logrus"
 )
 
 var DumpCommand dumpCommand
@@ -30,9 +29,15 @@ func (dumpCommand) RunCommand(prog string, args []string, stdin io.Reader, stdou
                }
        }()
 
+       loader := &Loader{
+               Stdin:  stdin,
+               Logger: ctxlog.New(stderr, "text", "info"),
+       }
+
        flags := flag.NewFlagSet("", flag.ContinueOnError)
        flags.SetOutput(stderr)
-       configFile := flags.String("config", arvados.DefaultConfigFile, "Site configuration `file`")
+       loader.SetupFlags(flags)
+
        err = flags.Parse(args)
        if err == flag.ErrHelp {
                err = nil
@@ -45,8 +50,8 @@ func (dumpCommand) RunCommand(prog string, args []string, stdin io.Reader, stdou
                flags.Usage()
                return 2
        }
-       log := ctxlog.New(stderr, "text", "info")
-       cfg, err := loadFileOrStdin(*configFile, stdin, log)
+
+       cfg, err := loader.Load()
        if err != nil {
                return 1
        }
@@ -67,15 +72,25 @@ type checkCommand struct{}
 
 func (checkCommand) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
        var err error
+       var logbuf = &bytes.Buffer{}
        defer func() {
+               io.Copy(stderr, logbuf)
                if err != nil {
                        fmt.Fprintf(stderr, "%s\n", err)
                }
        }()
 
+       logger := logrus.New()
+       logger.Out = logbuf
+       loader := &Loader{
+               Stdin:  stdin,
+               Logger: logger,
+       }
+
        flags := flag.NewFlagSet("", flag.ContinueOnError)
        flags.SetOutput(stderr)
-       configFile := flags.String("config", arvados.DefaultConfigFile, "Site configuration `file`")
+       loader.SetupFlags(flags)
+
        err = flags.Parse(args)
        if err == flag.ErrHelp {
                err = nil
@@ -88,21 +103,22 @@ func (checkCommand) RunCommand(prog string, args []string, stdin io.Reader, stdo
                flags.Usage()
                return 2
        }
-       log := &plainLogger{w: stderr}
-       var buf []byte
-       if *configFile == "-" {
-               buf, err = ioutil.ReadAll(stdin)
-       } else {
-               buf, err = ioutil.ReadFile(*configFile)
-       }
-       if err != nil {
-               return 1
-       }
-       withoutDepr, err := load(bytes.NewBuffer(buf), log, false)
+
+       // Load the config twice -- once without loading deprecated
+       // keys/files, once with -- and then compare the two resulting
+       // configs. This reveals whether the deprecated keys/files
+       // have any effect on the final configuration.
+       //
+       // If they do, show the operator how to update their config
+       // such that the deprecated keys/files are superfluous and can
+       // be deleted.
+       loader.SkipDeprecated = true
+       withoutDepr, err := loader.Load()
        if err != nil {
                return 1
        }
-       withDepr, err := load(bytes.NewBuffer(buf), nil, true)
+       loader.SkipDeprecated = false
+       withDepr, err := loader.Load()
        if err != nil {
                return 1
        }
@@ -124,6 +140,7 @@ func (checkCommand) RunCommand(prog string, args []string, stdin io.Reader, stdo
        if bytes.HasPrefix(diff, []byte("--- ")) {
                fmt.Fprintln(stdout, "Your configuration is relying on deprecated entries. Suggest making the following changes.")
                stdout.Write(diff)
+               err = nil
                return 1
        } else if len(diff) > 0 {
                fmt.Fprintf(stderr, "Unexpected diff output:\n%s", diff)
@@ -131,36 +148,20 @@ func (checkCommand) RunCommand(prog string, args []string, stdin io.Reader, stdo
        } else if err != nil {
                return 1
        }
-       if log.used {
+       if logbuf.Len() > 0 {
                return 1
        }
        return 0
 }
 
-type plainLogger struct {
-       w    io.Writer
-       used bool
-}
-
-func (pl *plainLogger) Warnf(format string, args ...interface{}) {
-       pl.used = true
-       fmt.Fprintf(pl.w, format+"\n", args...)
-}
-
 var DumpDefaultsCommand defaultsCommand
 
 type defaultsCommand struct{}
 
 func (defaultsCommand) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
-       var err error
-       defer func() {
-               if err != nil {
-                       fmt.Fprintf(stderr, "%s\n", err)
-               }
-       }()
-
-       _, err = stdout.Write(DefaultYAML)
+       _, err := stdout.Write(DefaultYAML)
        if err != nil {
+               fmt.Fprintln(stderr, err)
                return 1
        }
        return 0
index 997935e5dcb4d54658de8f0546264fe6c8d903fb..af7c571203b3dcf48f8a2198ded9e6d90648e8be 100644 (file)
@@ -6,6 +6,9 @@ package config
 
 import (
        "bytes"
+       "io"
+       "io/ioutil"
+       "os"
 
        "git.curoverse.com/arvados.git/lib/cmd"
        check "gopkg.in/check.v1"
@@ -77,6 +80,26 @@ Clusters:
        c.Check(stdout.String(), check.Matches, `(?ms).*\n\- +.*MaxItemsPerResponse: 1000\n\+ +MaxItemsPerResponse: 1234\n.*`)
 }
 
+func (s *CommandSuite) TestCheckOldKeepstoreConfigFile(c *check.C) {
+       f, err := ioutil.TempFile("", "")
+       c.Assert(err, check.IsNil)
+       defer os.Remove(f.Name())
+
+       io.WriteString(f, "Debug: true\n")
+
+       var stdout, stderr bytes.Buffer
+       in := `
+Clusters:
+ z1234:
+  SystemLogs:
+    LogLevel: info
+`
+       code := CheckCommand.RunCommand("arvados config-check", []string{"-config", "-", "-legacy-keepstore-config", f.Name()}, bytes.NewBufferString(in), &stdout, &stderr)
+       c.Check(code, check.Equals, 1)
+       c.Check(stdout.String(), check.Matches, `(?ms).*\n\- +.*LogLevel: info\n\+ +LogLevel: debug\n.*`)
+       c.Check(stderr.String(), check.Matches, `.*you should remove the legacy keepstore config file.*\n`)
+}
+
 func (s *CommandSuite) TestCheckUnknownKey(c *check.C) {
        var stdout, stderr bytes.Buffer
        in := `
@@ -95,10 +118,10 @@ Clusters:
        code := CheckCommand.RunCommand("arvados config-check", []string{"-config", "-"}, bytes.NewBufferString(in), &stdout, &stderr)
        c.Log(stderr.String())
        c.Check(code, check.Equals, 1)
-       c.Check(stderr.String(), check.Matches, `(?ms).*deprecated or unknown config entry: Clusters.z1234.Bogus1\n.*`)
-       c.Check(stderr.String(), check.Matches, `(?ms).*deprecated or unknown config entry: Clusters.z1234.BogusSection\n.*`)
-       c.Check(stderr.String(), check.Matches, `(?ms).*deprecated or unknown config entry: Clusters.z1234.API.Bogus3\n.*`)
-       c.Check(stderr.String(), check.Matches, `(?ms).*unexpected object in config entry: Clusters.z1234.PostgreSQL.ConnectionPool\n.*`)
+       c.Check(stderr.String(), check.Matches, `(?ms).*deprecated or unknown config entry: Clusters.z1234.Bogus1"\n.*`)
+       c.Check(stderr.String(), check.Matches, `(?ms).*deprecated or unknown config entry: Clusters.z1234.BogusSection"\n.*`)
+       c.Check(stderr.String(), check.Matches, `(?ms).*deprecated or unknown config entry: Clusters.z1234.API.Bogus3"\n.*`)
+       c.Check(stderr.String(), check.Matches, `(?ms).*unexpected object in config entry: Clusters.z1234.PostgreSQL.ConnectionPool"\n.*`)
 }
 
 func (s *CommandSuite) TestDumpFormatting(c *check.C) {
index 8ffa2a58341e952b67eca0220e59bd423273ab9b..0b0bb26689902af4fb8c2b668e75cc4cc7e00981 100644 (file)
@@ -6,6 +6,7 @@ package config
 
 import (
        "fmt"
+       "io/ioutil"
        "os"
        "strings"
 
@@ -47,9 +48,9 @@ type systemServiceInstance struct {
        Insecure bool
 }
 
-func applyDeprecatedConfig(cfg *arvados.Config, configdata []byte, log logger) error {
+func (ldr *Loader) applyDeprecatedConfig(cfg *arvados.Config) error {
        var dc deprecatedConfig
-       err := yaml.Unmarshal(configdata, &dc)
+       err := yaml.Unmarshal(ldr.configdata, &dc)
        if err != nil {
                return err
        }
@@ -65,8 +66,8 @@ func applyDeprecatedConfig(cfg *arvados.Config, configdata []byte, log logger) e
                for name, np := range dcluster.NodeProfiles {
                        if name == "*" || name == os.Getenv("ARVADOS_NODE_PROFILE") || name == hostname {
                                name = "localhost"
-                       } else if log != nil {
-                               log.Warnf("overriding Clusters.%s.Services using Clusters.%s.NodeProfiles.%s (guessing %q is a hostname)", id, id, name, name)
+                       } else if ldr.Logger != nil {
+                               ldr.Logger.Warnf("overriding Clusters.%s.Services using Clusters.%s.NodeProfiles.%s (guessing %q is a hostname)", id, id, name, name)
                        }
                        applyDeprecatedNodeProfile(name, np.RailsAPI, &cluster.Services.RailsAPI)
                        applyDeprecatedNodeProfile(name, np.Controller, &cluster.Services.Controller)
@@ -100,3 +101,45 @@ func applyDeprecatedNodeProfile(hostname string, ssi systemServiceInstance, svc
        }
        svc.InternalURLs[arvados.URL{Scheme: scheme, Host: host}] = arvados.ServiceInstance{}
 }
+
+const defaultKeepstoreConfigPath = "/etc/arvados/keepstore/keepstore.yml"
+
+type oldKeepstoreConfig struct {
+       Debug *bool
+}
+
+// update config using values from an old-style keepstore config file.
+func (ldr *Loader) loadOldKeepstoreConfig(cfg *arvados.Config) error {
+       path := ldr.KeepstorePath
+       if path == "" {
+               return nil
+       }
+       buf, err := ioutil.ReadFile(path)
+       if os.IsNotExist(err) && path == defaultKeepstoreConfigPath {
+               return nil
+       } else if err != nil {
+               return err
+       } else {
+               ldr.Logger.Warnf("you should remove the legacy keepstore config file (%s) after migrating all config keys to the cluster configuration file (%s)", path, ldr.Path)
+       }
+       cluster, err := cfg.GetCluster("")
+       if err != nil {
+               return err
+       }
+
+       var oc oldKeepstoreConfig
+       err = yaml.Unmarshal(buf, &oc)
+       if err != nil {
+               return fmt.Errorf("%s: %s", path, err)
+       }
+
+       if v := oc.Debug; v == nil {
+       } else if *v && cluster.SystemLogs.LogLevel != "debug" {
+               cluster.SystemLogs.LogLevel = "debug"
+       } else if !*v && cluster.SystemLogs.LogLevel != "info" {
+               cluster.SystemLogs.LogLevel = "info"
+       }
+
+       cfg.Clusters[cluster.ClusterID] = *cluster
+       return nil
+}
index 581e54cdc6c80f463b3f922014b31b9a986e4638..e12fbdc7f6aa65ed3f181374aca15f932f2ff0da 100644 (file)
@@ -9,7 +9,6 @@ import (
        "regexp"
        "strings"
 
-       "git.curoverse.com/arvados.git/sdk/go/ctxlog"
        check "gopkg.in/check.v1"
 )
 
@@ -18,8 +17,8 @@ var _ = check.Suite(&ExportSuite{})
 type ExportSuite struct{}
 
 func (s *ExportSuite) TestExport(c *check.C) {
-       confdata := bytes.Replace(DefaultYAML, []byte("SAMPLE"), []byte("testkey"), -1)
-       cfg, err := Load(bytes.NewBuffer(confdata), ctxlog.TestLogger(c))
+       confdata := strings.Replace(string(DefaultYAML), "SAMPLE", "testkey", -1)
+       cfg, err := testLoader(c, confdata, nil).Load()
        c.Assert(err, check.IsNil)
        cluster := cfg.Clusters["xxxxx"]
        cluster.ManagementToken = "abcdefg"
index ee9cdd60f43e71cc70ec6b766ea2bb0a4c78b1dc..168c1aa22a8554ef649cc65463b10b8437970494 100644 (file)
@@ -8,6 +8,7 @@ import (
        "bytes"
        "encoding/json"
        "errors"
+       "flag"
        "fmt"
        "io"
        "io/ioutil"
@@ -17,37 +18,125 @@ import (
        "git.curoverse.com/arvados.git/sdk/go/arvados"
        "github.com/ghodss/yaml"
        "github.com/imdario/mergo"
+       "github.com/sirupsen/logrus"
 )
 
-type logger interface {
-       Warnf(string, ...interface{})
+var ErrNoClustersDefined = errors.New("config does not define any clusters")
+
+type Loader struct {
+       Stdin          io.Reader
+       Logger         logrus.FieldLogger
+       SkipDeprecated bool // Don't load legacy/deprecated config keys/files
+
+       Path          string
+       KeepstorePath string
+
+       configdata []byte
 }
 
-func loadFileOrStdin(path string, stdin io.Reader, log logger) (*arvados.Config, error) {
-       if path == "-" {
-               return load(stdin, log, true)
-       } else {
-               return LoadFile(path, log)
+// NewLoader returns a new Loader with Stdin and Logger set to the
+// given values, and all config paths set to their default values.
+func NewLoader(stdin io.Reader, logger logrus.FieldLogger) *Loader {
+       ldr := &Loader{Stdin: stdin, Logger: logger}
+       // Calling SetupFlags on a throwaway FlagSet has the side
+       // effect of assigning default values to the configurable
+       // fields.
+       ldr.SetupFlags(flag.NewFlagSet("", flag.ContinueOnError))
+       return ldr
+}
+
+// SetupFlags configures a flagset so arguments like -config X can be
+// used to change the loader's Path fields.
+//
+//     ldr := NewLoader(os.Stdin, logrus.New())
+//     flagset := flag.NewFlagSet("", flag.ContinueOnError)
+//     ldr.SetupFlags(flagset)
+//     // ldr.Path == "/etc/arvados/config.yml"
+//     flagset.Parse([]string{"-config", "/tmp/c.yaml"})
+//     // ldr.Path == "/tmp/c.yaml"
+func (ldr *Loader) SetupFlags(flagset *flag.FlagSet) {
+       flagset.StringVar(&ldr.Path, "config", arvados.DefaultConfigFile, "Site configuration `file` (default may be overridden by setting an ARVADOS_CONFIG environment variable)")
+       flagset.StringVar(&ldr.KeepstorePath, "legacy-keepstore-config", defaultKeepstoreConfigPath, "Legacy keepstore configuration `file`")
+}
+
+// MungeLegacyConfigArgs checks args for a -config flag whose argument
+// is a regular file (or a symlink to one), but doesn't have a
+// top-level "Clusters" key and therefore isn't a valid cluster
+// configuration file. If it finds such a flag, it replaces -config
+// with legacyConfigArg (e.g., "-legacy-keepstore-config").
+//
+// This is used by programs that still need to accept "-config" as a
+// way to specify a per-component config file until their config has
+// been migrated.
+//
+// If any errors are encountered while reading or parsing a config
+// file, the given args are not munged. We presume the same errors
+// will be encountered again and reported later on when trying to load
+// cluster configuration from the same file, regardless of which
+// struct we end up using.
+func (ldr *Loader) MungeLegacyConfigArgs(lgr logrus.FieldLogger, args []string, legacyConfigArg string) []string {
+       munged := append([]string(nil), args...)
+       for i := 0; i < len(args); i++ {
+               if !strings.HasPrefix(args[i], "-") || strings.SplitN(strings.TrimPrefix(args[i], "-"), "=", 2)[0] != "config" {
+                       continue
+               }
+               var operand string
+               if strings.Contains(args[i], "=") {
+                       operand = strings.SplitN(args[i], "=", 2)[1]
+               } else if i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") {
+                       i++
+                       operand = args[i]
+               } else {
+                       continue
+               }
+               if fi, err := os.Stat(operand); err != nil || !fi.Mode().IsRegular() {
+                       continue
+               }
+               f, err := os.Open(operand)
+               if err != nil {
+                       continue
+               }
+               defer f.Close()
+               buf, err := ioutil.ReadAll(f)
+               if err != nil {
+                       continue
+               }
+               var cfg arvados.Config
+               err = yaml.Unmarshal(buf, &cfg)
+               if err != nil {
+                       continue
+               }
+               if len(cfg.Clusters) == 0 {
+                       lgr.Warnf("%s is not a cluster config file -- interpreting %s as %s (please migrate your config!)", operand, "-config", legacyConfigArg)
+                       if operand == args[i] {
+                               munged[i-1] = legacyConfigArg
+                       } else {
+                               munged[i] = legacyConfigArg + "=" + operand
+                       }
+               }
        }
+       return munged
 }
 
-func LoadFile(path string, log logger) (*arvados.Config, error) {
+func (ldr *Loader) loadBytes(path string) ([]byte, error) {
+       if path == "-" {
+               return ioutil.ReadAll(ldr.Stdin)
+       }
        f, err := os.Open(path)
        if err != nil {
                return nil, err
        }
        defer f.Close()
-       return Load(f, log)
-}
-
-func Load(rdr io.Reader, log logger) (*arvados.Config, error) {
-       return load(rdr, log, true)
+       return ioutil.ReadAll(f)
 }
 
-func load(rdr io.Reader, log logger, useDeprecated bool) (*arvados.Config, error) {
-       buf, err := ioutil.ReadAll(rdr)
-       if err != nil {
-               return nil, err
+func (ldr *Loader) Load() (*arvados.Config, error) {
+       if ldr.configdata == nil {
+               buf, err := ldr.loadBytes(ldr.Path)
+               if err != nil {
+                       return nil, err
+               }
+               ldr.configdata = buf
        }
 
        // Load the config into a dummy map to get the cluster ID
@@ -57,12 +146,12 @@ func load(rdr io.Reader, log logger, useDeprecated bool) (*arvados.Config, error
        var dummy struct {
                Clusters map[string]struct{}
        }
-       err = yaml.Unmarshal(buf, &dummy)
+       err := yaml.Unmarshal(ldr.configdata, &dummy)
        if err != nil {
                return nil, err
        }
        if len(dummy.Clusters) == 0 {
-               return nil, errors.New("config does not define any clusters")
+               return nil, ErrNoClustersDefined
        }
 
        // We can't merge deep structs here; instead, we unmarshal the
@@ -82,11 +171,11 @@ func load(rdr io.Reader, log logger, useDeprecated bool) (*arvados.Config, error
                }
        }
        var src map[string]interface{}
-       err = yaml.Unmarshal(buf, &src)
+       err = yaml.Unmarshal(ldr.configdata, &src)
        if err != nil {
                return nil, fmt.Errorf("loading config data: %s", err)
        }
-       logExtraKeys(log, merged, src, "")
+       ldr.logExtraKeys(merged, src, "")
        removeSampleKeys(merged)
        err = mergo.Merge(&merged, src, mergo.WithOverride)
        if err != nil {
@@ -109,11 +198,18 @@ func load(rdr io.Reader, log logger, useDeprecated bool) (*arvados.Config, error
                return nil, fmt.Errorf("transcoding config data: %s", err)
        }
 
-       if useDeprecated {
-               err = applyDeprecatedConfig(&cfg, buf, log)
+       if !ldr.SkipDeprecated {
+               err = ldr.applyDeprecatedConfig(&cfg)
                if err != nil {
                        return nil, err
                }
+               for _, err := range []error{
+                       ldr.loadOldKeepstoreConfig(&cfg),
+               } {
+                       if err != nil {
+                               return nil, err
+                       }
+               }
        }
 
        // Check for known mistakes
@@ -147,8 +243,8 @@ func removeSampleKeys(m map[string]interface{}) {
        }
 }
 
-func logExtraKeys(log logger, expected, supplied map[string]interface{}, prefix string) {
-       if log == nil {
+func (ldr *Loader) logExtraKeys(expected, supplied map[string]interface{}, prefix string) {
+       if ldr.Logger == nil {
                return
        }
        allowed := map[string]interface{}{}
@@ -164,7 +260,7 @@ func logExtraKeys(log logger, expected, supplied map[string]interface{}, prefix
                if expected["SAMPLE"] != nil {
                        vexp = expected["SAMPLE"]
                } else if !ok {
-                       log.Warnf("deprecated or unknown config entry: %s%s", prefix, k)
+                       ldr.Logger.Warnf("deprecated or unknown config entry: %s%s", prefix, k)
                        continue
                }
                if vsupp, ok := vsupp.(map[string]interface{}); !ok {
@@ -172,9 +268,9 @@ func logExtraKeys(log logger, expected, supplied map[string]interface{}, prefix
                        // will be caught elsewhere; see TestBadType.
                        continue
                } else if vexp, ok := vexp.(map[string]interface{}); !ok {
-                       log.Warnf("unexpected object in config entry: %s%s", prefix, k)
+                       ldr.Logger.Warnf("unexpected object in config entry: %s%s", prefix, k)
                } else {
-                       logExtraKeys(log, vexp, vsupp, prefix+k+".")
+                       ldr.logExtraKeys(vexp, vsupp, prefix+k+".")
                }
        }
 }
index 6e8b6208d5661e67911eb2c8190e608455471871..eea3efbd98f61c680ea6a21727aac74c4a3c8e83 100644 (file)
@@ -6,7 +6,9 @@ package config
 
 import (
        "bytes"
+       "fmt"
        "io"
+       "io/ioutil"
        "os"
        "os/exec"
        "reflect"
@@ -27,16 +29,31 @@ func Test(t *testing.T) {
 
 var _ = check.Suite(&LoadSuite{})
 
+// Return a new Loader that reads cluster config from configdata
+// (instead of the usual default /etc/arvados/config.yml), and logs to
+// logdst or (if that's nil) c.Log.
+func testLoader(c *check.C, configdata string, logdst io.Writer) *Loader {
+       logger := ctxlog.TestLogger(c)
+       if logdst != nil {
+               lgr := logrus.New()
+               lgr.Out = logdst
+               logger = lgr
+       }
+       ldr := NewLoader(bytes.NewBufferString(configdata), logger)
+       ldr.Path = "-"
+       return ldr
+}
+
 type LoadSuite struct{}
 
 func (s *LoadSuite) TestEmpty(c *check.C) {
-       cfg, err := Load(&bytes.Buffer{}, ctxlog.TestLogger(c))
+       cfg, err := testLoader(c, "", nil).Load()
        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))
+       cfg, err := testLoader(c, `Clusters: {"z1111": {}}`, nil).Load()
        c.Assert(err, check.IsNil)
        c.Assert(cfg.Clusters, check.HasLen, 1)
        cc, err := cfg.GetCluster("z1111")
@@ -46,12 +63,99 @@ func (s *LoadSuite) TestNoConfigs(c *check.C) {
        c.Check(cc.API.MaxItemsPerResponse, check.Equals, 1000)
 }
 
+func (s *LoadSuite) TestMungeLegacyConfigArgs(c *check.C) {
+       f, err := ioutil.TempFile("", "")
+       c.Check(err, check.IsNil)
+       defer os.Remove(f.Name())
+       io.WriteString(f, "Debug: true\n")
+       oldfile := f.Name()
+
+       f, err = ioutil.TempFile("", "")
+       c.Check(err, check.IsNil)
+       defer os.Remove(f.Name())
+       io.WriteString(f, "Clusters: {aaaaa: {}}\n")
+       newfile := f.Name()
+
+       for _, trial := range []struct {
+               argsIn  []string
+               argsOut []string
+       }{
+               {
+                       []string{"-config", oldfile},
+                       []string{"-old-config", oldfile},
+               },
+               {
+                       []string{"-config=" + oldfile},
+                       []string{"-old-config=" + oldfile},
+               },
+               {
+                       []string{"-config", newfile},
+                       []string{"-config", newfile},
+               },
+               {
+                       []string{"-config=" + newfile},
+                       []string{"-config=" + newfile},
+               },
+               {
+                       []string{"-foo", oldfile},
+                       []string{"-foo", oldfile},
+               },
+               {
+                       []string{"-foo=" + oldfile},
+                       []string{"-foo=" + oldfile},
+               },
+               {
+                       []string{"-foo", "-config=" + oldfile},
+                       []string{"-foo", "-old-config=" + oldfile},
+               },
+               {
+                       []string{"-foo", "bar", "-config=" + oldfile},
+                       []string{"-foo", "bar", "-old-config=" + oldfile},
+               },
+               {
+                       []string{"-foo=bar", "baz", "-config=" + oldfile},
+                       []string{"-foo=bar", "baz", "-old-config=" + oldfile},
+               },
+               {
+                       []string{"-config=/dev/null"},
+                       []string{"-config=/dev/null"},
+               },
+               {
+                       []string{"-config=-"},
+                       []string{"-config=-"},
+               },
+               {
+                       []string{"-config="},
+                       []string{"-config="},
+               },
+               {
+                       []string{"-foo=bar", "baz", "-config"},
+                       []string{"-foo=bar", "baz", "-config"},
+               },
+               {
+                       []string{},
+                       nil,
+               },
+       } {
+               var logbuf bytes.Buffer
+               logger := logrus.New()
+               logger.Out = &logbuf
+
+               var ldr Loader
+               args := ldr.MungeLegacyConfigArgs(logger, trial.argsIn, "-old-config")
+               c.Check(args, check.DeepEquals, trial.argsOut)
+               if fmt.Sprintf("%v", trial.argsIn) != fmt.Sprintf("%v", trial.argsOut) {
+                       c.Check(logbuf.String(), check.Matches, `.*`+oldfile+` is not a cluster config file -- interpreting -config as -old-config.*\n`)
+               }
+       }
+}
+
 func (s *LoadSuite) TestSampleKeys(c *check.C) {
        for _, yaml := range []string{
                `{"Clusters":{"z1111":{}}}`,
                `{"Clusters":{"z1111":{"InstanceTypes":{"Foo":{"RAM": "12345M"}}}}}`,
        } {
-               cfg, err := Load(bytes.NewBufferString(yaml), ctxlog.TestLogger(c))
+               cfg, err := testLoader(c, yaml, nil).Load()
                c.Assert(err, check.IsNil)
                cc, err := cfg.GetCluster("z1111")
                _, hasSample := cc.InstanceTypes["SAMPLE"]
@@ -64,7 +168,7 @@ func (s *LoadSuite) TestSampleKeys(c *check.C) {
 }
 
 func (s *LoadSuite) TestMultipleClusters(c *check.C) {
-       cfg, err := Load(bytes.NewBufferString(`{"Clusters":{"z1111":{},"z2222":{}}}`), ctxlog.TestLogger(c))
+       cfg, err := testLoader(c, `{"Clusters":{"z1111":{},"z2222":{}}}`, nil).Load()
        c.Assert(err, check.IsNil)
        c1, err := cfg.GetCluster("z1111")
        c.Assert(err, check.IsNil)
@@ -76,9 +180,7 @@ func (s *LoadSuite) TestMultipleClusters(c *check.C) {
 
 func (s *LoadSuite) TestDeprecatedOrUnknownWarning(c *check.C) {
        var logbuf bytes.Buffer
-       logger := logrus.New()
-       logger.Out = &logbuf
-       _, err := Load(bytes.NewBufferString(`
+       _, err := testLoader(c, `
 Clusters:
   zzzzz:
     postgresql: {}
@@ -89,7 +191,7 @@ Clusters:
         Host: z2222.arvadosapi.com
         Proxy: true
         BadKey: badValue
-`), logger)
+`, &logbuf).Load()
        c.Assert(err, check.IsNil)
        logs := strings.Split(strings.TrimSuffix(logbuf.String(), "\n"), "\n")
        for _, log := range logs {
@@ -136,11 +238,11 @@ func (s *LoadSuite) TestDefaultConfigHasAllSAMPLEKeys(c *check.C) {
 
 func (s *LoadSuite) TestNoUnrecognizedKeysInDefaultConfig(c *check.C) {
        var logbuf bytes.Buffer
-       logger := logrus.New()
-       logger.Out = &logbuf
        var supplied map[string]interface{}
        yaml.Unmarshal(DefaultYAML, &supplied)
-       cfg, err := Load(bytes.NewBuffer(DefaultYAML), logger)
+
+       loader := testLoader(c, string(DefaultYAML), &logbuf)
+       cfg, err := loader.Load()
        c.Assert(err, check.IsNil)
        var loaded map[string]interface{}
        buf, err := yaml.Marshal(cfg)
@@ -148,7 +250,7 @@ func (s *LoadSuite) TestNoUnrecognizedKeysInDefaultConfig(c *check.C) {
        err = yaml.Unmarshal(buf, &loaded)
        c.Assert(err, check.IsNil)
 
-       logExtraKeys(logger, loaded, supplied, "")
+       loader.logExtraKeys(loaded, supplied, "")
        c.Check(logbuf.String(), check.Equals, "")
 }
 
@@ -156,25 +258,25 @@ func (s *LoadSuite) TestNoWarningsForDumpedConfig(c *check.C) {
        var logbuf bytes.Buffer
        logger := logrus.New()
        logger.Out = &logbuf
-       cfg, err := Load(bytes.NewBufferString(`{"Clusters":{"zzzzz":{}}}`), logger)
+       cfg, err := testLoader(c, `{"Clusters":{"zzzzz":{}}}`, &logbuf).Load()
        c.Assert(err, check.IsNil)
        yaml, err := yaml.Marshal(cfg)
        c.Assert(err, check.IsNil)
-       cfgDumped, err := Load(bytes.NewBuffer(yaml), logger)
+       cfgDumped, err := testLoader(c, string(yaml), &logbuf).Load()
        c.Assert(err, check.IsNil)
        c.Check(cfg, check.DeepEquals, cfgDumped)
        c.Check(logbuf.String(), check.Equals, "")
 }
 
 func (s *LoadSuite) TestPostgreSQLKeyConflict(c *check.C) {
-       _, err := Load(bytes.NewBufferString(`
+       _, err := testLoader(c, `
 Clusters:
  zzzzz:
   postgresql:
    connection:
      DBName: dbname
      Host: host
-`), ctxlog.TestLogger(c))
+`, nil).Load()
        c.Check(err, check.ErrorMatches, `Clusters.zzzzz.PostgreSQL.Connection: multiple entries for "(dbname|host)".*`)
 }
 
@@ -206,7 +308,7 @@ Clusters:
 `,
        } {
                c.Log(data)
-               v, err := Load(bytes.NewBufferString(data), ctxlog.TestLogger(c))
+               v, err := testLoader(c, data, nil).Load()
                if v != nil {
                        c.Logf("%#v", v.Clusters["zzzzz"].PostgreSQL.ConnectionPool)
                }
@@ -247,9 +349,9 @@ Clusters:
 }
 
 func (s *LoadSuite) checkEquivalent(c *check.C, goty, expectedy string) {
-       got, err := Load(bytes.NewBufferString(goty), ctxlog.TestLogger(c))
+       got, err := testLoader(c, goty, nil).Load()
        c.Assert(err, check.IsNil)
-       expected, err := Load(bytes.NewBufferString(expectedy), ctxlog.TestLogger(c))
+       expected, err := testLoader(c, expectedy, nil).Load()
        c.Assert(err, check.IsNil)
        if !c.Check(got, check.DeepEquals, expected) {
                cmd := exec.Command("diff", "-u", "--label", "expected", "--label", "got", "/dev/fd/3", "/dev/fd/4")
index 603f48890eccb6686a77ff8f80e99a8d020bfd52..b6737bc553d61258373d578fdac416452105ec43 100644 (file)
@@ -10,7 +10,6 @@ import (
        "flag"
        "fmt"
        "io"
-       "io/ioutil"
        "net"
        "net/http"
        "net/url"
@@ -62,20 +61,25 @@ func (c *command) RunCommand(prog string, args []string, stdin io.Reader, stdout
                        log.WithError(err).Info("exiting")
                }
        }()
+
        flags := flag.NewFlagSet("", flag.ContinueOnError)
        flags.SetOutput(stderr)
-       configFile := flags.String("config", arvados.DefaultConfigFile, "Site configuration `file`")
+
+       loader := config.NewLoader(stdin, log)
+       loader.SetupFlags(flags)
+       versionFlag := flags.Bool("version", false, "Write version information to stdout and exit 0")
+
        err = flags.Parse(args)
        if err == flag.ErrHelp {
                err = nil
                return 0
        } else if err != nil {
                return 2
+       } else if *versionFlag {
+               return cmd.Version.RunCommand(prog, args, stdin, stdout, stderr)
        }
-       // Logged warnings are discarded for now: the config template
-       // is incomplete, which causes extra warnings about keys that
-       // are really OK.
-       cfg, err := config.LoadFile(*configFile, ctxlog.New(ioutil.Discard, "json", "error"))
+
+       cfg, err := loader.Load()
        if err != nil {
                return 1
        }
index 07be5d51b61856ef97739a47d47360e61f98ecee..c8206c7da437c48ff963d563e976cc77cdb4ac3b 100644 (file)
@@ -9,11 +9,18 @@ import (
        "errors"
        "fmt"
        "net/url"
+       "os"
 
        "git.curoverse.com/arvados.git/sdk/go/config"
 )
 
-const DefaultConfigFile = "/etc/arvados/config.yml"
+var DefaultConfigFile = func() string {
+       if path := os.Getenv("ARVADOS_CONFIG"); path != "" {
+               return path
+       } else {
+               return "/etc/arvados/config.yml"
+       }
+}()
 
 type Config struct {
        Clusters map[string]Cluster