publish RunExec for use by docker/compose

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
This commit is contained in:
Nicolas De Loof 2022-02-22 10:36:43 +01:00
parent cf8c4bab64
commit 2d268392d1
No known key found for this signature in database
GPG Key ID: 9858809D6F8F6E7E
3 changed files with 122 additions and 112 deletions

View File

@ -16,62 +16,65 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
type execOptions struct { // ExecOptions group options for `exec` command
detachKeys string type ExecOptions struct {
interactive bool DetachKeys string
tty bool Interactive bool
detach bool TTY bool
user string Detach bool
privileged bool User string
env opts.ListOpts Privileged bool
workdir string Env opts.ListOpts
container string Workdir string
command []string Container string
envFile opts.ListOpts Command []string
EnvFile opts.ListOpts
} }
func newExecOptions() execOptions { // NewExecOptions creates a new ExecOptions
return execOptions{ func NewExecOptions() ExecOptions {
env: opts.NewListOpts(opts.ValidateEnv), return ExecOptions{
envFile: opts.NewListOpts(nil), Env: opts.NewListOpts(opts.ValidateEnv),
EnvFile: opts.NewListOpts(nil),
} }
} }
// NewExecCommand creates a new cobra.Command for `docker exec` // NewExecCommand creates a new cobra.Command for `docker exec`
func NewExecCommand(dockerCli command.Cli) *cobra.Command { func NewExecCommand(dockerCli command.Cli) *cobra.Command {
options := newExecOptions() options := NewExecOptions()
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "exec [OPTIONS] CONTAINER COMMAND [ARG...]", Use: "exec [OPTIONS] CONTAINER COMMAND [ARG...]",
Short: "Run a command in a running container", Short: "Run a command in a running container",
Args: cli.RequiresMinArgs(2), Args: cli.RequiresMinArgs(2),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
options.container = args[0] options.Container = args[0]
options.command = args[1:] options.Command = args[1:]
return runExec(dockerCli, options) return RunExec(dockerCli, options)
}, },
} }
flags := cmd.Flags() flags := cmd.Flags()
flags.SetInterspersed(false) flags.SetInterspersed(false)
flags.StringVarP(&options.detachKeys, "detach-keys", "", "", "Override the key sequence for detaching a container") 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.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.TTY, "tty", "t", false, "Allocate a pseudo-TTY")
flags.BoolVarP(&options.detach, "detach", "d", false, "Detached mode: run command in the background") 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.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.BoolVarP(&options.Privileged, "privileged", "", false, "Give extended privileges to the command")
flags.VarP(&options.env, "env", "e", "Set environment variables") flags.VarP(&options.Env, "env", "e", "Set environment variables")
flags.SetAnnotation("env", "version", []string{"1.25"}) 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.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"}) flags.SetAnnotation("workdir", "version", []string{"1.35"})
return cmd 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()) execConfig, err := parseExec(options, dockerCli.ConfigFile())
if err != nil { if err != nil {
return err 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 // 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 // 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. // 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 return err
} }
if !execConfig.Detach { 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 { if err != nil {
return err 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 // parseExec parses the specified args for the specified command and generates
// an ExecConfig from it. // 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{ execConfig := &types.ExecConfig{
User: execOpts.user, User: execOpts.User,
Privileged: execOpts.privileged, Privileged: execOpts.Privileged,
Tty: execOpts.tty, Tty: execOpts.TTY,
Cmd: execOpts.command, Cmd: execOpts.Command,
Detach: execOpts.detach, Detach: execOpts.Detach,
WorkingDir: execOpts.workdir, WorkingDir: execOpts.Workdir,
} }
// collect all the environment variables for the container // collect all the environment variables for the container
var err error 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 return nil, err
} }
// If -d is not set, attach to everything by default // If -d is not set, attach to everything by default
if !execOpts.detach { if !execOpts.Detach {
execConfig.AttachStdout = true execConfig.AttachStdout = true
execConfig.AttachStderr = true execConfig.AttachStderr = true
if execOpts.interactive { if execOpts.Interactive {
execConfig.AttachStdin = true execConfig.AttachStdin = true
} }
} }
if execOpts.detachKeys != "" { if execOpts.DetachKeys != "" {
execConfig.DetachKeys = execOpts.detachKeys execConfig.DetachKeys = execOpts.DetachKeys
} else { } else {
execConfig.DetachKeys = configFile.DetachKeys execConfig.DetachKeys = configFile.DetachKeys
} }

View File

@ -17,11 +17,11 @@ import (
"gotest.tools/v3/fs" "gotest.tools/v3/fs"
) )
func withDefaultOpts(options execOptions) execOptions { func withDefaultOpts(options ExecOptions) ExecOptions {
options.env = opts.NewListOpts(opts.ValidateEnv) options.Env = opts.NewListOpts(opts.ValidateEnv)
options.envFile = opts.NewListOpts(nil) options.EnvFile = opts.NewListOpts(nil)
if len(options.command) == 0 { if len(options.Command) == 0 {
options.command = []string{"command"} options.Command = []string{"command"}
} }
return options return options
} }
@ -35,7 +35,7 @@ TWO=2
defer tmpFile.Remove() defer tmpFile.Remove()
testcases := []struct { testcases := []struct {
options execOptions options ExecOptions
configFile configfile.ConfigFile configFile configfile.ConfigFile
expected types.ExecConfig expected types.ExecConfig
}{ }{
@ -45,7 +45,7 @@ TWO=2
AttachStdout: true, AttachStdout: true,
AttachStderr: true, AttachStderr: true,
}, },
options: withDefaultOpts(execOptions{}), options: withDefaultOpts(ExecOptions{}),
}, },
{ {
expected: types.ExecConfig{ expected: types.ExecConfig{
@ -53,15 +53,15 @@ TWO=2
AttachStdout: true, AttachStdout: true,
AttachStderr: true, AttachStderr: true,
}, },
options: withDefaultOpts(execOptions{ options: withDefaultOpts(ExecOptions{
command: []string{"command1", "command2"}, Command: []string{"command1", "command2"},
}), }),
}, },
{ {
options: withDefaultOpts(execOptions{ options: withDefaultOpts(ExecOptions{
interactive: true, Interactive: true,
tty: true, TTY: true,
user: "uid", User: "uid",
}), }),
expected: types.ExecConfig{ expected: types.ExecConfig{
User: "uid", User: "uid",
@ -73,17 +73,17 @@ TWO=2
}, },
}, },
{ {
options: withDefaultOpts(execOptions{detach: true}), options: withDefaultOpts(ExecOptions{Detach: true}),
expected: types.ExecConfig{ expected: types.ExecConfig{
Detach: true, Detach: true,
Cmd: []string{"command"}, Cmd: []string{"command"},
}, },
}, },
{ {
options: withDefaultOpts(execOptions{ options: withDefaultOpts(ExecOptions{
tty: true, TTY: true,
interactive: true, Interactive: true,
detach: true, Detach: true,
}), }),
expected: types.ExecConfig{ expected: types.ExecConfig{
Detach: true, Detach: true,
@ -92,7 +92,7 @@ TWO=2
}, },
}, },
{ {
options: withDefaultOpts(execOptions{detach: true}), options: withDefaultOpts(ExecOptions{Detach: true}),
configFile: configfile.ConfigFile{DetachKeys: "de"}, configFile: configfile.ConfigFile{DetachKeys: "de"},
expected: types.ExecConfig{ expected: types.ExecConfig{
Cmd: []string{"command"}, Cmd: []string{"command"},
@ -101,9 +101,9 @@ TWO=2
}, },
}, },
{ {
options: withDefaultOpts(execOptions{ options: withDefaultOpts(ExecOptions{
detach: true, Detach: true,
detachKeys: "ab", DetachKeys: "ab",
}), }),
configFile: configfile.ConfigFile{DetachKeys: "de"}, configFile: configfile.ConfigFile{DetachKeys: "de"},
expected: types.ExecConfig{ expected: types.ExecConfig{
@ -119,9 +119,9 @@ TWO=2
AttachStderr: true, AttachStderr: true,
Env: []string{"ONE=1", "TWO=2"}, Env: []string{"ONE=1", "TWO=2"},
}, },
options: func() execOptions { options: func() ExecOptions {
o := withDefaultOpts(execOptions{}) o := withDefaultOpts(ExecOptions{})
o.envFile.Set(tmpFile.Path()) o.EnvFile.Set(tmpFile.Path())
return o return o
}(), }(),
}, },
@ -132,10 +132,10 @@ TWO=2
AttachStderr: true, AttachStderr: true,
Env: []string{"ONE=1", "TWO=2", "ONE=override"}, Env: []string{"ONE=1", "TWO=2", "ONE=override"},
}, },
options: func() execOptions { options: func() ExecOptions {
o := withDefaultOpts(execOptions{}) o := withDefaultOpts(ExecOptions{})
o.envFile.Set(tmpFile.Path()) o.EnvFile.Set(tmpFile.Path())
o.env.Set("ONE=override") o.Env.Set("ONE=override")
return o return o
}(), }(),
}, },
@ -149,8 +149,8 @@ TWO=2
} }
func TestParseExecNoSuchFile(t *testing.T) { func TestParseExecNoSuchFile(t *testing.T) {
execOpts := withDefaultOpts(execOptions{}) execOpts := withDefaultOpts(ExecOptions{})
execOpts.envFile.Set("no-such-env-file") execOpts.EnvFile.Set("no-such-env-file")
execConfig, err := parseExec(execOpts, &configfile.ConfigFile{}) execConfig, err := parseExec(execOpts, &configfile.ConfigFile{})
assert.ErrorContains(t, err, "no-such-env-file") assert.ErrorContains(t, err, "no-such-env-file")
assert.Check(t, os.IsNotExist(err)) assert.Check(t, os.IsNotExist(err))
@ -160,7 +160,7 @@ func TestParseExecNoSuchFile(t *testing.T) {
func TestRunExec(t *testing.T) { func TestRunExec(t *testing.T) {
var testcases = []struct { var testcases = []struct {
doc string doc string
options execOptions options ExecOptions
client fakeClient client fakeClient
expectedError string expectedError string
expectedOut string expectedOut string
@ -168,15 +168,15 @@ func TestRunExec(t *testing.T) {
}{ }{
{ {
doc: "successful detach", doc: "successful detach",
options: withDefaultOpts(execOptions{ options: withDefaultOpts(ExecOptions{
container: "thecontainer", Container: "thecontainer",
detach: true, Detach: true,
}), }),
client: fakeClient{execCreateFunc: execCreateWithID}, client: fakeClient{execCreateFunc: execCreateWithID},
}, },
{ {
doc: "inspect error", doc: "inspect error",
options: newExecOptions(), options: NewExecOptions(),
client: fakeClient{ client: fakeClient{
inspectFunc: func(string) (types.ContainerJSON, error) { inspectFunc: func(string) (types.ContainerJSON, error) {
return types.ContainerJSON{}, errors.New("failed inspect") return types.ContainerJSON{}, errors.New("failed inspect")
@ -186,7 +186,7 @@ func TestRunExec(t *testing.T) {
}, },
{ {
doc: "missing exec ID", doc: "missing exec ID",
options: newExecOptions(), options: NewExecOptions(),
expectedError: "exec ID empty", expectedError: "exec ID empty",
}, },
} }
@ -195,7 +195,7 @@ func TestRunExec(t *testing.T) {
t.Run(testcase.doc, func(t *testing.T) { t.Run(testcase.doc, func(t *testing.T) {
cli := test.NewFakeCli(&testcase.client) cli := test.NewFakeCli(&testcase.client)
err := runExec(cli, testcase.options) err := RunExec(cli, testcase.options)
if testcase.expectedError != "" { if testcase.expectedError != "" {
assert.ErrorContains(t, err, testcase.expectedError) assert.ErrorContains(t, err, testcase.expectedError)
} else { } else {

View File

@ -15,58 +15,65 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
type startOptions struct { // StartOptions group options for `start` command
attach bool type StartOptions struct {
openStdin bool Attach bool
detachKeys string OpenStdin bool
checkpoint string DetachKeys string
checkpointDir 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` // NewStartCommand creates a new cobra.Command for `docker start`
func NewStartCommand(dockerCli command.Cli) *cobra.Command { func NewStartCommand(dockerCli command.Cli) *cobra.Command {
var opts startOptions var opts StartOptions
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "start [OPTIONS] CONTAINER [CONTAINER...]", Use: "start [OPTIONS] CONTAINER [CONTAINER...]",
Short: "Start one or more stopped containers", Short: "Start one or more stopped containers",
Args: cli.RequiresMinArgs(1), Args: cli.RequiresMinArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
opts.containers = args opts.Containers = args
return runStart(dockerCli, &opts) return RunStart(dockerCli, &opts)
}, },
} }
flags := cmd.Flags() flags := cmd.Flags()
flags.BoolVarP(&opts.attach, "attach", "a", false, "Attach STDOUT/STDERR and forward signals") 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.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.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", "experimental", nil)
flags.SetAnnotation("checkpoint", "ostype", []string{"linux"}) 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", "experimental", nil)
flags.SetAnnotation("checkpoint-dir", "ostype", []string{"linux"}) flags.SetAnnotation("checkpoint-dir", "ostype", []string{"linux"})
return cmd return cmd
} }
// RunStart executes a `start` command
// nolint: gocyclo // nolint: gocyclo
func runStart(dockerCli command.Cli, opts *startOptions) error { func RunStart(dockerCli command.Cli, opts *StartOptions) error {
ctx, cancelFun := context.WithCancel(context.Background()) ctx, cancelFun := context.WithCancel(context.Background())
defer cancelFun() defer cancelFun()
if opts.attach || opts.openStdin { if opts.Attach || opts.OpenStdin {
// We're going to attach to a container. // We're going to attach to a container.
// 1. Ensure we only have one 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") return errors.New("you cannot start and attach multiple containers at once")
} }
// 2. Attach to the container. // 2. Attach to the container.
container := opts.containers[0] container := opts.Containers[0]
c, err := dockerCli.Client().ContainerInspect(ctx, container) c, err := dockerCli.Client().ContainerInspect(ctx, container)
if err != nil { if err != nil {
return err return err
@ -79,13 +86,13 @@ func runStart(dockerCli command.Cli, opts *startOptions) error {
defer signal.StopCatch(sigc) defer signal.StopCatch(sigc)
} }
if opts.detachKeys != "" { if opts.DetachKeys != "" {
dockerCli.ConfigFile().DetachKeys = opts.detachKeys dockerCli.ConfigFile().DetachKeys = opts.DetachKeys
} }
options := types.ContainerAttachOptions{ options := types.ContainerAttachOptions{
Stream: true, Stream: true,
Stdin: opts.openStdin && c.Config.OpenStdin, Stdin: opts.OpenStdin && c.Config.OpenStdin,
Stdout: true, Stdout: true,
Stderr: true, Stderr: true,
DetachKeys: dockerCli.ConfigFile().DetachKeys, 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. // no matter it's detached, removed on daemon side(--rm) or exit normally.
statusChan := waitExitOrRemoved(ctx, dockerCli, c.ID, c.HostConfig.AutoRemove) statusChan := waitExitOrRemoved(ctx, dockerCli, c.ID, c.HostConfig.AutoRemove)
startOptions := types.ContainerStartOptions{ startOptions := types.ContainerStartOptions{
CheckpointID: opts.checkpoint, CheckpointID: opts.Checkpoint,
CheckpointDir: opts.checkpointDir, CheckpointDir: opts.CheckpointDir,
} }
// 4. Start the container. // 4. Start the container.
@ -161,21 +168,21 @@ func runStart(dockerCli command.Cli, opts *startOptions) error {
if status := <-statusChan; status != 0 { if status := <-statusChan; status != 0 {
return cli.StatusError{StatusCode: status} return cli.StatusError{StatusCode: status}
} }
} else if opts.checkpoint != "" { } else if opts.Checkpoint != "" {
if len(opts.containers) > 1 { if len(opts.Containers) > 1 {
return errors.New("you cannot restore multiple containers at once") return errors.New("you cannot restore multiple containers at once")
} }
container := opts.containers[0] container := opts.Containers[0]
startOptions := types.ContainerStartOptions{ startOptions := types.ContainerStartOptions{
CheckpointID: opts.checkpoint, CheckpointID: opts.Checkpoint,
CheckpointDir: opts.checkpointDir, CheckpointDir: opts.CheckpointDir,
} }
return dockerCli.Client().ContainerStart(ctx, container, startOptions) return dockerCli.Client().ContainerStart(ctx, container, startOptions)
} else { } else {
// We're not going to attach to anything. // We're not going to attach to anything.
// Start as many containers as we want. // Start as many containers as we want.
return startContainersWithoutAttachments(ctx, dockerCli, opts.containers) return startContainersWithoutAttachments(ctx, dockerCli, opts.Containers)
} }
return nil return nil