Merge pull request #43622 from vvoland/3554-exec-size
container/exec: Support ConsoleSize
This commit is contained in:
commit
4eb1c5bd52
14 changed files with 142 additions and 11 deletions
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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".
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"},
|
||||
}
|
||||
|
|
|
@ -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 = "/"
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ type Config struct {
|
|||
WorkingDir string
|
||||
Env []string
|
||||
Pid int
|
||||
ConsoleSize *[2]uint
|
||||
}
|
||||
|
||||
// NewConfig initializes the a new exec configuration
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
integration/container/exec_linux_test.go
Normal file
34
integration/container/exec_linux_test.go
Normal 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")
|
||||
}
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue