client/container_exec: Wrap options and result, rename to Exec

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
This commit is contained in:
Paweł Gronowski
2025-10-22 12:49:41 +02:00
committed by Sebastiaan van Stijn
parent 01c36ec0cd
commit 94ab385eb5
14 changed files with 190 additions and 126 deletions

View File

@@ -90,11 +90,11 @@ type ContainerAPIClient interface {
}
type ExecAPIClient interface {
ContainerExecCreate(ctx context.Context, container string, options ExecCreateOptions) (container.ExecCreateResponse, error)
ContainerExecStart(ctx context.Context, execID string, options ExecStartOptions) error
ContainerExecAttach(ctx context.Context, execID string, options ExecAttachOptions) (HijackedResponse, error)
ContainerExecInspect(ctx context.Context, execID string) (ExecInspect, error)
ContainerExecResize(ctx context.Context, execID string, options ContainerResizeOptions) error
ExecCreate(ctx context.Context, container string, options ExecCreateOptions) (ExecCreateResult, error)
ExecStart(ctx context.Context, execID string, options ExecStartOptions) (ExecStartResult, error)
ExecAttach(ctx context.Context, execID string, options ExecAttachOptions) (ExecAttachResult, error)
ExecInspect(ctx context.Context, execID string, options ExecInspectOptions) (ExecInspectResult, error)
ExecResize(ctx context.Context, execID string, options ExecResizeOptions) (ExecResizeResult, error)
}
// DistributionAPIClient defines API client methods for the registry

View File

@@ -24,11 +24,16 @@ type ExecCreateOptions struct {
Cmd []string // Execution commands and args
}
// ContainerExecCreate creates a new exec configuration to run an exec process.
func (cli *Client) ContainerExecCreate(ctx context.Context, containerID string, options ExecCreateOptions) (container.ExecCreateResponse, error) {
// ExecCreateResult holds the result of creating a container exec.
type ExecCreateResult struct {
container.ExecCreateResponse
}
// ExecCreate creates a new exec configuration to run an exec process.
func (cli *Client) ExecCreate(ctx context.Context, containerID string, options ExecCreateOptions) (ExecCreateResult, error) {
containerID, err := trimID("container", containerID)
if err != nil {
return container.ExecCreateResponse{}, err
return ExecCreateResult{}, err
}
req := container.ExecCreateRequest{
@@ -48,17 +53,15 @@ func (cli *Client) ContainerExecCreate(ctx context.Context, containerID string,
resp, err := cli.post(ctx, "/containers/"+containerID+"/exec", nil, req, nil)
defer ensureReaderClosed(resp)
if err != nil {
return container.ExecCreateResponse{}, err
return ExecCreateResult{}, err
}
var response container.ExecCreateResponse
err = json.NewDecoder(resp.Body).Decode(&response)
return response, err
return ExecCreateResult{ExecCreateResponse: response}, err
}
// ExecStartOptions is a temp struct used by execStart
// Config fields is part of ExecConfig in runconfig package
type ExecStartOptions struct {
type execStartAttachOptions struct {
// ExecStart will first check if it's detached
Detach bool
// Check if there's a tty
@@ -67,24 +70,34 @@ type ExecStartOptions struct {
ConsoleSize *[2]uint `json:",omitempty"`
}
// ContainerExecStart starts an exec process already created in the docker host.
func (cli *Client) ContainerExecStart(ctx context.Context, execID string, config ExecStartOptions) error {
// ExecStartOptions holds options for starting a container exec.
type ExecStartOptions execStartAttachOptions
// ExecStartResult holds the result of starting a container exec.
type ExecStartResult struct {
}
// ExecStart starts an exec process already created in the docker host.
func (cli *Client) ExecStart(ctx context.Context, execID string, options ExecStartOptions) (ExecStartResult, error) {
req := container.ExecStartRequest{
Detach: config.Detach,
Tty: config.Tty,
ConsoleSize: config.ConsoleSize,
Detach: options.Detach,
Tty: options.Tty,
ConsoleSize: options.ConsoleSize,
}
resp, err := cli.post(ctx, "/exec/"+execID+"/start", nil, req, nil)
defer ensureReaderClosed(resp)
return err
return ExecStartResult{}, err
}
// ExecAttachOptions is a temp struct used by execAttach.
//
// TODO(thaJeztah): make this a separate type; ContainerExecAttach does not use the Detach option, and cannot run detached.
type ExecAttachOptions = ExecStartOptions
// ExecAttachOptions holds options for attaching to a container exec.
type ExecAttachOptions execStartAttachOptions
// ContainerExecAttach attaches a connection to an exec process in the server.
// ExecAttachResult holds the result of attaching to a container exec.
type ExecAttachResult struct {
HijackedResponse
}
// ExecAttach attaches a connection to an exec process in the server.
//
// It returns a [HijackedResponse] with the hijacked connection
// and a reader to get output. It's up to the called to close
@@ -102,15 +115,16 @@ type ExecAttachOptions = ExecStartOptions
// [Client.ContainerAttach] for details about the multiplexed stream.
//
// [stdcopy.StdCopy]: https://pkg.go.dev/github.com/moby/moby/api/pkg/stdcopy#StdCopy
func (cli *Client) ContainerExecAttach(ctx context.Context, execID string, config ExecAttachOptions) (HijackedResponse, error) {
func (cli *Client) ExecAttach(ctx context.Context, execID string, options ExecAttachOptions) (ExecAttachResult, error) {
req := container.ExecStartRequest{
Detach: config.Detach,
Tty: config.Tty,
ConsoleSize: config.ConsoleSize,
Detach: options.Detach,
Tty: options.Tty,
ConsoleSize: options.ConsoleSize,
}
return cli.postHijacked(ctx, "/exec/"+execID+"/start", nil, req, http.Header{
response, err := cli.postHijacked(ctx, "/exec/"+execID+"/start", nil, req, http.Header{
"Content-Type": {"application/json"},
})
return ExecAttachResult{HijackedResponse: response}, err
}
// ExecInspect holds information returned by exec inspect.
@@ -126,18 +140,27 @@ type ExecInspect struct {
Pid int `json:"Pid"`
}
// ContainerExecInspect returns information about a specific exec process on the docker host.
func (cli *Client) ContainerExecInspect(ctx context.Context, execID string) (ExecInspect, error) {
// ExecInspectOptions holds options for inspecting a container exec.
type ExecInspectOptions struct {
}
// ExecInspectResult holds the result of inspecting a container exec.
type ExecInspectResult struct {
ExecInspect
}
// ExecInspect returns information about a specific exec process on the docker host.
func (cli *Client) ExecInspect(ctx context.Context, execID string, options ExecInspectOptions) (ExecInspectResult, error) {
resp, err := cli.get(ctx, "/exec/"+execID+"/json", nil, nil)
defer ensureReaderClosed(resp)
if err != nil {
return ExecInspect{}, err
return ExecInspectResult{}, err
}
var response container.ExecInspectResponse
err = json.NewDecoder(resp.Body).Decode(&response)
if err != nil {
return ExecInspect{}, err
return ExecInspectResult{}, err
}
var ec int
@@ -145,11 +168,11 @@ func (cli *Client) ContainerExecInspect(ctx context.Context, execID string) (Exe
ec = *response.ExitCode
}
return ExecInspect{
return ExecInspectResult{ExecInspect: ExecInspect{
ExecID: response.ID,
ContainerID: response.ContainerID,
Running: response.Running,
ExitCode: ec,
Pid: response.Pid,
}, nil
}}, nil
}

View File

@@ -15,37 +15,37 @@ import (
is "gotest.tools/v3/assert/cmp"
)
func TestContainerExecCreateError(t *testing.T) {
func TestExecCreateError(t *testing.T) {
client, err := NewClientWithOpts(
WithMockClient(errorMock(http.StatusInternalServerError, "Server error")),
)
assert.NilError(t, err)
_, err = client.ContainerExecCreate(context.Background(), "container_id", ExecCreateOptions{})
_, err = client.ExecCreate(context.Background(), "container_id", ExecCreateOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal))
_, err = client.ContainerExecCreate(context.Background(), "", ExecCreateOptions{})
_, err = client.ExecCreate(context.Background(), "", ExecCreateOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty"))
_, err = client.ContainerExecCreate(context.Background(), " ", ExecCreateOptions{})
_, err = client.ExecCreate(context.Background(), " ", ExecCreateOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty"))
}
// TestContainerExecCreateConnectionError verifies that connection errors occurring
// TestExecCreateConnectionError verifies that connection errors occurring
// during API-version negotiation are not shadowed by API-version errors.
//
// Regression test for https://github.com/docker/cli/issues/4890
func TestContainerExecCreateConnectionError(t *testing.T) {
func TestExecCreateConnectionError(t *testing.T) {
client, err := NewClientWithOpts(WithAPIVersionNegotiation(), WithHost("tcp://no-such-host.invalid"))
assert.NilError(t, err)
_, err = client.ContainerExecCreate(context.Background(), "container_id", ExecCreateOptions{})
_, err = client.ExecCreate(context.Background(), "container_id", ExecCreateOptions{})
assert.Check(t, is.ErrorType(err, IsErrConnectionFailed))
}
func TestContainerExecCreate(t *testing.T) {
func TestExecCreate(t *testing.T) {
const expectedURL = "/containers/container_id/exec"
client, err := NewClientWithOpts(
WithMockClient(func(req *http.Request) (*http.Response, error) {
@@ -77,24 +77,24 @@ func TestContainerExecCreate(t *testing.T) {
)
assert.NilError(t, err)
r, err := client.ContainerExecCreate(context.Background(), "container_id", ExecCreateOptions{
r, err := client.ExecCreate(context.Background(), "container_id", ExecCreateOptions{
User: "user",
})
assert.NilError(t, err)
assert.Check(t, is.Equal(r.ID, "exec_id"))
}
func TestContainerExecStartError(t *testing.T) {
func TestExecStartError(t *testing.T) {
client, err := NewClientWithOpts(
WithMockClient(errorMock(http.StatusInternalServerError, "Server error")),
)
assert.NilError(t, err)
err = client.ContainerExecStart(context.Background(), "nothing", ExecStartOptions{})
_, err = client.ExecStart(context.Background(), "nothing", ExecStartOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal))
}
func TestContainerExecStart(t *testing.T) {
func TestExecStart(t *testing.T) {
const expectedURL = "/exec/exec_id/start"
client, err := NewClientWithOpts(
WithMockClient(func(req *http.Request) (*http.Response, error) {
@@ -120,24 +120,24 @@ func TestContainerExecStart(t *testing.T) {
)
assert.NilError(t, err)
err = client.ContainerExecStart(context.Background(), "exec_id", ExecStartOptions{
_, err = client.ExecStart(context.Background(), "exec_id", ExecStartOptions{
Detach: true,
Tty: false,
})
assert.NilError(t, err)
}
func TestContainerExecInspectError(t *testing.T) {
func TestExecInspectError(t *testing.T) {
client, err := NewClientWithOpts(
WithMockClient(errorMock(http.StatusInternalServerError, "Server error")),
)
assert.NilError(t, err)
_, err = client.ContainerExecInspect(context.Background(), "nothing")
_, err = client.ExecInspect(context.Background(), "nothing", ExecInspectOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal))
}
func TestContainerExecInspect(t *testing.T) {
func TestExecInspect(t *testing.T) {
const expectedURL = "/exec/exec_id/json"
client, err := NewClientWithOpts(
WithMockClient(func(req *http.Request) (*http.Response, error) {
@@ -159,7 +159,7 @@ func TestContainerExecInspect(t *testing.T) {
)
assert.NilError(t, err)
inspect, err := client.ContainerExecInspect(context.Background(), "exec_id")
inspect, err := client.ExecInspect(context.Background(), "exec_id", ExecInspectOptions{})
assert.NilError(t, err)
assert.Check(t, is.Equal(inspect.ExecID, "exec_id"))
assert.Check(t, is.Equal(inspect.ContainerID, "container_id"))

View File

@@ -23,13 +23,21 @@ func (cli *Client) ContainerResize(ctx context.Context, containerID string, opti
return cli.resize(ctx, "/containers/"+containerID, options.Height, options.Width)
}
// ContainerExecResize changes the size of the tty for an exec process running inside a container.
func (cli *Client) ContainerExecResize(ctx context.Context, execID string, options ContainerResizeOptions) error {
// ExecResizeOptions holds options for resizing a container exec TTY.
type ExecResizeOptions ContainerResizeOptions
// ExecResizeResult holds the result of resizing a container exec TTY.
type ExecResizeResult struct {
}
// ExecResize changes the size of the tty for an exec process running inside a container.
func (cli *Client) ExecResize(ctx context.Context, execID string, options ExecResizeOptions) (ExecResizeResult, error) {
execID, err := trimID("exec", execID)
if err != nil {
return err
return ExecResizeResult{}, err
}
return cli.resize(ctx, "/exec/"+execID, options.Height, options.Width)
err = cli.resize(ctx, "/exec/"+execID, options.Height, options.Width)
return ExecResizeResult{}, err
}
func (cli *Client) resize(ctx context.Context, basePath string, height, width uint) error {

View File

@@ -28,10 +28,10 @@ func TestContainerResizeError(t *testing.T) {
assert.Check(t, is.ErrorContains(err, "value is empty"))
}
func TestContainerExecResizeError(t *testing.T) {
func TestExecResizeError(t *testing.T) {
client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusInternalServerError, "Server error")))
assert.NilError(t, err)
err = client.ContainerExecResize(context.Background(), "exec_id", ContainerResizeOptions{})
_, err = client.ExecResize(context.Background(), "exec_id", ExecResizeOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal))
}
@@ -78,22 +78,22 @@ func TestContainerResize(t *testing.T) {
}
}
func TestContainerExecResize(t *testing.T) {
func TestExecResize(t *testing.T) {
const expectedURL = "/exec/exec_id/resize"
tests := []struct {
doc string
opts ContainerResizeOptions
opts ExecResizeOptions
expectedHeight, expectedWidth string
}{
{
doc: "zero width height", // valid, but not very useful
opts: ContainerResizeOptions{},
opts: ExecResizeOptions{},
expectedWidth: "0",
expectedHeight: "0",
},
{
doc: "valid resize",
opts: ContainerResizeOptions{
opts: ExecResizeOptions{
Height: 500,
Width: 600,
},
@@ -102,7 +102,7 @@ func TestContainerExecResize(t *testing.T) {
},
{
doc: "larger than maxint64",
opts: ContainerResizeOptions{
opts: ExecResizeOptions{
Height: math.MaxInt64 + 1,
Width: math.MaxInt64 + 2,
},
@@ -114,7 +114,7 @@ func TestContainerExecResize(t *testing.T) {
t.Run(tc.doc, func(t *testing.T) {
client, err := NewClientWithOpts(WithMockClient(resizeTransport(t, expectedURL, tc.expectedHeight, tc.expectedWidth)))
assert.NilError(t, err)
err = client.ContainerExecResize(context.Background(), "exec_id", tc.opts)
_, err = client.ExecResize(context.Background(), "exec_id", tc.opts)
assert.NilError(t, err)
})
}