mirror of
https://github.com/containers/podman.git
synced 2025-05-20 00:27:03 +08:00

Conmon writes the exit file and oom file (if container was oom killed) to the persist directory. This directory is retained across reboots as well. Update podman to create a persist-dir/ctr-id for the exit and oom files for each container to be written to. The oom state of container is set after reading the files from the persist-dir/ctr-id directory. The exit code still continues to read the exit file from the exits directory. Signed-off-by: Urvashi Mohnani <umohnani@redhat.com>
780 lines
23 KiB
Go
780 lines
23 KiB
Go
//go:build !remote && (linux || freebsd)
|
|
|
|
package libpod
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/containers/common/pkg/config"
|
|
"github.com/containers/common/pkg/detach"
|
|
"github.com/containers/common/pkg/resize"
|
|
"github.com/containers/podman/v5/libpod/define"
|
|
"github.com/containers/podman/v5/pkg/errorhandling"
|
|
"github.com/containers/podman/v5/pkg/lookup"
|
|
spec "github.com/opencontainers/runtime-spec/specs-go"
|
|
"github.com/sirupsen/logrus"
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
// ExecContainer executes a command in a running container
|
|
func (r *ConmonOCIRuntime) ExecContainer(c *Container, sessionID string, options *ExecOptions, streams *define.AttachStreams, newSize *resize.TerminalSize) (int, chan error, error) {
|
|
if options == nil {
|
|
return -1, nil, fmt.Errorf("must provide an ExecOptions struct to ExecContainer: %w", define.ErrInvalidArg)
|
|
}
|
|
if len(options.Cmd) == 0 {
|
|
return -1, nil, fmt.Errorf("must provide a command to execute: %w", define.ErrInvalidArg)
|
|
}
|
|
|
|
if sessionID == "" {
|
|
return -1, nil, fmt.Errorf("must provide a session ID for exec: %w", define.ErrEmptyID)
|
|
}
|
|
|
|
// TODO: Should we default this to false?
|
|
// Or maybe make streams mandatory?
|
|
attachStdin := true
|
|
if streams != nil {
|
|
attachStdin = streams.AttachInput
|
|
}
|
|
|
|
var ociLog string
|
|
if logrus.GetLevel() != logrus.DebugLevel && r.supportsJSON {
|
|
ociLog = c.execOCILog(sessionID)
|
|
}
|
|
|
|
execCmd, pipes, err := r.startExec(c, sessionID, options, attachStdin, ociLog)
|
|
if err != nil {
|
|
return -1, nil, err
|
|
}
|
|
|
|
// Only close sync pipe. Start and attach are consumed in the attach
|
|
// goroutine.
|
|
defer func() {
|
|
if pipes.syncPipe != nil && !pipes.syncClosed {
|
|
errorhandling.CloseQuiet(pipes.syncPipe)
|
|
pipes.syncClosed = true
|
|
}
|
|
}()
|
|
|
|
// TODO Only create if !detach
|
|
// Attach to the container before starting it
|
|
attachChan := make(chan error)
|
|
go func() {
|
|
// attachToExec is responsible for closing pipes
|
|
attachChan <- c.attachToExec(streams, options.DetachKeys, sessionID, pipes.startPipe, pipes.attachPipe, newSize)
|
|
close(attachChan)
|
|
}()
|
|
|
|
if err := execCmd.Wait(); err != nil {
|
|
return -1, nil, fmt.Errorf("cannot run conmon: %w", err)
|
|
}
|
|
|
|
pid, err := readConmonPipeData(r.name, pipes.syncPipe, ociLog)
|
|
|
|
return pid, attachChan, err
|
|
}
|
|
|
|
// ExecContainerHTTP executes a new command in an existing container and
|
|
// forwards its standard streams over an attach
|
|
func (r *ConmonOCIRuntime) ExecContainerHTTP(ctr *Container, sessionID string, options *ExecOptions, req *http.Request, w http.ResponseWriter,
|
|
streams *HTTPAttachStreams, cancel <-chan bool, hijackDone chan<- bool, holdConnOpen <-chan bool, newSize *resize.TerminalSize) (int, chan error, error) {
|
|
if streams != nil {
|
|
if !streams.Stdin && !streams.Stdout && !streams.Stderr {
|
|
return -1, nil, fmt.Errorf("must provide at least one stream to attach to: %w", define.ErrInvalidArg)
|
|
}
|
|
}
|
|
|
|
if options == nil {
|
|
return -1, nil, fmt.Errorf("must provide exec options to ExecContainerHTTP: %w", define.ErrInvalidArg)
|
|
}
|
|
|
|
detachString := config.DefaultDetachKeys
|
|
if options.DetachKeys != nil {
|
|
detachString = *options.DetachKeys
|
|
}
|
|
detachKeys, err := processDetachKeys(detachString)
|
|
if err != nil {
|
|
return -1, nil, err
|
|
}
|
|
|
|
// TODO: Should we default this to false?
|
|
// Or maybe make streams mandatory?
|
|
attachStdin := true
|
|
if streams != nil {
|
|
attachStdin = streams.Stdin
|
|
}
|
|
|
|
var ociLog string
|
|
if logrus.GetLevel() != logrus.DebugLevel && r.supportsJSON {
|
|
ociLog = ctr.execOCILog(sessionID)
|
|
}
|
|
|
|
execCmd, pipes, err := r.startExec(ctr, sessionID, options, attachStdin, ociLog)
|
|
if err != nil {
|
|
return -1, nil, err
|
|
}
|
|
|
|
// Only close sync pipe. Start and attach are consumed in the attach
|
|
// goroutine.
|
|
defer func() {
|
|
if pipes.syncPipe != nil && !pipes.syncClosed {
|
|
errorhandling.CloseQuiet(pipes.syncPipe)
|
|
pipes.syncClosed = true
|
|
}
|
|
}()
|
|
|
|
attachChan := make(chan error)
|
|
conmonPipeDataChan := make(chan conmonPipeData)
|
|
go func() {
|
|
// attachToExec is responsible for closing pipes
|
|
attachChan <- attachExecHTTP(ctr, sessionID, req, w, streams, pipes, detachKeys, options.Terminal, cancel, hijackDone, holdConnOpen, execCmd, conmonPipeDataChan, ociLog, newSize, r.name)
|
|
close(attachChan)
|
|
}()
|
|
|
|
// NOTE: the channel is needed to communicate conmon's data. In case
|
|
// of an error, the error will be written on the hijacked http
|
|
// connection such that remote clients will receive the error.
|
|
pipeData := <-conmonPipeDataChan
|
|
|
|
return pipeData.pid, attachChan, pipeData.err
|
|
}
|
|
|
|
// conmonPipeData contains the data when reading from conmon's pipe.
|
|
type conmonPipeData struct {
|
|
pid int
|
|
err error
|
|
}
|
|
|
|
// ExecContainerDetached executes a command in a running container, but does
|
|
// not attach to it.
|
|
func (r *ConmonOCIRuntime) ExecContainerDetached(ctr *Container, sessionID string, options *ExecOptions, stdin bool) (int, error) {
|
|
if options == nil {
|
|
return -1, fmt.Errorf("must provide exec options to ExecContainerHTTP: %w", define.ErrInvalidArg)
|
|
}
|
|
|
|
var ociLog string
|
|
if logrus.GetLevel() != logrus.DebugLevel && r.supportsJSON {
|
|
ociLog = ctr.execOCILog(sessionID)
|
|
}
|
|
|
|
execCmd, pipes, err := r.startExec(ctr, sessionID, options, stdin, ociLog)
|
|
if err != nil {
|
|
return -1, err
|
|
}
|
|
|
|
defer func() {
|
|
pipes.cleanup()
|
|
}()
|
|
|
|
// Wait for Conmon to tell us we're ready to attach.
|
|
// We aren't actually *going* to attach, but this means that we're good
|
|
// to proceed.
|
|
if _, err := readConmonPipeData(r.name, pipes.attachPipe, ""); err != nil {
|
|
return -1, err
|
|
}
|
|
|
|
// Start the exec session
|
|
if err := writeConmonPipeData(pipes.startPipe); err != nil {
|
|
return -1, err
|
|
}
|
|
|
|
// Wait for conmon to succeed, when return.
|
|
if err := execCmd.Wait(); err != nil {
|
|
return -1, fmt.Errorf("cannot run conmon: %w", err)
|
|
}
|
|
|
|
pid, err := readConmonPipeData(r.name, pipes.syncPipe, ociLog)
|
|
|
|
return pid, err
|
|
}
|
|
|
|
// ExecAttachResize resizes the TTY of the given exec session.
|
|
func (r *ConmonOCIRuntime) ExecAttachResize(ctr *Container, sessionID string, newSize resize.TerminalSize) error {
|
|
controlFile, err := openControlFile(ctr, ctr.execBundlePath(sessionID))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer controlFile.Close()
|
|
|
|
if _, err = fmt.Fprintf(controlFile, "%d %d %d\n", 1, newSize.Height, newSize.Width); err != nil {
|
|
return fmt.Errorf("failed to write to ctl file to resize terminal: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ExecStopContainer stops a given exec session in a running container.
|
|
func (r *ConmonOCIRuntime) ExecStopContainer(ctr *Container, sessionID string, timeout uint) error {
|
|
pid, err := ctr.getExecSessionPID(sessionID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
logrus.Debugf("Going to stop container %s exec session %s", ctr.ID(), sessionID)
|
|
|
|
// Is the session dead?
|
|
// Ping the PID with signal 0 to see if it still exists.
|
|
if err := unix.Kill(pid, 0); err != nil {
|
|
if err == unix.ESRCH {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("pinging container %s exec session %s PID %d with signal 0: %w", ctr.ID(), sessionID, pid, err)
|
|
}
|
|
|
|
if timeout > 0 {
|
|
// Use SIGTERM by default, then SIGSTOP after timeout.
|
|
logrus.Debugf("Killing exec session %s (PID %d) of container %s with SIGTERM", sessionID, pid, ctr.ID())
|
|
if err := unix.Kill(pid, unix.SIGTERM); err != nil {
|
|
if err == unix.ESRCH {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("killing container %s exec session %s PID %d with SIGTERM: %w", ctr.ID(), sessionID, pid, err)
|
|
}
|
|
|
|
// Wait for the PID to stop
|
|
if err := waitPidStop(pid, time.Duration(timeout)*time.Second); err != nil {
|
|
logrus.Infof("Timed out waiting for container %s exec session %s to stop, resorting to SIGKILL: %v", ctr.ID(), sessionID, err)
|
|
} else {
|
|
// No error, container is dead
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// SIGTERM did not work. On to SIGKILL.
|
|
logrus.Debugf("Killing exec session %s (PID %d) of container %s with SIGKILL", sessionID, pid, ctr.ID())
|
|
if err := unix.Kill(pid, unix.SIGTERM); err != nil {
|
|
if err == unix.ESRCH {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("killing container %s exec session %s PID %d with SIGKILL: %w", ctr.ID(), sessionID, pid, err)
|
|
}
|
|
|
|
// Wait for the PID to stop
|
|
if err := waitPidStop(pid, killContainerTimeout); err != nil {
|
|
return fmt.Errorf("timed out waiting for container %s exec session %s PID %d to stop after SIGKILL: %w", ctr.ID(), sessionID, pid, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ExecUpdateStatus checks if the given exec session is still running.
|
|
func (r *ConmonOCIRuntime) ExecUpdateStatus(ctr *Container, sessionID string) (bool, error) {
|
|
pid, err := ctr.getExecSessionPID(sessionID)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
logrus.Debugf("Checking status of container %s exec session %s", ctr.ID(), sessionID)
|
|
|
|
// Is the session dead?
|
|
// Ping the PID with signal 0 to see if it still exists.
|
|
if err := unix.Kill(pid, 0); err != nil {
|
|
if err == unix.ESRCH {
|
|
return false, nil
|
|
}
|
|
return false, fmt.Errorf("pinging container %s exec session %s PID %d with signal 0: %w", ctr.ID(), sessionID, pid, err)
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// ExecAttachSocketPath is the path to a container's exec session attach socket.
|
|
func (r *ConmonOCIRuntime) ExecAttachSocketPath(ctr *Container, sessionID string) (string, error) {
|
|
// We don't even use container, so don't validity check it
|
|
if sessionID == "" {
|
|
return "", fmt.Errorf("must provide a valid session ID to get attach socket path: %w", define.ErrInvalidArg)
|
|
}
|
|
|
|
return filepath.Join(ctr.execBundlePath(sessionID), "attach"), nil
|
|
}
|
|
|
|
// This contains pipes used by the exec API.
|
|
type execPipes struct {
|
|
syncPipe *os.File
|
|
syncClosed bool
|
|
startPipe *os.File
|
|
startClosed bool
|
|
attachPipe *os.File
|
|
attachClosed bool
|
|
}
|
|
|
|
func (p *execPipes) cleanup() {
|
|
if p.syncPipe != nil && !p.syncClosed {
|
|
errorhandling.CloseQuiet(p.syncPipe)
|
|
p.syncClosed = true
|
|
}
|
|
if p.startPipe != nil && !p.startClosed {
|
|
errorhandling.CloseQuiet(p.startPipe)
|
|
p.startClosed = true
|
|
}
|
|
if p.attachPipe != nil && !p.attachClosed {
|
|
errorhandling.CloseQuiet(p.attachPipe)
|
|
p.attachClosed = true
|
|
}
|
|
}
|
|
|
|
// Start an exec session's conmon parent from the given options.
|
|
func (r *ConmonOCIRuntime) startExec(c *Container, sessionID string, options *ExecOptions, attachStdin bool, ociLog string) (_ *exec.Cmd, _ *execPipes, deferredErr error) {
|
|
pipes := new(execPipes)
|
|
|
|
if options == nil {
|
|
return nil, nil, fmt.Errorf("must provide an ExecOptions struct to ExecContainer: %w", define.ErrInvalidArg)
|
|
}
|
|
if len(options.Cmd) == 0 {
|
|
return nil, nil, fmt.Errorf("must provide a command to execute: %w", define.ErrInvalidArg)
|
|
}
|
|
|
|
if sessionID == "" {
|
|
return nil, nil, fmt.Errorf("must provide a session ID for exec: %w", define.ErrEmptyID)
|
|
}
|
|
|
|
// create sync pipe to receive the pid
|
|
parentSyncPipe, childSyncPipe, err := newPipe()
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("creating socket pair: %w", err)
|
|
}
|
|
pipes.syncPipe = parentSyncPipe
|
|
|
|
defer func() {
|
|
if deferredErr != nil {
|
|
pipes.cleanup()
|
|
}
|
|
}()
|
|
|
|
// create start pipe to set the cgroup before running
|
|
// attachToExec is responsible for closing parentStartPipe
|
|
childStartPipe, parentStartPipe, err := newPipe()
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("creating socket pair: %w", err)
|
|
}
|
|
pipes.startPipe = parentStartPipe
|
|
|
|
// create the attach pipe to allow attach socket to be created before
|
|
// $RUNTIME exec starts running. This is to make sure we can capture all output
|
|
// from the process through that socket, rather than half reading the log, half attaching to the socket
|
|
// attachToExec is responsible for closing parentAttachPipe
|
|
parentAttachPipe, childAttachPipe, err := newPipe()
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("creating socket pair: %w", err)
|
|
}
|
|
pipes.attachPipe = parentAttachPipe
|
|
|
|
childrenClosed := false
|
|
defer func() {
|
|
if !childrenClosed {
|
|
errorhandling.CloseQuiet(childSyncPipe)
|
|
errorhandling.CloseQuiet(childAttachPipe)
|
|
errorhandling.CloseQuiet(childStartPipe)
|
|
}
|
|
}()
|
|
|
|
finalEnv := make([]string, 0, len(options.Env))
|
|
for k, v := range options.Env {
|
|
finalEnv = append(finalEnv, fmt.Sprintf("%s=%s", k, v))
|
|
}
|
|
|
|
processFile, err := c.prepareProcessExec(options, finalEnv, sessionID)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
defer processFile.Close()
|
|
|
|
args, err := r.sharedConmonArgs(c, sessionID, c.execBundlePath(sessionID), c.execPidPath(sessionID), c.execLogPath(sessionID), c.execExitFileDir(sessionID), c.execPersistDir(sessionID), ociLog, define.NoLogging, c.config.LogTag)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
preserveFDs, filesToClose, extraFiles, err := getPreserveFdExtraFiles(options.PreserveFD, options.PreserveFDs)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
if preserveFDs > 0 {
|
|
args = append(args, formatRuntimeOpts("--preserve-fds", strconv.FormatUint(uint64(preserveFDs), 10))...)
|
|
}
|
|
|
|
if options.Terminal {
|
|
args = append(args, "-t")
|
|
}
|
|
|
|
if attachStdin {
|
|
args = append(args, "-i")
|
|
}
|
|
|
|
// Append container ID and command
|
|
args = append(args, "-e")
|
|
// TODO make this optional when we can detach
|
|
args = append(args, "--exec-attach")
|
|
args = append(args, "--exec-process-spec", processFile.Name())
|
|
|
|
if len(options.ExitCommand) > 0 {
|
|
args = append(args, "--exit-command", options.ExitCommand[0])
|
|
for _, arg := range options.ExitCommand[1:] {
|
|
args = append(args, []string{"--exit-command-arg", arg}...)
|
|
}
|
|
if options.ExitCommandDelay > 0 {
|
|
args = append(args, []string{"--exit-delay", strconv.FormatUint(uint64(options.ExitCommandDelay), 10)}...)
|
|
}
|
|
}
|
|
|
|
logrus.WithFields(logrus.Fields{
|
|
"args": args,
|
|
}).Debugf("running conmon: %s", r.conmonPath)
|
|
execCmd := exec.Command(r.conmonPath, args...)
|
|
|
|
// TODO: This is commented because it doesn't make much sense in HTTP
|
|
// attach, and I'm not certain it does for non-HTTP attach as well.
|
|
// if streams != nil {
|
|
// // Don't add the InputStream to the execCmd. Instead, the data should be passed
|
|
// // through CopyDetachable
|
|
// if streams.AttachOutput {
|
|
// execCmd.Stdout = options.Streams.OutputStream
|
|
// }
|
|
// if streams.AttachError {
|
|
// execCmd.Stderr = options.Streams.ErrorStream
|
|
// }
|
|
// }
|
|
|
|
conmonEnv, err := r.configureConmonEnv()
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("configuring conmon env: %w", err)
|
|
}
|
|
|
|
execCmd.ExtraFiles = extraFiles
|
|
|
|
// we don't want to step on users fds they asked to preserve
|
|
// Since 0-2 are used for stdio, start the fds we pass in at preserveFDs+3
|
|
execCmd.Env = r.conmonEnv
|
|
execCmd.Env = append(execCmd.Env, fmt.Sprintf("_OCI_SYNCPIPE=%d", preserveFDs+3), fmt.Sprintf("_OCI_STARTPIPE=%d", preserveFDs+4), fmt.Sprintf("_OCI_ATTACHPIPE=%d", preserveFDs+5))
|
|
execCmd.Env = append(execCmd.Env, conmonEnv...)
|
|
|
|
execCmd.ExtraFiles = append(execCmd.ExtraFiles, childSyncPipe, childStartPipe, childAttachPipe)
|
|
execCmd.Dir = c.execBundlePath(sessionID)
|
|
execCmd.SysProcAttr = &syscall.SysProcAttr{
|
|
Setpgid: true,
|
|
}
|
|
|
|
err = execCmd.Start()
|
|
|
|
// We don't need children pipes on the parent side
|
|
errorhandling.CloseQuiet(childSyncPipe)
|
|
errorhandling.CloseQuiet(childAttachPipe)
|
|
errorhandling.CloseQuiet(childStartPipe)
|
|
childrenClosed = true
|
|
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("cannot start container %s: %w", c.ID(), err)
|
|
}
|
|
if err := r.moveConmonToCgroupAndSignal(c, execCmd, parentStartPipe); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// These fds were passed down to the runtime. Close them
|
|
// and not interfere
|
|
for _, f := range filesToClose {
|
|
errorhandling.CloseQuiet(f)
|
|
}
|
|
|
|
return execCmd, pipes, nil
|
|
}
|
|
|
|
// Attach to a container over HTTP
|
|
func attachExecHTTP(c *Container, sessionID string, r *http.Request, w http.ResponseWriter, streams *HTTPAttachStreams, pipes *execPipes, detachKeys []byte, isTerminal bool, cancel <-chan bool, hijackDone chan<- bool, holdConnOpen <-chan bool, execCmd *exec.Cmd, conmonPipeDataChan chan<- conmonPipeData, ociLog string, newSize *resize.TerminalSize, runtimeName string) (deferredErr error) {
|
|
// NOTE: As you may notice, the attach code is quite complex.
|
|
// Many things happen concurrently and yet are interdependent.
|
|
// If you ever change this function, make sure to write to the
|
|
// conmonPipeDataChan in case of an error.
|
|
|
|
if pipes == nil || pipes.startPipe == nil || pipes.attachPipe == nil {
|
|
err := fmt.Errorf("must provide a start and attach pipe to finish an exec attach: %w", define.ErrInvalidArg)
|
|
conmonPipeDataChan <- conmonPipeData{-1, err}
|
|
return err
|
|
}
|
|
|
|
defer func() {
|
|
if !pipes.startClosed {
|
|
errorhandling.CloseQuiet(pipes.startPipe)
|
|
pipes.startClosed = true
|
|
}
|
|
if !pipes.attachClosed {
|
|
errorhandling.CloseQuiet(pipes.attachPipe)
|
|
pipes.attachClosed = true
|
|
}
|
|
}()
|
|
|
|
logrus.Debugf("Attaching to container %s exec session %s", c.ID(), sessionID)
|
|
|
|
// set up the socket path, such that it is the correct length and location for exec
|
|
sockPath, err := c.execAttachSocketPath(sessionID)
|
|
if err != nil {
|
|
conmonPipeDataChan <- conmonPipeData{-1, err}
|
|
return err
|
|
}
|
|
|
|
// 2: read from attachFd that the parent process has set up the console socket
|
|
if _, err := readConmonPipeData(runtimeName, pipes.attachPipe, ""); err != nil {
|
|
conmonPipeDataChan <- conmonPipeData{-1, err}
|
|
return err
|
|
}
|
|
|
|
// resize before we start the container process
|
|
if newSize != nil {
|
|
err = c.ociRuntime.ExecAttachResize(c, sessionID, *newSize)
|
|
if err != nil {
|
|
logrus.Warnf("Resize failed: %v", err)
|
|
}
|
|
}
|
|
|
|
// 2: then attach
|
|
conn, err := openUnixSocket(sockPath)
|
|
if err != nil {
|
|
conmonPipeDataChan <- conmonPipeData{-1, err}
|
|
return fmt.Errorf("failed to connect to container's attach socket: %v: %w", sockPath, err)
|
|
}
|
|
defer func() {
|
|
if err := conn.Close(); err != nil {
|
|
logrus.Errorf("Unable to close socket: %q", err)
|
|
}
|
|
}()
|
|
|
|
attachStdout := true
|
|
attachStderr := true
|
|
attachStdin := true
|
|
if streams != nil {
|
|
attachStdout = streams.Stdout
|
|
attachStderr = streams.Stderr
|
|
attachStdin = streams.Stdin
|
|
}
|
|
|
|
// Perform hijack
|
|
hijacker, ok := w.(http.Hijacker)
|
|
if !ok {
|
|
conmonPipeDataChan <- conmonPipeData{-1, err}
|
|
return errors.New("unable to hijack connection")
|
|
}
|
|
|
|
httpCon, httpBuf, err := hijacker.Hijack()
|
|
if err != nil {
|
|
conmonPipeDataChan <- conmonPipeData{-1, err}
|
|
return fmt.Errorf("hijacking connection: %w", err)
|
|
}
|
|
|
|
hijackDone <- true
|
|
|
|
// Write a header to let the client know what happened
|
|
writeHijackHeader(r, httpBuf, isTerminal)
|
|
|
|
// Force a flush after the header is written.
|
|
if err := httpBuf.Flush(); err != nil {
|
|
conmonPipeDataChan <- conmonPipeData{-1, err}
|
|
return fmt.Errorf("flushing HTTP hijack header: %w", err)
|
|
}
|
|
|
|
go func() {
|
|
// Wait for conmon to succeed, when return.
|
|
if err := execCmd.Wait(); err != nil {
|
|
conmonPipeDataChan <- conmonPipeData{-1, err}
|
|
} else {
|
|
pid, err := readConmonPipeData(runtimeName, pipes.syncPipe, ociLog)
|
|
if err != nil {
|
|
hijackWriteError(err, c.ID(), isTerminal, httpBuf)
|
|
conmonPipeDataChan <- conmonPipeData{pid, err}
|
|
} else {
|
|
conmonPipeDataChan <- conmonPipeData{pid, err}
|
|
}
|
|
}
|
|
// We need to hold the connection open until the complete exec
|
|
// function has finished. This channel will be closed in a defer
|
|
// in that function, so we can wait for it here.
|
|
// Can't be a defer, because this would block the function from
|
|
// returning.
|
|
<-holdConnOpen
|
|
hijackWriteErrorAndClose(deferredErr, c.ID(), isTerminal, httpCon, httpBuf)
|
|
}()
|
|
|
|
stdoutChan := make(chan error)
|
|
stdinChan := make(chan error)
|
|
|
|
// Next, STDIN. Avoid entirely if attachStdin unset.
|
|
if attachStdin {
|
|
go func() {
|
|
logrus.Debugf("Beginning STDIN copy")
|
|
_, err := detach.Copy(conn, httpBuf, detachKeys)
|
|
logrus.Debugf("STDIN copy completed")
|
|
stdinChan <- err
|
|
}()
|
|
}
|
|
|
|
// 4: send start message to child
|
|
if err := writeConmonPipeData(pipes.startPipe); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Handle STDOUT/STDERR *after* start message is sent
|
|
go func() {
|
|
var err error
|
|
if isTerminal {
|
|
// Hack: return immediately if attachStdout not set to
|
|
// emulate Docker.
|
|
// Basically, when terminal is set, STDERR goes nowhere.
|
|
// Everything does over STDOUT.
|
|
// Therefore, if not attaching STDOUT - we'll never copy
|
|
// anything from here.
|
|
logrus.Debugf("Performing terminal HTTP attach for container %s", c.ID())
|
|
if attachStdout {
|
|
err = httpAttachTerminalCopy(conn, httpBuf, c.ID())
|
|
}
|
|
} else {
|
|
logrus.Debugf("Performing non-terminal HTTP attach for container %s", c.ID())
|
|
err = httpAttachNonTerminalCopy(conn, httpBuf, c.ID(), attachStdin, attachStdout, attachStderr)
|
|
}
|
|
stdoutChan <- err
|
|
logrus.Debugf("STDOUT/ERR copy completed")
|
|
}()
|
|
|
|
for {
|
|
select {
|
|
case err := <-stdoutChan:
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
case err := <-stdinChan:
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// copy stdin is done, close it
|
|
if connErr := socketCloseWrite(conn); connErr != nil {
|
|
logrus.Errorf("Unable to close conn: %v", connErr)
|
|
}
|
|
case <-cancel:
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// prepareProcessExec returns the path of the process.json used in runc exec -p
|
|
// caller is responsible to close the returned *os.File if needed.
|
|
func (c *Container) prepareProcessExec(options *ExecOptions, env []string, sessionID string) (*os.File, error) {
|
|
f, err := os.CreateTemp(c.execBundlePath(sessionID), "exec-process-")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pspec := new(spec.Process)
|
|
if err := JSONDeepCopy(c.config.Spec.Process, pspec); err != nil {
|
|
return nil, err
|
|
}
|
|
pspec.SelinuxLabel = c.config.ProcessLabel
|
|
pspec.Args = options.Cmd
|
|
|
|
// We need to default this to false else it will inherit terminal as true
|
|
// from the container.
|
|
pspec.Terminal = false
|
|
if options.Terminal {
|
|
pspec.Terminal = true
|
|
}
|
|
if len(env) > 0 {
|
|
pspec.Env = append(pspec.Env, env...)
|
|
}
|
|
|
|
// Add secret envs if they exist
|
|
manager, err := c.runtime.SecretsManager()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for name, secr := range c.config.EnvSecrets {
|
|
_, data, err := manager.LookupSecretData(secr.Name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pspec.Env = append(pspec.Env, fmt.Sprintf("%s=%s", name, string(data)))
|
|
}
|
|
|
|
if options.Cwd != "" {
|
|
pspec.Cwd = options.Cwd
|
|
}
|
|
|
|
var addGroups []string
|
|
var sgids []uint32
|
|
|
|
// if the user is empty, we should inherit the user that the container is currently running with
|
|
user := options.User
|
|
if user == "" {
|
|
logrus.Debugf("Set user to %s", c.config.User)
|
|
user = c.config.User
|
|
addGroups = c.config.Groups
|
|
}
|
|
|
|
overrides := c.getUserOverrides()
|
|
execUser, err := lookup.GetUserGroupInfo(c.state.Mountpoint, user, overrides)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(addGroups) > 0 {
|
|
sgids, err = lookup.GetContainerGroups(addGroups, c.state.Mountpoint, overrides)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("looking up supplemental groups for container %s exec session %s: %w", c.ID(), sessionID, err)
|
|
}
|
|
}
|
|
|
|
// If user was set, look it up in the container to get a UID to use on
|
|
// the host
|
|
if user != "" || len(sgids) > 0 {
|
|
if user != "" {
|
|
for _, sgid := range execUser.Sgids {
|
|
sgids = append(sgids, uint32(sgid))
|
|
}
|
|
}
|
|
processUser := spec.User{
|
|
UID: uint32(execUser.Uid),
|
|
GID: uint32(execUser.Gid),
|
|
AdditionalGids: sgids,
|
|
}
|
|
|
|
pspec.User = processUser
|
|
}
|
|
|
|
if c.config.Umask != "" {
|
|
umask, err := c.umask()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pspec.User.Umask = &umask
|
|
}
|
|
|
|
if err := c.setProcessCapabilitiesExec(options, user, execUser, pspec); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
hasHomeSet := false
|
|
for _, s := range pspec.Env {
|
|
if strings.HasPrefix(s, "HOME=") {
|
|
hasHomeSet = true
|
|
break
|
|
}
|
|
}
|
|
if !hasHomeSet {
|
|
pspec.Env = append(pspec.Env, fmt.Sprintf("HOME=%s", execUser.Home))
|
|
}
|
|
|
|
processJSON, err := json.Marshal(pspec)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := os.WriteFile(f.Name(), processJSON, 0644); err != nil {
|
|
return nil, err
|
|
}
|
|
return f, nil
|
|
}
|