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"
)
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
}

View File

@ -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 {

View File

@ -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