Skip to content

Commit 8e8a907

Browse files
marcovgeorgethebeatle
authored andcommitted
Support for logging from children processes Add support for children processes logging (including nsexec). A pipe is used to send logs from children to parent in JSON. The JSON format used is the same used by logrus JSON formatted, i.e. children process can use standard logrus APIs. Signed-off-by: Marco Vedovati <mvedovati@suse.com> Co-authored-by: Georgi Sabev <georgethebeatle@gmail.com>
1 parent 029124d commit 8e8a907

File tree

9 files changed

+257
-6
lines changed

9 files changed

+257
-6
lines changed

init.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,31 @@ package main
33
import (
44
"os"
55
"runtime"
6+
"strconv"
67

78
"github.com/opencontainers/runc/libcontainer"
89
_ "github.com/opencontainers/runc/libcontainer/nsenter"
10+
"github.com/sirupsen/logrus"
911
"github.com/urfave/cli"
1012
)
1113

1214
func init() {
1315
if len(os.Args) > 1 && os.Args[1] == "init" {
1416
runtime.GOMAXPROCS(1)
1517
runtime.LockOSThread()
18+
19+
// in child process, we need to retrieve the log pipe
20+
envLogPipe := os.Getenv("_LIBCONTAINER_LOGPIPE")
21+
logPipeFd, err := strconv.Atoi(envLogPipe)
22+
23+
if err != nil {
24+
return
25+
}
26+
logPipe := os.NewFile(uintptr(logPipeFd), "logpipe")
27+
logrus.SetOutput(logPipe)
28+
logrus.SetFormatter(&logrus.JSONFormatter{})
29+
logrus.SetLevel(logrus.DebugLevel)
30+
logrus.Debug("child process in init()")
1631
}
1732
}
1833

libcontainer/container_linux.go

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,7 @@ func (c *linuxContainer) start(process *Process) error {
337337
if err != nil {
338338
return newSystemErrorWithCause(err, "creating new parent process")
339339
}
340+
parent.getChildLogs()
340341
if err := parent.start(); err != nil {
341342
// terminate the process to ensure that it properly is reaped.
342343
if err := ignoreTerminateErrors(parent.terminate()); err != nil {
@@ -442,12 +443,19 @@ func (c *linuxContainer) newParentProcess(p *Process) (parentProcess, error) {
442443
if err != nil {
443444
return nil, newSystemErrorWithCause(err, "creating new init pipe")
444445
}
445-
cmd, err := c.commandTemplate(p, childPipe)
446+
447+
r, w, err := os.Pipe()
448+
if err != nil {
449+
return nil, fmt.Errorf("Unable to create the log pipe: %s", err)
450+
}
451+
logPipe := pipePair{r, w}
452+
453+
cmd, err := c.commandTemplate(p, childPipe, logPipe.w)
446454
if err != nil {
447455
return nil, newSystemErrorWithCause(err, "creating new command template")
448456
}
449457
if !p.Init {
450-
return c.newSetnsProcess(p, cmd, parentPipe, childPipe)
458+
return c.newSetnsProcess(p, cmd, parentPipe, childPipe, &logPipe)
451459
}
452460

453461
// We only set up fifoFd if we're not doing a `runc exec`. The historic
@@ -458,10 +466,10 @@ func (c *linuxContainer) newParentProcess(p *Process) (parentProcess, error) {
458466
if err := c.includeExecFifo(cmd); err != nil {
459467
return nil, newSystemErrorWithCause(err, "including execfifo in cmd.Exec setup")
460468
}
461-
return c.newInitProcess(p, cmd, parentPipe, childPipe)
469+
return c.newInitProcess(p, cmd, parentPipe, childPipe, &logPipe)
462470
}
463471

464-
func (c *linuxContainer) commandTemplate(p *Process, childPipe *os.File) (*exec.Cmd, error) {
472+
func (c *linuxContainer) commandTemplate(p *Process, childPipe *os.File, logPipe *os.File) (*exec.Cmd, error) {
465473
cmd := exec.Command(c.initPath, c.initArgs[1:]...)
466474
cmd.Args[0] = c.initArgs[0]
467475
cmd.Stdin = p.Stdin
@@ -484,6 +492,12 @@ func (c *linuxContainer) commandTemplate(p *Process, childPipe *os.File) (*exec.
484492
fmt.Sprintf("_LIBCONTAINER_INITPIPE=%d", stdioFdCount+len(cmd.ExtraFiles)-1),
485493
fmt.Sprintf("_LIBCONTAINER_STATEDIR=%s", c.root),
486494
)
495+
496+
cmd.ExtraFiles = append(cmd.ExtraFiles, logPipe)
497+
cmd.Env = append(cmd.Env,
498+
fmt.Sprintf("_LIBCONTAINER_LOGPIPE=%d", stdioFdCount+len(cmd.ExtraFiles)-1),
499+
)
500+
487501
// NOTE: when running a container with no PID namespace and the parent process spawning the container is
488502
// PID1 the pdeathsig is being delivered to the container's init process by the kernel for some reason
489503
// even with the parent still running.
@@ -493,7 +507,7 @@ func (c *linuxContainer) commandTemplate(p *Process, childPipe *os.File) (*exec.
493507
return cmd, nil
494508
}
495509

496-
func (c *linuxContainer) newInitProcess(p *Process, cmd *exec.Cmd, parentPipe, childPipe *os.File) (*initProcess, error) {
510+
func (c *linuxContainer) newInitProcess(p *Process, cmd *exec.Cmd, parentPipe, childPipe *os.File, logPipe *pipePair) (*initProcess, error) {
497511
cmd.Env = append(cmd.Env, "_LIBCONTAINER_INITTYPE="+string(initStandard))
498512
nsMaps := make(map[configs.NamespaceType]string)
499513
for _, ns := range c.config.Namespaces {
@@ -510,6 +524,7 @@ func (c *linuxContainer) newInitProcess(p *Process, cmd *exec.Cmd, parentPipe, c
510524
cmd: cmd,
511525
childPipe: childPipe,
512526
parentPipe: parentPipe,
527+
logPipe: *logPipe,
513528
manager: c.cgroupManager,
514529
intelRdtManager: c.intelRdtManager,
515530
config: c.newInitConfig(p),
@@ -522,7 +537,7 @@ func (c *linuxContainer) newInitProcess(p *Process, cmd *exec.Cmd, parentPipe, c
522537
return init, nil
523538
}
524539

525-
func (c *linuxContainer) newSetnsProcess(p *Process, cmd *exec.Cmd, parentPipe, childPipe *os.File) (*setnsProcess, error) {
540+
func (c *linuxContainer) newSetnsProcess(p *Process, cmd *exec.Cmd, parentPipe, childPipe *os.File, logPipe *pipePair) (*setnsProcess, error) {
526541
cmd.Env = append(cmd.Env, "_LIBCONTAINER_INITTYPE="+string(initSetns))
527542
state, err := c.currentState()
528543
if err != nil {
@@ -541,6 +556,7 @@ func (c *linuxContainer) newSetnsProcess(p *Process, cmd *exec.Cmd, parentPipe,
541556
intelRdtPath: state.IntelRdtPath,
542557
childPipe: childPipe,
543558
parentPipe: parentPipe,
559+
logPipe: *logPipe,
544560
config: c.newInitConfig(p),
545561
process: p,
546562
bootstrapData: data,

libcontainer/logs.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package libcontainer
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io"
7+
"os"
8+
9+
"github.com/sirupsen/logrus"
10+
)
11+
12+
func forwardLogs(p *os.File) {
13+
defer p.Close()
14+
15+
type jsonLog struct {
16+
Level string `json:"level"`
17+
Msg string `json:"msg"`
18+
}
19+
20+
dec := json.NewDecoder(p)
21+
for {
22+
var jl jsonLog
23+
24+
type logFuncTable map[logrus.Level]func(args ...interface{})
25+
logMapping := logFuncTable{
26+
logrus.PanicLevel: logrus.Panic,
27+
logrus.FatalLevel: logrus.Fatal,
28+
logrus.ErrorLevel: logrus.Error,
29+
logrus.WarnLevel: logrus.Warn,
30+
logrus.InfoLevel: logrus.Info,
31+
logrus.DebugLevel: logrus.Debug,
32+
}
33+
34+
err := dec.Decode(&jl)
35+
if err != nil {
36+
if err == io.EOF {
37+
logrus.Debug("child pipe closed")
38+
return
39+
}
40+
logrus.Errorf("json logs decoding error: %+v", err)
41+
return
42+
}
43+
44+
lvl, err := logrus.ParseLevel(jl.Level)
45+
if err != nil {
46+
fmt.Printf("parsing error\n")
47+
}
48+
if logMapping[lvl] != nil {
49+
logMapping[lvl](jl.Msg)
50+
}
51+
}
52+
}
53+
54+
func rawLogs(p *os.File) {
55+
defer p.Close()
56+
57+
data := make([]byte, 128)
58+
for {
59+
_, err := p.Read(data)
60+
61+
if err != nil {
62+
if err == io.EOF {
63+
logrus.Debug("child pipe closed")
64+
return
65+
}
66+
return
67+
}
68+
fmt.Printf("Read data: %s\n", string(data))
69+
}
70+
}

libcontainer/nsenter/nsenter_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ type pid struct {
2121
Pid int `json:"Pid"`
2222
}
2323

24+
type logentry struct {
25+
Msg string
26+
Level string
27+
}
28+
2429
func TestNsenterValidPaths(t *testing.T) {
2530
args := []string{"nsenter-exec"}
2631
parent, child, err := newPipe()
@@ -159,6 +164,60 @@ func TestNsenterIncorrectPathType(t *testing.T) {
159164
}
160165
}
161166

167+
func TestNsenterChildLogging(t *testing.T) {
168+
args := []string{"nsenter-exec"}
169+
parent, child, err := newPipe()
170+
logread, logwrite, err := os.Pipe()
171+
if err != nil {
172+
t.Fatalf("failed to create pipe %v", err)
173+
}
174+
175+
namespaces := []string{
176+
// join pid ns of the current process
177+
fmt.Sprintf("pid:/proc/%d/ns/pid", os.Getpid()),
178+
}
179+
cmd := &exec.Cmd{
180+
Path: os.Args[0],
181+
Args: args,
182+
ExtraFiles: []*os.File{child, logwrite},
183+
Env: []string{"_LIBCONTAINER_INITPIPE=3", "_LIBCONTAINER_LOGPIPE=4"},
184+
Stdout: os.Stdout,
185+
Stderr: os.Stderr,
186+
}
187+
188+
if err := cmd.Start(); err != nil {
189+
t.Fatalf("nsenter failed to start %v", err)
190+
}
191+
// write cloneFlags
192+
r := nl.NewNetlinkRequest(int(libcontainer.InitMsg), 0)
193+
r.AddData(&libcontainer.Int32msg{
194+
Type: libcontainer.CloneFlagsAttr,
195+
Value: uint32(unix.CLONE_NEWNET),
196+
})
197+
r.AddData(&libcontainer.Bytemsg{
198+
Type: libcontainer.NsPathsAttr,
199+
Value: []byte(strings.Join(namespaces, ",")),
200+
})
201+
if _, err := io.Copy(parent, bytes.NewReader(r.Serialize())); err != nil {
202+
t.Fatal(err)
203+
}
204+
205+
logsDecoder := json.NewDecoder(logread)
206+
var logentry *logentry
207+
208+
err = logsDecoder.Decode(&logentry)
209+
if err != nil {
210+
t.Fatalf("child log: %v", err)
211+
}
212+
if logentry.Level == "" || logentry.Msg == "" {
213+
t.Fatalf("child log: empty log fileds: level=\"%s\" msg=\"%s\"", logentry.Level, logentry.Msg)
214+
}
215+
216+
if err := cmd.Wait(); err != nil {
217+
t.Fatalf("nsenter exits with a non-zero exit status")
218+
}
219+
}
220+
162221
func init() {
163222
if strings.HasPrefix(os.Args[0], "nsenter-") {
164223
os.Exit(0)

libcontainer/nsenter/nsexec.c

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,17 @@ struct nlconfig_t {
9595
size_t gidmappath_len;
9696
};
9797

98+
typedef enum {
99+
PANIC = 0,
100+
FATAL,
101+
ERROR,
102+
WARNING,
103+
INFO,
104+
DEBUG,
105+
} loglevel_t;
106+
107+
int logfd;
108+
98109
/*
99110
* List of netlink message types sent to us as part of bootstrapping the init.
100111
* These constants are defined in libcontainer/message_linux.go.
@@ -352,6 +363,22 @@ static int initpipe(void)
352363
return pipenum;
353364
}
354365

366+
static int logpipe(void)
367+
{
368+
int pipenum;
369+
char *logpipe, *endptr;
370+
371+
logpipe = getenv("_LIBCONTAINER_LOGPIPE");
372+
if (logpipe == NULL || *logpipe == '\0')
373+
return -1;
374+
375+
pipenum = strtol(logpipe, &endptr, 10);
376+
if (*endptr != '\0')
377+
bail("unable to parse _LIBCONTAINER_LOGPIPE");
378+
379+
return pipenum;
380+
}
381+
355382
/* Returns the clone(2) flag for a namespace, given the name of a namespace. */
356383
static int nsflag(char *name)
357384
{
@@ -537,6 +564,27 @@ void join_namespaces(char *nslist)
537564
/* Defined in cloned_binary.c. */
538565
extern int ensure_cloned_binary(void);
539566

567+
void write_log(loglevel_t level, const char *format, ...)
568+
{
569+
static const char *strlevel[] = {"panic", "fatal", "error", "warning", "info", "debug"};
570+
static char jsonbuffer[1024];
571+
int len;
572+
va_list args;
573+
if (logfd < 0 || level >= sizeof(strlevel) / sizeof(strlevel[0])) {
574+
return;
575+
}
576+
577+
len = snprintf(jsonbuffer, sizeof(jsonbuffer),
578+
"{\"level\":\"%s\", \"msg\": \"", strlevel[level]);
579+
580+
va_start(args, format);
581+
len += vsnprintf(&jsonbuffer[len], sizeof(jsonbuffer) - len, format, args);
582+
va_end(args);
583+
584+
len += snprintf(&jsonbuffer[len], sizeof(jsonbuffer) - len, "\"}");
585+
write(logfd, jsonbuffer, len);
586+
}
587+
540588
void nsexec(void)
541589
{
542590
int pipenum;
@@ -560,6 +608,11 @@ void nsexec(void)
560608
if (ensure_cloned_binary() < 0)
561609
bail("could not ensure we are a cloned binary");
562610

611+
/* Get the log pipe */
612+
logfd = logpipe();
613+
614+
write_log(DEBUG, "%s started", __FUNCTION__);
615+
563616
/* Parse all of the netlink configuration. */
564617
nl_parse(pipenum, &config);
565618

0 commit comments

Comments
 (0)