mirror of https://github.com/docker/cli.git
Merge pull request #3436 from ndeloof/public_RunExec
publish RunExec for use by docker/compose
This commit is contained in:
commit
cb65bd4de8
|
@ -16,62 +16,65 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type execOptions struct {
|
||||
detachKeys string
|
||||
interactive bool
|
||||
tty bool
|
||||
detach bool
|
||||
user string
|
||||
privileged bool
|
||||
env opts.ListOpts
|
||||
workdir string
|
||||
container string
|
||||
command []string
|
||||
envFile opts.ListOpts
|
||||
// ExecOptions group options for `exec` command
|
||||
type ExecOptions struct {
|
||||
DetachKeys string
|
||||
Interactive bool
|
||||
TTY bool
|
||||
Detach bool
|
||||
User string
|
||||
Privileged bool
|
||||
Env opts.ListOpts
|
||||
Workdir string
|
||||
Container string
|
||||
Command []string
|
||||
EnvFile opts.ListOpts
|
||||
}
|
||||
|
||||
func newExecOptions() execOptions {
|
||||
return execOptions{
|
||||
env: opts.NewListOpts(opts.ValidateEnv),
|
||||
envFile: opts.NewListOpts(nil),
|
||||
// NewExecOptions creates a new ExecOptions
|
||||
func NewExecOptions() ExecOptions {
|
||||
return ExecOptions{
|
||||
Env: opts.NewListOpts(opts.ValidateEnv),
|
||||
EnvFile: opts.NewListOpts(nil),
|
||||
}
|
||||
}
|
||||
|
||||
// NewExecCommand creates a new cobra.Command for `docker exec`
|
||||
func NewExecCommand(dockerCli command.Cli) *cobra.Command {
|
||||
options := newExecOptions()
|
||||
options := NewExecOptions()
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "exec [OPTIONS] CONTAINER COMMAND [ARG...]",
|
||||
Short: "Run a command in a running container",
|
||||
Args: cli.RequiresMinArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
options.container = args[0]
|
||||
options.command = args[1:]
|
||||
return runExec(dockerCli, options)
|
||||
options.Container = args[0]
|
||||
options.Command = args[1:]
|
||||
return RunExec(dockerCli, options)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.SetInterspersed(false)
|
||||
|
||||
flags.StringVarP(&options.detachKeys, "detach-keys", "", "", "Override the key sequence for detaching a container")
|
||||
flags.BoolVarP(&options.interactive, "interactive", "i", false, "Keep STDIN open even if not attached")
|
||||
flags.BoolVarP(&options.tty, "tty", "t", false, "Allocate a pseudo-TTY")
|
||||
flags.BoolVarP(&options.detach, "detach", "d", false, "Detached mode: run command in the background")
|
||||
flags.StringVarP(&options.user, "user", "u", "", "Username or UID (format: <name|uid>[:<group|gid>])")
|
||||
flags.BoolVarP(&options.privileged, "privileged", "", false, "Give extended privileges to the command")
|
||||
flags.VarP(&options.env, "env", "e", "Set environment variables")
|
||||
flags.StringVarP(&options.DetachKeys, "detach-keys", "", "", "Override the key sequence for detaching a container")
|
||||
flags.BoolVarP(&options.Interactive, "interactive", "i", false, "Keep STDIN open even if not attached")
|
||||
flags.BoolVarP(&options.TTY, "tty", "t", false, "Allocate a pseudo-TTY")
|
||||
flags.BoolVarP(&options.Detach, "detach", "d", false, "Detached mode: run command in the background")
|
||||
flags.StringVarP(&options.User, "user", "u", "", "Username or UID (format: <name|uid>[:<group|gid>])")
|
||||
flags.BoolVarP(&options.Privileged, "privileged", "", false, "Give extended privileges to the command")
|
||||
flags.VarP(&options.Env, "env", "e", "Set environment variables")
|
||||
flags.SetAnnotation("env", "version", []string{"1.25"})
|
||||
flags.Var(&options.envFile, "env-file", "Read in a file of environment variables")
|
||||
flags.Var(&options.EnvFile, "env-file", "Read in a file of environment variables")
|
||||
flags.SetAnnotation("env-file", "version", []string{"1.25"})
|
||||
flags.StringVarP(&options.workdir, "workdir", "w", "", "Working directory inside the container")
|
||||
flags.StringVarP(&options.Workdir, "workdir", "w", "", "Working directory inside the container")
|
||||
flags.SetAnnotation("workdir", "version", []string{"1.35"})
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runExec(dockerCli command.Cli, options execOptions) error {
|
||||
// RunExec executes an `exec` command
|
||||
func RunExec(dockerCli command.Cli, options ExecOptions) error {
|
||||
execConfig, err := parseExec(options, dockerCli.ConfigFile())
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -84,7 +87,7 @@ func runExec(dockerCli command.Cli, options execOptions) error {
|
|||
// otherwise if we error out we will leak execIDs on the server (and
|
||||
// there's no easy way to clean those up). But also in order to make "not
|
||||
// exist" errors take precedence we do a dummy inspect first.
|
||||
if _, err := client.ContainerInspect(ctx, options.container); err != nil {
|
||||
if _, err := client.ContainerInspect(ctx, options.Container); err != nil {
|
||||
return err
|
||||
}
|
||||
if !execConfig.Detach {
|
||||
|
@ -93,7 +96,7 @@ func runExec(dockerCli command.Cli, options execOptions) error {
|
|||
}
|
||||
}
|
||||
|
||||
response, err := client.ContainerExecCreate(ctx, options.container, *execConfig)
|
||||
response, err := client.ContainerExecCreate(ctx, options.Container, *execConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -195,33 +198,33 @@ func getExecExitStatus(ctx context.Context, client apiclient.ContainerAPIClient,
|
|||
|
||||
// parseExec parses the specified args for the specified command and generates
|
||||
// an ExecConfig from it.
|
||||
func parseExec(execOpts execOptions, configFile *configfile.ConfigFile) (*types.ExecConfig, error) {
|
||||
func parseExec(execOpts ExecOptions, configFile *configfile.ConfigFile) (*types.ExecConfig, error) {
|
||||
execConfig := &types.ExecConfig{
|
||||
User: execOpts.user,
|
||||
Privileged: execOpts.privileged,
|
||||
Tty: execOpts.tty,
|
||||
Cmd: execOpts.command,
|
||||
Detach: execOpts.detach,
|
||||
WorkingDir: execOpts.workdir,
|
||||
User: execOpts.User,
|
||||
Privileged: execOpts.Privileged,
|
||||
Tty: execOpts.TTY,
|
||||
Cmd: execOpts.Command,
|
||||
Detach: execOpts.Detach,
|
||||
WorkingDir: execOpts.Workdir,
|
||||
}
|
||||
|
||||
// collect all the environment variables for the container
|
||||
var err error
|
||||
if execConfig.Env, err = opts.ReadKVEnvStrings(execOpts.envFile.GetAll(), execOpts.env.GetAll()); err != nil {
|
||||
if execConfig.Env, err = opts.ReadKVEnvStrings(execOpts.EnvFile.GetAll(), execOpts.Env.GetAll()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If -d is not set, attach to everything by default
|
||||
if !execOpts.detach {
|
||||
if !execOpts.Detach {
|
||||
execConfig.AttachStdout = true
|
||||
execConfig.AttachStderr = true
|
||||
if execOpts.interactive {
|
||||
if execOpts.Interactive {
|
||||
execConfig.AttachStdin = true
|
||||
}
|
||||
}
|
||||
|
||||
if execOpts.detachKeys != "" {
|
||||
execConfig.DetachKeys = execOpts.detachKeys
|
||||
if execOpts.DetachKeys != "" {
|
||||
execConfig.DetachKeys = execOpts.DetachKeys
|
||||
} else {
|
||||
execConfig.DetachKeys = configFile.DetachKeys
|
||||
}
|
||||
|
|
|
@ -17,11 +17,11 @@ import (
|
|||
"gotest.tools/v3/fs"
|
||||
)
|
||||
|
||||
func withDefaultOpts(options execOptions) execOptions {
|
||||
options.env = opts.NewListOpts(opts.ValidateEnv)
|
||||
options.envFile = opts.NewListOpts(nil)
|
||||
if len(options.command) == 0 {
|
||||
options.command = []string{"command"}
|
||||
func withDefaultOpts(options ExecOptions) ExecOptions {
|
||||
options.Env = opts.NewListOpts(opts.ValidateEnv)
|
||||
options.EnvFile = opts.NewListOpts(nil)
|
||||
if len(options.Command) == 0 {
|
||||
options.Command = []string{"command"}
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ TWO=2
|
|||
defer tmpFile.Remove()
|
||||
|
||||
testcases := []struct {
|
||||
options execOptions
|
||||
options ExecOptions
|
||||
configFile configfile.ConfigFile
|
||||
expected types.ExecConfig
|
||||
}{
|
||||
|
@ -45,7 +45,7 @@ TWO=2
|
|||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
},
|
||||
options: withDefaultOpts(execOptions{}),
|
||||
options: withDefaultOpts(ExecOptions{}),
|
||||
},
|
||||
{
|
||||
expected: types.ExecConfig{
|
||||
|
@ -53,15 +53,15 @@ TWO=2
|
|||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
},
|
||||
options: withDefaultOpts(execOptions{
|
||||
command: []string{"command1", "command2"},
|
||||
options: withDefaultOpts(ExecOptions{
|
||||
Command: []string{"command1", "command2"},
|
||||
}),
|
||||
},
|
||||
{
|
||||
options: withDefaultOpts(execOptions{
|
||||
interactive: true,
|
||||
tty: true,
|
||||
user: "uid",
|
||||
options: withDefaultOpts(ExecOptions{
|
||||
Interactive: true,
|
||||
TTY: true,
|
||||
User: "uid",
|
||||
}),
|
||||
expected: types.ExecConfig{
|
||||
User: "uid",
|
||||
|
@ -73,17 +73,17 @@ TWO=2
|
|||
},
|
||||
},
|
||||
{
|
||||
options: withDefaultOpts(execOptions{detach: true}),
|
||||
options: withDefaultOpts(ExecOptions{Detach: true}),
|
||||
expected: types.ExecConfig{
|
||||
Detach: true,
|
||||
Cmd: []string{"command"},
|
||||
},
|
||||
},
|
||||
{
|
||||
options: withDefaultOpts(execOptions{
|
||||
tty: true,
|
||||
interactive: true,
|
||||
detach: true,
|
||||
options: withDefaultOpts(ExecOptions{
|
||||
TTY: true,
|
||||
Interactive: true,
|
||||
Detach: true,
|
||||
}),
|
||||
expected: types.ExecConfig{
|
||||
Detach: true,
|
||||
|
@ -92,7 +92,7 @@ TWO=2
|
|||
},
|
||||
},
|
||||
{
|
||||
options: withDefaultOpts(execOptions{detach: true}),
|
||||
options: withDefaultOpts(ExecOptions{Detach: true}),
|
||||
configFile: configfile.ConfigFile{DetachKeys: "de"},
|
||||
expected: types.ExecConfig{
|
||||
Cmd: []string{"command"},
|
||||
|
@ -101,9 +101,9 @@ TWO=2
|
|||
},
|
||||
},
|
||||
{
|
||||
options: withDefaultOpts(execOptions{
|
||||
detach: true,
|
||||
detachKeys: "ab",
|
||||
options: withDefaultOpts(ExecOptions{
|
||||
Detach: true,
|
||||
DetachKeys: "ab",
|
||||
}),
|
||||
configFile: configfile.ConfigFile{DetachKeys: "de"},
|
||||
expected: types.ExecConfig{
|
||||
|
@ -119,9 +119,9 @@ TWO=2
|
|||
AttachStderr: true,
|
||||
Env: []string{"ONE=1", "TWO=2"},
|
||||
},
|
||||
options: func() execOptions {
|
||||
o := withDefaultOpts(execOptions{})
|
||||
o.envFile.Set(tmpFile.Path())
|
||||
options: func() ExecOptions {
|
||||
o := withDefaultOpts(ExecOptions{})
|
||||
o.EnvFile.Set(tmpFile.Path())
|
||||
return o
|
||||
}(),
|
||||
},
|
||||
|
@ -132,10 +132,10 @@ TWO=2
|
|||
AttachStderr: true,
|
||||
Env: []string{"ONE=1", "TWO=2", "ONE=override"},
|
||||
},
|
||||
options: func() execOptions {
|
||||
o := withDefaultOpts(execOptions{})
|
||||
o.envFile.Set(tmpFile.Path())
|
||||
o.env.Set("ONE=override")
|
||||
options: func() ExecOptions {
|
||||
o := withDefaultOpts(ExecOptions{})
|
||||
o.EnvFile.Set(tmpFile.Path())
|
||||
o.Env.Set("ONE=override")
|
||||
return o
|
||||
}(),
|
||||
},
|
||||
|
@ -149,8 +149,8 @@ TWO=2
|
|||
}
|
||||
|
||||
func TestParseExecNoSuchFile(t *testing.T) {
|
||||
execOpts := withDefaultOpts(execOptions{})
|
||||
execOpts.envFile.Set("no-such-env-file")
|
||||
execOpts := withDefaultOpts(ExecOptions{})
|
||||
execOpts.EnvFile.Set("no-such-env-file")
|
||||
execConfig, err := parseExec(execOpts, &configfile.ConfigFile{})
|
||||
assert.ErrorContains(t, err, "no-such-env-file")
|
||||
assert.Check(t, os.IsNotExist(err))
|
||||
|
@ -160,7 +160,7 @@ func TestParseExecNoSuchFile(t *testing.T) {
|
|||
func TestRunExec(t *testing.T) {
|
||||
var testcases = []struct {
|
||||
doc string
|
||||
options execOptions
|
||||
options ExecOptions
|
||||
client fakeClient
|
||||
expectedError string
|
||||
expectedOut string
|
||||
|
@ -168,15 +168,15 @@ func TestRunExec(t *testing.T) {
|
|||
}{
|
||||
{
|
||||
doc: "successful detach",
|
||||
options: withDefaultOpts(execOptions{
|
||||
container: "thecontainer",
|
||||
detach: true,
|
||||
options: withDefaultOpts(ExecOptions{
|
||||
Container: "thecontainer",
|
||||
Detach: true,
|
||||
}),
|
||||
client: fakeClient{execCreateFunc: execCreateWithID},
|
||||
},
|
||||
{
|
||||
doc: "inspect error",
|
||||
options: newExecOptions(),
|
||||
options: NewExecOptions(),
|
||||
client: fakeClient{
|
||||
inspectFunc: func(string) (types.ContainerJSON, error) {
|
||||
return types.ContainerJSON{}, errors.New("failed inspect")
|
||||
|
@ -186,7 +186,7 @@ func TestRunExec(t *testing.T) {
|
|||
},
|
||||
{
|
||||
doc: "missing exec ID",
|
||||
options: newExecOptions(),
|
||||
options: NewExecOptions(),
|
||||
expectedError: "exec ID empty",
|
||||
},
|
||||
}
|
||||
|
@ -195,7 +195,7 @@ func TestRunExec(t *testing.T) {
|
|||
t.Run(testcase.doc, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(&testcase.client)
|
||||
|
||||
err := runExec(cli, testcase.options)
|
||||
err := RunExec(cli, testcase.options)
|
||||
if testcase.expectedError != "" {
|
||||
assert.ErrorContains(t, err, testcase.expectedError)
|
||||
} else {
|
||||
|
|
|
@ -15,58 +15,65 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type startOptions struct {
|
||||
attach bool
|
||||
openStdin bool
|
||||
detachKeys string
|
||||
checkpoint string
|
||||
checkpointDir string
|
||||
// StartOptions group options for `start` command
|
||||
type StartOptions struct {
|
||||
Attach bool
|
||||
OpenStdin bool
|
||||
DetachKeys string
|
||||
Checkpoint string
|
||||
CheckpointDir string
|
||||
|
||||
containers []string
|
||||
Containers []string
|
||||
}
|
||||
|
||||
// NewStartOptions creates a new StartOptions
|
||||
func NewStartOptions() StartOptions {
|
||||
return StartOptions{}
|
||||
}
|
||||
|
||||
// NewStartCommand creates a new cobra.Command for `docker start`
|
||||
func NewStartCommand(dockerCli command.Cli) *cobra.Command {
|
||||
var opts startOptions
|
||||
var opts StartOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "start [OPTIONS] CONTAINER [CONTAINER...]",
|
||||
Short: "Start one or more stopped containers",
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.containers = args
|
||||
return runStart(dockerCli, &opts)
|
||||
opts.Containers = args
|
||||
return RunStart(dockerCli, &opts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&opts.attach, "attach", "a", false, "Attach STDOUT/STDERR and forward signals")
|
||||
flags.BoolVarP(&opts.openStdin, "interactive", "i", false, "Attach container's STDIN")
|
||||
flags.StringVar(&opts.detachKeys, "detach-keys", "", "Override the key sequence for detaching a container")
|
||||
flags.BoolVarP(&opts.Attach, "attach", "a", false, "Attach STDOUT/STDERR and forward signals")
|
||||
flags.BoolVarP(&opts.OpenStdin, "interactive", "i", false, "Attach container's STDIN")
|
||||
flags.StringVar(&opts.DetachKeys, "detach-keys", "", "Override the key sequence for detaching a container")
|
||||
|
||||
flags.StringVar(&opts.checkpoint, "checkpoint", "", "Restore from this checkpoint")
|
||||
flags.StringVar(&opts.Checkpoint, "checkpoint", "", "Restore from this checkpoint")
|
||||
flags.SetAnnotation("checkpoint", "experimental", nil)
|
||||
flags.SetAnnotation("checkpoint", "ostype", []string{"linux"})
|
||||
flags.StringVar(&opts.checkpointDir, "checkpoint-dir", "", "Use a custom checkpoint storage directory")
|
||||
flags.StringVar(&opts.CheckpointDir, "checkpoint-dir", "", "Use a custom checkpoint storage directory")
|
||||
flags.SetAnnotation("checkpoint-dir", "experimental", nil)
|
||||
flags.SetAnnotation("checkpoint-dir", "ostype", []string{"linux"})
|
||||
return cmd
|
||||
}
|
||||
|
||||
// RunStart executes a `start` command
|
||||
// nolint: gocyclo
|
||||
func runStart(dockerCli command.Cli, opts *startOptions) error {
|
||||
func RunStart(dockerCli command.Cli, opts *StartOptions) error {
|
||||
ctx, cancelFun := context.WithCancel(context.Background())
|
||||
defer cancelFun()
|
||||
|
||||
if opts.attach || opts.openStdin {
|
||||
if opts.Attach || opts.OpenStdin {
|
||||
// We're going to attach to a container.
|
||||
// 1. Ensure we only have one container.
|
||||
if len(opts.containers) > 1 {
|
||||
if len(opts.Containers) > 1 {
|
||||
return errors.New("you cannot start and attach multiple containers at once")
|
||||
}
|
||||
|
||||
// 2. Attach to the container.
|
||||
container := opts.containers[0]
|
||||
container := opts.Containers[0]
|
||||
c, err := dockerCli.Client().ContainerInspect(ctx, container)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -79,13 +86,13 @@ func runStart(dockerCli command.Cli, opts *startOptions) error {
|
|||
defer signal.StopCatch(sigc)
|
||||
}
|
||||
|
||||
if opts.detachKeys != "" {
|
||||
dockerCli.ConfigFile().DetachKeys = opts.detachKeys
|
||||
if opts.DetachKeys != "" {
|
||||
dockerCli.ConfigFile().DetachKeys = opts.DetachKeys
|
||||
}
|
||||
|
||||
options := types.ContainerAttachOptions{
|
||||
Stream: true,
|
||||
Stdin: opts.openStdin && c.Config.OpenStdin,
|
||||
Stdin: opts.OpenStdin && c.Config.OpenStdin,
|
||||
Stdout: true,
|
||||
Stderr: true,
|
||||
DetachKeys: dockerCli.ConfigFile().DetachKeys,
|
||||
|
@ -129,8 +136,8 @@ func runStart(dockerCli command.Cli, opts *startOptions) error {
|
|||
// no matter it's detached, removed on daemon side(--rm) or exit normally.
|
||||
statusChan := waitExitOrRemoved(ctx, dockerCli, c.ID, c.HostConfig.AutoRemove)
|
||||
startOptions := types.ContainerStartOptions{
|
||||
CheckpointID: opts.checkpoint,
|
||||
CheckpointDir: opts.checkpointDir,
|
||||
CheckpointID: opts.Checkpoint,
|
||||
CheckpointDir: opts.CheckpointDir,
|
||||
}
|
||||
|
||||
// 4. Start the container.
|
||||
|
@ -161,21 +168,21 @@ func runStart(dockerCli command.Cli, opts *startOptions) error {
|
|||
if status := <-statusChan; status != 0 {
|
||||
return cli.StatusError{StatusCode: status}
|
||||
}
|
||||
} else if opts.checkpoint != "" {
|
||||
if len(opts.containers) > 1 {
|
||||
} else if opts.Checkpoint != "" {
|
||||
if len(opts.Containers) > 1 {
|
||||
return errors.New("you cannot restore multiple containers at once")
|
||||
}
|
||||
container := opts.containers[0]
|
||||
container := opts.Containers[0]
|
||||
startOptions := types.ContainerStartOptions{
|
||||
CheckpointID: opts.checkpoint,
|
||||
CheckpointDir: opts.checkpointDir,
|
||||
CheckpointID: opts.Checkpoint,
|
||||
CheckpointDir: opts.CheckpointDir,
|
||||
}
|
||||
return dockerCli.Client().ContainerStart(ctx, container, startOptions)
|
||||
|
||||
} else {
|
||||
// We're not going to attach to anything.
|
||||
// Start as many containers as we want.
|
||||
return startContainersWithoutAttachments(ctx, dockerCli, opts.containers)
|
||||
return startContainersWithoutAttachments(ctx, dockerCli, opts.Containers)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
Loading…
Reference in New Issue