"--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",
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",
} else {
cmd.Args = append(cmd.Args,
"--env=domain=localhost",
+ "--env=initargs=-tls=insecure",
"--env=bootargs=-shutdown")
}
cmd.Args = append(cmd.Args,
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
"io"
"net/http"
"os"
+ "path"
+ "path/filepath"
+ "strings"
"git.arvados.org/arvados.git/lib/boot"
"git.arvados.org/arvados.git/lib/cloud/cloudtest"
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)
})
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).
# 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
+# arvados-server init -cluster-id x9999 -domain x9999.example.com -tls acme -admin-email example@gmail.com.example
</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/token?api_token=zzzzzzzzzzzzzzzzzzzzzz@). This will log you in to your admin account.
h2. Enable login
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 {
}
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
}
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 {
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
}
"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{}
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")
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()
}
"os/exec"
"path/filepath"
"regexp"
+ "strings"
"git.arvados.org/arvados.git/sdk/go/arvados"
)
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"),
}
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"
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", "4")
if err != nil {
return err
}
if err != nil {
return err
}
+ super.wait(ctx, createCertificates{})
super.wait(ctx, runner.depends...)
for u := range runner.svc.InternalURLs {
u := u
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
return err
}
+ super.wait(ctx, createCertificates{})
super.wait(ctx, runner.depends...)
for u := range runner.svc.InternalURLs {
u := u
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()
}
}()
}
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"
}
}
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)
select {
case <-ticker.C:
case <-super.ctx.Done():
+ super.Stop()
return false
}
if super.healthChecker == nil {
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
gradleversion = "5.3.1"
nodejsversion = "v12.22.11"
devtestDatabasePassword = "insecure_arvados_test"
- workbench2version = "5e020488f67b5bc919796e0dc8b0b9f3b3ff23b0"
+ workbench2version = "2454ac35292a79594c32a80430740317ed5005cf"
)
//go:embed arvados.service
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)
}
}
- // 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
{"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"},
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
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
"os/user"
"regexp"
"strconv"
+ "strings"
"text/template"
"git.arvados.org/arvados.git/lib/cmd"
Domain string
PostgreSQLPassword string
Login string
- Insecure bool
+ TLS string
+ AdminEmail string
+ Start bool
+
+ 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 {
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, 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 {
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
+ }
+
wwwuser, err := user.Lookup("www-data")
if err != nil {
err = fmt.Errorf("user.Lookup(%q): %w", "www-data", err)
}
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(confdir, 0, wwwgid)
+ if err != nil {
+ err = fmt.Errorf("chown 0:%d %s: %w", wwwgid, 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)
+ f, err := os.OpenFile(conffile, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)
if err != nil {
- err = fmt.Errorf("open /etc/arvados/config.yml: %w", err)
+ err = fmt.Errorf("open %s: %w", conffile, err)
return 1
}
tmpl, err := template.New("config").Parse(`Clusters:
"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/": {}
"http://0.0.0.0:9011/": {}
Collections:
BlobSigningKey: {{printf "%q" ( .RandomHex 50 )}}
- {{if .Insecure}}
+ {{if eq .TLS "insecure"}}
TrustAllContent: true
{{end}}
Containers:
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 "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
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("")
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
}
`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()
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
package service
import (
+ "context"
"crypto/tls"
"errors"
"fmt"
"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)
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 {
Certificate string
Key string
Insecure bool
+ ACME struct {
+ Server string
+ }
}
Users struct {
ActivatedUsersAreVisibleToOthers bool
uwsgi_temp_path "{{TMPDIR}}";
scgi_temp_path "{{TMPDIR}}";
upstream controller {
- server {{LISTENHOST}}:{{CONTROLLERPORT}};
+ server {{UPSTREAMHOST}}:{{CONTROLLERPORT}};
}
server {
listen {{LISTENHOST}}:{{CONTROLLERSSLPORT}} ssl;
}
}
upstream arv-git-http {
- server {{LISTENHOST}}:{{GITPORT}};
+ server {{UPSTREAMHOST}}:{{GITPORT}};
}
server {
listen {{LISTENHOST}}:{{GITSSLPORT}} ssl;
}
}
upstream keepproxy {
- server {{LISTENHOST}}:{{KEEPPROXYPORT}};
+ server {{UPSTREAMHOST}}:{{KEEPPROXYPORT}};
}
server {
listen {{LISTENHOST}}:{{KEEPPROXYSSLPORT}} ssl;
}
}
upstream keep-web {
- server {{LISTENHOST}}:{{KEEPWEBPORT}};
+ server {{UPSTREAMHOST}}:{{KEEPWEBPORT}};
}
server {
listen {{LISTENHOST}}:{{KEEPWEBSSLPORT}} ssl;
}
}
upstream health {
- server {{LISTENHOST}}:{{HEALTHPORT}};
+ server {{UPSTREAMHOST}}:{{HEALTHPORT}};
}
server {
listen {{LISTENHOST}}:{{HEALTHSSLPORT}} ssl;
}
}
upstream ws {
- server {{LISTENHOST}}:{{WSPORT}};
+ server {{UPSTREAMHOST}}:{{WSPORT}};
}
server {
listen {{LISTENHOST}}:{{WSSSLPORT}} ssl;
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;
}
}
upstream workbench2 {
- server {{LISTENHOST}}:{{WORKBENCH2PORT}};
+ server {{UPSTREAMHOST}}:{{WORKBENCH2PORT}};
}
server {
listen {{LISTENHOST}}:{{WORKBENCH2SSLPORT}} ssl;
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"))