Skip to content

Commit 5d5a465

Browse files
Merge pull request #5363 from haircommander/revert-conmon-1.19
[1.19] oci: finish reverting exec changes
2 parents 7d25e5d + ba21e48 commit 5d5a465

File tree

3 files changed

+216
-45
lines changed

3 files changed

+216
-45
lines changed

internal/oci/oci.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -396,13 +396,6 @@ func (r *Runtime) ReopenContainerLog(c *Container) error {
396396
return impl.ReopenContainerLog(c)
397397
}
398398

399-
// ExecSyncResponse is returned from ExecSync.
400-
type ExecSyncResponse struct {
401-
Stdout []byte
402-
Stderr []byte
403-
ExitCode int32
404-
}
405-
406399
// ExecSyncError wraps command's streams, exit code and error on ExecSync error.
407400
type ExecSyncError struct {
408401
Stdout bytes.Buffer
@@ -414,3 +407,10 @@ type ExecSyncError struct {
414407
func (e *ExecSyncError) Error() string {
415408
return fmt.Sprintf("command error: %+v, stdout: %s, stderr: %s, exit code %d", e.Err, e.Stdout.Bytes(), e.Stderr.Bytes(), e.ExitCode)
416409
}
410+
411+
// ExecSyncResponse is returned from ExecSync.
412+
type ExecSyncResponse struct {
413+
Stdout []byte
414+
Stderr []byte
415+
ExitCode int32
416+
}

internal/oci/runtime_oci.go

Lines changed: 200 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -320,80 +320,242 @@ func (r *runtimeOCI) ExecSyncContainer(ctx context.Context, c *Container, comman
320320
return nil, nil
321321
}
322322

323-
processFile, err := prepareProcessExec(c, command, c.terminal)
323+
pidFile, parentPipe, childPipe, err := prepareExec()
324324
if err != nil {
325325
return nil, &ExecSyncError{
326326
ExitCode: -1,
327327
Err: err,
328328
}
329329
}
330-
defer os.RemoveAll(processFile)
330+
defer parentPipe.Close()
331+
defer func() {
332+
if e := os.Remove(pidFile); e != nil {
333+
log.Warnf(ctx, "Could not remove temporary PID file %s", pidFile)
334+
}
335+
}()
331336

332-
pidDir, err := ioutil.TempDir("", "pidfile")
337+
logFile, err := ioutil.TempFile("", "crio-log-"+c.id)
333338
if err != nil {
334-
return nil, err
339+
return nil, &ExecSyncError{
340+
ExitCode: -1,
341+
Err: err,
342+
}
335343
}
336-
defer os.RemoveAll(pidDir)
344+
logFile.Close()
337345

338-
pidFile := filepath.Join(pidDir, c.id)
346+
logPath := logFile.Name()
347+
defer func() {
348+
os.RemoveAll(logPath)
349+
}()
339350

340-
cmd := r.constructExecCommand(ctx, c, processFile, pidFile)
341-
cmd.SysProcAttr = sysProcAttrPlatform()
351+
args := []string{
352+
"-c", c.id,
353+
"-n", c.name,
354+
"-r", r.path,
355+
"-p", pidFile,
356+
"-e",
357+
"-l", logPath,
358+
"--socket-dir-path", r.config.ContainerAttachSocketDir,
359+
"--log-level", logrus.GetLevel().String(),
360+
}
361+
362+
if r.config.ConmonSupportsSync() {
363+
args = append(args, "--sync")
364+
}
365+
if c.terminal {
366+
args = append(args, "-t")
367+
}
368+
if timeout > 0 {
369+
args = append(args, "-T", fmt.Sprintf("%d", timeout))
370+
}
371+
372+
processFile, err := prepareProcessExec(c, command, c.terminal)
373+
if err != nil {
374+
return nil, &ExecSyncError{
375+
ExitCode: -1,
376+
Err: err,
377+
}
378+
}
379+
defer os.RemoveAll(processFile)
380+
381+
args = append(args,
382+
"--exec-process-spec", processFile,
383+
"--runtime-arg", fmt.Sprintf("%s=%s", rootFlag, r.root))
384+
385+
cmd := exec.Command(r.config.Conmon, args...) // nolint: gosec
342386

343387
var stdoutBuf, stderrBuf bytes.Buffer
344388
cmd.Stdout = &stdoutBuf
345389
cmd.Stderr = &stderrBuf
390+
cmd.ExtraFiles = append(cmd.ExtraFiles, childPipe)
391+
// 0, 1 and 2 are stdin, stdout and stderr
392+
cmd.Env = r.config.ConmonEnv
393+
cmd.Env = append(cmd.Env, fmt.Sprintf("_OCI_SYNCPIPE=%d", 3))
394+
if v, found := os.LookupEnv("XDG_RUNTIME_DIR"); found {
395+
cmd.Env = append(cmd.Env, fmt.Sprintf("XDG_RUNTIME_DIR=%s", v))
396+
}
346397

347398
err = cmd.Start()
348399
if err != nil {
349-
return nil, err
400+
childPipe.Close()
401+
return nil, &ExecSyncError{
402+
Stdout: stdoutBuf,
403+
Stderr: stderrBuf,
404+
ExitCode: -1,
405+
Err: err,
406+
}
350407
}
351408

352-
// wait till the command is done
353-
done := make(chan error, 1)
354-
go func() {
355-
done <- cmd.Wait()
356-
}()
357-
358-
if timeout > 0 {
359-
select {
360-
case <-time.After(time.Second * time.Duration(timeout)):
361-
// Ensure the process is not left behind
362-
killContainerExecProcess(ctx, pidFile, cmd)
363-
364-
// Make sure the runtime process has been cleaned up
365-
<-done
366-
367-
// If the command timed out, we should return an ExecSyncResponse with a non-zero exit code because
368-
// the prober code in the kubelet checks for it. If we return a custom error,
369-
// then the probes transition into Unknown status and the container isn't restarted as expected.
409+
// We don't need childPipe on the parent side
410+
childPipe.Close()
370411

412+
// first, wait till the command is done
413+
waitErr := cmd.Wait()
414+
415+
// regardless of what is in waitErr
416+
// we should attempt to decode the output of the parent pipe
417+
// this allows us to catch TimedOutMessage, which will cause waitErr to not be nil
418+
var ec *exitCodeInfo
419+
decodeErr := json.NewDecoder(parentPipe).Decode(&ec)
420+
if decodeErr == nil {
421+
log.Debugf(ctx, "Received container exit code: %v, message: %s", ec.ExitCode, ec.Message)
422+
423+
// When we timeout the command in conmon then we should return
424+
// an ExecSyncResponse with a non-zero exit code because
425+
// the prober code in the kubelet checks for it. If we return
426+
// a custom error, then the probes transition into Unknown status
427+
// and the container isn't restarted as expected.
428+
if ec.ExitCode == -1 && ec.Message == conmonconfig.TimedOutMessage {
371429
return &ExecSyncResponse{
372430
Stderr: []byte(conmonconfig.TimedOutMessage),
373431
ExitCode: -1,
374432
}, nil
375-
case err = <-done:
376-
break
377433
}
378-
} else {
379-
err = <-done
380434
}
381435

382-
// gather exit code from err
383-
exitCode := int32(0)
436+
if waitErr != nil {
437+
// if we aren't a ExitError, some I/O problems probably occurred
438+
if _, ok := waitErr.(*exec.ExitError); !ok {
439+
return nil, &ExecSyncError{
440+
Stdout: stdoutBuf,
441+
Stderr: stderrBuf,
442+
ExitCode: -1,
443+
Err: waitErr,
444+
}
445+
}
446+
}
447+
448+
if decodeErr != nil {
449+
return nil, &ExecSyncError{
450+
Stdout: stdoutBuf,
451+
Stderr: stderrBuf,
452+
ExitCode: -1,
453+
Err: decodeErr,
454+
}
455+
}
456+
457+
if ec.ExitCode == -1 {
458+
return nil, &ExecSyncError{
459+
Stdout: stdoutBuf,
460+
Stderr: stderrBuf,
461+
ExitCode: -1,
462+
Err: fmt.Errorf(ec.Message),
463+
}
464+
}
465+
466+
// The actual logged output is not the same as stdoutBuf and stderrBuf,
467+
// which are used for getting error information. For the actual
468+
// ExecSyncResponse we have to read the logfile.
469+
// XXX: Currently runC dups the same console over both stdout and stderr,
470+
// so we can't differentiate between the two.
471+
logBytes, err := ioutil.ReadFile(logPath)
384472
if err != nil {
385-
if exitError, ok := err.(*exec.ExitError); ok {
386-
exitCode = int32(exitError.ExitCode())
473+
return nil, &ExecSyncError{
474+
Stdout: stdoutBuf,
475+
Stderr: stderrBuf,
476+
ExitCode: -1,
477+
Err: err,
387478
}
388479
}
389480

481+
// We have to parse the log output into {stdout, stderr} buffers.
482+
stdoutBytes, stderrBytes := parseLog(ctx, logBytes)
390483
return &ExecSyncResponse{
391-
Stdout: stdoutBuf.Bytes(),
392-
Stderr: stderrBuf.Bytes(),
393-
ExitCode: exitCode,
484+
Stdout: stdoutBytes,
485+
Stderr: stderrBytes,
486+
ExitCode: ec.ExitCode,
394487
}, nil
395488
}
396489

490+
// exitCodeInfo is used to return the monitored process exit code to the daemon
491+
type exitCodeInfo struct {
492+
ExitCode int32 `json:"exit_code"`
493+
Message string `json:"message,omitempty"`
494+
}
495+
496+
func parseLog(ctx context.Context, l []byte) (stdout, stderr []byte) {
497+
// Split the log on newlines, which is what separates entries.
498+
lines := bytes.SplitAfter(l, []byte{'\n'})
499+
for _, line := range lines {
500+
// Ignore empty lines.
501+
if len(line) == 0 {
502+
continue
503+
}
504+
505+
// The format of log lines is "DATE pipe LogTag REST".
506+
parts := bytes.SplitN(line, []byte{' '}, 4)
507+
if len(parts) < 4 {
508+
// Ignore the line if it's formatted incorrectly, but complain
509+
// about it so it can be debugged.
510+
log.Warnf(ctx, "Hit invalid log format: %q", string(line))
511+
continue
512+
}
513+
514+
pipe := string(parts[1])
515+
content := parts[3]
516+
517+
linetype := string(parts[2])
518+
if linetype == "P" {
519+
contentLen := len(content)
520+
if contentLen > 0 && content[contentLen-1] == '\n' {
521+
content = content[:contentLen-1]
522+
}
523+
}
524+
525+
switch pipe {
526+
case "stdout":
527+
stdout = append(stdout, content...)
528+
case "stderr":
529+
stderr = append(stderr, content...)
530+
default:
531+
// Complain about unknown pipes.
532+
log.Warnf(ctx, "Hit invalid log format [unknown pipe %s]: %q", pipe, string(line))
533+
continue
534+
}
535+
}
536+
537+
return stdout, stderr
538+
}
539+
540+
func prepareExec() (pidFileName string, parentPipe, childPipe *os.File, _ error) {
541+
var err error
542+
parentPipe, childPipe, err = os.Pipe()
543+
if err != nil {
544+
return "", nil, nil, err
545+
}
546+
547+
pidFile, err := ioutil.TempFile("", "pidfile")
548+
if err != nil {
549+
parentPipe.Close()
550+
childPipe.Close()
551+
return "", nil, nil, err
552+
}
553+
pidFile.Close()
554+
pidFileName = pidFile.Name()
555+
556+
return pidFileName, parentPipe, childPipe, nil
557+
}
558+
397559
func (r *runtimeOCI) constructExecCommand(ctx context.Context, c *Container, processFile, pidFile string) *exec.Cmd {
398560
args := []string{rootFlag, r.root, "exec"}
399561
if pidFile != "" {

test/ctr.bats

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -808,6 +808,15 @@ function wait_until_exit() {
808808
[[ "$output" == *"Attempt: 1"* ]]
809809
}
810810

811+
@test "ctr execsync should succeed if container has a terminal" {
812+
start_crio
813+
814+
jq ' .tty = true' "$TESTDATA"/container_sleep.json > "$newconfig"
815+
816+
ctr_id=$(crictl run "$newconfig" "$TESTDATA"/sandbox_config.json)
817+
crictl exec --sync "$ctr_id" /bin/sh -c "[[ -t 1 ]]"
818+
}
819+
811820
@test "ctr execsync conflicting with conmon flags parsing" {
812821
start_crio
813822
run crictl runp "$TESTDATA"/sandbox_config.json

0 commit comments

Comments
 (0)