add support for detach keys on compose run|exec

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
This commit is contained in:
Nicolas De Loof 2021-06-29 10:16:30 +02:00
parent d20c3b0e22
commit b6552cd935
No known key found for this signature in database
GPG Key ID: 9858809D6F8F6E7E
4 changed files with 64 additions and 14 deletions

View File

@ -15,7 +15,6 @@ linters:
- gosimple - gosimple
- govet - govet
- ineffassign - ineffassign
- interfacer
- lll - lll
- misspell - misspell
- nakedret - nakedret

View File

@ -27,6 +27,7 @@ import (
"github.com/docker/compose-cli/pkg/api" "github.com/docker/compose-cli/pkg/api"
moby "github.com/docker/docker/api/types" moby "github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/stdcopy" "github.com/docker/docker/pkg/stdcopy"
"github.com/moby/term"
"github.com/docker/compose-cli/pkg/utils" "github.com/docker/compose-cli/pkg/utils"
) )
@ -77,23 +78,23 @@ func (s *composeService) attachContainer(ctx context.Context, container moby.Con
Line: line, Line: line,
}) })
}) })
_, err = s.attachContainerStreams(ctx, container.ID, service.Tty, nil, w) _, _, err = s.attachContainerStreams(ctx, container.ID, service.Tty, nil, w)
return err return err
} }
func (s *composeService) attachContainerStreams(ctx context.Context, container string, tty bool, r io.ReadCloser, w io.Writer) (func(), error) { func (s *composeService) attachContainerStreams(ctx context.Context, container string, tty bool, r io.ReadCloser, w io.Writer) (func(), chan bool, error) {
detached := make(chan bool)
var ( var (
in *streams.In in *streams.In
restore = func() { /* noop */ } restore = func() { /* noop */ }
) )
if r != nil { if r != nil {
in = streams.NewIn(r) in = streams.NewIn(r)
restore = in.RestoreTerminal
} }
stdin, stdout, err := s.getContainerStreams(ctx, container) stdin, stdout, err := s.getContainerStreams(ctx, container)
if err != nil { if err != nil {
return restore, err return restore, detached, err
} }
go func() { go func() {
@ -105,12 +106,20 @@ func (s *composeService) attachContainerStreams(ctx context.Context, container s
}() }()
if in != nil && stdin != nil { if in != nil && stdin != nil {
err := in.SetRawTerminal() if in.IsTerminal() {
if err != nil { state, err := term.SetRawTerminal(in.FD())
return restore, err if err != nil {
return restore, detached, err
}
restore = func() {
term.RestoreTerminal(in.FD(), state) //nolint:errcheck
}
} }
go func() { go func() {
io.Copy(stdin, in) //nolint:errcheck _, err := io.Copy(stdin, r)
if _, ok := err.(term.EscapeError); ok {
close(detached)
}
}() }()
} }
@ -123,7 +132,7 @@ func (s *composeService) attachContainerStreams(ctx context.Context, container s
} }
}() }()
} }
return restore, nil return restore, detached, nil
} }
func (s *composeService) getContainerStreams(ctx context.Context, container string) (io.WriteCloser, io.ReadCloser, error) { func (s *composeService) getContainerStreams(ctx context.Context, container string) (io.WriteCloser, io.ReadCloser, error) {

View File

@ -22,9 +22,11 @@ import (
"io" "io"
"github.com/compose-spec/compose-go/types" "github.com/compose-spec/compose-go/types"
"github.com/docker/cli/cli/streams"
moby "github.com/docker/docker/api/types" moby "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
"github.com/docker/docker/pkg/stdcopy" "github.com/docker/docker/pkg/stdcopy"
"github.com/moby/term"
"github.com/docker/compose-cli/pkg/api" "github.com/docker/compose-cli/pkg/api"
) )
@ -92,18 +94,34 @@ func (s *composeService) interactiveExec(ctx context.Context, opts api.RunOption
outputDone := make(chan error) outputDone := make(chan error)
inputDone := make(chan error) inputDone := make(chan error)
stdout := ContainerStdout{HijackedResponse: resp}
stdin := ContainerStdin{HijackedResponse: resp}
r, err := s.getEscapeKeyProxy(opts.Reader)
if err != nil {
return err
}
in := streams.NewIn(opts.Reader)
if in.IsTerminal() {
state, err := term.SetRawTerminal(in.FD())
if err != nil {
return err
}
defer term.RestoreTerminal(in.FD(), state) //nolint:errcheck
}
go func() { go func() {
if opts.Tty { if opts.Tty {
_, err := io.Copy(opts.Writer, resp.Reader) _, err := io.Copy(opts.Writer, stdout)
outputDone <- err outputDone <- err
} else { } else {
_, err := stdcopy.StdCopy(opts.Writer, opts.Writer, resp.Reader) _, err := stdcopy.StdCopy(opts.Writer, opts.Writer, stdout)
outputDone <- err outputDone <- err
} }
}() }()
go func() { go func() {
_, err := io.Copy(resp.Conn, opts.Reader) _, err := io.Copy(stdin, r)
inputDone <- err inputDone <- err
}() }()
@ -112,6 +130,9 @@ func (s *composeService) interactiveExec(ctx context.Context, opts api.RunOption
case err := <-outputDone: case err := <-outputDone:
return err return err
case err := <-inputDone: case err := <-inputDone:
if _, ok := err.(term.EscapeError); ok {
return nil
}
if err != nil { if err != nil {
return err return err
} }

View File

@ -19,13 +19,16 @@ package compose
import ( import (
"context" "context"
"fmt" "fmt"
"io"
"github.com/docker/compose-cli/pkg/api" "github.com/docker/compose-cli/pkg/api"
"github.com/compose-spec/compose-go/types" "github.com/compose-spec/compose-go/types"
moby "github.com/docker/docker/api/types" moby "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/pkg/ioutils"
"github.com/docker/docker/pkg/stringid" "github.com/docker/docker/pkg/stringid"
"github.com/moby/term"
) )
func (s *composeService) RunOneOffContainer(ctx context.Context, project *types.Project, opts api.RunOptions) (int, error) { func (s *composeService) RunOneOffContainer(ctx context.Context, project *types.Project, opts api.RunOptions) (int, error) {
@ -75,7 +78,11 @@ func (s *composeService) RunOneOffContainer(ctx context.Context, project *types.
return 0, nil return 0, nil
} }
restore, err := s.attachContainerStreams(ctx, containerID, service.Tty, opts.Reader, opts.Writer) r, err := s.getEscapeKeyProxy(opts.Reader)
if err != nil {
return 0, err
}
restore, detachC, err := s.attachContainerStreams(ctx, containerID, service.Tty, r, opts.Writer)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@ -93,12 +100,26 @@ func (s *composeService) RunOneOffContainer(ctx context.Context, project *types.
select { select {
case status := <-statusC: case status := <-statusC:
return int(status.StatusCode), nil return int(status.StatusCode), nil
case <-detachC:
return 0, nil
case err := <-errC: case err := <-errC:
return 0, err return 0, err
} }
} }
func (s *composeService) getEscapeKeyProxy(r io.ReadCloser) (io.ReadCloser, error) {
var escapeKeys = []byte{16, 17}
if s.configFile.DetachKeys != "" {
customEscapeKeys, err := term.ToBytes(s.configFile.DetachKeys)
if err != nil {
return nil, err
}
escapeKeys = customEscapeKeys
}
return ioutils.NewReadCloserWrapper(term.NewEscapeProxy(r, escapeKeys), r.Close), nil
}
func applyRunOptions(project *types.Project, service *types.ServiceConfig, opts api.RunOptions) { func applyRunOptions(project *types.Project, service *types.ServiceConfig, opts api.RunOptions) {
service.Tty = opts.Tty service.Tty = opts.Tty
service.ContainerName = opts.Name service.ContainerName = opts.Name