19175: Merge branch 'main' into 19175-doc-refactor-multi-host-installation
[arvados.git] / lib / crunchrun / docker.go
index f3808cb357aa5778e8c92c27a25b02edc6ee3419..54d0e680fe6af83494707f58298629db74ded486 100644 (file)
@@ -8,7 +8,9 @@ import (
        "io"
        "io/ioutil"
        "os"
+       "os/exec"
        "strings"
+       "sync/atomic"
        "time"
 
        "git.arvados.org/arvados.git/sdk/go/arvados"
@@ -21,12 +23,21 @@ import (
 // Docker daemon won't let you set a limit less than ~10 MiB
 const minDockerRAM = int64(16 * 1024 * 1024)
 
+// DockerAPIVersion is the API version we use to communicate with the
+// docker service.  The oldest OS we support is Ubuntu 18.04 (bionic)
+// which originally shipped docker 1.17.12 / API 1.35 so there is no
+// reason to use an older API version.  See
+// https://dev.arvados.org/issues/15370#note-38 and
+// https://docs.docker.com/engine/api/.
+const DockerAPIVersion = "1.35"
+
 type dockerExecutor struct {
        containerUUID    string
        logf             func(string, ...interface{})
        watchdogInterval time.Duration
        dockerclient     *dockerclient.Client
        containerID      string
+       savedIPAddress   atomic.Value
        doneIO           chan struct{}
        errIO            error
 }
@@ -34,7 +45,7 @@ type dockerExecutor struct {
 func newDockerExecutor(containerUUID string, logf func(string, ...interface{}), watchdogInterval time.Duration) (*dockerExecutor, error) {
        // API version 1.21 corresponds to Docker 1.9, which is
        // currently the minimum version we want to support.
-       client, err := dockerclient.NewClient(dockerclient.DefaultDockerHost, "1.21", nil, nil)
+       client, err := dockerclient.NewClient(dockerclient.DefaultDockerHost, DockerAPIVersion, nil, nil)
        if watchdogInterval < 1 {
                watchdogInterval = time.Minute
        }
@@ -310,3 +321,34 @@ func (e *dockerExecutor) handleStdoutStderr(stdout, stderr io.Writer, reader io.
 func (e *dockerExecutor) Close() {
        e.dockerclient.ContainerRemove(context.TODO(), e.containerID, dockertypes.ContainerRemoveOptions{Force: true})
 }
+
+func (e *dockerExecutor) InjectCommand(ctx context.Context, detachKeys, username string, usingTTY bool, injectcmd []string) (*exec.Cmd, error) {
+       cmd := exec.CommandContext(ctx, "docker", "exec", "-i", "--detach-keys="+detachKeys, "--user="+username)
+       if usingTTY {
+               cmd.Args = append(cmd.Args, "-t")
+       }
+       cmd.Args = append(cmd.Args, e.containerID)
+       cmd.Args = append(cmd.Args, injectcmd...)
+       return cmd, nil
+}
+
+func (e *dockerExecutor) IPAddress() (string, error) {
+       if ip, ok := e.savedIPAddress.Load().(*string); ok {
+               return *ip, nil
+       }
+       ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Minute))
+       defer cancel()
+       ctr, err := e.dockerclient.ContainerInspect(ctx, e.containerID)
+       if err != nil {
+               return "", fmt.Errorf("cannot get docker container info: %s", err)
+       }
+       ip := ctr.NetworkSettings.IPAddress
+       if ip == "" {
+               // TODO: try to enable networking if it wasn't
+               // already enabled when the container was
+               // created.
+               return "", fmt.Errorf("container has no IP address")
+       }
+       e.savedIPAddress.Store(&ip)
+       return ip, nil
+}