From 56a20dbc19786633f605d3871f5242d8e0f1be5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Gronowski?= Date: Wed, 15 Jun 2022 09:28:20 +0200 Subject: [PATCH] container/exec: Support ConsoleSize MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Now client have the possibility to set the console size of the executed process immediately at the creation. This makes a difference for example when executing commands that output some kind of text user interface which is bounded by the console dimensions. Signed-off-by: Paweł Gronowski --- api/server/router/container/backend.go | 2 +- api/server/router/container/exec.go | 28 ++++++++++++++++++- api/swagger.yaml | 22 ++++++++++++++- api/types/configs.go | 1 + api/types/container/config.go | 9 +++++++ api/types/types.go | 2 ++ client/container_exec.go | 10 +++++++ daemon/exec.go | 24 +++++++++++++---- daemon/exec/exec.go | 1 + daemon/health.go | 9 ++++++- docs/api/version-history.md | 2 ++ integration/container/exec_linux_test.go | 34 ++++++++++++++++++++++++ integration/container/run_linux_test.go | 2 +- integration/internal/container/exec.go | 7 ++++- 14 files changed, 142 insertions(+), 11 deletions(-) create mode 100644 integration/container/exec_linux_test.go diff --git a/api/server/router/container/backend.go b/api/server/router/container/backend.go index 6145c51de1..4db989a102 100644 --- a/api/server/router/container/backend.go +++ b/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) } diff --git a/api/server/router/container/exec.go b/api/server/router/container/exec.go index 3187fe6c83..b86af42d7f 100644 --- a/api/server/router/container/exec.go +++ b/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 } diff --git a/api/swagger.yaml b/api/swagger.yaml index a087de59dd..8f223c4a90 100644 --- a/api/swagger.yaml +++ b/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" diff --git a/api/types/configs.go b/api/types/configs.go index 3dd133a3a5..7689f38b33 100644 --- a/api/types/configs.go +++ b/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 diff --git a/api/types/container/config.go b/api/types/container/config.go index b073e5dd36..077583e66c 100644 --- a/api/types/container/config.go +++ b/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". diff --git a/api/types/types.go b/api/types/types.go index d368507282..8cec40fd52 100644 --- a/api/types/types.go +++ b/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 diff --git a/client/container_exec.go b/client/container_exec.go index e54da00fc6..6a2cb006f8 100644 --- a/client/container_exec.go +++ b/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"}, } diff --git a/daemon/exec.go b/daemon/exec.go index f53709e333..08687760a7 100644 --- a/daemon/exec.go +++ b/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 = "/" } diff --git a/daemon/exec/exec.go b/daemon/exec/exec.go index f1b5259854..2cf1833d7d 100644 --- a/daemon/exec/exec.go +++ b/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 diff --git a/daemon/health.go b/daemon/health.go index 6fccbdb546..335f91e0ec 100644 --- a/daemon/health.go +++ b/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 diff --git a/docs/api/version-history.md b/docs/api/version-history.md index 2bcddffe67..8630a91518 100644 --- a/docs/api/version-history.md +++ b/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 diff --git a/integration/container/exec_linux_test.go b/integration/container/exec_linux_test.go new file mode 100644 index 0000000000..812fdb5ea7 --- /dev/null +++ b/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") +} diff --git a/integration/container/run_linux_test.go b/integration/container/run_linux_test.go index bd1fc6e932..e0316dde3b 100644 --- a/integration/container/run_linux_test.go +++ b/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") diff --git a/integration/internal/container/exec.go b/integration/internal/container/exec.go index dfd46a2436..98e2a6c481 100644 --- a/integration/internal/container/exec.go +++ b/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