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