16356: Merge branch 'master' into 16356-arvboot-health-externalurl
authorLucas Di Pentima <lucas@di-pentima.com.ar>
Wed, 22 Apr 2020 21:09:43 +0000 (18:09 -0300)
committerLucas Di Pentima <lucas@di-pentima.com.ar>
Wed, 22 Apr 2020 21:09:43 +0000 (18:09 -0300)
Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas@di-pentima.com.ar>

16 files changed:
AUTHORS
apps/workbench/Gemfile.lock
doc/examples/config/zzzzz.yml
go.mod
go.sum
lib/boot/cmd.go
lib/boot/supervisor.go
lib/config/cmd_test.go
lib/config/config.default.yml
lib/config/generated_config.go
lib/controller/integration_test.go
lib/dispatchcloud/container/queue.go
sdk/go/arvados/config.go
services/api/Gemfile.lock
services/keepproxy/keepproxy.go
services/keepproxy/keepproxy_test.go

diff --git a/AUTHORS b/AUTHORS
index 436a504c36ab9f93d89e0742c9f9414032b6b8b6..93bdb37dc8f89fa0e8452804beee4943faa9c22e 100644 (file)
--- a/AUTHORS
+++ b/AUTHORS
@@ -19,3 +19,5 @@ Thomas Mooney <tmooney@genome.wustl.edu>
 Chen Chen <aflyhorse@gmail.com>
 Veritas Genetics, Inc. <*@veritasgenetics.com>
 Curii Corporation, Inc. <*@curii.com>
+Dante Tsang <dante@dantetsang.com>
+Codex Genetics Ltd <info@codexgenetics.com>
\ No newline at end of file
index 2af9c8b16f0b282ef3abc1a4d7c3880e690f7420..e722fa24196d1bc9b38ced9ba1cc27102471ac88 100644 (file)
@@ -214,7 +214,7 @@ GEM
       multi_json (~> 1.0)
       websocket-driver (>= 0.2.0)
     public_suffix (4.0.3)
-    rack (2.0.7)
+    rack (2.2.2)
     rack-mini-profiler (1.0.2)
       rack (>= 1.2.0)
     rack-test (0.6.3)
@@ -375,4 +375,4 @@ DEPENDENCIES
   uglifier (~> 2.0)
 
 BUNDLED WITH
-   1.11
+   1.16.6
index c63550edf709ad916f13c452b136c83208527afa..d1e1336d545c435fffc58c1778e5ffd4e9027367 100644 (file)
@@ -1,3 +1,4 @@
+AutoReloadConfig: true
 Clusters:
   zzzzz:
     ManagementToken: e687950a23c3a9bceec28c6223a06c79
diff --git a/go.mod b/go.mod
index 4491b359813c00ca2d39af34f4d6587e49290699..34b7e0779072fcf060d325ddffe77f54adffdfbf 100644 (file)
--- a/go.mod
+++ b/go.mod
@@ -11,6 +11,7 @@ require (
        github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 // indirect
        github.com/arvados/cgofuse v1.2.0-arvados1
        github.com/aws/aws-sdk-go v1.25.30
+       github.com/bgentry/speakeasy v0.1.0 // indirect
        github.com/coreos/go-oidc v2.1.0+incompatible
        github.com/coreos/go-systemd v0.0.0-20180108085132-cc4f39464dc7
        github.com/dgrijalva/jwt-go v3.1.0+incompatible // indirect
@@ -21,6 +22,7 @@ require (
        github.com/docker/go-connections v0.3.0 // indirect
        github.com/docker/go-units v0.3.3-0.20171221200356-d59758554a3d // indirect
        github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
+       github.com/fsnotify/fsnotify v1.4.9
        github.com/ghodss/yaml v1.0.0
        github.com/gliderlabs/ssh v0.2.2 // indirect
        github.com/gogo/protobuf v1.1.1
diff --git a/go.sum b/go.sum
index 18cf89b0e17e6130fa2e18cb2b4b067de54d506d..03b2f77b6d9ce62ecc5c2cf6abae2f4995bb3152 100644 (file)
--- a/go.sum
+++ b/go.sum
@@ -27,6 +27,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24
 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY=
+github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
 github.com/cespare/xxhash/v2 v2.1.0 h1:yTUvW7Vhb89inJ+8irsUqiWjh8iT6sQPZiQzI6ReGkA=
 github.com/cespare/xxhash/v2 v2.1.0/go.mod h1:dgIUBU3pDso/gPgZ1osOZ0iQf77oPR28Tjxl5dIMyVM=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
@@ -54,6 +56,8 @@ github.com/docker/go-units v0.3.3-0.20171221200356-d59758554a3d h1:dVaNRYvaGV23A
 github.com/docker/go-units v0.3.3-0.20171221200356-d59758554a3d/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
 github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
 github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
+github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
+github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
 github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
@@ -213,6 +217,7 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191105231009-c1f44814a5cd h1:3x5uuvBgE6oaXJjCOvpCC1IpgJogqQ+PqGGU3ZxAgII=
 golang.org/x/sys v0.0.0-20191105231009-c1f44814a5cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
index 5147e3ac33bb65ea8dc0305f986b30a69d736785..e0e2755220a1ec3bbdb8737067c54a579209f40e 100644 (file)
@@ -29,24 +29,33 @@ type supervisedTask interface {
        String() string
 }
 
+var errNeedConfigReload = errors.New("config changed, restart needed")
+
 type bootCommand struct{}
 
-func (bootCommand) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
-       super := &Supervisor{
-               Stderr: stderr,
-               logger: ctxlog.New(stderr, "json", "info"),
+func (bcmd bootCommand) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
+       logger := ctxlog.New(stderr, "json", "info")
+       ctx := ctxlog.Context(context.Background(), logger)
+       for {
+               err := bcmd.run(ctx, prog, args, stdin, stdout, stderr)
+               if err == errNeedConfigReload {
+                       continue
+               } else if err != nil {
+                       logger.WithError(err).Info("exiting")
+                       return 1
+               } else {
+                       return 0
+               }
        }
+}
 
-       ctx := ctxlog.Context(context.Background(), super.logger)
+func (bcmd bootCommand) run(ctx context.Context, prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) error {
        ctx, cancel := context.WithCancel(ctx)
        defer cancel()
-
-       var err error
-       defer func() {
-               if err != nil {
-                       super.logger.WithError(err).Info("exiting")
-               }
-       }()
+       super := &Supervisor{
+               Stderr: stderr,
+               logger: ctxlog.FromContext(ctx),
+       }
 
        flags := flag.NewFlagSet(prog, flag.ContinueOnError)
        flags.SetOutput(stderr)
@@ -60,26 +69,25 @@ func (bootCommand) RunCommand(prog string, args []string, stdin io.Reader, stdou
        flags.BoolVar(&super.OwnTemporaryDatabase, "own-temporary-database", false, "bring up a postgres server and create a temporary database")
        timeout := flags.Duration("timeout", 0, "maximum time to wait for cluster to be ready")
        shutdown := flags.Bool("shutdown", false, "shut down when the cluster becomes ready")
-       err = flags.Parse(args)
+       err := flags.Parse(args)
        if err == flag.ErrHelp {
-               err = nil
-               return 0
+               return nil
        } else if err != nil {
-               return 2
+               return err
        } else if *versionFlag {
-               return cmd.Version.RunCommand(prog, args, stdin, stdout, stderr)
+               cmd.Version.RunCommand(prog, args, stdin, stdout, stderr)
+               return nil
        } else if super.ClusterType != "development" && super.ClusterType != "test" && super.ClusterType != "production" {
-               err = fmt.Errorf("cluster type must be 'development', 'test', or 'production'")
-               return 2
+               return fmt.Errorf("cluster type must be 'development', 'test', or 'production'")
        }
 
        loader.SkipAPICalls = true
        cfg, err := loader.Load()
        if err != nil {
-               return 1
+               return err
        }
 
-       super.Start(ctx, cfg)
+       super.Start(ctx, cfg, loader.Path)
        defer super.Stop()
 
        var timer *time.Timer
@@ -89,20 +97,19 @@ func (bootCommand) RunCommand(prog string, args []string, stdin io.Reader, stdou
 
        url, ok := super.WaitReady()
        if timer != nil && !timer.Stop() {
-               err = errors.New("boot timed out")
-               return 1
+               return errors.New("boot timed out")
        } else if !ok {
-               err = errors.New("boot failed")
-               return 1
-       }
-       // Write controller URL to stdout. Nothing else goes to
-       // stdout, so this provides an easy way for a calling script
-       // to discover the controller URL when everything is ready.
-       fmt.Fprintln(stdout, url)
-       if *shutdown {
-               super.Stop()
+               super.logger.Error("boot failed")
+       } else {
+               // Write controller URL to stdout. Nothing else goes
+               // to stdout, so this provides an easy way for a
+               // calling script to discover the controller URL when
+               // everything is ready.
+               fmt.Fprintln(stdout, url)
+               if *shutdown {
+                       super.Stop()
+               }
        }
        // Wait for signal/crash + orderly shutdown
-       <-super.done
-       return 0
+       return super.Wait()
 }
index 26f38ac2319e520f99d574564031c48a550df4c6..c444ec3001acd3b3b8b86faa40acae38eb110922 100644 (file)
@@ -19,15 +19,18 @@ import (
        "os/signal"
        "os/user"
        "path/filepath"
+       "reflect"
        "strings"
        "sync"
        "syscall"
        "time"
 
+       "git.arvados.org/arvados.git/lib/config"
        "git.arvados.org/arvados.git/lib/service"
        "git.arvados.org/arvados.git/sdk/go/arvados"
        "git.arvados.org/arvados.git/sdk/go/ctxlog"
        "git.arvados.org/arvados.git/sdk/go/health"
+       "github.com/fsnotify/fsnotify"
        "github.com/sirupsen/logrus"
 )
 
@@ -45,7 +48,8 @@ type Supervisor struct {
 
        ctx           context.Context
        cancel        context.CancelFunc
-       done          chan struct{}
+       done          chan struct{} // closed when child procs/services have shut down
+       err           error         // error that caused shutdown (valid when done is closed)
        healthChecker *health.Aggregator
        tasksReady    map[string]chan bool
        waitShutdown  sync.WaitGroup
@@ -55,30 +59,66 @@ type Supervisor struct {
        environ    []string // for child processes
 }
 
-func (super *Supervisor) Start(ctx context.Context, cfg *arvados.Config) {
+func (super *Supervisor) Start(ctx context.Context, cfg *arvados.Config, cfgPath string) {
        super.ctx, super.cancel = context.WithCancel(ctx)
        super.done = make(chan struct{})
 
        go func() {
+               defer close(super.done)
+
                sigch := make(chan os.Signal)
                signal.Notify(sigch, syscall.SIGINT, syscall.SIGTERM)
                defer signal.Stop(sigch)
                go func() {
                        for sig := range sigch {
                                super.logger.WithField("signal", sig).Info("caught signal")
+                               if super.err == nil {
+                                       super.err = fmt.Errorf("caught signal %s", sig)
+                               }
+                               super.cancel()
+                       }
+               }()
+
+               hupch := make(chan os.Signal)
+               signal.Notify(hupch, syscall.SIGHUP)
+               defer signal.Stop(hupch)
+               go func() {
+                       for sig := range hupch {
+                               super.logger.WithField("signal", sig).Info("caught signal")
+                               if super.err == nil {
+                                       super.err = errNeedConfigReload
+                               }
                                super.cancel()
                        }
                }()
 
+               if cfgPath != "" && cfgPath != "-" && cfg.AutoReloadConfig {
+                       go watchConfig(super.ctx, super.logger, cfgPath, copyConfig(cfg), func() {
+                               if super.err == nil {
+                                       super.err = errNeedConfigReload
+                               }
+                               super.cancel()
+                       })
+               }
+
                err := super.run(cfg)
                if err != nil {
                        super.logger.WithError(err).Warn("supervisor shut down")
+                       if super.err == nil {
+                               super.err = err
+                       }
                }
-               close(super.done)
        }()
 }
 
+func (super *Supervisor) Wait() error {
+       <-super.done
+       return super.err
+}
+
 func (super *Supervisor) run(cfg *arvados.Config) error {
+       defer super.cancel()
+
        cwd, err := os.Getwd()
        if err != nil {
                return err
@@ -707,3 +747,67 @@ func waitForConnect(ctx context.Context, addr string) error {
        }
        return ctx.Err()
 }
+
+func copyConfig(cfg *arvados.Config) *arvados.Config {
+       pr, pw := io.Pipe()
+       go func() {
+               err := json.NewEncoder(pw).Encode(cfg)
+               if err != nil {
+                       panic(err)
+               }
+               pw.Close()
+       }()
+       cfg2 := new(arvados.Config)
+       err := json.NewDecoder(pr).Decode(cfg2)
+       if err != nil {
+               panic(err)
+       }
+       return cfg2
+}
+
+func watchConfig(ctx context.Context, logger logrus.FieldLogger, cfgPath string, prevcfg *arvados.Config, fn func()) {
+       watcher, err := fsnotify.NewWatcher()
+       if err != nil {
+               logger.WithError(err).Error("fsnotify setup failed")
+               return
+       }
+       defer watcher.Close()
+
+       err = watcher.Add(cfgPath)
+       if err != nil {
+               logger.WithError(err).Error("fsnotify watcher failed")
+               return
+       }
+
+       for {
+               select {
+               case <-ctx.Done():
+                       return
+               case err, ok := <-watcher.Errors:
+                       if !ok {
+                               return
+                       }
+                       logger.WithError(err).Warn("fsnotify watcher reported error")
+               case _, ok := <-watcher.Events:
+                       if !ok {
+                               return
+                       }
+                       for len(watcher.Events) > 0 {
+                               <-watcher.Events
+                       }
+                       loader := config.NewLoader(&bytes.Buffer{}, &logrus.Logger{Out: ioutil.Discard})
+                       loader.Path = cfgPath
+                       loader.SkipAPICalls = true
+                       cfg, err := loader.Load()
+                       if err != nil {
+                               logger.WithError(err).Warn("error reloading config file after change detected; ignoring new config for now")
+                       } else if reflect.DeepEqual(cfg, prevcfg) {
+                               logger.Debug("config file changed but is still DeepEqual to the existing config")
+                       } else {
+                               logger.Debug("config changed, notifying supervisor")
+                               fn()
+                               prevcfg = cfg
+                       }
+               }
+       }
+}
index f4f2d5653b66692315e45a7aa95f366579bf438a..3c420a04eb43e28e39fa69f8e1baa3330b803d64 100644 (file)
@@ -162,7 +162,7 @@ Clusters:
        code := DumpCommand.RunCommand("arvados config-dump", []string{"-config", "-"}, bytes.NewBufferString(in), &stdout, &stderr)
        c.Check(code, check.Equals, 0)
        c.Check(stderr.String(), check.Matches, `(?ms).*deprecated or unknown config entry: Clusters.z1234.UnknownKey.*`)
-       c.Check(stdout.String(), check.Matches, `(?ms)Clusters:\n  z1234:\n.*`)
+       c.Check(stdout.String(), check.Matches, `(?ms)(.*\n)?Clusters:\n  z1234:\n.*`)
        c.Check(stdout.String(), check.Matches, `(?ms).*\n *ManagementToken: secret\n.*`)
        c.Check(stdout.String(), check.Not(check.Matches), `(?ms).*UnknownKey.*`)
 }
index fcccdd0634e48fe1611f8bf531126f0d8803b081..d4870919eaad2bb0fdb7c1695bf7d28028b38236 100644 (file)
@@ -1228,3 +1228,8 @@ Clusters:
     # implementation. Note that it also disables some new federation
     # features and will be removed in a future release.
     ForceLegacyAPI14: false
+
+# (Experimental) Restart services automatically when config file
+# changes are detected. Only supported by `arvados-server boot` in
+# dev/test mode.
+AutoReloadConfig: false
index 4a8d7024fb5a75d9d275adcdd70241c3ace18fcd..42707396dddc0558d8a901d3ab6e39d79e36e03d 100644 (file)
@@ -1234,4 +1234,9 @@ Clusters:
     # implementation. Note that it also disables some new federation
     # features and will be removed in a future release.
     ForceLegacyAPI14: false
+
+# (Experimental) Restart services automatically when config file
+# changes are detected. Only supported by ` + "`" + `arvados-server boot` + "`" + ` in
+# dev/test mode.
+AutoReloadConfig: false
 `)
index d2ae1f6fbb33ad7dbcc4cc3151b8e7a06e64c490..aad2c4775ad20c0884cca173326e64fa97f98bc7 100644 (file)
@@ -114,7 +114,7 @@ func (s *IntegrationSuite) SetUpSuite(c *check.C) {
                        },
                        config: *cfg,
                }
-               s.testClusters[id].super.Start(context.Background(), &s.testClusters[id].config)
+               s.testClusters[id].super.Start(context.Background(), &s.testClusters[id].config, "-")
        }
        for _, tc := range s.testClusters {
                au, ok := tc.super.WaitReady()
index d128c265f84c13594ec309f5ac36d93b65d82b21..45b346383fab8641b27d16063a46bc4468fc96ce 100644 (file)
@@ -382,7 +382,7 @@ func (cq *Queue) poll() (map[string]*arvados.Container, error) {
                        *next[upd.UUID] = upd
                }
        }
-       selectParam := []string{"uuid", "state", "priority", "runtime_constraints", "container_image", "mounts"}
+       selectParam := []string{"uuid", "state", "priority", "runtime_constraints", "container_image", "mounts", "scheduling_parameters"}
        limitParam := 1000
 
        mine, err := cq.fetchAll(arvados.ResourceListParams{
index 6b83fb96d49e6359e656c3e634a273b6f29c4e16..38de6b8ea40983081a14db18b270e3db31a6a895 100644 (file)
@@ -23,7 +23,8 @@ var DefaultConfigFile = func() string {
 }()
 
 type Config struct {
-       Clusters map[string]Cluster
+       Clusters         map[string]Cluster
+       AutoReloadConfig bool
 }
 
 // GetConfig returns the current system config, loading it from
@@ -66,6 +67,7 @@ type WebDAVCacheConfig struct {
        MaxPermissionEntries int
        MaxUUIDEntries       int
 }
+
 type Cluster struct {
        ClusterID       string `json:"-"`
        ManagementToken string
index 24d5ad5b64982c1e5d10e8f218336978d1e2b857..9f3a5fb2b3d787e1a320d8dc3734126ac1ee33f4 100644 (file)
@@ -180,7 +180,7 @@ GEM
     pg (1.1.4)
     power_assert (1.1.4)
     public_suffix (4.0.3)
-    rack (2.0.7)
+    rack (2.2.2)
     rack-test (0.6.3)
       rack (>= 1.0)
     rails (5.0.7.2)
@@ -317,4 +317,4 @@ DEPENDENCIES
   uglifier (~> 2.0)
 
 BUNDLED WITH
-   1.11
+   1.16.6
index 2b15d79940844285bbb57c1f771bb31a4c15ff78..547e77e5f510922da5463f5f585f8eb6aa87efbd 100644 (file)
@@ -116,6 +116,12 @@ func run(logger log.FieldLogger, cluster *arvados.Cluster) error {
                return fmt.Errorf("Error setting up arvados client %v", err)
        }
 
+       // If a config file is available, use the keepstores defined there
+       // instead of the legacy autodiscover mechanism via the API server
+       for k := range cluster.Services.Keepstore.InternalURLs {
+               arv.KeepServiceURIs = append(arv.KeepServiceURIs, k.String())
+       }
+
        if cluster.SystemLogs.LogLevel == "debug" {
                keepclient.DebugPrintf = log.Printf
        }
index aa32356806abdb32e73ea0de9534e76567e5e83e..94ed05bff1dbdb0585226741a792d2b77fa7fd29 100644 (file)
@@ -40,6 +40,12 @@ var _ = Suite(&ServerRequiredSuite{})
 // Tests that require the Keep server running
 type ServerRequiredSuite struct{}
 
+// Gocheck boilerplate
+var _ = Suite(&ServerRequiredConfigYmlSuite{})
+
+// Tests that require the Keep servers running as defined in config.yml
+type ServerRequiredConfigYmlSuite struct{}
+
 // Gocheck boilerplate
 var _ = Suite(&NoKeepServerSuite{})
 
@@ -83,6 +89,21 @@ func (s *ServerRequiredSuite) TearDownSuite(c *C) {
        arvadostest.StopAPI()
 }
 
+func (s *ServerRequiredConfigYmlSuite) SetUpSuite(c *C) {
+       arvadostest.StartAPI()
+       // config.yml defines 4 keepstores
+       arvadostest.StartKeep(4, false)
+}
+
+func (s *ServerRequiredConfigYmlSuite) SetUpTest(c *C) {
+       arvadostest.ResetEnv()
+}
+
+func (s *ServerRequiredConfigYmlSuite) TearDownSuite(c *C) {
+       arvadostest.StopKeep(4)
+       arvadostest.StopAPI()
+}
+
 func (s *NoKeepServerSuite) SetUpSuite(c *C) {
        arvadostest.StartAPI()
        // We need API to have some keep services listed, but the
@@ -99,12 +120,17 @@ func (s *NoKeepServerSuite) TearDownSuite(c *C) {
        arvadostest.StopAPI()
 }
 
-func runProxy(c *C, bogusClientToken bool) *keepclient.KeepClient {
+func runProxy(c *C, bogusClientToken bool, loadKeepstoresFromConfig bool) *keepclient.KeepClient {
        cfg, err := config.NewLoader(nil, ctxlog.TestLogger(c)).Load()
        c.Assert(err, Equals, nil)
        cluster, err := cfg.GetCluster("")
        c.Assert(err, Equals, nil)
 
+       if !loadKeepstoresFromConfig {
+               // Do not load Keepstore InternalURLs from the config file
+               cluster.Services.Keepstore.InternalURLs = make(map[arvados.URL]arvados.ServiceInstance)
+       }
+
        cluster.Services.Keepproxy.InternalURLs = map[arvados.URL]arvados.ServiceInstance{arvados.URL{Host: ":0"}: arvados.ServiceInstance{}}
 
        listener = nil
@@ -131,7 +157,7 @@ func runProxy(c *C, bogusClientToken bool) *keepclient.KeepClient {
 }
 
 func (s *ServerRequiredSuite) TestResponseViaHeader(c *C) {
-       runProxy(c, false)
+       runProxy(c, false, false)
        defer closeListener()
 
        req, err := http.NewRequest("POST",
@@ -158,7 +184,7 @@ func (s *ServerRequiredSuite) TestResponseViaHeader(c *C) {
 }
 
 func (s *ServerRequiredSuite) TestLoopDetection(c *C) {
-       kc := runProxy(c, false)
+       kc := runProxy(c, false, false)
        defer closeListener()
 
        sr := map[string]string{
@@ -176,7 +202,7 @@ func (s *ServerRequiredSuite) TestLoopDetection(c *C) {
 }
 
 func (s *ServerRequiredSuite) TestStorageClassesHeader(c *C) {
-       kc := runProxy(c, false)
+       kc := runProxy(c, false, false)
        defer closeListener()
 
        // Set up fake keepstore to record request headers
@@ -203,7 +229,7 @@ func (s *ServerRequiredSuite) TestStorageClassesHeader(c *C) {
 }
 
 func (s *ServerRequiredSuite) TestDesiredReplicas(c *C) {
-       kc := runProxy(c, false)
+       kc := runProxy(c, false, false)
        defer closeListener()
 
        content := []byte("TestDesiredReplicas")
@@ -220,7 +246,7 @@ func (s *ServerRequiredSuite) TestDesiredReplicas(c *C) {
 }
 
 func (s *ServerRequiredSuite) TestPutWrongContentLength(c *C) {
-       kc := runProxy(c, false)
+       kc := runProxy(c, false, false)
        defer closeListener()
 
        content := []byte("TestPutWrongContentLength")
@@ -259,7 +285,7 @@ func (s *ServerRequiredSuite) TestPutWrongContentLength(c *C) {
 }
 
 func (s *ServerRequiredSuite) TestManyFailedPuts(c *C) {
-       kc := runProxy(c, false)
+       kc := runProxy(c, false, false)
        defer closeListener()
        router.(*proxyHandler).timeout = time.Nanosecond
 
@@ -286,7 +312,7 @@ func (s *ServerRequiredSuite) TestManyFailedPuts(c *C) {
 }
 
 func (s *ServerRequiredSuite) TestPutAskGet(c *C) {
-       kc := runProxy(c, false)
+       kc := runProxy(c, false, false)
        defer closeListener()
 
        hash := fmt.Sprintf("%x", md5.Sum([]byte("foo")))
@@ -363,7 +389,7 @@ func (s *ServerRequiredSuite) TestPutAskGet(c *C) {
 }
 
 func (s *ServerRequiredSuite) TestPutAskGetForbidden(c *C) {
-       kc := runProxy(c, true)
+       kc := runProxy(c, true, false)
        defer closeListener()
 
        hash := fmt.Sprintf("%x+3", md5.Sum([]byte("bar")))
@@ -389,7 +415,7 @@ func (s *ServerRequiredSuite) TestPutAskGetForbidden(c *C) {
 }
 
 func (s *ServerRequiredSuite) TestCorsHeaders(c *C) {
-       runProxy(c, false)
+       runProxy(c, false, false)
        defer closeListener()
 
        {
@@ -420,7 +446,7 @@ func (s *ServerRequiredSuite) TestCorsHeaders(c *C) {
 }
 
 func (s *ServerRequiredSuite) TestPostWithoutHash(c *C) {
-       runProxy(c, false)
+       runProxy(c, false, false)
        defer closeListener()
 
        {
@@ -463,7 +489,22 @@ func (s *ServerRequiredSuite) TestStripHint(c *C) {
 //   With a valid but non-existing prefix (expect "\n")
 //   With an invalid prefix (expect error)
 func (s *ServerRequiredSuite) TestGetIndex(c *C) {
-       kc := runProxy(c, false)
+       getIndexWorker(c, false)
+}
+
+// Test GetIndex
+//   Uses config.yml
+//   Put one block, with 2 replicas
+//   With no prefix (expect the block locator, twice)
+//   With an existing prefix (expect the block locator, twice)
+//   With a valid but non-existing prefix (expect "\n")
+//   With an invalid prefix (expect error)
+func (s *ServerRequiredConfigYmlSuite) TestGetIndex(c *C) {
+       getIndexWorker(c, true)
+}
+
+func getIndexWorker(c *C, useConfig bool) {
+       kc := runProxy(c, false, useConfig)
        defer closeListener()
 
        // Put "index-data" blocks
@@ -526,7 +567,7 @@ func (s *ServerRequiredSuite) TestGetIndex(c *C) {
 }
 
 func (s *ServerRequiredSuite) TestCollectionSharingToken(c *C) {
-       kc := runProxy(c, false)
+       kc := runProxy(c, false, false)
        defer closeListener()
        hash, _, err := kc.PutB([]byte("shareddata"))
        c.Check(err, IsNil)
@@ -539,7 +580,7 @@ func (s *ServerRequiredSuite) TestCollectionSharingToken(c *C) {
 }
 
 func (s *ServerRequiredSuite) TestPutAskGetInvalidToken(c *C) {
-       kc := runProxy(c, false)
+       kc := runProxy(c, false, false)
        defer closeListener()
 
        // Put a test block
@@ -576,7 +617,7 @@ func (s *ServerRequiredSuite) TestPutAskGetInvalidToken(c *C) {
 }
 
 func (s *ServerRequiredSuite) TestAskGetKeepProxyConnectionError(c *C) {
-       kc := runProxy(c, false)
+       kc := runProxy(c, false, false)
        defer closeListener()
 
        // Point keepproxy at a non-existent keepstore
@@ -602,7 +643,7 @@ func (s *ServerRequiredSuite) TestAskGetKeepProxyConnectionError(c *C) {
 }
 
 func (s *NoKeepServerSuite) TestAskGetNoKeepServerError(c *C) {
-       kc := runProxy(c, false)
+       kc := runProxy(c, false, false)
        defer closeListener()
 
        hash := fmt.Sprintf("%x", md5.Sum([]byte("foo")))
@@ -625,7 +666,7 @@ func (s *NoKeepServerSuite) TestAskGetNoKeepServerError(c *C) {
 }
 
 func (s *ServerRequiredSuite) TestPing(c *C) {
-       kc := runProxy(c, false)
+       kc := runProxy(c, false, false)
        defer closeListener()
 
        rtr := MakeRESTRouter(kc, 10*time.Second, arvadostest.ManagementToken)