From e8a32e3722e0b814c654cdb8809565a21fc120d3 Mon Sep 17 00:00:00 2001 From: baude Date: Tue, 28 Nov 2017 16:25:01 -0600 Subject: kpod exec Initial wiring of kpod exec. We wont support the following options for exec: * detach -- unsure of use case * detach-keys -- not supported by runc * interactive -- all terminals will be interactive Not adding exec tests as we need to think about how to support a test that requires console access but our CI tests have no console. Signed-off-by: baude --- README.md | 1 + cmd/kpod/exec.go | 86 +++++++++++++++++++++++++++++++++++++ cmd/kpod/main.go | 1 + completions/bash/kpod | 16 +++++++ docs/kpod-exec.1.md | 43 +++++++++++++++++++ docs/kpod.1.md | 3 ++ libpod/container.go | 34 +++++++++++++-- libpod/oci.go | 6 +++ libpod/runc.go | 117 ++++++++++++++++++++++++++++++++++++++++++++++++++ test/kpod_exec.bats | 30 +++++++++++++ 10 files changed, 334 insertions(+), 3 deletions(-) create mode 100644 cmd/kpod/exec.go create mode 100644 docs/kpod-exec.1.md create mode 100644 libpod/runc.go create mode 100644 test/kpod_exec.bats diff --git a/README.md b/README.md index 049dbf00a..967682cb3 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ libpod is currently in active development. | [kpod-attach(1)](/docs/kpod-attach.1.md) | Attach to a running container. | [kpod-cp(1)](/docs/kpod-cp.1.md) | Instead of providing a `kpod cp` command, the man page `kpod-cp` describes how to use the `kpod mount` command to have even more flexibility and functionality.|| | [kpod-diff(1)](/docs/kpod-diff.1.md) | Inspect changes on a container or image's filesystem |[![...](/docs/play.png)](https://asciinema.org/a/FXfWB9CKYFwYM4EfqW3NSZy1G)| +| [kpod-exec(1)](/docs/kpod-exec.1.md) | Execute a command in a running container. | [kpod-export(1)](/docs/kpod-export.1.md) | Export container's filesystem contents as a tar archive |[![...](/docs/play.png)](https://asciinema.org/a/913lBIRAg5hK8asyIhhkQVLtV)| | [kpod-history(1)](/docs/kpod-history.1.md) | Shows the history of an image |[![...](/docs/play.png)](https://asciinema.org/a/bCvUQJ6DkxInMELZdc5DinNSx)| | [kpod-images(1)](/docs/kpod-images.1.md) | List images in local storage |[![...](/docs/play.png)](https://asciinema.org/a/133649)| diff --git a/cmd/kpod/exec.go b/cmd/kpod/exec.go new file mode 100644 index 000000000..f76983810 --- /dev/null +++ b/cmd/kpod/exec.go @@ -0,0 +1,86 @@ +package main + +import ( + "fmt" + "strings" + + "github.com/pkg/errors" + "github.com/projectatomic/libpod/libpod" + "github.com/urfave/cli" +) + +var ( + execFlags = []cli.Flag{ + cli.StringSliceFlag{ + Name: "env, e", + Usage: "Set environment variables", + }, + cli.BoolFlag{ + Name: "privileged", + Usage: "Give the process extended Linux capabilities inside the container. The default is false", + }, + cli.BoolFlag{ + Name: "tty, t", + Usage: "Allocate a pseudo-TTY. The default is false", + }, + cli.StringFlag{ + Name: "user, u", + Usage: "Sets the username or UID used and optionally the groupname or GID for the specified command", + }, + } + execDescription = ` + kpod exec + + Run a command in a running container +` + + execCommand = cli.Command{ + Name: "exec", + Usage: "Run a process in a running container", + Description: execDescription, + Flags: execFlags, + Action: execCmd, + ArgsUsage: "CONTAINER-NAME", + } +) + +func execCmd(c *cli.Context) error { + var envs []string + args := c.Args() + if len(args) < 1 { + return errors.Errorf("you must provide one container name or id") + } + if len(args) < 2 { + return errors.Errorf("you must provide a command to exec") + } + cmd := args[1:] + runtime, err := getRuntime(c) + if err != nil { + return errors.Wrapf(err, "error creating libpod runtime") + } + defer runtime.Shutdown(false) + + ctr, err := runtime.LookupContainer(args[0]) + if err != nil { + return errors.Wrapf(err, "unable to exec into %s", args[0]) + } + // Create a list of keys provided by the user + var userEnvKeys []string + for _, env := range c.StringSlice("env") { + splitEnv := strings.Split(env, "=") + userEnvKeys = append(userEnvKeys, splitEnv[0]) + } + + envs = append(envs, c.StringSlice("env")...) + + // if the default key isnt in the user-provided list, add the default + // key and value to the environment variables. this is needed to set + // PATH for example. + for k, v := range defaultEnvVariables { + if !libpod.StringInSlice(k, userEnvKeys) { + envs = append(envs, fmt.Sprintf("%s=%s", k, v)) + } + } + + return ctr.Exec(c.Bool("tty"), c.Bool("privileged"), envs, cmd, c.String("user")) +} diff --git a/cmd/kpod/main.go b/cmd/kpod/main.go index 7382644b9..708031dc9 100644 --- a/cmd/kpod/main.go +++ b/cmd/kpod/main.go @@ -37,6 +37,7 @@ func main() { attachCommand, createCommand, diffCommand, + execCommand, exportCommand, historyCommand, imagesCommand, diff --git a/completions/bash/kpod b/completions/bash/kpod index a5da95a3e..6c66c63c0 100644 --- a/completions/bash/kpod +++ b/completions/bash/kpod @@ -682,6 +682,21 @@ _kpod_diff() { esac } +_kpod_exec() { + local options_with_args=" + -e + --env + --user + -u + " + local boolean_options=" + --privileged + --tty + -t + " + _complete_ "$options_with_args" "$boolean_options" + +} _kpod_export() { local options_with_args=" --output @@ -1437,6 +1452,7 @@ _kpod_kpod() { attach create diff + exec export history images diff --git a/docs/kpod-exec.1.md b/docs/kpod-exec.1.md new file mode 100644 index 000000000..1de5da39b --- /dev/null +++ b/docs/kpod-exec.1.md @@ -0,0 +1,43 @@ +% kpod(1) kpod-exec - Execute a command in a running container +% Brent Baude +# kpod-exec "1" "December 2017" "kpod" + +## NAME +kpod-exec - Execute a command in a running container + +## SYNOPSIS +**kpod exec** +**CONTAINER** +[COMMAND] [ARG...] +[**--help**|**-h**] + +## DESCRIPTION +**kpod exec** executes a command in a running container. + +## OPTIONS +**--env, e** +You may specify arbitrary environment variables that are available for the +command to be executed. + +**--interactive, -i** +Not supported. All exec commands are interactive by default. + +**--privileged** +Give the process extended Linux capabilities when running the command in container. + +**--tty, -t** +Allocate a pseudo-TTY. + +**--user, -u** +Sets the username or UID used and optionally the groupname or GID for the specified command. +The following examples are all valid: +--user [user | user:group | uid | uid:gid | user:gid | uid:group ] + +## EXAMPLES + + +## SEE ALSO +kpod(1), kpod-run(1) + +## HISTORY +December 2017, Originally compiled by Brent Baude diff --git a/docs/kpod.1.md b/docs/kpod.1.md index 7009289c7..dec29e395 100644 --- a/docs/kpod.1.md +++ b/docs/kpod.1.md @@ -61,6 +61,9 @@ create a new container ### diff Inspect changes on a container or image's filesystem +## exec +Execute a command in a running container. + ### export Export container's filesystem contents as a tar archive diff --git a/libpod/container.go b/libpod/container.go index 40aa689ff..474d4be95 100644 --- a/libpod/container.go +++ b/libpod/container.go @@ -14,6 +14,7 @@ import ( "github.com/containers/storage" "github.com/containers/storage/pkg/archive" + "github.com/docker/docker/daemon/caps" "github.com/docker/docker/pkg/mount" "github.com/docker/docker/pkg/namesgenerator" "github.com/docker/docker/pkg/stringid" @@ -552,9 +553,36 @@ func (c *Container) Kill(signal uint) error { } // Exec starts a new process inside the container -// Returns fully qualified URL of streaming server for executed process -func (c *Container) Exec(cmd []string, tty bool, stdin bool) (string, error) { - return "", ErrNotImplemented +func (c *Container) Exec(tty, privileged bool, env, cmd []string, user string) error { + var capList []string + c.lock.Lock() + defer c.lock.Unlock() + + if err := c.syncContainer(); err != nil { + return err + } + + conState := c.state.State + + if conState != ContainerStateRunning { + return errors.Errorf("cannot attach to container that is not running") + } + if privileged { + capList = caps.GetAllCapabilities() + } + globalOpts := runcGlobalOptions{ + log: c.LogPath(), + } + execOpts := runcExecOptions{ + capAdd: capList, + pidFile: filepath.Join(c.state.RunDir, fmt.Sprintf("%s-execpid", stringid.GenerateNonCryptoID()[:12])), + env: env, + user: user, + cwd: c.config.Spec.Process.Cwd, + tty: tty, + } + + return c.runtime.ociRuntime.execContainer(c, cmd, globalOpts, execOpts) } // Attach attaches to a container diff --git a/libpod/oci.go b/libpod/oci.go index 6ea6bb64f..323e97273 100644 --- a/libpod/oci.go +++ b/libpod/oci.go @@ -454,3 +454,9 @@ func (r *OCIRuntime) pauseContainer(ctr *Container) error { func (r *OCIRuntime) unpauseContainer(ctr *Container) error { return utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, r.path, "resume", ctr.ID()) } + +//execContiner executes a command in a running container +func (r *OCIRuntime) execContainer(c *Container, cmd []string, globalOpts runcGlobalOptions, commandOpts runcExecOptions) error { + r.RuncExec(c, cmd, globalOpts, commandOpts) + return nil +} diff --git a/libpod/runc.go b/libpod/runc.go new file mode 100644 index 000000000..efbb51b24 --- /dev/null +++ b/libpod/runc.go @@ -0,0 +1,117 @@ +package libpod + +import ( + "os" + "os/exec" + "strings" + + "github.com/sirupsen/logrus" +) + +type runcGlobalOptions struct { + log string + logFormat string + root string + criu string + systemdCgroup bool +} +type runcExecOptions struct { + consoleSocket string + cwd string + env []string + tty bool + user string + processPath string + detach bool + pidFile string + processLabel string + apparmor string + noNewPrivs bool + capAdd []string +} + +func parseGlobalOptionsToArgs(opts runcGlobalOptions) []string { + args := []string{} + if opts.log != "" { + args = append(args, "--log", opts.log) + } + if opts.logFormat != "" { + args = append(args, "--log-format", opts.logFormat) + } + if opts.root != "" { + args = append(args, "--root", opts.root) + } + if opts.criu != "" { + args = append(args, "--criu", opts.criu) + } + if opts.systemdCgroup { + args = append(args, "--systemd-cgroup") + } + return args +} + +// RuncExec executes 'runc --options exec --options cmd' +func (r *OCIRuntime) RuncExec(container *Container, command []string, globalOpts runcGlobalOptions, execOpts runcExecOptions) error { + args := []string{} + args = append(args, parseGlobalOptionsToArgs(globalOpts)...) + // Add subcommand + args = append(args, "exec") + // Now add subcommand args + + if execOpts.consoleSocket != "" { + args = append(args, "--console-socket", execOpts.consoleSocket) + } + if execOpts.cwd != "" { + args = append(args, "--cwd", execOpts.cwd) + } + + if len(execOpts.env) > 0 { + for _, envInput := range execOpts.env { + args = append(args, "--env", envInput) + } + } + if execOpts.tty { + args = append(args, "--tty") + } + if execOpts.user != "" { + args = append(args, "--user", execOpts.user) + + } + if execOpts.processPath != "" { + args = append(args, "--process", execOpts.processPath) + } + if execOpts.detach { + args = append(args, "--detach") + } + if execOpts.pidFile != "" { + args = append(args, "--pid-file", execOpts.pidFile) + } + if execOpts.processLabel != "" { + args = append(args, "--process-label", execOpts.processLabel) + } + if execOpts.apparmor != "" { + args = append(args, "--apparmor", execOpts.apparmor) + } + if execOpts.noNewPrivs { + args = append(args, "--no-new-privs") + } + if len(execOpts.capAdd) > 0 { + for _, capAddValue := range execOpts.capAdd { + args = append(args, "--cap", capAddValue) + } + } + + // Append Cid + args = append(args, container.ID()) + // Append Cmd + args = append(args, command...) + + logrus.Debug("Executing runc command: %s %s", r.path, strings.Join(args, " ")) + cmd := exec.Command(r.path, args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + cmd.Start() + err := cmd.Wait() + return err +} diff --git a/test/kpod_exec.bats b/test/kpod_exec.bats new file mode 100644 index 000000000..d495b1687 --- /dev/null +++ b/test/kpod_exec.bats @@ -0,0 +1,30 @@ +#!/usr/bin/env bats + +load helpers + +function teardown() { + cleanup_test +} + +function setup() { + copy_images +} + +@test "exec into a bogus container" { + run ${KPOD_BINARY} ${KPOD_OPTIONS} exec foobar ls + echo "$output" + [ "$status" -eq 1 ] +} + +@test "exec without command should fail" { + run ${KPOD_BINARY} ${KPOD_OPTIONS} exec foobar + echo "$output" + [ "$status" -eq 1 ] +} + +@test "exec simple command" { + ${KPOD_BINARY} ${KPOD_OPTIONS} run -d -t --name foobar1 ${ALPINE} sleep 60 + run ${KPOD_BINARY} ${KPOD_OPTIONS} exec foobar1 ls + echo "$output" + [ "$status" -eq 0 ] +} -- cgit v1.2.3-54-g00ecf