"--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=acme",
"--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)
})
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.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
}
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"
)
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"),
}
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"
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 ".../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
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
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
}
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, 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 {
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)
}
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:
"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 "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
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.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)
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
+ Automatic bool
+ Staging bool
}
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"))