github.1git.de/docker/cli@v26.1.3+incompatible/cli/command/container/exec.go (about) 1 package container 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "os" 8 9 "github.com/docker/cli/cli" 10 "github.com/docker/cli/cli/command" 11 "github.com/docker/cli/cli/command/completion" 12 "github.com/docker/cli/cli/config/configfile" 13 "github.com/docker/cli/opts" 14 "github.com/docker/docker/api/types" 15 apiclient "github.com/docker/docker/client" 16 "github.com/pkg/errors" 17 "github.com/sirupsen/logrus" 18 "github.com/spf13/cobra" 19 ) 20 21 // ExecOptions group options for `exec` command 22 type ExecOptions struct { 23 DetachKeys string 24 Interactive bool 25 TTY bool 26 Detach bool 27 User string 28 Privileged bool 29 Env opts.ListOpts 30 Workdir string 31 Command []string 32 EnvFile opts.ListOpts 33 } 34 35 // NewExecOptions creates a new ExecOptions 36 func NewExecOptions() ExecOptions { 37 return ExecOptions{ 38 Env: opts.NewListOpts(opts.ValidateEnv), 39 EnvFile: opts.NewListOpts(nil), 40 } 41 } 42 43 // NewExecCommand creates a new cobra.Command for `docker exec` 44 func NewExecCommand(dockerCli command.Cli) *cobra.Command { 45 options := NewExecOptions() 46 var container string 47 48 cmd := &cobra.Command{ 49 Use: "exec [OPTIONS] CONTAINER COMMAND [ARG...]", 50 Short: "Execute a command in a running container", 51 Args: cli.RequiresMinArgs(2), 52 RunE: func(cmd *cobra.Command, args []string) error { 53 container = args[0] 54 options.Command = args[1:] 55 return RunExec(cmd.Context(), dockerCli, container, options) 56 }, 57 ValidArgsFunction: completion.ContainerNames(dockerCli, false, func(container types.Container) bool { 58 return container.State != "paused" 59 }), 60 Annotations: map[string]string{ 61 "category-top": "2", 62 "aliases": "docker container exec, docker exec", 63 }, 64 } 65 66 flags := cmd.Flags() 67 flags.SetInterspersed(false) 68 69 flags.StringVar(&options.DetachKeys, "detach-keys", "", "Override the key sequence for detaching a container") 70 flags.BoolVarP(&options.Interactive, "interactive", "i", false, "Keep STDIN open even if not attached") 71 flags.BoolVarP(&options.TTY, "tty", "t", false, "Allocate a pseudo-TTY") 72 flags.BoolVarP(&options.Detach, "detach", "d", false, "Detached mode: run command in the background") 73 flags.StringVarP(&options.User, "user", "u", "", `Username or UID (format: "<name|uid>[:<group|gid>]")`) 74 flags.BoolVar(&options.Privileged, "privileged", false, "Give extended privileges to the command") 75 flags.VarP(&options.Env, "env", "e", "Set environment variables") 76 flags.SetAnnotation("env", "version", []string{"1.25"}) 77 flags.Var(&options.EnvFile, "env-file", "Read in a file of environment variables") 78 flags.SetAnnotation("env-file", "version", []string{"1.25"}) 79 flags.StringVarP(&options.Workdir, "workdir", "w", "", "Working directory inside the container") 80 flags.SetAnnotation("workdir", "version", []string{"1.35"}) 81 82 cmd.RegisterFlagCompletionFunc( 83 "env", 84 func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 85 return os.Environ(), cobra.ShellCompDirectiveNoFileComp 86 }, 87 ) 88 cmd.RegisterFlagCompletionFunc( 89 "env-file", 90 func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 91 return nil, cobra.ShellCompDirectiveDefault // _filedir 92 }, 93 ) 94 95 return cmd 96 } 97 98 // RunExec executes an `exec` command 99 func RunExec(ctx context.Context, dockerCli command.Cli, container string, options ExecOptions) error { 100 execConfig, err := parseExec(options, dockerCli.ConfigFile()) 101 if err != nil { 102 return err 103 } 104 105 client := dockerCli.Client() 106 107 // We need to check the tty _before_ we do the ContainerExecCreate, because 108 // otherwise if we error out we will leak execIDs on the server (and 109 // there's no easy way to clean those up). But also in order to make "not 110 // exist" errors take precedence we do a dummy inspect first. 111 if _, err := client.ContainerInspect(ctx, container); err != nil { 112 return err 113 } 114 if !execConfig.Detach { 115 if err := dockerCli.In().CheckTty(execConfig.AttachStdin, execConfig.Tty); err != nil { 116 return err 117 } 118 } 119 120 fillConsoleSize(execConfig, dockerCli) 121 122 response, err := client.ContainerExecCreate(ctx, container, *execConfig) 123 if err != nil { 124 return err 125 } 126 127 execID := response.ID 128 if execID == "" { 129 return errors.New("exec ID empty") 130 } 131 132 if execConfig.Detach { 133 execStartCheck := types.ExecStartCheck{ 134 Detach: execConfig.Detach, 135 Tty: execConfig.Tty, 136 ConsoleSize: execConfig.ConsoleSize, 137 } 138 return client.ContainerExecStart(ctx, execID, execStartCheck) 139 } 140 return interactiveExec(ctx, dockerCli, execConfig, execID) 141 } 142 143 func fillConsoleSize(execConfig *types.ExecConfig, dockerCli command.Cli) { 144 if execConfig.Tty { 145 height, width := dockerCli.Out().GetTtySize() 146 execConfig.ConsoleSize = &[2]uint{height, width} 147 } 148 } 149 150 func interactiveExec(ctx context.Context, dockerCli command.Cli, execConfig *types.ExecConfig, execID string) error { 151 // Interactive exec requested. 152 var ( 153 out, stderr io.Writer 154 in io.ReadCloser 155 ) 156 157 if execConfig.AttachStdin { 158 in = dockerCli.In() 159 } 160 if execConfig.AttachStdout { 161 out = dockerCli.Out() 162 } 163 if execConfig.AttachStderr { 164 if execConfig.Tty { 165 stderr = dockerCli.Out() 166 } else { 167 stderr = dockerCli.Err() 168 } 169 } 170 fillConsoleSize(execConfig, dockerCli) 171 172 client := dockerCli.Client() 173 execStartCheck := types.ExecStartCheck{ 174 Tty: execConfig.Tty, 175 ConsoleSize: execConfig.ConsoleSize, 176 } 177 resp, err := client.ContainerExecAttach(ctx, execID, execStartCheck) 178 if err != nil { 179 return err 180 } 181 defer resp.Close() 182 183 errCh := make(chan error, 1) 184 185 go func() { 186 defer close(errCh) 187 errCh <- func() error { 188 streamer := hijackedIOStreamer{ 189 streams: dockerCli, 190 inputStream: in, 191 outputStream: out, 192 errorStream: stderr, 193 resp: resp, 194 tty: execConfig.Tty, 195 detachKeys: execConfig.DetachKeys, 196 } 197 198 return streamer.stream(ctx) 199 }() 200 }() 201 202 if execConfig.Tty && dockerCli.In().IsTerminal() { 203 if err := MonitorTtySize(ctx, dockerCli, execID, true); err != nil { 204 fmt.Fprintln(dockerCli.Err(), "Error monitoring TTY size:", err) 205 } 206 } 207 208 if err := <-errCh; err != nil { 209 logrus.Debugf("Error hijack: %s", err) 210 return err 211 } 212 213 return getExecExitStatus(ctx, client, execID) 214 } 215 216 func getExecExitStatus(ctx context.Context, client apiclient.ContainerAPIClient, execID string) error { 217 resp, err := client.ContainerExecInspect(ctx, execID) 218 if err != nil { 219 // If we can't connect, then the daemon probably died. 220 if !apiclient.IsErrConnectionFailed(err) { 221 return err 222 } 223 return cli.StatusError{StatusCode: -1} 224 } 225 status := resp.ExitCode 226 if status != 0 { 227 return cli.StatusError{StatusCode: status} 228 } 229 return nil 230 } 231 232 // parseExec parses the specified args for the specified command and generates 233 // an ExecConfig from it. 234 func parseExec(execOpts ExecOptions, configFile *configfile.ConfigFile) (*types.ExecConfig, error) { 235 execConfig := &types.ExecConfig{ 236 User: execOpts.User, 237 Privileged: execOpts.Privileged, 238 Tty: execOpts.TTY, 239 Cmd: execOpts.Command, 240 Detach: execOpts.Detach, 241 WorkingDir: execOpts.Workdir, 242 } 243 244 // collect all the environment variables for the container 245 var err error 246 if execConfig.Env, err = opts.ReadKVEnvStrings(execOpts.EnvFile.GetAll(), execOpts.Env.GetAll()); err != nil { 247 return nil, err 248 } 249 250 // If -d is not set, attach to everything by default 251 if !execOpts.Detach { 252 execConfig.AttachStdout = true 253 execConfig.AttachStderr = true 254 if execOpts.Interactive { 255 execConfig.AttachStdin = true 256 } 257 } 258 259 if execOpts.DetachKeys != "" { 260 execConfig.DetachKeys = execOpts.DetachKeys 261 } else { 262 execConfig.DetachKeys = configFile.DetachKeys 263 } 264 return execConfig, nil 265 }