Add the ability to attach remotely to a container

Also, you can now podman-remote run -it.  There are some bugs that need
to be ironed out but I would prefer to merge this so we can make both
progress on start and exec as well as the bugs.

* when doing podman-remote run -it foo /bin/bash, you have to press
enter to get the prompt to display. with the localized podman, we had to
teach it connect to the console first and then start the container so we
did not miss anything.

* when executing "exit" in the console, we get a hard lockup likely
because nobody knows what to do.

* custom detach keys are not supported

* podman-remote run -it alpine ls does not currently work.  only
dropping to a shell works.

Signed-off-by: baude <bbaude@redhat.com>
This commit is contained in:
baude
2019-03-22 13:32:48 -05:00
parent 2f2c7660c3
commit fbcda7772d
11 changed files with 459 additions and 56 deletions

21
API.md
View File

@ -3,6 +3,10 @@ Podman Service Interface and API description. The master version of this docume
in the [API.md](https://github.com/containers/libpod/blob/master/API.md) file in the upstream libpod repository.
## Index
[func Attach(name: string) ](#Attach)
[func AttachControl(name: string) ](#AttachControl)
[func BuildImage(build: BuildInfo) MoreResponse](#BuildImage)
[func BuildImageHierarchyMap(name: string) string](#BuildImageHierarchyMap)
@ -135,6 +139,8 @@ in the [API.md](https://github.com/containers/libpod/blob/master/API.md) file in
[func SendFile(type: string, length: int) string](#SendFile)
[func Spec(name: string) string](#Spec)
[func StartContainer(name: string) string](#StartContainer)
[func StartPod(name: string) string](#StartPod)
@ -252,6 +258,16 @@ in the [API.md](https://github.com/containers/libpod/blob/master/API.md) file in
[error WantsMoreRequired](#WantsMoreRequired)
## Methods
### <a name="Attach"></a>func Attach
<div style="background-color: #E8E8E8; padding: 15px; margin: 10px; border-radius: 10px;">
method Attach(name: [string](https://godoc.org/builtin#string)) </div>
### <a name="AttachControl"></a>func AttachControl
<div style="background-color: #E8E8E8; padding: 15px; margin: 10px; border-radius: 10px;">
method AttachControl(name: [string](https://godoc.org/builtin#string)) </div>
### <a name="BuildImage"></a>func BuildImage
<div style="background-color: #E8E8E8; padding: 15px; margin: 10px; border-radius: 10px;">
@ -945,6 +961,11 @@ search results per registry.
method SendFile(type: [string](https://godoc.org/builtin#string), length: [int](https://godoc.org/builtin#int)) [string](https://godoc.org/builtin#string)</div>
Sendfile allows a remote client to send a file to the host
### <a name="Spec"></a>func Spec
<div style="background-color: #E8E8E8; padding: 15px; margin: 10px; border-radius: 10px;">
method Spec(name: [string](https://godoc.org/builtin#string)) [string](https://godoc.org/builtin#string)</div>
Spec returns the oci spec for a container. This call is for development of Podman only and generally should not be used.
### <a name="StartContainer"></a>func StartContainer
<div style="background-color: #E8E8E8; padding: 15px; margin: 10px; border-radius: 10px;">

View File

@ -1,11 +1,7 @@
package main
import (
"os"
"github.com/containers/libpod/cmd/podman/cliconfig"
"github.com/containers/libpod/cmd/podman/libpodruntime"
"github.com/containers/libpod/libpod"
"github.com/containers/libpod/pkg/adapter"
"github.com/pkg/errors"
"github.com/spf13/cobra"
@ -39,49 +35,21 @@ func init() {
flags.BoolVar(&attachCommand.SigProxy, "sig-proxy", true, "Proxy received signals to the process")
flags.BoolVarP(&attachCommand.Latest, "latest", "l", false, "Act on the latest container podman is aware of")
markFlagHiddenForRemoteClient("latest", flags)
// TODO allow for passing of a new deatch keys
markFlagHiddenForRemoteClient("detach-keys", flags)
}
func attachCmd(c *cliconfig.AttachValues) error {
args := c.InputArgs
var ctr *libpod.Container
if len(c.InputArgs) > 1 || (len(c.InputArgs) == 0 && !c.Latest) {
return errors.Errorf("attach requires the name or id of one running container or the latest flag")
}
runtime, err := libpodruntime.GetRuntime(&c.PodmanCommand)
if remoteclient && len(c.InputArgs) != 1 {
return errors.Errorf("attach requires the name or id of one running container")
}
runtime, err := adapter.GetRuntime(&c.PodmanCommand)
if err != nil {
return errors.Wrapf(err, "error creating libpod runtime")
return errors.Wrapf(err, "error creating runtime")
}
defer runtime.Shutdown(false)
if c.Latest {
ctr, err = runtime.GetLatestContainer()
} else {
ctr, err = runtime.LookupContainer(args[0])
}
if err != nil {
return errors.Wrapf(err, "unable to exec into %s", args[0])
}
conState, err := ctr.State()
if err != nil {
return errors.Wrapf(err, "unable to determine state of %s", args[0])
}
if conState != libpod.ContainerStateRunning {
return errors.Errorf("you can only attach to running containers")
}
inputStream := os.Stdin
if c.NoStdin {
inputStream = nil
}
// If the container is in a pod, also set to recursively start dependencies
if err := adapter.StartAttachCtr(getContext(), ctr, os.Stdout, os.Stderr, inputStream, c.DetachKeys, c.SigProxy, false, ctr.PodID() != ""); err != nil && errors.Cause(err) != libpod.ErrDetach {
return errors.Wrapf(err, "error attaching to container %s", ctr.ID())
}
return nil
return runtime.Attach(getContext(), c)
}

View File

@ -11,7 +11,6 @@ const remoteclient = false
// Commands that the local client implements
func getMainCommands() []*cobra.Command {
rootCommands := []*cobra.Command{
_attachCommand,
_commitCommand,
_execCommand,
_generateCommand,
@ -47,7 +46,6 @@ func getImageSubCommands() []*cobra.Command {
func getContainerSubCommands() []*cobra.Command {
return []*cobra.Command{
_attachCommand,
_checkpointCommand,
_cleanupCommand,
_commitCommand,

View File

@ -50,6 +50,7 @@ var (
// Commands that are universally implemented.
containerCommands = []*cobra.Command{
_attachCommand,
_containerExistsCommand,
_contInspectSubCommand,
_diffCommand,

View File

@ -38,6 +38,7 @@ var (
// Commands that the remote and local client have
// implemented.
var mainCommands = []*cobra.Command{
_attachCommand,
_buildCommand,
_diffCommand,
_createCommand,

View File

@ -658,8 +658,9 @@ method PauseContainer(name: string) -> (container: string)
# See also [PauseContainer](#PauseContainer).
method UnpauseContainer(name: string) -> (container: string)
# This method has not be implemented yet.
# method AttachToContainer() -> (notimplemented: NotImplemented)
method Attach(name: string) -> ()
method AttachControl(name: string) -> ()
# GetAttachSockets takes the name or ID of an existing container. It returns file paths for two sockets needed
# to properly communicate with a container. The first is the actual I/O socket that the container uses. The
@ -1154,6 +1155,9 @@ method PodStateData(name: string) -> (config: string)
# This call is for the development of Podman only and should not be used.
method CreateFromCC(in: []string) -> (id: string)
# Spec returns the oci spec for a container. This call is for development of Podman only and generally should not be used.
method Spec(name: string) -> (config: string)
# Sendfile allows a remote client to send a file to the host
method SendFile(type: string, length: int) -> (file_handle: string)

View File

@ -407,3 +407,39 @@ func (r *LocalRuntime) Ps(c *cliconfig.PsValues, opts shared.PsOptions) ([]share
logrus.Debugf("Setting maximum workers to %d", maxWorkers)
return shared.GetPsContainerOutput(r.Runtime, opts, c.Filter, maxWorkers)
}
// Attach ...
func (r *LocalRuntime) Attach(ctx context.Context, c *cliconfig.AttachValues) error {
var (
ctr *libpod.Container
err error
)
if c.Latest {
ctr, err = r.Runtime.GetLatestContainer()
} else {
ctr, err = r.Runtime.LookupContainer(c.InputArgs[0])
}
if err != nil {
return errors.Wrapf(err, "unable to exec into %s", c.InputArgs[0])
}
conState, err := ctr.State()
if err != nil {
return errors.Wrapf(err, "unable to determine state of %s", ctr.ID())
}
if conState != libpod.ContainerStateRunning {
return errors.Errorf("you can only attach to running containers")
}
inputStream := os.Stdin
if c.NoStdin {
inputStream = nil
}
// If the container is in a pod, also set to recursively start dependencies
if err := StartAttachCtr(ctx, ctr, os.Stdout, os.Stderr, inputStream, c.DetachKeys, c.SigProxy, false, ctr.PodID() != ""); err != nil && errors.Cause(err) != libpod.ErrDetach {
return errors.Wrapf(err, "error attaching to container %s", ctr.ID())
}
return nil
}

View File

@ -6,19 +6,25 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"os"
"strconv"
"syscall"
"time"
"github.com/containers/libpod/cmd/podman/cliconfig"
"github.com/containers/libpod/cmd/podman/shared"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
iopodman "github.com/containers/libpod/cmd/podman/varlink"
"github.com/containers/libpod/libpod"
"github.com/containers/libpod/pkg/inspect"
"github.com/containers/libpod/pkg/varlinkapi/virtwriter"
"github.com/docker/docker/pkg/term"
"github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/varlink/go/varlink"
"golang.org/x/crypto/ssh/terminal"
"k8s.io/client-go/tools/remotecommand"
)
// Inspect returns an inspect struct from varlink
@ -71,6 +77,19 @@ func (r *LocalRuntime) ContainerState(name string) (*libpod.ContainerState, erro
}
// Spec obtains the container spec.
func (r *LocalRuntime) Spec(name string) (*specs.Spec, error) {
reply, err := iopodman.Spec().Call(r.Conn, name)
if err != nil {
return nil, err
}
data := specs.Spec{}
if err := json.Unmarshal([]byte(reply), &data); err != nil {
return nil, err
}
return &data, nil
}
// LookupContainer gets basic information about container over a varlink
// connection and then translates it to a *Container
func (r *LocalRuntime) LookupContainer(idOrName string) (*Container, error) {
@ -79,10 +98,6 @@ func (r *LocalRuntime) LookupContainer(idOrName string) (*Container, error) {
return nil, err
}
config := r.Config(idOrName)
if err != nil {
return nil, err
}
return &Container{
remoteContainer{
r,
@ -322,19 +337,33 @@ func (r *LocalRuntime) CreateContainer(ctx context.Context, c *cliconfig.CreateV
// Run creates a container overvarlink and then starts it
func (r *LocalRuntime) Run(ctx context.Context, c *cliconfig.RunValues, exitCode int) (int, error) {
// FIXME
// podman-remote run -it alpine ls DOES NOT WORK YET
// podman-remote run -it alpine /bin/sh does, i suspect there is some sort of
// timing issue between the socket availability and terminal setup and the command
// being run.
// TODO the exit codes for run need to be figured out for remote connections
if !c.Bool("detach") {
return 0, errors.New("the remote client only supports detached containers")
}
results := shared.NewIntermediateLayer(&c.PodmanCommand)
cid, err := iopodman.CreateContainer().Call(r.Conn, results.MakeVarlink())
if err != nil {
return 0, err
}
fmt.Println(cid)
_, err = iopodman.StartContainer().Call(r.Conn, cid)
if err != nil {
return 0, err
}
errChan, err := r.attach(ctx, os.Stdin, os.Stdout, cid)
if err != nil {
return 0, err
}
if c.Bool("detach") {
fmt.Println(cid)
return 0, err
}
finalError := <-errChan
return 0, finalError
}
func ReadExitFile(runtimeTmp, ctrID string) (int, error) {
return 0, libpod.ErrNotImplemented
@ -411,3 +440,102 @@ func (r *LocalRuntime) Ps(c *cliconfig.PsValues, opts shared.PsOptions) ([]share
}
return psContainers, nil
}
func (r *LocalRuntime) attach(ctx context.Context, stdin, stdout *os.File, cid string) (chan error, error) {
var (
oldTermState *term.State
)
errChan := make(chan error)
spec, err := r.Spec(cid)
if err != nil {
return nil, err
}
resize := make(chan remotecommand.TerminalSize)
haveTerminal := terminal.IsTerminal(int(os.Stdin.Fd()))
// Check if we are attached to a terminal. If we are, generate resize
// events, and set the terminal to raw mode
if haveTerminal && spec.Process.Terminal {
logrus.Debugf("Handling terminal attach")
subCtx, cancel := context.WithCancel(ctx)
defer cancel()
resizeTty(subCtx, resize)
oldTermState, err = term.SaveState(os.Stdin.Fd())
if err != nil {
return nil, errors.Wrapf(err, "unable to save terminal state")
}
logrus.SetFormatter(&RawTtyFormatter{})
term.SetRawTerminal(os.Stdin.Fd())
}
_, err = iopodman.Attach().Send(r.Conn, varlink.Upgrade, cid)
if err != nil {
restoreTerminal(oldTermState)
return nil, err
}
// These are the varlink sockets
reader := r.Conn.Reader
writer := r.Conn.Writer
// These are the special writers that encode input from the client.
varlinkStdinWriter := virtwriter.NewVirtWriteCloser(writer, virtwriter.ToStdin)
varlinkResizeWriter := virtwriter.NewVirtWriteCloser(writer, virtwriter.TerminalResize)
go func() {
// Read from the wire and direct to stdout or stderr
err := virtwriter.Reader(reader, stdout, os.Stderr, nil, nil)
defer restoreTerminal(oldTermState)
errChan <- err
}()
go func() {
for termResize := range resize {
b, err := json.Marshal(termResize)
if err != nil {
defer restoreTerminal(oldTermState)
errChan <- err
}
_, err = varlinkResizeWriter.Write(b)
if err != nil {
defer restoreTerminal(oldTermState)
errChan <- err
}
}
}()
// Takes stdinput and sends it over the wire after being encoded
go func() {
if _, err := io.Copy(varlinkStdinWriter, stdin); err != nil {
defer restoreTerminal(oldTermState)
errChan <- err
}
}()
return errChan, nil
}
// Attach to a remote terminal
func (r *LocalRuntime) Attach(ctx context.Context, c *cliconfig.AttachValues) error {
ctr, err := r.LookupContainer(c.InputArgs[0])
if err != nil {
return nil
}
if ctr.state.State != libpod.ContainerStateRunning {
return errors.New("you can only attach to running containers")
}
inputStream := os.Stdin
if c.NoStdin {
inputStream = nil
}
errChan, err := r.attach(ctx, inputStream, os.Stdout, c.InputArgs[0])
if err != nil {
return err
}
return <-errChan
}

75
pkg/varlinkapi/attach.go Normal file
View File

@ -0,0 +1,75 @@
// +build varlink
package varlinkapi
import (
"io"
"github.com/containers/libpod/cmd/podman/varlink"
"github.com/containers/libpod/libpod"
"github.com/containers/libpod/pkg/varlinkapi/virtwriter"
"k8s.io/client-go/tools/remotecommand"
)
// Close is method to close the writer
// Attach ...
func (i *LibpodAPI) Attach(call iopodman.VarlinkCall, name string) error {
var finalErr error
resize := make(chan remotecommand.TerminalSize)
errChan := make(chan error)
if !call.WantsUpgrade() {
return call.ReplyErrorOccurred("client must use upgraded connection to attach")
}
ctr, err := i.Runtime.LookupContainer(name)
if err != nil {
return call.ReplyErrorOccurred(err.Error())
}
// These are the varlink sockets
reader := call.Call.Reader
writer := call.Call.Writer
// This pipe is used to pass stdin from the client to the input stream
// once the msg has been "decoded"
pr, pw := io.Pipe()
stdoutWriter := virtwriter.NewVirtWriteCloser(writer, virtwriter.ToStdout)
// TODO if runc ever starts passing stderr, we can too
//stderrWriter := NewVirtWriteCloser(writer, ToStderr)
streams := libpod.AttachStreams{
OutputStream: stdoutWriter,
InputStream: pr,
// Runc eats the error stream
ErrorStream: stdoutWriter,
AttachInput: true,
AttachOutput: true,
// Runc eats the error stream
AttachError: true,
}
go func() {
if err := virtwriter.Reader(reader, nil, nil, pw, resize); err != nil {
errChan <- err
}
}()
go func() {
// TODO allow for customizable detach keys
if err := ctr.Attach(&streams, "", resize); err != nil {
errChan <- err
}
}()
select {
// Blocking on an error
case finalErr = <-errChan:
// Need to close up shop
_ = finalErr
}
quitWriter := virtwriter.NewVirtWriteCloser(writer, virtwriter.Quit)
_, err = quitWriter.Write([]byte("HANG-UP"))
return call.Writer.Flush()
}

View File

@ -634,6 +634,22 @@ func (i *LibpodAPI) GetContainerStatsWithHistory(call iopodman.VarlinkCall, prev
return call.ReplyGetContainerStatsWithHistory(cStats)
}
// Spec ...
func (i *LibpodAPI) Spec(call iopodman.VarlinkCall, name string) error {
ctr, err := i.Runtime.LookupContainer(name)
if err != nil {
return call.ReplyErrorOccurred(err.Error())
}
spec := ctr.Spec()
b, err := json.Marshal(spec)
if err != nil {
return call.ReplyErrorOccurred(err.Error())
}
return call.ReplySpec(string(b))
}
// GetContainersLogs is the varlink endpoint to obtain one or more container logs
func (i *LibpodAPI) GetContainersLogs(call iopodman.VarlinkCall, names []string, follow, latest bool, since string, tail int64, timestamps bool) error {
var wg sync.WaitGroup

View File

@ -0,0 +1,155 @@
package virtwriter
import (
"bufio"
"encoding/binary"
"encoding/json"
"errors"
"io"
"os"
"k8s.io/client-go/tools/remotecommand"
)
// SocketDest is the "key" to where IO should go on the varlink
// multiplexed socket
type SocketDest int
const (
// ToStdout indicates traffic should go stdout
ToStdout SocketDest = iota
// ToStdin indicates traffic came from stdin
ToStdin SocketDest = iota
// ToStderr indicates traffuc should go to stderr
ToStderr SocketDest = iota
// TerminalResize indicates a terminal resize event has occurred
// and data should be passed to resizer
TerminalResize SocketDest = iota
// Quit and detach
Quit SocketDest = iota
)
// IntToSocketDest returns a socketdest based on integer input
func IntToSocketDest(i int) SocketDest {
switch i {
case ToStdout.Int():
return ToStdout
case ToStderr.Int():
return ToStderr
case ToStdin.Int():
return ToStdin
case TerminalResize.Int():
return TerminalResize
case Quit.Int():
return Quit
default:
return ToStderr
}
}
// Int returns the integer representation of the socket dest
func (sd SocketDest) Int() int {
return int(sd)
}
// VirtWriteCloser are writers for attach which include the dest
// of the data
type VirtWriteCloser struct {
writer *bufio.Writer
dest SocketDest
}
// NewVirtWriteCloser is a constructor
func NewVirtWriteCloser(w *bufio.Writer, dest SocketDest) VirtWriteCloser {
return VirtWriteCloser{w, dest}
}
// Close is a required method for a writecloser
func (v VirtWriteCloser) Close() error {
return nil
}
// Write prepends a header to the input message. The header is
// 8bytes. Position one contains the destination. Positions
// 5,6,7,8 are a big-endian encoded uint32 for len of the message.
func (v VirtWriteCloser) Write(input []byte) (int, error) {
header := []byte{byte(v.dest), 0, 0, 0}
// Go makes us define the byte for big endian
mlen := make([]byte, 4)
binary.BigEndian.PutUint32(mlen, uint32(len(input)))
// append the message len to the header
msg := append(header, mlen...)
// append the message to the header
msg = append(msg, input...)
_, err := v.writer.Write(msg)
if err != nil {
return 0, err
}
err = v.writer.Flush()
return len(input), err
}
// Reader decodes the content that comes over the wire and directs it to the proper destination.
func Reader(r *bufio.Reader, output, errput *os.File, input *io.PipeWriter, resize chan remotecommand.TerminalSize) error {
var saveb []byte
var eom int
for {
readb := make([]byte, 32*1024)
n, err := r.Read(readb)
// TODO, later may be worth checking in len of the read is 0
if err != nil {
return err
}
b := append(saveb, readb[0:n]...)
// no sense in reading less than the header len
for len(b) > 7 {
eom = int(binary.BigEndian.Uint32(b[4:8])) + 8
// The message and header are togther
if len(b) >= eom {
out := append([]byte{}, b[8:eom]...)
switch IntToSocketDest(int(b[0])) {
case ToStdout:
n, err := output.Write(out)
if err != nil {
return err
}
if n < len(out) {
return errors.New("short write error occurred on stdout")
}
case ToStderr:
n, err := errput.Write(out)
if err != nil {
return err
}
if n < len(out) {
return errors.New("short write error occurred on stderr")
}
case ToStdin:
n, err := input.Write(out)
if err != nil {
return err
}
if n < len(out) {
return errors.New("short write error occurred on stdin")
}
case TerminalResize:
// Resize events come over in bytes, need to be reserialized
resizeEvent := remotecommand.TerminalSize{}
if err := json.Unmarshal(out, &resizeEvent); err != nil {
return err
}
resize <- resizeEvent
case Quit:
return nil
}
b = b[eom:]
} else {
// We do not have the header and full message, need to slurp again
saveb = b
break
}
}
}
return nil
}