-
Notifications
You must be signed in to change notification settings - Fork 950
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
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.There was a problem hiding this comment.
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
?