Skip to content

Commit

Permalink
feature: add cli attach command
Browse files Browse the repository at this point in the history
Signed-off-by: zhangyue <zy675793960@yeah.net>
  • Loading branch information
zhangyue committed Dec 4, 2018
1 parent 69278d9 commit e82e57e
Show file tree
Hide file tree
Showing 14 changed files with 905 additions and 1 deletion.
186 changes: 186 additions & 0 deletions cli/attach.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package main

import (
"context"
"fmt"
"io"
"os"

"github.com/alibaba/pouch/apis/types"
"github.com/alibaba/pouch/client"
"github.com/alibaba/pouch/pkg/ioutils"

"github.com/docker/docker/pkg/term"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)

// AttachDescription is used to describe attach command in detail and auto generate command doc.
var AttachDescription = "Attach local standard input, output, and error streams to a running container"

var defaultEscapeKeys = []byte{16, 17}

// AttachCommand is used to implement 'attach' command.
type AttachCommand struct {
baseCommand

// flags for attach command
noStdin bool
detachKeys string
}

// Init initialize "attach" command.
func (ac *AttachCommand) Init(c *Cli) {
ac.cli = c
ac.cmd = &cobra.Command{
Use: "attach [OPTIONS] CONTAINER",
Short: "Attach local standard input, output, and error streams to a running container",
Long: AttachDescription,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return ac.runAttach(args)
},
Example: ac.example(),
}
ac.addFlags()
}

// addFlags adds flags for specific command.
func (ac *AttachCommand) addFlags() {
flagSet := ac.cmd.Flags()
flagSet.BoolVar(&ac.noStdin, "no-stdin", false, "Do not attach STDIN")
flagSet.StringVar(&ac.detachKeys, "detach-keys", "", "Override the key sequence for detaching a container")
// TODO: sig-proxy will be supported in the future.
//flagSet.BoolVar(&ac.sigProxy, "sig-proxy", true, "Proxy all received signals to the process")
}

func inspectAndCheckState(ctx context.Context, cli client.CommonAPIClient, name string) (*types.ContainerJSON, error) {
c, err := cli.ContainerGet(ctx, name)
if err != nil {
return nil, err
}
if !c.State.Running {
return nil, errors.New("You cannot attach to a stopped container, start it first")
}
if c.State.Paused {
return nil, errors.New("You cannot attach to a paused container, unpause it first")
}
if c.State.Restarting {
return nil, errors.New("You cannot attach to a restarting container, wait until it is running")
}

return c, nil
}

// runAttach is used to attach a container.
func (ac *AttachCommand) runAttach(args []string) error {
name := args[0]

ctx := context.Background()
apiClient := ac.cli.Client()

c, err := inspectAndCheckState(ctx, apiClient, name)
if err != nil {
return err
}

if err := checkTty(!ac.noStdin, c.Config.Tty, os.Stdin.Fd()); err != nil {
return err
}

var inReader io.Reader = os.Stdin
if !ac.noStdin && c.Config.Tty {
in, out, err := setRawMode(!ac.noStdin, false)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to set raw mode %s", err)
return fmt.Errorf("failed to set raw mode")
}
defer func() {
if err := restoreMode(in, out); err != nil {
fmt.Fprintf(os.Stderr, "failed to restore term mode %s", err)
}
}()

escapeKeys := defaultEscapeKeys
// Wrap the input to detect detach escape sequence.
// Use default escape keys if an invalid sequence is given.
if ac.detachKeys != "" {
customEscapeKeys, err := term.ToBytes(ac.detachKeys)
if err != nil {
return fmt.Errorf("invalid detach keys (%s) provided", ac.detachKeys)
}
escapeKeys = customEscapeKeys
}
inReader = ioutils.NewReadCloserWrapper(term.NewEscapeProxy(os.Stdin, escapeKeys), os.Stdin.Close)
}

conn, br, err := apiClient.ContainerAttach(ctx, name, !ac.noStdin)
if err != nil {
return fmt.Errorf("failed to attach container: %v", err)
}
defer conn.Close()

outputDone := make(chan error, 1)
go func() {
var err error
_, err = io.Copy(os.Stdout, br)
if err != nil {
logrus.Debugf("Error receive stdout: %s", err)
}
outputDone <- err
}()

inputDone := make(chan struct{})
detached := make(chan error, 1)
go func() {
if !ac.noStdin {
_, err := io.Copy(conn, inReader)
// close write if receive CTRL-D
if cw, ok := conn.(ioutils.CloseWriter); ok {
cw.CloseWrite()
}
if _, ok := err.(term.EscapeError); ok {
detached <- err
}
if err != nil {
logrus.Debugf("Error send stdin: %s", err)
}
}
close(inputDone)

}()

select {
case err := <-outputDone:
if err != nil {
logrus.Debugf("receive stdout error: %s", err)
return err
}
case <-inputDone:
select {
// Wait for output to complete streaming.
case err := <-outputDone:
logrus.Debugf("receive stdout error: %s", err)
return err
case <-ctx.Done():
}
case err := <-detached:
// Got a detach key sequence.
return err
case <-ctx.Done():
}

return nil
}

// example shows examples in attach command, and is used in auto-generated cli docs.
func (ac *AttachCommand) example() string {
return `$ pouch run -d --name foo busybox sh -c 'while true; do sleep 1; echo hello; done'
Name ID Status Image Runtime
foo 71b9c1 Running docker.io/library/busybox:latest runc
$ pouch attach foo
hello
hello
hello`
}
1 change: 1 addition & 0 deletions cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ func main() {
cli.AddCommand(base, &PullCommand{})
cli.AddCommand(base, &CreateCommand{})
cli.AddCommand(base, &StartCommand{})
cli.AddCommand(base, &AttachCommand{})
cli.AddCommand(base, &StopCommand{})
cli.AddCommand(base, &PsCommand{})
cli.AddCommand(base, &RmCommand{})
Expand Down
25 changes: 25 additions & 0 deletions pkg/ioutils/readers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package ioutils

import "io"

// ReadCloserWrapper wraps an io.Reader, and implements an io.ReadCloser
// It calls the given callback function when closed. It should be constructed
// with NewReadCloserWrapper
type ReadCloserWrapper struct {
io.Reader
closer func() error
}

// NewReadCloserWrapper returns a new io.ReadCloser.
func NewReadCloserWrapper(r io.Reader, closer func() error) io.ReadCloser {
return &ReadCloserWrapper{
Reader: r,
closer: closer,
}

}

// Close calls back the passed closer function
func (r *ReadCloserWrapper) Close() error {
return r.closer()
}
76 changes: 76 additions & 0 deletions test/cli_attach_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package main

import (
"io"
"os/exec"
"strings"

"github.com/alibaba/pouch/test/command"
"github.com/alibaba/pouch/test/environment"

"github.com/go-check/check"
"github.com/gotestyourself/gotestyourself/icmd"
"github.com/stretchr/testify/assert"
)

// PouchCreateSuite is the test suite for attach CLI.
type PouchAttachSuite struct{}

func init() {
check.Suite(&PouchAttachSuite{})
}

// SetUpSuite does common setup in the beginning of each test suite.
func (suite *PouchAttachSuite) SetUpSuite(c *check.C) {
SkipIfFalse(c, environment.IsLinux)

environment.PruneAllContainers(apiClient)

PullImage(c, busyboxImage)
}

// TearDownTest does cleanup work in the end of each test.
func (suite *PouchAttachSuite) TearDownTest(c *check.C) {
}

// TestPouchAttachRunningContainer is to verify the correctness of attach a running container.
func (suite *PouchAttachSuite) TestPouchAttachRunningContainer(c *check.C) {
name := "TestPouchAttachRunningContainer"

res := command.PouchRun("run", "-d", "--name", name, busyboxImage, "/bin/sh", "-c", "while true; do echo hello; done")

defer DelContainerForceMultyTime(c, name)
res.Assert(c, icmd.Success)

cmd := exec.Command(environment.PouchBinary, "attach", name)

out, err := cmd.StdoutPipe()
if err != nil {
c.Fatal(err)
}
defer out.Close()

if err := cmd.Start(); err != nil {
c.Fatal(err)
}

buf := make([]byte, 1024)

if _, err := out.Read(buf); err != nil && err != io.EOF {
c.Fatal(err)
}

if !strings.Contains(string(buf), "hello") {
c.Fatalf("unexpected output %s expected hello\n", string(buf))
}
}

// TestAttachWithTty tests running container with -tty flag and attach stdin in a non-tty client.
func (suite *PouchAttachSuite) TestAttachWithTty(c *check.C) {
name := "TestAttachWithTty"
command.PouchRun("run", "-d", "-t", "--name", name, busyboxImage, "sleep", "100000").Assert(c, icmd.Success)
defer DelContainerForceMultyTime(c, name)
attachRes := command.PouchRun("attach", name)
errString := attachRes.Stderr()
assert.Equal(c, errString, "Error: the input device is not a TTY\n")
}
2 changes: 1 addition & 1 deletion vendor/github.com/docker/docker/LICENSE

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

66 changes: 66 additions & 0 deletions vendor/github.com/docker/docker/pkg/term/ascii.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit e82e57e

Please sign in to comment.