Merge branch '17165-vscode-training-doc' refs #17165
authorPeter Amstutz <peter.amstutz@curii.com>
Wed, 10 Feb 2021 20:23:23 +0000 (15:23 -0500)
committerPeter Amstutz <peter.amstutz@curii.com>
Wed, 10 Feb 2021 20:23:23 +0000 (15:23 -0500)
Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz@curii.com>

77 files changed:
build/run-tests.sh
cmd/arvados-client/cmd.go
cmd/arvados-client/container_gateway.go [new file with mode: 0644]
cmd/arvados-client/container_gateway_test.go [new file with mode: 0644]
cmd/arvados-package/build.go [new file with mode: 0644]
cmd/arvados-package/build_test.go [new file with mode: 0644]
cmd/arvados-package/cmd.go [new file with mode: 0644]
cmd/arvados-package/fpm.go [new file with mode: 0644]
cmd/arvados-package/install.go [new file with mode: 0644]
cmd/arvados-server/cmd.go
doc/Rakefile
doc/admin/federation.html.textile.liquid
doc/admin/upgrading.html.textile.liquid
doc/admin/user-management-cli.html.textile.liquid
doc/admin/user-management.html.textile.liquid
doc/api/methods/containers.html.textile.liquid
doc/api/methods/users.html.textile.liquid
doc/api/tokens.html.textile.liquid
doc/architecture/federation.html.textile.liquid
doc/install/automatic.html.textile.liquid [new file with mode: 0644]
doc/install/install-api-server.html.textile.liquid
doc/install/install-manual-prerequisites.html.textile.liquid
go.mod
go.sum
lib/boot/cert.go
lib/boot/cmd.go
lib/boot/nginx.go
lib/boot/passenger.go
lib/boot/postgresql.go
lib/boot/seed.go
lib/boot/service.go
lib/boot/supervisor.go
lib/config/config.default.yml
lib/config/export.go
lib/config/generated_config.go
lib/controller/federation/conn.go
lib/controller/handler.go
lib/controller/localdb/container_gateway.go [new file with mode: 0644]
lib/controller/localdb/container_gateway_test.go [new file with mode: 0644]
lib/controller/localdb/login.go
lib/controller/localdb/login_oidc.go
lib/controller/localdb/login_oidc_test.go
lib/controller/router/router.go
lib/controller/rpc/conn.go
lib/costanalyzer/costanalyzer.go
lib/costanalyzer/costanalyzer_test.go
lib/crunchrun/container_gateway.go [new file with mode: 0644]
lib/crunchrun/crunchrun.go
lib/dispatchcloud/dispatcher.go
lib/dispatchcloud/node_size.go
lib/dispatchcloud/node_size_test.go
lib/dispatchcloud/worker/pool.go
lib/dispatchcloud/worker/runner.go
lib/install/deps.go
lib/install/init.go [new file with mode: 0644]
lib/selfsigned/cert.go [new file with mode: 0644]
lib/selfsigned/cert_test.go [new file with mode: 0644]
sdk/go/arvados/api.go
sdk/go/arvados/client.go
sdk/go/arvados/config.go
sdk/go/arvados/container.go
sdk/go/arvados/container_gateway.go [new file with mode: 0644]
sdk/go/arvadostest/api.go
sdk/go/health/aggregator.go
sdk/python/tests/nginx.conf
sdk/python/tests/run_test_server.py
services/api/app/models/arvados_model.rb
services/api/app/models/container.rb
services/api/db/migrate/20210108033940_add_gateway_address_to_containers.rb [new file with mode: 0644]
services/api/db/migrate/20210126183521_add_interactive_session_started_to_containers.rb [new file with mode: 0644]
services/api/db/structure.sql
services/api/test/unit/container_test.rb
services/keep-balance/main.go
services/keepstore/volume.go
tools/compute-images/arvados-images-aws.json
tools/compute-images/arvados-images-azure.json
tools/compute-images/build.sh

index 4067a37cff5bb10c9b95665700f5409d00b39fca..d6dc43416a5bc8bf8e3aab56c0a5df78c2e759e5 100755 (executable)
@@ -550,7 +550,7 @@ setup_ruby_environment() {
         (
             export HOME=$GEMHOME
             bundlers="$(gem list --details bundler)"
-            versions=(1.11.0 1.17.3 2.0.2)
+            versions=(1.16.6 1.17.3 2.0.2)
             for v in ${versions[@]}; do
                 if ! echo "$bundlers" | fgrep -q "($v)"; then
                     gem install --user $(for v in ${versions[@]}; do echo bundler:${v}; done)
index 47fcd5ad7dc88275c5c9ce47369f3432ac861632..aefcce79a45a4eb65fbbed209801d684da6b4923 100644 (file)
@@ -57,6 +57,8 @@ var (
                "mount":                mount.Command,
                "deduplication-report": deduplicationreport.Command,
                "costanalyzer":         costanalyzer.Command,
+               "shell":                shellCommand{},
+               "connect-ssh":          connectSSHCommand{},
        })
 )
 
diff --git a/cmd/arvados-client/container_gateway.go b/cmd/arvados-client/container_gateway.go
new file mode 100644 (file)
index 0000000..e3b6b93
--- /dev/null
@@ -0,0 +1,208 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package main
+
+import (
+       "bytes"
+       "context"
+       "flag"
+       "fmt"
+       "io"
+       "net/url"
+       "os"
+       "os/exec"
+       "path/filepath"
+       "strings"
+       "syscall"
+
+       "git.arvados.org/arvados.git/lib/controller/rpc"
+       "git.arvados.org/arvados.git/sdk/go/arvados"
+)
+
+// shellCommand connects the terminal to an interactive shell on a
+// running container.
+type shellCommand struct{}
+
+func (shellCommand) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
+       f := flag.NewFlagSet(prog, flag.ContinueOnError)
+       f.SetOutput(stderr)
+       f.Usage = func() {
+               _, prog := filepath.Split(prog)
+               fmt.Fprint(stderr, prog+`: open an interactive shell on a running container.
+
+Usage: `+prog+` [options] [username@]container-uuid [ssh-options] [remote-command [args...]]
+
+Options:
+`)
+               f.PrintDefaults()
+       }
+       detachKeys := f.String("detach-keys", "ctrl-],ctrl-]", "set detach key sequence, as in docker-attach(1)")
+       err := f.Parse(args)
+       if err != nil {
+               fmt.Fprintln(stderr, err)
+               return 2
+       }
+
+       if f.NArg() < 1 {
+               f.Usage()
+               return 2
+       }
+       target := f.Args()[0]
+       if !strings.Contains(target, "@") {
+               target = "root@" + target
+       }
+       sshargs := f.Args()[1:]
+
+       // Try setting up a tunnel, and exit right away if it
+       // fails. This tunnel won't get used -- we'll set up a new
+       // tunnel when running as SSH client's ProxyCommand child --
+       // but in most cases where the real tunnel setup would fail,
+       // we catch the problem earlier here. This makes it less
+       // likely that an error message about tunnel setup will get
+       // hidden behind noisy errors from SSH client like this:
+       //
+       // [useful tunnel setup error message here]
+       // kex_exchange_identification: Connection closed by remote host
+       // Connection closed by UNKNOWN port 65535
+       // exit status 255
+       exitcode := connectSSHCommand{}.RunCommand(
+               "arvados-client connect-ssh",
+               []string{"-detach-keys=" + *detachKeys, "-probe-only=true", target},
+               &bytes.Buffer{}, &bytes.Buffer{}, stderr)
+       if exitcode != 0 {
+               return exitcode
+       }
+
+       selfbin, err := os.Readlink("/proc/self/exe")
+       if err != nil {
+               fmt.Fprintln(stderr, err)
+               return 2
+       }
+       sshargs = append([]string{
+               "ssh",
+               "-o", "ProxyCommand " + selfbin + " connect-ssh -detach-keys=" + shellescape(*detachKeys) + " " + shellescape(target),
+               "-o", "StrictHostKeyChecking no",
+               target},
+               sshargs...)
+       sshbin, err := exec.LookPath("ssh")
+       if err != nil {
+               fmt.Fprintln(stderr, err)
+               return 1
+       }
+       err = syscall.Exec(sshbin, sshargs, os.Environ())
+       fmt.Fprintf(stderr, "exec(%q) failed: %s\n", sshbin, err)
+       return 1
+}
+
+// connectSSHCommand connects stdin/stdout to a container's gateway
+// server (see lib/crunchrun/ssh.go).
+//
+// It is intended to be invoked with OpenSSH client's ProxyCommand
+// config.
+type connectSSHCommand struct{}
+
+func (connectSSHCommand) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
+       f := flag.NewFlagSet(prog, flag.ContinueOnError)
+       f.SetOutput(stderr)
+       f.Usage = func() {
+               _, prog := filepath.Split(prog)
+               fmt.Fprint(stderr, prog+`: connect to the gateway service for a running container.
+
+NOTE: You almost certainly don't want to use this command directly. It
+is meant to be used internally. Use "arvados-client shell" instead.
+
+Usage: `+prog+` [options] [username@]container-uuid
+
+Options:
+`)
+               f.PrintDefaults()
+       }
+       probeOnly := f.Bool("probe-only", false, "do not transfer IO, just exit 0 immediately if tunnel setup succeeds")
+       detachKeys := f.String("detach-keys", "", "set detach key sequence, as in docker-attach(1)")
+       if err := f.Parse(args); err != nil {
+               fmt.Fprintln(stderr, err)
+               return 2
+       } else if f.NArg() != 1 {
+               f.Usage()
+               return 2
+       }
+       targetUUID := f.Args()[0]
+       loginUsername := "root"
+       if i := strings.Index(targetUUID, "@"); i >= 0 {
+               loginUsername = targetUUID[:i]
+               targetUUID = targetUUID[i+1:]
+       }
+       if os.Getenv("ARVADOS_API_HOST") == "" || os.Getenv("ARVADOS_API_TOKEN") == "" {
+               fmt.Fprintln(stderr, "fatal: ARVADOS_API_HOST and ARVADOS_API_TOKEN environment variables are not set")
+               return 1
+       }
+       insecure := os.Getenv("ARVADOS_API_HOST_INSECURE")
+       rpcconn := rpc.NewConn("",
+               &url.URL{
+                       Scheme: "https",
+                       Host:   os.Getenv("ARVADOS_API_HOST"),
+               },
+               insecure == "1" || insecure == "yes" || insecure == "true",
+               func(context.Context) ([]string, error) {
+                       return []string{os.Getenv("ARVADOS_API_TOKEN")}, nil
+               })
+       if strings.Contains(targetUUID, "-xvhdp-") {
+               crs, err := rpcconn.ContainerRequestList(context.TODO(), arvados.ListOptions{Limit: -1, Filters: []arvados.Filter{{"uuid", "=", targetUUID}}})
+               if err != nil {
+                       fmt.Fprintln(stderr, err)
+                       return 1
+               }
+               if len(crs.Items) < 1 {
+                       fmt.Fprintf(stderr, "container request %q not found\n", targetUUID)
+                       return 1
+               }
+               cr := crs.Items[0]
+               if cr.ContainerUUID == "" {
+                       fmt.Fprintf(stderr, "no container assigned, container request state is %s\n", strings.ToLower(string(cr.State)))
+                       return 1
+               }
+               targetUUID = cr.ContainerUUID
+               fmt.Fprintln(stderr, "connecting to container", targetUUID)
+       } else if !strings.Contains(targetUUID, "-dz642-") {
+               fmt.Fprintf(stderr, "target UUID is not a container or container request UUID: %s\n", targetUUID)
+               return 1
+       }
+       sshconn, err := rpcconn.ContainerSSH(context.TODO(), arvados.ContainerSSHOptions{
+               UUID:          targetUUID,
+               DetachKeys:    *detachKeys,
+               LoginUsername: loginUsername,
+       })
+       if err != nil {
+               fmt.Fprintln(stderr, "error setting up tunnel:", err)
+               return 1
+       }
+       defer sshconn.Conn.Close()
+
+       if *probeOnly {
+               return 0
+       }
+
+       ctx, cancel := context.WithCancel(context.Background())
+       go func() {
+               defer cancel()
+               _, err := io.Copy(stdout, sshconn.Conn)
+               if err != nil && ctx.Err() == nil {
+                       fmt.Fprintf(stderr, "receive: %v\n", err)
+               }
+       }()
+       go func() {
+               defer cancel()
+               _, err := io.Copy(sshconn.Conn, stdin)
+               if err != nil && ctx.Err() == nil {
+                       fmt.Fprintf(stderr, "send: %v\n", err)
+               }
+       }()
+       <-ctx.Done()
+       return 0
+}
+
+func shellescape(s string) string {
+       return "'" + strings.Replace(s, "'", "'\\''", -1) + "'"
+}
diff --git a/cmd/arvados-client/container_gateway_test.go b/cmd/arvados-client/container_gateway_test.go
new file mode 100644 (file)
index 0000000..97a615e
--- /dev/null
@@ -0,0 +1,82 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package main
+
+import (
+       "bytes"
+       "context"
+       "crypto/hmac"
+       "crypto/sha256"
+       "fmt"
+       "net/url"
+       "os"
+       "os/exec"
+
+       "git.arvados.org/arvados.git/lib/controller/rpc"
+       "git.arvados.org/arvados.git/lib/crunchrun"
+       "git.arvados.org/arvados.git/sdk/go/arvados"
+       "git.arvados.org/arvados.git/sdk/go/arvadostest"
+       check "gopkg.in/check.v1"
+)
+
+func (s *ClientSuite) TestShellGatewayNotAvailable(c *check.C) {
+       var stdout, stderr bytes.Buffer
+       cmd := exec.Command("go", "run", ".", "shell", arvadostest.QueuedContainerUUID, "-o", "controlpath=none", "echo", "ok")
+       cmd.Env = append(cmd.Env, os.Environ()...)
+       cmd.Env = append(cmd.Env, "ARVADOS_API_TOKEN="+arvadostest.ActiveTokenV2)
+       cmd.Stdout = &stdout
+       cmd.Stderr = &stderr
+       c.Check(cmd.Run(), check.NotNil)
+       c.Log(stderr.String())
+       c.Check(stderr.String(), check.Matches, `(?ms).*container is not running yet \(state is "Queued"\).*`)
+}
+
+func (s *ClientSuite) TestShellGateway(c *check.C) {
+       defer func() {
+               c.Check(arvados.NewClientFromEnv().RequestAndDecode(nil, "POST", "database/reset", nil, nil), check.IsNil)
+       }()
+       uuid := arvadostest.QueuedContainerUUID
+       h := hmac.New(sha256.New, []byte(arvadostest.SystemRootToken))
+       fmt.Fprint(h, uuid)
+       authSecret := fmt.Sprintf("%x", h.Sum(nil))
+       dcid := "theperthcountyconspiracy"
+       gw := crunchrun.Gateway{
+               DockerContainerID: &dcid,
+               ContainerUUID:     uuid,
+               Address:           "0.0.0.0:0",
+               AuthSecret:        authSecret,
+       }
+       err := gw.Start()
+       c.Assert(err, check.IsNil)
+
+       rpcconn := rpc.NewConn("",
+               &url.URL{
+                       Scheme: "https",
+                       Host:   os.Getenv("ARVADOS_API_HOST"),
+               },
+               true,
+               func(context.Context) ([]string, error) {
+                       return []string{arvadostest.SystemRootToken}, nil
+               })
+       _, err = rpcconn.ContainerUpdate(context.TODO(), arvados.UpdateOptions{UUID: uuid, Attrs: map[string]interface{}{
+               "state": arvados.ContainerStateLocked,
+       }})
+       c.Assert(err, check.IsNil)
+       _, err = rpcconn.ContainerUpdate(context.TODO(), arvados.UpdateOptions{UUID: uuid, Attrs: map[string]interface{}{
+               "state":           arvados.ContainerStateRunning,
+               "gateway_address": gw.Address,
+       }})
+       c.Assert(err, check.IsNil)
+
+       var stdout, stderr bytes.Buffer
+       cmd := exec.Command("go", "run", ".", "shell", uuid, "-o", "controlpath=none", "-o", "userknownhostsfile="+c.MkDir()+"/known_hosts", "echo", "ok")
+       cmd.Env = append(cmd.Env, os.Environ()...)
+       cmd.Env = append(cmd.Env, "ARVADOS_API_TOKEN="+arvadostest.ActiveTokenV2)
+       cmd.Stdout = &stdout
+       cmd.Stderr = &stderr
+       c.Check(cmd.Run(), check.NotNil)
+       c.Log(stderr.String())
+       c.Check(stderr.String(), check.Matches, `(?ms).*(No such container: theperthcountyconspiracy|exec: \"docker\": executable file not found in \$PATH).*`)
+}
diff --git a/cmd/arvados-package/build.go b/cmd/arvados-package/build.go
new file mode 100644 (file)
index 0000000..1437f4b
--- /dev/null
@@ -0,0 +1,155 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package main
+
+import (
+       "bytes"
+       "context"
+       "fmt"
+       "io"
+       "io/ioutil"
+       "os"
+       "os/exec"
+       "os/user"
+       "path/filepath"
+       "strings"
+
+       "git.arvados.org/arvados.git/sdk/go/ctxlog"
+       "github.com/docker/docker/api/types"
+       "github.com/docker/docker/client"
+)
+
+func build(ctx context.Context, opts opts, stdin io.Reader, stdout, stderr io.Writer) error {
+       if opts.PackageVersion == "" {
+               var buf bytes.Buffer
+               cmd := exec.CommandContext(ctx, "git", "describe", "--tag", "--dirty")
+               cmd.Stdout = &buf
+               cmd.Stderr = stderr
+               cmd.Dir = opts.SourceDir
+               err := cmd.Run()
+               if err != nil {
+                       return fmt.Errorf("git describe: %w", err)
+               }
+               opts.PackageVersion = strings.TrimSpace(buf.String())
+               ctxlog.FromContext(ctx).Infof("version not specified; using %s", opts.PackageVersion)
+       }
+
+       if opts.PackageChown == "" {
+               whoami, err := user.Current()
+               if err != nil {
+                       return fmt.Errorf("user.Current: %w", err)
+               }
+               opts.PackageChown = whoami.Uid + ":" + whoami.Gid
+       }
+
+       // Build in a tempdir, then move to the desired destination
+       // dir. Otherwise, errors might cause us to leave a mess:
+       // truncated files, files owned by root, etc.
+       _, prog := filepath.Split(os.Args[0])
+       tmpdir, err := ioutil.TempDir(opts.PackageDir, prog+".")
+       if err != nil {
+               return err
+       }
+       defer os.RemoveAll(tmpdir)
+
+       selfbin, err := os.Readlink("/proc/self/exe")
+       if err != nil {
+               return fmt.Errorf("readlink /proc/self/exe: %w", err)
+       }
+       buildImageName := "arvados-package-build-" + opts.TargetOS
+       packageFilename := "arvados-server-easy_" + opts.PackageVersion + "_amd64.deb"
+
+       if ok, err := dockerImageExists(ctx, buildImageName); err != nil {
+               return err
+       } else if !ok || opts.RebuildImage {
+               buildCtrName := strings.Replace(buildImageName, ":", "-", -1)
+               err = dockerRm(ctx, buildCtrName)
+               if err != nil {
+                       return err
+               }
+
+               defer dockerRm(ctx, buildCtrName)
+               cmd := exec.CommandContext(ctx, "docker", "run",
+                       "--name", buildCtrName,
+                       "--tmpfs", "/tmp:exec,mode=01777",
+                       "-v", selfbin+":/arvados-package:ro",
+                       "-v", opts.SourceDir+":/arvados:ro",
+                       opts.TargetOS,
+                       "/arvados-package", "_install",
+                       "-eatmydata",
+                       "-type", "package",
+                       "-source", "/arvados",
+                       "-package-version", opts.PackageVersion,
+               )
+               cmd.Stdout = stdout
+               cmd.Stderr = stderr
+               err = cmd.Run()
+               if err != nil {
+                       return fmt.Errorf("docker run: %w", err)
+               }
+
+               cmd = exec.CommandContext(ctx, "docker", "commit", buildCtrName, buildImageName)
+               cmd.Stdout = stdout
+               cmd.Stderr = stderr
+               err = cmd.Run()
+               if err != nil {
+                       return fmt.Errorf("docker commit: %w", err)
+               }
+
+               ctxlog.FromContext(ctx).Infof("created docker image %s", buildImageName)
+       }
+
+       cmd := exec.CommandContext(ctx, "docker", "run",
+               "--rm",
+               "--tmpfs", "/tmp:exec,mode=01777",
+               "-v", tmpdir+":/pkg",
+               "-v", selfbin+":/arvados-package:ro",
+               "-v", opts.SourceDir+":/arvados:ro",
+               buildImageName,
+               "eatmydata", "/arvados-package", "_fpm",
+               "-source", "/arvados",
+               "-package-version", opts.PackageVersion,
+               "-package-dir", "/pkg",
+               "-package-chown", opts.PackageChown,
+               "-package-maintainer", opts.Maintainer,
+               "-package-vendor", opts.Vendor,
+       )
+       cmd.Stdout = stdout
+       cmd.Stderr = stderr
+       err = cmd.Run()
+       if err != nil {
+               return fmt.Errorf("docker run: %w", err)
+       }
+
+       err = os.Rename(tmpdir+"/"+packageFilename, opts.PackageDir+"/"+packageFilename)
+       if err != nil {
+               return err
+       }
+
+       return nil
+}
+
+func dockerRm(ctx context.Context, name string) error {
+       cli, err := client.NewEnvClient()
+       if err != nil {
+               return err
+       }
+       ctrs, err := cli.ContainerList(ctx, types.ContainerListOptions{All: true, Limit: -1})
+       if err != nil {
+               return err
+       }
+       for _, ctr := range ctrs {
+               for _, ctrname := range ctr.Names {
+                       if ctrname == "/"+name {
+                               err = cli.ContainerRemove(ctx, ctr.ID, types.ContainerRemoveOptions{})
+                               if err != nil {
+                                       return fmt.Errorf("error removing container %s: %w", ctr.ID, err)
+                               }
+                               break
+                       }
+               }
+       }
+       return nil
+}
diff --git a/cmd/arvados-package/build_test.go b/cmd/arvados-package/build_test.go
new file mode 100644 (file)
index 0000000..75e8f76
--- /dev/null
@@ -0,0 +1,68 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package main
+
+import (
+       "os"
+       "os/exec"
+       "testing"
+
+       "gopkg.in/check.v1"
+)
+
+var buildimage string
+
+func init() {
+       os.Args = append(os.Args, "-test.timeout=30m") // kludge
+}
+
+type BuildSuite struct{}
+
+var _ = check.Suite(&BuildSuite{})
+
+func Test(t *testing.T) { check.TestingT(t) }
+
+func (s *BuildSuite) TestBuildAndInstall(c *check.C) {
+       if testing.Short() {
+               c.Skip("skipping docker tests in short mode")
+       } else if _, err := exec.Command("docker", "info").CombinedOutput(); err != nil {
+               c.Skip("skipping docker tests because docker is not available")
+       }
+       tmpdir := c.MkDir()
+       defer os.RemoveAll(tmpdir)
+       err := os.Chmod(tmpdir, 0755)
+       c.Assert(err, check.IsNil)
+
+       cmd := exec.Command("go", "run", ".",
+               "build",
+               "-package-dir", tmpdir,
+               "-package-version", "1.2.3~rc4",
+               "-source", "../..",
+       )
+       cmd.Stdout = os.Stderr
+       cmd.Stderr = os.Stderr
+       err = cmd.Run()
+       c.Check(err, check.IsNil)
+
+       fi, err := os.Stat(tmpdir + "/arvados-server-easy_1.2.3~rc4_amd64.deb")
+       c.Assert(err, check.IsNil)
+       c.Logf("%#v", fi)
+
+       buf, _ := exec.Command("ls", "-l", tmpdir).CombinedOutput()
+       c.Logf("%s", buf)
+
+       cmd = exec.Command("go", "run", ".",
+               "testinstall",
+               "-package-dir", tmpdir,
+               "-package-version", "1.2.3~rc4",
+       )
+       cmd.Stdout = os.Stderr
+       cmd.Stderr = os.Stderr
+       err = cmd.Run()
+       c.Check(err, check.IsNil)
+
+       err = os.RemoveAll(tmpdir)
+       c.Check(err, check.IsNil)
+}
diff --git a/cmd/arvados-package/cmd.go b/cmd/arvados-package/cmd.go
new file mode 100644 (file)
index 0000000..54f0809
--- /dev/null
@@ -0,0 +1,143 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package main
+
+import (
+       "context"
+       "flag"
+       "fmt"
+       "io"
+       "os"
+       "path/filepath"
+       "strings"
+
+       "git.arvados.org/arvados.git/lib/cmd"
+       "git.arvados.org/arvados.git/lib/install"
+       "git.arvados.org/arvados.git/sdk/go/ctxlog"
+)
+
+var (
+       handler = cmd.Multi(map[string]cmd.Handler{
+               "version":   cmd.Version,
+               "-version":  cmd.Version,
+               "--version": cmd.Version,
+
+               "build":       cmdFunc(build),
+               "testinstall": cmdFunc(testinstall),
+               "_fpm":        cmdFunc(fpm),    // internal use
+               "_install":    install.Command, // internal use
+       })
+)
+
+func main() {
+       if len(os.Args) < 2 || strings.HasPrefix(os.Args[1], "-") {
+               parseFlags([]string{"-help"})
+               os.Exit(2)
+       }
+       os.Exit(handler.RunCommand(os.Args[0], os.Args[1:], os.Stdin, os.Stdout, os.Stderr))
+}
+
+type cmdFunc func(ctx context.Context, opts opts, stdin io.Reader, stdout, stderr io.Writer) error
+
+func (cf cmdFunc) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
+       logger := ctxlog.New(stderr, "text", "info")
+       ctx := ctxlog.Context(context.Background(), logger)
+       opts, err := parseFlags(args)
+       if err != nil {
+               logger.WithError(err).Error("error parsing command line flags")
+               return 1
+       }
+       err = cf(ctx, opts, stdin, stdout, stderr)
+       if err != nil {
+               logger.WithError(err).Error("failed")
+               return 1
+       }
+       return 0
+}
+
+type opts struct {
+       PackageVersion string
+       PackageDir     string
+       PackageChown   string
+       RebuildImage   bool
+       SourceDir      string
+       TargetOS       string
+       Maintainer     string
+       Vendor         string
+}
+
+func parseFlags(args []string) (opts, error) {
+       opts := opts{
+               SourceDir:  ".",
+               TargetOS:   "debian:10",
+               Maintainer: "Arvados Package Maintainers <packaging@arvados.org>",
+               Vendor:     "The Arvados Project",
+       }
+       flags := flag.NewFlagSet("", flag.ContinueOnError)
+       flags.StringVar(&opts.PackageVersion, "package-version", opts.PackageVersion, "package version to build/test, like \"1.2.3\"")
+       flags.StringVar(&opts.SourceDir, "source", opts.SourceDir, "arvados source tree location")
+       flags.StringVar(&opts.PackageDir, "package-dir", opts.PackageDir, "destination directory for new package (default is cwd)")
+       flags.StringVar(&opts.PackageChown, "package-chown", opts.PackageChown, "desired uid:gid for new package (default is current user:group)")
+       flags.StringVar(&opts.TargetOS, "target-os", opts.TargetOS, "target operating system vendor:version")
+       flags.StringVar(&opts.Maintainer, "package-maintainer", opts.Maintainer, "maintainer to be listed in package metadata")
+       flags.StringVar(&opts.Vendor, "package-vendor", opts.Vendor, "vendor to be listed in package metadata")
+       flags.BoolVar(&opts.RebuildImage, "rebuild-image", opts.RebuildImage, "rebuild docker image(s) instead of using existing")
+       flags.Usage = func() {
+               fmt.Fprint(flags.Output(), `Usage: arvados-package <subcommand> [options]
+
+Subcommands:
+       build
+               use a docker container to build a package from a checked
+               out version of the arvados source tree
+       testinstall
+               use a docker container to install a package and confirm
+               the resulting installation is functional
+       version
+               show program version
+
+Internally used subcommands:
+       _fpm
+               build a package
+       _install
+               equivalent to "arvados-server install"
+
+Automation/integration notes:
+       The first time a given machine runs "build" or "testinstall" (and
+       any time the -rebuild-image is used), new docker images are built,
+       which is quite slow. If you use on-demand VMs to run automated builds,
+       run "build" and "testinstall" once when setting up your initial VM
+       image, and be prepared to rebuild that VM image when package-building
+       slows down (this will happen when new dependencies are introduced).
+
+       The "build" subcommand, if successful, also runs
+       dpkg-scanpackages to create/replace Packages.gz in the package
+       dir. This enables the "testinstall" subcommand to list the
+       package dir as a source in /etc/apt/sources.*.
+
+Options:
+`)
+               flags.PrintDefaults()
+       }
+       err := flags.Parse(args)
+       if err != nil {
+               return opts, err
+       }
+       if len(flags.Args()) > 0 {
+               return opts, fmt.Errorf("unrecognized command line arguments: %v", flags.Args())
+       }
+       if opts.SourceDir == "" {
+               d, err := os.Getwd()
+               if err != nil {
+                       return opts, fmt.Errorf("Getwd: %w", err)
+               }
+               opts.SourceDir = d
+       }
+       opts.PackageDir = filepath.Clean(opts.PackageDir)
+       opts.SourceDir, err = filepath.Abs(opts.SourceDir)
+       if err != nil {
+               return opts, err
+       }
+       return opts, nil
+}
diff --git a/cmd/arvados-package/fpm.go b/cmd/arvados-package/fpm.go
new file mode 100644 (file)
index 0000000..ca63929
--- /dev/null
@@ -0,0 +1,127 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package main
+
+import (
+       "bytes"
+       "context"
+       "fmt"
+       "io"
+       "os"
+       "os/exec"
+       "path/filepath"
+
+       "git.arvados.org/arvados.git/lib/install"
+)
+
+func fpm(ctx context.Context, opts opts, stdin io.Reader, stdout, stderr io.Writer) error {
+       var chownUid, chownGid int
+       if opts.PackageChown != "" {
+               _, err := fmt.Sscanf(opts.PackageChown, "%d:%d", &chownUid, &chownGid)
+               if err != nil {
+                       return fmt.Errorf("invalid value %q for PackageChown: %w", opts.PackageChown, err)
+               }
+       }
+
+       exitcode := install.Command.RunCommand("arvados-server install", []string{
+               "-type", "package",
+               "-package-version", opts.PackageVersion,
+               "-source", opts.SourceDir,
+       }, stdin, stdout, stderr)
+       if exitcode != 0 {
+               return fmt.Errorf("arvados-server install failed: exit code %d", exitcode)
+       }
+
+       cmd := exec.Command("/var/lib/arvados/bin/gem", "install", "--user", "--no-document", "fpm")
+       cmd.Stdout = stdout
+       cmd.Stderr = stderr
+       err := cmd.Run()
+       if err != nil {
+               return fmt.Errorf("gem install fpm: %w", err)
+       }
+
+       cmd = exec.Command("/var/lib/arvados/bin/gem", "env", "gempath")
+       cmd.Stderr = stderr
+       buf, err := cmd.Output() // /root/.gem/ruby/2.7.0:...
+       if err != nil || len(buf) == 0 {
+               return fmt.Errorf("gem env gempath: %w", err)
+       }
+       gempath := string(bytes.TrimRight(bytes.Split(buf, []byte{':'})[0], "\n"))
+
+       if _, err := os.Stat(gempath + "/gems/fpm-1.11.0/lib/fpm/package/deb.rb"); err == nil {
+               // Workaround for fpm bug https://github.com/jordansissel/fpm/issues/1739
+               cmd = exec.Command("sed", "-i", `/require "digest"/a require "zlib"`, gempath+"/gems/fpm-1.11.0/lib/fpm/package/deb.rb")
+               cmd.Stdout = stdout
+               cmd.Stderr = stderr
+               err = cmd.Run()
+               if err != nil {
+                       return fmt.Errorf("monkeypatch fpm: %w", err)
+               }
+       }
+
+       // Remove unneeded files. This is much faster than "fpm
+       // --exclude X" because fpm copies everything into a staging
+       // area before looking at the --exclude args.
+       cmd = exec.Command("bash", "-c", "cd /var/www/.gem/ruby && rm -rf */cache */bundler/gems/*/.git */bundler/gems/arvados-*/[^s]* */bundler/gems/arvados-*/s[^d]* */bundler/gems/arvados-*/sdk/[^cr]* */gems/passenger-*/src/cxx* ruby/*/gems/*/ext /var/lib/arvados/go")
+       cmd.Stdout = stdout
+       cmd.Stderr = stderr
+       err = cmd.Run()
+       if err != nil {
+               return fmt.Errorf("rm -rf [...]: %w", err)
+       }
+
+       format := "deb" // TODO: rpm
+       pkgfile := filepath.Join(opts.PackageDir, "arvados-server-easy_"+opts.PackageVersion+"_amd64."+format)
+
+       cmd = exec.Command(gempath+"/bin/fpm",
+               "--package", pkgfile,
+               "--name", "arvados-server-easy",
+               "--version", opts.PackageVersion,
+               "--url", "https://arvados.org",
+               "--maintainer", opts.Maintainer,
+               "--vendor", opts.Vendor,
+               "--license", "GNU Affero General Public License, version 3.0",
+               "--description", "platform for managing, processing, and sharing genomic and other large scientific and biomedical data",
+               "--input-type", "dir",
+               "--output-type", format)
+       deps, err := install.ProductionDependencies()
+       if err != nil {
+               return err
+       }
+       for _, pkg := range deps {
+               cmd.Args = append(cmd.Args, "--depends", pkg)
+       }
+       cmd.Args = append(cmd.Args,
+               "--verbose",
+               "--deb-use-file-permissions",
+               "--rpm-use-file-permissions",
+               "/var/lib/arvados",
+               "/var/www/.gem",
+               "/var/www/.passenger",
+               "/var/www/.bundle",
+       )
+       fmt.Fprintf(stderr, "... %s\n", cmd.Args)
+       cmd.Dir = opts.PackageDir
+       cmd.Stdout = stdout
+       cmd.Stderr = stderr
+       err = cmd.Run()
+       if err != nil {
+               return fmt.Errorf("fpm: %w", err)
+       }
+
+       if opts.PackageChown != "" {
+               err = os.Chown(pkgfile, chownUid, chownGid)
+               if err != nil {
+                       return fmt.Errorf("chown %s: %w", pkgfile, err)
+               }
+       }
+
+       cmd = exec.Command("ls", "-l", pkgfile)
+       cmd.Stdout = stdout
+       cmd.Stderr = stderr
+       _ = cmd.Run()
+
+       return nil
+}
diff --git a/cmd/arvados-package/install.go b/cmd/arvados-package/install.go
new file mode 100644 (file)
index 0000000..85c64b8
--- /dev/null
@@ -0,0 +1,134 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package main
+
+import (
+       "context"
+       "fmt"
+       "io"
+       "io/ioutil"
+       "os"
+       "os/exec"
+       "path/filepath"
+       "strings"
+
+       "github.com/docker/docker/api/types"
+       "github.com/docker/docker/client"
+)
+
+func testinstall(ctx context.Context, opts opts, stdin io.Reader, stdout, stderr io.Writer) error {
+       depsImageName := "arvados-package-deps-" + opts.TargetOS
+       depsCtrName := strings.Replace(depsImageName, ":", "-", -1)
+
+       _, prog := filepath.Split(os.Args[0])
+       tmpdir, err := ioutil.TempDir("", prog+".")
+       if err != nil {
+               return fmt.Errorf("TempDir: %w", err)
+       }
+       defer os.RemoveAll(tmpdir)
+
+       if exists, err := dockerImageExists(ctx, depsImageName); err != nil {
+               return err
+       } else if !exists || opts.RebuildImage {
+               err = dockerRm(ctx, depsCtrName)
+               if err != nil {
+                       return err
+               }
+               defer dockerRm(ctx, depsCtrName)
+               cmd := exec.CommandContext(ctx, "docker", "run",
+                       "--name", depsCtrName,
+                       "--tmpfs", "/tmp:exec,mode=01777",
+                       "-v", opts.PackageDir+":/pkg:ro",
+                       "--env", "DEBIAN_FRONTEND=noninteractive",
+                       opts.TargetOS,
+                       "bash", "-c", `
+set -e -o pipefail
+apt-get update
+apt-get install -y --no-install-recommends dpkg-dev eatmydata
+
+mkdir /tmp/pkg
+ln -s /pkg/*.deb /tmp/pkg/
+(cd /tmp/pkg; dpkg-scanpackages --multiversion . | gzip > Packages.gz)
+echo >/etc/apt/sources.list.d/arvados-local.list "deb [trusted=yes] file:///tmp/pkg ./"
+apt-get update
+
+eatmydata apt-get install -y --no-install-recommends arvados-server-easy postgresql
+eatmydata apt-get remove -y dpkg-dev
+SUDO_FORCE_REMOVE=yes apt-get autoremove -y
+eatmydata apt-get remove -y arvados-server-easy
+rm /etc/apt/sources.list.d/arvados-local.list
+`)
+               cmd.Stdout = stdout
+               cmd.Stderr = stderr
+               err = cmd.Run()
+               if err != nil {
+                       return fmt.Errorf("docker run: %w", err)
+               }
+
+               cmd = exec.CommandContext(ctx, "docker", "commit", depsCtrName, depsImageName)
+               cmd.Stdout = stdout
+               cmd.Stderr = stderr
+               err = cmd.Run()
+               if err != nil {
+                       return fmt.Errorf("docker commit: %w", err)
+               }
+       }
+
+       versionsuffix := ""
+       if opts.PackageVersion != "" {
+               versionsuffix = "=" + opts.PackageVersion
+       }
+       cmd := exec.CommandContext(ctx, "docker", "run", "--rm",
+               "--tmpfs", "/tmp:exec,mode=01777",
+               "-v", opts.PackageDir+":/pkg:ro",
+               "--env", "DEBIAN_FRONTEND=noninteractive",
+               depsImageName,
+               "bash", "-c", `
+set -e -o pipefail
+PATH="/var/lib/arvados/bin:$PATH"
+apt-get update
+apt-get install -y --no-install-recommends dpkg-dev
+mkdir /tmp/pkg
+ln -s /pkg/*.deb /tmp/pkg/
+(cd /tmp/pkg; dpkg-scanpackages --multiversion . | gzip > Packages.gz)
+apt-get remove -y dpkg-dev
+echo
+
+echo >/etc/apt/sources.list.d/arvados-local.list "deb [trusted=yes] file:///tmp/pkg ./"
+apt-get update
+eatmydata apt-get install --reinstall -y --no-install-recommends arvados-server-easy`+versionsuffix+`
+SUDO_FORCE_REMOVE=yes apt-get autoremove -y
+
+/etc/init.d/postgresql start
+arvados-server init -cluster-id x1234
+exec arvados-server boot -listen-host 0.0.0.0 -shutdown
+`)
+       cmd.Stdout = stdout
+       cmd.Stderr = stderr
+       err = cmd.Run()
+       if err != nil {
+               return fmt.Errorf("docker run: %w", err)
+       }
+       return nil
+}
+
+func dockerImageExists(ctx context.Context, name string) (bool, error) {
+       cli, err := client.NewEnvClient()
+       if err != nil {
+               return false, err
+       }
+       imgs, err := cli.ImageList(ctx, types.ImageListOptions{All: true})
+       if err != nil {
+               return false, err
+       }
+       for _, img := range imgs {
+               for _, tag := range img.RepoTags {
+                       if tag == name {
+                               return true, nil
+                       }
+               }
+       }
+       return false, nil
+}
index ff99de75c41ad13f630d0902c2e695c6c17ad5c9..d0aa9da94df537bf80a3ed232c2a0ae2c3a0e1d6 100644 (file)
@@ -34,6 +34,7 @@ var (
                "crunch-run":         crunchrun.Command,
                "dispatch-cloud":     dispatchcloud.Command,
                "install":            install.Command,
+               "init":               install.InitCommand,
                "recover-collection": recovercollection.Command,
                "ws":                 ws.Command,
        })
index c13cea3c1abf9e720730218f9828c215ca6b3fa6..ee87062f7ec275716f454c298db96eb9239e0e0a 100644 (file)
@@ -20,6 +20,15 @@ end
 
 task :generate => [ :realclean, 'sdk/python/arvados/index.html', 'sdk/R/arvados/index.html', 'sdk/java-v2/javadoc/index.html' ] do
   vars = ['baseurl', 'arvados_cluster_uuid', 'arvados_api_host', 'arvados_workbench_host']
+  if ! ENV.key?('baseurl') || ENV['baseurl'] == ""
+    if !ENV.key?('WORKSPACE') || ENV['WORKSPACE'] == ""
+      puts "The `baseurl` variable was not specified and the `WORKSPACE` environment variable is not set. Defaulting `baseurl` to file://#{pwd}/.site"
+      ENV['baseurl'] = "file://#{pwd}/.site/"
+    else
+      puts "The `baseurl` variable was not specified, defaulting to a value derived from the `WORKSPACE` environment variable"
+      ENV['baseurl'] = "file://#{ENV['WORKSPACE']}/doc/.site/"
+    end
+  end
   vars.each do |v|
     if ENV[v]
       website.config.h[v] = ENV[v]
index eddd247e9a42dc963b0f67648ab30dfb25a01f30..74480e7dee5000c14815475ea74d5e65d301ac13 100644 (file)
@@ -36,7 +36,9 @@ Clusters:
 
 Similar settings should be added to @clsr2@ & @clsr3@ hosts, so that all clusters in the federation can talk to each other.
 
-The @ActivateUsers@ setting indicates whether users from a given cluster are automatically activated or they require manual activation.  User activation is covered in more detail in the "user activation section":{{site.baseurl}}/admin/activation.html.  In the current example, users from @clsr2@ would be automatically, activated, but users from @clsr3@ would require an admin to activate the account.
+The @ActivateUsers@ setting indicates whether users from a given cluster are automatically activated or they require manual activation.  User activation is covered in more detail in the "user activation section":{{site.baseurl}}/admin/user-management.html.  In the current example, users from @clsr2@ would be automatically activated but users from @clsr3@ would require an admin to activate the account.
+
+Note: The @Proxy:@ variable is intended for future use, and should always be set to @true@.
 
 h2(#LoginCluster). User management
 
index 8317a744a705e614051f16f6f3c908ff85329e31..e0324f6b86d7bbdcf4b4a7525f85f9735654c2ef 100644 (file)
@@ -39,6 +39,15 @@ h2(#main). development main (as of 2020-12-10)
 
 "Upgrading from 2.1.0":#v2_1_0
 
+h3. New proxy parameters for arvados-controller
+
+In your Nginx configuration file (@/etc/nginx/conf.d/arvados-api-and-controller.conf@), add the following lines to the @location /@ block with @http://controller@ (see "Update nginx configuration":{{site.baseurl}}/install/install-api-server.html#update-nginx for an example) and reload/restart Nginx (@sudo nginx -s reload@).
+
+<pre>
+    proxy_set_header      Upgrade           $http_upgrade;
+    proxy_set_header      Connection        "upgrade";
+</pre>
+
 h3. Changes on the collection's @preserve_version@ attribute semantics
 
 The @preserve_version@ attribute on collections was originally designed to allow clients to persist a preexisting collection version. This forced clients to make 2 requests if the intention is to "make this set of changes in a new version that will be kept", so we have changed the semantics to do just that: When passing @preserve_version=true@ along with other collection updates, the current version is persisted and also the newly created one will be persisted on the next update.
index d68529a226f47e33628acb4c81fe8c8f3ad2e677..9e0256c632a037c2484aad0d30eea292e6d6be1b 100644 (file)
@@ -18,7 +18,7 @@ ARVADOS_API_TOKEN=1234567890qwertyuiopasdfghjklzxcvbnm1234567890zzzz
 
 In these examples, @zzzzz-tpzed-3kz0nwtjehhl0u4@ is the sample user account.  Replace with the uuid of the user you wish to manipulate.
 
-See "user management":{{site.baseurl}}/admin/activation.html for an overview of how to use these commands.
+See "user management":{{site.baseurl}}/admin/user-management.html for an overview of how to use these commands.
 
 h3. Setup a user
 
index 9e53775ed4abc212ead38e249e42125a3eb260b1..296660d01bda247653b68958a0b9f67f15aa5d24 100644 (file)
@@ -43,7 +43,7 @@ This section describes the different user account states.
 notextile. <div class="spaced-out">
 
 # A new user record is not set up, and not active.  An inactive user cannot create or update any object, but can read Arvados objects that the user account has permission to read (such as publicly available items readable by the "anonymous" user).
-# Using Workbench or the "command line":{{site.baseurl}}/install/cheat_sheet.html , the admin invokes @setup@ on the user.  The setup method adds the user to the "All users" group.
+# Using Workbench or the "command line":{{site.baseurl}}/admin/user-management-cli.html , the admin invokes @setup@ on the user.  The setup method adds the user to the "All users" group.
 - If "Users.AutoSetupNewUsers":config.html is true, this happens automatically during user creation, so in that case new users start at step (3).
 - If "Users.AutoSetupNewUsersWithRepository":config.html is true, a new git repo is created for the user.
 - If "Users.AutoSetupNewUsersWithVmUUID":config.html is set, the user is given login permission to the specified shell node
@@ -58,7 +58,7 @@ Unsetup removes the user from the "All users" group and makes them inactive, pre
 
 notextile. </div>
 
-User management can be performed through the web using Workbench or the command line.  See "user management at the CLI":{{site.baseurl}}/install/cheat_sheet.html for specific examples.
+User management can be performed through the web using Workbench or the command line.  See "user management at the CLI":{{site.baseurl}}/admin/user-management-cli.html for specific examples.
 
 h2(#user_agreements). User agreements and self-activation
 
index 8a7ebc36e5b33613fc6947c88f833ad093aaf351..096a1fcaa9b628e6d5907ac33e8c6625f1114fd7 100644 (file)
@@ -57,6 +57,8 @@ Generally this will contain additional keys that are not present in any correspo
 |auth_uuid|string|UUID of a token to be passed into the container itself, used to access Keep-backed mounts, etc.  Automatically assigned.|Null if state∉{"Locked","Running"} or if @runtime_token@ was provided.|
 |locked_by_uuid|string|UUID of a token, indicating which dispatch process changed state to Locked. If null, any token can be used to lock. If not null, only the indicated token can modify this container.|Null if state∉{"Locked","Running"}|
 |runtime_token|string|A v2 token to be passed into the container itself, used to access Keep-backed mounts, etc.|Not returned in API responses.  Reset to null when state is "Complete" or "Cancelled".|
+|gateway_address|string|Address (host:port) of gateway server.|Internal use only.|
+|interactive_session_started|boolean|Indicates whether @arvados-client shell@ has been used to run commands in the container, which may have altered the container's behavior and output.||
 
 h2(#container_states). Container states
 
index cde189d6ffa341833cadd7cd08be32fd79146a7c..6db8d963e744b9a85459501ccf69bcf892321a11 100644 (file)
@@ -127,7 +127,7 @@ table(table table-bordered table-condensed).
 
 h3. setup
 
-Set up a user.  Adds the user to the "All users" group.  Enables the user to invoke @activate@.  See "user management":{{site.baseurl}}/admin/activation.html for details.
+Set up a user.  Adds the user to the "All users" group.  Enables the user to invoke @activate@.  See "user management":{{site.baseurl}}/admin/user-management.html for details.
 
 Arguments:
 
@@ -137,7 +137,7 @@ table(table table-bordered table-condensed).
 
 h3. activate
 
-Check that a user has is set up and has signed all the user agreements.  If so, activate the user.  Users can invoke this for themselves.  See "user agreements":{{site.baseurl}}/admin/activation.html#user_agreements for details.
+Check that a user has is set up and has signed all the user agreements.  If so, activate the user.  Users can invoke this for themselves.  See "user agreements":{{site.baseurl}}/admin/user-management.html#user_agreements for details.
 
 Arguments:
 
@@ -147,7 +147,7 @@ table(table table-bordered table-condensed).
 
 h3. unsetup
 
-Remove the user from the "All users" group and deactivate the user.  See "user management":{{site.baseurl}}/admin/activation.html for details.
+Remove the user from the "All users" group and deactivate the user.  See "user management":{{site.baseurl}}/admin/user-management.html for details.
 
 Arguments:
 
index 67e66eecec2bba6f7f234d06e328a0a5bc116691..9d8f456509b12d730d2d22bdcae6a8b785f74eb6 100644 (file)
@@ -27,7 +27,7 @@ The "browser authentication process is documented in detail on the Arvados wiki.
 
 h2. User activation
 
-"Creation and activation of new users is described here.":{{site.baseurl}}/admin/activation.html
+"Creation and activation of new users is described here.":{{site.baseurl}}/admin/user-management.html
 
 h2. Creating tokens via the API
 
index 7512828430fd821696d46fad2631b51d4edb9599..1ae8b6006405af727a4d4d22c4ffc99accfd53fa 100644 (file)
@@ -20,9 +20,9 @@ h2(#cluster_id). Cluster identifiers
 
 Clusters are identified by a five-digit alphanumeric id (numbers and lowercase letters).  There are 36 ^5^ = 60466176 possible cluster identifiers.
 
-* For automated tests purposes, use "z****"
+* For automated test purposes, use "z****"
 * For experimental/local-only/private clusters that won't ever be visible on the public Internet, use "x****"
-* For long-lived clusters, we recommend reserving a cluster id.  Contact "info@curii.com":mailto:info@curii.com
+* For long-lived clusters, we recommend reserving a cluster id.  Contact "info@curii.com":mailto:info@curii.com for more information.
 
 Cluster identifiers are mapped API server hosts one of two ways:
 
diff --git a/doc/install/automatic.html.textile.liquid b/doc/install/automatic.html.textile.liquid
new file mode 100644 (file)
index 0000000..79e8505
--- /dev/null
@@ -0,0 +1,47 @@
+---
+layout: default
+navsection: installguide
+title: Automatic single-node install
+...
+{% comment %}
+Copyright (C) The Arvados Authors. All rights reserved.
+
+SPDX-License-Identifier: CC-BY-SA-3.0
+{% endcomment %}
+
+{% include 'notebox_begin' %}
+This installation method is not fully implemented, which is why this page is not yet listed in the "table of installation options":{{site.baseurl}}/install/index.html or in the left nav.
+{% include 'notebox_end' %}
+
+This method sets up a new Arvados cluster using a single host/VM. It is the easiest way to get a new production cluster up and running.
+
+A single-node installation supports all Arvados functionality at small scale. Substantial workloads will require additional nodes and configuration steps.
+
+h2. Prerequisites
+
+You will need:
+* a server host running Debian 10 (buster).
+* 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).
+
+h2. Initialize the cluster
+
+<pre>
+# echo > /etc/apt/sources.list.d/arvados.list "deb http://apt.arvados.org/buster buster main"
+# apt-get update
+# apt-get install arvados-server-easy
+# arvados-server init -type production -cluster-id x9999 -controller-address x9999.example.com -admin-email example@gmail.com.example
+</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.
+
+h2. Enable login
+
+Follow the instructions to "set up Google login":{{site.baseurl}}/install/setup-login.html or another authentication option.
+
+After updating your configuration file (@/etc/arvados/config.yml@), restart the server to make your changes take effect:
+
+<pre>
+# systemctl restart arvados-server
+</pre>
index ca55be53e332f12196b359b17a711e5b13c495fc..7d0353c9e71283617e0bb6c5e53c1de89204663b 100644 (file)
@@ -153,11 +153,13 @@ server {
     proxy_connect_timeout 90s;
     proxy_read_timeout    300s;
 
-    proxy_set_header      X-Forwarded-Proto https;
-    proxy_set_header      Host $http_host;
+    proxy_set_header      Host              $http_host;
+    proxy_set_header      Upgrade           $http_upgrade;
+    proxy_set_header      Connection        "upgrade";
     proxy_set_header      X-External-Client $external_client;
-    proxy_set_header      X-Real-IP $remote_addr;
-    proxy_set_header      X-Forwarded-For $proxy_add_x_forwarded_for;
+    proxy_set_header      X-Forwarded-For   $proxy_add_x_forwarded_for;
+    proxy_set_header      X-Forwarded-Proto https;
+    proxy_set_header      X-Real-IP         $remote_addr;
   }
 }
 
index e6f1ba8fdcdb6e562831f197ae1a262dc76b25a1..364e8cd2bb2267e119961042e05a38f9eebb9b3f 100644 (file)
@@ -63,7 +63,7 @@ table(table table-bordered table-condensed).
 |"Git server":install-arv-git-httpd.html |Arvados-hosted git repositories, with Arvados-token based authentication.|Optional, but required by Workflow Composer.|
 |\3=. *Crunch (running containers)*|
 |"arvados-dispatch-cloud":crunch2-cloud/install-dispatch-cloud.html |Allocate and free cloud VM instances on demand based on workload.|Optional, not needed for a static Slurm cluster such as on-premises HPC.|
-|"crunch-dispatch-slurm":crunch2-slurm/install-prerequisites.html |Run analysis workflows using Docker containers distributed across a Slurm cluster.|Optional, not needed for a Cloud installation, or if you wish to use Arvados for data management only.|
+|"crunch-dispatch-slurm":crunch2-slurm/install-dispatch.html |Run analysis workflows using Docker containers distributed across a Slurm cluster.|Optional, not needed for a Cloud installation, or if you wish to use Arvados for data management only.|
 
 h2(#identity). Identity provider
 
@@ -119,7 +119,13 @@ For a small demo installation, it is possible to run all the Arvados services on
 
 h2(#clusterid). Arvados Cluster ID
 
-Each Arvados installation should have a cluster identifier, which is a unique 5-character lowercase alphanumeric string.   Here is one way to make a random 5-character string:
+Each Arvados installation is identified by a cluster identifier, which is a unique 5-character lowercase alphanumeric string. There are 36 5 = 60466176 possible cluster identifiers.
+
+* For automated test purposes, use “z****”
+* For experimental/local-only/private clusters that won’t ever be visible on the public Internet, use “x****”
+* For long-lived clusters, we recommend reserving a cluster id.  Contact "info@curii.com":mailto:info@curii.com for more information.
+
+Here is one way to make a random 5-character string:
 
 <notextile>
 <pre><code>~$ <span class="userinput">tr -dc 0-9a-z &lt;/dev/urandom | head -c5; echo</span>
diff --git a/go.mod b/go.mod
index 262978d9125d412b32bfee22508bcfe517de8ec6..88dcb86c72e35cafcf61905f7d67ebb554026b0d 100644 (file)
--- a/go.mod
+++ b/go.mod
@@ -20,6 +20,7 @@ require (
        github.com/bradleypeabody/godap v0.0.0-20170216002349-c249933bc092
        github.com/coreos/go-oidc v2.1.0+incompatible
        github.com/coreos/go-systemd v0.0.0-20180108085132-cc4f39464dc7
+       github.com/creack/pty v1.1.7
        github.com/dnaeon/go-vcr v1.0.1 // indirect
        github.com/docker/distribution v2.6.0-rc.1.0.20180105232752-277ed486c948+incompatible // indirect
        github.com/docker/docker v1.4.2-0.20180109013817-94b8a116fbf1
@@ -33,6 +34,7 @@ require (
        github.com/go-asn1-ber/asn1-ber v1.4.1 // indirect
        github.com/go-ldap/ldap v3.0.3+incompatible
        github.com/gogo/protobuf v1.1.1
+       github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
        github.com/gorilla/context v1.1.1 // indirect
        github.com/gorilla/mux v1.6.1-0.20180107155708-5bbbb5b2b572
        github.com/hashicorp/golang-lru v0.5.1
diff --git a/go.sum b/go.sum
index 85d205112fb95ecf1895d96122686e0e2e2a849b..91b5689eb33424ce130f8c06400c74da002e585e 100644 (file)
--- a/go.sum
+++ b/go.sum
@@ -72,6 +72,8 @@ github.com/coreos/go-oidc v2.1.0+incompatible h1:sdJrfw8akMnCuUlaZU3tE/uYXFgfqom
 github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
 github.com/coreos/go-systemd v0.0.0-20180108085132-cc4f39464dc7 h1:e3u8KWFMR3irlDo1Z/tL8Hsz1MJmCLkSoX5AZRMKZkg=
 github.com/coreos/go-systemd v0.0.0-20180108085132-cc4f39464dc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/creack/pty v1.1.7 h1:6pwm8kMQKCmgUg0ZHTm5+/YvRK0s3THD/28+T6/kk4A=
+github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
 github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -134,6 +136,8 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
 github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
+github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
 github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
 github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
index f0797c2ac51fb7ec9f861413a371f133b3237bd9..b2b8c896c2866bceeb399475d5aa7e6cc2dea75c 100644 (file)
@@ -9,6 +9,7 @@ import (
        "fmt"
        "io/ioutil"
        "net"
+       "os"
        "path/filepath"
 )
 
@@ -27,23 +28,28 @@ func (createCertificates) String() string {
 func (createCertificates) Run(ctx context.Context, fail func(error), super *Supervisor) error {
        var san string
        if net.ParseIP(super.ListenHost) != nil {
-               san = fmt.Sprintf("IP:%s", super.ListenHost)
+               san += fmt.Sprintf(",IP:%s", super.ListenHost)
        } else {
-               san = fmt.Sprintf("DNS:%s", super.ListenHost)
+               san += fmt.Sprintf(",DNS:%s", super.ListenHost)
+       }
+       if hostname, err := os.Hostname(); err != nil {
+               return fmt.Errorf("hostname: %w", err)
+       } else {
+               san += ",DNS:" + hostname
        }
 
        // Generate root key
-       err := super.RunProgram(ctx, super.tempdir, nil, nil, "openssl", "genrsa", "-out", "rootCA.key", "4096")
+       err := super.RunProgram(ctx, super.tempdir, runOptions{}, "openssl", "genrsa", "-out", "rootCA.key", "4096")
        if err != nil {
                return err
        }
        // Generate a self-signed root certificate
-       err = super.RunProgram(ctx, super.tempdir, nil, nil, "openssl", "req", "-x509", "-new", "-nodes", "-key", "rootCA.key", "-sha256", "-days", "3650", "-out", "rootCA.crt", "-subj", "/C=US/ST=MA/O=Example Org/CN=localhost")
+       err = super.RunProgram(ctx, super.tempdir, runOptions{}, "openssl", "req", "-x509", "-new", "-nodes", "-key", "rootCA.key", "-sha256", "-days", "3650", "-out", "rootCA.crt", "-subj", "/C=US/ST=MA/O=Example Org/CN=localhost")
        if err != nil {
                return err
        }
        // Generate server key
-       err = super.RunProgram(ctx, super.tempdir, nil, nil, "openssl", "genrsa", "-out", "server.key", "2048")
+       err = super.RunProgram(ctx, super.tempdir, runOptions{}, "openssl", "genrsa", "-out", "server.key", "2048")
        if err != nil {
                return err
        }
@@ -52,17 +58,17 @@ func (createCertificates) Run(ctx context.Context, fail func(error), super *Supe
        if err != nil {
                return err
        }
-       err = ioutil.WriteFile(filepath.Join(super.tempdir, "server.cfg"), append(defaultconf, []byte(fmt.Sprintf("\n[SAN]\nsubjectAltName=DNS:localhost,DNS:localhost.localdomain,%s\n", san))...), 0644)
+       err = ioutil.WriteFile(filepath.Join(super.tempdir, "server.cfg"), append(defaultconf, []byte(fmt.Sprintf("\n[SAN]\nsubjectAltName=DNS:localhost,DNS:localhost.localdomain%s\n", san))...), 0644)
        if err != nil {
                return err
        }
        // Generate signing request
-       err = super.RunProgram(ctx, super.tempdir, nil, nil, "openssl", "req", "-new", "-sha256", "-key", "server.key", "-subj", "/C=US/ST=MA/O=Example Org/CN=localhost", "-reqexts", "SAN", "-config", "server.cfg", "-out", "server.csr")
+       err = super.RunProgram(ctx, super.tempdir, runOptions{}, "openssl", "req", "-new", "-sha256", "-key", "server.key", "-subj", "/C=US/ST=MA/O=Example Org/CN=localhost", "-reqexts", "SAN", "-config", "server.cfg", "-out", "server.csr")
        if err != nil {
                return err
        }
        // Sign certificate
-       err = super.RunProgram(ctx, super.tempdir, nil, nil, "openssl", "x509", "-req", "-in", "server.csr", "-CA", "rootCA.crt", "-CAkey", "rootCA.key", "-CAcreateserial", "-out", "server.crt", "-extfile", "server.cfg", "-extensions", "SAN", "-days", "3650", "-sha256")
+       err = super.RunProgram(ctx, super.tempdir, runOptions{}, "openssl", "x509", "-req", "-in", "server.csr", "-CA", "rootCA.crt", "-CAkey", "rootCA.key", "-CAcreateserial", "-out", "server.crt", "-extfile", "server.cfg", "-extensions", "SAN", "-days", "3650", "-sha256")
        if err != nil {
                return err
        }
index e0e2755220a1ec3bbdb8737067c54a579209f40e..963d16226b343341cdf3394af646097b498b9b1c 100644 (file)
@@ -108,6 +108,12 @@ func (bcmd bootCommand) run(ctx context.Context, prog string, args []string, std
                fmt.Fprintln(stdout, url)
                if *shutdown {
                        super.Stop()
+                       // Wait for children to exit. Don't report the
+                       // ensuing "context cancelled" error, though:
+                       // return nil to indicate successful startup.
+                       _ = super.Wait()
+                       fmt.Fprintln(stderr, "PASS - all services booted successfully")
+                       return nil
                }
        }
        // Wait for signal/crash + orderly shutdown
index 0f105d6b6ca3ad8b835f90c626060edd454aa513..dc4aebd528d4bf3f6c8d43c359efb8b51ed6b73a 100644 (file)
@@ -9,8 +9,10 @@ import (
        "fmt"
        "io/ioutil"
        "net"
+       "net/url"
        "os"
        "os/exec"
+       "os/user"
        "path/filepath"
        "regexp"
 
@@ -36,7 +38,7 @@ func (runNginx) Run(ctx context.Context, fail func(error), super *Supervisor) er
                "SSLKEY":     filepath.Join(super.tempdir, "server.key"),
                "ACCESSLOG":  filepath.Join(super.tempdir, "nginx_access.log"),
                "ERRORLOG":   filepath.Join(super.tempdir, "nginx_error.log"),
-               "TMPDIR":     super.tempdir,
+               "TMPDIR":     super.wwwtempdir,
        }
        for _, cmpt := range []struct {
                varname string
@@ -51,25 +53,31 @@ func (runNginx) Run(ctx context.Context, fail func(error), super *Supervisor) er
                {"WORKBENCH1", super.cluster.Services.Workbench1},
                {"WS", super.cluster.Services.Websocket},
        } {
-               port, err := internalPort(cmpt.svc)
+               host, port, err := internalPort(cmpt.svc)
                if err != nil {
-                       return fmt.Errorf("%s internal port: %s (%v)", cmpt.varname, err, cmpt.svc)
+                       return fmt.Errorf("%s internal port: %w (%v)", cmpt.varname, err, cmpt.svc)
                }
-               if ok, err := addrIsLocal(net.JoinHostPort(super.ListenHost, port)); !ok || err != nil {
-                       return fmt.Errorf("urlIsLocal() failed for host %q port %q: %v", super.ListenHost, port, err)
+               if ok, err := addrIsLocal(net.JoinHostPort(host, port)); !ok || err != nil {
+                       return fmt.Errorf("urlIsLocal() failed for host %q port %q: %v", host, port, err)
                }
                vars[cmpt.varname+"PORT"] = port
 
                port, err = externalPort(cmpt.svc)
                if err != nil {
-                       return fmt.Errorf("%s external port: %s (%v)", cmpt.varname, err, cmpt.svc)
+                       return fmt.Errorf("%s external port: %w (%v)", cmpt.varname, err, cmpt.svc)
                }
                if ok, err := addrIsLocal(net.JoinHostPort(super.ListenHost, port)); !ok || err != nil {
                        return fmt.Errorf("urlIsLocal() failed for host %q port %q: %v", super.ListenHost, port, err)
                }
                vars[cmpt.varname+"SSLPORT"] = port
        }
-       tmpl, err := ioutil.ReadFile(filepath.Join(super.SourcePath, "sdk", "python", "tests", "nginx.conf"))
+       var conftemplate string
+       if super.ClusterType == "production" {
+               conftemplate = "/var/lib/arvados/share/nginx.conf"
+       } else {
+               conftemplate = filepath.Join(super.SourcePath, "sdk", "python", "tests", "nginx.conf")
+       }
+       tmpl, err := ioutil.ReadFile(conftemplate)
        if err != nil {
                return err
        }
@@ -93,13 +101,32 @@ func (runNginx) Run(ctx context.Context, fail func(error), super *Supervisor) er
                        }
                }
        }
+
+       args := []string{
+               "-g", "error_log stderr info;",
+               "-g", "pid " + filepath.Join(super.wwwtempdir, "nginx.pid") + ";",
+               "-c", conffile,
+       }
+       // Nginx ignores "user www-data;" when running as a non-root
+       // user... except that it causes it to ignore our other -g
+       // options. So we still have to decide for ourselves whether
+       // it's needed.
+       if u, err := user.Current(); err != nil {
+               return fmt.Errorf("user.Current(): %w", err)
+       } else if u.Uid == "0" {
+               args = append([]string{"-g", "user www-data;"}, args...)
+       }
+
        super.waitShutdown.Add(1)
        go func() {
                defer super.waitShutdown.Done()
-               fail(super.RunProgram(ctx, ".", nil, nil, nginx,
-                       "-g", "error_log stderr info;",
-                       "-g", "pid "+filepath.Join(super.tempdir, "nginx.pid")+";",
-                       "-c", conffile))
+               fail(super.RunProgram(ctx, ".", runOptions{}, nginx, args...))
        }()
-       return waitForConnect(ctx, super.cluster.Services.Controller.ExternalURL.Host)
+       // Choose one of the ports where Nginx should listen, and wait
+       // here until we can connect. If ExternalURL is https://foo (with no port) then we connect to "foo:https"
+       testurl := url.URL(super.cluster.Services.Controller.ExternalURL)
+       if testurl.Port() == "" {
+               testurl.Host = net.JoinHostPort(testurl.Host, testurl.Scheme)
+       }
+       return waitForConnect(ctx, testurl.Host)
 }
index 6a2c4b61f5a93584fa3a4ba023e2b25b1a5a545c..0340ebc8c64272944cf202fd2ec2c5c9f0788f32 100644 (file)
@@ -37,6 +37,10 @@ func (runner installPassenger) String() string {
 }
 
 func (runner installPassenger) Run(ctx context.Context, fail func(error), super *Supervisor) error {
+       if super.ClusterType == "production" {
+               // passenger has already been installed via package
+               return nil
+       }
        err := super.wait(ctx, runner.depends...)
        if err != nil {
                return err
@@ -46,32 +50,32 @@ func (runner installPassenger) Run(ctx context.Context, fail func(error), super
        defer passengerInstallMutex.Unlock()
 
        var buf bytes.Buffer
-       err = super.RunProgram(ctx, runner.src, &buf, nil, "gem", "list", "--details", "bundler")
+       err = super.RunProgram(ctx, runner.src, runOptions{output: &buf}, "gem", "list", "--details", "bundler")
        if err != nil {
                return err
        }
-       for _, version := range []string{"1.11.0", "1.17.3", "2.0.2"} {
+       for _, version := range []string{"1.16.6", "1.17.3", "2.0.2"} {
                if !strings.Contains(buf.String(), "("+version+")") {
-                       err = super.RunProgram(ctx, runner.src, nil, nil, "gem", "install", "--user", "bundler:1.11", "bundler:1.17.3", "bundler:2.0.2")
+                       err = super.RunProgram(ctx, runner.src, runOptions{}, "gem", "install", "--user", "--conservative", "--no-document", "bundler:1.16.6", "bundler:1.17.3", "bundler:2.0.2")
                        if err != nil {
                                return err
                        }
                        break
                }
        }
-       err = super.RunProgram(ctx, runner.src, nil, nil, "bundle", "install", "--jobs", "4", "--path", filepath.Join(os.Getenv("HOME"), ".gem"))
+       err = super.RunProgram(ctx, runner.src, runOptions{}, "bundle", "install", "--jobs", "4", "--path", filepath.Join(os.Getenv("HOME"), ".gem"))
        if err != nil {
                return err
        }
-       err = super.RunProgram(ctx, runner.src, nil, nil, "bundle", "exec", "passenger-config", "build-native-support")
+       err = super.RunProgram(ctx, runner.src, runOptions{}, "bundle", "exec", "passenger-config", "build-native-support")
        if err != nil {
                return err
        }
-       err = super.RunProgram(ctx, runner.src, nil, nil, "bundle", "exec", "passenger-config", "install-standalone-runtime")
+       err = super.RunProgram(ctx, runner.src, runOptions{}, "bundle", "exec", "passenger-config", "install-standalone-runtime")
        if err != nil {
                return err
        }
-       err = super.RunProgram(ctx, runner.src, nil, nil, "bundle", "exec", "passenger-config", "validate-install")
+       err = super.RunProgram(ctx, runner.src, runOptions{}, "bundle", "exec", "passenger-config", "validate-install")
        if err != nil && !strings.Contains(err.Error(), "exit status 2") {
                // Exit code 2 indicates there were warnings (like
                // "other passenger installations have been detected",
@@ -83,9 +87,10 @@ func (runner installPassenger) Run(ctx context.Context, fail func(error), super
 }
 
 type runPassenger struct {
-       src     string
-       svc     arvados.Service
-       depends []supervisedTask
+       src       string // path to app in source tree
+       varlibdir string // path to app (relative to /var/lib/arvados) in OS package
+       svc       arvados.Service
+       depends   []supervisedTask
 }
 
 func (runner runPassenger) String() string {
@@ -97,10 +102,16 @@ func (runner runPassenger) Run(ctx context.Context, fail func(error), super *Sup
        if err != nil {
                return err
        }
-       port, err := internalPort(runner.svc)
+       host, port, err := internalPort(runner.svc)
        if err != nil {
                return fmt.Errorf("bug: no internalPort for %q: %v (%#v)", runner, err, runner.svc)
        }
+       var appdir string
+       if super.ClusterType == "production" {
+               appdir = "/var/lib/arvados/" + runner.varlibdir
+       } else {
+               appdir = runner.src
+       }
        loglevel := "4"
        if lvl, ok := map[string]string{
                "debug":   "5",
@@ -116,13 +127,37 @@ func (runner runPassenger) Run(ctx context.Context, fail func(error), super *Sup
        super.waitShutdown.Add(1)
        go func() {
                defer super.waitShutdown.Done()
-               err = super.RunProgram(ctx, runner.src, nil, railsEnv, "bundle", "exec",
+               cmdline := []string{
+                       "bundle", "exec",
                        "passenger", "start",
-                       "-p", port,
+                       "--address", host,
+                       "--port", port,
                        "--log-file", "/dev/stderr",
                        "--log-level", loglevel,
                        "--no-friendly-error-pages",
-                       "--pid-file", filepath.Join(super.tempdir, "passenger."+strings.Replace(runner.src, "/", "_", -1)+".pid"))
+                       "--disable-anonymous-telemetry",
+                       "--disable-security-update-check",
+                       "--no-compile-runtime",
+                       "--no-install-runtime",
+                       "--pid-file", filepath.Join(super.wwwtempdir, "passenger."+strings.Replace(appdir, "/", "_", -1)+".pid"),
+               }
+               opts := runOptions{
+                       env: append([]string{
+                               "TMPDIR=" + super.wwwtempdir,
+                       }, railsEnv...),
+               }
+               if super.ClusterType == "production" {
+                       opts.user = "www-data"
+                       opts.env = append(opts.env, "HOME=/var/www")
+               } else {
+                       // This would be desirable when changing uid
+                       // too, but it fails because /dev/stderr is a
+                       // symlink to a pty owned by root: "nginx:
+                       // [emerg] open() "/dev/stderr" failed (13:
+                       // Permission denied)"
+                       cmdline = append(cmdline, "--log-file", "/dev/stderr")
+               }
+               err = super.RunProgram(ctx, appdir, opts, cmdline[0], cmdline[1:]...)
                fail(err)
        }()
        return nil
index 7661c6b58795e623e3ebac21e99b100b2c474d34..d105b0b62342b4defc60838e14accd9f38ce9ed8 100644 (file)
@@ -36,15 +36,19 @@ func (runPostgreSQL) Run(ctx context.Context, fail func(error), super *Superviso
                return err
        }
 
+       if super.ClusterType == "production" {
+               return nil
+       }
+
        iamroot := false
        if u, err := user.Current(); err != nil {
-               return fmt.Errorf("user.Current(): %s", err)
+               return fmt.Errorf("user.Current(): %w", err)
        } else if u.Uid == "0" {
                iamroot = true
        }
 
        buf := bytes.NewBuffer(nil)
-       err = super.RunProgram(ctx, super.tempdir, buf, nil, "pg_config", "--bindir")
+       err = super.RunProgram(ctx, super.tempdir, runOptions{output: buf}, "pg_config", "--bindir")
        if err != nil {
                return err
        }
@@ -56,6 +60,7 @@ func (runPostgreSQL) Run(ctx context.Context, fail func(error), super *Superviso
                return err
        }
        prog, args := filepath.Join(bindir, "initdb"), []string{"-D", datadir, "-E", "utf8"}
+       opts := runOptions{}
        if iamroot {
                postgresUser, err := user.Lookup("postgres")
                if err != nil {
@@ -81,25 +86,19 @@ func (runPostgreSQL) Run(ctx context.Context, fail func(error), super *Superviso
                if err != nil {
                        return err
                }
-               // We can't use "sudo -u" here because it creates an
-               // intermediate process that interferes with our
-               // ability to reliably kill postgres. The setuidgid
-               // program just calls exec without forking, so it
-               // doesn't have this problem.
-               args = append([]string{"postgres", prog}, args...)
-               prog = "setuidgid"
-       }
-       err = super.RunProgram(ctx, super.tempdir, nil, nil, prog, args...)
+               opts.user = "postgres"
+       }
+       err = super.RunProgram(ctx, super.tempdir, opts, prog, args...)
        if err != nil {
                return err
        }
 
-       err = super.RunProgram(ctx, super.tempdir, nil, nil, "cp", "server.crt", "server.key", datadir)
+       err = super.RunProgram(ctx, super.tempdir, runOptions{}, "cp", "server.crt", "server.key", datadir)
        if err != nil {
                return err
        }
        if iamroot {
-               err = super.RunProgram(ctx, super.tempdir, nil, nil, "chown", "postgres", datadir+"/server.crt", datadir+"/server.key")
+               err = super.RunProgram(ctx, super.tempdir, runOptions{}, "chown", "postgres", datadir+"/server.crt", datadir+"/server.key")
                if err != nil {
                        return err
                }
@@ -114,13 +113,14 @@ func (runPostgreSQL) Run(ctx context.Context, fail func(error), super *Superviso
                        "-l",          // enable ssl
                        "-D", datadir, // data dir
                        "-k", datadir, // socket dir
+                       "-h", super.cluster.PostgreSQL.Connection["host"],
                        "-p", super.cluster.PostgreSQL.Connection["port"],
                }
+               opts := runOptions{}
                if iamroot {
-                       args = append([]string{"postgres", prog}, args...)
-                       prog = "setuidgid"
+                       opts.user = "postgres"
                }
-               fail(super.RunProgram(ctx, super.tempdir, nil, nil, prog, args...))
+               fail(super.RunProgram(ctx, super.tempdir, opts, prog, args...))
        }()
 
        for {
index 2afccc45b628cc01b00ddac873abdfc4eae20b61..bd1e942658e9f50fba873d3de4f3a1c971dd54dc 100644 (file)
@@ -20,11 +20,14 @@ func (seedDatabase) Run(ctx context.Context, fail func(error), super *Supervisor
        if err != nil {
                return err
        }
-       err = super.RunProgram(ctx, "services/api", nil, railsEnv, "bundle", "exec", "rake", "db:setup")
+       if super.ClusterType == "production" {
+               return nil
+       }
+       err = super.RunProgram(ctx, "services/api", runOptions{env: railsEnv}, "bundle", "exec", "rake", "db:setup")
        if err != nil {
                return err
        }
-       err = super.RunProgram(ctx, "services/api", nil, railsEnv, "bundle", "exec", "./script/get_anonymous_user_token.rb")
+       err = super.RunProgram(ctx, "services/api", runOptions{env: railsEnv}, "bundle", "exec", "./script/get_anonymous_user_token.rb")
        if err != nil {
                return err
        }
index 5afacfe7161c28604e8d56de4a5f83a7c80f320f..090e852446f7c3270f50c94a7ac88870d162a38e 100644 (file)
@@ -30,8 +30,8 @@ func (runner runServiceCommand) String() string {
 }
 
 func (runner runServiceCommand) Run(ctx context.Context, fail func(error), super *Supervisor) error {
-       binfile := filepath.Join(super.tempdir, "bin", "arvados-server")
-       err := super.RunProgram(ctx, super.tempdir, nil, nil, binfile, "-version")
+       binfile := filepath.Join(super.bindir, "arvados-server")
+       err := super.RunProgram(ctx, super.bindir, runOptions{}, binfile, "-version")
        if err != nil {
                return err
        }
@@ -46,7 +46,7 @@ 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, nil, []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()}}, binfile, runner.name, "-config", super.configfile))
                }()
        }
        return nil
@@ -77,7 +77,7 @@ func (runner runGoProgram) Run(ctx context.Context, fail func(error), super *Sup
                return ctx.Err()
        }
 
-       err = super.RunProgram(ctx, super.tempdir, nil, nil, binfile, "-version")
+       err = super.RunProgram(ctx, super.tempdir, runOptions{}, binfile, "-version")
        if err != nil {
                return err
        }
@@ -93,7 +93,7 @@ func (runner runGoProgram) Run(ctx context.Context, fail func(error), super *Sup
                super.waitShutdown.Add(1)
                go func() {
                        defer super.waitShutdown.Done()
-                       fail(super.RunProgram(ctx, super.tempdir, nil, []string{"ARVADOS_SERVICE_INTERNAL_URL=" + u.String()}, binfile))
+                       fail(super.RunProgram(ctx, super.tempdir, runOptions{env: []string{"ARVADOS_SERVICE_INTERNAL_URL=" + u.String()}}, binfile))
                }()
        }
        return nil
index 752466c2a2030d9ff5db66bbb49cfaf1e1646153..961ed55de37e1f7fb5d165edfa63f562689ceeff 100644 (file)
@@ -14,12 +14,14 @@ import (
        "io"
        "io/ioutil"
        "net"
+       "net/url"
        "os"
        "os/exec"
        "os/signal"
        "os/user"
        "path/filepath"
        "reflect"
+       "strconv"
        "strings"
        "sync"
        "syscall"
@@ -54,7 +56,9 @@ type Supervisor struct {
        tasksReady    map[string]chan bool
        waitShutdown  sync.WaitGroup
 
+       bindir     string
        tempdir    string
+       wwwtempdir string
        configfile string
        environ    []string // for child processes
 }
@@ -133,13 +137,26 @@ func (super *Supervisor) run(cfg *arvados.Config) error {
                return err
        }
 
-       super.tempdir, err = ioutil.TempDir("", "arvados-server-boot-")
-       if err != nil {
-               return err
-       }
-       defer os.RemoveAll(super.tempdir)
-       if err := os.Mkdir(filepath.Join(super.tempdir, "bin"), 0755); err != nil {
-               return err
+       // Choose bin and temp dirs: /var/lib/arvados/... in
+       // production, transient tempdir otherwise.
+       if super.ClusterType == "production" {
+               // These dirs have already been created by
+               // "arvados-server install" (or by extracting a
+               // package).
+               super.tempdir = "/var/lib/arvados/tmp"
+               super.wwwtempdir = "/var/lib/arvados/wwwtmp"
+               super.bindir = "/var/lib/arvados/bin"
+       } else {
+               super.tempdir, err = ioutil.TempDir("", "arvados-server-boot-")
+               if err != nil {
+                       return err
+               }
+               defer os.RemoveAll(super.tempdir)
+               super.wwwtempdir = super.tempdir
+               super.bindir = filepath.Join(super.tempdir, "bin")
+               if err := os.Mkdir(super.bindir, 0755); err != nil {
+                       return err
+               }
        }
 
        // Fill in any missing config keys, and write the resulting
@@ -148,7 +165,7 @@ func (super *Supervisor) run(cfg *arvados.Config) error {
        if err != nil {
                return err
        }
-       conffile, err := os.OpenFile(filepath.Join(super.tempdir, "config.yml"), os.O_CREATE|os.O_WRONLY, 0644)
+       conffile, err := os.OpenFile(filepath.Join(super.wwwtempdir, "config.yml"), os.O_CREATE|os.O_WRONLY, 0644)
        if err != nil {
                return err
        }
@@ -168,7 +185,10 @@ func (super *Supervisor) run(cfg *arvados.Config) error {
        super.setEnv("ARVADOS_CONFIG", super.configfile)
        super.setEnv("RAILS_ENV", super.ClusterType)
        super.setEnv("TMPDIR", super.tempdir)
-       super.prependEnv("PATH", super.tempdir+"/bin:/var/lib/arvados/bin:")
+       super.prependEnv("PATH", "/var/lib/arvados/bin:")
+       if super.ClusterType != "production" {
+               super.prependEnv("PATH", super.tempdir+"/bin:")
+       }
 
        super.cluster, err = cfg.GetCluster("")
        if err != nil {
@@ -184,16 +204,18 @@ func (super *Supervisor) run(cfg *arvados.Config) error {
                "PID": os.Getpid(),
        })
 
-       if super.SourceVersion == "" {
+       if super.SourceVersion == "" && super.ClusterType == "production" {
+               // don't need SourceVersion
+       } else if super.SourceVersion == "" {
                // Find current source tree version.
                var buf bytes.Buffer
-               err = super.RunProgram(super.ctx, ".", &buf, nil, "git", "diff", "--shortstat")
+               err = super.RunProgram(super.ctx, ".", runOptions{output: &buf}, "git", "diff", "--shortstat")
                if err != nil {
                        return err
                }
                dirty := buf.Len() > 0
                buf.Reset()
-               err = super.RunProgram(super.ctx, ".", &buf, nil, "git", "log", "-n1", "--format=%H")
+               err = super.RunProgram(super.ctx, ".", runOptions{output: &buf}, "git", "log", "-n1", "--format=%H")
                if err != nil {
                        return err
                }
@@ -226,15 +248,15 @@ func (super *Supervisor) run(cfg *arvados.Config) error {
                runGoProgram{src: "services/keep-web", svc: super.cluster.Services.WebDAV},
                runServiceCommand{name: "ws", svc: super.cluster.Services.Websocket, depends: []supervisedTask{seedDatabase{}}},
                installPassenger{src: "services/api"},
-               runPassenger{src: "services/api", svc: super.cluster.Services.RailsAPI, depends: []supervisedTask{createCertificates{}, seedDatabase{}, installPassenger{src: "services/api"}}},
+               runPassenger{src: "services/api", varlibdir: "railsapi", svc: super.cluster.Services.RailsAPI, depends: []supervisedTask{createCertificates{}, seedDatabase{}, installPassenger{src: "services/api"}}},
                installPassenger{src: "apps/workbench", depends: []supervisedTask{seedDatabase{}}}, // dependency ensures workbench doesn't delay api install/startup
-               runPassenger{src: "apps/workbench", svc: super.cluster.Services.Workbench1, depends: []supervisedTask{installPassenger{src: "apps/workbench"}}},
+               runPassenger{src: "apps/workbench", varlibdir: "workbench1", svc: super.cluster.Services.Workbench1, depends: []supervisedTask{installPassenger{src: "apps/workbench"}}},
                seedDatabase{},
        }
        if super.ClusterType != "test" {
                tasks = append(tasks,
-                       runServiceCommand{name: "dispatch-cloud", svc: super.cluster.Services.Controller},
-                       runGoProgram{src: "services/keep-balance"},
+                       runServiceCommand{name: "dispatch-cloud", svc: super.cluster.Services.DispatchCloud},
+                       runGoProgram{src: "services/keep-balance", svc: super.cluster.Services.Keepbalance},
                )
        }
        super.tasksReady = map[string]chan bool{}
@@ -384,9 +406,11 @@ func dedupEnv(in []string) []string {
 
 func (super *Supervisor) installGoProgram(ctx context.Context, srcpath string) (string, error) {
        _, basename := filepath.Split(srcpath)
-       bindir := filepath.Join(super.tempdir, "bin")
-       binfile := filepath.Join(bindir, basename)
-       err := super.RunProgram(ctx, filepath.Join(super.SourcePath, srcpath), nil, []string{"GOBIN=" + bindir}, "go", "install", "-ldflags", "-X git.arvados.org/arvados.git/lib/cmd.version="+super.SourceVersion+" -X main.version="+super.SourceVersion)
+       binfile := filepath.Join(super.bindir, basename)
+       if super.ClusterType == "production" {
+               return binfile, nil
+       }
+       err := super.RunProgram(ctx, filepath.Join(super.SourcePath, srcpath), runOptions{env: []string{"GOBIN=" + super.bindir}}, "go", "install", "-ldflags", "-X git.arvados.org/arvados.git/lib/cmd.version="+super.SourceVersion+" -X main.version="+super.SourceVersion)
        return binfile, err
 }
 
@@ -403,14 +427,23 @@ func (super *Supervisor) setupRubyEnv() error {
                        "GEM_PATH=",
                })
                gem := "gem"
-               if _, err := os.Stat("/var/lib/arvados/bin/gem"); err == nil {
+               if _, err := os.Stat("/var/lib/arvados/bin/gem"); err == nil || super.ClusterType == "production" {
                        gem = "/var/lib/arvados/bin/gem"
                }
                cmd := exec.Command(gem, "env", "gempath")
+               if super.ClusterType == "production" {
+                       cmd.Args = append([]string{"sudo", "-u", "www-data", "-E", "HOME=/var/www"}, cmd.Args...)
+                       path, err := exec.LookPath("sudo")
+                       if err != nil {
+                               return fmt.Errorf("LookPath(\"sudo\"): %w", err)
+                       }
+                       cmd.Path = path
+               }
+               cmd.Stderr = super.Stderr
                cmd.Env = super.environ
                buf, err := cmd.Output() // /var/lib/arvados/.gem/ruby/2.5.0/bin:...
                if err != nil || len(buf) == 0 {
-                       return fmt.Errorf("gem env gempath: %v", err)
+                       return fmt.Errorf("gem env gempath: %w", err)
                }
                gempath := string(bytes.Split(buf, []byte{':'})[0])
                super.prependEnv("PATH", gempath+"/bin:")
@@ -440,6 +473,12 @@ func (super *Supervisor) lookPath(prog string) string {
        return prog
 }
 
+type runOptions struct {
+       output io.Writer // attach stdout
+       env    []string  // add/replace environment variables
+       user   string    // run as specified user
+}
+
 // RunProgram runs prog with args, using dir as working directory. If ctx is
 // cancelled while the child is running, RunProgram terminates the child, waits
 // for it to exit, then returns.
@@ -448,22 +487,36 @@ func (super *Supervisor) lookPath(prog string) string {
 //
 // Child's stdout will be written to output if non-nil, otherwise the
 // boot command's stderr.
-func (super *Supervisor) RunProgram(ctx context.Context, dir string, output io.Writer, env []string, prog string, args ...string) error {
+func (super *Supervisor) RunProgram(ctx context.Context, dir string, opts runOptions, prog string, args ...string) error {
        cmdline := fmt.Sprintf("%s", append([]string{prog}, args...))
        super.logger.WithField("command", cmdline).WithField("dir", dir).Info("executing")
 
        logprefix := prog
-       if logprefix == "setuidgid" && len(args) >= 2 {
-               logprefix = args[1]
-       }
-       logprefix = strings.TrimPrefix(logprefix, super.tempdir+"/bin/")
-       if logprefix == "bundle" && len(args) > 2 && args[0] == "exec" {
-               logprefix = args[1]
-       } else if logprefix == "arvados-server" && len(args) > 1 {
-               logprefix = args[0]
-       }
-       if !strings.HasPrefix(dir, "/") {
-               logprefix = dir + ": " + logprefix
+       {
+               innerargs := args
+               if logprefix == "sudo" {
+                       for i := 0; i < len(args); i++ {
+                               if args[i] == "-u" {
+                                       i++
+                               } else if args[i] == "-E" || strings.Contains(args[i], "=") {
+                               } else {
+                                       logprefix = args[i]
+                                       innerargs = args[i+1:]
+                                       break
+                               }
+                       }
+               }
+               logprefix = strings.TrimPrefix(logprefix, "/var/lib/arvados/bin/")
+               logprefix = strings.TrimPrefix(logprefix, super.tempdir+"/bin/")
+               if logprefix == "bundle" && len(innerargs) > 2 && innerargs[0] == "exec" {
+                       _, dirbase := filepath.Split(dir)
+                       logprefix = innerargs[1] + "@" + dirbase
+               } else if logprefix == "arvados-server" && len(args) > 1 {
+                       logprefix = args[0]
+               }
+               if !strings.HasPrefix(dir, "/") {
+                       logprefix = dir + ": " + logprefix
+               }
        }
 
        cmd := exec.Command(super.lookPath(prog), args...)
@@ -484,10 +537,10 @@ func (super *Supervisor) RunProgram(ctx context.Context, dir string, output io.W
        }()
        copiers.Add(1)
        go func() {
-               if output == nil {
+               if opts.output == nil {
                        io.Copy(logwriter, stdout)
                } else {
-                       io.Copy(output, stdout)
+                       io.Copy(opts.output, stdout)
                }
                copiers.Done()
        }()
@@ -497,10 +550,34 @@ func (super *Supervisor) RunProgram(ctx context.Context, dir string, output io.W
        } else {
                cmd.Dir = filepath.Join(super.SourcePath, dir)
        }
-       env = append([]string(nil), env...)
+       env := append([]string(nil), opts.env...)
        env = append(env, super.environ...)
        cmd.Env = dedupEnv(env)
 
+       if opts.user != "" {
+               // Note: We use this approach instead of "sudo"
+               // because in certain circumstances (we are pid 1 in a
+               // docker container, and our passenger child process
+               // changes to pgid 1) the intermediate sudo process
+               // notices we have the same pgid as our child and
+               // refuses to propagate signals from us to our child,
+               // so we can't signal/shutdown our passenger/rails
+               // apps. "chpst" or "setuidgid" would work, but these
+               // few lines avoid depending on runit/daemontools.
+               u, err := user.Lookup(opts.user)
+               if err != nil {
+                       return fmt.Errorf("user.Lookup(%q): %w", opts.user, err)
+               }
+               uid, _ := strconv.Atoi(u.Uid)
+               gid, _ := strconv.Atoi(u.Gid)
+               cmd.SysProcAttr = &syscall.SysProcAttr{
+                       Credential: &syscall.Credential{
+                               Uid: uint32(uid),
+                               Gid: uint32(gid),
+                       },
+               }
+       }
+
        exited := false
        defer func() { exited = true }()
        go func() {
@@ -607,27 +684,26 @@ func (super *Supervisor) autofillConfig(cfg *arvados.Config) error {
                        }
                }
        }
-       if cluster.SystemRootToken == "" {
-               cluster.SystemRootToken = randomHexString(64)
-       }
-       if cluster.ManagementToken == "" {
-               cluster.ManagementToken = randomHexString(64)
-       }
-       if cluster.Collections.BlobSigningKey == "" {
-               cluster.Collections.BlobSigningKey = randomHexString(64)
-       }
-       if cluster.Users.AnonymousUserToken == "" {
-               cluster.Users.AnonymousUserToken = randomHexString(64)
-       }
-
-       if super.ClusterType != "production" && cluster.Containers.DispatchPrivateKey == "" {
-               buf, err := ioutil.ReadFile(filepath.Join(super.SourcePath, "lib", "dispatchcloud", "test", "sshkey_dispatch"))
-               if err != nil {
-                       return err
-               }
-               cluster.Containers.DispatchPrivateKey = string(buf)
-       }
        if super.ClusterType != "production" {
+               if cluster.SystemRootToken == "" {
+                       cluster.SystemRootToken = randomHexString(64)
+               }
+               if cluster.ManagementToken == "" {
+                       cluster.ManagementToken = randomHexString(64)
+               }
+               if cluster.Collections.BlobSigningKey == "" {
+                       cluster.Collections.BlobSigningKey = randomHexString(64)
+               }
+               if cluster.Users.AnonymousUserToken == "" {
+                       cluster.Users.AnonymousUserToken = randomHexString(64)
+               }
+               if cluster.Containers.DispatchPrivateKey == "" {
+                       buf, err := ioutil.ReadFile(filepath.Join(super.SourcePath, "lib", "dispatchcloud", "test", "sshkey_dispatch"))
+                       if err != nil {
+                               return err
+                       }
+                       cluster.Containers.DispatchPrivateKey = string(buf)
+               }
                cluster.TLS.Insecure = true
        }
        if super.ClusterType == "test" {
@@ -658,7 +734,7 @@ func (super *Supervisor) autofillConfig(cfg *arvados.Config) error {
        if super.OwnTemporaryDatabase {
                cluster.PostgreSQL.Connection = arvados.PostgreSQLConnection{
                        "client_encoding": "utf8",
-                       "host":            "localhost",
+                       "host":            super.ListenHost,
                        "port":            nextPort(super.ListenHost),
                        "dbname":          "arvados_test",
                        "user":            "arvados",
@@ -692,30 +768,30 @@ func randomHexString(chars int) string {
        return fmt.Sprintf("%x", b)
 }
 
-func internalPort(svc arvados.Service) (string, error) {
+func internalPort(svc arvados.Service) (host, port string, err error) {
        if len(svc.InternalURLs) > 1 {
-               return "", errors.New("internalPort() doesn't work with multiple InternalURLs")
+               return "", "", errors.New("internalPort() doesn't work with multiple InternalURLs")
        }
        for u := range svc.InternalURLs {
-               if _, p, err := net.SplitHostPort(u.Host); err != nil {
-                       return "", err
-               } else if p != "" {
-                       return p, nil
-               } else if u.Scheme == "https" {
-                       return "443", nil
-               } else {
-                       return "80", nil
+               u := url.URL(u)
+               host, port = u.Hostname(), u.Port()
+               switch {
+               case port != "":
+               case u.Scheme == "https", u.Scheme == "ws":
+                       port = "443"
+               default:
+                       port = "80"
                }
+               return
        }
-       return "", fmt.Errorf("service has no InternalURLs")
+       return "", "", fmt.Errorf("service has no InternalURLs")
 }
 
 func externalPort(svc arvados.Service) (string, error) {
-       if _, p, err := net.SplitHostPort(svc.ExternalURL.Host); err != nil {
-               return "", err
-       } else if p != "" {
+       u := url.URL(svc.ExternalURL)
+       if p := u.Port(); p != "" {
                return p, nil
-       } else if svc.ExternalURL.Scheme == "https" {
+       } else if u.Scheme == "https" || u.Scheme == "wss" {
                return "443", nil
        } else {
                return "80", nil
index 771dc2ee799584e4853c5e59c1769bd46bf44509..68e518732d6f85b8ef377a4f22ea1efebf16af46 100644 (file)
@@ -197,7 +197,7 @@ Clusters:
       # * 1.1) fits comfortably in memory. On a host dedicated to running
       # Keepstore, divide total memory by 88MiB to suggest a suitable value.
       # For example, if grep MemTotal /proc/meminfo reports MemTotal: 7125440
-      # kB, compute 7125440 / (88 * 1024)=79 and configure MaxBuffers: 79
+      # kB, compute 7125440 / (88 * 1024)=79 and set MaxKeepBlobBuffers: 79
       MaxKeepBlobBuffers: 128
 
       # API methods to disable. Disabled methods are not listed in the
@@ -567,6 +567,17 @@ Clusters:
         # work. If false, only the primary email address will be used.
         AlternateEmailAddresses: true
 
+        # Send additional parameters with authentication requests. See
+        # https://developers.google.com/identity/protocols/oauth2/openid-connect#authenticationuriparameters
+        # for a list of supported parameters.
+        AuthenticationRequestParameters:
+          # Show the "choose which Google account" page, even if the
+          # client is currently logged in to exactly one Google
+          # account.
+          prompt: select_account
+
+          SAMPLE: ""
+
       OpenIDConnect:
         # Authenticate with an OpenID Connect provider.
         Enable: false
@@ -601,6 +612,14 @@ Clusters:
         # address.
         UsernameClaim: ""
 
+        # Send additional parameters with authentication requests,
+        # like {display: page, prompt: consent}. See
+        # https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
+        # and refer to your provider's documentation for supported
+        # parameters.
+        AuthenticationRequestParameters:
+          SAMPLE: ""
+
       PAM:
         # (Experimental) Use PAM to authenticate users.
         Enable: false
@@ -868,6 +887,26 @@ Clusters:
         # period.
         LogUpdateSize: 32MiB
 
+      ShellAccess:
+        # An admin user can use "arvados-client shell" to start an
+        # interactive shell (with any user ID) in any running
+        # container.
+        Admin: false
+
+        # Any user can use "arvados-client shell" to start an
+        # interactive shell (with any user ID) in any running
+        # container that they started, provided it isn't also
+        # associated with a different user's container request.
+        #
+        # Interactive sessions make it easy to alter the container's
+        # runtime environment in ways that aren't recorded or
+        # reproducible. Consider the implications for automatic
+        # container reuse before enabling and using this feature. In
+        # particular, note that starting an interactive session does
+        # not disqualify a container from being reused by a different
+        # user/workflow in the future.
+        User: false
+
       SLURM:
         PrioritySpread: 0
         SbatchArgumentsList: []
index e4917032ffe06ca72d5a016fe7cd747a4cc12e02..3d0e27c7224f0c886643ef8be7f671ae8a1a2d74 100644 (file)
@@ -59,208 +59,213 @@ func ExportJSON(w io.Writer, cluster *arvados.Cluster) error {
 // exists.
 var whitelist = map[string]bool{
        // | sort -t'"' -k2,2
-       "API":                                          true,
-       "API.AsyncPermissionsUpdateInterval":           false,
-       "API.DisabledAPIs":                             false,
-       "API.KeepServiceRequestTimeout":                false,
-       "API.MaxConcurrentRequests":                    false,
-       "API.MaxIndexDatabaseRead":                     false,
-       "API.MaxItemsPerResponse":                      true,
-       "API.MaxKeepBlobBuffers":                       false,
-       "API.MaxRequestAmplification":                  false,
-       "API.MaxRequestSize":                           true,
-       "API.RequestTimeout":                           true,
-       "API.SendTimeout":                              true,
-       "API.WebsocketClientEventQueue":                false,
-       "API.WebsocketServerEventQueue":                false,
-       "AuditLogs":                                    false,
-       "AuditLogs.MaxAge":                             false,
-       "AuditLogs.MaxDeleteBatch":                     false,
-       "AuditLogs.UnloggedAttributes":                 false,
-       "ClusterID":                                    true,
-       "Collections":                                  true,
-       "Collections.BalanceCollectionBatch":           false,
-       "Collections.BalanceCollectionBuffers":         false,
-       "Collections.BalancePeriod":                    false,
-       "Collections.BalanceTimeout":                   false,
-       "Collections.BlobDeleteConcurrency":            false,
-       "Collections.BlobMissingReport":                false,
-       "Collections.BlobReplicateConcurrency":         false,
-       "Collections.BlobSigning":                      true,
-       "Collections.BlobSigningKey":                   false,
-       "Collections.BlobSigningTTL":                   true,
-       "Collections.BlobTrash":                        false,
-       "Collections.BlobTrashCheckInterval":           false,
-       "Collections.BlobTrashConcurrency":             false,
-       "Collections.BlobTrashLifetime":                false,
-       "Collections.CollectionVersioning":             false,
-       "Collections.DefaultReplication":               true,
-       "Collections.DefaultTrashLifetime":             true,
-       "Collections.ForwardSlashNameSubstitution":     true,
-       "Collections.ManagedProperties":                true,
-       "Collections.ManagedProperties.*":              true,
-       "Collections.ManagedProperties.*.*":            true,
-       "Collections.PreserveVersionIfIdle":            true,
-       "Collections.S3FolderObjects":                  true,
-       "Collections.TrashSweepInterval":               false,
-       "Collections.TrustAllContent":                  false,
-       "Collections.WebDAVCache":                      false,
-       "Containers":                                   true,
-       "Containers.CloudVMs":                          false,
-       "Containers.CrunchRunArgumentsList":            false,
-       "Containers.CrunchRunCommand":                  false,
-       "Containers.DefaultKeepCacheRAM":               true,
-       "Containers.DispatchPrivateKey":                false,
-       "Containers.JobsAPI":                           true,
-       "Containers.JobsAPI.Enable":                    true,
-       "Containers.JobsAPI.GitInternalDir":            false,
-       "Containers.Logging":                           false,
-       "Containers.LogReuseDecisions":                 false,
-       "Containers.MaxComputeVMs":                     false,
-       "Containers.MaxDispatchAttempts":               false,
-       "Containers.MaxRetryAttempts":                  true,
-       "Containers.MinRetryPeriod":                    true,
-       "Containers.ReserveExtraRAM":                   true,
-       "Containers.SLURM":                             false,
-       "Containers.StaleLockTimeout":                  false,
-       "Containers.SupportedDockerImageFormats":       true,
-       "Containers.SupportedDockerImageFormats.*":     true,
-       "Containers.UsePreemptibleInstances":           true,
-       "ForceLegacyAPI14":                             false,
-       "Git":                                          false,
-       "InstanceTypes":                                true,
-       "InstanceTypes.*":                              true,
-       "InstanceTypes.*.*":                            true,
-       "Login":                                        true,
-       "Login.Google":                                 true,
-       "Login.Google.AlternateEmailAddresses":         false,
-       "Login.Google.ClientID":                        false,
-       "Login.Google.ClientSecret":                    false,
-       "Login.Google.Enable":                          true,
-       "Login.LDAP":                                   true,
-       "Login.LDAP.AppendDomain":                      false,
-       "Login.LDAP.EmailAttribute":                    false,
-       "Login.LDAP.Enable":                            true,
-       "Login.LDAP.InsecureTLS":                       false,
-       "Login.LDAP.SearchAttribute":                   false,
-       "Login.LDAP.SearchBase":                        false,
-       "Login.LDAP.SearchBindPassword":                false,
-       "Login.LDAP.SearchBindUser":                    false,
-       "Login.LDAP.SearchFilters":                     false,
-       "Login.LDAP.StartTLS":                          false,
-       "Login.LDAP.StripDomain":                       false,
-       "Login.LDAP.URL":                               false,
-       "Login.LDAP.UsernameAttribute":                 false,
-       "Login.LoginCluster":                           true,
-       "Login.OpenIDConnect":                          true,
-       "Login.OpenIDConnect.ClientID":                 false,
-       "Login.OpenIDConnect.ClientSecret":             false,
-       "Login.OpenIDConnect.EmailClaim":               false,
-       "Login.OpenIDConnect.EmailVerifiedClaim":       false,
-       "Login.OpenIDConnect.Enable":                   true,
-       "Login.OpenIDConnect.Issuer":                   false,
-       "Login.OpenIDConnect.UsernameClaim":            false,
-       "Login.PAM":                                    true,
-       "Login.PAM.DefaultEmailDomain":                 false,
-       "Login.PAM.Enable":                             true,
-       "Login.PAM.Service":                            false,
-       "Login.RemoteTokenRefresh":                     true,
-       "Login.SSO":                                    true,
-       "Login.SSO.Enable":                             true,
-       "Login.SSO.ProviderAppID":                      false,
-       "Login.SSO.ProviderAppSecret":                  false,
-       "Login.Test":                                   true,
-       "Login.Test.Enable":                            true,
-       "Login.Test.Users":                             false,
-       "Login.TokenLifetime":                          false,
-       "Login.TrustedClients":                         false,
-       "Mail":                                         true,
-       "Mail.EmailFrom":                               false,
-       "Mail.IssueReporterEmailFrom":                  false,
-       "Mail.IssueReporterEmailTo":                    false,
-       "Mail.MailchimpAPIKey":                         false,
-       "Mail.MailchimpListID":                         false,
-       "Mail.SendUserSetupNotificationEmail":          false,
-       "Mail.SupportEmailAddress":                     true,
-       "ManagementToken":                              false,
-       "PostgreSQL":                                   false,
-       "RemoteClusters":                               true,
-       "RemoteClusters.*":                             true,
-       "RemoteClusters.*.ActivateUsers":               true,
-       "RemoteClusters.*.Host":                        true,
-       "RemoteClusters.*.Insecure":                    true,
-       "RemoteClusters.*.Proxy":                       true,
-       "RemoteClusters.*.Scheme":                      true,
-       "Services":                                     true,
-       "Services.*":                                   true,
-       "Services.*.ExternalURL":                       true,
-       "Services.*.InternalURLs":                      false,
-       "SystemLogs":                                   false,
-       "SystemRootToken":                              false,
-       "TLS":                                          false,
-       "Users":                                        true,
-       "Users.AdminNotifierEmailFrom":                 false,
-       "Users.AnonymousUserToken":                     true,
-       "Users.AutoAdminFirstUser":                     false,
-       "Users.AutoAdminUserWithEmail":                 false,
-       "Users.AutoSetupNewUsers":                      false,
-       "Users.AutoSetupNewUsersWithRepository":        false,
-       "Users.AutoSetupNewUsersWithVmUUID":            false,
-       "Users.AutoSetupUsernameBlacklist":             false,
-       "Users.EmailSubjectPrefix":                     false,
-       "Users.NewInactiveUserNotificationRecipients":  false,
-       "Users.NewUserNotificationRecipients":          false,
-       "Users.NewUsersAreActive":                      false,
-       "Users.PreferDomainForUsername":                false,
-       "Users.UserNotifierEmailFrom":                  false,
-       "Users.UserProfileNotificationAddress":         false,
-       "Users.UserSetupMailText":                      false,
-       "Volumes":                                      true,
-       "Volumes.*":                                    true,
-       "Volumes.*.*":                                  false,
-       "Volumes.*.AccessViaHosts":                     true,
-       "Volumes.*.AccessViaHosts.*":                   true,
-       "Volumes.*.AccessViaHosts.*.ReadOnly":          true,
-       "Volumes.*.ReadOnly":                           true,
-       "Volumes.*.Replication":                        true,
-       "Volumes.*.StorageClasses":                     true,
-       "Volumes.*.StorageClasses.*":                   false,
-       "Workbench":                                    true,
-       "Workbench.ActivationContactLink":              false,
-       "Workbench.APIClientConnectTimeout":            true,
-       "Workbench.APIClientReceiveTimeout":            true,
-       "Workbench.APIResponseCompression":             true,
-       "Workbench.ApplicationMimetypesWithViewIcon":   true,
-       "Workbench.ApplicationMimetypesWithViewIcon.*": true,
-       "Workbench.ArvadosDocsite":                     true,
-       "Workbench.ArvadosPublicDataDocURL":            true,
-       "Workbench.DefaultOpenIdPrefix":                false,
-       "Workbench.EnableGettingStartedPopup":          true,
-       "Workbench.EnablePublicProjectsPage":           true,
-       "Workbench.FileViewersConfigURL":               true,
-       "Workbench.IdleTimeout":                        true,
-       "Workbench.InactivePageHTML":                   true,
-       "Workbench.LogViewerMaxBytes":                  true,
-       "Workbench.MultiSiteSearch":                    true,
-       "Workbench.ProfilingEnabled":                   true,
-       "Workbench.Repositories":                       false,
-       "Workbench.RepositoryCache":                    false,
-       "Workbench.RunningJobLogRecordsToFetch":        true,
-       "Workbench.SecretKeyBase":                      false,
-       "Workbench.ShowRecentCollectionsOnDashboard":   true,
-       "Workbench.ShowUserAgreementInline":            true,
-       "Workbench.ShowUserNotifications":              true,
-       "Workbench.SiteName":                           true,
-       "Workbench.SSHHelpHostSuffix":                  true,
-       "Workbench.SSHHelpPageHTML":                    true,
-       "Workbench.Theme":                              true,
-       "Workbench.UserProfileFormFields":              true,
-       "Workbench.UserProfileFormFields.*":            true,
-       "Workbench.UserProfileFormFields.*.*":          true,
-       "Workbench.UserProfileFormFields.*.*.*":        true,
-       "Workbench.UserProfileFormMessage":             true,
-       "Workbench.VocabularyURL":                      true,
-       "Workbench.WelcomePageHTML":                    true,
+       "API":                                                 true,
+       "API.AsyncPermissionsUpdateInterval":                  false,
+       "API.DisabledAPIs":                                    false,
+       "API.KeepServiceRequestTimeout":                       false,
+       "API.MaxConcurrentRequests":                           false,
+       "API.MaxIndexDatabaseRead":                            false,
+       "API.MaxItemsPerResponse":                             true,
+       "API.MaxKeepBlobBuffers":                              false,
+       "API.MaxRequestAmplification":                         false,
+       "API.MaxRequestSize":                                  true,
+       "API.RequestTimeout":                                  true,
+       "API.SendTimeout":                                     true,
+       "API.WebsocketClientEventQueue":                       false,
+       "API.WebsocketServerEventQueue":                       false,
+       "AuditLogs":                                           false,
+       "AuditLogs.MaxAge":                                    false,
+       "AuditLogs.MaxDeleteBatch":                            false,
+       "AuditLogs.UnloggedAttributes":                        false,
+       "ClusterID":                                           true,
+       "Collections":                                         true,
+       "Collections.BalanceCollectionBatch":                  false,
+       "Collections.BalanceCollectionBuffers":                false,
+       "Collections.BalancePeriod":                           false,
+       "Collections.BalanceTimeout":                          false,
+       "Collections.BlobDeleteConcurrency":                   false,
+       "Collections.BlobMissingReport":                       false,
+       "Collections.BlobReplicateConcurrency":                false,
+       "Collections.BlobSigning":                             true,
+       "Collections.BlobSigningKey":                          false,
+       "Collections.BlobSigningTTL":                          true,
+       "Collections.BlobTrash":                               false,
+       "Collections.BlobTrashCheckInterval":                  false,
+       "Collections.BlobTrashConcurrency":                    false,
+       "Collections.BlobTrashLifetime":                       false,
+       "Collections.CollectionVersioning":                    false,
+       "Collections.DefaultReplication":                      true,
+       "Collections.DefaultTrashLifetime":                    true,
+       "Collections.ForwardSlashNameSubstitution":            true,
+       "Collections.ManagedProperties":                       true,
+       "Collections.ManagedProperties.*":                     true,
+       "Collections.ManagedProperties.*.*":                   true,
+       "Collections.PreserveVersionIfIdle":                   true,
+       "Collections.S3FolderObjects":                         true,
+       "Collections.TrashSweepInterval":                      false,
+       "Collections.TrustAllContent":                         false,
+       "Collections.WebDAVCache":                             false,
+       "Containers":                                          true,
+       "Containers.CloudVMs":                                 false,
+       "Containers.CrunchRunArgumentsList":                   false,
+       "Containers.CrunchRunCommand":                         false,
+       "Containers.DefaultKeepCacheRAM":                      true,
+       "Containers.DispatchPrivateKey":                       false,
+       "Containers.JobsAPI":                                  true,
+       "Containers.JobsAPI.Enable":                           true,
+       "Containers.JobsAPI.GitInternalDir":                   false,
+       "Containers.Logging":                                  false,
+       "Containers.LogReuseDecisions":                        false,
+       "Containers.MaxComputeVMs":                            false,
+       "Containers.MaxDispatchAttempts":                      false,
+       "Containers.MaxRetryAttempts":                         true,
+       "Containers.MinRetryPeriod":                           true,
+       "Containers.ReserveExtraRAM":                          true,
+       "Containers.ShellAccess":                              true,
+       "Containers.ShellAccess.Admin":                        true,
+       "Containers.ShellAccess.User":                         true,
+       "Containers.SLURM":                                    false,
+       "Containers.StaleLockTimeout":                         false,
+       "Containers.SupportedDockerImageFormats":              true,
+       "Containers.SupportedDockerImageFormats.*":            true,
+       "Containers.UsePreemptibleInstances":                  true,
+       "ForceLegacyAPI14":                                    false,
+       "Git":                                                 false,
+       "InstanceTypes":                                       true,
+       "InstanceTypes.*":                                     true,
+       "InstanceTypes.*.*":                                   true,
+       "Login":                                               true,
+       "Login.Google":                                        true,
+       "Login.Google.AlternateEmailAddresses":                false,
+       "Login.Google.AuthenticationRequestParameters":        false,
+       "Login.Google.ClientID":                               false,
+       "Login.Google.ClientSecret":                           false,
+       "Login.Google.Enable":                                 true,
+       "Login.LDAP":                                          true,
+       "Login.LDAP.AppendDomain":                             false,
+       "Login.LDAP.EmailAttribute":                           false,
+       "Login.LDAP.Enable":                                   true,
+       "Login.LDAP.InsecureTLS":                              false,
+       "Login.LDAP.SearchAttribute":                          false,
+       "Login.LDAP.SearchBase":                               false,
+       "Login.LDAP.SearchBindPassword":                       false,
+       "Login.LDAP.SearchBindUser":                           false,
+       "Login.LDAP.SearchFilters":                            false,
+       "Login.LDAP.StartTLS":                                 false,
+       "Login.LDAP.StripDomain":                              false,
+       "Login.LDAP.URL":                                      false,
+       "Login.LDAP.UsernameAttribute":                        false,
+       "Login.LoginCluster":                                  true,
+       "Login.OpenIDConnect":                                 true,
+       "Login.OpenIDConnect.AuthenticationRequestParameters": false,
+       "Login.OpenIDConnect.ClientID":                        false,
+       "Login.OpenIDConnect.ClientSecret":                    false,
+       "Login.OpenIDConnect.EmailClaim":                      false,
+       "Login.OpenIDConnect.EmailVerifiedClaim":              false,
+       "Login.OpenIDConnect.Enable":                          true,
+       "Login.OpenIDConnect.Issuer":                          false,
+       "Login.OpenIDConnect.UsernameClaim":                   false,
+       "Login.PAM":                                           true,
+       "Login.PAM.DefaultEmailDomain":                        false,
+       "Login.PAM.Enable":                                    true,
+       "Login.PAM.Service":                                   false,
+       "Login.RemoteTokenRefresh":                            true,
+       "Login.SSO":                                           true,
+       "Login.SSO.Enable":                                    true,
+       "Login.SSO.ProviderAppID":                             false,
+       "Login.SSO.ProviderAppSecret":                         false,
+       "Login.Test":                                          true,
+       "Login.Test.Enable":                                   true,
+       "Login.Test.Users":                                    false,
+       "Login.TokenLifetime":                                 false,
+       "Login.TrustedClients":                                false,
+       "Mail":                                                true,
+       "Mail.EmailFrom":                                      false,
+       "Mail.IssueReporterEmailFrom":                         false,
+       "Mail.IssueReporterEmailTo":                           false,
+       "Mail.MailchimpAPIKey":                                false,
+       "Mail.MailchimpListID":                                false,
+       "Mail.SendUserSetupNotificationEmail":                 false,
+       "Mail.SupportEmailAddress":                            true,
+       "ManagementToken":                                     false,
+       "PostgreSQL":                                          false,
+       "RemoteClusters":                                      true,
+       "RemoteClusters.*":                                    true,
+       "RemoteClusters.*.ActivateUsers":                      true,
+       "RemoteClusters.*.Host":                               true,
+       "RemoteClusters.*.Insecure":                           true,
+       "RemoteClusters.*.Proxy":                              true,
+       "RemoteClusters.*.Scheme":                             true,
+       "Services":                                            true,
+       "Services.*":                                          true,
+       "Services.*.ExternalURL":                              true,
+       "Services.*.InternalURLs":                             false,
+       "SystemLogs":                                          false,
+       "SystemRootToken":                                     false,
+       "TLS":                                                 false,
+       "Users":                                               true,
+       "Users.AdminNotifierEmailFrom":                        false,
+       "Users.AnonymousUserToken":                            true,
+       "Users.AutoAdminFirstUser":                            false,
+       "Users.AutoAdminUserWithEmail":                        false,
+       "Users.AutoSetupNewUsers":                             false,
+       "Users.AutoSetupNewUsersWithRepository":               false,
+       "Users.AutoSetupNewUsersWithVmUUID":                   false,
+       "Users.AutoSetupUsernameBlacklist":                    false,
+       "Users.EmailSubjectPrefix":                            false,
+       "Users.NewInactiveUserNotificationRecipients":         false,
+       "Users.NewUserNotificationRecipients":                 false,
+       "Users.NewUsersAreActive":                             false,
+       "Users.PreferDomainForUsername":                       false,
+       "Users.UserNotifierEmailFrom":                         false,
+       "Users.UserProfileNotificationAddress":                false,
+       "Users.UserSetupMailText":                             false,
+       "Volumes":                                             true,
+       "Volumes.*":                                           true,
+       "Volumes.*.*":                                         false,
+       "Volumes.*.AccessViaHosts":                            true,
+       "Volumes.*.AccessViaHosts.*":                          true,
+       "Volumes.*.AccessViaHosts.*.ReadOnly":                 true,
+       "Volumes.*.ReadOnly":                                  true,
+       "Volumes.*.Replication":                               true,
+       "Volumes.*.StorageClasses":                            true,
+       "Volumes.*.StorageClasses.*":                          false,
+       "Workbench":                                           true,
+       "Workbench.ActivationContactLink":                     false,
+       "Workbench.APIClientConnectTimeout":                   true,
+       "Workbench.APIClientReceiveTimeout":                   true,
+       "Workbench.APIResponseCompression":                    true,
+       "Workbench.ApplicationMimetypesWithViewIcon":          true,
+       "Workbench.ApplicationMimetypesWithViewIcon.*":        true,
+       "Workbench.ArvadosDocsite":                            true,
+       "Workbench.ArvadosPublicDataDocURL":                   true,
+       "Workbench.DefaultOpenIdPrefix":                       false,
+       "Workbench.EnableGettingStartedPopup":                 true,
+       "Workbench.EnablePublicProjectsPage":                  true,
+       "Workbench.FileViewersConfigURL":                      true,
+       "Workbench.IdleTimeout":                               true,
+       "Workbench.InactivePageHTML":                          true,
+       "Workbench.LogViewerMaxBytes":                         true,
+       "Workbench.MultiSiteSearch":                           true,
+       "Workbench.ProfilingEnabled":                          true,
+       "Workbench.Repositories":                              false,
+       "Workbench.RepositoryCache":                           false,
+       "Workbench.RunningJobLogRecordsToFetch":               true,
+       "Workbench.SecretKeyBase":                             false,
+       "Workbench.ShowRecentCollectionsOnDashboard":          true,
+       "Workbench.ShowUserAgreementInline":                   true,
+       "Workbench.ShowUserNotifications":                     true,
+       "Workbench.SiteName":                                  true,
+       "Workbench.SSHHelpHostSuffix":                         true,
+       "Workbench.SSHHelpPageHTML":                           true,
+       "Workbench.Theme":                                     true,
+       "Workbench.UserProfileFormFields":                     true,
+       "Workbench.UserProfileFormFields.*":                   true,
+       "Workbench.UserProfileFormFields.*.*":                 true,
+       "Workbench.UserProfileFormFields.*.*.*":               true,
+       "Workbench.UserProfileFormMessage":                    true,
+       "Workbench.VocabularyURL":                             true,
+       "Workbench.WelcomePageHTML":                           true,
 }
 
 func redactUnsafe(m map[string]interface{}, mPrefix, lookupPrefix string) error {
index a202a540476ed54a36097244005c24822503ccf2..8ef787771ebb9986f3b88f52ec69a6851d2eb8d2 100644 (file)
@@ -203,7 +203,7 @@ Clusters:
       # * 1.1) fits comfortably in memory. On a host dedicated to running
       # Keepstore, divide total memory by 88MiB to suggest a suitable value.
       # For example, if grep MemTotal /proc/meminfo reports MemTotal: 7125440
-      # kB, compute 7125440 / (88 * 1024)=79 and configure MaxBuffers: 79
+      # kB, compute 7125440 / (88 * 1024)=79 and set MaxKeepBlobBuffers: 79
       MaxKeepBlobBuffers: 128
 
       # API methods to disable. Disabled methods are not listed in the
@@ -573,6 +573,17 @@ Clusters:
         # work. If false, only the primary email address will be used.
         AlternateEmailAddresses: true
 
+        # Send additional parameters with authentication requests. See
+        # https://developers.google.com/identity/protocols/oauth2/openid-connect#authenticationuriparameters
+        # for a list of supported parameters.
+        AuthenticationRequestParameters:
+          # Show the "choose which Google account" page, even if the
+          # client is currently logged in to exactly one Google
+          # account.
+          prompt: select_account
+
+          SAMPLE: ""
+
       OpenIDConnect:
         # Authenticate with an OpenID Connect provider.
         Enable: false
@@ -607,6 +618,14 @@ Clusters:
         # address.
         UsernameClaim: ""
 
+        # Send additional parameters with authentication requests,
+        # like {display: page, prompt: consent}. See
+        # https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
+        # and refer to your provider's documentation for supported
+        # parameters.
+        AuthenticationRequestParameters:
+          SAMPLE: ""
+
       PAM:
         # (Experimental) Use PAM to authenticate users.
         Enable: false
@@ -874,6 +893,26 @@ Clusters:
         # period.
         LogUpdateSize: 32MiB
 
+      ShellAccess:
+        # An admin user can use "arvados-client shell" to start an
+        # interactive shell (with any user ID) in any running
+        # container.
+        Admin: false
+
+        # Any user can use "arvados-client shell" to start an
+        # interactive shell (with any user ID) in any running
+        # container that they started, provided it isn't also
+        # associated with a different user's container request.
+        #
+        # Interactive sessions make it easy to alter the container's
+        # runtime environment in ways that aren't recorded or
+        # reproducible. Consider the implications for automatic
+        # container reuse before enabling and using this feature. In
+        # particular, note that starting an interactive session does
+        # not disqualify a container from being reused by a different
+        # user/workflow in the future.
+        User: false
+
       SLURM:
         PrioritySpread: 0
         SbatchArgumentsList: []
index 00523c7826a74331ea8c1560013c40aebca86f3d..b86266d67e6f02c170deb631d32c777ecb072781 100644 (file)
@@ -336,6 +336,10 @@ func (conn *Conn) ContainerUnlock(ctx context.Context, options arvados.GetOption
        return conn.chooseBackend(options.UUID).ContainerUnlock(ctx, options)
 }
 
+func (conn *Conn) ContainerSSH(ctx context.Context, options arvados.ContainerSSHOptions) (arvados.ContainerSSHConnection, error) {
+       return conn.chooseBackend(options.UUID).ContainerSSH(ctx, options)
+}
+
 func (conn *Conn) ContainerRequestList(ctx context.Context, options arvados.ListOptions) (arvados.ContainerRequestList, error) {
        return conn.generated_ContainerRequestList(ctx, options)
 }
index b04757ac338fc1549f38daf960c07381803442dd..5f6fb192e1731a75b9052e9096c9a04dab6ddd99 100644 (file)
@@ -25,6 +25,7 @@ import (
        "git.arvados.org/arvados.git/sdk/go/health"
        "git.arvados.org/arvados.git/sdk/go/httpserver"
        "github.com/jmoiron/sqlx"
+
        // sqlx needs lib/pq to talk to PostgreSQL
        _ "github.com/lib/pq"
 )
@@ -100,6 +101,7 @@ func (h *Handler) setup() {
                mux.Handle("/arvados/v1/collections/", rtr)
                mux.Handle("/arvados/v1/users", rtr)
                mux.Handle("/arvados/v1/users/", rtr)
+               mux.Handle("/arvados/v1/connect/", rtr)
                mux.Handle("/arvados/v1/container_requests", rtr)
                mux.Handle("/arvados/v1/container_requests/", rtr)
                mux.Handle("/login", rtr)
diff --git a/lib/controller/localdb/container_gateway.go b/lib/controller/localdb/container_gateway.go
new file mode 100644 (file)
index 0000000..3b40ecc
--- /dev/null
@@ -0,0 +1,174 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package localdb
+
+import (
+       "bufio"
+       "context"
+       "crypto/hmac"
+       "crypto/sha256"
+       "crypto/tls"
+       "crypto/x509"
+       "errors"
+       "fmt"
+       "net/http"
+       "net/url"
+       "strings"
+
+       "git.arvados.org/arvados.git/sdk/go/arvados"
+       "git.arvados.org/arvados.git/sdk/go/auth"
+       "git.arvados.org/arvados.git/sdk/go/ctxlog"
+       "git.arvados.org/arvados.git/sdk/go/httpserver"
+)
+
+// ContainerSSH returns a connection to the SSH server in the
+// appropriate crunch-run process on the worker node where the
+// specified container is running.
+//
+// If the returned error is nil, the caller is responsible for closing
+// sshconn.Conn.
+func (conn *Conn) ContainerSSH(ctx context.Context, opts arvados.ContainerSSHOptions) (sshconn arvados.ContainerSSHConnection, err error) {
+       user, err := conn.railsProxy.UserGetCurrent(ctx, arvados.GetOptions{})
+       if err != nil {
+               return
+       }
+       ctr, err := conn.railsProxy.ContainerGet(ctx, arvados.GetOptions{UUID: opts.UUID})
+       if err != nil {
+               return
+       }
+       ctxRoot := auth.NewContext(ctx, &auth.Credentials{Tokens: []string{conn.cluster.SystemRootToken}})
+       if !user.IsAdmin || !conn.cluster.Containers.ShellAccess.Admin {
+               if !conn.cluster.Containers.ShellAccess.User {
+                       err = httpserver.ErrorWithStatus(errors.New("shell access is disabled in config"), http.StatusServiceUnavailable)
+                       return
+               }
+               var crs arvados.ContainerRequestList
+               crs, err = conn.railsProxy.ContainerRequestList(ctxRoot, arvados.ListOptions{Limit: -1, Filters: []arvados.Filter{{"container_uuid", "=", opts.UUID}}})
+               if err != nil {
+                       return
+               }
+               for _, cr := range crs.Items {
+                       if cr.ModifiedByUserUUID != user.UUID {
+                               err = httpserver.ErrorWithStatus(errors.New("permission denied: container is associated with requests submitted by other users"), http.StatusForbidden)
+                               return
+                       }
+               }
+               if crs.ItemsAvailable != len(crs.Items) {
+                       err = httpserver.ErrorWithStatus(errors.New("incomplete response while checking permission"), http.StatusInternalServerError)
+                       return
+               }
+       }
+
+       switch ctr.State {
+       case arvados.ContainerStateQueued, arvados.ContainerStateLocked:
+               err = httpserver.ErrorWithStatus(fmt.Errorf("container is not running yet (state is %q)", ctr.State), http.StatusServiceUnavailable)
+               return
+       case arvados.ContainerStateRunning:
+               if ctr.GatewayAddress == "" {
+                       err = httpserver.ErrorWithStatus(errors.New("container is running but gateway is not available -- installation problem or feature not supported"), http.StatusServiceUnavailable)
+                       return
+               }
+       default:
+               err = httpserver.ErrorWithStatus(fmt.Errorf("container has ended (state is %q)", ctr.State), http.StatusGone)
+               return
+       }
+       // crunch-run uses a self-signed / unverifiable TLS
+       // certificate, so we use the following scheme to ensure we're
+       // not talking to a MITM.
+       //
+       // 1. Compute ctrKey = HMAC-SHA256(sysRootToken,ctrUUID) --
+       // this will be the same ctrKey that a-d-c supplied to
+       // crunch-run in the GatewayAuthSecret env var.
+       //
+       // 2. Compute requestAuth = HMAC-SHA256(ctrKey,serverCert) and
+       // send it to crunch-run as the X-Arvados-Authorization
+       // header, proving that we know ctrKey. (Note a MITM cannot
+       // replay the proof to a real crunch-run server, because the
+       // real crunch-run server would have a different cert.)
+       //
+       // 3. Compute respondAuth = HMAC-SHA256(ctrKey,requestAuth)
+       // and ensure the server returns it in the
+       // X-Arvados-Authorization-Response header, proving that the
+       // server knows ctrKey.
+       var requestAuth, respondAuth string
+       netconn, err := tls.Dial("tcp", ctr.GatewayAddress, &tls.Config{
+               InsecureSkipVerify: true,
+               VerifyPeerCertificate: func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
+                       if len(rawCerts) == 0 {
+                               return errors.New("no certificate received, cannot compute authorization header")
+                       }
+                       h := hmac.New(sha256.New, []byte(conn.cluster.SystemRootToken))
+                       fmt.Fprint(h, opts.UUID)
+                       authKey := fmt.Sprintf("%x", h.Sum(nil))
+                       h = hmac.New(sha256.New, []byte(authKey))
+                       h.Write(rawCerts[0])
+                       requestAuth = fmt.Sprintf("%x", h.Sum(nil))
+                       h.Reset()
+                       h.Write([]byte(requestAuth))
+                       respondAuth = fmt.Sprintf("%x", h.Sum(nil))
+                       return nil
+               },
+       })
+       if err != nil {
+               err = httpserver.ErrorWithStatus(err, http.StatusBadGateway)
+               return
+       }
+       if respondAuth == "" {
+               err = httpserver.ErrorWithStatus(errors.New("BUG: no respondAuth"), http.StatusInternalServerError)
+               return
+       }
+       bufr := bufio.NewReader(netconn)
+       bufw := bufio.NewWriter(netconn)
+
+       u := url.URL{
+               Scheme: "http",
+               Host:   ctr.GatewayAddress,
+               Path:   "/ssh",
+       }
+       bufw.WriteString("GET " + u.String() + " HTTP/1.1\r\n")
+       bufw.WriteString("Host: " + u.Host + "\r\n")
+       bufw.WriteString("Upgrade: ssh\r\n")
+       bufw.WriteString("X-Arvados-Target-Uuid: " + opts.UUID + "\r\n")
+       bufw.WriteString("X-Arvados-Authorization: " + requestAuth + "\r\n")
+       bufw.WriteString("X-Arvados-Detach-Keys: " + opts.DetachKeys + "\r\n")
+       bufw.WriteString("X-Arvados-Login-Username: " + opts.LoginUsername + "\r\n")
+       bufw.WriteString("\r\n")
+       bufw.Flush()
+       resp, err := http.ReadResponse(bufr, &http.Request{Method: "GET"})
+       if err != nil {
+               err = httpserver.ErrorWithStatus(fmt.Errorf("error reading http response from gateway: %w", err), http.StatusBadGateway)
+               netconn.Close()
+               return
+       }
+       if resp.Header.Get("X-Arvados-Authorization-Response") != respondAuth {
+               err = httpserver.ErrorWithStatus(errors.New("bad X-Arvados-Authorization-Response header"), http.StatusBadGateway)
+               netconn.Close()
+               return
+       }
+       if strings.ToLower(resp.Header.Get("Upgrade")) != "ssh" ||
+               strings.ToLower(resp.Header.Get("Connection")) != "upgrade" {
+               err = httpserver.ErrorWithStatus(errors.New("bad upgrade"), http.StatusBadGateway)
+               netconn.Close()
+               return
+       }
+
+       if !ctr.InteractiveSessionStarted {
+               _, err = conn.railsProxy.ContainerUpdate(ctxRoot, arvados.UpdateOptions{
+                       UUID: opts.UUID,
+                       Attrs: map[string]interface{}{
+                               "interactive_session_started": true,
+                       },
+               })
+               if err != nil {
+                       netconn.Close()
+                       return
+               }
+       }
+
+       sshconn.Conn = netconn
+       sshconn.Bufrw = &bufio.ReadWriter{Reader: bufr, Writer: bufw}
+       sshconn.Logger = ctxlog.FromContext(ctx)
+       return
+}
diff --git a/lib/controller/localdb/container_gateway_test.go b/lib/controller/localdb/container_gateway_test.go
new file mode 100644 (file)
index 0000000..aff569b
--- /dev/null
@@ -0,0 +1,172 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package localdb
+
+import (
+       "context"
+       "crypto/hmac"
+       "crypto/sha256"
+       "fmt"
+       "io"
+       "time"
+
+       "git.arvados.org/arvados.git/lib/config"
+       "git.arvados.org/arvados.git/lib/crunchrun"
+       "git.arvados.org/arvados.git/sdk/go/arvados"
+       "git.arvados.org/arvados.git/sdk/go/arvadostest"
+       "git.arvados.org/arvados.git/sdk/go/auth"
+       "git.arvados.org/arvados.git/sdk/go/ctxlog"
+       check "gopkg.in/check.v1"
+)
+
+var _ = check.Suite(&ContainerGatewaySuite{})
+
+type ContainerGatewaySuite struct {
+       cluster *arvados.Cluster
+       localdb *Conn
+       ctx     context.Context
+       ctrUUID string
+       gw      *crunchrun.Gateway
+}
+
+func (s *ContainerGatewaySuite) TearDownSuite(c *check.C) {
+       // Undo any changes/additions to the user database so they
+       // don't affect subsequent tests.
+       arvadostest.ResetEnv()
+       c.Check(arvados.NewClientFromEnv().RequestAndDecode(nil, "POST", "database/reset", nil, nil), check.IsNil)
+}
+
+func (s *ContainerGatewaySuite) SetUpSuite(c *check.C) {
+       cfg, err := config.NewLoader(nil, ctxlog.TestLogger(c)).Load()
+       c.Assert(err, check.IsNil)
+       s.cluster, err = cfg.GetCluster("")
+       c.Assert(err, check.IsNil)
+       s.localdb = NewConn(s.cluster)
+       s.ctx = auth.NewContext(context.Background(), &auth.Credentials{Tokens: []string{arvadostest.ActiveTokenV2}})
+
+       s.ctrUUID = arvadostest.QueuedContainerUUID
+
+       h := hmac.New(sha256.New, []byte(s.cluster.SystemRootToken))
+       fmt.Fprint(h, s.ctrUUID)
+       authKey := fmt.Sprintf("%x", h.Sum(nil))
+
+       s.gw = &crunchrun.Gateway{
+               DockerContainerID: new(string),
+               ContainerUUID:     s.ctrUUID,
+               AuthSecret:        authKey,
+               Address:           "localhost:0",
+               Log:               ctxlog.TestLogger(c),
+       }
+       c.Assert(s.gw.Start(), check.IsNil)
+       rootctx := auth.NewContext(context.Background(), &auth.Credentials{Tokens: []string{s.cluster.SystemRootToken}})
+       _, err = s.localdb.ContainerUpdate(rootctx, arvados.UpdateOptions{
+               UUID: s.ctrUUID,
+               Attrs: map[string]interface{}{
+                       "state": arvados.ContainerStateLocked}})
+       c.Assert(err, check.IsNil)
+       _, err = s.localdb.ContainerUpdate(rootctx, arvados.UpdateOptions{
+               UUID: s.ctrUUID,
+               Attrs: map[string]interface{}{
+                       "state":           arvados.ContainerStateRunning,
+                       "gateway_address": s.gw.Address}})
+       c.Assert(err, check.IsNil)
+}
+
+func (s *ContainerGatewaySuite) SetUpTest(c *check.C) {
+       s.cluster.Containers.ShellAccess.Admin = true
+       s.cluster.Containers.ShellAccess.User = true
+       _, err := arvadostest.DB(c, s.cluster).Exec(`update containers set interactive_session_started=$1 where uuid=$2`, false, s.ctrUUID)
+       c.Check(err, check.IsNil)
+}
+
+func (s *ContainerGatewaySuite) TestConfig(c *check.C) {
+       for _, trial := range []struct {
+               configAdmin bool
+               configUser  bool
+               sendToken   string
+               errorCode   int
+       }{
+               {true, true, arvadostest.ActiveTokenV2, 0},
+               {true, false, arvadostest.ActiveTokenV2, 503},
+               {false, true, arvadostest.ActiveTokenV2, 0},
+               {false, false, arvadostest.ActiveTokenV2, 503},
+               {true, true, arvadostest.AdminToken, 0},
+               {true, false, arvadostest.AdminToken, 0},
+               {false, true, arvadostest.AdminToken, 403},
+               {false, false, arvadostest.AdminToken, 503},
+       } {
+               c.Logf("trial %#v", trial)
+               s.cluster.Containers.ShellAccess.Admin = trial.configAdmin
+               s.cluster.Containers.ShellAccess.User = trial.configUser
+               ctx := auth.NewContext(s.ctx, &auth.Credentials{Tokens: []string{trial.sendToken}})
+               sshconn, err := s.localdb.ContainerSSH(ctx, arvados.ContainerSSHOptions{UUID: s.ctrUUID})
+               if trial.errorCode == 0 {
+                       if !c.Check(err, check.IsNil) {
+                               continue
+                       }
+                       if !c.Check(sshconn.Conn, check.NotNil) {
+                               continue
+                       }
+                       sshconn.Conn.Close()
+               } else {
+                       c.Check(err, check.NotNil)
+                       err, ok := err.(interface{ HTTPStatus() int })
+                       if c.Check(ok, check.Equals, true) {
+                               c.Check(err.HTTPStatus(), check.Equals, trial.errorCode)
+                       }
+               }
+       }
+}
+
+func (s *ContainerGatewaySuite) TestConnect(c *check.C) {
+       c.Logf("connecting to %s", s.gw.Address)
+       sshconn, err := s.localdb.ContainerSSH(s.ctx, arvados.ContainerSSHOptions{UUID: s.ctrUUID})
+       c.Assert(err, check.IsNil)
+       c.Assert(sshconn.Conn, check.NotNil)
+       defer sshconn.Conn.Close()
+
+       done := make(chan struct{})
+       go func() {
+               defer close(done)
+
+               // Receive text banner
+               buf := make([]byte, 12)
+               _, err := io.ReadFull(sshconn.Conn, buf)
+               c.Check(err, check.IsNil)
+               c.Check(string(buf), check.Equals, "SSH-2.0-Go\r\n")
+
+               // Send text banner
+               _, err = sshconn.Conn.Write([]byte("SSH-2.0-Fake\r\n"))
+               c.Check(err, check.IsNil)
+
+               // Receive binary
+               _, err = io.ReadFull(sshconn.Conn, buf[:4])
+               c.Check(err, check.IsNil)
+               c.Check(buf[:4], check.DeepEquals, []byte{0, 0, 1, 0xfc})
+
+               // If we can get this far into an SSH handshake...
+               c.Log("success, tunnel is working")
+       }()
+       select {
+       case <-done:
+       case <-time.After(time.Second):
+               c.Fail()
+       }
+       ctr, err := s.localdb.ContainerGet(s.ctx, arvados.GetOptions{UUID: s.ctrUUID})
+       c.Check(err, check.IsNil)
+       c.Check(ctr.InteractiveSessionStarted, check.Equals, true)
+}
+
+func (s *ContainerGatewaySuite) TestConnectFail(c *check.C) {
+       c.Log("trying with no token")
+       ctx := auth.NewContext(context.Background(), &auth.Credentials{})
+       _, err := s.localdb.ContainerSSH(ctx, arvados.ContainerSSHOptions{UUID: s.ctrUUID})
+       c.Check(err, check.ErrorMatches, `.* 401 .*`)
+
+       c.Log("trying with anonymous token")
+       ctx = auth.NewContext(context.Background(), &auth.Credentials{Tokens: []string{arvadostest.AnonymousToken}})
+       _, err = s.localdb.ContainerSSH(ctx, arvados.ContainerSSHOptions{UUID: s.ctrUUID})
+       c.Check(err, check.ErrorMatches, `.* 404 .*`)
+}
index 4bf515fc3f1113b6a240e991ecc3e236c8b49ee3..4e76b176d99a9c7846a3c0cae55c37e13c031573 100644 (file)
@@ -47,6 +47,7 @@ func chooseLoginController(cluster *arvados.Cluster, parent *Conn) loginControll
                        Issuer:             "https://accounts.google.com",
                        ClientID:           cluster.Login.Google.ClientID,
                        ClientSecret:       cluster.Login.Google.ClientSecret,
+                       AuthParams:         cluster.Login.Google.AuthenticationRequestParameters,
                        UseGooglePeopleAPI: cluster.Login.Google.AlternateEmailAddresses,
                        EmailClaim:         "email",
                        EmailVerifiedClaim: "email_verified",
@@ -58,6 +59,7 @@ func chooseLoginController(cluster *arvados.Cluster, parent *Conn) loginControll
                        Issuer:             cluster.Login.OpenIDConnect.Issuer,
                        ClientID:           cluster.Login.OpenIDConnect.ClientID,
                        ClientSecret:       cluster.Login.OpenIDConnect.ClientSecret,
+                       AuthParams:         cluster.Login.OpenIDConnect.AuthenticationRequestParameters,
                        EmailClaim:         cluster.Login.OpenIDConnect.EmailClaim,
                        EmailVerifiedClaim: cluster.Login.OpenIDConnect.EmailVerifiedClaim,
                        UsernameClaim:      cluster.Login.OpenIDConnect.UsernameClaim,
index a5fe45181b3319c0b07b881f747719762dcabb8a..2b67a95046620c00621de88017e29124e273b5a4 100644 (file)
@@ -50,10 +50,11 @@ type oidcLoginController struct {
        Issuer             string // OIDC issuer URL, e.g., "https://accounts.google.com"
        ClientID           string
        ClientSecret       string
-       UseGooglePeopleAPI bool   // Use Google People API to look up alternate email addresses
-       EmailClaim         string // OpenID claim to use as email address; typically "email"
-       EmailVerifiedClaim string // If non-empty, ensure claim value is true before accepting EmailClaim; typically "email_verified"
-       UsernameClaim      string // If non-empty, use as preferred username
+       UseGooglePeopleAPI bool              // Use Google People API to look up alternate email addresses
+       EmailClaim         string            // OpenID claim to use as email address; typically "email"
+       EmailVerifiedClaim string            // If non-empty, ensure claim value is true before accepting EmailClaim; typically "email_verified"
+       UsernameClaim      string            // If non-empty, use as preferred username
+       AuthParams         map[string]string // Additional parameters to pass with authentication request
 
        // override Google People API base URL for testing purposes
        // (normally empty, set by google pkg to
@@ -111,14 +112,12 @@ func (ctrl *oidcLoginController) Login(ctx context.Context, opts arvados.LoginOp
                        return loginError(errors.New("missing return_to parameter"))
                }
                state := ctrl.newOAuth2State([]byte(ctrl.Cluster.SystemRootToken), opts.Remote, opts.ReturnTo)
+               var authparams []oauth2.AuthCodeOption
+               for k, v := range ctrl.AuthParams {
+                       authparams = append(authparams, oauth2.SetAuthURLParam(k, v))
+               }
                return arvados.LoginResponse{
-                       RedirectLocation: ctrl.oauth2conf.AuthCodeURL(state.String(),
-                               // prompt=select_account tells Google
-                               // to show the "choose which Google
-                               // account" page, even if the client
-                               // is currently logged in to exactly
-                               // one Google account.
-                               oauth2.SetAuthURLParam("prompt", "select_account")),
+                       RedirectLocation: ctrl.oauth2conf.AuthCodeURL(state.String(), authparams...),
                }, nil
        }
        // Callback after OIDC sign-in.
index e157b73fc6d25ed158d25703e6e4bb961007932f..e3c72adddcdbbf76650fada2a8eb8401add88431 100644 (file)
@@ -165,12 +165,14 @@ func (s *OIDCLoginSuite) TestConfig(c *check.C) {
        s.cluster.Login.OpenIDConnect.Issuer = "https://accounts.example.com/"
        s.cluster.Login.OpenIDConnect.ClientID = "oidc-client-id"
        s.cluster.Login.OpenIDConnect.ClientSecret = "oidc-client-secret"
+       s.cluster.Login.OpenIDConnect.AuthenticationRequestParameters = map[string]string{"testkey": "testvalue"}
        localdb := NewConn(s.cluster)
        ctrl := localdb.loginController.(*oidcLoginController)
        c.Check(ctrl.Issuer, check.Equals, "https://accounts.example.com/")
        c.Check(ctrl.ClientID, check.Equals, "oidc-client-id")
        c.Check(ctrl.ClientSecret, check.Equals, "oidc-client-secret")
        c.Check(ctrl.UseGooglePeopleAPI, check.Equals, false)
+       c.Check(ctrl.AuthParams["testkey"], check.Equals, "testvalue")
 
        for _, enableAltEmails := range []bool{false, true} {
                s.cluster.Login.OpenIDConnect.Enable = false
@@ -178,12 +180,14 @@ func (s *OIDCLoginSuite) TestConfig(c *check.C) {
                s.cluster.Login.Google.ClientID = "google-client-id"
                s.cluster.Login.Google.ClientSecret = "google-client-secret"
                s.cluster.Login.Google.AlternateEmailAddresses = enableAltEmails
+               s.cluster.Login.Google.AuthenticationRequestParameters = map[string]string{"testkey": "testvalue"}
                localdb = NewConn(s.cluster)
                ctrl = localdb.loginController.(*oidcLoginController)
                c.Check(ctrl.Issuer, check.Equals, "https://accounts.google.com")
                c.Check(ctrl.ClientID, check.Equals, "google-client-id")
                c.Check(ctrl.ClientSecret, check.Equals, "google-client-secret")
                c.Check(ctrl.UseGooglePeopleAPI, check.Equals, enableAltEmails)
+               c.Check(ctrl.AuthParams["testkey"], check.Equals, "testvalue")
        }
 }
 
@@ -260,6 +264,7 @@ func (s *OIDCLoginSuite) TestGenericOIDCLogin(c *check.C) {
        json.Unmarshal([]byte(fmt.Sprintf("%q", s.fakeProvider.Issuer.URL)), &s.cluster.Login.OpenIDConnect.Issuer)
        s.cluster.Login.OpenIDConnect.ClientID = "oidc#client#id"
        s.cluster.Login.OpenIDConnect.ClientSecret = "oidc#client#secret"
+       s.cluster.Login.OpenIDConnect.AuthenticationRequestParameters = map[string]string{"testkey": "testvalue"}
        s.fakeProvider.ValidClientID = "oidc#client#id"
        s.fakeProvider.ValidClientSecret = "oidc#client#secret"
        for _, trial := range []struct {
@@ -319,7 +324,9 @@ func (s *OIDCLoginSuite) TestGenericOIDCLogin(c *check.C) {
                s.localdb = NewConn(s.cluster)
                *s.localdb.railsProxy = *rpc.NewConn(s.cluster.ClusterID, s.railsSpy.URL, true, rpc.PassthroughTokenProvider)
 
-               state := s.startLogin(c)
+               state := s.startLogin(c, func(form url.Values) {
+                       c.Check(form.Get("testkey"), check.Equals, "testvalue")
+               })
                resp, err := s.localdb.Login(context.Background(), arvados.LoginOptions{
                        Code:  s.fakeProvider.ValidCode,
                        State: state,
@@ -350,7 +357,12 @@ func (s *OIDCLoginSuite) TestGenericOIDCLogin(c *check.C) {
 }
 
 func (s *OIDCLoginSuite) TestGoogleLogin_Success(c *check.C) {
-       state := s.startLogin(c)
+       s.cluster.Login.Google.AuthenticationRequestParameters["prompt"] = "consent"
+       s.cluster.Login.Google.AuthenticationRequestParameters["foo"] = "bar"
+       state := s.startLogin(c, func(form url.Values) {
+               c.Check(form.Get("foo"), check.Equals, "bar")
+               c.Check(form.Get("prompt"), check.Equals, "consent")
+       })
        resp, err := s.localdb.Login(context.Background(), arvados.LoginOptions{
                Code:  s.fakeProvider.ValidCode,
                State: state,
@@ -515,7 +527,7 @@ func (s *OIDCLoginSuite) TestGoogleLogin_NoPrimaryEmailAddress(c *check.C) {
        c.Check(authinfo.Username, check.Equals, "")
 }
 
-func (s *OIDCLoginSuite) startLogin(c *check.C) (state string) {
+func (s *OIDCLoginSuite) startLogin(c *check.C, checks ...func(url.Values)) (state string) {
        // Initiate login, but instead of following the redirect to
        // the provider, just grab state from the redirect URL.
        resp, err := s.localdb.Login(context.Background(), arvados.LoginOptions{ReturnTo: "https://app.example.com/foo?bar"})
@@ -524,6 +536,10 @@ func (s *OIDCLoginSuite) startLogin(c *check.C) (state string) {
        c.Check(err, check.IsNil)
        state = target.Query().Get("state")
        c.Check(state, check.Not(check.Equals), "")
+       for _, fn := range checks {
+               fn(target.Query())
+       }
+       s.cluster.Login.OpenIDConnect.AuthenticationRequestParameters = map[string]string{"testkey": "testvalue"}
        return
 }
 
index 9fb2a0d32b49b5af5c6337cc2187c94b7d86a991..83c89d322ab3d85aa31fff177b92115efa469ed8 100644 (file)
@@ -221,6 +221,13 @@ func (rtr *router) addRoutes() {
                                return rtr.backend.ContainerUnlock(ctx, *opts.(*arvados.GetOptions))
                        },
                },
+               {
+                       arvados.EndpointContainerSSH,
+                       func() interface{} { return &arvados.ContainerSSHOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.backend.ContainerSSH(ctx, *opts.(*arvados.ContainerSSHOptions))
+                       },
+               },
                {
                        arvados.EndpointSpecimenCreate,
                        func() interface{} { return &arvados.CreateOptions{} },
index d9d24260bbc98e617b62083f2bb3dd899d59995a..3a19f4ab5ad50d2ad5ceb5fbdf9981108bf1213f 100644 (file)
@@ -5,6 +5,7 @@
 package rpc
 
 import (
+       "bufio"
        "bytes"
        "context"
        "crypto/tls"
@@ -12,6 +13,7 @@ import (
        "errors"
        "fmt"
        "io"
+       "io/ioutil"
        "net"
        "net/http"
        "net/url"
@@ -21,6 +23,7 @@ import (
 
        "git.arvados.org/arvados.git/sdk/go/arvados"
        "git.arvados.org/arvados.git/sdk/go/auth"
+       "git.arvados.org/arvados.git/sdk/go/httpserver"
 )
 
 const rfc3339NanoFixed = "2006-01-02T15:04:05.000000000Z07:00"
@@ -302,6 +305,82 @@ func (conn *Conn) ContainerUnlock(ctx context.Context, options arvados.GetOption
        return resp, err
 }
 
+// ContainerSSH returns a connection to the out-of-band SSH server for
+// a running container. If the returned error is nil, the caller is
+// responsible for closing sshconn.Conn.
+func (conn *Conn) ContainerSSH(ctx context.Context, options arvados.ContainerSSHOptions) (sshconn arvados.ContainerSSHConnection, err error) {
+       addr := conn.baseURL.Host
+       if strings.Index(addr, ":") < 1 || (strings.Contains(addr, "::") && addr[0] != '[') {
+               // hostname or ::1 or 1::1
+               addr = net.JoinHostPort(addr, "https")
+       }
+       insecure := false
+       if tlsconf := conn.httpClient.Transport.(*http.Transport).TLSClientConfig; tlsconf != nil && tlsconf.InsecureSkipVerify {
+               insecure = true
+       }
+       netconn, err := tls.Dial("tcp", addr, &tls.Config{InsecureSkipVerify: insecure})
+       if err != nil {
+               err = fmt.Errorf("tls.Dial: %w", err)
+               return
+       }
+       defer func() {
+               if err != nil {
+                       netconn.Close()
+               }
+       }()
+       bufr := bufio.NewReader(netconn)
+       bufw := bufio.NewWriter(netconn)
+
+       u, err := conn.baseURL.Parse("/" + strings.Replace(arvados.EndpointContainerSSH.Path, "{uuid}", options.UUID, -1))
+       if err != nil {
+               err = fmt.Errorf("tls.Dial: %w", err)
+               return
+       }
+       u.RawQuery = url.Values{
+               "detach_keys":    {options.DetachKeys},
+               "login_username": {options.LoginUsername},
+       }.Encode()
+       tokens, err := conn.tokenProvider(ctx)
+       if err != nil {
+               return
+       } else if len(tokens) < 1 {
+               err = httpserver.ErrorWithStatus(errors.New("unauthorized"), http.StatusUnauthorized)
+               return
+       }
+       bufw.WriteString("GET " + u.String() + " HTTP/1.1\r\n")
+       bufw.WriteString("Authorization: Bearer " + tokens[0] + "\r\n")
+       bufw.WriteString("Host: " + u.Host + "\r\n")
+       bufw.WriteString("Upgrade: ssh\r\n")
+       bufw.WriteString("\r\n")
+       bufw.Flush()
+       resp, err := http.ReadResponse(bufr, &http.Request{Method: "GET"})
+       if err != nil {
+               err = fmt.Errorf("http.ReadResponse: %w", err)
+               return
+       }
+       if resp.StatusCode != http.StatusSwitchingProtocols {
+               defer resp.Body.Close()
+               body, _ := ioutil.ReadAll(resp.Body)
+               var message string
+               var errDoc httpserver.ErrorResponse
+               if err := json.Unmarshal(body, &errDoc); err == nil {
+                       message = strings.Join(errDoc.Errors, "; ")
+               } else {
+                       message = fmt.Sprintf("%q", body)
+               }
+               err = fmt.Errorf("server did not provide a tunnel: %s (HTTP %d)", message, resp.StatusCode)
+               return
+       }
+       if strings.ToLower(resp.Header.Get("Upgrade")) != "ssh" ||
+               strings.ToLower(resp.Header.Get("Connection")) != "upgrade" {
+               err = fmt.Errorf("bad response from server: Upgrade %q Connection %q", resp.Header.Get("Upgrade"), resp.Header.Get("Connection"))
+               return
+       }
+       sshconn.Conn = netconn
+       sshconn.Bufrw = &bufio.ReadWriter{Reader: bufr, Writer: bufw}
+       return
+}
+
 func (conn *Conn) ContainerRequestCreate(ctx context.Context, options arvados.CreateOptions) (arvados.ContainerRequest, error) {
        ep := arvados.EndpointContainerRequestCreate
        var resp arvados.ContainerRequest
index e8dd186050fb200ca575a480ff6d15db1d88adf0..37e655e53a3391b8820a3855efeb2ca891dcd5f8 100644 (file)
@@ -35,6 +35,7 @@ type nodeInfo struct {
        // Modern
        ProviderType string
        Price        float64
+       Preemptible  bool
 }
 
 type arrayFlags []string
@@ -90,6 +91,12 @@ Usage:
        was fulfilled. This program uses the cost data stored at the time of the
        execution of the container, stored in the 'node.json' file in its log
        collection.
+       - if a container was run on a preemptible ("spot") instance, the cost data
+       reported by this program may be wildly inaccurate, because it does not have
+       access to the spot pricing in effect for the node then the container ran. The
+       UUID report file that is generated when the '-output' option is specified has
+       a column that indicates the preemptible state of the instance that ran the
+       container.
 
        In order to get the data for the uuids supplied, the ARVADOS_API_HOST and
        ARVADOS_API_TOKEN environment variables must be set.
@@ -181,7 +188,7 @@ func addContainerLine(logger *logrus.Logger, node nodeInfo, cr arvados.Container
                size = node.ProviderType
        }
        cost = delta.Seconds() / 3600 * price
-       csv += size + "," + strconv.FormatFloat(price, 'f', 8, 64) + "," + strconv.FormatFloat(cost, 'f', 8, 64) + "\n"
+       csv += size + "," + fmt.Sprintf("%+v", node.Preemptible) + "," + strconv.FormatFloat(price, 'f', 8, 64) + "," + strconv.FormatFloat(cost, 'f', 8, 64) + "\n"
        return
 }
 
@@ -369,7 +376,7 @@ func generateCrCsv(logger *logrus.Logger, uuid string, arv *arvadosclient.Arvado
 
        cost = make(map[string]float64)
 
-       csv := "CR UUID,CR name,Container UUID,State,Started At,Finished At,Duration in seconds,Compute node type,Hourly node cost,Total cost\n"
+       csv := "CR UUID,CR name,Container UUID,State,Started At,Finished At,Duration in seconds,Compute node type,Preemptible,Hourly node cost,Total cost\n"
        var tmpCsv string
        var tmpTotalCost float64
        var totalCost float64
index b1ddf97a36af2c3a7a18de7f4ef7e5a865b60199..f4d8d1073068651d8e0f0c0bc72eb91316108763 100644 (file)
@@ -53,7 +53,7 @@ func (s *Suite) SetUpSuite(c *check.C) {
     "IncludedScratch": 64000000000,
     "AddedScratch": 0,
     "Price": 0.292,
-    "Preemptible": false
+    "Preemptible": true
 }`
        standardD32sV3JSON := `{
     "Name": "Standard_D32s_v3",
@@ -168,6 +168,8 @@ func (*Suite) TestContainerRequestUUID(c *check.C) {
 
        uuidReport, err := ioutil.ReadFile(resultsDir + "/" + arvadostest.CompletedContainerRequestUUID + ".csv")
        c.Assert(err, check.IsNil)
+       // Make sure the 'preemptible' flag was picked up
+       c.Check(string(uuidReport), check.Matches, "(?ms).*,Standard_E4s_v3,true,.*")
        c.Check(string(uuidReport), check.Matches, "(?ms).*TOTAL,,,,,,,,,7.01302889")
        re := regexp.MustCompile(`(?ms).*supplied uuids in (.*?)\n`)
        matches := re.FindStringSubmatch(stderr.String()) // matches[1] contains a string like 'results/2020-11-02-18-57-45-aggregate-costaccounting.csv'
diff --git a/lib/crunchrun/container_gateway.go b/lib/crunchrun/container_gateway.go
new file mode 100644 (file)
index 0000000..1116c4b
--- /dev/null
@@ -0,0 +1,335 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package crunchrun
+
+import (
+       "crypto/hmac"
+       "crypto/rand"
+       "crypto/rsa"
+       "crypto/sha256"
+       "crypto/tls"
+       "fmt"
+       "io"
+       "net"
+       "net/http"
+       "os"
+       "os/exec"
+       "sync"
+       "syscall"
+
+       "git.arvados.org/arvados.git/lib/selfsigned"
+       "git.arvados.org/arvados.git/sdk/go/httpserver"
+       "github.com/creack/pty"
+       "github.com/google/shlex"
+       "golang.org/x/crypto/ssh"
+)
+
+type Gateway struct {
+       DockerContainerID *string
+       ContainerUUID     string
+       Address           string // listen host:port; if port=0, Start() will change it to the selected port
+       AuthSecret        string
+       Log               interface {
+               Printf(fmt string, args ...interface{})
+       }
+
+       sshConfig   ssh.ServerConfig
+       requestAuth string
+       respondAuth string
+}
+
+// startGatewayServer starts an http server that allows authenticated
+// clients to open an interactive "docker exec" session and (in
+// future) connect to tcp ports inside the docker container.
+func (gw *Gateway) Start() error {
+       gw.sshConfig = ssh.ServerConfig{
+               NoClientAuth: true,
+               PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
+                       if c.User() == "_" {
+                               return nil, nil
+                       } else {
+                               return nil, fmt.Errorf("cannot specify user %q via ssh client", c.User())
+                       }
+               },
+               PublicKeyCallback: func(c ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) {
+                       if c.User() == "_" {
+                               return &ssh.Permissions{
+                                       Extensions: map[string]string{
+                                               "pubkey-fp": ssh.FingerprintSHA256(pubKey),
+                                       },
+                               }, nil
+                       } else {
+                               return nil, fmt.Errorf("cannot specify user %q via ssh client", c.User())
+                       }
+               },
+       }
+       pvt, err := rsa.GenerateKey(rand.Reader, 2048)
+       if err != nil {
+               return err
+       }
+       err = pvt.Validate()
+       if err != nil {
+               return err
+       }
+       signer, err := ssh.NewSignerFromKey(pvt)
+       if err != nil {
+               return err
+       }
+       gw.sshConfig.AddHostKey(signer)
+
+       // Address (typically provided by arvados-dispatch-cloud) is
+       // HOST:PORT where HOST is our IP address or hostname as seen
+       // from arvados-controller, and PORT is either the desired
+       // port where we should run our gateway server, or "0" if we
+       // should choose an available port.
+       host, port, err := net.SplitHostPort(gw.Address)
+       if err != nil {
+               return err
+       }
+       cert, err := selfsigned.CertGenerator{}.Generate()
+       if err != nil {
+               return err
+       }
+       h := hmac.New(sha256.New, []byte(gw.AuthSecret))
+       h.Write(cert.Certificate[0])
+       gw.requestAuth = fmt.Sprintf("%x", h.Sum(nil))
+       h.Reset()
+       h.Write([]byte(gw.requestAuth))
+       gw.respondAuth = fmt.Sprintf("%x", h.Sum(nil))
+
+       srv := &httpserver.Server{
+               Server: http.Server{
+                       Handler: http.HandlerFunc(gw.handleSSH),
+                       TLSConfig: &tls.Config{
+                               Certificates: []tls.Certificate{cert},
+                       },
+               },
+               Addr: ":" + port,
+       }
+       err = srv.Start()
+       if err != nil {
+               return err
+       }
+       // Get the port number we are listening on (the port might be
+       // "0" or a port name, in which case this will be different).
+       _, port, err = net.SplitHostPort(srv.Addr)
+       if err != nil {
+               return err
+       }
+       // When changing state to Running, we will set
+       // gateway_address to "HOST:PORT" where HOST is our
+       // external hostname/IP as provided by arvados-dispatch-cloud,
+       // and PORT is the port number we ended up listening on.
+       gw.Address = net.JoinHostPort(host, port)
+       return nil
+}
+
+// handleSSH connects to an SSH server that allows the caller to run
+// interactive commands as root (or any other desired user) inside the
+// container. The tunnel itself can only be created by an
+// authenticated caller, so the SSH server itself is wide open (any
+// password or key will be accepted).
+//
+// Requests must have path "/ssh" and the following headers:
+//
+// Connection: upgrade
+// Upgrade: ssh
+// X-Arvados-Target-Uuid: uuid of container
+// X-Arvados-Authorization: must match
+// hmac(AuthSecret,certfingerprint) (this prevents other containers
+// and shell nodes from connecting directly)
+//
+// Optional headers:
+//
+// X-Arvados-Detach-Keys: argument to "docker exec --detach-keys",
+// e.g., "ctrl-p,ctrl-q"
+// X-Arvados-Login-Username: argument to "docker exec --user": account
+// used to run command(s) inside the container.
+func (gw *Gateway) handleSSH(w http.ResponseWriter, req *http.Request) {
+       // In future we'll handle browser traffic too, but for now the
+       // only traffic we expect is an SSH tunnel from
+       // (*lib/controller/localdb.Conn)ContainerSSH()
+       if req.Method != "GET" || req.Header.Get("Upgrade") != "ssh" {
+               http.Error(w, "path not found", http.StatusNotFound)
+               return
+       }
+       if want := req.Header.Get("X-Arvados-Target-Uuid"); want != gw.ContainerUUID {
+               http.Error(w, fmt.Sprintf("misdirected request: meant for %q but received by crunch-run %q", want, gw.ContainerUUID), http.StatusBadGateway)
+               return
+       }
+       if req.Header.Get("X-Arvados-Authorization") != gw.requestAuth {
+               http.Error(w, "bad X-Arvados-Authorization header", http.StatusUnauthorized)
+               return
+       }
+       detachKeys := req.Header.Get("X-Arvados-Detach-Keys")
+       username := req.Header.Get("X-Arvados-Login-Username")
+       if username == "" {
+               username = "root"
+       }
+       hj, ok := w.(http.Hijacker)
+       if !ok {
+               http.Error(w, "ResponseWriter does not support connection upgrade", http.StatusInternalServerError)
+               return
+       }
+       netconn, _, err := hj.Hijack()
+       if !ok {
+               http.Error(w, err.Error(), http.StatusInternalServerError)
+               return
+       }
+       defer netconn.Close()
+       w.Header().Set("Connection", "upgrade")
+       w.Header().Set("Upgrade", "ssh")
+       w.Header().Set("X-Arvados-Authorization-Response", gw.respondAuth)
+       netconn.Write([]byte("HTTP/1.1 101 Switching Protocols\r\n"))
+       w.Header().Write(netconn)
+       netconn.Write([]byte("\r\n"))
+
+       ctx := req.Context()
+
+       conn, newchans, reqs, err := ssh.NewServerConn(netconn, &gw.sshConfig)
+       if err != nil {
+               gw.Log.Printf("ssh.NewServerConn: %s", err)
+               return
+       }
+       defer conn.Close()
+       go ssh.DiscardRequests(reqs)
+       for newch := range newchans {
+               if newch.ChannelType() != "session" {
+                       newch.Reject(ssh.UnknownChannelType, fmt.Sprintf("unsupported channel type %q", newch.ChannelType()))
+                       continue
+               }
+               ch, reqs, err := newch.Accept()
+               if err != nil {
+                       gw.Log.Printf("accept channel: %s", err)
+                       return
+               }
+               var pty0, tty0 *os.File
+               go func() {
+                       // Where to send errors/messages for the
+                       // client to see
+                       logw := io.Writer(ch.Stderr())
+                       // How to end lines when sending
+                       // errors/messages to the client (changes to
+                       // \r\n when using a pty)
+                       eol := "\n"
+                       // Env vars to add to child process
+                       termEnv := []string(nil)
+                       for req := range reqs {
+                               ok := false
+                               switch req.Type {
+                               case "shell", "exec":
+                                       ok = true
+                                       var payload struct {
+                                               Command string
+                                       }
+                                       ssh.Unmarshal(req.Payload, &payload)
+                                       execargs, err := shlex.Split(payload.Command)
+                                       if err != nil {
+                                               fmt.Fprintf(logw, "error parsing supplied command: %s"+eol, err)
+                                               return
+                                       }
+                                       if len(execargs) == 0 {
+                                               execargs = []string{"/bin/bash", "-login"}
+                                       }
+                                       go func() {
+                                               cmd := exec.CommandContext(ctx, "docker", "exec", "-i", "--detach-keys="+detachKeys, "--user="+username)
+                                               cmd.Stdin = ch
+                                               cmd.Stdout = ch
+                                               cmd.Stderr = ch.Stderr()
+                                               if tty0 != nil {
+                                                       cmd.Args = append(cmd.Args, "-t")
+                                                       cmd.Stdin = tty0
+                                                       cmd.Stdout = tty0
+                                                       cmd.Stderr = tty0
+                                                       var wg sync.WaitGroup
+                                                       defer wg.Wait()
+                                                       wg.Add(2)
+                                                       go func() { io.Copy(ch, pty0); wg.Done() }()
+                                                       go func() { io.Copy(pty0, ch); wg.Done() }()
+                                                       // Send our own debug messages to tty as well.
+                                                       logw = tty0
+                                               }
+                                               cmd.Args = append(cmd.Args, *gw.DockerContainerID)
+                                               cmd.Args = append(cmd.Args, execargs...)
+                                               cmd.SysProcAttr = &syscall.SysProcAttr{
+                                                       Setctty: tty0 != nil,
+                                                       Setsid:  true,
+                                               }
+                                               cmd.Env = append(os.Environ(), termEnv...)
+                                               err := cmd.Run()
+                                               var resp struct {
+                                                       Status uint32
+                                               }
+                                               if exiterr, ok := err.(*exec.ExitError); ok {
+                                                       if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
+                                                               resp.Status = uint32(status.ExitStatus())
+                                                       }
+                                               } else if err != nil {
+                                                       // Propagate errors like `exec: "docker": executable file not found in $PATH`
+                                                       fmt.Fprintln(ch.Stderr(), err)
+                                               }
+                                               errClose := ch.CloseWrite()
+                                               if resp.Status == 0 && (err != nil || errClose != nil) {
+                                                       resp.Status = 1
+                                               }
+                                               ch.SendRequest("exit-status", false, ssh.Marshal(&resp))
+                                               ch.Close()
+                                       }()
+                               case "pty-req":
+                                       eol = "\r\n"
+                                       p, t, err := pty.Open()
+                                       if err != nil {
+                                               fmt.Fprintf(ch.Stderr(), "pty failed: %s"+eol, err)
+                                               break
+                                       }
+                                       defer p.Close()
+                                       defer t.Close()
+                                       pty0, tty0 = p, t
+                                       ok = true
+                                       var payload struct {
+                                               Term string
+                                               Cols uint32
+                                               Rows uint32
+                                               X    uint32
+                                               Y    uint32
+                                       }
+                                       ssh.Unmarshal(req.Payload, &payload)
+                                       termEnv = []string{"TERM=" + payload.Term, "USE_TTY=1"}
+                                       err = pty.Setsize(pty0, &pty.Winsize{Rows: uint16(payload.Rows), Cols: uint16(payload.Cols), X: uint16(payload.X), Y: uint16(payload.Y)})
+                                       if err != nil {
+                                               fmt.Fprintf(logw, "pty-req: setsize failed: %s"+eol, err)
+                                       }
+                               case "window-change":
+                                       var payload struct {
+                                               Cols uint32
+                                               Rows uint32
+                                               X    uint32
+                                               Y    uint32
+                                       }
+                                       ssh.Unmarshal(req.Payload, &payload)
+                                       err := pty.Setsize(pty0, &pty.Winsize{Rows: uint16(payload.Rows), Cols: uint16(payload.Cols), X: uint16(payload.X), Y: uint16(payload.Y)})
+                                       if err != nil {
+                                               fmt.Fprintf(logw, "window-change: setsize failed: %s"+eol, err)
+                                               break
+                                       }
+                                       ok = true
+                               case "env":
+                                       // TODO: implement "env"
+                                       // requests by setting env
+                                       // vars in the docker-exec
+                                       // command (not docker-exec's
+                                       // own environment, which
+                                       // would be a gaping security
+                                       // hole).
+                               default:
+                                       // fmt.Fprintf(logw, "declining %q req"+eol, req.Type)
+                               }
+                               if req.WantReply {
+                                       req.Reply(ok, nil)
+                               }
+                       }
+               }()
+       }
+}
index 6e7e6feba5715bdd6c3528f9f97c328826036647..7d6fb4ed47bef547f4eb3cb1728163c77021bf02 100644 (file)
@@ -178,6 +178,8 @@ type ContainerRunner struct {
        arvMountLog   *ThrottledLogger
 
        containerWatchdogInterval time.Duration
+
+       gateway Gateway
 }
 
 // setupSignals sets up signal handling to gracefully terminate the underlying
@@ -1478,7 +1480,7 @@ func (runner *ContainerRunner) UpdateContainerRunning() error {
                return ErrCancelled
        }
        return runner.DispatcherArvClient.Update("containers", runner.Container.UUID,
-               arvadosclient.Dict{"container": arvadosclient.Dict{"state": "Running"}}, nil)
+               arvadosclient.Dict{"container": arvadosclient.Dict{"state": "Running", "gateway_address": runner.gateway.Address}}, nil)
 }
 
 // ContainerToken returns the api_token the container (and any
@@ -1877,6 +1879,20 @@ func (command) RunCommand(prog string, args []string, stdin io.Reader, stdout, s
                return 1
        }
 
+       cr.gateway = Gateway{
+               Address:           os.Getenv("GatewayAddress"),
+               AuthSecret:        os.Getenv("GatewayAuthSecret"),
+               ContainerUUID:     containerID,
+               DockerContainerID: &cr.ContainerID,
+               Log:               cr.CrunchLog,
+       }
+       os.Unsetenv("GatewayAuthSecret")
+       err = cr.gateway.Start()
+       if err != nil {
+               log.Printf("error starting gateway server: %s", err)
+               return 1
+       }
+
        parentTemp, tmperr := cr.MkTempDir("", "crunch-run."+containerID+".")
        if tmperr != nil {
                log.Printf("%s: %v", containerID, tmperr)
index 7614a143abded97b08757138bd4b152771eb3588..ae91a710e395295f47a34cb5645f980021e79021 100644 (file)
@@ -22,6 +22,7 @@ import (
        "git.arvados.org/arvados.git/sdk/go/arvados"
        "git.arvados.org/arvados.git/sdk/go/auth"
        "git.arvados.org/arvados.git/sdk/go/ctxlog"
+       "git.arvados.org/arvados.git/sdk/go/health"
        "git.arvados.org/arvados.git/sdk/go/httpserver"
        "github.com/julienschmidt/httprouter"
        "github.com/prometheus/client_golang/prometheus"
@@ -164,6 +165,11 @@ func (disp *dispatcher) initialize() {
                })
                mux.Handler("GET", "/metrics", metricsH)
                mux.Handler("GET", "/metrics.json", metricsH)
+               mux.Handler("GET", "/_health/:check", &health.Handler{
+                       Token:  disp.Cluster.ManagementToken,
+                       Prefix: "/_health/",
+                       Routes: health.Routes{"ping": disp.CheckHealth},
+               })
                disp.httpHandler = auth.RequireLiteralToken(disp.Cluster.ManagementToken, mux)
        }
 }
index fd04860861a14f9ff6b526ac6a562de02f0c74e4..7e8ce0bf4206834c80a176a7411ea614c2e1e3f7 100644 (file)
@@ -96,6 +96,7 @@ func ChooseInstanceType(cc *arvados.Cluster, ctr *arvados.Container) (best arvad
        needVCPUs := ctr.RuntimeConstraints.VCPUs
 
        needRAM := ctr.RuntimeConstraints.RAM + ctr.RuntimeConstraints.KeepCacheRAM
+       needRAM += int64(cc.Containers.ReserveExtraRAM)
        needRAM = (needRAM * 100) / int64(100-discountConfiguredRAMPercent)
 
        ok := false
index ea98efe1d2b175c70eb7394d106af45e491e41e6..abd292cbaf1278c7ca147e7da7352d74d0ad39d0 100644 (file)
@@ -73,8 +73,14 @@ func (*NodeSizeSuite) TestChoose(c *check.C) {
                        "best":   {Price: 3.3, RAM: 4000000000, VCPUs: 4, Scratch: 2 * GiB, Name: "best"},
                        "costly": {Price: 4.4, RAM: 4000000000, VCPUs: 8, Scratch: 2 * GiB, Name: "costly"},
                },
+               {
+                       "small":  {Price: 1.1, RAM: 1000000000, VCPUs: 2, Scratch: GiB, Name: "small"},
+                       "nearly": {Price: 2.2, RAM: 1200000000, VCPUs: 4, Scratch: 2 * GiB, Name: "nearly"},
+                       "best":   {Price: 3.3, RAM: 4000000000, VCPUs: 4, Scratch: 2 * GiB, Name: "best"},
+                       "costly": {Price: 4.4, RAM: 4000000000, VCPUs: 8, Scratch: 2 * GiB, Name: "costly"},
+               },
        } {
-               best, err := ChooseInstanceType(&arvados.Cluster{InstanceTypes: menu}, &arvados.Container{
+               best, err := ChooseInstanceType(&arvados.Cluster{InstanceTypes: menu, Containers: arvados.ContainersConfig{ReserveExtraRAM: 268435456}}, &arvados.Container{
                        Mounts: map[string]arvados.Mount{
                                "/tmp": {Kind: "tmp", Capacity: 2 * int64(GiB)},
                        },
index a25ed60150718f83829a003d6b0e8267a382a430..6a74280ca452e9b365f6a976f96e04ef03edc7e6 100644 (file)
@@ -5,8 +5,10 @@
 package worker
 
 import (
+       "crypto/hmac"
        "crypto/md5"
        "crypto/rand"
+       "crypto/sha256"
        "errors"
        "fmt"
        "io"
@@ -116,6 +118,7 @@ func NewPool(logger logrus.FieldLogger, arvClient *arvados.Client, reg *promethe
                timeoutTERM:                    duration(cluster.Containers.CloudVMs.TimeoutTERM, defaultTimeoutTERM),
                timeoutSignal:                  duration(cluster.Containers.CloudVMs.TimeoutSignal, defaultTimeoutSignal),
                timeoutStaleRunLock:            duration(cluster.Containers.CloudVMs.TimeoutStaleRunLock, defaultTimeoutStaleRunLock),
+               systemRootToken:                cluster.SystemRootToken,
                installPublicKey:               installPublicKey,
                tagKeyPrefix:                   cluster.Containers.CloudVMs.TagKeyPrefix,
                stop:                           make(chan bool),
@@ -154,6 +157,7 @@ type Pool struct {
        timeoutTERM                    time.Duration
        timeoutSignal                  time.Duration
        timeoutStaleRunLock            time.Duration
+       systemRootToken                string
        installPublicKey               ssh.PublicKey
        tagKeyPrefix                   string
 
@@ -990,6 +994,12 @@ func (wp *Pool) waitUntilLoaded() {
        }
 }
 
+func (wp *Pool) gatewayAuthSecret(uuid string) string {
+       h := hmac.New(sha256.New, []byte(wp.systemRootToken))
+       fmt.Fprint(h, uuid)
+       return fmt.Sprintf("%x", h.Sum(nil))
+}
+
 // Return a random string of n hexadecimal digits (n*4 random bits). n
 // must be even.
 func randomHex(n int) string {
index 47521213427610a531fa269df32b046b17ba72aa..0fd99aeeef136cdc1113f55466b1342b7c975cc1 100644 (file)
@@ -8,6 +8,7 @@ import (
        "bytes"
        "encoding/json"
        "fmt"
+       "net"
        "syscall"
        "time"
 
@@ -48,6 +49,8 @@ func newRemoteRunner(uuid string, wkr *worker) *remoteRunner {
                "ARVADOS_API_HOST":  wkr.wp.arvClient.APIHost,
                "ARVADOS_API_TOKEN": wkr.wp.arvClient.AuthToken,
                "InstanceType":      instJSON.String(),
+               "GatewayAddress":    net.JoinHostPort(wkr.instance.Address(), "0"),
+               "GatewayAuthSecret": wkr.wp.gatewayAuthSecret(uuid),
        }
        if wkr.wp.arvClient.Insecure {
                env["ARVADOS_API_HOST_INSECURE"] = "1"
index 6902688253c0a1a6ee55aeea5af258fc388dc2ed..6d9556f985f13bd76efbe9bf5b557e71b615f8ef 100644 (file)
@@ -14,6 +14,8 @@ import (
        "io"
        "os"
        "os/exec"
+       "os/user"
+       "path/filepath"
        "strconv"
        "strings"
        "syscall"
@@ -24,13 +26,18 @@ import (
        "github.com/lib/pq"
 )
 
-var Command cmd.Handler = installCommand{}
+var Command cmd.Handler = &installCommand{}
 
 const devtestDatabasePassword = "insecure_arvados_test"
 
-type installCommand struct{}
+type installCommand struct {
+       ClusterType    string
+       SourcePath     string
+       PackageVersion string
+       EatMyData      bool
+}
 
-func (installCommand) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
+func (inst *installCommand) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
        logger := ctxlog.New(stderr, "text", "info")
        ctx := ctxlog.Context(context.Background(), logger)
        ctx, cancel := context.WithCancel(ctx)
@@ -46,7 +53,10 @@ func (installCommand) RunCommand(prog string, args []string, stdin io.Reader, st
        flags := flag.NewFlagSet(prog, flag.ContinueOnError)
        flags.SetOutput(stderr)
        versionFlag := flags.Bool("version", false, "Write version information to stdout and exit 0")
-       clusterType := flags.String("type", "production", "cluster `type`: development, test, or production")
+       flags.StringVar(&inst.ClusterType, "type", "production", "cluster `type`: development, test, production, or package")
+       flags.StringVar(&inst.SourcePath, "source", "/arvados", "source tree location (required for -type=package)")
+       flags.StringVar(&inst.PackageVersion, "package-version", "0.0.0", "version string to embed in executable files")
+       flags.BoolVar(&inst.EatMyData, "eatmydata", false, "use eatmydata to speed up install")
        err = flags.Parse(args)
        if err == flag.ErrHelp {
                err = nil
@@ -55,18 +65,23 @@ func (installCommand) RunCommand(prog string, args []string, stdin io.Reader, st
                return 2
        } else if *versionFlag {
                return cmd.Version.RunCommand(prog, args, stdin, stdout, stderr)
+       } else if len(flags.Args()) > 0 {
+               err = fmt.Errorf("unrecognized command line arguments: %v", flags.Args())
+               return 2
        }
 
-       var dev, test, prod bool
-       switch *clusterType {
+       var dev, test, prod, pkg bool
+       switch inst.ClusterType {
        case "development":
                dev = true
        case "test":
                test = true
        case "production":
                prod = true
+       case "package":
+               pkg = true
        default:
-               err = fmt.Errorf("invalid cluster type %q (must be 'development', 'test', or 'production')", *clusterType)
+               err = fmt.Errorf("invalid cluster type %q (must be 'development', 'test', 'production', or 'package')", inst.ClusterType)
                return 2
        }
 
@@ -96,34 +111,45 @@ func (installCommand) RunCommand(prog string, args []string, stdin io.Reader, st
                }
        }
 
-       if dev || test {
-               debs := []string{
+       if inst.EatMyData {
+               cmd := exec.CommandContext(ctx, "apt-get", "install", "--yes", "--no-install-recommends", "eatmydata")
+               cmd.Env = append(os.Environ(), "DEBIAN_FRONTEND=noninteractive")
+               cmd.Stdout = stdout
+               cmd.Stderr = stderr
+               err = cmd.Run()
+               if err != nil {
+                       return 1
+               }
+       }
+
+       pkgs := prodpkgs(osv)
+
+       if pkg {
+               pkgs = append(pkgs,
+                       "dpkg-dev",
+                       "eatmydata", // install it for later steps, even if we're not using it now
+                       "rsync",
+               )
+       }
+
+       if dev || test || pkg {
+               pkgs = append(pkgs,
+                       "automake",
                        "bison",
                        "bsdmainutils",
                        "build-essential",
-                       "ca-certificates",
                        "cadaver",
                        "curl",
                        "cython3",
-                       "daemontools", // lib/boot uses setuidgid to drop privileges when running as root
                        "default-jdk-headless",
                        "default-jre-headless",
-                       "fuse",
                        "gettext",
-                       "git",
-                       "gitolite3",
-                       "graphviz",
-                       "haveged",
                        "iceweasel",
                        "libattr1-dev",
                        "libcrypt-ssleay-perl",
-                       "libcrypt-ssleay-perl",
-                       "libcurl3-gnutls",
-                       "libcurl4-openssl-dev",
                        "libfuse-dev",
                        "libgnutls28-dev",
                        "libjson-perl",
-                       "libjson-perl",
                        "libpam-dev",
                        "libpcre3-dev",
                        "libpq-dev",
@@ -131,11 +157,11 @@ func (installCommand) RunCommand(prog string, args []string, stdin io.Reader, st
                        "libssl-dev",
                        "libwww-perl",
                        "libxml2-dev",
-                       "libxslt1.1",
+                       "libxslt1-dev",
                        "linkchecker",
                        "lsof",
+                       "make",
                        "net-tools",
-                       "nginx",
                        "pandoc",
                        "perl-modules",
                        "pkg-config",
@@ -154,16 +180,19 @@ func (installCommand) RunCommand(prog string, args []string, stdin io.Reader, st
                        "sudo",
                        "wget",
                        "xvfb",
-                       "zlib1g-dev",
-               }
+               )
                switch {
                case osv.Debian && osv.Major >= 10:
-                       debs = append(debs, "libcurl4")
+                       pkgs = append(pkgs, "libcurl4")
                default:
-                       debs = append(debs, "libcurl3")
+                       pkgs = append(pkgs, "libcurl3")
                }
-               cmd := exec.CommandContext(ctx, "apt-get", "install", "--yes", "--no-install-recommends")
-               cmd.Args = append(cmd.Args, debs...)
+               cmd := exec.CommandContext(ctx, "apt-get")
+               if inst.EatMyData {
+                       cmd = exec.CommandContext(ctx, "eatmydata", "apt-get")
+               }
+               cmd.Args = append(cmd.Args, "install", "--yes", "--no-install-recommends")
+               cmd.Args = append(cmd.Args, pkgs...)
                cmd.Env = append(os.Environ(), "DEBIAN_FRONTEND=noninteractive")
                cmd.Stdout = stdout
                cmd.Stderr = stderr
@@ -174,21 +203,35 @@ func (installCommand) RunCommand(prog string, args []string, stdin io.Reader, st
        }
 
        os.Mkdir("/var/lib/arvados", 0755)
-       rubyversion := "2.5.7"
+       os.Mkdir("/var/lib/arvados/tmp", 0700)
+       if prod || pkg {
+               os.Mkdir("/var/lib/arvados/wwwtmp", 0700)
+               u, er := user.Lookup("www-data")
+               if er != nil {
+                       err = fmt.Errorf("user.Lookup(%q): %w", "www-data", er)
+                       return 1
+               }
+               uid, _ := strconv.Atoi(u.Uid)
+               gid, _ := strconv.Atoi(u.Gid)
+               err = os.Chown("/var/lib/arvados/wwwtmp", uid, gid)
+               if err != nil {
+                       return 1
+               }
+       }
+       rubyversion := "2.7.2"
+       rubymajorversion := rubyversion[:strings.LastIndex(rubyversion, ".")]
        if haverubyversion, err := exec.Command("/var/lib/arvados/bin/ruby", "-v").CombinedOutput(); err == nil && bytes.HasPrefix(haverubyversion, []byte("ruby "+rubyversion)) {
                logger.Print("ruby " + rubyversion + " already installed")
        } else {
-               err = runBash(`
-mkdir -p /var/lib/arvados/tmp
-tmp=/var/lib/arvados/tmp/ruby-`+rubyversion+`
-trap "rm -r ${tmp}" ERR
-wget --progress=dot:giga -O- https://cache.ruby-lang.org/pub/ruby/2.5/ruby-`+rubyversion+`.tar.gz | tar -C /var/lib/arvados/tmp -xzf -
-cd ${tmp}
-./configure --disable-install-doc --prefix /var/lib/arvados
-make -j4
+               err = inst.runBash(`
+tmp="$(mktemp -d)"
+trap 'rm -r "${tmp}"' ERR EXIT
+wget --progress=dot:giga -O- https://cache.ruby-lang.org/pub/ruby/`+rubymajorversion+`/ruby-`+rubyversion+`.tar.gz | tar -C "${tmp}" -xzf -
+cd "${tmp}/ruby-`+rubyversion+`"
+./configure --disable-install-static-library --enable-shared --disable-install-doc --prefix /var/lib/arvados
+make -j8
 make install
-/var/lib/arvados/bin/gem install bundler
-rm -r ${tmp}
+/var/lib/arvados/bin/gem install bundler --no-document
 `, stdout, stderr)
                if err != nil {
                        return 1
@@ -200,7 +243,7 @@ rm -r ${tmp}
                if havegoversion, err := exec.Command("/usr/local/bin/go", "version").CombinedOutput(); err == nil && bytes.HasPrefix(havegoversion, []byte("go version go"+goversion+" ")) {
                        logger.Print("go " + goversion + " already installed")
                } else {
-                       err = runBash(`
+                       err = inst.runBash(`
 cd /tmp
 wget --progress=dot:giga -O- https://storage.googleapis.com/golang/go`+goversion+`.linux-amd64.tar.gz | tar -C /var/lib/arvados -xzf -
 ln -sf /var/lib/arvados/go/bin/* /usr/local/bin/
@@ -209,12 +252,14 @@ ln -sf /var/lib/arvados/go/bin/* /usr/local/bin/
                                return 1
                        }
                }
+       }
 
+       if !prod && !pkg {
                pjsversion := "1.9.8"
                if havepjsversion, err := exec.Command("/usr/local/bin/phantomjs", "--version").CombinedOutput(); err == nil && string(havepjsversion) == "1.9.8\n" {
                        logger.Print("phantomjs " + pjsversion + " already installed")
                } else {
-                       err = runBash(`
+                       err = inst.runBash(`
 PJS=phantomjs-`+pjsversion+`-linux-x86_64
 wget --progress=dot:giga -O- https://bitbucket.org/ariya/phantomjs/downloads/$PJS.tar.bz2 | tar -C /var/lib/arvados -xjf -
 ln -sf /var/lib/arvados/$PJS/bin/phantomjs /usr/local/bin/
@@ -228,7 +273,7 @@ ln -sf /var/lib/arvados/$PJS/bin/phantomjs /usr/local/bin/
                if havegeckoversion, err := exec.Command("/usr/local/bin/geckodriver", "--version").CombinedOutput(); err == nil && strings.Contains(string(havegeckoversion), " "+geckoversion+" ") {
                        logger.Print("geckodriver " + geckoversion + " already installed")
                } else {
-                       err = runBash(`
+                       err = inst.runBash(`
 GD=v`+geckoversion+`
 wget --progress=dot:giga -O- https://github.com/mozilla/geckodriver/releases/download/$GD/geckodriver-$GD-linux64.tar.gz | tar -C /var/lib/arvados/bin -xzf - geckodriver
 ln -sf /var/lib/arvados/bin/geckodriver /usr/local/bin/
@@ -242,7 +287,7 @@ ln -sf /var/lib/arvados/bin/geckodriver /usr/local/bin/
                if havenodejsversion, err := exec.Command("/usr/local/bin/node", "--version").CombinedOutput(); err == nil && string(havenodejsversion) == nodejsversion+"\n" {
                        logger.Print("nodejs " + nodejsversion + " already installed")
                } else {
-                       err = runBash(`
+                       err = inst.runBash(`
 NJS=`+nodejsversion+`
 wget --progress=dot:giga -O- https://nodejs.org/dist/${NJS}/node-${NJS}-linux-x64.tar.xz | sudo tar -C /var/lib/arvados -xJf -
 ln -sf /var/lib/arvados/node-${NJS}-linux-x64/bin/{node,npm} /usr/local/bin/
@@ -256,9 +301,8 @@ ln -sf /var/lib/arvados/node-${NJS}-linux-x64/bin/{node,npm} /usr/local/bin/
                if havegradleversion, err := exec.Command("/usr/local/bin/gradle", "--version").CombinedOutput(); err == nil && strings.Contains(string(havegradleversion), "Gradle "+gradleversion+"\n") {
                        logger.Print("gradle " + gradleversion + " already installed")
                } else {
-                       err = runBash(`
+                       err = inst.runBash(`
 G=`+gradleversion+`
-mkdir -p /var/lib/arvados/tmp
 zip=/var/lib/arvados/tmp/gradle-${G}-bin.zip
 trap "rm ${zip}" ERR
 wget --progress=dot:giga -O${zip} https://services.gradle.org/distributions/gradle-${G}-bin.zip
@@ -278,7 +322,7 @@ rm ${zip}
                if havelocales, err := exec.Command("locale", "-a").CombinedOutput(); err == nil && bytes.Contains(havelocales, []byte(strings.Replace(wantlocale+"\n", "UTF-", "utf", 1))) {
                        logger.Print("locale " + wantlocale + " already installed")
                } else {
-                       err = runBash(`sed -i 's/^# *\(`+wantlocale+`\)/\1/' /etc/locale.gen && locale-gen`, stdout, stderr)
+                       err = inst.runBash(`sed -i 's/^# *\(`+wantlocale+`\)/\1/' /etc/locale.gen && locale-gen`, stdout, stderr)
                        if err != nil {
                                return 1
                        }
@@ -357,7 +401,7 @@ rm ${zip}
                        // locales. Otherwise, it might need a
                        // restart, so we attempt to restart it with
                        // systemd.
-                       if err = runBash(`sudo systemctl restart postgresql`, stdout, stderr); err != nil {
+                       if err = inst.runBash(`sudo systemctl restart postgresql`, stdout, stderr); err != nil {
                                logger.Warn("`systemctl restart postgresql` failed; hoping postgresql does not need to be restarted")
                        } else if err = waitPostgreSQLReady(); err != nil {
                                return 1
@@ -392,12 +436,105 @@ rm ${zip}
                }
        }
 
+       if prod || pkg {
+               // Install Rails apps to /var/lib/arvados/{railsapi,workbench1}/
+               for dstdir, srcdir := range map[string]string{
+                       "railsapi":   "services/api",
+                       "workbench1": "apps/workbench",
+               } {
+                       fmt.Fprintf(stderr, "building %s...\n", srcdir)
+                       cmd := exec.Command("rsync",
+                               "-a", "--no-owner", "--no-group", "--delete-after", "--delete-excluded",
+                               "--exclude", "/coverage",
+                               "--exclude", "/log",
+                               "--exclude", "/tmp",
+                               "--exclude", "/vendor",
+                               "--exclude", "/config/environments",
+                               "./", "/var/lib/arvados/"+dstdir+"/")
+                       cmd.Dir = filepath.Join(inst.SourcePath, srcdir)
+                       cmd.Stdout = stdout
+                       cmd.Stderr = stderr
+                       err = cmd.Run()
+                       if err != nil {
+                               return 1
+                       }
+                       for _, cmdline := range [][]string{
+                               {"mkdir", "-p", "log", "tmp", ".bundle", "/var/www/.gem", "/var/www/.bundle", "/var/www/.passenger"},
+                               {"touch", "log/production.log"},
+                               {"chown", "-R", "--from=root", "www-data:www-data", "/var/www/.gem", "/var/www/.bundle", "/var/www/.passenger", "log", "tmp", ".bundle", "Gemfile.lock", "config.ru", "config/environment.rb"},
+                               {"sudo", "-u", "www-data", "/var/lib/arvados/bin/gem", "install", "--user", "--conservative", "--no-document", "bundler:1.16.6", "bundler:1.17.3", "bundler:2.0.2"},
+                               {"sudo", "-u", "www-data", "/var/lib/arvados/bin/bundle", "install", "--deployment", "--jobs", "8", "--path", "/var/www/.gem"},
+                               {"sudo", "-u", "www-data", "/var/lib/arvados/bin/bundle", "exec", "passenger-config", "build-native-support"},
+                               {"sudo", "-u", "www-data", "/var/lib/arvados/bin/bundle", "exec", "passenger-config", "install-standalone-runtime"},
+                       } {
+                               cmd = exec.Command(cmdline[0], cmdline[1:]...)
+                               cmd.Dir = "/var/lib/arvados/" + dstdir
+                               cmd.Stdout = stdout
+                               cmd.Stderr = stderr
+                               fmt.Fprintf(stderr, "... %s\n", cmd.Args)
+                               err = cmd.Run()
+                               if err != nil {
+                                       return 1
+                               }
+                       }
+                       cmd = exec.Command("sudo", "-u", "www-data", "/var/lib/arvados/bin/bundle", "exec", "passenger-config", "validate-install")
+                       cmd.Dir = "/var/lib/arvados/" + dstdir
+                       cmd.Stdout = stdout
+                       cmd.Stderr = stderr
+                       err = cmd.Run()
+                       if err != nil && !strings.Contains(err.Error(), "exit status 2") {
+                               // Exit code 2 indicates there were warnings (like
+                               // "other passenger installations have been detected",
+                               // which we can't expect to avoid) but no errors.
+                               // Other non-zero exit codes (1, 9) indicate errors.
+                               return 1
+                       }
+               }
+
+               // Install Go programs to /var/lib/arvados/bin/
+               for _, srcdir := range []string{
+                       "cmd/arvados-client",
+                       "cmd/arvados-server",
+                       "services/arv-git-httpd",
+                       "services/crunch-dispatch-local",
+                       "services/crunch-dispatch-slurm",
+                       "services/health",
+                       "services/keep-balance",
+                       "services/keep-web",
+                       "services/keepproxy",
+                       "services/keepstore",
+                       "services/ws",
+               } {
+                       fmt.Fprintf(stderr, "building %s...\n", srcdir)
+                       cmd := exec.Command("go", "install", "-ldflags", "-X git.arvados.org/arvados.git/lib/cmd.version="+inst.PackageVersion+" -X main.version="+inst.PackageVersion)
+                       cmd.Env = append(cmd.Env, os.Environ()...)
+                       cmd.Env = append(cmd.Env, "GOBIN=/var/lib/arvados/bin")
+                       cmd.Dir = filepath.Join(inst.SourcePath, srcdir)
+                       cmd.Stdout = stdout
+                       cmd.Stderr = stderr
+                       err = cmd.Run()
+                       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
+               cmd.Stderr = stderr
+               err = cmd.Run()
+               if err != nil {
+                       return 1
+               }
+       }
+
        return 0
 }
 
 type osversion struct {
        Debian bool
        Ubuntu bool
+       Centos bool
        Major  int
 }
 
@@ -435,6 +572,8 @@ func identifyOS() (osversion, error) {
                osv.Ubuntu = true
        case "debian":
                osv.Debian = true
+       case "centos":
+               osv.Centos = true
        default:
                return osv, fmt.Errorf("unsupported ID in /etc/os-release: %q", kv["ID"])
        }
@@ -462,10 +601,64 @@ func waitPostgreSQLReady() error {
        }
 }
 
-func runBash(script string, stdout, stderr io.Writer) error {
+func (inst *installCommand) runBash(script string, stdout, stderr io.Writer) error {
        cmd := exec.Command("bash", "-")
+       if inst.EatMyData {
+               cmd = exec.Command("eatmydata", "bash", "-")
+       }
        cmd.Stdin = bytes.NewBufferString("set -ex -o pipefail\n" + script)
        cmd.Stdout = stdout
        cmd.Stderr = stderr
        return cmd.Run()
 }
+
+func prodpkgs(osv osversion) []string {
+       pkgs := []string{
+               "ca-certificates",
+               "curl",
+               "fuse",
+               "git",
+               "gitolite3",
+               "graphviz",
+               "haveged",
+               "libcurl3-gnutls",
+               "libxslt1.1",
+               "nginx",
+               "python",
+               "sudo",
+       }
+       if osv.Debian || osv.Ubuntu {
+               if osv.Debian && osv.Major == 8 {
+                       pkgs = append(pkgs, "libgnutls-deb0-28") // sdk/cwl
+               } else if osv.Debian && osv.Major >= 10 || osv.Ubuntu && osv.Major >= 16 {
+                       pkgs = append(pkgs, "python3-distutils") // sdk/cwl
+               }
+               return append(pkgs,
+                       "g++",
+                       "libcurl4-openssl-dev", // services/api
+                       "libpq-dev",
+                       "libpython2.7", // services/fuse
+                       "mime-support", // keep-web
+                       "zlib1g-dev",   // services/api
+               )
+       } else if osv.Centos {
+               return append(pkgs,
+                       "fuse-libs", // services/fuse
+                       "gcc",
+                       "gcc-c++",
+                       "libcurl-devel",    // services/api
+                       "mailcap",          // keep-web
+                       "postgresql-devel", // services/api
+               )
+       } else {
+               panic("os version not supported")
+       }
+}
+
+func ProductionDependencies() ([]string, error) {
+       osv, err := identifyOS()
+       if err != nil {
+               return nil, err
+       }
+       return prodpkgs(osv), nil
+}
diff --git a/lib/install/init.go b/lib/install/init.go
new file mode 100644 (file)
index 0000000..7ae42c5
--- /dev/null
@@ -0,0 +1,267 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package install
+
+import (
+       "context"
+       "crypto/rand"
+       "crypto/rsa"
+       "crypto/x509"
+       "encoding/pem"
+       "flag"
+       "fmt"
+       "io"
+       "os"
+       "os/exec"
+       "os/user"
+       "regexp"
+       "strconv"
+       "text/template"
+
+       "git.arvados.org/arvados.git/lib/cmd"
+       "git.arvados.org/arvados.git/lib/config"
+       "git.arvados.org/arvados.git/sdk/go/arvados"
+       "git.arvados.org/arvados.git/sdk/go/ctxlog"
+       "github.com/lib/pq"
+)
+
+var InitCommand cmd.Handler = &initCommand{}
+
+type initCommand struct {
+       ClusterID          string
+       Domain             string
+       PostgreSQLPassword string
+}
+
+func (initcmd *initCommand) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
+       logger := ctxlog.New(stderr, "text", "info")
+       ctx := ctxlog.Context(context.Background(), logger)
+       ctx, cancel := context.WithCancel(ctx)
+       defer cancel()
+
+       var err error
+       defer func() {
+               if err != nil {
+                       logger.WithError(err).Info("exiting")
+               }
+       }()
+
+       hostname, err := os.Hostname()
+       if err != nil {
+               err = fmt.Errorf("Hostname(): %w", err)
+               return 1
+       }
+
+       flags := flag.NewFlagSet(prog, flag.ContinueOnError)
+       flags.SetOutput(stderr)
+       versionFlag := flags.Bool("version", false, "Write version information to stdout and exit 0")
+       flags.StringVar(&initcmd.ClusterID, "cluster-id", "", "cluster `id`, like x1234 for a dev cluster")
+       flags.StringVar(&initcmd.Domain, "domain", hostname, "cluster public DNS `name`, like x1234.arvadosapi.com")
+       err = flags.Parse(args)
+       if err == flag.ErrHelp {
+               err = nil
+               return 0
+       } else if err != nil {
+               return 2
+       } else if *versionFlag {
+               return cmd.Version.RunCommand(prog, args, stdin, stdout, stderr)
+       } else if len(flags.Args()) > 0 {
+               err = fmt.Errorf("unrecognized command line arguments: %v", flags.Args())
+               return 2
+       } else if !regexp.MustCompile(`^[a-z][a-z0-9]{4}`).MatchString(initcmd.ClusterID) {
+               err = fmt.Errorf("cluster ID %q is invalid; must be an ASCII letter followed by 4 alphanumerics (try -help)", initcmd.ClusterID)
+               return 1
+       }
+
+       wwwuser, err := user.Lookup("www-data")
+       if err != nil {
+               err = fmt.Errorf("user.Lookup(%q): %w", "www-data", err)
+               return 1
+       }
+       wwwgid, err := strconv.Atoi(wwwuser.Gid)
+       if err != nil {
+               return 1
+       }
+       initcmd.PostgreSQLPassword = initcmd.RandomHex(32)
+
+       err = os.Mkdir("/var/lib/arvados/keep", 0600)
+       if err != nil && !os.IsExist(err) {
+               err = fmt.Errorf("mkdir /var/lib/arvados/keep: %w", err)
+               return 1
+       }
+       fmt.Fprintln(stderr, "created /var/lib/arvados/keep")
+
+       err = os.Mkdir("/etc/arvados", 0750)
+       if err != nil && !os.IsExist(err) {
+               err = fmt.Errorf("mkdir /etc/arvados: %w", 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)
+       if err != nil {
+               err = fmt.Errorf("open /etc/arvados/config.yml: %w", err)
+               return 1
+       }
+       tmpl, err := template.New("config").Parse(`Clusters:
+  {{.ClusterID}}:
+    Services:
+      Controller:
+        InternalURLs:
+          "http://0.0.0.0:8003/": {}
+        ExternalURL: {{printf "%q" ( print "https://" .Domain "/" ) }}
+      RailsAPI:
+        InternalURLs:
+          "http://0.0.0.0:8004/": {}
+      Websocket:
+        InternalURLs:
+          "http://0.0.0.0:8005/": {}
+        ExternalURL: {{printf "%q" ( print "wss://ws." .Domain "/" ) }}
+      Keepbalance:
+        InternalURLs:
+          "http://0.0.0.0:9005/": {}
+      GitHTTP:
+        InternalURLs:
+          "http://0.0.0.0:9001/": {}
+        ExternalURL: {{printf "%q" ( print "https://git." .Domain "/" ) }}
+      DispatchCloud:
+        InternalURLs:
+          "http://0.0.0.0:9006/": {}
+      Keepproxy:
+        InternalURLs:
+          "http://0.0.0.0:25108/": {}
+        ExternalURL: {{printf "%q" ( print "https://keep." .Domain "/" ) }}
+      WebDAV:
+        InternalURLs:
+          "http://0.0.0.0:9002/": {}
+        ExternalURL: {{printf "%q" ( print "https://*.collections." .Domain "/" ) }}
+      WebDAVDownload:
+        InternalURLs:
+          "http://0.0.0.0:8004/": {}
+        ExternalURL: {{printf "%q" ( print "https://download." .Domain "/" ) }}
+      Keepstore:
+        InternalURLs:
+          "http://0.0.0.0:25107/": {}
+      Composer:
+        ExternalURL: {{printf "%q" ( print "https://workbench." .Domain "/composer" ) }}
+      Workbench1:
+        InternalURLs:
+          "http://0.0.0.0:8001/": {}
+        ExternalURL: {{printf "%q" ( print "https://workbench." .Domain "/" ) }}
+      #Workbench2:
+      #  InternalURLs:
+      #    "http://0.0.0.0:8002/": {}
+      #  ExternalURL: {{printf "%q" ( print "https://workbench2." .Domain "/" ) }}
+      Health:
+        InternalURLs:
+          "http://0.0.0.0:9007/": {}
+    Collections:
+      BlobSigningKey: {{printf "%q" ( .RandomHex 50 )}}
+    Containers:
+      DispatchPrivateKey: {{printf "%q" .GenerateSSHPrivateKey}}
+    ManagementToken: {{printf "%q" ( .RandomHex 50 )}}
+    PostgreSQL:
+      Connection:
+        dbname: arvados_production
+        host: localhost
+        user: arvados
+        password: {{printf "%q" .PostgreSQLPassword}}
+    SystemRootToken: {{printf "%q" ( .RandomHex 50 )}}
+    TLS:
+      Insecure: true
+    Volumes:
+      {{.ClusterID}}-nyw5e-000000000000000:
+        Driver: Directory
+        DriverParameters:
+          Root: /var/lib/arvados/keep
+        Replication: 2
+    Workbench:
+      SecretKeyBase: {{printf "%q" ( .RandomHex 50 )}}
+`)
+       if err != nil {
+               return 1
+       }
+       err = tmpl.Execute(f, initcmd)
+       if err != nil {
+               err = fmt.Errorf("/etc/arvados/config.yml: tmpl.Execute: %w", err)
+               return 1
+       }
+       err = f.Close()
+       if err != nil {
+               err = fmt.Errorf("/etc/arvados/config.yml: close: %w", err)
+               return 1
+       }
+       fmt.Fprintln(stderr, "created /etc/arvados/config.yml")
+
+       ldr := config.NewLoader(nil, logger)
+       ldr.SkipLegacy = true
+       cfg, err := ldr.Load()
+       if err != nil {
+               err = fmt.Errorf("/etc/arvados/config.yml: %w", err)
+               return 1
+       }
+       cluster, err := cfg.GetCluster("")
+       if err != nil {
+               return 1
+       }
+
+       err = initcmd.createDB(ctx, cluster.PostgreSQL.Connection, stderr)
+       if err != nil {
+               return 1
+       }
+
+       cmd := exec.CommandContext(ctx, "sudo", "-u", "www-data", "-E", "HOME=/var/www", "PATH=/var/lib/arvados/bin:"+os.Getenv("PATH"), "/var/lib/arvados/bin/bundle", "exec", "rake", "db:setup")
+       cmd.Dir = "/var/lib/arvados/railsapi"
+       cmd.Stdout = stderr
+       cmd.Stderr = stderr
+       err = cmd.Run()
+       if err != nil {
+               err = fmt.Errorf("rake db:setup: %w", err)
+               return 1
+       }
+       fmt.Fprintln(stderr, "initialized database")
+
+       return 0
+}
+
+func (initcmd *initCommand) GenerateSSHPrivateKey() (string, error) {
+       privkey, err := rsa.GenerateKey(rand.Reader, 4096)
+       if err != nil {
+               return "", err
+       }
+       err = privkey.Validate()
+       if err != nil {
+               return "", err
+       }
+       return string(pem.EncodeToMemory(&pem.Block{
+               Type:  "RSA PRIVATE KEY",
+               Bytes: x509.MarshalPKCS1PrivateKey(privkey),
+       })), nil
+}
+
+func (initcmd *initCommand) RandomHex(chars int) string {
+       b := make([]byte, chars/2)
+       _, err := rand.Read(b)
+       if err != nil {
+               panic(err)
+       }
+       return fmt.Sprintf("%x", b)
+}
+
+func (initcmd *initCommand) createDB(ctx context.Context, dbconn arvados.PostgreSQLConnection, stderr io.Writer) error {
+       for _, sql := range []string{
+               `CREATE USER ` + pq.QuoteIdentifier(dbconn["user"]) + ` WITH SUPERUSER ENCRYPTED PASSWORD ` + pq.QuoteLiteral(dbconn["password"]),
+               `CREATE DATABASE ` + pq.QuoteIdentifier(dbconn["dbname"]) + ` WITH TEMPLATE template0 ENCODING 'utf8'`,
+               `CREATE EXTENSION IF NOT EXISTS pg_trgm`,
+       } {
+               cmd := exec.CommandContext(ctx, "sudo", "-u", "postgres", "psql", "-c", sql)
+               cmd.Stdout = stderr
+               cmd.Stderr = stderr
+               err := cmd.Run()
+               if err != nil {
+                       return fmt.Errorf("error setting up arvados user/database: %w", err)
+               }
+       }
+       return nil
+}
diff --git a/lib/selfsigned/cert.go b/lib/selfsigned/cert.go
new file mode 100644 (file)
index 0000000..ae521dd
--- /dev/null
@@ -0,0 +1,76 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package selfsigned
+
+import (
+       "crypto/rand"
+       "crypto/rsa"
+       "crypto/tls"
+       "crypto/x509"
+       "crypto/x509/pkix"
+       "fmt"
+       "math/big"
+       "net"
+       "time"
+)
+
+type CertGenerator struct {
+       Bits  int
+       Hosts []string
+       IsCA  bool
+}
+
+func (gen CertGenerator) Generate() (cert tls.Certificate, err error) {
+       keyUsage := x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment
+       if gen.IsCA {
+               keyUsage |= x509.KeyUsageCertSign
+       }
+       notBefore := time.Now()
+       notAfter := time.Now().Add(time.Hour * 24 * 365)
+       snMax := new(big.Int).Lsh(big.NewInt(1), 128)
+       sn, err := rand.Int(rand.Reader, snMax)
+       if err != nil {
+               err = fmt.Errorf("Failed to generate serial number: %w", err)
+               return
+       }
+       template := x509.Certificate{
+               SerialNumber: sn,
+               Subject: pkix.Name{
+                       Organization: []string{"N/A"},
+               },
+               NotBefore:             notBefore,
+               NotAfter:              notAfter,
+               KeyUsage:              keyUsage,
+               ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
+               BasicConstraintsValid: true,
+               IsCA:                  gen.IsCA,
+       }
+       for _, h := range gen.Hosts {
+               if ip := net.ParseIP(h); ip != nil {
+                       template.IPAddresses = append(template.IPAddresses, ip)
+               } else {
+                       template.DNSNames = append(template.DNSNames, h)
+               }
+       }
+       bits := gen.Bits
+       if bits == 0 {
+               bits = 4096
+       }
+       priv, err := rsa.GenerateKey(rand.Reader, bits)
+       if err != nil {
+               err = fmt.Errorf("error generating key: %w", err)
+               return
+       }
+       certder, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
+       if err != nil {
+               err = fmt.Errorf("error creating certificate: %w", err)
+               return
+       }
+       cert = tls.Certificate{
+               Certificate: [][]byte{certder},
+               PrivateKey:  priv,
+       }
+       return
+}
diff --git a/lib/selfsigned/cert_test.go b/lib/selfsigned/cert_test.go
new file mode 100644 (file)
index 0000000..16ed8bd
--- /dev/null
@@ -0,0 +1,26 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package selfsigned
+
+import (
+       "testing"
+)
+
+func TestCert(t *testing.T) {
+       cert, err := CertGenerator{Bits: 1024, Hosts: []string{"localhost"}, IsCA: false}.Generate()
+       if err != nil {
+               t.Error(err)
+       }
+       if len(cert.Certificate) < 1 {
+               t.Error("no certificate!")
+       }
+       cert, err = CertGenerator{Bits: 2048, Hosts: []string{"localhost"}, IsCA: true}.Generate()
+       if err != nil {
+               t.Error(err)
+       }
+       if len(cert.Certificate) < 1 {
+               t.Error("no certificate!")
+       }
+}
index a11872971a9dffc9a2563dedb7338a1438ca40b3..37a3e007b16108cb6a2f9c6627c29106eee52bca 100644 (file)
@@ -5,8 +5,12 @@
 package arvados
 
 import (
+       "bufio"
        "context"
        "encoding/json"
+       "net"
+
+       "github.com/sirupsen/logrus"
 )
 
 type APIEndpoint struct {
@@ -41,6 +45,7 @@ var (
        EndpointContainerDelete               = APIEndpoint{"DELETE", "arvados/v1/containers/{uuid}", ""}
        EndpointContainerLock                 = APIEndpoint{"POST", "arvados/v1/containers/{uuid}/lock", ""}
        EndpointContainerUnlock               = APIEndpoint{"POST", "arvados/v1/containers/{uuid}/unlock", ""}
+       EndpointContainerSSH                  = APIEndpoint{"GET", "arvados/v1/connect/{uuid}/ssh", ""} // move to /containers after #17014 fixes routing
        EndpointContainerRequestCreate        = APIEndpoint{"POST", "arvados/v1/container_requests", "container_request"}
        EndpointContainerRequestUpdate        = APIEndpoint{"PATCH", "arvados/v1/container_requests/{uuid}", "container_request"}
        EndpointContainerRequestGet           = APIEndpoint{"GET", "arvados/v1/container_requests/{uuid}", ""}
@@ -65,6 +70,18 @@ var (
        EndpointAPIClientAuthorizationCurrent = APIEndpoint{"GET", "arvados/v1/api_client_authorizations/current", ""}
 )
 
+type ContainerSSHOptions struct {
+       UUID          string `json:"uuid"`
+       DetachKeys    string `json:"detach_keys"`
+       LoginUsername string `json:"login_username"`
+}
+
+type ContainerSSHConnection struct {
+       Conn   net.Conn           `json:"-"`
+       Bufrw  *bufio.ReadWriter  `json:"-"`
+       Logger logrus.FieldLogger `json:"-"`
+}
+
 type GetOptions struct {
        UUID         string   `json:"uuid,omitempty"`
        Select       []string `json:"select"`
@@ -180,6 +197,7 @@ type API interface {
        ContainerDelete(ctx context.Context, options DeleteOptions) (Container, error)
        ContainerLock(ctx context.Context, options GetOptions) (Container, error)
        ContainerUnlock(ctx context.Context, options GetOptions) (Container, error)
+       ContainerSSH(ctx context.Context, options ContainerSSHOptions) (ContainerSSHConnection, error)
        ContainerRequestCreate(ctx context.Context, options CreateOptions) (ContainerRequest, error)
        ContainerRequestUpdate(ctx context.Context, options UpdateOptions) (ContainerRequest, error)
        ContainerRequestGet(ctx context.Context, options GetOptions) (ContainerRequest, error)
index 52c75d5113c2a9399267e90fb8c18c8a5aeeaad7..ea3cb6899e7bd5ac9f92cc0d3127e6aa3a485f13 100644 (file)
@@ -9,6 +9,7 @@ import (
        "context"
        "crypto/tls"
        "encoding/json"
+       "errors"
        "fmt"
        "io"
        "io/ioutil"
@@ -67,6 +68,10 @@ type Client struct {
        dd *DiscoveryDocument
 
        defaultRequestID string
+
+       // APIHost and AuthToken were loaded from ARVADOS_* env vars
+       // (used to customize "no host/token" error messages)
+       loadedFromEnv bool
 }
 
 // InsecureHTTPClient is the default http.Client used by a Client with
@@ -123,6 +128,7 @@ func NewClientFromEnv() *Client {
                Insecure:        insecure,
                KeepServiceURIs: svcs,
                Timeout:         5 * time.Minute,
+               loadedFromEnv:   true,
        }
 }
 
@@ -312,6 +318,13 @@ func (c *Client) RequestAndDecodeContext(ctx context.Context, dst interface{}, m
                // Ensure body is closed even if we error out early
                defer body.Close()
        }
+       if c.APIHost == "" {
+               if c.loadedFromEnv {
+                       return errors.New("ARVADOS_API_HOST and/or ARVADOS_API_TOKEN environment variables are not set")
+               } else {
+                       return errors.New("arvados.Client cannot perform request: APIHost is not set")
+               }
+       }
        urlString := c.apiURL(path)
        urlValues, err := anythingToValues(params)
        if err != nil {
index 9dc9e17dd815b1818bc17bb20c074d69982e146d..4a56c930213abf389a782fd593622d776da9584f 100644 (file)
@@ -151,19 +151,21 @@ type Cluster struct {
                        UsernameAttribute  string
                }
                Google struct {
-                       Enable                  bool
-                       ClientID                string
-                       ClientSecret            string
-                       AlternateEmailAddresses bool
+                       Enable                          bool
+                       ClientID                        string
+                       ClientSecret                    string
+                       AlternateEmailAddresses         bool
+                       AuthenticationRequestParameters map[string]string
                }
                OpenIDConnect struct {
-                       Enable             bool
-                       Issuer             string
-                       ClientID           string
-                       ClientSecret       string
-                       EmailClaim         string
-                       EmailVerifiedClaim string
-                       UsernameClaim      string
+                       Enable                          bool
+                       Issuer                          string
+                       ClientID                        string
+                       ClientSecret                    string
+                       EmailClaim                      string
+                       EmailVerifiedClaim              string
+                       UsernameClaim                   string
+                       AuthenticationRequestParameters map[string]string
                }
                PAM struct {
                        Enable             bool
@@ -429,6 +431,10 @@ type ContainersConfig struct {
                LogUpdatePeriod              Duration
                LogUpdateSize                ByteSize
        }
+       ShellAccess struct {
+               Admin bool
+               User  bool
+       }
        SLURM struct {
                PrioritySpread             int64
                SbatchArgumentsList        []string
index 3ff7c52055caf0e6ce72d0f32afc0fa1809007a3..b8530db342d212f3b64e746466db07bf8ffe04df 100644 (file)
@@ -8,28 +8,30 @@ import "time"
 
 // Container is an arvados#container resource.
 type Container struct {
-       UUID                 string                 `json:"uuid"`
-       Etag                 string                 `json:"etag"`
-       CreatedAt            time.Time              `json:"created_at"`
-       ModifiedByClientUUID string                 `json:"modified_by_client_uuid"`
-       ModifiedByUserUUID   string                 `json:"modified_by_user_uuid"`
-       ModifiedAt           time.Time              `json:"modified_at"`
-       Command              []string               `json:"command"`
-       ContainerImage       string                 `json:"container_image"`
-       Cwd                  string                 `json:"cwd"`
-       Environment          map[string]string      `json:"environment"`
-       LockedByUUID         string                 `json:"locked_by_uuid"`
-       Mounts               map[string]Mount       `json:"mounts"`
-       Output               string                 `json:"output"`
-       OutputPath           string                 `json:"output_path"`
-       Priority             int64                  `json:"priority"`
-       RuntimeConstraints   RuntimeConstraints     `json:"runtime_constraints"`
-       State                ContainerState         `json:"state"`
-       SchedulingParameters SchedulingParameters   `json:"scheduling_parameters"`
-       ExitCode             int                    `json:"exit_code"`
-       RuntimeStatus        map[string]interface{} `json:"runtime_status"`
-       StartedAt            *time.Time             `json:"started_at"`  // nil if not yet started
-       FinishedAt           *time.Time             `json:"finished_at"` // nil if not yet finished
+       UUID                      string                 `json:"uuid"`
+       Etag                      string                 `json:"etag"`
+       CreatedAt                 time.Time              `json:"created_at"`
+       ModifiedByClientUUID      string                 `json:"modified_by_client_uuid"`
+       ModifiedByUserUUID        string                 `json:"modified_by_user_uuid"`
+       ModifiedAt                time.Time              `json:"modified_at"`
+       Command                   []string               `json:"command"`
+       ContainerImage            string                 `json:"container_image"`
+       Cwd                       string                 `json:"cwd"`
+       Environment               map[string]string      `json:"environment"`
+       LockedByUUID              string                 `json:"locked_by_uuid"`
+       Mounts                    map[string]Mount       `json:"mounts"`
+       Output                    string                 `json:"output"`
+       OutputPath                string                 `json:"output_path"`
+       Priority                  int64                  `json:"priority"`
+       RuntimeConstraints        RuntimeConstraints     `json:"runtime_constraints"`
+       State                     ContainerState         `json:"state"`
+       SchedulingParameters      SchedulingParameters   `json:"scheduling_parameters"`
+       ExitCode                  int                    `json:"exit_code"`
+       RuntimeStatus             map[string]interface{} `json:"runtime_status"`
+       StartedAt                 *time.Time             `json:"started_at"`  // nil if not yet started
+       FinishedAt                *time.Time             `json:"finished_at"` // nil if not yet finished
+       GatewayAddress            string                 `json:"gateway_address"`
+       InteractiveSessionStarted bool                   `json:"interactive_session_started"`
 }
 
 // ContainerRequest is an arvados#container_request resource.
@@ -89,7 +91,7 @@ type Mount struct {
 // RuntimeConstraints specify a container's compute resources (RAM,
 // CPU) and network connectivity.
 type RuntimeConstraints struct {
-       API          bool  `json:"api"`
+       API          bool  `json:"API"`
        RAM          int64 `json:"ram"`
        VCPUs        int   `json:"vcpus"`
        KeepCacheRAM int64 `json:"keep_cache_ram"`
diff --git a/sdk/go/arvados/container_gateway.go b/sdk/go/arvados/container_gateway.go
new file mode 100644 (file)
index 0000000..00c98d5
--- /dev/null
@@ -0,0 +1,74 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package arvados
+
+import (
+       "context"
+       "io"
+       "net/http"
+       "sync"
+
+       "git.arvados.org/arvados.git/sdk/go/ctxlog"
+       "github.com/sirupsen/logrus"
+)
+
+func (sshconn ContainerSSHConnection) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+       hj, ok := w.(http.Hijacker)
+       if !ok {
+               http.Error(w, "ResponseWriter does not support connection upgrade", http.StatusInternalServerError)
+               return
+       }
+       w.Header().Set("Connection", "upgrade")
+       w.Header().Set("Upgrade", "ssh")
+       w.WriteHeader(http.StatusSwitchingProtocols)
+       conn, bufrw, err := hj.Hijack()
+       if err != nil {
+               ctxlog.FromContext(req.Context()).WithError(err).Error("error hijacking ResponseWriter")
+               return
+       }
+       defer conn.Close()
+
+       var bytesIn, bytesOut int64
+       var wg sync.WaitGroup
+       ctx, cancel := context.WithCancel(context.Background())
+       wg.Add(1)
+       go func() {
+               defer wg.Done()
+               defer cancel()
+               n, err := io.CopyN(conn, sshconn.Bufrw, int64(sshconn.Bufrw.Reader.Buffered()))
+               bytesOut += n
+               if err == nil {
+                       n, err = io.Copy(conn, sshconn.Conn)
+                       bytesOut += n
+               }
+               if err != nil {
+                       ctxlog.FromContext(req.Context()).WithError(err).Error("error copying downstream")
+               }
+       }()
+       wg.Add(1)
+       go func() {
+               defer wg.Done()
+               defer cancel()
+               n, err := io.CopyN(sshconn.Conn, bufrw, int64(bufrw.Reader.Buffered()))
+               bytesIn += n
+               if err == nil {
+                       n, err = io.Copy(sshconn.Conn, conn)
+                       bytesIn += n
+               }
+               if err != nil {
+                       ctxlog.FromContext(req.Context()).WithError(err).Error("error copying upstream")
+               }
+       }()
+       <-ctx.Done()
+       if sshconn.Logger != nil {
+               go func() {
+                       wg.Wait()
+                       sshconn.Logger.WithFields(logrus.Fields{
+                               "bytesIn":  bytesIn,
+                               "bytesOut": bytesOut,
+                       }).Info("closed connection")
+               }()
+       }
+}
index df3e46febd5598f9d065fba24c964701b480c1d1..930eabf27ef997a2662d0a56c8c5a1494e59a98a 100644 (file)
@@ -105,6 +105,10 @@ func (as *APIStub) ContainerUnlock(ctx context.Context, options arvados.GetOptio
        as.appendCall(ctx, as.ContainerUnlock, options)
        return arvados.Container{}, as.Error
 }
+func (as *APIStub) ContainerSSH(ctx context.Context, options arvados.ContainerSSHOptions) (arvados.ContainerSSHConnection, error) {
+       as.appendCall(ctx, as.ContainerSSH, options)
+       return arvados.ContainerSSHConnection{}, as.Error
+}
 func (as *APIStub) ContainerRequestCreate(ctx context.Context, options arvados.CreateOptions) (arvados.ContainerRequest, error) {
        as.appendCall(ctx, as.ContainerRequestCreate, options)
        return arvados.ContainerRequest{}, as.Error
index 794adabdd3926b6b04036a6c62b1044f2e8f13d5..a666ef8ec02803354e80bc3a80e61910b5f3c0f4 100644 (file)
@@ -193,6 +193,9 @@ func (agg *Aggregator) ping(target *url.URL) (result CheckResult) {
        }
        req.Header.Set("Authorization", "Bearer "+agg.Cluster.ManagementToken)
 
+       // Avoid workbench1's redirect-http-to-https feature
+       req.Header.Set("X-Forwarded-Proto", "https")
+
        ctx, cancel := context.WithTimeout(req.Context(), time.Duration(agg.timeout))
        defer cancel()
        req = req.WithContext(ctx)
index 85b4f5b37bc619b3da2076c130b2494d9f977956..a4336049f2447bd18cf396cbec0b76e7cdf69356 100644 (file)
@@ -11,17 +11,33 @@ http {
     '[$time_local] "$http_x_request_id" $server_name $status $body_bytes_sent $request_time $request_method "$scheme://$http_host$request_uri" $remote_addr:$remote_port '
     '"$http_referer" "$http_user_agent"';
   access_log "{{ACCESSLOG}}" customlog;
-  client_body_temp_path "{{TMPDIR}}";
-  proxy_temp_path "{{TMPDIR}}";
-  fastcgi_temp_path "{{TMPDIR}}";
-  uwsgi_temp_path "{{TMPDIR}}";
-  scgi_temp_path "{{TMPDIR}}";
+  client_body_temp_path "{{TMPDIR}}/nginx";
+  proxy_temp_path "{{TMPDIR}}/nginx";
+  fastcgi_temp_path "{{TMPDIR}}/nginx";
+  uwsgi_temp_path "{{TMPDIR}}/nginx";
+  scgi_temp_path "{{TMPDIR}}/nginx";
+  upstream controller {
+    server {{LISTENHOST}}:{{CONTROLLERPORT}};
+  }
+  server {
+    listen {{LISTENHOST}}:{{CONTROLLERSSLPORT}} ssl;
+    server_name controller ~.*;
+    ssl_certificate "{{SSLCERT}}";
+    ssl_certificate_key "{{SSLKEY}}";
+    location  / {
+      proxy_pass http://controller;
+      proxy_set_header Host $http_host;
+      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+      proxy_set_header X-Forwarded-Proto https;
+      proxy_redirect off;
+    }
+  }
   upstream arv-git-http {
     server {{LISTENHOST}}:{{GITPORT}};
   }
   server {
-    listen {{LISTENHOST}}:{{GITSSLPORT}} ssl default_server;
-    server_name arv-git-http;
+    listen {{LISTENHOST}}:{{GITSSLPORT}} ssl;
+    server_name arv-git-http git.*;
     ssl_certificate "{{SSLCERT}}";
     ssl_certificate_key "{{SSLKEY}}";
     location  / {
@@ -36,8 +52,8 @@ http {
     server {{LISTENHOST}}:{{KEEPPROXYPORT}};
   }
   server {
-    listen {{LISTENHOST}}:{{KEEPPROXYSSLPORT}} ssl default_server;
-    server_name keepproxy;
+    listen {{LISTENHOST}}:{{KEEPPROXYSSLPORT}} ssl;
+    server_name keepproxy keep.*;
     ssl_certificate "{{SSLCERT}}";
     ssl_certificate_key "{{SSLKEY}}";
     location  / {
@@ -55,8 +71,8 @@ http {
     server {{LISTENHOST}}:{{KEEPWEBPORT}};
   }
   server {
-    listen {{LISTENHOST}}:{{KEEPWEBSSLPORT}} ssl default_server;
-    server_name keep-web;
+    listen {{LISTENHOST}}:{{KEEPWEBSSLPORT}} ssl;
+    server_name keep-web collections.* ~\.collections\.;
     ssl_certificate "{{SSLCERT}}";
     ssl_certificate_key "{{SSLKEY}}";
     location  / {
@@ -75,8 +91,8 @@ http {
     server {{LISTENHOST}}:{{HEALTHPORT}};
   }
   server {
-    listen {{LISTENHOST}}:{{HEALTHSSLPORT}} ssl default_server;
-    server_name health;
+    listen {{LISTENHOST}}:{{HEALTHSSLPORT}} ssl;
+    server_name health health.*;
     ssl_certificate "{{SSLCERT}}";
     ssl_certificate_key "{{SSLKEY}}";
     location  / {
@@ -91,8 +107,8 @@ http {
     }
   }
   server {
-    listen {{LISTENHOST}}:{{KEEPWEBDLSSLPORT}} ssl default_server;
-    server_name keep-web-dl ~.*;
+    listen {{LISTENHOST}}:{{KEEPWEBDLSSLPORT}} ssl;
+    server_name keep-web-dl download.* ~.*;
     ssl_certificate "{{SSLCERT}}";
     ssl_certificate_key "{{SSLKEY}}";
     location  / {
@@ -111,8 +127,8 @@ http {
     server {{LISTENHOST}}:{{WSPORT}};
   }
   server {
-    listen {{LISTENHOST}}:{{WSSSLPORT}} ssl default_server;
-    server_name websocket;
+    listen {{LISTENHOST}}:{{WSSSLPORT}} ssl;
+    server_name websocket ws.*;
     ssl_certificate "{{SSLCERT}}";
     ssl_certificate_key "{{SSLKEY}}";
     location  / {
@@ -129,8 +145,8 @@ http {
     server {{LISTENHOST}}:{{WORKBENCH1PORT}};
   }
   server {
-    listen {{LISTENHOST}}:{{WORKBENCH1SSLPORT}} ssl default_server;
-    server_name workbench1;
+    listen {{LISTENHOST}}:{{WORKBENCH1SSLPORT}} ssl;
+    server_name workbench1 workbench.*;
     ssl_certificate "{{SSLCERT}}";
     ssl_certificate_key "{{SSLKEY}}";
     location  / {
@@ -141,20 +157,4 @@ http {
       proxy_redirect off;
     }
   }
-  upstream controller {
-    server {{LISTENHOST}}:{{CONTROLLERPORT}};
-  }
-  server {
-    listen {{LISTENHOST}}:{{CONTROLLERSSLPORT}} ssl default_server;
-    server_name controller;
-    ssl_certificate "{{SSLCERT}}";
-    ssl_certificate_key "{{SSLKEY}}";
-    location  / {
-      proxy_pass http://controller;
-      proxy_set_header Host $http_host;
-      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
-      proxy_set_header X-Forwarded-Proto https;
-      proxy_redirect off;
-    }
-  }
 }
index 6a0f7d9f49d0820476449998030b951688aab7a6..953021f0e7a18bc622920a19e8d7e73cc764ae13 100644 (file)
@@ -802,6 +802,10 @@ def setup_config():
                         "GitInternalDir": os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'internal.git'),
                     },
                     "SupportedDockerImageFormats": {"v1": {}},
+                    "ShellAccess": {
+                        "Admin": True,
+                        "User": True,
+                    },
                 },
                 "Volumes": {
                     "zzzzz-nyw5e-%015d"%n: {
index 7d5bea8faeb791482be7be26cc47834c42d88db5..f2bae3a4b5b2c79a9a3aacc882b7e570d713bc92 100644 (file)
@@ -877,7 +877,7 @@ class ArvadosModel < ApplicationRecord
   # request.
   def fill_container_defaults
     self.runtime_constraints = {
-      'api' => false,
+      'API' => false,
       'keep_cache_ram' => 0,
       'ram' => 0,
       'vcpus' => 0,
index d01787cbc7c0389e50fc0ebe421bc9bd0672f5ed..8feee77ff23553eaba0429125c1b06f3f5688d50 100644 (file)
@@ -77,6 +77,8 @@ class Container < ArvadosModel
     t.add :runtime_user_uuid
     t.add :runtime_auth_scopes
     t.add :lock_count
+    t.add :gateway_address
+    t.add :interactive_session_started
   end
 
   # Supported states for a container
@@ -102,11 +104,11 @@ class Container < ArvadosModel
   end
 
   def self.full_text_searchable_columns
-    super - ["secret_mounts", "secret_mounts_md5", "runtime_token"]
+    super - ["secret_mounts", "secret_mounts_md5", "runtime_token", "gateway_address"]
   end
 
   def self.searchable_columns *args
-    super - ["secret_mounts_md5", "runtime_token"]
+    super - ["secret_mounts_md5", "runtime_token", "gateway_address"]
   end
 
   def logged_attributes
@@ -478,7 +480,10 @@ class Container < ArvadosModel
     when Running
       permitted.push :priority, *progress_attrs
       if self.state_changed?
-        permitted.push :started_at
+        permitted.push :started_at, :gateway_address
+      end
+      if !self.interactive_session_started_was
+        permitted.push :interactive_session_started
       end
 
     when Complete
diff --git a/services/api/db/migrate/20210108033940_add_gateway_address_to_containers.rb b/services/api/db/migrate/20210108033940_add_gateway_address_to_containers.rb
new file mode 100644 (file)
index 0000000..8683b51
--- /dev/null
@@ -0,0 +1,9 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+class AddGatewayAddressToContainers < ActiveRecord::Migration[5.2]
+  def change
+    add_column :containers, :gateway_address, :string
+  end
+end
diff --git a/services/api/db/migrate/20210126183521_add_interactive_session_started_to_containers.rb b/services/api/db/migrate/20210126183521_add_interactive_session_started_to_containers.rb
new file mode 100644 (file)
index 0000000..3fe23f6
--- /dev/null
@@ -0,0 +1,9 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+class AddInteractiveSessionStartedToContainers < ActiveRecord::Migration[5.2]
+  def change
+    add_column :containers, :interactive_session_started, :boolean, null: false, default: false
+  end
+end
index 12a28c6c723609bd14b2c20129b8c749695bdc28..14eca609eb0e35c91215a2d70e5af898d90168d4 100644 (file)
@@ -521,7 +521,9 @@ CREATE TABLE public.containers (
     runtime_user_uuid text,
     runtime_auth_scopes jsonb,
     runtime_token text,
-    lock_count integer DEFAULT 0 NOT NULL
+    lock_count integer DEFAULT 0 NOT NULL,
+    gateway_address character varying,
+    interactive_session_started boolean DEFAULT false NOT NULL
 );
 
 
@@ -3187,6 +3189,8 @@ INSERT INTO "schema_migrations" (version) VALUES
 ('20200914203202'),
 ('20201103170213'),
 ('20201105190435'),
-('20201202174753');
+('20201202174753'),
+('20210108033940'),
+('20210126183521');
 
 
index 4d853852422bc5c7abeadd2b5e3d72601a2d7a37..35e2b7ed1d0501d02162e22cd08e3556107b2799 100644 (file)
@@ -23,7 +23,7 @@ class ContainerTest < ActiveSupport::TestCase
     command: ["echo", "hello"],
     output_path: "test",
     runtime_constraints: {
-      "api" => false,
+      "API" => false,
       "keep_cache_ram" => 0,
       "ram" => 12000000000,
       "vcpus" => 4,
@@ -229,7 +229,7 @@ class ContainerTest < ActiveSupport::TestCase
     set_user_from_auth :active
     env = {"C" => "3", "B" => "2", "A" => "1"}
     m = {"F" => {"kind" => "3"}, "E" => {"kind" => "2"}, "D" => {"kind" => "1"}}
-    rc = {"vcpus" => 1, "ram" => 1, "keep_cache_ram" => 1, "api" => true}
+    rc = {"vcpus" => 1, "ram" => 1, "keep_cache_ram" => 1, "API" => true}
     c, _ = minimal_new(environment: env, mounts: m, runtime_constraints: rc)
     c.reload
     assert_equal Container.deep_sort_hash(env).to_json, c.environment.to_json
@@ -803,6 +803,8 @@ class ContainerTest < ActiveSupport::TestCase
     [Container::Running, {priority: 123456789}],
     [Container::Running, {runtime_status: {'error' => 'oops'}}],
     [Container::Running, {cwd: '/'}],
+    [Container::Running, {gateway_address: "172.16.0.1:12345"}],
+    [Container::Running, {interactive_session_started: true}],
     [Container::Complete, {state: Container::Cancelled}],
     [Container::Complete, {priority: 123456789}],
     [Container::Complete, {runtime_status: {'error' => 'oops'}}],
index 65bd8d4cf098a17610953810ab8147f678616aee..8b4ee84c716e4596987b1371b38035610f9ffa2f 100644 (file)
@@ -9,13 +9,13 @@ import (
        "flag"
        "fmt"
        "io"
-       "net/http"
        "os"
 
        "git.arvados.org/arvados.git/lib/config"
        "git.arvados.org/arvados.git/lib/service"
        "git.arvados.org/arvados.git/sdk/go/arvados"
        "git.arvados.org/arvados.git/sdk/go/ctxlog"
+       "git.arvados.org/arvados.git/sdk/go/health"
        "github.com/prometheus/client_golang/prometheus"
        "github.com/sirupsen/logrus"
 )
@@ -83,7 +83,6 @@ func runCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.W
                        }
 
                        srv := &Server{
-                               Handler:    http.NotFoundHandler(),
                                Cluster:    cluster,
                                ArvClient:  ac,
                                RunOptions: options,
@@ -91,6 +90,11 @@ func runCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.W
                                Logger:     options.Logger,
                                Dumper:     options.Dumper,
                        }
+                       srv.Handler = &health.Handler{
+                               Token:  cluster.ManagementToken,
+                               Prefix: "/_health/",
+                               Routes: health.Routes{"ping": srv.CheckHealth},
+                       }
 
                        go srv.run()
                        return srv
index 4d8a0aec7ac06ec4f38e815fe0d23447723e38fa..26e6b731828f9be0861044cb6a7c4e10d097d05f 100644 (file)
@@ -315,7 +315,7 @@ func makeRRVolumeManager(logger logrus.FieldLogger, cluster *arvados.Cluster, my
                if err != nil {
                        return nil, fmt.Errorf("error initializing volume %s: %s", uuid, err)
                }
-               logger.Printf("started volume %s (%s), ReadOnly=%v", uuid, vol, cfgvol.ReadOnly)
+               logger.Printf("started volume %s (%s), ReadOnly=%v", uuid, vol, cfgvol.ReadOnly || va.ReadOnly)
 
                sc := cfgvol.StorageClasses
                if len(sc) == 0 {
index b83207bd28286ccceac19190c985124c9b635029..6a1c45da2676d903f03155c92f1ccfd13bb09f41 100644 (file)
@@ -6,7 +6,6 @@
     "build_environment": "aws",
     "arvados_cluster": "",
     "aws_source_ami": "ami-04d70e069399af2e9",
-    "fqdn": "",
     "ssh_user": "admin",
     "vpc_id": "",
     "subnet_id": "",
@@ -75,6 +74,6 @@
     "type": "shell",
     "execute_command": "sudo -S env {{ .Vars }} /bin/bash '{{ .Path }}'",
     "script": "scripts/base.sh",
-    "environment_vars": ["ROLE=compute","RESOLVER={{user `resolver`}}","REPOSUFFIX={{user `reposuffix`}}"]
+    "environment_vars": ["RESOLVER={{user `resolver`}}","REPOSUFFIX={{user `reposuffix`}}"]
   }]
 }
index c8db9499cda716240ed8ba421d9f72d496a0bf27..a0278d515af8c5d9515dc2f5833f6310ae9e8149 100644 (file)
@@ -14,7 +14,6 @@
     "arvados_cluster": "",
     "project_id": "",
     "account_file": "",
-    "fqdn": "",
     "resolver": "",
     "reposuffix": "",
     "public_key_file": ""
@@ -66,6 +65,6 @@
     "type": "shell",
     "execute_command": "sudo -S env {{ .Vars }} /bin/bash '{{ .Path }}'",
     "script": "scripts/base.sh",
-    "environment_vars": ["ROLE=compute","RESOLVER={{user `resolver`}}","REPOSUFFIX={{user `reposuffix`}}"]
+    "environment_vars": ["RESOLVER={{user `resolver`}}","REPOSUFFIX={{user `reposuffix`}}"]
   }]
 }
index 030eb410b8d52fcf7c1e72e2a8be79c0af90bf7d..fb02ce944210c852b5e9d6cc3c3919d2abc7645d 100755 (executable)
@@ -49,8 +49,6 @@ Options:
       Azure SKU image to use
   --ssh_user  (default: packer)
       The user packer will use to log into the image
-  --domain  (default: arvadosapi.com)
-      The domain part of the FQDN for the cluster
   --resolver (default: 8.8.8.8)
       The dns resolver for the machine
   --reposuffix (default: unset)
@@ -78,12 +76,11 @@ AZURE_LOCATION=
 AZURE_CLOUD_ENVIRONMENT=
 DEBUG=
 SSH_USER=
-DOMAIN="arvadosapi.com"
 AWS_DEFAULT_REGION=us-east-1
 PUBLIC_KEY_FILE=
 
 PARSEDOPTS=$(getopt --name "$0" --longoptions \
-    help,json-file:,arvados-cluster-id:,aws-source-ami:,aws-profile:,aws-secrets-file:,aws-region:,aws-vpc-id:,aws-subnet-id:,gcp-project-id:,gcp-account-file:,gcp-zone:,azure-secrets-file:,azure-resource-group:,azure-location:,azure-sku:,azure-cloud-environment:,ssh_user:,domain:,resolver:,reposuffix:,public-key-file:,debug \
+    help,json-file:,arvados-cluster-id:,aws-source-ami:,aws-profile:,aws-secrets-file:,aws-region:,aws-vpc-id:,aws-subnet-id:,gcp-project-id:,gcp-account-file:,gcp-zone:,azure-secrets-file:,azure-resource-group:,azure-location:,azure-sku:,azure-cloud-environment:,ssh_user:,resolver:,reposuffix:,public-key-file:,debug \
     -- "" "$@")
 if [ $? -ne 0 ]; then
     exit 1
@@ -148,9 +145,6 @@ while [ $# -gt 0 ]; do
         --ssh_user)
             SSH_USER="$2"; shift
             ;;
-        --domain)
-            DOMAIN="$2"; shift
-            ;;
         --resolver)
             RESOLVER="$2"; shift
             ;;
@@ -211,7 +205,6 @@ if [[ ! -z "$AZURE_SECRETS_FILE" ]]; then
   source $AZURE_SECRETS_FILE
 fi
 
-FQDN=" -var fqdn=compute.$ARVADOS_CLUSTER_ID.$DOMAIN ";
 
 EXTRA2=""
 
@@ -264,5 +257,5 @@ if [[ "$PUBLIC_KEY_FILE" != "" ]]; then
   EXTRA2+=" -var public_key_file=$PUBLIC_KEY_FILE"
 fi
 
-echo packer build$EXTRA$FQDN -var "role=$role" -var "arvados_cluster=$ARVADOS_CLUSTER_ID"$EXTRA2 $JSON_FILE
-packer build$EXTRA$FQDN -var "role=$role" -var "arvados_cluster=$ARVADOS_CLUSTER_ID"$EXTRA2 $JSON_FILE
+echo packer build$EXTRA -var "arvados_cluster=$ARVADOS_CLUSTER_ID"$EXTRA2 $JSON_FILE
+packer build$EXTRA -var "arvados_cluster=$ARVADOS_CLUSTER_ID"$EXTRA2 $JSON_FILE