Merge pull request #6411 from mheon/exec_bindings

Add bindings for exec and enable attached remote exec
This commit is contained in:
OpenShift Merge Robot
2020-06-02 06:08:21 -04:00
committed by GitHub
8 changed files with 544 additions and 298 deletions

View File

@ -67,14 +67,14 @@ func execFlags(flags *pflag.FlagSet) {
func init() { func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{ registry.Commands = append(registry.Commands, registry.CliCommand{
Mode: []entities.EngineMode{entities.ABIMode}, Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode},
Command: execCommand, Command: execCommand,
}) })
flags := execCommand.Flags() flags := execCommand.Flags()
execFlags(flags) execFlags(flags)
registry.Commands = append(registry.Commands, registry.CliCommand{ registry.Commands = append(registry.Commands, registry.CliCommand{
Mode: []entities.EngineMode{entities.ABIMode}, Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode},
Command: containerExecCommand, Command: containerExecCommand,
Parent: containerCmd, Parent: containerCmd,
}) })

View File

@ -8,6 +8,7 @@ import (
"github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod"
"github.com/containers/libpod/libpod/define" "github.com/containers/libpod/libpod/define"
"github.com/containers/libpod/pkg/api/handlers/utils" "github.com/containers/libpod/pkg/api/handlers/utils"
"github.com/gorilla/mux"
"github.com/gorilla/schema" "github.com/gorilla/schema"
"github.com/pkg/errors" "github.com/pkg/errors"
"k8s.io/client-go/tools/remotecommand" "k8s.io/client-go/tools/remotecommand"
@ -37,9 +38,9 @@ func ResizeTTY(w http.ResponseWriter, r *http.Request) {
} }
var status int var status int
name := utils.GetName(r)
switch { switch {
case strings.Contains(r.URL.Path, "/containers/"): case strings.Contains(r.URL.Path, "/containers/"):
name := utils.GetName(r)
ctnr, err := runtime.LookupContainer(name) ctnr, err := runtime.LookupContainer(name)
if err != nil { if err != nil {
utils.ContainerNotFound(w, name, err) utils.ContainerNotFound(w, name, err)
@ -61,6 +62,7 @@ func ResizeTTY(w http.ResponseWriter, r *http.Request) {
// reasons. // reasons.
status = http.StatusOK status = http.StatusOK
case strings.Contains(r.URL.Path, "/exec/"): case strings.Contains(r.URL.Path, "/exec/"):
name := mux.Vars(r)["id"]
ctnr, err := runtime.GetExecSessionContainer(name) ctnr, err := runtime.GetExecSessionContainer(name)
if err != nil { if err != nil {
utils.SessionNotFound(w, name, err) utils.SessionNotFound(w, name, err)

View File

@ -310,7 +310,7 @@ func (s *APIServer) registerExecHandlers(r *mux.Router) error {
// $ref: "#/responses/NoSuchExecInstance" // $ref: "#/responses/NoSuchExecInstance"
// 500: // 500:
// $ref: "#/responses/InternalError" // $ref: "#/responses/InternalError"
r.Handle(VersionedPath("/libpod/exec/{id}/resize"), s.APIHandler(compat.UnsupportedHandler)).Methods(http.MethodPost) r.Handle(VersionedPath("/libpod/exec/{id}/resize"), s.APIHandler(compat.ResizeTTY)).Methods(http.MethodPost)
// swagger:operation GET /libpod/exec/{id}/json libpod libpodInspectExec // swagger:operation GET /libpod/exec/{id}/json libpod libpodInspectExec
// --- // ---
// tags: // tags:

View File

@ -24,7 +24,7 @@ import (
) )
var ( var (
basePath = &url.URL{ BasePath = &url.URL{
Scheme: "http", Scheme: "http",
Host: "d", Host: "d",
Path: "/v" + APIVersion.String() + "/libpod", Path: "/v" + APIVersion.String() + "/libpod",
@ -37,15 +37,14 @@ type APIResponse struct {
} }
type Connection struct { type Connection struct {
_url *url.URL Uri *url.URL
client *http.Client Client *http.Client
conn *net.Conn
} }
type valueKey string type valueKey string
const ( const (
clientKey = valueKey("client") clientKey = valueKey("Client")
) )
// GetClient from context build by NewConnection() // GetClient from context build by NewConnection()
@ -59,7 +58,7 @@ func GetClient(ctx context.Context) (*Connection, error) {
// JoinURL elements with '/' // JoinURL elements with '/'
func JoinURL(elements ...string) string { func JoinURL(elements ...string) string {
return strings.Join(elements, "/") return "/" + strings.Join(elements, "/")
} }
// NewConnection takes a URI as a string and returns a context with the // NewConnection takes a URI as a string and returns a context with the
@ -88,7 +87,7 @@ func NewConnection(ctx context.Context, uri string, identity ...string) (context
return nil, errors.Wrapf(err, "Value of PODMAN_HOST is not a valid url: %s", uri) return nil, errors.Wrapf(err, "Value of PODMAN_HOST is not a valid url: %s", uri)
} }
// Now we setup the http client to use the connection above // Now we setup the http Client to use the connection above
var connection Connection var connection Connection
switch _url.Scheme { switch _url.Scheme {
case "ssh": case "ssh":
@ -125,16 +124,12 @@ func NewConnection(ctx context.Context, uri string, identity ...string) (context
func tcpClient(_url *url.URL) (Connection, error) { func tcpClient(_url *url.URL) (Connection, error) {
connection := Connection{ connection := Connection{
_url: _url, Uri: _url,
} }
connection.client = &http.Client{ connection.Client = &http.Client{
Transport: &http.Transport{ Transport: &http.Transport{
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
conn, err := net.Dial("tcp", _url.Host) return net.Dial("tcp", _url.Host)
if c, ok := ctx.Value(clientKey).(*Connection); ok {
c.conn = &conn
}
return conn, err
}, },
DisableCompression: true, DisableCompression: true,
}, },
@ -167,11 +162,11 @@ func pingNewConnection(ctx context.Context) error {
} }
switch APIVersion.Compare(versionSrv) { switch APIVersion.Compare(versionSrv) {
case 1, 0: case -1, 0:
// Server's job when client version is equal or older // Server's job when Client version is equal or older
return nil return nil
case -1: case 1:
return errors.Errorf("server API version is too old. client %q server %q", APIVersion.String(), versionSrv.String()) return errors.Errorf("server API version is too old. Client %q server %q", APIVersion.String(), versionSrv.String())
} }
} }
return errors.Errorf("ping response was %q", response.StatusCode) return errors.Errorf("ping response was %q", response.StatusCode)
@ -217,31 +212,22 @@ func sshClient(_url *url.URL, identity string, secure bool) (Connection, error)
return Connection{}, errors.Wrapf(err, "Connection to bastion host (%s) failed.", _url.String()) return Connection{}, errors.Wrapf(err, "Connection to bastion host (%s) failed.", _url.String())
} }
connection := Connection{_url: _url} connection := Connection{Uri: _url}
connection.client = &http.Client{ connection.Client = &http.Client{
Transport: &http.Transport{ Transport: &http.Transport{
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
conn, err := bastion.Dial("unix", _url.Path) return bastion.Dial("unix", _url.Path)
if c, ok := ctx.Value(clientKey).(*Connection); ok {
c.conn = &conn
}
return conn, err
}, },
}} }}
return connection, nil return connection, nil
} }
func unixClient(_url *url.URL) (Connection, error) { func unixClient(_url *url.URL) (Connection, error) {
connection := Connection{_url: _url} connection := Connection{Uri: _url}
connection.client = &http.Client{ connection.Client = &http.Client{
Transport: &http.Transport{ Transport: &http.Transport{
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
d := net.Dialer{} return (&net.Dialer{}).DialContext(ctx, "unix", _url.Path)
conn, err := d.DialContext(ctx, "unix", _url.Path)
if c, ok := ctx.Value(clientKey).(*Connection); ok {
c.conn = &conn
}
return conn, err
}, },
DisableCompression: true, DisableCompression: true,
}, },
@ -263,7 +249,7 @@ func (c *Connection) DoRequest(httpBody io.Reader, httpMethod, endpoint string,
// Lets eventually use URL for this which might lead to safer // Lets eventually use URL for this which might lead to safer
// usage // usage
safeEndpoint := fmt.Sprintf(endpoint, safePathValues...) safeEndpoint := fmt.Sprintf(endpoint, safePathValues...)
e := basePath.String() + safeEndpoint e := BasePath.String() + safeEndpoint
req, err := http.NewRequest(httpMethod, e, httpBody) req, err := http.NewRequest(httpMethod, e, httpBody)
if err != nil { if err != nil {
return nil, err return nil, err
@ -277,7 +263,7 @@ func (c *Connection) DoRequest(httpBody io.Reader, httpMethod, endpoint string,
req = req.WithContext(context.WithValue(context.Background(), clientKey, c)) req = req.WithContext(context.WithValue(context.Background(), clientKey, c))
// Give the Do three chances in the case of a comm/service hiccup // Give the Do three chances in the case of a comm/service hiccup
for i := 0; i < 3; i++ { for i := 0; i < 3; i++ {
response, err = c.client.Do(req) // nolint response, err = c.Client.Do(req) // nolint
if err == nil { if err == nil {
break break
} }
@ -286,10 +272,6 @@ func (c *Connection) DoRequest(httpBody io.Reader, httpMethod, endpoint string,
return &APIResponse{response, req}, err return &APIResponse{response, req}, err
} }
func (c *Connection) Write(b []byte) (int, error) {
return (*c.conn).Write(b)
}
// FiltersToString converts our typical filter format of a // FiltersToString converts our typical filter format of a
// map[string][]string to a query/html safe string. // map[string][]string to a query/html safe string.
func FiltersToString(filters map[string][]string) (string, error) { func FiltersToString(filters map[string][]string) (string, error) {

View File

@ -0,0 +1,481 @@
package containers
import (
"bytes"
"context"
"encoding/binary"
"fmt"
"io"
"net"
"net/http"
"net/url"
"os"
"os/signal"
"reflect"
"strconv"
"time"
"github.com/containers/libpod/libpod/define"
"github.com/containers/libpod/pkg/bindings"
sig "github.com/containers/libpod/pkg/signal"
"github.com/containers/libpod/utils"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh/terminal"
)
// Attach attaches to a running container
func Attach(ctx context.Context, nameOrId string, detachKeys *string, logs, stream *bool, stdin io.Reader, stdout io.Writer, stderr io.Writer, attachReady chan bool) error {
isSet := struct {
stdin bool
stdout bool
stderr bool
}{
stdin: !(stdin == nil || reflect.ValueOf(stdin).IsNil()),
stdout: !(stdout == nil || reflect.ValueOf(stdout).IsNil()),
stderr: !(stderr == nil || reflect.ValueOf(stderr).IsNil()),
}
// Ensure golang can determine that interfaces are "really" nil
if !isSet.stdin {
stdin = (io.Reader)(nil)
}
if !isSet.stdout {
stdout = (io.Writer)(nil)
}
if !isSet.stderr {
stderr = (io.Writer)(nil)
}
conn, err := bindings.GetClient(ctx)
if err != nil {
return err
}
// Do we need to wire in stdin?
ctnr, err := Inspect(ctx, nameOrId, bindings.PFalse)
if err != nil {
return err
}
params := url.Values{}
if detachKeys != nil {
params.Add("detachKeys", *detachKeys)
}
if logs != nil {
params.Add("logs", fmt.Sprintf("%t", *logs))
}
if stream != nil {
params.Add("stream", fmt.Sprintf("%t", *stream))
}
if isSet.stdin {
params.Add("stdin", "true")
}
if isSet.stdout {
params.Add("stdout", "true")
}
if isSet.stderr {
params.Add("stderr", "true")
}
// Unless all requirements are met, don't use "stdin" is a terminal
file, ok := stdin.(*os.File)
needTTY := ok && terminal.IsTerminal(int(file.Fd())) && ctnr.Config.Tty
if needTTY {
state, err := setRawTerminal(file)
if err != nil {
return err
}
defer func() {
if err := terminal.Restore(int(file.Fd()), state); err != nil {
logrus.Errorf("unable to restore terminal: %q", err)
}
logrus.SetFormatter(&logrus.TextFormatter{})
}()
}
headers := make(map[string]string)
headers["Connection"] = "Upgrade"
headers["Upgrade"] = "tcp"
var socket net.Conn
socketSet := false
dialContext := conn.Client.Transport.(*http.Transport).DialContext
t := &http.Transport{
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
c, err := dialContext(ctx, network, address)
if err != nil {
return nil, err
}
if !socketSet {
socket = c
socketSet = true
}
return c, err
},
IdleConnTimeout: time.Duration(0),
}
conn.Client.Transport = t
response, err := conn.DoRequest(nil, http.MethodPost, "/containers/%s/attach", params, headers, nameOrId)
if err != nil {
return err
}
if !(response.IsSuccess() || response.IsInformational()) {
return response.Process(nil)
}
if needTTY {
winChange := make(chan os.Signal, 1)
signal.Notify(winChange, sig.SIGWINCH)
winCtx, winCancel := context.WithCancel(ctx)
defer winCancel()
go attachHandleResize(ctx, winCtx, winChange, false, nameOrId, file)
}
// If we are attaching around a start, we need to "signal"
// back that we are in fact attached so that started does
// not execute before we can attach.
if attachReady != nil {
attachReady <- true
}
if isSet.stdin {
go func() {
logrus.Debugf("Copying STDIN to socket")
_, err := utils.CopyDetachable(socket, stdin, []byte{})
if err != nil {
logrus.Error("failed to write input to service: " + err.Error())
}
}()
}
buffer := make([]byte, 1024)
if ctnr.Config.Tty {
logrus.Debugf("Copying STDOUT of container in terminal mode")
if !isSet.stdout {
return fmt.Errorf("container %q requires stdout to be set", ctnr.ID)
}
// If not multiplex'ed, read from server and write to stdout
_, err := io.Copy(stdout, socket)
if err != nil {
return err
}
} else {
logrus.Debugf("Copying standard streams of container in non-terminal mode")
for {
// Read multiplexed channels and write to appropriate stream
fd, l, err := DemuxHeader(socket, buffer)
if err != nil {
if errors.Is(err, io.EOF) {
return nil
}
return err
}
frame, err := DemuxFrame(socket, buffer, l)
if err != nil {
return err
}
switch {
case fd == 0 && isSet.stdout:
_, err := stdout.Write(frame[0:l])
if err != nil {
return err
}
case fd == 1 && isSet.stdout:
_, err := stdout.Write(frame[0:l])
if err != nil {
return err
}
case fd == 2 && isSet.stderr:
_, err := stderr.Write(frame[0:l])
if err != nil {
return err
}
case fd == 3:
return fmt.Errorf("error from service from stream: %s", frame)
default:
return fmt.Errorf("unrecognized channel in header: %d, 0-3 supported", fd)
}
}
}
return nil
}
// DemuxHeader reads header for stream from server multiplexed stdin/stdout/stderr/2nd error channel
func DemuxHeader(r io.Reader, buffer []byte) (fd, sz int, err error) {
n, err := io.ReadFull(r, buffer[0:8])
if err != nil {
return
}
if n < 8 {
err = io.ErrUnexpectedEOF
return
}
fd = int(buffer[0])
if fd < 0 || fd > 3 {
err = errors.Wrapf(ErrLostSync, fmt.Sprintf(`channel "%d" found, 0-3 supported`, fd))
return
}
sz = int(binary.BigEndian.Uint32(buffer[4:8]))
return
}
// DemuxFrame reads contents for frame from server multiplexed stdin/stdout/stderr/2nd error channel
func DemuxFrame(r io.Reader, buffer []byte, length int) (frame []byte, err error) {
if len(buffer) < length {
buffer = append(buffer, make([]byte, length-len(buffer)+1)...)
}
n, err := io.ReadFull(r, buffer[0:length])
if err != nil {
return nil, nil
}
if n < length {
err = io.ErrUnexpectedEOF
return
}
return buffer[0:length], nil
}
// ResizeContainerTTY sets container's TTY height and width in characters
func ResizeContainerTTY(ctx context.Context, nameOrId string, height *int, width *int) error {
return resizeTTY(ctx, bindings.JoinURL("containers", nameOrId, "resize"), height, width)
}
// ResizeExecTTY sets session's TTY height and width in characters
func ResizeExecTTY(ctx context.Context, nameOrId string, height *int, width *int) error {
return resizeTTY(ctx, bindings.JoinURL("exec", nameOrId, "resize"), height, width)
}
// resizeTTY set size of TTY of container
func resizeTTY(ctx context.Context, endpoint string, height *int, width *int) error {
conn, err := bindings.GetClient(ctx)
if err != nil {
return err
}
params := url.Values{}
if height != nil {
params.Set("h", strconv.Itoa(*height))
}
if width != nil {
params.Set("w", strconv.Itoa(*width))
}
rsp, err := conn.DoRequest(nil, http.MethodPost, endpoint, params, nil)
if err != nil {
return err
}
return rsp.Process(nil)
}
type rawFormatter struct {
logrus.TextFormatter
}
func (f *rawFormatter) Format(entry *logrus.Entry) ([]byte, error) {
buffer, err := f.TextFormatter.Format(entry)
if err != nil {
return buffer, err
}
return append(buffer, '\r'), nil
}
// This is intended to be run as a goroutine, handling resizing for a container
// or exec session.
func attachHandleResize(ctx, winCtx context.Context, winChange chan os.Signal, isExec bool, id string, file *os.File) {
// Prime the pump, we need one reset to ensure everything is ready
winChange <- sig.SIGWINCH
for {
select {
case <-winCtx.Done():
return
case <-winChange:
h, w, err := terminal.GetSize(int(file.Fd()))
if err != nil {
logrus.Warnf("failed to obtain TTY size: " + err.Error())
}
var resizeErr error
if isExec {
resizeErr = ResizeExecTTY(ctx, id, &h, &w)
} else {
resizeErr = ResizeContainerTTY(ctx, id, &h, &w)
}
if resizeErr != nil {
logrus.Warnf("failed to resize TTY: " + resizeErr.Error())
}
}
}
}
// Configure the given terminal for raw mode
func setRawTerminal(file *os.File) (*terminal.State, error) {
state, err := terminal.MakeRaw(int(file.Fd()))
if err != nil {
return nil, err
}
logrus.SetFormatter(&rawFormatter{})
return state, err
}
// ExecStartAndAttach starts and attaches to a given exec session.
func ExecStartAndAttach(ctx context.Context, sessionID string, streams *define.AttachStreams) error {
conn, err := bindings.GetClient(ctx)
if err != nil {
return err
}
// TODO: Make this configurable (can't use streams' InputStream as it's
// buffered)
terminalFile := os.Stdin
logrus.Debugf("Starting & Attaching to exec session ID %q", sessionID)
// We need to inspect the exec session first to determine whether to use
// -t.
resp, err := conn.DoRequest(nil, http.MethodGet, "/exec/%s/json", nil, nil, sessionID)
if err != nil {
return err
}
respStruct := new(define.InspectExecSession)
if err := resp.Process(respStruct); err != nil {
return err
}
isTerm := true
if respStruct.ProcessConfig != nil {
isTerm = respStruct.ProcessConfig.Tty
}
// If we are in TTY mode, we need to set raw mode for the terminal.
// TODO: Share all of this with Attach() for containers.
needTTY := terminalFile != nil && terminal.IsTerminal(int(terminalFile.Fd())) && isTerm
if needTTY {
state, err := setRawTerminal(terminalFile)
if err != nil {
return err
}
defer func() {
if err := terminal.Restore(int(terminalFile.Fd()), state); err != nil {
logrus.Errorf("unable to restore terminal: %q", err)
}
logrus.SetFormatter(&logrus.TextFormatter{})
}()
}
body := struct {
Detach bool `json:"Detach"`
}{
Detach: false,
}
bodyJSON, err := json.Marshal(body)
if err != nil {
return err
}
var socket net.Conn
socketSet := false
dialContext := conn.Client.Transport.(*http.Transport).DialContext
t := &http.Transport{
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
c, err := dialContext(ctx, network, address)
if err != nil {
return nil, err
}
if !socketSet {
socket = c
socketSet = true
}
return c, err
},
IdleConnTimeout: time.Duration(0),
}
conn.Client.Transport = t
response, err := conn.DoRequest(bytes.NewReader(bodyJSON), http.MethodPost, "/exec/%s/start", nil, nil, sessionID)
if err != nil {
return err
}
if !(response.IsSuccess() || response.IsInformational()) {
return response.Process(nil)
}
if needTTY {
winChange := make(chan os.Signal, 1)
signal.Notify(winChange, sig.SIGWINCH)
winCtx, winCancel := context.WithCancel(ctx)
defer winCancel()
go attachHandleResize(ctx, winCtx, winChange, true, sessionID, terminalFile)
}
if streams.AttachInput {
go func() {
logrus.Debugf("Copying STDIN to socket")
_, err := utils.CopyDetachable(socket, streams.InputStream, []byte{})
if err != nil {
logrus.Error("failed to write input to service: " + err.Error())
}
}()
}
buffer := make([]byte, 1024)
if isTerm {
logrus.Debugf("Handling terminal attach to exec")
if !streams.AttachOutput {
return fmt.Errorf("exec session %s has a terminal and must have STDOUT enabled", sessionID)
}
// If not multiplex'ed, read from server and write to stdout
_, err := utils.CopyDetachable(streams.OutputStream, socket, []byte{})
if err != nil {
return err
}
} else {
logrus.Debugf("Handling non-terminal attach to exec")
for {
// Read multiplexed channels and write to appropriate stream
fd, l, err := DemuxHeader(socket, buffer)
if err != nil {
if errors.Is(err, io.EOF) {
return nil
}
return err
}
frame, err := DemuxFrame(socket, buffer, l)
if err != nil {
return err
}
switch {
case fd == 0 && streams.AttachOutput:
_, err := streams.OutputStream.Write(frame[0:l])
if err != nil {
return err
}
case fd == 1 && streams.AttachInput:
// Write STDIN to STDOUT (echoing characters
// typed by another attach session)
_, err := streams.OutputStream.Write(frame[0:l])
if err != nil {
return err
}
case fd == 2 && streams.AttachError:
_, err := streams.ErrorStream.Write(frame[0:l])
if err != nil {
return err
}
case fd == 3:
return fmt.Errorf("error from service from stream: %s", frame)
default:
return fmt.Errorf("unrecognized channel in header: %d, 0-3 supported", fd)
}
}
}
return nil
}

View File

@ -2,14 +2,9 @@ package containers
import ( import (
"context" "context"
"encoding/binary"
"fmt"
"io" "io"
"net/http" "net/http"
"net/url" "net/url"
"os"
"os/signal"
"reflect"
"strconv" "strconv"
"strings" "strings"
@ -17,10 +12,7 @@ import (
"github.com/containers/libpod/pkg/api/handlers" "github.com/containers/libpod/pkg/api/handlers"
"github.com/containers/libpod/pkg/bindings" "github.com/containers/libpod/pkg/bindings"
"github.com/containers/libpod/pkg/domain/entities" "github.com/containers/libpod/pkg/domain/entities"
sig "github.com/containers/libpod/pkg/signal"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh/terminal"
) )
var ( var (
@ -345,248 +337,3 @@ func ContainerInit(ctx context.Context, nameOrID string) error {
} }
return response.Process(nil) return response.Process(nil)
} }
// Attach attaches to a running container
func Attach(ctx context.Context, nameOrId string, detachKeys *string, logs, stream *bool, stdin io.Reader, stdout io.Writer, stderr io.Writer, attachReady chan bool) error {
isSet := struct {
stdin bool
stdout bool
stderr bool
}{
stdin: !(stdin == nil || reflect.ValueOf(stdin).IsNil()),
stdout: !(stdout == nil || reflect.ValueOf(stdout).IsNil()),
stderr: !(stderr == nil || reflect.ValueOf(stderr).IsNil()),
}
// Ensure golang can determine that interfaces are "really" nil
if !isSet.stdin {
stdin = (io.Reader)(nil)
}
if !isSet.stdout {
stdout = (io.Writer)(nil)
}
if !isSet.stderr {
stderr = (io.Writer)(nil)
}
conn, err := bindings.GetClient(ctx)
if err != nil {
return err
}
// Do we need to wire in stdin?
ctnr, err := Inspect(ctx, nameOrId, bindings.PFalse)
if err != nil {
return err
}
params := url.Values{}
if detachKeys != nil {
params.Add("detachKeys", *detachKeys)
}
if logs != nil {
params.Add("logs", fmt.Sprintf("%t", *logs))
}
if stream != nil {
params.Add("stream", fmt.Sprintf("%t", *stream))
}
if isSet.stdin {
params.Add("stdin", "true")
}
if isSet.stdout {
params.Add("stdout", "true")
}
if isSet.stderr {
params.Add("stderr", "true")
}
// Unless all requirements are met, don't use "stdin" is a terminal
file, ok := stdin.(*os.File)
needTTY := ok && terminal.IsTerminal(int(file.Fd())) && ctnr.Config.Tty
if needTTY {
state, err := terminal.MakeRaw(int(file.Fd()))
if err != nil {
return err
}
logrus.SetFormatter(&rawFormatter{})
defer func() {
if err := terminal.Restore(int(file.Fd()), state); err != nil {
logrus.Errorf("unable to restore terminal: %q", err)
}
logrus.SetFormatter(&logrus.TextFormatter{})
}()
winChange := make(chan os.Signal, 1)
signal.Notify(winChange, sig.SIGWINCH)
winCtx, winCancel := context.WithCancel(ctx)
defer winCancel()
go func() {
// Prime the pump, we need one reset to ensure everything is ready
winChange <- sig.SIGWINCH
for {
select {
case <-winCtx.Done():
return
case <-winChange:
h, w, err := terminal.GetSize(int(file.Fd()))
if err != nil {
logrus.Warnf("failed to obtain TTY size: " + err.Error())
}
if err := ResizeContainerTTY(ctx, nameOrId, &h, &w); err != nil {
logrus.Warnf("failed to resize TTY: " + err.Error())
}
}
}
}()
}
response, err := conn.DoRequest(stdin, http.MethodPost, "/containers/%s/attach", params, nil, nameOrId)
if err != nil {
return err
}
if !(response.IsSuccess() || response.IsInformational()) {
return response.Process(nil)
}
// If we are attaching around a start, we need to "signal"
// back that we are in fact attached so that started does
// not execute before we can attach.
if attachReady != nil {
attachReady <- true
}
buffer := make([]byte, 1024)
if ctnr.Config.Tty {
if !isSet.stdout {
return fmt.Errorf("container %q requires stdout to be set", ctnr.ID)
}
// If not multiplex'ed, read from server and write to stdout
_, err := io.Copy(stdout, response.Body)
if err != nil {
return err
}
} else {
for {
// Read multiplexed channels and write to appropriate stream
fd, l, err := DemuxHeader(response.Body, buffer)
if err != nil {
if errors.Is(err, io.EOF) {
return nil
}
return err
}
frame, err := DemuxFrame(response.Body, buffer, l)
if err != nil {
return err
}
switch {
case fd == 0 && isSet.stdout:
_, err := stdout.Write(frame[0:l])
if err != nil {
return err
}
case fd == 1 && isSet.stdout:
_, err := stdout.Write(frame[0:l])
if err != nil {
return err
}
case fd == 2 && isSet.stderr:
_, err := stderr.Write(frame[0:l])
if err != nil {
return err
}
case fd == 3:
return fmt.Errorf("error from service from stream: %s", frame)
default:
return fmt.Errorf("unrecognized channel in header: %d, 0-3 supported", fd)
}
}
}
return nil
}
// DemuxHeader reads header for stream from server multiplexed stdin/stdout/stderr/2nd error channel
func DemuxHeader(r io.Reader, buffer []byte) (fd, sz int, err error) {
n, err := io.ReadFull(r, buffer[0:8])
if err != nil {
return
}
if n < 8 {
err = io.ErrUnexpectedEOF
return
}
fd = int(buffer[0])
if fd < 0 || fd > 3 {
err = errors.Wrapf(ErrLostSync, fmt.Sprintf(`channel "%d" found, 0-3 supported`, fd))
return
}
sz = int(binary.BigEndian.Uint32(buffer[4:8]))
return
}
// DemuxFrame reads contents for frame from server multiplexed stdin/stdout/stderr/2nd error channel
func DemuxFrame(r io.Reader, buffer []byte, length int) (frame []byte, err error) {
if len(buffer) < length {
buffer = append(buffer, make([]byte, length-len(buffer)+1)...)
}
n, err := io.ReadFull(r, buffer[0:length])
if err != nil {
return nil, nil
}
if n < length {
err = io.ErrUnexpectedEOF
return
}
return buffer[0:length], nil
}
// ResizeContainerTTY sets container's TTY height and width in characters
func ResizeContainerTTY(ctx context.Context, nameOrId string, height *int, width *int) error {
return resizeTTY(ctx, bindings.JoinURL("containers", nameOrId, "resize"), height, width)
}
// ResizeExecTTY sets session's TTY height and width in characters
func ResizeExecTTY(ctx context.Context, nameOrId string, height *int, width *int) error {
return resizeTTY(ctx, bindings.JoinURL("exec", nameOrId, "resize"), height, width)
}
// resizeTTY set size of TTY of container
func resizeTTY(ctx context.Context, endpoint string, height *int, width *int) error {
conn, err := bindings.GetClient(ctx)
if err != nil {
return err
}
params := url.Values{}
if height != nil {
params.Set("h", strconv.Itoa(*height))
}
if width != nil {
params.Set("w", strconv.Itoa(*width))
}
rsp, err := conn.DoRequest(nil, http.MethodPost, endpoint, params, nil)
if err != nil {
return err
}
return rsp.Process(nil)
}
type rawFormatter struct {
logrus.TextFormatter
}
func (f *rawFormatter) Format(entry *logrus.Entry) ([]byte, error) {
buffer, err := f.TextFormatter.Format(entry)
if err != nil {
return buffer, err
}
return append(buffer, '\r'), nil
}

View File

@ -2,6 +2,7 @@ package tunnel
import ( import (
"context" "context"
"fmt"
"io" "io"
"os" "os"
"strconv" "strconv"
@ -11,6 +12,7 @@ import (
"github.com/containers/common/pkg/config" "github.com/containers/common/pkg/config"
"github.com/containers/image/v5/docker/reference" "github.com/containers/image/v5/docker/reference"
"github.com/containers/libpod/libpod/define" "github.com/containers/libpod/libpod/define"
"github.com/containers/libpod/pkg/api/handlers"
"github.com/containers/libpod/pkg/bindings" "github.com/containers/libpod/pkg/bindings"
"github.com/containers/libpod/pkg/bindings/containers" "github.com/containers/libpod/pkg/bindings/containers"
"github.com/containers/libpod/pkg/domain/entities" "github.com/containers/libpod/pkg/domain/entities"
@ -375,7 +377,39 @@ func (ic *ContainerEngine) ContainerAttach(ctx context.Context, nameOrId string,
} }
func (ic *ContainerEngine) ContainerExec(ctx context.Context, nameOrId string, options entities.ExecOptions, streams define.AttachStreams) (int, error) { func (ic *ContainerEngine) ContainerExec(ctx context.Context, nameOrId string, options entities.ExecOptions, streams define.AttachStreams) (int, error) {
return 125, errors.New("not implemented") env := []string{}
for k, v := range options.Envs {
env = append(env, fmt.Sprintf("%s=%s", k, v))
}
createConfig := new(handlers.ExecCreateConfig)
createConfig.User = options.User
createConfig.Privileged = options.Privileged
createConfig.Tty = options.Tty
createConfig.AttachStdin = options.Interactive
createConfig.AttachStdout = true
createConfig.AttachStderr = true
createConfig.Detach = false
createConfig.DetachKeys = options.DetachKeys
createConfig.Env = env
createConfig.WorkingDir = options.WorkDir
createConfig.Cmd = options.Cmd
sessionID, err := containers.ExecCreate(ic.ClientCxt, nameOrId, createConfig)
if err != nil {
return 125, err
}
if err := containers.ExecStartAndAttach(ic.ClientCxt, sessionID, &streams); err != nil {
return 125, err
}
inspectOut, err := containers.ExecInspect(ic.ClientCxt, sessionID)
if err != nil {
return 125, err
}
return inspectOut.ExitCode, nil
} }
func (ic *ContainerEngine) ContainerExecDetached(ctx context.Context, nameOrID string, options entities.ExecOptions) (string, error) { func (ic *ContainerEngine) ContainerExecDetached(ctx context.Context, nameOrID string, options entities.ExecOptions) (string, error) {

View File

@ -18,7 +18,6 @@ var _ = Describe("Podman exec", func() {
) )
BeforeEach(func() { BeforeEach(func() {
Skip(v2remotefail)
tempdir, err = CreateTempDirInTempDir() tempdir, err = CreateTempDirInTempDir()
if err != nil { if err != nil {
os.Exit(1) os.Exit(1)
@ -285,6 +284,7 @@ var _ = Describe("Podman exec", func() {
}) })
It("podman exec --detach", func() { It("podman exec --detach", func() {
Skip(v2remotefail)
ctrName := "testctr" ctrName := "testctr"
ctr := podmanTest.Podman([]string{"run", "-t", "-i", "-d", "--name", ctrName, ALPINE, "top"}) ctr := podmanTest.Podman([]string{"run", "-t", "-i", "-d", "--name", ctrName, ALPINE, "top"})
ctr.WaitWithDefaultTimeout() ctr.WaitWithDefaultTimeout()