Merge branch '19249-header-ctrl-chars'
authorTom Clegg <tom@curii.com>
Fri, 22 Jul 2022 17:48:21 +0000 (13:48 -0400)
committerTom Clegg <tom@curii.com>
Fri, 22 Jul 2022 17:48:21 +0000 (13:48 -0400)
fixes #19249

Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom@curii.com>

32 files changed:
apps/workbench/Gemfile.lock
cmd/arvados-client/cmd.go
cmd/arvados-client/sudo.go [new file with mode: 0644]
cmd/arvados-package/fpm.go
cmd/arvados-package/install.go
cmd/arvados-server/cmd.go
doc/install/automatic.html.textile.liquid
lib/boot/cert.go
lib/boot/cmd.go
lib/boot/nginx.go
lib/boot/passenger.go
lib/boot/service.go
lib/boot/supervisor.go
lib/cli/external.go
lib/config/config.default.yml
lib/crunchrun/crunchrun.go
lib/crunchrun/integration_test.go
lib/diagnostics/cmd.go
lib/install/arvados.service
lib/install/deps.go
lib/install/init.go
lib/service/cmd.go
lib/service/tls.go
sdk/go/arvados/config.go
sdk/python/tests/nginx.conf
sdk/python/tests/run_test_server.py
services/api/Gemfile.lock
services/api/app/models/user.rb
services/api/test/integration/users_test.rb
services/keep-web/handler.go
services/keep-web/handler_test.go
tools/salt-install/config_examples/multi_host/aws/pillars/nginx_webshell_configuration.sls

index ebb4b64c607314606f9fb8c4c54e092ff7be1353..a70add7affbafd5364b1bc86149a26af4860c893 100644 (file)
@@ -16,43 +16,43 @@ GEM
   remote: https://rubygems.org/
   specs:
     RedCloth (4.3.2)
-    actioncable (5.2.8)
-      actionpack (= 5.2.8)
+    actioncable (5.2.8.1)
+      actionpack (= 5.2.8.1)
       nio4r (~> 2.0)
       websocket-driver (>= 0.6.1)
-    actionmailer (5.2.8)
-      actionpack (= 5.2.8)
-      actionview (= 5.2.8)
-      activejob (= 5.2.8)
+    actionmailer (5.2.8.1)
+      actionpack (= 5.2.8.1)
+      actionview (= 5.2.8.1)
+      activejob (= 5.2.8.1)
       mail (~> 2.5, >= 2.5.4)
       rails-dom-testing (~> 2.0)
-    actionpack (5.2.8)
-      actionview (= 5.2.8)
-      activesupport (= 5.2.8)
+    actionpack (5.2.8.1)
+      actionview (= 5.2.8.1)
+      activesupport (= 5.2.8.1)
       rack (~> 2.0, >= 2.0.8)
       rack-test (>= 0.6.3)
       rails-dom-testing (~> 2.0)
       rails-html-sanitizer (~> 1.0, >= 1.0.2)
-    actionview (5.2.8)
-      activesupport (= 5.2.8)
+    actionview (5.2.8.1)
+      activesupport (= 5.2.8.1)
       builder (~> 3.1)
       erubi (~> 1.4)
       rails-dom-testing (~> 2.0)
       rails-html-sanitizer (~> 1.0, >= 1.0.3)
-    activejob (5.2.8)
-      activesupport (= 5.2.8)
+    activejob (5.2.8.1)
+      activesupport (= 5.2.8.1)
       globalid (>= 0.3.6)
-    activemodel (5.2.8)
-      activesupport (= 5.2.8)
-    activerecord (5.2.8)
-      activemodel (= 5.2.8)
-      activesupport (= 5.2.8)
+    activemodel (5.2.8.1)
+      activesupport (= 5.2.8.1)
+    activerecord (5.2.8.1)
+      activemodel (= 5.2.8.1)
+      activesupport (= 5.2.8.1)
       arel (>= 9.0)
-    activestorage (5.2.8)
-      actionpack (= 5.2.8)
-      activerecord (= 5.2.8)
+    activestorage (5.2.8.1)
+      actionpack (= 5.2.8.1)
+      activerecord (= 5.2.8.1)
       marcel (~> 1.0.0)
-    activesupport (5.2.8)
+    activesupport (5.2.8.1)
       concurrent-ruby (~> 1.0, >= 1.0.2)
       i18n (>= 0.7, < 2)
       minitest (~> 5.1)
@@ -179,7 +179,7 @@ GEM
     net-ssh-gateway (2.0.0)
       net-ssh (>= 4.0.0)
     nio4r (2.5.8)
-    nokogiri (1.13.6)
+    nokogiri (1.13.7)
       mini_portile2 (~> 2.8.0)
       racc (~> 1.4)
     npm-rails (0.2.1)
@@ -200,23 +200,23 @@ GEM
       websocket-driver (>= 0.2.0)
     public_suffix (4.0.6)
     racc (1.6.0)
-    rack (2.2.3.1)
+    rack (2.2.4)
     rack-mini-profiler (1.0.2)
       rack (>= 1.2.0)
-    rack-test (1.1.0)
-      rack (>= 1.0, < 3)
-    rails (5.2.8)
-      actioncable (= 5.2.8)
-      actionmailer (= 5.2.8)
-      actionpack (= 5.2.8)
-      actionview (= 5.2.8)
-      activejob (= 5.2.8)
-      activemodel (= 5.2.8)
-      activerecord (= 5.2.8)
-      activestorage (= 5.2.8)
-      activesupport (= 5.2.8)
+    rack-test (2.0.2)
+      rack (>= 1.3)
+    rails (5.2.8.1)
+      actioncable (= 5.2.8.1)
+      actionmailer (= 5.2.8.1)
+      actionpack (= 5.2.8.1)
+      actionview (= 5.2.8.1)
+      activejob (= 5.2.8.1)
+      activemodel (= 5.2.8.1)
+      activerecord (= 5.2.8.1)
+      activestorage (= 5.2.8.1)
+      activesupport (= 5.2.8.1)
       bundler (>= 1.3.0)
-      railties (= 5.2.8)
+      railties (= 5.2.8.1)
       sprockets-rails (>= 2.0.0)
     rails-controller-testing (1.0.4)
       actionpack (>= 5.0.1.x)
@@ -228,9 +228,9 @@ GEM
     rails-html-sanitizer (1.4.3)
       loofah (~> 2.3)
     rails-perftest (0.0.7)
-    railties (5.2.8)
-      actionpack (= 5.2.8)
-      activesupport (= 5.2.8)
+    railties (5.2.8.1)
+      actionpack (= 5.2.8.1)
+      activesupport (= 5.2.8.1)
       method_source
       rake (>= 0.8.7)
       thor (>= 0.19.0, < 2.0)
@@ -291,7 +291,7 @@ GEM
     thor (1.2.1)
     thread_safe (0.3.6)
     tilt (2.0.9)
-    tzinfo (1.2.9)
+    tzinfo (1.2.10)
       thread_safe (~> 0.1)
     uglifier (2.7.2)
       execjs (>= 0.3.0)
index cb15462119d4d1d3368382e1cbbcceafba40464f..c10783c978be6c48e6f7aea0c9ad1a959303b656 100644 (file)
@@ -61,6 +61,7 @@ var (
                "shell":                shellCommand{},
                "connect-ssh":          connectSSHCommand{},
                "diagnostics":          diagnostics.Command{},
+               "sudo":                 sudoCommand{},
        })
 )
 
diff --git a/cmd/arvados-client/sudo.go b/cmd/arvados-client/sudo.go
new file mode 100644 (file)
index 0000000..94c2d27
--- /dev/null
@@ -0,0 +1,48 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package main
+
+import (
+       "flag"
+       "fmt"
+       "io"
+       "os"
+
+       "git.arvados.org/arvados.git/lib/cmd"
+       "git.arvados.org/arvados.git/lib/config"
+       "git.arvados.org/arvados.git/sdk/go/ctxlog"
+)
+
+// sudoCommand runs another command using API connection info and
+// SystemRootToken from the system config file instead of the caller's
+// environment vars.
+type sudoCommand struct{}
+
+func (sudoCommand) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
+       ldr := config.NewLoader(stdin, ctxlog.New(stderr, "text", "info"))
+       flags := flag.NewFlagSet(prog, flag.ContinueOnError)
+       ldr.SetupFlags(flags)
+       if ok, code := cmd.ParseFlags(flags, prog, args, "subcommand ...", stderr); !ok {
+               return code
+       }
+       cfg, err := ldr.Load()
+       if err != nil {
+               fmt.Fprintln(stderr, err)
+               return 1
+       }
+       cluster, err := cfg.GetCluster("")
+       if err != nil {
+               fmt.Fprintln(stderr, err)
+               return 1
+       }
+       os.Setenv("ARVADOS_API_HOST", cluster.Services.Controller.ExternalURL.Host)
+       os.Setenv("ARVADOS_API_TOKEN", cluster.SystemRootToken)
+       if cluster.TLS.Insecure {
+               os.Setenv("ARVADOS_API_HOST_INSECURE", "1")
+       } else {
+               os.Unsetenv("ARVADOS_API_HOST_INSECURE")
+       }
+       return handler.RunCommand(prog, flags.Args(), stdin, stdout, stderr)
+}
index d81abab583cc78a3ddb5b0421ee38963d8187f83..64d0adabe39190452891db68ee6eeb91d20717ee 100644 (file)
@@ -34,15 +34,7 @@ func fpm(ctx context.Context, opts opts, stdin io.Reader, stdout, stderr io.Writ
                return fmt.Errorf("arvados-server install failed: exit code %d", exitcode)
        }
 
-       cmd := exec.Command("/var/lib/arvados/bin/gem", "install", "--user", "--no-document", "fpm")
-       cmd.Stdout = stdout
-       cmd.Stderr = stderr
-       err := cmd.Run()
-       if err != nil {
-               return fmt.Errorf("gem install fpm: %w", err)
-       }
-
-       cmd = exec.Command("/var/lib/arvados/bin/gem", "env", "gempath")
+       cmd := exec.Command("/var/lib/arvados/bin/gem", "env", "gempath")
        cmd.Stderr = stderr
        buf, err := cmd.Output() // /root/.gem/ruby/2.7.0:...
        if err != nil || len(buf) == 0 {
@@ -50,6 +42,17 @@ func fpm(ctx context.Context, opts opts, stdin io.Reader, stdout, stderr io.Writ
        }
        gempath := string(bytes.TrimRight(bytes.Split(buf, []byte{':'})[0], "\n"))
 
+       cmd = exec.Command("/var/lib/arvados/bin/gem", "install", "--user", "--no-document", "fpm")
+       cmd.Stdout = stdout
+       cmd.Stderr = stderr
+       // Avoid "WARNING: You don't have [...] in your PATH, gem
+       // executables will not run"
+       cmd.Env = append(os.Environ(), "PATH="+os.Getenv("PATH")+":"+gempath)
+       err = cmd.Run()
+       if err != nil {
+               return fmt.Errorf("gem install fpm: %w", err)
+       }
+
        if _, err := os.Stat(gempath + "/gems/fpm-1.11.0/lib/fpm/package/deb.rb"); err == nil {
                // Workaround for fpm bug https://github.com/jordansissel/fpm/issues/1739
                cmd = exec.Command("sed", "-i", `/require "digest"/a require "zlib"`, gempath+"/gems/fpm-1.11.0/lib/fpm/package/deb.rb")
@@ -97,11 +100,28 @@ func fpm(ctx context.Context, opts opts, stdin io.Reader, stdout, stderr io.Writ
                "--verbose",
                "--deb-use-file-permissions",
                "--rpm-use-file-permissions",
-               "/etc/systemd/system/multi-user.target.wants/arvados.service",
-               "/lib/systemd/system/arvados.service",
+               "--deb-systemd", "/lib/systemd/system/arvados.service",
+               "--deb-systemd-enable",
+               "--no-deb-systemd-auto-start",
+               "--no-deb-systemd-restart-after-upgrade",
+               "--deb-suggests", "postgresql",
+               "--deb-suggests", "docker.io",
                "/usr/bin/arvados-client",
                "/usr/bin/arvados-server",
+               "/usr/bin/arv",
+               "/usr/bin/arv-ruby",
+               "/usr/bin/arv-tag",
                "/var/lib/arvados",
+               "/usr/bin/arv-copy",
+               "/usr/bin/arv-federation-migrate",
+               "/usr/bin/arv-get",
+               "/usr/bin/arv-keepdocker",
+               "/usr/bin/arv-ls",
+               "/usr/bin/arv-migrate-docker19",
+               "/usr/bin/arv-normalize",
+               "/usr/bin/arv-put",
+               "/usr/bin/arv-ws",
+               "/usr/bin/arv-mount",
                "/var/www/.gem",
                "/var/www/.passenger",
                "/var/www/.bundle",
index d8dbdcc4a066c18bb8df7fdcba31eb3cd8a8d45e..38efae0461dba43593332f353e48f883b006c6c9 100644 (file)
@@ -92,6 +92,7 @@ rm /etc/apt/sources.list.d/arvados-local.list
        if opts.Live != "" {
                cmd.Args = append(cmd.Args,
                        "--env=domain="+opts.Live,
+                       "--env=initargs=-tls=/var/lib/acme/live/"+opts.Live,
                        "--env=bootargs=",
                        "--publish=:443:443",
                        "--publish=:4440-4460:4440-4460",
@@ -101,6 +102,7 @@ rm /etc/apt/sources.list.d/arvados-local.list
        } else {
                cmd.Args = append(cmd.Args,
                        "--env=domain=localhost",
+                       "--env=initargs=-tls=insecure",
                        "--env=bootargs=-shutdown")
        }
        cmd.Args = append(cmd.Args,
@@ -122,8 +124,8 @@ eatmydata apt-get install --reinstall -y --no-install-recommends arvados-server-
 SUDO_FORCE_REMOVE=yes apt-get autoremove -y
 
 /etc/init.d/postgresql start
-arvados-server init -cluster-id x1234 -domain=$domain -login=test -insecure
-exec arvados-server boot -listen-host=0.0.0.0 -no-workbench2=false $bootargs
+arvados-server init -cluster-id x1234 -domain=$domain -login=test -start=false $initargs
+exec arvados-server boot -listen-host=0.0.0.0 $bootargs
 `)
        cmd.Stdout = stdout
        cmd.Stderr = stderr
index 3a1fcd4c64e29b981ddb0234f1bf3eae6a14da7b..d9c41ca587b1194415e44377267d565c4ce4eeb5 100644 (file)
@@ -11,6 +11,9 @@ import (
        "io"
        "net/http"
        "os"
+       "path"
+       "path/filepath"
+       "strings"
 
        "git.arvados.org/arvados.git/lib/boot"
        "git.arvados.org/arvados.git/lib/cloud/cloudtest"
@@ -80,8 +83,21 @@ func (wb2command) RunCommand(prog string, args []string, stdin io.Reader, stdout
                fmt.Fprintf(stderr, "json.Marshal: %s\n", err)
                return 1
        }
+       servefs := http.FileServer(http.Dir(args[2]))
        mux := http.NewServeMux()
-       mux.Handle("/", http.FileServer(http.Dir(args[2])))
+       mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+               for _, ent := range strings.Split(req.URL.Path, "/") {
+                       if ent == ".." {
+                               http.Error(w, "invalid URL path", http.StatusBadRequest)
+                               return
+                       }
+               }
+               fnm := filepath.Join(args[2], filepath.FromSlash(path.Clean("/"+req.URL.Path)))
+               if _, err := os.Stat(fnm); os.IsNotExist(err) {
+                       req.URL.Path = "/"
+               }
+               servefs.ServeHTTP(w, req)
+       }))
        mux.HandleFunc("/config.json", func(w http.ResponseWriter, _ *http.Request) {
                w.Write(configJSON)
        })
index 79e85053876efceac9c1328336d187cbd02eb929..398ebc20e024c745f4ba49a0b5ecab260ce215e8 100644 (file)
@@ -20,28 +20,76 @@ A single-node installation supports all Arvados functionality at small scale. Su
 h2. Prerequisites
 
 You will need:
-* a server host running Debian 10 (buster).
+* a server host running Debian 10 (buster) or Debian 11 (bullseye).
 * a unique 5-character ID like @x9999@ for your cluster (first character should be @[a-w]@ for a long-lived / production cluster; all characters are @[a-z0-9]@).
-* a DNS name like @x9999.example.com@ that resolves to your server host (or a load balancer / proxy that passes HTTP and HTTPS requests through to your server host).
-* a Google account (use it in place of <code>example@gmail.com.example</code> in the instructions below).
+* a DNS name like @x9999.example.com@ that resolves to your server host (or a load balancer / proxy that passes HTTP requests on port 80[1] and HTTPS requests on ports 443 and 4440-4460 through to the same port on your server host).
+* a firewall setup that allows incoming connections to ports 80[1], 443, and 4440-4460.
+
+fn1. Port 80 is only used to obtain TLS certificates automatically from Let's Encrypt. It is not needed if you have another way to provision certificates.
+
+h2. Options
+
+Arvados needs a PostgreSQL database. To get started quickly, install the postgresql-server package on your server host.
+
+<pre>
+# apt install postgresql
+</pre>
+
+Arvados normally uses cloud VMs or a Slurm/LSF cluster to run containers. To get started quickly, install Docker on your system host. The @arvados-server init@ command, as shown below, will configure Arvados to run containers on the system host.
+
+<pre>
+# apt install docker.io
+</pre>
+
+Arvados needs a login backend. To get started quickly, add a user account on your server host and assign a password. The @arvados-server init ... -login pam@ option, as shown below, will configure Arvados so you can log in with this username and password.
+
+<pre>
+# adduser exampleUserName
+</pre>
 
 h2. Initialize the cluster
 
 <pre>
-# echo > /etc/apt/sources.list.d/arvados.list "deb http://apt.arvados.org/buster buster main"
-# apt-get update
-# apt-get install arvados-server-easy
-# arvados-server init -type production -cluster-id x9999 -controller-address x9999.example.com -admin-email example@gmail.com.example
+# echo > /etc/apt/sources.list.d/arvados.list "deb http://apt.arvados.org/$(lsb_release -sc) $(lsb_release -sc) main"
+# apt update
+# apt install arvados-server-easy
+# arvados-server init -cluster-id x9999 -domain x9999.example.com -tls acme -login pam
 </pre>
 
-When the "init" command is finished, navigate to the link shown in the terminal (e.g., @https://x9999.example.com/?api_token=zzzzzzzzzzzzzzzzzzzzzz@). This will log you in to your admin account.
+When the "init" command is finished, navigate to the link shown in the terminal (e.g., @https://x9999.example.com/@) and log in with the account you created above.
 
-h2. Enable login
+Activate your new Arvados user account. Copy the UUID (looks like @x9999-tpzed-xxxxxxxxxxxxxxx@) from your browser's location bar and run:
 
-Follow the instructions to "set up Google login":{{site.baseurl}}/install/setup-login.html or another authentication option.
+<pre>
+# arv sudo user setup --uuid x9999-tpzed-xxxxxxxxxxxxxxx
+</pre>
+
+Run the diagnostics tool to ensure everything is working.
+
+<pre>
+# arv sudo diagnostics
+</pre>
+
+h2. Customize the cluster
+
+Things you should plan to update before using your cluster in production:
+* "Set up Google login":{{site.baseurl}}/install/setup-login.html or another authentication option.
+* "Set up a wildcard TLS certificate and DNS name,":{{site.baseurl}}/install/install-manual-prerequisites.html#dnstls or enable @TrustAllContent@ mode.
+* Update storage configuration to use a cloud storage bucket ("S3":{{site.baseurl}}/install/configure-s3-object-storage.html or "Azure":{{site.baseurl}}/install/configure-azure-blob-storage.html) instead of the local filesystem.
+* Update "CloudVMs configuration":{{site.baseurl}}/install/crunch2-cloud/install-dispatch-cloud.html to use a cloud provider to bring up VMs on demand instead of running containers on the server host.
+
+h2. Updating configuration
+
+After updating your configuration file (@/etc/arvados/config.yml@), notify the server:
+
+<pre>
+# systemctl reload arvados-server
+</pre>
 
-After updating your configuration file (@/etc/arvados/config.yml@), restart the server to make your changes take effect:
+Optionally, add "AutoReloadConfig: true" at the top of @/etc/arvados/config.yml@. Arvados will automatically reload the config file when it changes.
 
 <pre>
-# systemctl restart arvados-server
+AutoReloadConfig: true
+Clusters:
+  [...]
 </pre>
index 916f9f53b2af7b9109474c3e662b395174f982b9..175a35080373ae33b56e7b79be14aa7774e38167 100644 (file)
@@ -6,19 +6,29 @@ package boot
 
 import (
        "context"
+       "crypto/rsa"
+       "crypto/tls"
+       "crypto/x509"
+       "encoding/pem"
+       "errors"
        "fmt"
        "io/ioutil"
        "net"
+       "net/http"
+       "net/url"
        "os"
        "path/filepath"
+       "strings"
+       "time"
+
+       "golang.org/x/crypto/acme"
+       "golang.org/x/crypto/acme/autocert"
 )
 
-// Create a root CA key and use it to make a new server
-// certificate+key pair.
-//
-// In future we'll make one root CA key per host instead of one per
-// cluster, so it only needs to be imported to a browser once for
-// ongoing dev/test usage.
+const stagingDirectoryURL = "https://acme-staging-v02.api.letsencrypt.org/directory"
+
+var errInvalidHost = errors.New("unrecognized target host in incoming TLS request")
+
 type createCertificates struct{}
 
 func (createCertificates) String() string {
@@ -26,8 +36,187 @@ func (createCertificates) String() string {
 }
 
 func (createCertificates) Run(ctx context.Context, fail func(error), super *Supervisor) error {
+       if super.cluster.TLS.ACME.Server != "" {
+               return bootAutoCert(ctx, fail, super)
+       } else if super.cluster.TLS.Key == "" && super.cluster.TLS.Certificate == "" {
+               return createSelfSignedCert(ctx, fail, super)
+       } else {
+               return nil
+       }
+}
+
+// bootAutoCert uses Let's Encrypt to get certificates for all the
+// domains appearing in ExternalURLs, writes them to files where Nginx
+// can load them, and updates super.cluster.TLS fields (Key and
+// Certificiate) to point to those files.
+//
+// It also runs a background task to keep the files up to date.
+//
+// After bootAutoCert returns, other service components will get the
+// certificates they need by reading these files or by using a
+// read-only autocert cache.
+//
+// Currently this only works when port 80 of every ExternalURL domain
+// is routed to this host, i.e., on a single-node cluster. Wildcard
+// domains [for WebDAV] are not supported.
+func bootAutoCert(ctx context.Context, fail func(error), super *Supervisor) error {
+       hosts := map[string]bool{}
+       for _, svc := range super.cluster.Services.Map() {
+               u := url.URL(svc.ExternalURL)
+               if u.Scheme == "https" || u.Scheme == "wss" {
+                       hosts[strings.ToLower(u.Hostname())] = true
+               }
+       }
+       mgr := &autocert.Manager{
+               Cache:  autocert.DirCache(super.tempdir + "/autocert"),
+               Prompt: autocert.AcceptTOS,
+               HostPolicy: func(ctx context.Context, host string) error {
+                       if hosts[strings.ToLower(host)] {
+                               return nil
+                       } else {
+                               return errInvalidHost
+                       }
+               },
+       }
+       if srv := super.cluster.TLS.ACME.Server; srv == "LE" {
+               // Leaving mgr.Client == nil means use Let's Encrypt
+               // production environment
+       } else if srv == "LE-staging" {
+               mgr.Client = &acme.Client{DirectoryURL: stagingDirectoryURL}
+       } else if strings.HasPrefix(srv, "https://") {
+               mgr.Client = &acme.Client{DirectoryURL: srv}
+       } else {
+               return fmt.Errorf("autocert setup: invalid directory URL in TLS.ACME.Server: %q", srv)
+       }
+       go func() {
+               err := http.ListenAndServe(":80", mgr.HTTPHandler(nil))
+               fail(fmt.Errorf("autocert http-01 challenge handler stopped: %w", err))
+       }()
+       u := url.URL(super.cluster.Services.Controller.ExternalURL)
+       extHost := u.Hostname()
+       update := func() error {
+               for h := range hosts {
+                       cert, err := mgr.GetCertificate(&tls.ClientHelloInfo{ServerName: h})
+                       if err != nil {
+                               return err
+                       }
+                       if h == extHost {
+                               err = writeCert(super.tempdir, "server.key", "server.crt", cert)
+                               if err != nil {
+                                       return err
+                               }
+                       }
+               }
+               return nil
+       }
+       err := update()
+       if err != nil {
+               return err
+       }
+       go func() {
+               for range time.NewTicker(time.Hour).C {
+                       err := update()
+                       if err != nil {
+                               super.logger.WithError(err).Error("error getting certificate from autocert")
+                       }
+               }
+       }()
+       super.cluster.TLS.Key = "file://" + super.tempdir + "/server.key"
+       super.cluster.TLS.Certificate = "file://" + super.tempdir + "/server.crt"
+       return nil
+}
+
+// Save cert chain and key in a format Nginx can read.
+func writeCert(outdir, keyfile, certfile string, cert *tls.Certificate) error {
+       keytmp, err := os.CreateTemp(outdir, keyfile+".tmp.*")
+       if err != nil {
+               return err
+       }
+       defer keytmp.Close()
+       defer os.Remove(keytmp.Name())
+
+       certtmp, err := os.CreateTemp(outdir, certfile+".tmp.*")
+       if err != nil {
+               return err
+       }
+       defer certtmp.Close()
+       defer os.Remove(certtmp.Name())
+
+       switch privkey := cert.PrivateKey.(type) {
+       case *rsa.PrivateKey:
+               err = pem.Encode(keytmp, &pem.Block{
+                       Type:  "RSA PRIVATE KEY",
+                       Bytes: x509.MarshalPKCS1PrivateKey(privkey),
+               })
+               if err != nil {
+                       return err
+               }
+       default:
+               buf, err := x509.MarshalPKCS8PrivateKey(privkey)
+               if err != nil {
+                       return err
+               }
+               err = pem.Encode(keytmp, &pem.Block{
+                       Type:  "PRIVATE KEY",
+                       Bytes: buf,
+               })
+               if err != nil {
+                       return err
+               }
+       }
+       err = keytmp.Close()
+       if err != nil {
+               return err
+       }
+
+       for _, cert := range cert.Certificate {
+               err = pem.Encode(certtmp, &pem.Block{
+                       Type:  "CERTIFICATE",
+                       Bytes: cert,
+               })
+               if err != nil {
+                       return err
+               }
+       }
+       err = certtmp.Close()
+       if err != nil {
+               return err
+       }
+
+       err = os.Rename(keytmp.Name(), filepath.Join(outdir, keyfile))
+       if err != nil {
+               return err
+       }
+       err = os.Rename(certtmp.Name(), filepath.Join(outdir, certfile))
+       if err != nil {
+               return err
+       }
+       return nil
+}
+
+// Create a root CA key and use it to make a new server
+// certificate+key pair.
+//
+// In future we'll make one root CA key per host instead of one per
+// cluster, so it only needs to be imported to a browser once for
+// ongoing dev/test usage.
+func createSelfSignedCert(ctx context.Context, fail func(error), super *Supervisor) error {
+       san := "DNS:localhost,DNS:localhost.localdomain"
+       if net.ParseIP(super.ListenHost) != nil {
+               san += fmt.Sprintf(",IP:%s", super.ListenHost)
+       } else {
+               san += fmt.Sprintf(",DNS:%s", super.ListenHost)
+       }
+       hostname, err := os.Hostname()
+       if err != nil {
+               return fmt.Errorf("hostname: %w", err)
+       }
+       if hostname != super.ListenHost {
+               san += ",DNS:" + hostname
+       }
+
        // Generate root key
-       err := super.RunProgram(ctx, super.tempdir, runOptions{}, "openssl", "genrsa", "-out", "rootCA.key", "4096")
+       err = super.RunProgram(ctx, super.tempdir, runOptions{}, "openssl", "genrsa", "-out", "rootCA.key", "4096")
        if err != nil {
                return err
        }
@@ -46,18 +235,6 @@ func (createCertificates) Run(ctx context.Context, fail func(error), super *Supe
        if err != nil {
                return err
        }
-       hostname, err := os.Hostname()
-       if err != nil {
-               return fmt.Errorf("hostname: %w", err)
-       }
-       san := "DNS:localhost,DNS:localhost.localdomain,DNS:" + hostname
-       if super.ListenHost == hostname || super.ListenHost == "localhost" {
-               // already have it
-       } else if net.ParseIP(super.ListenHost) != nil {
-               san += fmt.Sprintf(",IP:%s", super.ListenHost)
-       } else {
-               san += fmt.Sprintf(",DNS:%s", super.ListenHost)
-       }
        conf := append(defaultconf, []byte(fmt.Sprintf("\n[SAN]\nsubjectAltName=%s\n", san))...)
        err = ioutil.WriteFile(filepath.Join(super.tempdir, "server.cfg"), conf, 0644)
        if err != nil {
@@ -73,5 +250,7 @@ func (createCertificates) Run(ctx context.Context, fail func(error), super *Supe
        if err != nil {
                return err
        }
+       super.cluster.TLS.Key = "file://" + super.tempdir + "/server.key"
+       super.cluster.TLS.Certificate = "file://" + super.tempdir + "/server.crt"
        return nil
 }
index 15af548e96f91f9e11c20beea6be97da750afc1c..4b7284556eef20c17594ec5115238d47f308455b 100644 (file)
@@ -15,6 +15,7 @@ import (
 
        "git.arvados.org/arvados.git/lib/cmd"
        "git.arvados.org/arvados.git/sdk/go/ctxlog"
+       "github.com/coreos/go-systemd/daemon"
 )
 
 var Command cmd.Handler = bootCommand{}
@@ -66,11 +67,11 @@ func (bcmd bootCommand) run(ctx context.Context, prog string, args []string, std
        flags.StringVar(&super.ConfigPath, "config", "/etc/arvados/config.yml", "arvados config file `path`")
        flags.StringVar(&super.SourcePath, "source", ".", "arvados source tree `directory`")
        flags.StringVar(&super.ClusterType, "type", "production", "cluster `type`: development, test, or production")
-       flags.StringVar(&super.ListenHost, "listen-host", "localhost", "host name or interface address for external services, and internal services whose InternalURLs are not configured")
+       flags.StringVar(&super.ListenHost, "listen-host", "localhost", "host name or interface address for internal services whose InternalURLs are not configured")
        flags.StringVar(&super.ControllerAddr, "controller-address", ":0", "desired controller address, `host:port` or `:port`")
        flags.StringVar(&super.Workbench2Source, "workbench2-source", "../arvados-workbench2", "path to arvados-workbench2 source tree")
        flags.BoolVar(&super.NoWorkbench1, "no-workbench1", false, "do not run workbench1")
-       flags.BoolVar(&super.NoWorkbench2, "no-workbench2", true, "do not run workbench2")
+       flags.BoolVar(&super.NoWorkbench2, "no-workbench2", false, "do not run workbench2")
        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")
@@ -134,6 +135,9 @@ func (bcmd bootCommand) run(ctx context.Context, prog string, args []string, std
                        return nil
                }
        }
+       if _, err := daemon.SdNotify(false, "READY=1"); err != nil {
+               super.logger.WithError(err).Errorf("error notifying init daemon")
+       }
        // Wait for signal/crash + orderly shutdown
        return super.Wait()
 }
index e67bc1d900b60fd74ad5260f19e5cab20687fccc..b391c4dc8c93f61f50d221bb116cf71da3c10960 100644 (file)
@@ -14,6 +14,7 @@ import (
        "os/exec"
        "path/filepath"
        "regexp"
+       "strings"
 
        "git.arvados.org/arvados.git/sdk/go/arvados"
 )
@@ -31,8 +32,20 @@ func (runNginx) Run(ctx context.Context, fail func(error), super *Supervisor) er
        if err != nil {
                return err
        }
+       extListenHost := "0.0.0.0"
+       if super.ClusterType == "test" {
+               // Our dynamic port number assignment strategy (choose
+               // an available port, write it in a config file, and
+               // have another process/goroutine bind to it) is prone
+               // to races when used by concurrent supervisors. In
+               // test mode we don't accept remote connections, so we
+               // can avoid collisions by using the per-cluster
+               // loopback address instead of 0.0.0.0.
+               extListenHost = super.ListenHost
+       }
        vars := map[string]string{
-               "LISTENHOST":       super.ListenHost,
+               "LISTENHOST":       extListenHost,
+               "UPSTREAMHOST":     super.ListenHost,
                "SSLCERT":          filepath.Join(super.tempdir, "server.crt"),
                "SSLKEY":           filepath.Join(super.tempdir, "server.key"),
                "ACCESSLOG":        filepath.Join(super.tempdir, "nginx_access.log"),
@@ -42,7 +55,10 @@ func (runNginx) Run(ctx context.Context, fail func(error), super *Supervisor) er
        }
        u := url.URL(super.cluster.Services.Controller.ExternalURL)
        ctrlHost := u.Hostname()
-       if f, err := os.Open("/var/lib/acme/live/" + ctrlHost + "/privkey"); err == nil {
+       if strings.HasPrefix(super.cluster.TLS.Certificate, "file:/") && strings.HasPrefix(super.cluster.TLS.Key, "file:/") {
+               vars["SSLCERT"] = filepath.Clean(super.cluster.TLS.Certificate[5:])
+               vars["SSLKEY"] = filepath.Clean(super.cluster.TLS.Key[5:])
+       } else if f, err := os.Open("/var/lib/acme/live/" + ctrlHost + "/privkey"); err == nil {
                f.Close()
                vars["SSLCERT"] = "/var/lib/acme/live/" + ctrlHost + "/cert"
                vars["SSLKEY"] = "/var/lib/acme/live/" + ctrlHost + "/privkey"
index f86f1f930398f48a133cc33fe4752333246b73cb..5367337e81a1d0e605160c9c0dd789c9282a21f2 100644 (file)
@@ -10,6 +10,7 @@ import (
        "fmt"
        "os"
        "path/filepath"
+       "runtime"
        "strings"
        "sync"
 
@@ -92,7 +93,11 @@ func (runner installPassenger) Run(ctx context.Context, fail func(error), super
                        break
                }
        }
-       err = super.RunProgram(ctx, appdir, runOptions{}, "bundle", "install", "--jobs", "4", "--path", filepath.Join(os.Getenv("HOME"), ".gem"))
+       err = super.RunProgram(ctx, appdir, runOptions{}, "bundle", "config", "--set", "local", "path", filepath.Join(os.Getenv("HOME"), ".gem"))
+       if err != nil {
+               return err
+       }
+       err = super.RunProgram(ctx, appdir, runOptions{}, "bundle", "install", "--jobs", fmt.Sprintf("%d", runtime.NumCPU()))
        if err != nil {
                return err
        }
index 090e852446f7c3270f50c94a7ac88870d162a38e..506407f4e8537a451ab47029ff5c7eeba55465dd 100644 (file)
@@ -35,6 +35,7 @@ func (runner runServiceCommand) Run(ctx context.Context, fail func(error), super
        if err != nil {
                return err
        }
+       super.wait(ctx, createCertificates{})
        super.wait(ctx, runner.depends...)
        for u := range runner.svc.InternalURLs {
                u := u
@@ -46,7 +47,15 @@ func (runner runServiceCommand) Run(ctx context.Context, fail func(error), super
                super.waitShutdown.Add(1)
                go func() {
                        defer super.waitShutdown.Done()
-                       fail(super.RunProgram(ctx, super.tempdir, runOptions{env: []string{"ARVADOS_SERVICE_INTERNAL_URL=" + u.String()}}, binfile, runner.name, "-config", super.configfile))
+                       fail(super.RunProgram(ctx, super.tempdir, runOptions{
+                               env: []string{
+                                       "ARVADOS_SERVICE_INTERNAL_URL=" + u.String(),
+                                       // Child process should not
+                                       // try to tell systemd that we
+                                       // are ready.
+                                       "NOTIFY_SOCKET=",
+                               },
+                       }, binfile, runner.name, "-config", super.configfile))
                }()
        }
        return nil
@@ -82,6 +91,7 @@ func (runner runGoProgram) Run(ctx context.Context, fail func(error), super *Sup
                return err
        }
 
+       super.wait(ctx, createCertificates{})
        super.wait(ctx, runner.depends...)
        for u := range runner.svc.InternalURLs {
                u := u
index 8eb375b874853821944cef1c87bbd650bd6547ee..ddc17953d2363d020d6aa37332c97c36c5b48646 100644 (file)
@@ -113,28 +113,24 @@ func (super *Supervisor) Start(ctx context.Context) {
        super.done = make(chan struct{})
 
        sigch := make(chan os.Signal)
-       signal.Notify(sigch, syscall.SIGINT, syscall.SIGTERM)
-       defer signal.Stop(sigch)
+       signal.Notify(sigch, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
        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
+               defer signal.Stop(sigch)
+               for {
+                       select {
+                       case <-ctx.Done():
+                               return
+                       case sig := <-sigch:
+                               super.logger.WithField("signal", sig).Info("caught signal")
+                               if super.err == nil {
+                                       if sig == syscall.SIGHUP {
+                                               super.err = errNeedConfigReload
+                                       } else {
+                                               super.err = fmt.Errorf("caught signal %s", sig)
+                                       }
+                               }
+                               super.cancel()
                        }
-                       super.cancel()
                }
        }()
 
@@ -251,13 +247,9 @@ func (super *Supervisor) runCluster() error {
        }
 
        if super.ListenHost == "" {
-               if urlhost := super.cluster.Services.Controller.ExternalURL.Host; urlhost != "" {
-                       if h, _, _ := net.SplitHostPort(urlhost); h != "" {
-                               super.ListenHost = h
-                       } else {
-                               super.ListenHost = urlhost
-                       }
-               } else {
+               u := url.URL(super.cluster.Services.Controller.ExternalURL)
+               super.ListenHost = u.Hostname()
+               if super.ListenHost == "" {
                        super.ListenHost = "0.0.0.0"
                }
        }
@@ -471,6 +463,7 @@ func (super *Supervisor) WaitReady() bool {
                        super.logger.Infof("waiting for %s to be ready", id)
                        if !super2.WaitReady() {
                                super.logger.Infof("%s startup failed", id)
+                               super.Stop()
                                return false
                        }
                        super.logger.Infof("%s is ready", id)
@@ -484,6 +477,7 @@ func (super *Supervisor) WaitReady() bool {
                select {
                case <-ticker.C:
                case <-super.ctx.Done():
+                       super.Stop()
                        return false
                }
                if super.healthChecker == nil {
index 7d9bb6f200253405b15fbd3edaed82132efe61e4..54dfd9a91f9fff8429da645d39ee9029e5571335 100644 (file)
@@ -57,7 +57,7 @@ func (cmd apiCallCmd) RunCommand(prog string, args []string, stdin io.Reader, st
                return 2
        }
        model := split[len(split)-1]
-       return externalCmd{"arv"}.RunCommand("arv", legacyFlagsToFront(model, args), stdin, stdout, stderr)
+       return rubyArvCmd{model}.RunCommand(prog, args, stdin, stdout, stderr)
 }
 
 type rubyArvCmd struct {
@@ -65,7 +65,18 @@ type rubyArvCmd struct {
 }
 
 func (rc rubyArvCmd) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
-       return externalCmd{"arv"}.RunCommand("arv", legacyFlagsToFront(rc.subcommand, args), stdin, stdout, stderr)
+       wrapprog := "arv-ruby"
+       if _, err := exec.LookPath(wrapprog); err != nil && !strings.Contains(prog, "arv ") {
+               // arv-ruby isn't in PATH (i.e., installation method
+               // wasn't a recent "arvados-server install", which
+               // symlinks /usr/bin/arv-ruby ->
+               // /var/lib/arvados/bin/arv), so fall back to looking
+               // for the arvados-cli program as "arv". (But don't do
+               // this if we are being run as "arv" -- that would
+               // probably cause a recursive fork bomb.)
+               wrapprog = "arv"
+       }
+       return externalCmd{wrapprog}.RunCommand(wrapprog, legacyFlagsToFront(rc.subcommand, args), stdin, stdout, stderr)
 }
 
 type externalCmd struct {
@@ -90,7 +101,7 @@ func (ec externalCmd) RunCommand(prog string, args []string, stdin io.Reader, st
                return 1
        case *exec.Error:
                fmt.Fprintln(stderr, err)
-               if ec.prog == "arv" {
+               if ec.prog == "arv" || ec.prog == "arv-ruby" {
                        fmt.Fprint(stderr, rubyInstallHints)
                } else if strings.HasPrefix(ec.prog, "arv-") {
                        fmt.Fprint(stderr, pythonInstallHints)
index 472a22c6b2cb11a3566d882e6420f52400ca4b13..b23c6a12745088fc02c65cb670fc0a1936e30a45 100644 (file)
@@ -900,10 +900,31 @@ Clusters:
       Repositories: /var/lib/arvados/git/repositories
 
     TLS:
+      # Use "file:///var/lib/acme/live/example.com/cert" and
+      # ".../privkey" to load externally managed certificates.
       Certificate: ""
       Key: ""
+
+      # Accept invalid certificates when connecting to servers. Never
+      # use this in production.
       Insecure: false
 
+      ACME:
+        # Obtain certificates automatically for ExternalURL domains
+        # using an ACME server and http-01 validation.
+        #
+        # To use Let's Encrypt, specify "LE".  To use the Let's
+        # Encrypt staging environment, specify "LE-staging".  To use a
+        # different ACME server, specify the full directory URL
+        # ("https://...").
+        #
+        # Note: this feature is not yet implemented in released
+        # versions, only in the alpha/prerelease arvados-server-easy
+        # package.
+        #
+        # Implies agreement with the server's terms of service.
+        Server: ""
+
     Containers:
       # List of supported Docker Registry image formats that compute nodes
       # are able to use. `arv keep docker` will error out if a user tries
index 0e86f604a7733b1a12296c0f389386d5d352526b..68181395fadcbafba4227b0d7cc16eb4b2f8624e 100644 (file)
@@ -999,6 +999,7 @@ func (runner *ContainerRunner) CreateContainer(imageID string, bindmounts map[st
                env["ARVADOS_API_TOKEN"] = tok
                env["ARVADOS_API_HOST"] = os.Getenv("ARVADOS_API_HOST")
                env["ARVADOS_API_HOST_INSECURE"] = os.Getenv("ARVADOS_API_HOST_INSECURE")
+               env["ARVADOS_KEEP_SERVICES"] = os.Getenv("ARVADOS_KEEP_SERVICES")
        }
        workdir := runner.Container.Cwd
        if workdir == "." {
@@ -2045,7 +2046,8 @@ func startLocalKeepstore(configData ConfigData, logbuf io.Writer) (*exec.Cmd, er
        // modify the cluster configuration that we feed it on stdin.
        configData.Cluster.API.MaxKeepBlobBuffers = configData.KeepBuffers
 
-       ln, err := net.Listen("tcp", "localhost:0")
+       localaddr := localKeepstoreAddr()
+       ln, err := net.Listen("tcp", net.JoinHostPort(localaddr, "0"))
        if err != nil {
                return nil, err
        }
@@ -2055,7 +2057,7 @@ func startLocalKeepstore(configData ConfigData, logbuf io.Writer) (*exec.Cmd, er
                return nil, err
        }
        ln.Close()
-       url := "http://localhost:" + port
+       url := "http://" + net.JoinHostPort(localaddr, port)
 
        fmt.Fprintf(logbuf, "starting keepstore on %s\n", url)
 
@@ -2147,3 +2149,43 @@ func currentUserAndGroups() string {
        }
        return s
 }
+
+// Return a suitable local interface address for a local keepstore
+// service. Currently this is the numerically lowest non-loopback ipv4
+// address assigned to a local interface that is not in any of the
+// link-local/vpn/loopback ranges 169.254/16, 100.64/10, or 127/8.
+func localKeepstoreAddr() string {
+       var ips []net.IP
+       // Ignore error (proceed with zero IPs)
+       addrs, _ := processIPs(os.Getpid())
+       for addr := range addrs {
+               ip := net.ParseIP(addr)
+               if ip == nil {
+                       // invalid
+                       continue
+               }
+               if ip.Mask(net.CIDRMask(8, 32)).Equal(net.IPv4(127, 0, 0, 0)) ||
+                       ip.Mask(net.CIDRMask(10, 32)).Equal(net.IPv4(100, 64, 0, 0)) ||
+                       ip.Mask(net.CIDRMask(16, 32)).Equal(net.IPv4(169, 254, 0, 0)) {
+                       // unsuitable
+                       continue
+               }
+               ips = append(ips, ip)
+       }
+       if len(ips) == 0 {
+               return "0.0.0.0"
+       }
+       sort.Slice(ips, func(ii, jj int) bool {
+               i, j := ips[ii], ips[jj]
+               if len(i) != len(j) {
+                       return len(i) < len(j)
+               }
+               for x := range i {
+                       if i[x] != j[x] {
+                               return i[x] < j[x]
+                       }
+               }
+               return false
+       })
+       return ips[0].String()
+}
index 3f7c7e50f14f7713c0de4885f0cae4fd64ab11f8..9860c7949727b169ccc1df66e15e4f223dc7e7cd 100644 (file)
@@ -223,6 +223,11 @@ func (s *integrationSuite) TestRunTrivialContainerWithLocalKeepstore(c *C) {
                        c.Check(log, trial.matchGetReq, `(?ms).*"reqMethod":"GET".*`)
                        c.Check(log, trial.matchPutReq, `(?ms).*"reqMethod":"PUT".*,"reqPath":"0e3bcff26d51c895a60ea0d4585e134d".*`)
                }
+
+               c.Check(s.logFiles["crunch-run.txt"], Matches, `(?ms).*using local keepstore process .* at http://[\d\.]{7,}:\d+.*`)
+               c.Check(s.logFiles["crunch-run.txt"], Not(Matches), `(?ms).* at http://127\..*`)
+               c.Check(s.logFiles["crunch-run.txt"], Not(Matches), `(?ms).* at http://169\.254\..*`)
+               c.Check(s.logFiles["stderr.txt"], Matches, `(?ms).*ARVADOS_KEEP_SERVICES=http://[\d\.]{7,}:\d+\n.*`)
        }
 
        // Check that (1) config is loaded from $ARVADOS_CONFIG when
@@ -258,7 +263,7 @@ func (s *integrationSuite) testRunTrivialContainer(c *C) {
        if err := exec.Command("which", s.engine).Run(); err != nil {
                c.Skip(fmt.Sprintf("%s: %s", s.engine, err))
        }
-       s.cr.Command = []string{"sh", "-c", "cat /mnt/in/inputfile >/mnt/out/inputfile && cat /mnt/json >/mnt/out/json && ! touch /mnt/in/shouldbereadonly && mkdir /mnt/out/emptydir"}
+       s.cr.Command = []string{"sh", "-c", "env >&2 && cat /mnt/in/inputfile >/mnt/out/inputfile && cat /mnt/json >/mnt/out/json && ! touch /mnt/in/shouldbereadonly && mkdir /mnt/out/emptydir"}
        s.setup(c)
 
        args := []string{
index 71fe1c5dc60c8501353da59f5f49df31a2f7f805..3455d3307e71bff663aa44ea3023bf7b71efca26 100644 (file)
@@ -30,6 +30,7 @@ func (Command) RunCommand(prog string, args []string, stdin io.Reader, stdout, s
        f := flag.NewFlagSet(prog, flag.ContinueOnError)
        f.StringVar(&diag.projectName, "project-name", "scratch area for diagnostics", "name of project to find/create in home project and use for temporary/test objects")
        f.StringVar(&diag.logLevel, "log-level", "info", "logging level (debug, info, warning, error)")
+       f.StringVar(&diag.dockerImage, "docker-image", "alpine:latest", "image to use when running a test container")
        f.BoolVar(&diag.checkInternal, "internal-client", false, "check that this host is considered an \"internal\" client")
        f.BoolVar(&diag.checkExternal, "external-client", false, "check that this host is considered an \"external\" client")
        f.IntVar(&diag.priority, "priority", 500, "priority for test container (1..1000, or 0 to skip)")
@@ -60,6 +61,7 @@ type diagnoser struct {
        logLevel      string
        priority      int
        projectName   string
+       dockerImage   string
        checkInternal bool
        checkExternal bool
        timeout       time.Duration
@@ -132,6 +134,7 @@ func (diag *diagnoser) runtests() {
 
        var cluster arvados.Cluster
        cfgpath := "arvados/v1/config"
+       cfgOK := false
        diag.dotest(20, fmt.Sprintf("getting exported config from https://%s/%s", client.APIHost, cfgpath), func() error {
                ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(diag.timeout))
                defer cancel()
@@ -140,6 +143,7 @@ func (diag *diagnoser) runtests() {
                        return err
                }
                diag.debugf("Collections.BlobSigning = %v", cluster.Collections.BlobSigning)
+               cfgOK = true
                return nil
        })
 
@@ -155,6 +159,11 @@ func (diag *diagnoser) runtests() {
                return nil
        })
 
+       if !cfgOK {
+               diag.errorf("cannot proceed without cluster config -- aborting without running any further tests")
+               return
+       }
+
        // uncomment to create some spurious errors
        // cluster.Services.WebDAVDownload.ExternalURL.Host = "0.0.0.0:9"
 
@@ -388,29 +397,35 @@ func (diag *diagnoser) runtests() {
        })
 
        davurl := cluster.Services.WebDAV.ExternalURL
+       davWildcard := strings.HasPrefix(davurl.Host, "*--") || strings.HasPrefix(davurl.Host, "*.")
        diag.dotest(110, fmt.Sprintf("checking WebDAV ExternalURL wildcard (%s)", davurl), func() error {
                if davurl.Host == "" {
                        return fmt.Errorf("host missing - content previews will not work")
                }
-               if !strings.HasPrefix(davurl.Host, "*--") && !strings.HasPrefix(davurl.Host, "*.") && !cluster.Collections.TrustAllContent {
+               if !davWildcard && !cluster.Collections.TrustAllContent {
                        diag.warnf("WebDAV ExternalURL has no leading wildcard and TrustAllContent==false - content previews will not work")
                }
                return nil
        })
 
        for i, trial := range []struct {
-               needcoll bool
-               status   int
-               fileurl  string
+               needcoll     bool
+               needWildcard bool
+               status       int
+               fileurl      string
        }{
-               {false, http.StatusNotFound, strings.Replace(davurl.String(), "*", "d41d8cd98f00b204e9800998ecf8427e-0", 1) + "foo"},
-               {false, http.StatusNotFound, strings.Replace(davurl.String(), "*", "d41d8cd98f00b204e9800998ecf8427e-0", 1) + "testfile"},
-               {false, http.StatusNotFound, cluster.Services.WebDAVDownload.ExternalURL.String() + "c=d41d8cd98f00b204e9800998ecf8427e+0/_/foo"},
-               {false, http.StatusNotFound, cluster.Services.WebDAVDownload.ExternalURL.String() + "c=d41d8cd98f00b204e9800998ecf8427e+0/_/testfile"},
-               {true, http.StatusOK, strings.Replace(davurl.String(), "*", strings.Replace(collection.PortableDataHash, "+", "-", -1), 1) + "testfile"},
-               {true, http.StatusOK, cluster.Services.WebDAVDownload.ExternalURL.String() + "c=" + collection.UUID + "/_/testfile"},
+               {false, false, http.StatusNotFound, strings.Replace(davurl.String(), "*", "d41d8cd98f00b204e9800998ecf8427e-0", 1) + "foo"},
+               {false, false, http.StatusNotFound, strings.Replace(davurl.String(), "*", "d41d8cd98f00b204e9800998ecf8427e-0", 1) + "testfile"},
+               {false, false, http.StatusNotFound, cluster.Services.WebDAVDownload.ExternalURL.String() + "c=d41d8cd98f00b204e9800998ecf8427e+0/_/foo"},
+               {false, false, http.StatusNotFound, cluster.Services.WebDAVDownload.ExternalURL.String() + "c=d41d8cd98f00b204e9800998ecf8427e+0/_/testfile"},
+               {true, true, http.StatusOK, strings.Replace(davurl.String(), "*", strings.Replace(collection.PortableDataHash, "+", "-", -1), 1) + "testfile"},
+               {true, false, http.StatusOK, cluster.Services.WebDAVDownload.ExternalURL.String() + "c=" + collection.UUID + "/_/testfile"},
        } {
                diag.dotest(120+i, fmt.Sprintf("downloading from webdav (%s)", trial.fileurl), func() error {
+                       if trial.needWildcard && !davWildcard {
+                               diag.warnf("skipping collection-id-in-vhost test because WebDAV ExternalURL has no leading wildcard")
+                               return nil
+                       }
                        ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(diag.timeout))
                        defer cancel()
                        if trial.needcoll && collection.UUID == "" {
@@ -450,9 +465,10 @@ func (diag *diagnoser) runtests() {
                        return err
                }
                if len(vmlist.Items) < 1 {
-                       return fmt.Errorf("no VMs found")
+                       diag.warnf("no VMs found")
+               } else {
+                       vm = vmlist.Items[0]
                }
-               vm = vmlist.Items[0]
                return nil
        })
 
@@ -460,7 +476,8 @@ func (diag *diagnoser) runtests() {
                ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(diag.timeout))
                defer cancel()
                if vm.UUID == "" {
-                       return fmt.Errorf("skipping, no vm available")
+                       diag.warnf("skipping, no vm available")
+                       return nil
                }
                webshelltermurl := cluster.Services.Workbench1.ExternalURL.String() + "virtual_machines/" + vm.UUID + "/webshell/testusername"
                diag.debugf("url %s", webshelltermurl)
@@ -488,7 +505,8 @@ func (diag *diagnoser) runtests() {
                ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(diag.timeout))
                defer cancel()
                if vm.UUID == "" {
-                       return fmt.Errorf("skipping, no vm available")
+                       diag.warnf("skipping, no vm available")
+                       return nil
                }
                u := cluster.Services.WebShell.ExternalURL
                webshellurl := u.String() + vm.Hostname + "?"
@@ -545,7 +563,7 @@ func (diag *diagnoser) runtests() {
                err := client.RequestAndDecodeContext(ctx, &cr, "POST", "arvados/v1/container_requests", nil, map[string]interface{}{"container_request": map[string]interface{}{
                        "owner_uuid":      project.UUID,
                        "name":            fmt.Sprintf("diagnostics container request %s", timestamp),
-                       "container_image": "arvados/jobs",
+                       "container_image": diag.dockerImage,
                        "command":         []string{"echo", timestamp},
                        "use_existing":    false,
                        "output_path":     "/mnt/output",
index cb411c63a5407aec56a95735343752ee1b560957..3b68f31e9fac07208c8b4dcff46ee58f4e99deda 100644 (file)
@@ -15,10 +15,11 @@ StartLimitIntervalSec=0
 Type=notify
 EnvironmentFile=-/etc/arvados/environment
 ExecStart=/usr/bin/arvados-server boot
-# Set a reasonable default for the open file limit
-LimitNOFILE=65536
+ExecReload=/usr/bin/arvados-server config-check
+ExecReload=kill -HUP $MAINPID
 Restart=always
 RestartSec=1
+LimitNOFILE=65536
 
 # systemd<=219 (centos:7, debian:8, ubuntu:trusty) obeys StartLimitInterval in the [Service] section
 StartLimitInterval=0
index a7c32c5052b1970a4d57999faede6ac71dc89de9..27b8d1dc8aedb70c9c5720ace830bc3bbaabd10a 100644 (file)
@@ -17,6 +17,7 @@ import (
        "os/exec"
        "os/user"
        "path/filepath"
+       "runtime"
        "strconv"
        "strings"
        "syscall"
@@ -40,7 +41,7 @@ const (
        gradleversion           = "5.3.1"
        nodejsversion           = "v12.22.11"
        devtestDatabasePassword = "insecure_arvados_test"
-       workbench2version       = "5e020488f67b5bc919796e0dc8b0b9f3b3ff23b0"
+       workbench2version       = "2454ac35292a79594c32a80430740317ed5005cf"
 )
 
 //go:embed arvados.service
@@ -189,6 +190,7 @@ func (inst *installCommand) RunCommand(prog string, args []string, stdin io.Read
                        "uuid-dev",
                        "wget",
                        "xvfb",
+                       "zlib1g-dev", // services/api
                )
                if test {
                        if osv.Debian && osv.Major <= 10 {
@@ -203,11 +205,13 @@ func (inst *installCommand) RunCommand(prog string, args []string, stdin io.Read
                }
                switch {
                case osv.Debian && osv.Major >= 11:
-                       pkgs = append(pkgs, "libcurl4", "perl-modules-5.32")
+                       pkgs = append(pkgs, "g++", "libcurl4", "libcurl4-openssl-dev", "perl-modules-5.32")
                case osv.Debian && osv.Major >= 10:
-                       pkgs = append(pkgs, "libcurl4", "perl-modules")
-               default:
-                       pkgs = append(pkgs, "libcurl3", "perl-modules")
+                       pkgs = append(pkgs, "g++", "libcurl4", "libcurl4-openssl-dev", "perl-modules")
+               case osv.Debian || osv.Ubuntu:
+                       pkgs = append(pkgs, "g++", "libcurl3", "libcurl3-openssl-dev", "perl-modules")
+               case osv.Centos:
+                       pkgs = append(pkgs, "gcc", "gcc-c++", "libcurl-devel", "postgresql-devel")
                }
                cmd := exec.CommandContext(ctx, "apt-get")
                if inst.EatMyData {
@@ -563,8 +567,6 @@ yarn install
                for _, srcdir := range []string{
                        "cmd/arvados-client",
                        "cmd/arvados-server",
-                       "services/crunch-dispatch-local",
-                       "services/crunch-dispatch-slurm",
                } {
                        fmt.Fprintf(stderr, "building %s...\n", srcdir)
                        cmd := exec.Command("go", "install", "-ldflags", "-X git.arvados.org/arvados.git/lib/cmd.version="+inst.PackageVersion+" -X main.version="+inst.PackageVersion+" -s -w")
@@ -579,19 +581,6 @@ yarn install
                        }
                }
 
-               // Symlink user-facing Go programs /usr/bin/x ->
-               // /var/lib/arvados/bin/x
-               for _, prog := range []string{"arvados-client", "arvados-server"} {
-                       err = os.Remove("/usr/bin/" + prog)
-                       if err != nil && !errors.Is(err, os.ErrNotExist) {
-                               return 1
-                       }
-                       err = os.Symlink("/var/lib/arvados/bin/"+prog, "/usr/bin/"+prog)
-                       if err != nil {
-                               return 1
-                       }
-               }
-
                // Copy assets from source tree to /var/lib/arvados/share
                cmd := exec.Command("install", "-v", "-t", "/var/lib/arvados/share", filepath.Join(inst.SourcePath, "sdk/python/tests/nginx.conf"))
                cmd.Stdout = stdout
@@ -601,6 +590,33 @@ yarn install
                        return 1
                }
 
+               // Install python SDK and arv-mount in
+               // /var/lib/arvados/lib/python.
+               //
+               // setup.py writes a file in the source directory in
+               // order to include the version number in the package
+               // itself.  We don't want to write to the source tree
+               // (in "arvados-package" context it's mounted
+               // readonly) so we run setup.py in a temporary copy of
+               // the source dir.
+               if err = inst.runBash(`
+v=/var/lib/arvados/lib/python
+tmp=/var/lib/arvados/tmp/python
+python3 -m venv "$v"
+. "$v/bin/activate"
+pip3 install --no-cache-dir 'setuptools>=18.5' 'pip>=7'
+export ARVADOS_BUILDING_VERSION="`+inst.PackageVersion+`"
+for src in "`+inst.SourcePath+`/sdk/python" "`+inst.SourcePath+`/services/fuse"; do
+  rsync -a --delete-after "$src/" "$tmp/"
+  cd "$tmp"
+  python3 setup.py install
+  cd ..
+  rm -rf "$tmp"
+done
+`, stdout, stderr); err != nil {
+                       return 1
+               }
+
                // Install Rails apps to /var/lib/arvados/{railsapi,workbench1}/
                for dstdir, srcdir := range map[string]string{
                        "railsapi":   "services/api",
@@ -629,12 +645,15 @@ yarn install
                                {"touch", "log/production.log"},
                                {"chown", "-R", "--from=root", "www-data:www-data", "/var/www/.bundle", "/var/www/.gem", "/var/www/.npm", "/var/www/.passenger", "log", "tmp", "vendor", ".bundle", "Gemfile.lock", "config.ru", "config/environment.rb"},
                                {"sudo", "-u", "www-data", "/var/lib/arvados/bin/gem", "install", "--user", "--conservative", "--no-document", "bundler:" + bundlerversion},
-                               {"sudo", "-u", "www-data", "/var/lib/arvados/bin/bundle", "install", "--deployment", "--jobs", "8", "--path", "/var/www/.gem", "--without", "development test diagnostics performance"},
+                               {"sudo", "-u", "www-data", "/var/lib/arvados/bin/bundle", "config", "set", "--local", "deployment", "true"},
+                               {"sudo", "-u", "www-data", "/var/lib/arvados/bin/bundle", "config", "set", "--local", "path", "/var/www/.gem"},
+                               {"sudo", "-u", "www-data", "/var/lib/arvados/bin/bundle", "config", "set", "--local", "without", "development test diagnostics performance"},
+                               {"sudo", "-u", "www-data", "/var/lib/arvados/bin/bundle", "install", "--jobs", fmt.Sprintf("%d", runtime.NumCPU())},
 
                                {"chown", "www-data:www-data", ".", "public/assets"},
                                // {"sudo", "-u", "www-data", "/var/lib/arvados/bin/bundle", "config", "set", "--local", "system", "true"},
-                               {"sudo", "-u", "www-data", "ARVADOS_CONFIG=none", "RAILS_GROUPS=assets", "RAILS_ENV=production", "/var/lib/arvados/bin/bundle", "exec", "rake", "npm:install"},
-                               {"sudo", "-u", "www-data", "ARVADOS_CONFIG=none", "RAILS_GROUPS=assets", "RAILS_ENV=production", "/var/lib/arvados/bin/bundle", "exec", "rake", "assets:precompile"},
+                               {"sudo", "-u", "www-data", "ARVADOS_CONFIG=none", "RAILS_GROUPS=assets", "RAILS_ENV=production", "PATH=/var/lib/arvados/bin:" + os.Getenv("PATH"), "/var/lib/arvados/bin/bundle", "exec", "rake", "npm:install"},
+                               {"sudo", "-u", "www-data", "ARVADOS_CONFIG=none", "RAILS_GROUPS=assets", "RAILS_ENV=production", "PATH=/var/lib/arvados/bin:" + os.Getenv("PATH"), "/var/lib/arvados/bin/bundle", "exec", "rake", "assets:precompile"},
                                {"chown", "root:root", "."},
                                {"chown", "-R", "root:root", "public/assets", "vendor"},
 
@@ -677,21 +696,71 @@ rsync -a --delete-after build/ /var/lib/arvados/workbench2/
                        return 1
                }
 
+               // Install arvados-cli gem (binaries go in
+               // /var/lib/arvados/bin)
+               if err = inst.runBash(`
+/var/lib/arvados/bin/gem install --conservative --no-document arvados-cli
+`, stdout, stderr); err != nil {
+                       return 1
+               }
+
                err = os.WriteFile("/lib/systemd/system/arvados.service", arvadosServiceFile, 0777)
                if err != nil {
                        return 1
                }
-               // This is equivalent to "systemd enable", but does
-               // not depend on the systemctl program being
-               // available.
-               symlink := "/etc/systemd/system/multi-user.target.wants/arvados.service"
-               err = os.Remove(symlink)
-               if err != nil && !errors.Is(err, os.ErrNotExist) {
-                       return 1
+               if prod {
+                       // (fpm will do this for us in the pkg case)
+                       // This is equivalent to "systemd enable", but
+                       // does not depend on the systemctl program
+                       // being available:
+                       symlink := "/etc/systemd/system/multi-user.target.wants/arvados.service"
+                       err = os.Remove(symlink)
+                       if err != nil && !errors.Is(err, os.ErrNotExist) {
+                               return 1
+                       }
+                       err = os.Symlink("/lib/systemd/system/arvados.service", symlink)
+                       if err != nil {
+                               return 1
+                       }
                }
-               err = os.Symlink("/lib/systemd/system/arvados.service", symlink)
-               if err != nil {
-                       return 1
+
+               // Add symlinks in /usr/bin for user-facing programs
+               for _, srcdst := range [][]string{
+                       // go
+                       {"bin/arvados-client"},
+                       {"bin/arvados-client", "arv"},
+                       {"bin/arvados-server"},
+                       // sdk/cli
+                       {"bin/arv", "arv-ruby"},
+                       {"bin/arv-tag"},
+                       // sdk/python
+                       {"lib/python/bin/arv-copy"},
+                       {"lib/python/bin/arv-federation-migrate"},
+                       {"lib/python/bin/arv-get"},
+                       {"lib/python/bin/arv-keepdocker"},
+                       {"lib/python/bin/arv-ls"},
+                       {"lib/python/bin/arv-migrate-docker19"},
+                       {"lib/python/bin/arv-normalize"},
+                       {"lib/python/bin/arv-put"},
+                       {"lib/python/bin/arv-ws"},
+                       // services/fuse
+                       {"lib/python/bin/arv-mount"},
+               } {
+                       src := "/var/lib/arvados/" + srcdst[0]
+                       if _, err = os.Stat(src); err != nil {
+                               return 1
+                       }
+                       dst := srcdst[len(srcdst)-1]
+                       _, dst = filepath.Split(dst)
+                       dst = "/usr/bin/" + dst
+                       err = os.Remove(dst)
+                       if err != nil && !errors.Is(err, os.ErrNotExist) {
+                               return 1
+                       }
+                       err = os.Symlink(src, dst)
+                       if err != nil {
+                               return 1
+                       }
                }
        }
 
@@ -791,7 +860,7 @@ func prodpkgs(osv osversion) []string {
                "libcurl3-gnutls",
                "libxslt1.1",
                "nginx",
-               "python",
+               "python3",
                "sudo",
        }
        if osv.Debian || osv.Ubuntu {
@@ -801,21 +870,12 @@ func prodpkgs(osv osversion) []string {
                        pkgs = append(pkgs, "python3-distutils") // sdk/cwl
                }
                return append(pkgs,
-                       "g++",
-                       "libcurl4-openssl-dev", // services/api
-                       "libpq-dev",
-                       "libpython2.7", // services/fuse
                        "mime-support", // keep-web
-                       "zlib1g-dev",   // services/api
                )
        } else if osv.Centos {
                return append(pkgs,
                        "fuse-libs", // services/fuse
-                       "gcc",
-                       "gcc-c++",
-                       "libcurl-devel",    // services/api
-                       "mailcap",          // keep-web
-                       "postgresql-devel", // services/api
+                       "mailcap",   // keep-web
                )
        } else {
                panic("os version not supported")
index d2fed1dd7ad4fee9fac408da3e2070c913174b19..36501adf81e60c6b4f9c8f7919f9ee57c5c13581 100644 (file)
@@ -13,16 +13,24 @@ import (
        "flag"
        "fmt"
        "io"
+       "net"
+       "net/http"
+       "net/url"
        "os"
        "os/exec"
        "os/user"
        "regexp"
        "strconv"
+       "strings"
+       "sync/atomic"
        "text/template"
+       "time"
 
        "git.arvados.org/arvados.git/lib/cmd"
        "git.arvados.org/arvados.git/lib/config"
+       "git.arvados.org/arvados.git/lib/controller/rpc"
        "git.arvados.org/arvados.git/sdk/go/arvados"
+       "git.arvados.org/arvados.git/sdk/go/auth"
        "git.arvados.org/arvados.git/sdk/go/ctxlog"
        "github.com/lib/pq"
 )
@@ -30,11 +38,26 @@ import (
 var InitCommand cmd.Handler = &initCommand{}
 
 type initCommand struct {
-       ClusterID          string
-       Domain             string
-       PostgreSQLPassword string
-       Login              string
-       Insecure           bool
+       ClusterID  string
+       Domain     string
+       CreateDB   bool
+       Login      string
+       TLS        string
+       AdminEmail string
+       Start      bool
+
+       PostgreSQL struct {
+               Host     string
+               User     string
+               Password string
+               DB       string
+       }
+       LoginPAM                bool
+       LoginTest               bool
+       LoginGoogle             bool
+       LoginGoogleClientID     string
+       LoginGoogleClientSecret string
+       TLSDir                  string
 }
 
 func (initcmd *initCommand) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
@@ -60,9 +83,12 @@ func (initcmd *initCommand) RunCommand(prog string, args []string, stdin io.Read
        flags.SetOutput(stderr)
        versionFlag := flags.Bool("version", false, "Write version information to stdout and exit 0")
        flags.StringVar(&initcmd.ClusterID, "cluster-id", "", "cluster `id`, like x1234 for a dev cluster")
+       flags.BoolVar(&initcmd.CreateDB, "create-db", true, "create an 'arvados' postgresql role and database using 'sudo -u postgres psql ...' (if false, use existing database specified by POSTGRES_HOST, POSTGRES_USER, POSTGRES_PASSWORD, and POSTGRES_DB env vars, and assume 'CREATE EXTENSION IF NOT EXISTS pg_trgm' has already been done)")
        flags.StringVar(&initcmd.Domain, "domain", hostname, "cluster public DNS `name`, like x1234.arvadosapi.com")
-       flags.StringVar(&initcmd.Login, "login", "", "login `backend`: test, pam, or ''")
-       flags.BoolVar(&initcmd.Insecure, "insecure", false, "accept invalid TLS certificates and configure TrustAllContent (do not use in production!)")
+       flags.StringVar(&initcmd.Login, "login", "", "login `backend`: test, pam, 'google {client-id} {client-secret}', or ''")
+       flags.StringVar(&initcmd.AdminEmail, "admin-email", "", "give admin privileges to user with given `email`")
+       flags.StringVar(&initcmd.TLS, "tls", "none", "tls certificate `source`: acme, insecure, none, or /path/to/dir containing privkey and cert files")
+       flags.BoolVar(&initcmd.Start, "start", true, "start systemd service after creating config")
        if ok, code := cmd.ParseFlags(flags, prog, args, "", stderr); !ok {
                return code
        } else if *versionFlag {
@@ -72,6 +98,87 @@ func (initcmd *initCommand) RunCommand(prog string, args []string, stdin io.Read
                return 1
        }
 
+       if fields := strings.Fields(initcmd.Login); len(fields) == 3 && fields[0] == "google" {
+               initcmd.LoginGoogle = true
+               initcmd.LoginGoogleClientID = fields[1]
+               initcmd.LoginGoogleClientSecret = fields[2]
+       } else if initcmd.Login == "test" {
+               initcmd.LoginTest = true
+               if initcmd.AdminEmail == "" {
+                       initcmd.AdminEmail = "admin@example.com"
+               }
+       } else if initcmd.Login == "pam" {
+               initcmd.LoginPAM = true
+       } else if initcmd.Login == "" {
+               // none; login will show an error page
+       } else {
+               err = fmt.Errorf("invalid argument to -login: %q: should be 'test', 'pam', 'google {client-id} {client-secret}', or empty", initcmd.Login)
+               return 1
+       }
+
+       switch initcmd.TLS {
+       case "none", "acme", "insecure":
+       default:
+               if !strings.HasPrefix(initcmd.TLS, "/") {
+                       err = fmt.Errorf("invalid argument to -tls: %q; see %s -help", initcmd.TLS, prog)
+                       return 1
+               }
+               initcmd.TLSDir = initcmd.TLS
+       }
+
+       confdir := "/etc/arvados"
+       conffile := confdir + "/config.yml"
+       if _, err = os.Stat(conffile); err == nil {
+               err = fmt.Errorf("config file %s already exists; delete it first if you really want to start over", conffile)
+               return 1
+       }
+
+       ports := []int{443}
+       for i := 4440; i < 4460; i++ {
+               ports = append(ports, i)
+       }
+       if initcmd.TLS == "acme" {
+               ports = append(ports, 80)
+       }
+       for _, port := range ports {
+               err = initcmd.checkPort(ctx, fmt.Sprintf("%d", port))
+               if err != nil {
+                       return 1
+               }
+       }
+
+       if initcmd.CreateDB {
+               // Do the "create extension" thing early. This way, if
+               // there's no local postgresql server (a likely
+               // failure mode), we can bail out without any side
+               // effects, and the user can start over easily.
+               fmt.Fprintln(stderr, "installing pg_trgm postgresql extension...")
+               cmd := exec.CommandContext(ctx, "sudo", "-u", "postgres", "psql", "--quiet",
+                       "-c", `CREATE EXTENSION IF NOT EXISTS pg_trgm`)
+               cmd.Dir = "/"
+               cmd.Stdout = stdout
+               cmd.Stderr = stderr
+               err = cmd.Run()
+               if err != nil {
+                       err = fmt.Errorf("error preparing postgresql server: %w", err)
+                       return 1
+               }
+               fmt.Fprintln(stderr, "...done")
+               initcmd.PostgreSQL.Host = "localhost"
+               initcmd.PostgreSQL.User = "arvados"
+               initcmd.PostgreSQL.Password = initcmd.RandomHex(32)
+               initcmd.PostgreSQL.DB = "arvados"
+       } else {
+               initcmd.PostgreSQL.Host = os.Getenv("POSTGRES_HOST")
+               initcmd.PostgreSQL.User = os.Getenv("POSTGRES_USER")
+               initcmd.PostgreSQL.Password = os.Getenv("POSTGRES_PASSWORD")
+               initcmd.PostgreSQL.DB = os.Getenv("POSTGRES_DB")
+               if initcmd.PostgreSQL.Host == "" || initcmd.PostgreSQL.User == "" || initcmd.PostgreSQL.Password == "" || initcmd.PostgreSQL.DB == "" {
+                       err = fmt.Errorf("missing $POSTGRES_* env var(s) for -create-db=false; see %s -help", prog)
+                       return 1
+               }
+       }
+
        wwwuser, err := user.Lookup("www-data")
        if err != nil {
                err = fmt.Errorf("user.Lookup(%q): %w", "www-data", err)
@@ -81,24 +188,29 @@ func (initcmd *initCommand) RunCommand(prog string, args []string, stdin io.Read
        if err != nil {
                return 1
        }
-       initcmd.PostgreSQLPassword = initcmd.RandomHex(32)
 
+       fmt.Fprintln(stderr, "creating data storage directory /var/lib/arvados/keep ...")
        err = os.Mkdir("/var/lib/arvados/keep", 0600)
        if err != nil && !os.IsExist(err) {
                err = fmt.Errorf("mkdir /var/lib/arvados/keep: %w", err)
                return 1
        }
-       fmt.Fprintln(stderr, "created /var/lib/arvados/keep")
+       fmt.Fprintln(stderr, "...done")
 
-       err = os.Mkdir("/etc/arvados", 0750)
+       fmt.Fprintln(stderr, "creating config file", conffile, "...")
+       err = os.Mkdir(confdir, 0750)
        if err != nil && !os.IsExist(err) {
-               err = fmt.Errorf("mkdir /etc/arvados: %w", err)
+               err = fmt.Errorf("mkdir %s: %w", confdir, err)
                return 1
        }
-       err = os.Chown("/etc/arvados", 0, wwwgid)
-       f, err := os.OpenFile("/etc/arvados/config.yml", os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)
+       err = os.Chown(confdir, 0, wwwgid)
        if err != nil {
-               err = fmt.Errorf("open /etc/arvados/config.yml: %w", err)
+               err = fmt.Errorf("chown 0:%d %s: %w", wwwgid, confdir, err)
+               return 1
+       }
+       f, err := os.OpenFile(conffile+".tmp", os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)
+       if err != nil {
+               err = fmt.Errorf("open %s: %w", conffile+".tmp", err)
                return 1
        }
        tmpl, err := template.New("config").Parse(`Clusters:
@@ -113,8 +225,8 @@ func (initcmd *initCommand) RunCommand(prog string, args []string, stdin io.Read
           "http://0.0.0.0:9001/": {}
       Websocket:
         InternalURLs:
-          "http://0.0.0.0:9004/": {}
-        ExternalURL: {{printf "%q" ( print "wss://" .Domain ":4444/websocket" ) }}
+          "http://0.0.0.0:8005/": {}
+        ExternalURL: {{printf "%q" ( print "wss://" .Domain ":4446/" ) }}
       Keepbalance:
         InternalURLs:
           "http://0.0.0.0:9019/": {}
@@ -155,7 +267,7 @@ func (initcmd *initCommand) RunCommand(prog string, args []string, stdin io.Read
           "http://0.0.0.0:9011/": {}
     Collections:
       BlobSigningKey: {{printf "%q" ( .RandomHex 50 )}}
-      {{if .Insecure}}
+      {{if eq .TLS "insecure"}}
       TrustAllContent: true
       {{end}}
     Containers:
@@ -166,15 +278,23 @@ func (initcmd *initCommand) RunCommand(prog string, args []string, stdin io.Read
     ManagementToken: {{printf "%q" ( .RandomHex 50 )}}
     PostgreSQL:
       Connection:
-        dbname: arvados_production
-        host: localhost
-        user: arvados
-        password: {{printf "%q" .PostgreSQLPassword}}
+        dbname: {{printf "%q" .PostgreSQL.DB}}
+        host: {{printf "%q" .PostgreSQL.Host}}
+        user: {{printf "%q" .PostgreSQL.User}}
+        password: {{printf "%q" .PostgreSQL.Password}}
     SystemRootToken: {{printf "%q" ( .RandomHex 50 )}}
-    {{if .Insecure}}
     TLS:
+      {{if eq .TLS "insecure"}}
       Insecure: true
-    {{end}}
+      {{else if eq .TLS "acme"}}
+      ACME:
+        Server: LE
+      {{else if ne .TLSDir ""}}
+      Certificate: {{printf "%q" (print .TLSDir "/cert")}}
+      Key: {{printf "%q" (print .TLSDir "/privkey")}}
+      {{else}}
+      {}
+      {{end}}
     Volumes:
       {{.ClusterID}}-nyw5e-000000000000000:
         Driver: Directory
@@ -183,47 +303,54 @@ func (initcmd *initCommand) RunCommand(prog string, args []string, stdin io.Read
         Replication: 2
     Workbench:
       SecretKeyBase: {{printf "%q" ( .RandomHex 50 )}}
+    {{if .LoginPAM}}
     Login:
-      {{if eq .Login "pam"}}
       PAM:
         Enable: true
-      {{else if eq .Login "test"}}
+    {{else if .LoginTest}}
+    Login:
       Test:
         Enable: true
         Users:
           admin:
-            Email: admin@example.com
+            Email: {{printf "%q" .AdminEmail}}
             Password: admin
-      {{else}}
-      {}
-      {{end}}
+    {{else if .LoginGoogle}}
+    Login:
+      Google:
+        Enable: true
+        ClientID: {{printf "%q" .LoginGoogleClientID}}
+        ClientSecret: {{printf "%q" .LoginGoogleClientSecret}}
+    {{end}}
     Users:
-      {{if eq .Login "test"}}
-      AutoAdminUserWithEmail: admin@example.com
-      {{else}}
-      {}
-      {{end}}
+      AutoAdminUserWithEmail: {{printf "%q" .AdminEmail}}
 `)
        if err != nil {
                return 1
        }
        err = tmpl.Execute(f, initcmd)
        if err != nil {
-               err = fmt.Errorf("/etc/arvados/config.yml: tmpl.Execute: %w", err)
+               err = fmt.Errorf("%s: tmpl.Execute: %w", conffile+".tmp", err)
                return 1
        }
        err = f.Close()
        if err != nil {
-               err = fmt.Errorf("/etc/arvados/config.yml: close: %w", err)
+               err = fmt.Errorf("%s: close: %w", conffile+".tmp", err)
+               return 1
+       }
+       err = os.Rename(conffile+".tmp", conffile)
+       if err != nil {
+               err = fmt.Errorf("rename %s -> %s: %w", conffile+".tmp", conffile, err)
                return 1
        }
-       fmt.Fprintln(stderr, "created /etc/arvados/config.yml")
+       fmt.Fprintln(stderr, "...done")
 
        ldr := config.NewLoader(nil, logger)
        ldr.SkipLegacy = true
+       ldr.Path = conffile // load the file we just wrote, even if $ARVADOS_CONFIG is set
        cfg, err := ldr.Load()
        if err != nil {
-               err = fmt.Errorf("/etc/arvados/config.yml: %w", err)
+               err = fmt.Errorf("%s: %w", conffile, err)
                return 1
        }
        cluster, err := cfg.GetCluster("")
@@ -231,21 +358,79 @@ func (initcmd *initCommand) RunCommand(prog string, args []string, stdin io.Read
                return 1
        }
 
+       fmt.Fprintln(stderr, "creating postresql user and database...")
        err = initcmd.createDB(ctx, cluster.PostgreSQL.Connection, stderr)
        if err != nil {
                return 1
        }
+       fmt.Fprintln(stderr, "...done")
 
+       fmt.Fprintln(stderr, "initializing database...")
        cmd := exec.CommandContext(ctx, "sudo", "-u", "www-data", "-E", "HOME=/var/www", "PATH=/var/lib/arvados/bin:"+os.Getenv("PATH"), "/var/lib/arvados/bin/bundle", "exec", "rake", "db:setup")
        cmd.Dir = "/var/lib/arvados/railsapi"
        cmd.Stdout = stderr
        cmd.Stderr = stderr
        err = cmd.Run()
        if err != nil {
-               err = fmt.Errorf("rake db:setup: %w", err)
+               err = fmt.Errorf("rake db:setup failed: %w", err)
                return 1
        }
-       fmt.Fprintln(stderr, "initialized database")
+       fmt.Fprintln(stderr, "...done")
+
+       if initcmd.Start {
+               fmt.Fprintln(stderr, "starting systemd service...")
+               cmd := exec.CommandContext(ctx, "systemctl", "start", "arvados")
+               cmd.Dir = "/"
+               cmd.Stdout = stderr
+               cmd.Stderr = stderr
+               err = cmd.Run()
+               if err != nil {
+                       err = fmt.Errorf("%v: %w", cmd.Args, err)
+                       return 1
+               }
+               fmt.Fprintln(stderr, "...done")
+
+               fmt.Fprintln(stderr, "checking controller API endpoint...")
+               u := url.URL(cluster.Services.Controller.ExternalURL)
+               conn := rpc.NewConn(cluster.ClusterID, &u, cluster.TLS.Insecure, rpc.PassthroughTokenProvider)
+               ctx := auth.NewContext(context.Background(), auth.NewCredentials(cluster.SystemRootToken))
+               _, err = conn.UserGetCurrent(ctx, arvados.GetOptions{})
+               if err != nil {
+                       err = fmt.Errorf("API request failed: %w", err)
+                       return 1
+               }
+               fmt.Fprintln(stderr, "...looks good")
+       }
+
+       if out, err := exec.CommandContext(ctx, "docker", "version").CombinedOutput(); err == nil && strings.Contains(string(out), "\nServer:\n") {
+               fmt.Fprintln(stderr, "loading alpine docker image for diagnostics...")
+               cmd := exec.CommandContext(ctx, "docker", "pull", "alpine")
+               cmd.Stdout = stderr
+               cmd.Stderr = stderr
+               err = cmd.Run()
+               if err != nil {
+                       err = fmt.Errorf("%v: %w", cmd.Args, err)
+                       return 1
+               }
+               cmd = exec.CommandContext(ctx, "arv", "sudo", "keep", "docker", "alpine")
+               cmd.Stdout = stderr
+               cmd.Stderr = stderr
+               err = cmd.Run()
+               if err != nil {
+                       err = fmt.Errorf("%v: %w", cmd.Args, err)
+                       return 1
+               }
+               fmt.Fprintln(stderr, "...done")
+       } else {
+               fmt.Fprintln(stderr, "docker is not installed -- skipping step of downloading 'alpine' image")
+       }
+
+       fmt.Fprintf(stderr, `
+Setup complete. Next steps:
+* run 'arv sudo diagnostics'
+* log in to workbench2 at %s
+* see documentation at https://doc.arvados.org/install/automatic.html
+`, cluster.Services.Workbench2.ExternalURL.String())
 
        return 0
 }
@@ -275,18 +460,98 @@ func (initcmd *initCommand) RandomHex(chars int) string {
 }
 
 func (initcmd *initCommand) createDB(ctx context.Context, dbconn arvados.PostgreSQLConnection, stderr io.Writer) error {
-       for _, sql := range []string{
-               `CREATE USER ` + pq.QuoteIdentifier(dbconn["user"]) + ` WITH SUPERUSER ENCRYPTED PASSWORD ` + pq.QuoteLiteral(dbconn["password"]),
-               `CREATE DATABASE ` + pq.QuoteIdentifier(dbconn["dbname"]) + ` WITH TEMPLATE template0 ENCODING 'utf8'`,
-               `CREATE EXTENSION IF NOT EXISTS pg_trgm`,
-       } {
-               cmd := exec.CommandContext(ctx, "sudo", "-u", "postgres", "psql", "-c", sql)
-               cmd.Stdout = stderr
-               cmd.Stderr = stderr
-               err := cmd.Run()
-               if err != nil {
-                       return fmt.Errorf("error setting up arvados user/database: %w", err)
-               }
+       cmd := exec.CommandContext(ctx, "sudo", "-u", "postgres", "psql", "--quiet",
+               "-c", `CREATE USER `+pq.QuoteIdentifier(dbconn["user"])+` WITH SUPERUSER ENCRYPTED PASSWORD `+pq.QuoteLiteral(dbconn["password"]),
+               "-c", `CREATE DATABASE `+pq.QuoteIdentifier(dbconn["dbname"])+` WITH TEMPLATE template0 ENCODING 'utf8'`,
+       )
+       cmd.Dir = "/"
+       cmd.Stdout = stderr
+       cmd.Stderr = stderr
+       err := cmd.Run()
+       if err != nil {
+               return fmt.Errorf("error setting up arvados user/database: %w", err)
+       }
+       return nil
+}
+
+// Confirm that http://{initcmd.Domain}:{port} reaches a server that
+// we run on {port}.
+//
+// If port is "80", listening fails, and Nginx appears to be using the
+// debian-packaged default configuration that listens on port 80,
+// disable that Nginx config and try again.
+//
+// (Typically, the reason Nginx is installed is so that Arvados can
+// run an Nginx child process; the default Nginx service using config
+// from /etc/nginx is just an unfortunate side effect of installing
+// Nginx by way of the Debian package.)
+func (initcmd *initCommand) checkPort(ctx context.Context, port string) error {
+       err := initcmd.checkPortOnce(ctx, port)
+       if err == nil || port != "80" {
+               // success, or poking Nginx in the eye won't help
+               return err
+       }
+       d, err2 := os.Open("/etc/nginx/sites-enabled/.")
+       if err2 != nil {
+               return err
+       }
+       fis, err2 := d.Readdir(-1)
+       if err2 != nil || len(fis) != 1 {
+               return err
+       }
+       if target, err2 := os.Readlink("/etc/nginx/sites-enabled/default"); err2 != nil || target != "/etc/nginx/sites-available/default" {
+               return err
+       }
+       err2 = os.Remove("/etc/nginx/sites-enabled/default")
+       if err2 != nil {
+               return err
+       }
+       exec.CommandContext(ctx, "nginx", "-s", "reload").Run()
+       time.Sleep(time.Second)
+       return initcmd.checkPortOnce(ctx, port)
+}
+
+// Start an http server on 0.0.0.0:{port} and confirm that
+// http://{initcmd.Domain}:{port} reaches that server.
+func (initcmd *initCommand) checkPortOnce(ctx context.Context, port string) error {
+       b := make([]byte, 128)
+       _, err := rand.Read(b)
+       if err != nil {
+               return err
+       }
+       token := fmt.Sprintf("%x", b)
+
+       srv := http.Server{
+               Addr: net.JoinHostPort("", port),
+               Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+                       fmt.Fprint(w, token)
+               })}
+       var errServe atomic.Value
+       go func() {
+               errServe.Store(srv.ListenAndServe())
+       }()
+       defer srv.Close()
+       url := "http://" + net.JoinHostPort(initcmd.Domain, port) + "/probe"
+       req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
+       if err != nil {
+               return err
+       }
+       resp, err := http.DefaultClient.Do(req)
+       if err == nil {
+               defer resp.Body.Close()
+       }
+       if errServe, _ := errServe.Load().(error); errServe != nil {
+               // If server already exited, return that error
+               // (probably "can't listen"), not the request error.
+               return errServe
+       }
+       if err != nil {
+               return err
+       }
+       buf := make([]byte, len(token))
+       n, err := io.ReadFull(resp.Body, buf)
+       if string(buf[:n]) != token {
+               return fmt.Errorf("listened on port %s but %s connected to something else, returned %q, err %v", port, url, buf[:n], err)
        }
        return nil
 }
index 4b640c4e4773225ccb0e9312bc18a436552e9cfb..20441c2a6c4534eb697a85bfc4c369e64ae0aad9 100644 (file)
@@ -159,7 +159,7 @@ func (c *command) RunCommand(prog string, args []string, stdin io.Reader, stdout
                Addr: listenURL.Host,
        }
        if listenURL.Scheme == "https" || listenURL.Scheme == "wss" {
-               tlsconfig, err := tlsConfigWithCertUpdater(cluster, logger)
+               tlsconfig, err := makeTLSConfig(cluster, logger)
                if err != nil {
                        logger.WithError(err).Errorf("cannot start %s service on %s", c.svcName, listenURL.String())
                        return 1
index c6307b76ab02b79342cfa3395899c5f27ffd5f57..88a2858beb13e2f5db216a7645423fd7d2c7c541 100644 (file)
@@ -5,6 +5,7 @@
 package service
 
 import (
+       "context"
        "crypto/tls"
        "errors"
        "fmt"
@@ -12,20 +13,68 @@ import (
        "os/signal"
        "strings"
        "syscall"
+       "time"
 
        "git.arvados.org/arvados.git/sdk/go/arvados"
        "github.com/sirupsen/logrus"
+       "golang.org/x/crypto/acme/autocert"
 )
 
-func tlsConfigWithCertUpdater(cluster *arvados.Cluster, logger logrus.FieldLogger) (*tls.Config, error) {
+func makeTLSConfig(cluster *arvados.Cluster, logger logrus.FieldLogger) (*tls.Config, error) {
+       if cluster.TLS.ACME.Server != "" {
+               return makeAutocertConfig(cluster, logger)
+       } else {
+               return makeFileLoaderConfig(cluster, logger)
+       }
+}
+
+var errCertUnavailable = errors.New("certificate unavailable, waiting for supervisor to update cache")
+
+type readonlyDirCache autocert.DirCache
+
+func (c readonlyDirCache) Get(ctx context.Context, name string) ([]byte, error) {
+       data, err := autocert.DirCache(c).Get(ctx, name)
+       if err != nil {
+               // Returning an error other than autocert.ErrCacheMiss
+               // causes GetCertificate() to fail early instead of
+               // trying to obtain a certificate itself (which
+               // wouldn't work because we're not in a position to
+               // answer challenges).
+               return nil, errCertUnavailable
+       }
+       return data, nil
+}
+
+func (c readonlyDirCache) Put(ctx context.Context, name string, data []byte) error {
+       return fmt.Errorf("(bug?) (readonlyDirCache)Put(%s) called", name)
+}
+
+func (c readonlyDirCache) Delete(ctx context.Context, name string) error {
+       return nil
+}
+
+func makeAutocertConfig(cluster *arvados.Cluster, logger logrus.FieldLogger) (*tls.Config, error) {
+       mgr := &autocert.Manager{
+               Cache:  readonlyDirCache("/var/lib/arvados/tmp/autocert"),
+               Prompt: autocert.AcceptTOS,
+               // HostPolicy accepts all names because this Manager
+               // doesn't request certs. Whoever writes certs to our
+               // cache is effectively responsible for HostPolicy.
+               HostPolicy: func(ctx context.Context, host string) error { return nil },
+               // Keep using whatever's in the cache as long as
+               // possible. Assume some other process (see lib/boot)
+               // handles renewals.
+               RenewBefore: time.Second,
+       }
+       return mgr.TLSConfig(), nil
+}
+
+func makeFileLoaderConfig(cluster *arvados.Cluster, logger logrus.FieldLogger) (*tls.Config, error) {
        currentCert := make(chan *tls.Certificate, 1)
        loaded := false
 
-       key, cert := cluster.TLS.Key, cluster.TLS.Certificate
-       if !strings.HasPrefix(key, "file://") || !strings.HasPrefix(cert, "file://") {
-               return nil, errors.New("cannot use TLS certificate: TLS.Key and TLS.Certificate must be specified with a 'file://' prefix")
-       }
-       key, cert = key[7:], cert[7:]
+       key := strings.TrimPrefix(cluster.TLS.Key, "file://")
+       cert := strings.TrimPrefix(cluster.TLS.Certificate, "file://")
 
        update := func() error {
                cert, err := tls.LoadX509KeyPair(cert, key)
@@ -45,9 +94,14 @@ func tlsConfigWithCertUpdater(cluster *arvados.Cluster, logger logrus.FieldLogge
                return nil, err
        }
 
+       reload := make(chan os.Signal, 1)
+       signal.Notify(reload, syscall.SIGHUP)
+       go func() {
+               for range time.NewTicker(time.Hour).C {
+                       reload <- nil
+               }
+       }()
        go func() {
-               reload := make(chan os.Signal, 1)
-               signal.Notify(reload, syscall.SIGHUP)
                for range reload {
                        err := update()
                        if err != nil {
index c90551a6109af9dc9afbdd33bed9c78f5f7bc5ed..6d8f39dfb316fbaba1cf64f71ef5f5f778f91e8e 100644 (file)
@@ -227,6 +227,9 @@ type Cluster struct {
                Certificate string
                Key         string
                Insecure    bool
+               ACME        struct {
+                       Server string
+               }
        }
        Users struct {
                ActivatedUsersAreVisibleToOthers      bool
index 543390004b7479be19d0a4422b4f41366a0f2014..4ad3eda42008b4c7f5ddd200dca9ca14336a436a 100644 (file)
@@ -16,7 +16,7 @@ http {
   uwsgi_temp_path "{{TMPDIR}}";
   scgi_temp_path "{{TMPDIR}}";
   upstream controller {
-    server {{LISTENHOST}}:{{CONTROLLERPORT}};
+    server {{UPSTREAMHOST}}:{{CONTROLLERPORT}};
   }
   server {
     listen {{LISTENHOST}}:{{CONTROLLERSSLPORT}} ssl;
@@ -37,7 +37,7 @@ http {
     }
   }
   upstream arv-git-http {
-    server {{LISTENHOST}}:{{GITPORT}};
+    server {{UPSTREAMHOST}}:{{GITPORT}};
   }
   server {
     listen {{LISTENHOST}}:{{GITSSLPORT}} ssl;
@@ -53,7 +53,7 @@ http {
     }
   }
   upstream keepproxy {
-    server {{LISTENHOST}}:{{KEEPPROXYPORT}};
+    server {{UPSTREAMHOST}}:{{KEEPPROXYPORT}};
   }
   server {
     listen {{LISTENHOST}}:{{KEEPPROXYSSLPORT}} ssl;
@@ -73,7 +73,7 @@ http {
     }
   }
   upstream keep-web {
-    server {{LISTENHOST}}:{{KEEPWEBPORT}};
+    server {{UPSTREAMHOST}}:{{KEEPWEBPORT}};
   }
   server {
     listen {{LISTENHOST}}:{{KEEPWEBSSLPORT}} ssl;
@@ -93,7 +93,7 @@ http {
     }
   }
   upstream health {
-    server {{LISTENHOST}}:{{HEALTHPORT}};
+    server {{UPSTREAMHOST}}:{{HEALTHPORT}};
   }
   server {
     listen {{LISTENHOST}}:{{HEALTHSSLPORT}} ssl;
@@ -129,7 +129,7 @@ http {
     }
   }
   upstream ws {
-    server {{LISTENHOST}}:{{WSPORT}};
+    server {{UPSTREAMHOST}}:{{WSPORT}};
   }
   server {
     listen {{LISTENHOST}}:{{WSSSLPORT}} ssl;
@@ -144,10 +144,14 @@ http {
       proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
       proxy_set_header X-Forwarded-Proto https;
       proxy_redirect off;
+
+      client_max_body_size 0;
+      proxy_http_version 1.1;
+      proxy_request_buffering off;
     }
   }
   upstream workbench1 {
-    server {{LISTENHOST}}:{{WORKBENCH1PORT}};
+    server {{UPSTREAMHOST}}:{{WORKBENCH1PORT}};
   }
   server {
     listen {{LISTENHOST}}:{{WORKBENCH1SSLPORT}} ssl;
@@ -163,7 +167,7 @@ http {
     }
   }
   upstream workbench2 {
-    server {{LISTENHOST}}:{{WORKBENCH2PORT}};
+    server {{UPSTREAMHOST}}:{{WORKBENCH2PORT}};
   }
   server {
     listen {{LISTENHOST}}:{{WORKBENCH2SSLPORT}} ssl;
index 2c01b35aeac79b1642b18c7af7d166ef2cffdc3c..28cb0953f3c42a348a623a4f3f54aadc27d7958c 100644 (file)
@@ -635,6 +635,7 @@ def run_nginx():
         return
     stop_nginx()
     nginxconf = {}
+    nginxconf['UPSTREAMHOST'] = 'localhost'
     nginxconf['LISTENHOST'] = 'localhost'
     nginxconf['CONTROLLERPORT'] = internal_port_from_config("Controller")
     nginxconf['ARVADOS_API_HOST'] = "0.0.0.0:" + str(external_port_from_config("Controller"))
index 49fa58493557a45584a59c992b14e595492ad935..6bc53be4f886286103023537dd3b335fae8f34b1 100644 (file)
@@ -8,43 +8,43 @@ GIT
 GEM
   remote: https://rubygems.org/
   specs:
-    actioncable (5.2.8)
-      actionpack (= 5.2.8)
+    actioncable (5.2.8.1)
+      actionpack (= 5.2.8.1)
       nio4r (~> 2.0)
       websocket-driver (>= 0.6.1)
-    actionmailer (5.2.8)
-      actionpack (= 5.2.8)
-      actionview (= 5.2.8)
-      activejob (= 5.2.8)
+    actionmailer (5.2.8.1)
+      actionpack (= 5.2.8.1)
+      actionview (= 5.2.8.1)
+      activejob (= 5.2.8.1)
       mail (~> 2.5, >= 2.5.4)
       rails-dom-testing (~> 2.0)
-    actionpack (5.2.8)
-      actionview (= 5.2.8)
-      activesupport (= 5.2.8)
+    actionpack (5.2.8.1)
+      actionview (= 5.2.8.1)
+      activesupport (= 5.2.8.1)
       rack (~> 2.0, >= 2.0.8)
       rack-test (>= 0.6.3)
       rails-dom-testing (~> 2.0)
       rails-html-sanitizer (~> 1.0, >= 1.0.2)
-    actionview (5.2.8)
-      activesupport (= 5.2.8)
+    actionview (5.2.8.1)
+      activesupport (= 5.2.8.1)
       builder (~> 3.1)
       erubi (~> 1.4)
       rails-dom-testing (~> 2.0)
       rails-html-sanitizer (~> 1.0, >= 1.0.3)
-    activejob (5.2.8)
-      activesupport (= 5.2.8)
+    activejob (5.2.8.1)
+      activesupport (= 5.2.8.1)
       globalid (>= 0.3.6)
-    activemodel (5.2.8)
-      activesupport (= 5.2.8)
-    activerecord (5.2.8)
-      activemodel (= 5.2.8)
-      activesupport (= 5.2.8)
+    activemodel (5.2.8.1)
+      activesupport (= 5.2.8.1)
+    activerecord (5.2.8.1)
+      activemodel (= 5.2.8.1)
+      activesupport (= 5.2.8.1)
       arel (>= 9.0)
-    activestorage (5.2.8)
-      actionpack (= 5.2.8)
-      activerecord (= 5.2.8)
+    activestorage (5.2.8.1)
+      actionpack (= 5.2.8.1)
+      activerecord (= 5.2.8.1)
       marcel (~> 1.0.0)
-    activesupport (5.2.8)
+    activesupport (5.2.8.1)
       concurrent-ruby (~> 1.0, >= 1.0.2)
       i18n (>= 0.7, < 2)
       minitest (~> 5.1)
@@ -140,7 +140,7 @@ GEM
     multi_json (1.15.0)
     multipart-post (2.1.1)
     nio4r (2.5.8)
-    nokogiri (1.13.6)
+    nokogiri (1.13.7)
       mini_portile2 (~> 2.8.0)
       racc (~> 1.4)
     oj (3.9.2)
@@ -153,21 +153,21 @@ GEM
     power_assert (1.1.4)
     public_suffix (4.0.6)
     racc (1.6.0)
-    rack (2.2.3.1)
-    rack-test (1.1.0)
-      rack (>= 1.0, < 3)
-    rails (5.2.8)
-      actioncable (= 5.2.8)
-      actionmailer (= 5.2.8)
-      actionpack (= 5.2.8)
-      actionview (= 5.2.8)
-      activejob (= 5.2.8)
-      activemodel (= 5.2.8)
-      activerecord (= 5.2.8)
-      activestorage (= 5.2.8)
-      activesupport (= 5.2.8)
+    rack (2.2.4)
+    rack-test (2.0.2)
+      rack (>= 1.3)
+    rails (5.2.8.1)
+      actioncable (= 5.2.8.1)
+      actionmailer (= 5.2.8.1)
+      actionpack (= 5.2.8.1)
+      actionview (= 5.2.8.1)
+      activejob (= 5.2.8.1)
+      activemodel (= 5.2.8.1)
+      activerecord (= 5.2.8.1)
+      activestorage (= 5.2.8.1)
+      activesupport (= 5.2.8.1)
       bundler (>= 1.3.0)
-      railties (= 5.2.8)
+      railties (= 5.2.8.1)
       sprockets-rails (>= 2.0.0)
     rails-controller-testing (1.0.4)
       actionpack (>= 5.0.1.x)
@@ -181,9 +181,9 @@ GEM
     rails-observers (0.1.5)
       activemodel (>= 4.0)
     rails-perftest (0.0.7)
-    railties (5.2.8)
-      actionpack (= 5.2.8)
-      activesupport (= 5.2.8)
+    railties (5.2.8.1)
+      actionpack (= 5.2.8.1)
+      activesupport (= 5.2.8.1)
       method_source
       rake (>= 0.8.7)
       thor (>= 0.19.0, < 2.0)
@@ -222,7 +222,7 @@ GEM
       power_assert
     thor (1.2.1)
     thread_safe (0.3.6)
-    tzinfo (1.2.9)
+    tzinfo (1.2.10)
       thread_safe (~> 0.1)
     websocket-driver (0.7.5)
       websocket-extensions (>= 0.1.0)
index 52d36ac57735f0c16d4b0ed6271a50681e08e05b..1662278cc3edc0c4b9c681d51b1696c195b27a98 100644 (file)
@@ -24,6 +24,7 @@ class User < ArvadosModel
   validate :identity_url_nil_if_empty
   before_update :prevent_privilege_escalation
   before_update :prevent_inactive_admin
+  before_update :prevent_nonadmin_system_root
   before_update :verify_repositories_empty, :if => Proc.new {
     username.nil? and username_changed?
   }
@@ -301,6 +302,10 @@ SELECT target_uuid, perm_level
 
   # delete user signatures, login, repo, and vm perms, and mark as inactive
   def unsetup
+    if self.uuid == system_user_uuid
+      raise "System root user cannot be deactivated"
+    end
+
     # delete oid_login_perms for this user
     #
     # note: these permission links are obsolete, they have no effect
@@ -345,6 +350,11 @@ SELECT target_uuid, perm_level
     self.save!
   end
 
+  # Called from ArvadosModel
+  def set_default_owner
+    self.owner_uuid = system_user_uuid
+  end
+
   def must_unsetup_to_deactivate
     if !self.new_record? &&
        self.uuid[0..4] == Rails.configuration.Login.LoginCluster &&
@@ -702,6 +712,13 @@ SELECT target_uuid, perm_level
     true
   end
 
+  def prevent_nonadmin_system_root
+    if self.uuid == system_user_uuid and self.is_admin_changed? and !self.is_admin
+      raise "System root user cannot be non-admin"
+    end
+    true
+  end
+
   def search_permissions(start, graph, merged={}, upstream_mask=nil, upstream_path={})
     nextpaths = graph[start]
     return merged if !nextpaths
index f3e787e3dffc6ed01570b249865068f1ecfacb7e..430f0d385d7e3789995af57219ece58eeec59367 100644 (file)
@@ -480,4 +480,60 @@ class UsersTest < ActionDispatch::IntegrationTest
     assert_response 403
   end
 
+  test "disabling system root user not permitted" do
+    put("/arvados/v1/users/#{users(:system_user).uuid}",
+      params: {
+        user: {is_admin: false}
+      },
+      headers: auth(:admin))
+    assert_response 422
+
+    post("/arvados/v1/users/#{users(:system_user).uuid}/unsetup",
+      params: {},
+      headers: auth(:admin))
+    assert_response 422
+  end
+
+  test "creating users only accepted for admins" do
+    assert_equal false, users(:active).is_admin
+    post '/arvados/v1/users',
+      params: {
+        "user" => {
+          "email" => 'foo@example.com',
+          "username" => "barney"
+        }
+      },
+      headers: auth(:active)
+    assert_response 403
+  end
+
+  test "create users assigns the system root user as their owner" do
+    post '/arvados/v1/users',
+      params: {
+        "user" => {
+          "email" => 'foo@example.com',
+          "username" => "barney"
+        }
+      },
+      headers: auth(:admin)
+    assert_response :success
+    assert_not_nil json_response["uuid"]
+    assert_equal users(:system_user).uuid, json_response["owner_uuid"]
+  end
+
+  test "create users ignores provided owner_uuid field" do
+    assert_equal false, users(:admin).uuid == users(:system_user).uuid
+    post '/arvados/v1/users',
+      params: {
+        "user" => {
+          "email" => 'foo@example.com',
+          "owner_uuid" => users(:admin).uuid,
+          "username" => "barney"
+        }
+      },
+      headers: auth(:admin)
+    assert_response :success
+    assert_not_nil json_response["uuid"]
+    assert_equal users(:system_user).uuid, json_response["owner_uuid"]
+  end
 end
index 54b8c02165e79c03e8a23410b80691e0561e7688..1f1f509860bb9950d95e5d9c566e9e57f9d4df36 100644 (file)
@@ -411,16 +411,44 @@ func (h *handler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) {
                        }
                }
                // The client's token was invalid (e.g., expired), or
-               // the client didn't even provide one.  Propagate the
-               // 401 to encourage the client to use a [different]
-               // token.
+               // the client didn't even provide one.  Redirect to
+               // workbench2's login-and-redirect-to-download url if
+               // this is a browser navigation request. (The redirect
+               // flow can't preserve the original method if it's not
+               // GET, and doesn't make sense if the UA is a
+               // command-line tool, is trying to load an inline
+               // image, etc.; in these cases, there's nothing we can
+               // do, so return 401 unauthorized.)
+               //
+               // Note Sec-Fetch-Mode is sent by all non-EOL
+               // browsers, except Safari.
+               // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-Fetch-Mode
                //
                // TODO(TC): This response would be confusing to
                // someone trying (anonymously) to download public
                // data that has been deleted.  Allow a referrer to
                // provide this context somehow?
-               w.Header().Add("WWW-Authenticate", "Basic realm=\"collections\"")
-               http.Error(w, unauthorizedMessage, http.StatusUnauthorized)
+               if r.Method == http.MethodGet && r.Header.Get("Sec-Fetch-Mode") == "navigate" {
+                       target := url.URL(h.Cluster.Services.Workbench2.ExternalURL)
+                       redirkey := "redirectToPreview"
+                       if attachment {
+                               redirkey = "redirectToDownload"
+                       }
+                       callback := "/c=" + collectionID + "/" + strings.Join(targetPath, "/")
+                       // target.RawQuery = url.Values{redirkey:
+                       // {target}}.Encode() would be the obvious
+                       // thing to do here, but wb2 doesn't decode
+                       // this as a query param -- it takes
+                       // everything after "${redirkey}=" as the
+                       // target URL. If we encode "/" as "%2F" etc.,
+                       // the redirect won't work.
+                       target.RawQuery = redirkey + "=" + callback
+                       w.Header().Add("Location", target.String())
+                       w.WriteHeader(http.StatusSeeOther)
+               } else {
+                       w.Header().Add("WWW-Authenticate", "Basic realm=\"collections\"")
+                       http.Error(w, unauthorizedMessage, http.StatusUnauthorized)
+               }
                return
        }
 
index 92fea87a01c0eec63ce53162a3d61f0634ebba37..768013185ae7d50dc450fecd342ebcdc343db059 100644 (file)
@@ -391,7 +391,7 @@ func (s *IntegrationSuite) TestVhostRedirectQueryTokenToCookie(c *check.C) {
        s.testVhostRedirectTokenToCookie(c, "GET",
                arvadostest.FooCollection+".example.com/foo",
                "?api_token="+arvadostest.ActiveToken,
-               "",
+               nil,
                "",
                http.StatusOK,
                "foo",
@@ -402,7 +402,7 @@ func (s *IntegrationSuite) TestSingleOriginSecretLink(c *check.C) {
        s.testVhostRedirectTokenToCookie(c, "GET",
                "example.com/c="+arvadostest.FooCollection+"/t="+arvadostest.ActiveToken+"/foo",
                "",
-               "",
+               nil,
                "",
                http.StatusOK,
                "foo",
@@ -415,7 +415,7 @@ func (s *IntegrationSuite) TestSingleOriginSecretLinkBadToken(c *check.C) {
        s.testVhostRedirectTokenToCookie(c, "GET",
                "example.com/c="+arvadostest.FooCollection+"/t=bogus/foo",
                "",
-               "",
+               nil,
                "",
                http.StatusNotFound,
                notFoundMessage+"\n",
@@ -423,13 +423,70 @@ func (s *IntegrationSuite) TestSingleOriginSecretLinkBadToken(c *check.C) {
 }
 
 // Bad token in a cookie (even if it got there via our own
-// query-string-to-cookie redirect) is, in principle, retryable at the
-// same URL so it's 401 Unauthorized.
+// query-string-to-cookie redirect) is, in principle, retryable via
+// wb2-login-and-redirect flow.
 func (s *IntegrationSuite) TestVhostRedirectQueryTokenToBogusCookie(c *check.C) {
-       s.testVhostRedirectTokenToCookie(c, "GET",
+       // Inline
+       resp := s.testVhostRedirectTokenToCookie(c, "GET",
+               arvadostest.FooCollection+".example.com/foo",
+               "?api_token=thisisabogustoken",
+               http.Header{"Sec-Fetch-Mode": {"navigate"}},
+               "",
+               http.StatusSeeOther,
+               "",
+       )
+       u, err := url.Parse(resp.Header().Get("Location"))
+       c.Assert(err, check.IsNil)
+       c.Logf("redirected to %s", u)
+       c.Check(u.Host, check.Equals, s.handler.Cluster.Services.Workbench2.ExternalURL.Host)
+       c.Check(u.Query().Get("redirectToPreview"), check.Equals, "/c="+arvadostest.FooCollection+"/foo")
+       c.Check(u.Query().Get("redirectToDownload"), check.Equals, "")
+
+       // Download/attachment indicated by ?disposition=attachment
+       resp = s.testVhostRedirectTokenToCookie(c, "GET",
                arvadostest.FooCollection+".example.com/foo",
+               "?api_token=thisisabogustoken&disposition=attachment",
+               http.Header{"Sec-Fetch-Mode": {"navigate"}},
+               "",
+               http.StatusSeeOther,
+               "",
+       )
+       u, err = url.Parse(resp.Header().Get("Location"))
+       c.Assert(err, check.IsNil)
+       c.Logf("redirected to %s", u)
+       c.Check(u.Host, check.Equals, s.handler.Cluster.Services.Workbench2.ExternalURL.Host)
+       c.Check(u.Query().Get("redirectToPreview"), check.Equals, "")
+       c.Check(u.Query().Get("redirectToDownload"), check.Equals, "/c="+arvadostest.FooCollection+"/foo")
+
+       // Download/attachment indicated by vhost
+       resp = s.testVhostRedirectTokenToCookie(c, "GET",
+               s.handler.Cluster.Services.WebDAVDownload.ExternalURL.Host+"/c="+arvadostest.FooCollection+"/foo",
                "?api_token=thisisabogustoken",
+               http.Header{"Sec-Fetch-Mode": {"navigate"}},
                "",
+               http.StatusSeeOther,
+               "",
+       )
+       u, err = url.Parse(resp.Header().Get("Location"))
+       c.Assert(err, check.IsNil)
+       c.Logf("redirected to %s", u)
+       c.Check(u.Host, check.Equals, s.handler.Cluster.Services.Workbench2.ExternalURL.Host)
+       c.Check(u.Query().Get("redirectToPreview"), check.Equals, "")
+       c.Check(u.Query().Get("redirectToDownload"), check.Equals, "/c="+arvadostest.FooCollection+"/foo")
+
+       // Without "Sec-Fetch-Mode: navigate" header, just 401.
+       s.testVhostRedirectTokenToCookie(c, "GET",
+               s.handler.Cluster.Services.WebDAVDownload.ExternalURL.Host+"/c="+arvadostest.FooCollection+"/foo",
+               "?api_token=thisisabogustoken",
+               http.Header{"Sec-Fetch-Mode": {"cors"}},
+               "",
+               http.StatusUnauthorized,
+               unauthorizedMessage+"\n",
+       )
+       s.testVhostRedirectTokenToCookie(c, "GET",
+               s.handler.Cluster.Services.WebDAVDownload.ExternalURL.Host+"/c="+arvadostest.FooCollection+"/foo",
+               "?api_token=thisisabogustoken",
+               nil,
                "",
                http.StatusUnauthorized,
                unauthorizedMessage+"\n",
@@ -440,7 +497,7 @@ func (s *IntegrationSuite) TestVhostRedirectQueryTokenSingleOriginError(c *check
        s.testVhostRedirectTokenToCookie(c, "GET",
                "example.com/c="+arvadostest.FooCollection+"/foo",
                "?api_token="+arvadostest.ActiveToken,
-               "",
+               nil,
                "",
                http.StatusBadRequest,
                "cannot serve inline content at this URL (possible configuration error; see https://doc.arvados.org/install/install-keep-web.html#dns)\n",
@@ -454,7 +511,7 @@ func (s *IntegrationSuite) TestVhostRedirectQueryTokenRequestAttachment(c *check
        resp := s.testVhostRedirectTokenToCookie(c, "GET",
                arvadostest.FooCollection+".example.com/foo",
                "?disposition=attachment&api_token="+arvadostest.ActiveToken,
-               "",
+               nil,
                "",
                http.StatusOK,
                "foo",
@@ -467,7 +524,7 @@ func (s *IntegrationSuite) TestVhostRedirectQueryTokenSiteFS(c *check.C) {
        resp := s.testVhostRedirectTokenToCookie(c, "GET",
                "download.example.com/by_id/"+arvadostest.FooCollection+"/foo",
                "?api_token="+arvadostest.ActiveToken,
-               "",
+               nil,
                "",
                http.StatusOK,
                "foo",
@@ -480,7 +537,7 @@ func (s *IntegrationSuite) TestPastCollectionVersionFileAccess(c *check.C) {
        resp := s.testVhostRedirectTokenToCookie(c, "GET",
                "download.example.com/c="+arvadostest.WazVersion1Collection+"/waz",
                "?api_token="+arvadostest.ActiveToken,
-               "",
+               nil,
                "",
                http.StatusOK,
                "waz",
@@ -489,7 +546,7 @@ func (s *IntegrationSuite) TestPastCollectionVersionFileAccess(c *check.C) {
        resp = s.testVhostRedirectTokenToCookie(c, "GET",
                "download.example.com/by_id/"+arvadostest.WazVersion1Collection+"/waz",
                "?api_token="+arvadostest.ActiveToken,
-               "",
+               nil,
                "",
                http.StatusOK,
                "waz",
@@ -502,7 +559,7 @@ func (s *IntegrationSuite) TestVhostRedirectQueryTokenTrustAllContent(c *check.C
        s.testVhostRedirectTokenToCookie(c, "GET",
                "example.com/c="+arvadostest.FooCollection+"/foo",
                "?api_token="+arvadostest.ActiveToken,
-               "",
+               nil,
                "",
                http.StatusOK,
                "foo",
@@ -515,7 +572,7 @@ func (s *IntegrationSuite) TestVhostRedirectQueryTokenAttachmentOnlyHost(c *chec
        s.testVhostRedirectTokenToCookie(c, "GET",
                "example.com/c="+arvadostest.FooCollection+"/foo",
                "?api_token="+arvadostest.ActiveToken,
-               "",
+               nil,
                "",
                http.StatusBadRequest,
                "cannot serve inline content at this URL (possible configuration error; see https://doc.arvados.org/install/install-keep-web.html#dns)\n",
@@ -524,7 +581,7 @@ func (s *IntegrationSuite) TestVhostRedirectQueryTokenAttachmentOnlyHost(c *chec
        resp := s.testVhostRedirectTokenToCookie(c, "GET",
                "example.com:1234/c="+arvadostest.FooCollection+"/foo",
                "?api_token="+arvadostest.ActiveToken,
-               "",
+               nil,
                "",
                http.StatusOK,
                "foo",
@@ -536,7 +593,7 @@ func (s *IntegrationSuite) TestVhostRedirectPOSTFormTokenToCookie(c *check.C) {
        s.testVhostRedirectTokenToCookie(c, "POST",
                arvadostest.FooCollection+".example.com/foo",
                "",
-               "application/x-www-form-urlencoded",
+               http.Header{"Content-Type": {"application/x-www-form-urlencoded"}},
                url.Values{"api_token": {arvadostest.ActiveToken}}.Encode(),
                http.StatusOK,
                "foo",
@@ -547,7 +604,7 @@ func (s *IntegrationSuite) TestVhostRedirectPOSTFormTokenToCookie404(c *check.C)
        s.testVhostRedirectTokenToCookie(c, "POST",
                arvadostest.FooCollection+".example.com/foo",
                "",
-               "application/x-www-form-urlencoded",
+               http.Header{"Content-Type": {"application/x-www-form-urlencoded"}},
                url.Values{"api_token": {arvadostest.SpectatorToken}}.Encode(),
                http.StatusNotFound,
                notFoundMessage+"\n",
@@ -559,7 +616,7 @@ func (s *IntegrationSuite) TestAnonymousTokenOK(c *check.C) {
        s.testVhostRedirectTokenToCookie(c, "GET",
                "example.com/c="+arvadostest.HelloWorldCollection+"/Hello%20world.txt",
                "",
-               "",
+               nil,
                "",
                http.StatusOK,
                "Hello world\n",
@@ -571,7 +628,7 @@ func (s *IntegrationSuite) TestAnonymousTokenError(c *check.C) {
        s.testVhostRedirectTokenToCookie(c, "GET",
                "example.com/c="+arvadostest.HelloWorldCollection+"/Hello%20world.txt",
                "",
-               "",
+               nil,
                "",
                http.StatusNotFound,
                notFoundMessage+"\n",
@@ -711,14 +768,18 @@ func (s *IntegrationSuite) TestXHRNoRedirect(c *check.C) {
        c.Check(resp.Header().Get("Access-Control-Allow-Origin"), check.Equals, "*")
 }
 
-func (s *IntegrationSuite) testVhostRedirectTokenToCookie(c *check.C, method, hostPath, queryString, contentType, reqBody string, expectStatus int, expectRespBody string) *httptest.ResponseRecorder {
+func (s *IntegrationSuite) testVhostRedirectTokenToCookie(c *check.C, method, hostPath, queryString string, reqHeader http.Header, reqBody string, expectStatus int, expectRespBody string) *httptest.ResponseRecorder {
+       if reqHeader == nil {
+               reqHeader = http.Header{}
+       }
        u, _ := url.Parse(`http://` + hostPath + queryString)
+       c.Logf("requesting %s", u)
        req := &http.Request{
                Method:     method,
                Host:       u.Host,
                URL:        u,
                RequestURI: u.RequestURI(),
-               Header:     http.Header{"Content-Type": {contentType}},
+               Header:     reqHeader,
                Body:       ioutil.NopCloser(strings.NewReader(reqBody)),
        }
 
@@ -733,15 +794,18 @@ func (s *IntegrationSuite) testVhostRedirectTokenToCookie(c *check.C, method, ho
                return resp
        }
        c.Check(resp.Body.String(), check.Matches, `.*href="http://`+regexp.QuoteMeta(html.EscapeString(hostPath))+`(\?[^"]*)?".*`)
+       c.Check(strings.Split(resp.Header().Get("Location"), "?")[0], check.Equals, "http://"+hostPath)
        cookies := (&http.Response{Header: resp.Header()}).Cookies()
 
-       u, _ = u.Parse(resp.Header().Get("Location"))
+       u, err := u.Parse(resp.Header().Get("Location"))
+       c.Assert(err, check.IsNil)
+       c.Logf("following redirect to %s", u)
        req = &http.Request{
                Method:     "GET",
                Host:       u.Host,
                URL:        u,
                RequestURI: u.RequestURI(),
-               Header:     http.Header{},
+               Header:     reqHeader,
        }
        for _, c := range cookies {
                req.AddCookie(c)
@@ -749,7 +813,10 @@ func (s *IntegrationSuite) testVhostRedirectTokenToCookie(c *check.C, method, ho
 
        resp = httptest.NewRecorder()
        s.handler.ServeHTTP(resp, req)
-       c.Check(resp.Header().Get("Location"), check.Equals, "")
+
+       if resp.Code != http.StatusSeeOther {
+               c.Check(resp.Header().Get("Location"), check.Equals, "")
+       }
        return resp
 }
 
index f2c88c83cfa632ef41ce9afe23ffad63a25331d2..d631c89a816ffb783246c87655eb8ab03748d260 100644 (file)
@@ -12,7 +12,7 @@ nginx:
       ### STREAMS
       http:
         upstream webshell_upstream:
-          - server: 'localhost:4200 fail_timeout=10s'
+          - server: 'shell.__CLUSTER__.__DOMAIN__:4200 fail_timeout=10s'
 
   ### SITES
   servers: