2016-09-08 13:11:39 -04:00
|
|
|
package container
|
|
|
|
|
|
|
|
import (
|
2018-05-03 21:02:44 -04:00
|
|
|
"context"
|
2016-09-08 13:11:39 -04:00
|
|
|
"io"
|
|
|
|
|
2017-04-17 18:07:56 -04:00
|
|
|
"github.com/docker/cli/cli"
|
|
|
|
"github.com/docker/cli/cli/command"
|
2022-03-30 09:27:25 -04:00
|
|
|
"github.com/docker/cli/cli/command/completion"
|
2017-11-15 22:18:23 -05:00
|
|
|
"github.com/docker/docker/api/types/container"
|
2017-05-18 00:12:37 -04:00
|
|
|
"github.com/docker/docker/client"
|
2021-08-09 13:15:46 -04:00
|
|
|
"github.com/moby/sys/signal"
|
2017-03-27 21:21:59 -04:00
|
|
|
"github.com/pkg/errors"
|
2017-08-07 05:52:40 -04:00
|
|
|
"github.com/sirupsen/logrus"
|
2016-09-08 13:11:39 -04:00
|
|
|
"github.com/spf13/cobra"
|
|
|
|
)
|
|
|
|
|
2023-11-06 04:00:13 -05:00
|
|
|
// AttachOptions group options for `attach` command
|
|
|
|
type AttachOptions struct {
|
|
|
|
NoStdin bool
|
|
|
|
Proxy bool
|
|
|
|
DetachKeys string
|
2016-09-08 13:11:39 -04:00
|
|
|
}
|
|
|
|
|
2024-07-03 09:35:44 -04:00
|
|
|
func inspectContainerAndCheckState(ctx context.Context, apiClient client.APIClient, args string) (*container.InspectResponse, error) {
|
2023-11-20 11:38:50 -05:00
|
|
|
c, err := apiClient.ContainerInspect(ctx, args)
|
2017-05-18 00:12:37 -04:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if !c.State.Running {
|
|
|
|
return nil, errors.New("You cannot attach to a stopped container, start it first")
|
|
|
|
}
|
|
|
|
if c.State.Paused {
|
|
|
|
return nil, errors.New("You cannot attach to a paused container, unpause it first")
|
|
|
|
}
|
2017-06-07 19:12:39 -04:00
|
|
|
if c.State.Restarting {
|
|
|
|
return nil, errors.New("You cannot attach to a restarting container, wait until it is running")
|
|
|
|
}
|
2017-06-07 19:03:52 -04:00
|
|
|
|
2017-05-18 00:12:37 -04:00
|
|
|
return &c, nil
|
|
|
|
}
|
|
|
|
|
2016-09-08 13:11:39 -04:00
|
|
|
// NewAttachCommand creates a new cobra.Command for `docker attach`
|
2024-01-26 07:38:09 -05:00
|
|
|
func NewAttachCommand(dockerCLI command.Cli) *cobra.Command {
|
2023-11-06 04:00:13 -05:00
|
|
|
var opts AttachOptions
|
2016-09-08 13:11:39 -04:00
|
|
|
|
|
|
|
cmd := &cobra.Command{
|
|
|
|
Use: "attach [OPTIONS] CONTAINER",
|
2017-03-31 16:22:21 -04:00
|
|
|
Short: "Attach local standard input, output, and error streams to a running container",
|
2016-09-08 13:11:39 -04:00
|
|
|
Args: cli.ExactArgs(1),
|
|
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
2024-01-26 07:38:09 -05:00
|
|
|
containerID := args[0]
|
|
|
|
return RunAttach(cmd.Context(), dockerCLI, containerID, &opts)
|
2016-09-08 13:11:39 -04:00
|
|
|
},
|
cli: use custom annotation for aliases
Cobra allows for aliases to be defined for a command, but only allows these
to be defined at the same level (for example, `docker image ls` as alias for
`docker image list`). Our CLI has some commands that are available both as a
top-level shorthand as well as `docker <object> <verb>` subcommands. For example,
`docker ps` is a shorthand for `docker container ps` / `docker container ls`.
This patch introduces a custom "aliases" annotation that can be used to print
all available aliases for a command. While this requires these aliases to be
defined manually, in practice the list of aliases rarely changes, so maintenance
should be minimal.
As a convention, we could consider the first command in this list to be the
canonical command, so that we can use this information to add redirects in
our documentation in future.
Before this patch:
docker images --help
Usage: docker images [OPTIONS] [REPOSITORY[:TAG]]
List images
Options:
-a, --all Show all images (default hides intermediate images)
...
With this patch:
docker images --help
Usage: docker images [OPTIONS] [REPOSITORY[:TAG]]
List images
Aliases:
docker image ls, docker image list, docker images
Options:
-a, --all Show all images (default hides intermediate images)
...
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2022-06-28 04:52:25 -04:00
|
|
|
Annotations: map[string]string{
|
|
|
|
"aliases": "docker container attach, docker attach",
|
|
|
|
},
|
2024-07-03 09:35:44 -04:00
|
|
|
ValidArgsFunction: completion.ContainerNames(dockerCLI, false, func(ctr container.Summary) bool {
|
2023-11-20 11:38:50 -05:00
|
|
|
return ctr.State != "paused"
|
2022-05-12 07:18:48 -04:00
|
|
|
}),
|
2016-09-08 13:11:39 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
flags := cmd.Flags()
|
2023-11-06 04:00:13 -05:00
|
|
|
flags.BoolVar(&opts.NoStdin, "no-stdin", false, "Do not attach STDIN")
|
|
|
|
flags.BoolVar(&opts.Proxy, "sig-proxy", true, "Proxy all received signals to the process")
|
|
|
|
flags.StringVar(&opts.DetachKeys, "detach-keys", "", "Override the key sequence for detaching a container")
|
2016-09-08 13:11:39 -04:00
|
|
|
return cmd
|
|
|
|
}
|
|
|
|
|
2023-11-06 04:00:13 -05:00
|
|
|
// RunAttach executes an `attach` command
|
2024-01-26 07:38:09 -05:00
|
|
|
func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, opts *AttachOptions) error {
|
2023-11-20 11:38:50 -05:00
|
|
|
apiClient := dockerCLI.Client()
|
2016-09-08 13:11:39 -04:00
|
|
|
|
2017-11-15 22:18:23 -05:00
|
|
|
// request channel to wait for client
|
2024-01-26 07:38:09 -05:00
|
|
|
resultC, errC := apiClient.ContainerWait(ctx, containerID, "")
|
2017-11-15 22:18:23 -05:00
|
|
|
|
2024-01-26 07:38:09 -05:00
|
|
|
c, err := inspectContainerAndCheckState(ctx, apiClient, containerID)
|
2016-09-08 13:11:39 -04:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2023-11-20 11:38:50 -05:00
|
|
|
if err := dockerCLI.In().CheckTty(!opts.NoStdin, c.Config.Tty); err != nil {
|
2016-09-08 13:11:39 -04:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2023-11-20 11:38:50 -05:00
|
|
|
detachKeys := dockerCLI.ConfigFile().DetachKeys
|
2023-11-06 04:00:13 -05:00
|
|
|
if opts.DetachKeys != "" {
|
|
|
|
detachKeys = opts.DetachKeys
|
2016-09-08 13:11:39 -04:00
|
|
|
}
|
|
|
|
|
2023-10-13 14:34:32 -04:00
|
|
|
options := container.AttachOptions{
|
2016-09-08 13:11:39 -04:00
|
|
|
Stream: true,
|
2023-11-06 04:00:13 -05:00
|
|
|
Stdin: !opts.NoStdin && c.Config.OpenStdin,
|
2016-09-08 13:11:39 -04:00
|
|
|
Stdout: true,
|
|
|
|
Stderr: true,
|
2023-06-08 10:53:30 -04:00
|
|
|
DetachKeys: detachKeys,
|
2016-09-08 13:11:39 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
var in io.ReadCloser
|
|
|
|
if options.Stdin {
|
2023-11-20 11:38:50 -05:00
|
|
|
in = dockerCLI.In()
|
2016-09-08 13:11:39 -04:00
|
|
|
}
|
|
|
|
|
2023-11-06 04:00:13 -05:00
|
|
|
if opts.Proxy && !c.Config.Tty {
|
2021-09-16 09:46:27 -04:00
|
|
|
sigc := notifyAllSignals()
|
2024-04-08 04:11:09 -04:00
|
|
|
// since we're explicitly setting up signal handling here, and the daemon will
|
|
|
|
// get notified independently of the clients ctx cancellation, we use this context
|
|
|
|
// but without cancellation to avoid ForwardAllSignals from returning
|
|
|
|
// before all signals are forwarded.
|
|
|
|
bgCtx := context.WithoutCancel(ctx)
|
|
|
|
go ForwardAllSignals(bgCtx, apiClient, containerID, sigc)
|
2016-09-08 13:11:39 -04:00
|
|
|
defer signal.StopCatch(sigc)
|
|
|
|
}
|
|
|
|
|
2024-01-26 07:38:09 -05:00
|
|
|
resp, errAttach := apiClient.ContainerAttach(ctx, containerID, options)
|
2019-04-02 05:20:07 -04:00
|
|
|
if errAttach != nil {
|
2016-09-08 13:11:39 -04:00
|
|
|
return errAttach
|
|
|
|
}
|
|
|
|
defer resp.Close()
|
|
|
|
|
2017-05-18 00:12:37 -04:00
|
|
|
// If use docker attach command to attach to a stop container, it will return
|
|
|
|
// "You cannot attach to a stopped container" error, it's ok, but when
|
|
|
|
// attach to a running container, it(docker attach) use inspect to check
|
|
|
|
// the container's state, if it pass the state check on the client side,
|
|
|
|
// and then the container is stopped, docker attach command still attach to
|
|
|
|
// the container and not exit.
|
|
|
|
//
|
|
|
|
// Recheck the container's state to avoid attach block.
|
2024-01-26 07:38:09 -05:00
|
|
|
_, err = inspectContainerAndCheckState(ctx, apiClient, containerID)
|
2017-05-18 00:12:37 -04:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2023-11-20 11:38:50 -05:00
|
|
|
if c.Config.Tty && dockerCLI.Out().IsTerminal() {
|
2024-01-26 07:38:09 -05:00
|
|
|
resizeTTY(ctx, dockerCLI, containerID)
|
2016-09-08 13:11:39 -04:00
|
|
|
}
|
2017-05-17 14:40:56 -04:00
|
|
|
|
|
|
|
streamer := hijackedIOStreamer{
|
2023-11-20 11:38:50 -05:00
|
|
|
streams: dockerCLI,
|
2017-05-17 14:40:56 -04:00
|
|
|
inputStream: in,
|
2023-11-20 11:38:50 -05:00
|
|
|
outputStream: dockerCLI.Out(),
|
|
|
|
errorStream: dockerCLI.Err(),
|
2017-05-17 14:40:56 -04:00
|
|
|
resp: resp,
|
|
|
|
tty: c.Config.Tty,
|
|
|
|
detachKeys: options.DetachKeys,
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := streamer.stream(ctx); err != nil {
|
2016-09-08 13:11:39 -04:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2017-11-15 22:18:23 -05:00
|
|
|
return getExitStatus(errC, resultC)
|
|
|
|
}
|
|
|
|
|
2022-04-29 13:26:50 -04:00
|
|
|
func getExitStatus(errC <-chan error, resultC <-chan container.WaitResponse) error {
|
2017-11-15 22:18:23 -05:00
|
|
|
select {
|
|
|
|
case result := <-resultC:
|
|
|
|
if result.Error != nil {
|
linting: fmt.Errorf can be replaced with errors.New (perfsprint)
internal/test/cli.go:175:14: fmt.Errorf can be replaced with errors.New (perfsprint)
return nil, fmt.Errorf("no notary client available unless defined")
^
cli/command/cli.go:318:29: fmt.Errorf can be replaced with errors.New (perfsprint)
return docker.Endpoint{}, fmt.Errorf("no context store initialized")
^
cli/command/container/attach.go:161:11: fmt.Errorf can be replaced with errors.New (perfsprint)
return fmt.Errorf(result.Error.Message)
^
cli/command/container/opts.go:577:16: fmt.Errorf can be replaced with errors.New (perfsprint)
return nil, fmt.Errorf("--health-start-period cannot be negative")
^
cli/command/container/opts.go:580:16: fmt.Errorf can be replaced with errors.New (perfsprint)
return nil, fmt.Errorf("--health-start-interval cannot be negative")
^
cli/command/container/stats.go:221:11: fmt.Errorf can be replaced with errors.New (perfsprint)
return fmt.Errorf("filtering is not supported when specifying a list of containers")
^
cli/command/container/attach_test.go:82:17: fmt.Errorf can be replaced with errors.New (perfsprint)
expectedErr = fmt.Errorf("unexpected error")
^
cli/command/container/create_test.go:234:40: fmt.Errorf can be replaced with errors.New (perfsprint)
return container.CreateResponse{}, fmt.Errorf("shouldn't try to pull image")
^
cli/command/container/list_test.go:150:17: fmt.Errorf can be replaced with errors.New (perfsprint)
return nil, fmt.Errorf("error listing containers")
^
cli/command/container/rm_test.go:40:31: fmt.Errorf can be replaced with errors.New (perfsprint)
return errdefs.NotFound(fmt.Errorf("Error: no such container: " + container))
^
cli/command/container/run_test.go:138:40: fmt.Errorf can be replaced with errors.New (perfsprint)
return container.CreateResponse{}, fmt.Errorf("shouldn't try to pull image")
^
cli/command/image/pull_test.go:115:49: fmt.Errorf can be replaced with errors.New (perfsprint)
return io.NopCloser(strings.NewReader("")), fmt.Errorf("shouldn't try to pull image")
^
cli/command/network/connect.go:88:16: fmt.Errorf can be replaced with errors.New (perfsprint)
return nil, fmt.Errorf("invalid key/value pair format in driver options")
^
cli/command/plugin/create_test.go:96:11: fmt.Errorf can be replaced with errors.New (perfsprint)
return fmt.Errorf("Error creating plugin")
^
cli/command/plugin/disable_test.go:32:12: fmt.Errorf can be replaced with errors.New (perfsprint)
return fmt.Errorf("Error disabling plugin")
^
cli/command/plugin/enable_test.go:32:12: fmt.Errorf can be replaced with errors.New (perfsprint)
return fmt.Errorf("failed to enable plugin")
^
cli/command/plugin/inspect_test.go:55:22: fmt.Errorf can be replaced with errors.New (perfsprint)
return nil, nil, fmt.Errorf("error inspecting plugin")
^
cli/command/plugin/install_test.go:43:17: fmt.Errorf can be replaced with errors.New (perfsprint)
return nil, fmt.Errorf("Error installing plugin")
^
cli/command/plugin/install_test.go:51:17: fmt.Errorf can be replaced with errors.New (perfsprint)
return nil, fmt.Errorf("(image) when fetching")
^
cli/command/plugin/install_test.go:95:17: fmt.Errorf can be replaced with errors.New (perfsprint)
return nil, fmt.Errorf("should not try to install plugin")
^
cli/command/plugin/list_test.go:35:41: fmt.Errorf can be replaced with errors.New (perfsprint)
return types.PluginsListResponse{}, fmt.Errorf("error listing plugins")
^
cli/command/plugin/remove_test.go:27:12: fmt.Errorf can be replaced with errors.New (perfsprint)
return fmt.Errorf("Error removing plugin")
^
cli/command/registry/login_test.go:36:46: fmt.Errorf can be replaced with errors.New (perfsprint)
return registrytypes.AuthenticateOKBody{}, fmt.Errorf("Invalid Username or Password")
^
cli/command/registry/login_test.go:44:46: fmt.Errorf can be replaced with errors.New (perfsprint)
return registrytypes.AuthenticateOKBody{}, fmt.Errorf(errUnknownUser)
^
cli/command/system/info.go:190:10: fmt.Errorf can be replaced with errors.New (perfsprint)
return fmt.Errorf("errors pretty printing info")
^
cli/command/system/prune.go:77:10: fmt.Errorf can be replaced with errors.New (perfsprint)
return fmt.Errorf(`ERROR: The "until" filter is not supported with "--volumes"`)
^
cli/command/system/version_test.go:19:28: fmt.Errorf can be replaced with errors.New (perfsprint)
return types.Version{}, fmt.Errorf("no server")
^
cli/command/trust/key_load.go:112:22: fmt.Errorf can be replaced with errors.New (perfsprint)
return []byte{}, fmt.Errorf("could not decrypt key")
^
cli/command/trust/revoke.go:44:10: fmt.Errorf can be replaced with errors.New (perfsprint)
return fmt.Errorf("cannot use a digest reference for IMAGE:TAG")
^
cli/command/trust/revoke.go:105:10: fmt.Errorf can be replaced with errors.New (perfsprint)
return fmt.Errorf("no signed tags to remove")
^
cli/command/trust/signer_add.go:56:10: fmt.Errorf can be replaced with errors.New (perfsprint)
return fmt.Errorf("releases is a reserved keyword, please use a different signer name")
^
cli/command/trust/signer_add.go:60:10: fmt.Errorf can be replaced with errors.New (perfsprint)
return fmt.Errorf("path to a public key must be provided using the `--key` flag")
^
opts/config.go:71:10: fmt.Errorf can be replaced with errors.New (perfsprint)
return fmt.Errorf("source is required")
^
opts/mount.go:168:10: fmt.Errorf can be replaced with errors.New (perfsprint)
return fmt.Errorf("type is required")
^
opts/mount.go:172:10: fmt.Errorf can be replaced with errors.New (perfsprint)
return fmt.Errorf("target is required")
^
opts/network.go:90:11: fmt.Errorf can be replaced with errors.New (perfsprint)
return fmt.Errorf("network name/id is not specified")
^
opts/network.go:129:18: fmt.Errorf can be replaced with errors.New (perfsprint)
return "", "", fmt.Errorf("invalid key value pair format in driver options")
^
opts/opts.go:404:13: fmt.Errorf can be replaced with errors.New (perfsprint)
return 0, fmt.Errorf("value is too precise")
^
opts/opts.go:412:18: fmt.Errorf can be replaced with errors.New (perfsprint)
return "", "", fmt.Errorf("empty string specified for links")
^
opts/parse.go:84:37: fmt.Errorf can be replaced with errors.New (perfsprint)
return container.RestartPolicy{}, fmt.Errorf("invalid restart policy format: no policy provided before colon")
^
opts/parse.go:89:38: fmt.Errorf can be replaced with errors.New (perfsprint)
return container.RestartPolicy{}, fmt.Errorf("invalid restart policy format: maximum retry count must be an integer")
^
opts/port.go:105:13: fmt.Errorf can be replaced with errors.New (perfsprint)
return fmt.Errorf("hostip is not supported")
^
opts/secret.go:70:10: fmt.Errorf can be replaced with errors.New (perfsprint)
return fmt.Errorf("source is required")
^
opts/env_test.go:57:11: fmt.Errorf can be replaced with errors.New (perfsprint)
err: fmt.Errorf("invalid environment variable: =a"),
^
opts/env_test.go:93:11: fmt.Errorf can be replaced with errors.New (perfsprint)
err: fmt.Errorf("invalid environment variable: ="),
^
cli-plugins/manager/error_test.go:16:11: fmt.Errorf can be replaced with errors.New (perfsprint)
inner := fmt.Errorf("testing")
^
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-06-10 14:47:07 -04:00
|
|
|
return errors.New(result.Error.Message)
|
2017-11-15 22:18:23 -05:00
|
|
|
}
|
|
|
|
if result.StatusCode != 0 {
|
|
|
|
return cli.StatusError{StatusCode: int(result.StatusCode)}
|
|
|
|
}
|
|
|
|
case err := <-errC:
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
2017-05-09 18:35:25 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
func resizeTTY(ctx context.Context, dockerCli command.Cli, containerID string) {
|
|
|
|
height, width := dockerCli.Out().GetTtySize()
|
|
|
|
// To handle the case where a user repeatedly attaches/detaches without resizing their
|
|
|
|
// terminal, the only way to get the shell prompt to display for attaches 2+ is to artificially
|
|
|
|
// resize it, then go back to normal. Without this, every attach after the first will
|
|
|
|
// require the user to manually resize or hit enter.
|
|
|
|
resizeTtyTo(ctx, dockerCli.Client(), containerID, height+1, width+1, false)
|
2016-09-08 13:11:39 -04:00
|
|
|
|
2017-05-09 18:35:25 -04:00
|
|
|
// After the above resizing occurs, the call to MonitorTtySize below will handle resetting back
|
|
|
|
// to the actual size.
|
|
|
|
if err := MonitorTtySize(ctx, dockerCli, containerID, false); err != nil {
|
|
|
|
logrus.Debugf("Error monitoring TTY size: %s", err)
|
|
|
|
}
|
|
|
|
}
|