16552: Merge branch 'main'
authorTom Clegg <tom@curii.com>
Thu, 7 Jul 2022 13:42:02 +0000 (09:42 -0400)
committerTom Clegg <tom@curii.com>
Thu, 7 Jul 2022 13:42:02 +0000 (09:42 -0400)
Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom@curii.com>

16 files changed:
cmd/arvados-package/fpm.go
cmd/arvados-package/install.go
cmd/arvados-server/cmd.go
lib/boot/cert.go
lib/boot/cmd.go
lib/boot/nginx.go
lib/boot/service.go
lib/boot/supervisor.go
lib/config/config.default.yml
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

index d81abab583cc78a3ddb5b0421ee38963d8187f83..9a20ec76fc5c8058423403ed9e6af6d9c7517b62 100644 (file)
@@ -97,10 +97,15 @@ 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",
+               "--deb-systemd", "/etc/systemd/system/multi-user.target.wants/arvados.service",
+               "--deb-systemd-enable",
+               "--no-deb-systemd-auto-start",
+               "--no-deb-systemd-restart-after-upgrade",
                "/lib/systemd/system/arvados.service",
                "/usr/bin/arvados-client",
                "/usr/bin/arvados-server",
+               "/usr/bin/arv",
+               "/usr/bin/arv-tag",
                "/var/lib/arvados",
                "/var/www/.gem",
                "/var/www/.passenger",
index d8dbdcc4a066c18bb8df7fdcba31eb3cd8a8d45e..9273ac9c73e4850f18d580899dfeae72b2719bd9 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=acme",
                        "--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 916f9f53b2af7b9109474c3e662b395174f982b9..10fd0aa9f6bc90e0b014145e25bb68d1d496fe9a 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,180 @@ func (createCertificates) String() string {
 }
 
 func (createCertificates) Run(ctx context.Context, fail func(error), super *Supervisor) error {
+       if super.cluster.TLS.Automatic {
+               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 super.cluster.TLS.Staging {
+               mgr.Client = &acme.Client{DirectoryURL: stagingDirectoryURL}
+       }
+       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 +228,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 +243,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..8a29823a12a411298aadc15009c7ae75b2ac08fe 100644 (file)
@@ -14,6 +14,7 @@ import (
        "os/exec"
        "path/filepath"
        "regexp"
+       "strings"
 
        "git.arvados.org/arvados.git/sdk/go/arvados"
 )
@@ -32,7 +33,8 @@ func (runNginx) Run(ctx context.Context, fail func(error), super *Supervisor) er
                return err
        }
        vars := map[string]string{
-               "LISTENHOST":       super.ListenHost,
+               "LISTENHOST":       "0.0.0.0",
+               "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 +44,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 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 472a22c6b2cb11a3566d882e6420f52400ca4b13..29d9d9cc41df6db1b857450d59164f93078aa47f 100644 (file)
@@ -900,10 +900,23 @@ Clusters:
       Repositories: /var/lib/arvados/git/repositories
 
     TLS:
+      # Use "file:///var/lib/acme/live/example.com/cert" and ".../key"
+      # to load externally managed certificates.
       Certificate: ""
       Key: ""
+
+      # Accept invalid certificates when connecting to servers. Never
+      # use this in production.
       Insecure: false
 
+      # Agree to Let's Encrypt terms of service and obtain
+      # certificates automatically for ExternalURL domains.
+      Automatic: false
+
+      # Use Let's Encrypt staging environment instead of production
+      # environment.
+      Staging: false
+
     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 a7c32c5052b1970a4d57999faede6ac71dc89de9..017771b637bb9897aab14ce7a234098c77d48be5 100644 (file)
@@ -40,7 +40,7 @@ const (
        gradleversion           = "5.3.1"
        nodejsversion           = "v12.22.11"
        devtestDatabasePassword = "insecure_arvados_test"
-       workbench2version       = "5e020488f67b5bc919796e0dc8b0b9f3b3ff23b0"
+       workbench2version       = "2454ac35292a79594c32a80430740317ed5005cf"
 )
 
 //go:embed arvados.service
@@ -563,7 +563,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)
@@ -579,19 +578,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
@@ -677,6 +663,14 @@ 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
@@ -693,6 +687,19 @@ rsync -a --delete-after build/ /var/lib/arvados/workbench2/
                if err != nil {
                        return 1
                }
+
+               // Symlink user-facing programs /usr/bin/x ->
+               // /var/lib/arvados/bin/x
+               for _, prog := range []string{"arvados-client", "arvados-server", "arv", "arv-tag"} {
+                       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
+                       }
+               }
        }
 
        return 0
index d2fed1dd7ad4fee9fac408da3e2070c913174b19..8c565bb5d6468512bf2de8d45adbfbfb675303e9 100644 (file)
@@ -18,6 +18,7 @@ import (
        "os/user"
        "regexp"
        "strconv"
+       "strings"
        "text/template"
 
        "git.arvados.org/arvados.git/lib/cmd"
@@ -34,7 +35,15 @@ type initCommand struct {
        Domain             string
        PostgreSQLPassword string
        Login              string
-       Insecure           bool
+       TLS                string
+       AdminEmail         string
+       Start              bool
+
+       LoginPAM                bool
+       LoginTest               bool
+       LoginGoogle             bool
+       LoginGoogleClientID     string
+       LoginGoogleClientSecret string
 }
 
 func (initcmd *initCommand) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
@@ -61,8 +70,10 @@ func (initcmd *initCommand) RunCommand(prog string, args []string, stdin io.Read
        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.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, auto, insecure, or none")
+       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 +83,31 @@ 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")
+               return 1
+       }
+
+       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
+       }
+
        wwwuser, err := user.Lookup("www-data")
        if err != nil {
                err = fmt.Errorf("user.Lookup(%q): %w", "www-data", err)
@@ -90,15 +126,19 @@ func (initcmd *initCommand) RunCommand(prog string, args []string, stdin io.Read
        }
        fmt.Fprintln(stderr, "created /var/lib/arvados/keep")
 
-       err = os.Mkdir("/etc/arvados", 0750)
+       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, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)
+       if err != nil {
+               err = fmt.Errorf("open %s: %w", conffile, err)
                return 1
        }
        tmpl, err := template.New("config").Parse(`Clusters:
@@ -113,8 +153,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 +195,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 +206,22 @@ func (initcmd *initCommand) RunCommand(prog string, args []string, stdin io.Read
     ManagementToken: {{printf "%q" ( .RandomHex 50 )}}
     PostgreSQL:
       Connection:
-        dbname: arvados_production
+        dbname: arvados
         host: localhost
         user: arvados
         password: {{printf "%q" .PostgreSQLPassword}}
     SystemRootToken: {{printf "%q" ( .RandomHex 50 )}}
-    {{if .Insecure}}
     TLS:
+      {{if eq .TLS "insecure"}}
       Insecure: true
-    {{end}}
+      {{else if eq .TLS "auto"}}
+      Automatic: true
+      {{else if eq .TLS "acme"}}
+      Certificate: {{printf "%q" (print "/var/lib/acme/live/" .Domain "/cert")}}
+      Key: {{printf "%q" (print "/var/lib/acme/live/" .Domain "/privkey")}}
+      {{else}}
+      {}
+      {{end}}
     Volumes:
       {{.ClusterID}}-nyw5e-000000000000000:
         Driver: Directory
@@ -183,47 +230,48 @@ 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, 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, err)
                return 1
        }
-       fmt.Fprintln(stderr, "created /etc/arvados/config.yml")
+       fmt.Fprintln(stderr, "created", conffile)
 
        ldr := config.NewLoader(nil, logger)
        ldr.SkipLegacy = true
        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("")
@@ -242,11 +290,24 @@ func (initcmd *initCommand) RunCommand(prog string, args []string, stdin io.Read
        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")
 
+       if initcmd.Start {
+               fmt.Fprintln(stderr, "starting systemd service")
+               cmd := exec.CommandContext(ctx, "systemctl", "start", "--no-block", "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
+               }
+       }
+
        return 0
 }
 
@@ -281,6 +342,7 @@ func (initcmd *initCommand) createDB(ctx context.Context, dbconn arvados.Postgre
                `CREATE EXTENSION IF NOT EXISTS pg_trgm`,
        } {
                cmd := exec.CommandContext(ctx, "sudo", "-u", "postgres", "psql", "-c", sql)
+               cmd.Dir = "/"
                cmd.Stdout = stderr
                cmd.Stderr = stderr
                err := cmd.Run()
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..234ee5787855c53929af5885590fb1abdeaece80 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.Automatic {
+               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..d9aa92b65d0fbfe1c97da212f5b1661e92e381be 100644 (file)
@@ -227,6 +227,8 @@ type Cluster struct {
                Certificate string
                Key         string
                Insecure    bool
+               Automatic   bool
+               Staging     bool
        }
        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"))