Browse Source

Merge pull request #43622 from vvoland/3554-exec-size

container/exec: Support ConsoleSize
Sebastiaan van Stijn 3 years ago
parent
commit
4eb1c5bd52

+ 1 - 1
api/server/router/container/backend.go

@@ -17,7 +17,7 @@ type execBackend interface {
 	ContainerExecCreate(name string, config *types.ExecConfig) (string, error)
 	ContainerExecInspect(id string) (*backend.ExecInspect, error)
 	ContainerExecResize(name string, height, width int) error
-	ContainerExecStart(ctx context.Context, name string, stdin io.Reader, stdout io.Writer, stderr io.Writer) error
+	ContainerExecStart(ctx context.Context, name string, options container.ExecStartOptions) error
 	ExecExists(name string) (bool, error)
 }
 

+ 27 - 1
api/server/router/container/exec.go

@@ -9,6 +9,7 @@ import (
 
 	"github.com/docker/docker/api/server/httputils"
 	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/container"
 	"github.com/docker/docker/api/types/versions"
 	"github.com/docker/docker/errdefs"
 	"github.com/docker/docker/pkg/stdcopy"
@@ -46,6 +47,12 @@ func (s *containerRouter) postContainerExecCreate(ctx context.Context, w http.Re
 		return execCommandError{}
 	}
 
+	version := httputils.VersionFromContext(ctx)
+	if versions.LessThan(version, "1.42") {
+		// Not supported by API versions before 1.42
+		execConfig.ConsoleSize = nil
+	}
+
 	// Register an instance of Exec in container.
 	id, err := s.backend.ContainerExecCreate(vars["name"], execConfig)
 	if err != nil {
@@ -88,6 +95,18 @@ func (s *containerRouter) postContainerExecStart(ctx context.Context, w http.Res
 		return err
 	}
 
+	if execStartCheck.ConsoleSize != nil {
+		// Not supported before 1.42
+		if versions.LessThan(version, "1.42") {
+			execStartCheck.ConsoleSize = nil
+		}
+
+		// No console without tty
+		if !execStartCheck.Tty {
+			execStartCheck.ConsoleSize = nil
+		}
+	}
+
 	if !execStartCheck.Detach {
 		var err error
 		// Setting up the streaming http interface.
@@ -121,9 +140,16 @@ func (s *containerRouter) postContainerExecStart(ctx context.Context, w http.Res
 		}
 	}
 
+	options := container.ExecStartOptions{
+		Stdin:       stdin,
+		Stdout:      stdout,
+		Stderr:      stderr,
+		ConsoleSize: execStartCheck.ConsoleSize,
+	}
+
 	// Now run the user process in container.
 	// Maybe we should we pass ctx here if we're not detaching?
-	if err := s.backend.ContainerExecStart(context.Background(), execName, stdin, stdout, stderr); err != nil {
+	if err := s.backend.ContainerExecStart(context.Background(), execName, options); err != nil {
 		if execStartCheck.Detach {
 			return err
 		}

+ 21 - 1
api/swagger.yaml

@@ -966,6 +966,7 @@ definitions:
             type: "array"
             description: |
               Initial console size, as an `[height, width]` array.
+            x-nullable: true
             minItems: 2
             maxItems: 2
             items:
@@ -9207,6 +9208,15 @@ paths:
               AttachStderr:
                 type: "boolean"
                 description: "Attach to `stderr` of the exec command."
+              ConsoleSize:
+                type: "array"
+                description: "Initial console size, as an `[height, width]` array."
+                x-nullable: true
+                minItems: 2
+                maxItems: 2
+                items:
+                  type: "integer"
+                  minimum: 0
               DetachKeys:
                 type: "string"
                 description: |
@@ -9296,9 +9306,19 @@ paths:
               Tty:
                 type: "boolean"
                 description: "Allocate a pseudo-TTY."
+              ConsoleSize:
+                type: "array"
+                description: "Initial console size, as an `[height, width]` array."
+                x-nullable: true
+                minItems: 2
+                maxItems: 2
+                items:
+                  type: "integer"
+                  minimum: 0
             example:
               Detach: false
-              Tty: false
+              Tty: true
+              ConsoleSize: [80, 64]
         - name: "id"
           in: "path"
           description: "Exec instance ID"

+ 1 - 0
api/types/configs.go

@@ -33,6 +33,7 @@ type ExecConfig struct {
 	User         string   // User that will run the command
 	Privileged   bool     // Is the container in privileged mode
 	Tty          bool     // Attach standard streams to a tty.
+	ConsoleSize  *[2]uint `json:",omitempty"` // Initial console size [height, width]
 	AttachStdin  bool     // Attach the standard input, makes possible user interaction
 	AttachStderr bool     // Attach the standard error
 	AttachStdout bool     // Attach the standard output

+ 9 - 0
api/types/container/config.go

@@ -1,6 +1,7 @@
 package container // import "github.com/docker/docker/api/types/container"
 
 import (
+	"io"
 	"time"
 
 	"github.com/docker/docker/api/types/strslice"
@@ -52,6 +53,14 @@ type HealthConfig struct {
 	Retries int `json:",omitempty"`
 }
 
+// ExecStartOptions holds the options to start container's exec.
+type ExecStartOptions struct {
+	Stdin       io.Reader
+	Stdout      io.Writer
+	Stderr      io.Writer
+	ConsoleSize *[2]uint `json:",omitempty"`
+}
+
 // Config contains the configuration data about a container.
 // It should hold only portable information about the container.
 // Here, "portable" means "independent from the host we are running on".

+ 2 - 0
api/types/types.go

@@ -390,6 +390,8 @@ type ExecStartCheck struct {
 	Detach bool
 	// Check if there's a tty
 	Tty bool
+	// Terminal size [height, width], unused if Tty == false
+	ConsoleSize *[2]uint `json:",omitempty"`
 }
 
 // HealthcheckResult stores information about a single run of a healthcheck probe

+ 10 - 0
client/container_exec.go

@@ -5,6 +5,7 @@ import (
 	"encoding/json"
 
 	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/versions"
 )
 
 // ContainerExecCreate creates a new exec configuration to run an exec process.
@@ -14,6 +15,9 @@ func (cli *Client) ContainerExecCreate(ctx context.Context, container string, co
 	if err := cli.NewVersionError("1.25", "env"); len(config.Env) != 0 && err != nil {
 		return response, err
 	}
+	if versions.LessThan(cli.ClientVersion(), "1.42") {
+		config.ConsoleSize = nil
+	}
 
 	resp, err := cli.post(ctx, "/containers/"+container+"/exec", nil, config, nil)
 	defer ensureReaderClosed(resp)
@@ -26,6 +30,9 @@ func (cli *Client) ContainerExecCreate(ctx context.Context, container string, co
 
 // ContainerExecStart starts an exec process already created in the docker host.
 func (cli *Client) ContainerExecStart(ctx context.Context, execID string, config types.ExecStartCheck) error {
+	if versions.LessThan(cli.ClientVersion(), "1.42") {
+		config.ConsoleSize = nil
+	}
 	resp, err := cli.post(ctx, "/exec/"+execID+"/start", nil, config, nil)
 	ensureReaderClosed(resp)
 	return err
@@ -36,6 +43,9 @@ func (cli *Client) ContainerExecStart(ctx context.Context, execID string, config
 // and the a reader to get output. It's up to the called to close
 // the hijacked connection by calling types.HijackedResponse.Close.
 func (cli *Client) ContainerExecAttach(ctx context.Context, execID string, config types.ExecStartCheck) (types.HijackedResponse, error) {
+	if versions.LessThan(cli.ClientVersion(), "1.42") {
+		config.ConsoleSize = nil
+	}
 	headers := map[string][]string{
 		"Content-Type": {"application/json"},
 	}

+ 19 - 5
daemon/exec.go

@@ -9,6 +9,7 @@ import (
 	"time"
 
 	"github.com/docker/docker/api/types"
+	containertypes "github.com/docker/docker/api/types/container"
 	"github.com/docker/docker/api/types/strslice"
 	"github.com/docker/docker/container"
 	"github.com/docker/docker/container/stream"
@@ -121,6 +122,7 @@ func (daemon *Daemon) ContainerExecCreate(name string, config *types.ExecConfig)
 	execConfig.Entrypoint = entrypoint
 	execConfig.Args = args
 	execConfig.Tty = config.Tty
+	execConfig.ConsoleSize = config.ConsoleSize
 	execConfig.Privileged = config.Privileged
 	execConfig.User = config.User
 	execConfig.WorkingDir = config.WorkingDir
@@ -150,7 +152,7 @@ func (daemon *Daemon) ContainerExecCreate(name string, config *types.ExecConfig)
 // ContainerExecStart starts a previously set up exec instance. The
 // std streams are set up.
 // If ctx is cancelled, the process is terminated.
-func (daemon *Daemon) ContainerExecStart(ctx context.Context, name string, stdin io.Reader, stdout io.Writer, stderr io.Writer) (err error) {
+func (daemon *Daemon) ContainerExecStart(ctx context.Context, name string, options containertypes.ExecStartOptions) (err error) {
 	var (
 		cStdin           io.ReadCloser
 		cStdout, cStderr io.Writer
@@ -199,20 +201,20 @@ func (daemon *Daemon) ContainerExecStart(ctx context.Context, name string, stdin
 		}
 	}()
 
-	if ec.OpenStdin && stdin != nil {
+	if ec.OpenStdin && options.Stdin != nil {
 		r, w := io.Pipe()
 		go func() {
 			defer w.Close()
 			defer logrus.Debug("Closing buffered stdin pipe")
-			pools.Copy(w, stdin)
+			pools.Copy(w, options.Stdin)
 		}()
 		cStdin = r
 	}
 	if ec.OpenStdout {
-		cStdout = stdout
+		cStdout = options.Stdout
 	}
 	if ec.OpenStderr {
-		cStderr = stderr
+		cStderr = options.Stderr
 	}
 
 	if ec.OpenStdin {
@@ -238,6 +240,18 @@ func (daemon *Daemon) ContainerExecStart(ctx context.Context, name string, stdin
 	p.Cwd = ec.WorkingDir
 	p.Terminal = ec.Tty
 
+	consoleSize := options.ConsoleSize
+	// If size isn't specified for start, use the one provided for create
+	if consoleSize == nil {
+		consoleSize = ec.ConsoleSize
+	}
+	if p.Terminal && consoleSize != nil {
+		p.ConsoleSize = &specs.Box{
+			Height: consoleSize[0],
+			Width:  consoleSize[1],
+		}
+	}
+
 	if p.Cwd == "" {
 		p.Cwd = "/"
 	}

+ 1 - 0
daemon/exec/exec.go

@@ -35,6 +35,7 @@ type Config struct {
 	WorkingDir   string
 	Env          []string
 	Pid          int
+	ConsoleSize  *[2]uint
 }
 
 // NewConfig initializes the a new exec configuration

+ 8 - 1
daemon/health.go

@@ -10,6 +10,7 @@ import (
 	"time"
 
 	"github.com/docker/docker/api/types"
+	containertypes "github.com/docker/docker/api/types/container"
 	"github.com/docker/docker/api/types/strslice"
 	"github.com/docker/docker/container"
 	"github.com/docker/docker/daemon/exec"
@@ -97,7 +98,13 @@ func (p *cmdProbe) run(ctx context.Context, d *Daemon, cntr *container.Container
 	probeCtx, cancelProbe := context.WithCancel(ctx)
 	defer cancelProbe()
 	execErr := make(chan error, 1)
-	go func() { execErr <- d.ContainerExecStart(probeCtx, execConfig.ID, nil, output, output) }()
+
+	options := containertypes.ExecStartOptions{
+		Stdout: output,
+		Stderr: output,
+	}
+
+	go func() { execErr <- d.ContainerExecStart(probeCtx, execConfig.ID, options) }()
 
 	// Starting an exec can take a significant amount of time: on the order
 	// of 1s in extreme cases. The time it takes dockerd and containerd to

+ 2 - 0
docs/api/version-history.md

@@ -101,6 +101,8 @@ keywords: "API, Docker, rcli, REST, documentation"
   created if missing. This brings parity with `Binds`
 * `POST /containers/create` rejects request if BindOptions|VolumeOptions|TmpfsOptions
   is set with a non-matching mount Type.
+* `POST /containers/{id}/exec` now accepts an optional `ConsoleSize` parameter.
+  It allows to set the console size of the executed process immediately when it's created.
 
 ## v1.41 API changes
 

+ 34 - 0
integration/container/exec_linux_test.go

@@ -0,0 +1,34 @@
+package container // import "github.com/docker/docker/integration/container"
+
+import (
+	"context"
+	"strings"
+	"testing"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/versions"
+	"github.com/docker/docker/integration/internal/container"
+	"gotest.tools/v3/assert"
+	"gotest.tools/v3/skip"
+)
+
+func TestExecConsoleSize(t *testing.T) {
+	skip.If(t, testEnv.DaemonInfo.OSType != "linux")
+	skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.42"), "skip test from new feature")
+
+	defer setupTest(t)()
+	client := testEnv.APIClient()
+	ctx := context.Background()
+
+	cID := container.Run(ctx, t, client, container.WithImage("busybox"))
+
+	result, err := container.Exec(ctx, client, cID, []string{"stty", "size"},
+		func(ec *types.ExecConfig) {
+			ec.Tty = true
+			ec.ConsoleSize = &[2]uint{57, 123}
+		},
+	)
+
+	assert.NilError(t, err)
+	assert.Equal(t, strings.TrimSpace(result.Stdout()), "57 123")
+}

+ 1 - 1
integration/container/run_linux_test.go

@@ -187,7 +187,7 @@ func TestPrivilegedHostDevices(t *testing.T) {
 	}
 }
 
-func TestConsoleSize(t *testing.T) {
+func TestRunConsoleSize(t *testing.T) {
 	skip.If(t, testEnv.DaemonInfo.OSType != "linux")
 	skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.42"), "skip test from new feature")
 

+ 6 - 1
integration/internal/container/exec.go

@@ -35,13 +35,18 @@ func (res *ExecResult) Combined() string {
 // containing stdout, stderr, and exit code. Note:
 //  - this is a synchronous operation;
 //  - cmd stdin is closed.
-func Exec(ctx context.Context, cli client.APIClient, id string, cmd []string) (ExecResult, error) {
+func Exec(ctx context.Context, cli client.APIClient, id string, cmd []string, ops ...func(*types.ExecConfig)) (ExecResult, error) {
 	// prepare exec
 	execConfig := types.ExecConfig{
 		AttachStdout: true,
 		AttachStderr: true,
 		Cmd:          cmd,
 	}
+
+	for _, op := range ops {
+		op(&execConfig)
+	}
+
 	cresp, err := cli.ContainerExecCreate(ctx, id, execConfig)
 	if err != nil {
 		return ExecResult{}, err