Repository: moby/moby
File: client/container_exec.go (Go)

Code before:
// ContainerExecStart starts an exec process already created in the docker host.
func (cli *Client) ContainerExecStart(ctx context.Context, execID string, config ExecStartOptions) error {
	// Make sure we negotiated (if the client is configured to do so),
	// as code below contains API-version specific handling of options.
	//
	// Normally, version-negotiation (if enabled) would not happen until
	// the API request is made.
	if err := cli.checkVersion(ctx); err != nil {
		return err
	}

	if versions.LessThan(cli.ClientVersion(), "1.42") {
		config.ConsoleSize = nil
	}

	req := container.ExecStartRequest{
		Detach:      config.Detach,
		Tty:         config.Tty,
		ConsoleSize: config.ConsoleSize,
	}
	resp, err := cli.post(ctx, "/exec/"+execID+"/start", nil, req, nil)
	defer ensureReaderClosed(resp)
	return 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

// ContainerExecAttach 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
// the hijacked connection by calling [HijackedResponse.Close].
//
// The stream format on the response uses one of two formats:
//
//   - If the container is using a TTY, there is only a single stream (stdout)
//     and data is copied directly from the container output stream, no extra
//     multiplexing or headers.
//   - If the container is *not* using a TTY, streams for stdout and stderr are
//     multiplexed.
//
// You can use [stdcopy.StdCopy] to demultiplex this stream. Refer to
// [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) {
	if versions.LessThan(cli.ClientVersion(), "1.42") {
		config.ConsoleSize = nil

Review comment (suggestion):
I need to look at the options; both `ContainerExecStart` and `ContainerExecAttach` use the `/start/` endpoint, but only `ContainerExecAttach` handles attaching, so likely this option shouldn't be available in both.

Code after:
		AttachStderr: options.AttachStderr,
		AttachStdout: options.AttachStdout,
		DetachKeys:   options.DetachKeys,
		Env:          options.Env,
		WorkingDir:   options.WorkingDir,
		Cmd:          options.Cmd,
	}

	resp, err := cli.post(ctx, "/containers/"+containerID+"/exec", nil, req, nil)
	defer ensureReaderClosed(resp)
	if err != nil {
		return container.ExecCreateResponse{}, err
	}

	var response container.ExecCreateResponse
	err = json.NewDecoder(resp.Body).Decode(&response)
	return response, err
}

// ExecStartOptions is a temp struct used by execStart
// Config fields is part of ExecConfig in runconfig package
type ExecStartOptions struct {
	// ExecStart will first check if it's detached
	Detach bool
	// Check if there's a tty
	Tty bool
	// Terminal size [height, width], unused if Tty == false
	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 {
	// Make sure we negotiated (if the client is configured to do so),
	// as code below contains API-version specific handling of options.
	//
	// Normally, version-negotiation (if enabled) would not happen until
	// the API request is made.
	if err := cli.checkVersion(ctx); err != nil {
		return err
	}

	if versions.LessThan(cli.ClientVersion(), "1.42") {
		config.ConsoleSize = nil
	}

	req := container.ExecStartRequest{
		Detach:      config.Detach,
		Tty:         config.Tty,
		ConsoleSize: config.ConsoleSize,
	}
	resp, err := cli.post(ctx, "/exec/"+execID+"/start", nil, req, nil)