Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: add cli attach command #2248

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Contributor

@fuweid fuweid Dec 4, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the current design, we have not supported the EscapeKeys yet in backend. Please check this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just want to know, why the backend need work to support EscapeKeys?

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