diff --git a/cmd/podman/commands.go b/cmd/podman/commands.go index 6156fc2f830..36b717036f7 100644 --- a/cmd/podman/commands.go +++ b/cmd/podman/commands.go @@ -12,7 +12,6 @@ const remoteclient = false func getMainCommands() []*cobra.Command { rootCommands := []*cobra.Command{ _commitCommand, - _execCommand, _playCommand, _loginCommand, _logoutCommand, diff --git a/cmd/podman/exec.go b/cmd/podman/exec.go index d0d88ee8b8b..d75c5c56bf3 100644 --- a/cmd/podman/exec.go +++ b/cmd/podman/exec.go @@ -1,16 +1,10 @@ package main import ( - "fmt" - "io/ioutil" - "os" - "strconv" - "github.com/containers/libpod/cmd/podman/cliconfig" - "github.com/containers/libpod/cmd/podman/libpodruntime" - "github.com/containers/libpod/cmd/podman/shared/parse" - "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/adapter" "github.com/pkg/errors" + "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -51,79 +45,33 @@ func init() { flags.IntVar(&execCommand.PreserveFDs, "preserve-fds", 0, "Pass N additional file descriptors to the container") flags.StringVarP(&execCommand.Workdir, "workdir", "w", "", "Working directory inside the container") markFlagHiddenForRemoteClient("latest", flags) + markFlagHiddenForRemoteClient("preserve-fds", flags) } func execCmd(c *cliconfig.ExecValues) error { - args := c.InputArgs - var ctr *libpod.Container - var err error - argStart := 1 - if len(args) < 1 && !c.Latest { - return errors.Errorf("you must provide one container name or id") - } - if len(args) < 2 && !c.Latest { - return errors.Errorf("you must provide a command to exec") - } if c.Latest { - argStart = 0 + if len(c.InputArgs) < 1 { + return errors.Errorf("you must provide a command to exec") + } + } else { + switch { + case len(c.InputArgs) < 1: + return errors.Errorf("you must provide one container name or id") + case len(c.InputArgs) < 2: + return errors.Errorf("you must provide a command to exec") + } } - cmd := args[argStart:] - runtime, err := libpodruntime.GetRuntime(&c.PodmanCommand) + + runtime, err := adapter.GetRuntime(&c.PodmanCommand) if err != nil { - return errors.Wrapf(err, "error creating libpod runtime") + return errors.Wrapf(err, "could not get runtime") } defer runtime.Shutdown(false) - if c.Latest { - ctr, err = runtime.GetLatestContainer() - } else { - ctr, err = runtime.LookupContainer(args[0]) - } + _, _, err = runtime.ContainerExecute(getContext(), c) if err != nil { - return errors.Wrapf(err, "unable to exec into %s", args[0]) - } - - if c.PreserveFDs > 0 { - entries, err := ioutil.ReadDir("/proc/self/fd") - if err != nil { - return errors.Wrapf(err, "unable to read /proc/self/fd") - } - m := make(map[int]bool) - for _, e := range entries { - i, err := strconv.Atoi(e.Name()) - if err != nil { - if err != nil { - return errors.Wrapf(err, "cannot parse %s in /proc/self/fd", e.Name()) - } - } - m[i] = true - } - for i := 3; i < 3+c.PreserveFDs; i++ { - if _, found := m[i]; !found { - return errors.New("invalid --preserve-fds=N specified. Not enough FDs available") - } - } - + logrus.Error(err) + return errors.Cause(err) } - - // ENVIRONMENT VARIABLES - env := map[string]string{} - - if err := parse.ReadKVStrings(env, []string{}, c.Env); err != nil { - return errors.Wrapf(err, "unable to process environment variables") - } - envs := []string{} - for k, v := range env { - envs = append(envs, fmt.Sprintf("%s=%s", k, v)) - } - - streams := new(libpod.AttachStreams) - streams.OutputStream = os.Stdout - streams.ErrorStream = os.Stderr - streams.InputStream = os.Stdin - streams.AttachOutput = true - streams.AttachError = true - streams.AttachInput = true - - return ctr.Exec(c.Tty, c.Privileged, envs, cmd, c.User, c.Workdir, streams, c.PreserveFDs) + return nil } diff --git a/cmd/podman/main.go b/cmd/podman/main.go index e8c3e14ea1f..c038d61d7f5 100644 --- a/cmd/podman/main.go +++ b/cmd/podman/main.go @@ -35,6 +35,7 @@ var mainCommands = []*cobra.Command{ _diffCommand, _createCommand, _eventsCommand, + _execCommand, _exportCommand, _generateCommand, _historyCommand, diff --git a/cmd/podman/varlink/io.podman.varlink b/cmd/podman/varlink/io.podman.varlink index b5295273a67..4a6b43ccc3d 100644 --- a/cmd/podman/varlink/io.podman.varlink +++ b/cmd/podman/varlink/io.podman.varlink @@ -502,6 +502,23 @@ type DiffInfo( changeType: string ) +type ExecOpts( + # container name or id + name: string, + # Create pseudo tty + tty: bool, + # privileged access in container + privileged: bool, + # command to execute in container + cmd: []string, + # user to use in container + user: ?string, + # workdir to run command in container + workdir: ?string, + # slice of keyword=value environment variables + env: ?[]string +) + # GetVersion returns version and build information of the podman service method GetVersion() -> ( version: string, @@ -1088,6 +1105,9 @@ method ContainerRestore(name: string, keep: bool, tcpEstablished: bool) -> (id: # ContainerRunlabel runs executes a command as described by a given container image label. method ContainerRunlabel(runlabel: Runlabel) -> () +# ContainerExecute runs executes a command in the given container. +method ContainerExecute(opts: ExecOpts) -> () + # ListContainerMounts gathers all the mounted container mount points and returns them as an array # of strings # #### Example diff --git a/pkg/adapter/containers.go b/pkg/adapter/containers.go index 931c55a57eb..30ed5e7f6d8 100644 --- a/pkg/adapter/containers.go +++ b/pkg/adapter/containers.go @@ -16,6 +16,7 @@ import ( "github.com/containers/libpod/cmd/podman/cliconfig" "github.com/containers/libpod/cmd/podman/shared" + "github.com/containers/libpod/cmd/podman/shared/parse" "github.com/containers/libpod/libpod" "github.com/containers/libpod/pkg/adapter/shortcuts" "github.com/containers/storage" @@ -510,3 +511,76 @@ func (r *LocalRuntime) Restore(c *cliconfig.RestoreValues, options libpod.Contai } return lastError } + +// ContainerExecute executes a command in the container +func (r *LocalRuntime) ContainerExecute(ctx context.Context, cli *cliconfig.ExecValues) ([]string, map[string]error, error) { + var ( + cmd []string + ctr *Container + err error + failures = map[string]error{} + ok = []string{} + ) + + if cli.Latest { + if ctr, err = r.GetLatestContainer(); err != nil { + return ok, failures, err + } + cmd = cli.InputArgs[0:] + } else { + if ctr, err = r.LookupContainer(cli.InputArgs[0]); err != nil { + return ok, failures, err + } + cmd = cli.InputArgs[1:] + } + + if cli.PreserveFDs > 0 { + entries, err := ioutil.ReadDir("/proc/self/fd") + if err != nil { + return ok, failures, errors.Wrapf(err, "Exec unable to read /proc/self/fd") + } + + m := make(map[int]bool) + for _, e := range entries { + i, err := strconv.Atoi(e.Name()) + if err != nil { + return ok, failures, errors.Wrapf(err, "Exec cannot parse %s in /proc/self/fd", e.Name()) + } + m[i] = true + } + + for i := 3; i < 3+cli.PreserveFDs; i++ { + if _, found := m[i]; !found { + return ok, failures, errors.New("invalid --preserve-fds=N specified. Not enough FDs available") + } + } + } + + // Validate given environment variables + env := map[string]string{} + if err := parse.ReadKVStrings(env, []string{}, cli.Env); err != nil { + return ok, failures, errors.Wrapf(err, "Exec unable to process environment variables") + } + + // Build env slice of key=value strings for Exec + envs := []string{} + for k, v := range env { + envs = append(envs, fmt.Sprintf("%s=%s", k, v)) + } + + streams := new(libpod.AttachStreams) + streams.OutputStream = os.Stdout + streams.ErrorStream = os.Stderr + streams.InputStream = os.Stdin + streams.AttachOutput = true + streams.AttachError = true + streams.AttachInput = true + + err = ctr.Exec(cli.Tty, cli.Privileged, envs, cmd, cli.User, cli.Workdir, streams, cli.PreserveFDs) + if err != nil { + failures[ctr.ID()] = err + } else { + ok = append(ok, ctr.ID()) + } + return ok, failures, err +} diff --git a/pkg/adapter/containers_remote.go b/pkg/adapter/containers_remote.go index 50cff9fa0ae..77f7205bca4 100644 --- a/pkg/adapter/containers_remote.go +++ b/pkg/adapter/containers_remote.go @@ -14,6 +14,7 @@ import ( "github.com/containers/libpod/cmd/podman/cliconfig" "github.com/containers/libpod/cmd/podman/shared" + "github.com/containers/libpod/cmd/podman/shared/parse" iopodman "github.com/containers/libpod/cmd/podman/varlink" "github.com/containers/libpod/libpod" "github.com/containers/libpod/pkg/inspect" @@ -609,3 +610,40 @@ func (r *LocalRuntime) Restore(c *cliconfig.RestoreValues, options libpod.Contai } return lastError } + +// ContainerExecute executes a command in the container +func (r *LocalRuntime) ContainerExecute(ctx context.Context, cli *cliconfig.ExecValues) ([]string, map[string]error, error) { + // Validate given environment variables + env := map[string]string{} + if err := parse.ReadKVStrings(env, []string{}, cli.Env); err != nil { + return nil, nil, errors.Wrapf(err, "Exec unable to process environment variables") + } + + // Build env slice of key=value strings for Exec + envs := []string{} + for k, v := range env { + envs = append(envs, fmt.Sprintf("%s=%s", k, v)) + } + + opts := iopodman.ExecOpts{ + Name: cli.InputArgs[0], + Tty: cli.Tty, + Privileged: cli.Privileged, + Cmd: cli.InputArgs[1:], + User: &cli.User, + Workdir: &cli.Workdir, + Env: &envs, + } + + receive, err := iopodman.ContainerExecute().Send(r.Conn, varlink.Upgrade, opts) + if err != nil { + return nil, nil, errors.Wrapf(err, "Exec failed to contact service for %s", cli.InputArgs) + } + + _, err = receive() + if err != nil { + return nil, nil, errors.Wrapf(err, "Exec operation failed for %s", cli.InputArgs) + } + + return nil, nil, nil +} diff --git a/pkg/adapter/runtime.go b/pkg/adapter/runtime.go index b5ec9f7a9e6..440138173cb 100644 --- a/pkg/adapter/runtime.go +++ b/pkg/adapter/runtime.go @@ -7,7 +7,6 @@ import ( "context" "io" "io/ioutil" - "k8s.io/api/core/v1" "os" "text/template" @@ -25,6 +24,7 @@ import ( "github.com/containers/libpod/pkg/rootless" "github.com/containers/storage/pkg/archive" "github.com/pkg/errors" + v1 "k8s.io/api/core/v1" ) // LocalRuntime describes a typical libpod runtime diff --git a/pkg/varlinkapi/attach.go b/pkg/varlinkapi/attach.go index 9e2a265be14..81a10e91612 100644 --- a/pkg/varlinkapi/attach.go +++ b/pkg/varlinkapi/attach.go @@ -71,9 +71,10 @@ func (i *LibpodAPI) Attach(call iopodman.VarlinkCall, name string, detachKeys st if finalErr != libpod.ErrDetach && finalErr != nil { logrus.Error(finalErr) } - quitWriter := virtwriter.NewVirtWriteCloser(writer, virtwriter.Quit) - _, err = quitWriter.Write([]byte("HANG-UP")) - // TODO error handling is not quite right here yet + + if err = virtwriter.HangUp(writer); err != nil { + logrus.Errorf("Failed to HANG-UP attach to %s: %s", ctr.ID(), err.Error()) + } return call.Writer.Flush() } diff --git a/pkg/varlinkapi/containers.go b/pkg/varlinkapi/containers.go index 17792ccfe95..1c1be220cd4 100644 --- a/pkg/varlinkapi/containers.go +++ b/pkg/varlinkapi/containers.go @@ -18,8 +18,11 @@ import ( "github.com/containers/libpod/libpod" "github.com/containers/libpod/pkg/adapter/shortcuts" cc "github.com/containers/libpod/pkg/spec" + "github.com/containers/libpod/pkg/varlinkapi/virtwriter" "github.com/containers/storage/pkg/archive" "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "k8s.io/client-go/tools/remotecommand" ) // ListContainers ... @@ -702,3 +705,79 @@ func newPodmanLogLine(line *libpod.LogLine) iopodman.LogLine { Cid: line.CID, } } + +// ContainerExecute is the varlink endpoint to execute a command in a container +func (i *LibpodAPI) ContainerExecute(call iopodman.VarlinkCall, opts iopodman.ExecOpts) error { + if !call.WantsUpgrade() { + return call.ReplyErrorOccurred("client must use upgraded connection to exec") + } + + ctr, err := i.Runtime.LookupContainer(opts.Name) + if err != nil { + return call.ReplyContainerNotFound(opts.Name, err.Error()) + } + + state, err := ctr.State() + if err != nil { + return call.ReplyErrorOccurred( + fmt.Sprintf("exec failed to obtain container %s state: %s", ctr.ID(), err.Error())) + } + + if state != libpod.ContainerStateRunning { + return call.ReplyErrorOccurred( + fmt.Sprintf("exec requires a running container, %s is %s", ctr.ID(), state.String())) + } + + envs := []string{} + if opts.Env != nil { + envs = *opts.Env + } + + var user string + if opts.User != nil { + user = *opts.User + } + + var workDir string + if opts.Workdir != nil { + workDir = *opts.Workdir + } + + resizeChan := make(chan remotecommand.TerminalSize) + errChan := make(chan error) + + reader, writer, _, pipeWriter, streams := setupStreams(call) + + go func() { + fmt.Printf("ContainerExec Start Reader\n") + if err := virtwriter.Reader(reader, nil, nil, pipeWriter, resizeChan); err != nil { + fmt.Printf("ContainerExec Reader err %s, %s\n", err.Error(), errors.Cause(err).Error()) + errChan <- err + } + }() + + // Debugging... + time.Sleep(5 * time.Second) + + go func() { + fmt.Printf("ContainerExec Start ctr.Exec\n") + err := ctr.Exec(opts.Tty, opts.Privileged, envs, opts.Cmd, user, workDir, streams, 0) + if err != nil { + fmt.Printf("ContainerExec Exec err %s, %s\n", err.Error(), errors.Cause(err).Error()) + errChan <- errors.Wrapf(err, "ContainerExec failed for container %s", ctr.ID()) + } + }() + + execErr := <-errChan + + if execErr != nil && errors.Cause(execErr) != io.EOF { + fmt.Printf("ContainerExec err: %s\n", execErr.Error()) + return call.ReplyErrorOccurred(execErr.Error()) + } + + if err = virtwriter.HangUp(writer); err != nil { + fmt.Printf("ContainerExec hangup err: %s\n", err.Error()) + logrus.Errorf("ContainerExec failed to HANG-UP on %s: %s", ctr.ID(), err.Error()) + } + return call.Writer.Flush() +} diff --git a/pkg/varlinkapi/virtwriter/virtwriter.go b/pkg/varlinkapi/virtwriter/virtwriter.go index 3adaf6e1714..55b72046272 100644 --- a/pkg/varlinkapi/virtwriter/virtwriter.go +++ b/pkg/varlinkapi/virtwriter/virtwriter.go @@ -4,10 +4,12 @@ import ( "bufio" "encoding/binary" "encoding/json" - "errors" + "fmt" "io" "os" + "github.com/pkg/errors" + "k8s.io/client-go/tools/remotecommand" ) @@ -96,9 +98,10 @@ func Reader(r *bufio.Reader, output, errput *os.File, input *io.PipeWriter, resi for { readb := make([]byte, 32*1024) n, err := r.Read(readb) + fmt.Println("Bytes read: ", n, err) // TODO, later may be worth checking in len of the read is 0 if err != nil { - return err + return errors.Wrapf(err, "Virtual Read failed, %d", n) } b := append(saveb, readb[0:n]...) // no sense in reading less than the header len @@ -137,7 +140,7 @@ func Reader(r *bufio.Reader, output, errput *os.File, input *io.PipeWriter, resi // Resize events come over in bytes, need to be reserialized resizeEvent := remotecommand.TerminalSize{} if err := json.Unmarshal(out, &resizeEvent); err != nil { - return err + return errors.Wrapf(err, "TerminalResize failed") } resize <- resizeEvent case Quit: @@ -145,7 +148,7 @@ func Reader(r *bufio.Reader, output, errput *os.File, input *io.PipeWriter, resi } b = b[eom:] } else { - // We do not have the header and full message, need to slurp again + // We do not have the header and full message, need to slurp again saveb = b break } @@ -153,3 +156,19 @@ func Reader(r *bufio.Reader, output, errput *os.File, input *io.PipeWriter, resi } return nil } + +// HangUp sends message to peer to close connection +func HangUp(writer *bufio.Writer) (err error) { + n := 0 + msg := []byte("HANG-UP") + + writeQuit := NewVirtWriteCloser(writer, Quit) + if n, err = writeQuit.Write(msg); err != nil { + return + } + + if n != len(msg) { + return errors.New(fmt.Sprintf("Failed to send complete %s message", string(msg))) + } + return +}