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

container/exec: Support ConsoleSize
This commit is contained in:
Sebastiaan van Stijn 2022-06-24 14:31:39 +02:00 committed by GitHub
commit 4eb1c5bd52
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 142 additions and 11 deletions

View file

@ -17,7 +17,7 @@ type execBackend interface {
ContainerExecCreate(name string, config *types.ExecConfig) (string, error) ContainerExecCreate(name string, config *types.ExecConfig) (string, error)
ContainerExecInspect(id string) (*backend.ExecInspect, error) ContainerExecInspect(id string) (*backend.ExecInspect, error)
ContainerExecResize(name string, height, width int) 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) ExecExists(name string) (bool, error)
} }

View file

@ -9,6 +9,7 @@ import (
"github.com/docker/docker/api/server/httputils" "github.com/docker/docker/api/server/httputils"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/versions" "github.com/docker/docker/api/types/versions"
"github.com/docker/docker/errdefs" "github.com/docker/docker/errdefs"
"github.com/docker/docker/pkg/stdcopy" "github.com/docker/docker/pkg/stdcopy"
@ -46,6 +47,12 @@ func (s *containerRouter) postContainerExecCreate(ctx context.Context, w http.Re
return execCommandError{} 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. // Register an instance of Exec in container.
id, err := s.backend.ContainerExecCreate(vars["name"], execConfig) id, err := s.backend.ContainerExecCreate(vars["name"], execConfig)
if err != nil { if err != nil {
@ -88,6 +95,18 @@ func (s *containerRouter) postContainerExecStart(ctx context.Context, w http.Res
return err 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 { if !execStartCheck.Detach {
var err error var err error
// Setting up the streaming http interface. // 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. // Now run the user process in container.
// Maybe we should we pass ctx here if we're not detaching? // 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 { if execStartCheck.Detach {
return err return err
} }

View file

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

View file

@ -33,6 +33,7 @@ type ExecConfig struct {
User string // User that will run the command User string // User that will run the command
Privileged bool // Is the container in privileged mode Privileged bool // Is the container in privileged mode
Tty bool // Attach standard streams to a tty. 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 AttachStdin bool // Attach the standard input, makes possible user interaction
AttachStderr bool // Attach the standard error AttachStderr bool // Attach the standard error
AttachStdout bool // Attach the standard output AttachStdout bool // Attach the standard output

View file

@ -1,6 +1,7 @@
package container // import "github.com/docker/docker/api/types/container" package container // import "github.com/docker/docker/api/types/container"
import ( import (
"io"
"time" "time"
"github.com/docker/docker/api/types/strslice" "github.com/docker/docker/api/types/strslice"
@ -52,6 +53,14 @@ type HealthConfig struct {
Retries int `json:",omitempty"` 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. // Config contains the configuration data about a container.
// It should hold only portable information about the container. // It should hold only portable information about the container.
// Here, "portable" means "independent from the host we are running on". // Here, "portable" means "independent from the host we are running on".

View file

@ -390,6 +390,8 @@ type ExecStartCheck struct {
Detach bool Detach bool
// Check if there's a tty // Check if there's a tty
Tty bool 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 // HealthcheckResult stores information about a single run of a healthcheck probe

View file

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

View file

@ -9,6 +9,7 @@ import (
"time" "time"
"github.com/docker/docker/api/types" "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/api/types/strslice"
"github.com/docker/docker/container" "github.com/docker/docker/container"
"github.com/docker/docker/container/stream" "github.com/docker/docker/container/stream"
@ -121,6 +122,7 @@ func (daemon *Daemon) ContainerExecCreate(name string, config *types.ExecConfig)
execConfig.Entrypoint = entrypoint execConfig.Entrypoint = entrypoint
execConfig.Args = args execConfig.Args = args
execConfig.Tty = config.Tty execConfig.Tty = config.Tty
execConfig.ConsoleSize = config.ConsoleSize
execConfig.Privileged = config.Privileged execConfig.Privileged = config.Privileged
execConfig.User = config.User execConfig.User = config.User
execConfig.WorkingDir = config.WorkingDir 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 // ContainerExecStart starts a previously set up exec instance. The
// std streams are set up. // std streams are set up.
// If ctx is cancelled, the process is terminated. // 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 ( var (
cStdin io.ReadCloser cStdin io.ReadCloser
cStdout, cStderr io.Writer 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() r, w := io.Pipe()
go func() { go func() {
defer w.Close() defer w.Close()
defer logrus.Debug("Closing buffered stdin pipe") defer logrus.Debug("Closing buffered stdin pipe")
pools.Copy(w, stdin) pools.Copy(w, options.Stdin)
}() }()
cStdin = r cStdin = r
} }
if ec.OpenStdout { if ec.OpenStdout {
cStdout = stdout cStdout = options.Stdout
} }
if ec.OpenStderr { if ec.OpenStderr {
cStderr = stderr cStderr = options.Stderr
} }
if ec.OpenStdin { if ec.OpenStdin {
@ -238,6 +240,18 @@ func (daemon *Daemon) ContainerExecStart(ctx context.Context, name string, stdin
p.Cwd = ec.WorkingDir p.Cwd = ec.WorkingDir
p.Terminal = ec.Tty 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 == "" { if p.Cwd == "" {
p.Cwd = "/" p.Cwd = "/"
} }

View file

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

View file

@ -10,6 +10,7 @@ import (
"time" "time"
"github.com/docker/docker/api/types" "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/api/types/strslice"
"github.com/docker/docker/container" "github.com/docker/docker/container"
"github.com/docker/docker/daemon/exec" "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) probeCtx, cancelProbe := context.WithCancel(ctx)
defer cancelProbe() defer cancelProbe()
execErr := make(chan error, 1) 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 // 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 // of 1s in extreme cases. The time it takes dockerd and containerd to

View file

@ -101,6 +101,8 @@ keywords: "API, Docker, rcli, REST, documentation"
created if missing. This brings parity with `Binds` created if missing. This brings parity with `Binds`
* `POST /containers/create` rejects request if BindOptions|VolumeOptions|TmpfsOptions * `POST /containers/create` rejects request if BindOptions|VolumeOptions|TmpfsOptions
is set with a non-matching mount Type. 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 ## v1.41 API changes

View file

@ -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")
}

View file

@ -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, testEnv.DaemonInfo.OSType != "linux")
skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.42"), "skip test from new feature") skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.42"), "skip test from new feature")

View file

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