diff --git a/init.go b/init.go index 2f44ce8a555..7933630635b 100644 --- a/init.go +++ b/init.go @@ -2,41 +2,15 @@ package main import ( "os" - "runtime" - "strconv" "github.com/opencontainers/runc/libcontainer" _ "github.com/opencontainers/runc/libcontainer/nsenter" - "github.com/sirupsen/logrus" ) func init() { if len(os.Args) > 1 && os.Args[1] == "init" { // This is the golang entry point for runc init, executed // before main() but after libcontainer/nsenter's nsexec(). - runtime.GOMAXPROCS(1) - runtime.LockOSThread() - - level, err := strconv.Atoi(os.Getenv("_LIBCONTAINER_LOGLEVEL")) - if err != nil { - panic(err) - } - - logPipeFd, err := strconv.Atoi(os.Getenv("_LIBCONTAINER_LOGPIPE")) - if err != nil { - panic(err) - } - - logrus.SetLevel(logrus.Level(level)) - logrus.SetOutput(os.NewFile(uintptr(logPipeFd), "logpipe")) - logrus.SetFormatter(new(logrus.JSONFormatter)) - logrus.Debug("child process in init()") - - if err := libcontainer.StartInitialization(); err != nil { - // as the error is sent back to the parent there is no need to log - // or write it to stderr because the parent process will handle this - os.Exit(1) - } - panic("libcontainer: container init failed to exec") + libcontainer.Init() } } diff --git a/libcontainer/README.md b/libcontainer/README.md index 303dd6125aa..0a4ae96c9b3 100644 --- a/libcontainer/README.md +++ b/libcontainer/README.md @@ -23,22 +23,9 @@ function as the entry of "bootstrap". In addition to the go init function the early stage bootstrap is handled by importing [nsenter](https://github.com/opencontainers/runc/blob/master/libcontainer/nsenter/README.md). -```go -import ( - _ "github.com/opencontainers/runc/libcontainer/nsenter" -) - -func init() { - if len(os.Args) > 1 && os.Args[1] == "init" { - runtime.GOMAXPROCS(1) - runtime.LockOSThread() - if err := libcontainer.StartInitialization(); err != nil { - logrus.Fatal(err) - } - panic("--this line should have never been executed, congratulations--") - } -} -``` +For details on how runc implements such "init", see +[init.go](https://github.com/opencontainers/runc/blob/master/init.go) +and [libcontainer/init_linux.go](https://github.com/opencontainers/runc/blob/master/libcontainer/init_linux.go). Then to create a container you first have to create a configuration struct describing how the container is to be created. A sample would look similar to this: diff --git a/libcontainer/container_linux.go b/libcontainer/container_linux.go index 2f0a6c64166..1756c384cad 100644 --- a/libcontainer/container_linux.go +++ b/libcontainer/container_linux.go @@ -501,9 +501,10 @@ func (c *Container) commandTemplate(p *Process, childInitPipe *os.File, childLog cmd.ExtraFiles = append(cmd.ExtraFiles, childLogPipe) cmd.Env = append(cmd.Env, - "_LIBCONTAINER_LOGPIPE="+strconv.Itoa(stdioFdCount+len(cmd.ExtraFiles)-1), - "_LIBCONTAINER_LOGLEVEL="+p.LogLevel, - ) + "_LIBCONTAINER_LOGPIPE="+strconv.Itoa(stdioFdCount+len(cmd.ExtraFiles)-1)) + if p.LogLevel != "" { + cmd.Env = append(cmd.Env, "_LIBCONTAINER_LOGLEVEL="+p.LogLevel) + } // NOTE: when running a container with no PID namespace and the parent process spawning the container is // PID1 the pdeathsig is being delivered to the container's init process by the kernel for some reason diff --git a/libcontainer/init_linux.go b/libcontainer/init_linux.go index 42cae1ccb65..2e2d00ff83a 100644 --- a/libcontainer/init_linux.go +++ b/libcontainer/init_linux.go @@ -8,6 +8,7 @@ import ( "io" "net" "os" + "runtime" "runtime/debug" "strconv" "strings" @@ -84,34 +85,74 @@ type initConfig struct { Cgroup2Path string `json:"cgroup2_path,omitempty"` } -// StartInitialization loads a container by opening the pipe fd from the parent -// to read the configuration and state. This is a low level implementation -// detail of the reexec and should not be consumed externally. -func StartInitialization() (retErr error) { +// Init is part of "runc init" implementation. +func Init() { + runtime.GOMAXPROCS(1) + runtime.LockOSThread() + + if err := startInitialization(); err != nil { + // If the error is returned, it was not communicated + // back to the parent (which is not a common case), + // so print it to stderr here as a last resort. + // + // Do not use logrus as we are not sure if it has been + // set up yet, but most important, if the parent is + // alive (and its log forwarding is working). + fmt.Fprintln(os.Stderr, err) + } + // Normally, StartInitialization() never returns, meaning + // if we are here, it had failed. + os.Exit(1) +} + +// Normally, this function does not return. If it returns, with or without an +// error, it means the initialization has failed. If the error is returned, +// it means the error can not be communicated back to the parent. +func startInitialization() (retErr error) { // Get the INITPIPE. envInitPipe := os.Getenv("_LIBCONTAINER_INITPIPE") pipefd, err := strconv.Atoi(envInitPipe) if err != nil { - err = fmt.Errorf("unable to convert _LIBCONTAINER_INITPIPE: %w", err) - logrus.Error(err) - return err + return fmt.Errorf("unable to convert _LIBCONTAINER_INITPIPE: %w", err) } pipe := os.NewFile(uintptr(pipefd), "pipe") defer pipe.Close() defer func() { - // We have an error during the initialization of the container's init, - // send it back to the parent process in the form of an initError. + // If this defer is ever called, this means initialization has failed. + // Send the error back to the parent process in the form of an initError. if err := writeSync(pipe, procError); err != nil { - fmt.Fprintln(os.Stderr, retErr) + fmt.Fprintln(os.Stderr, err) return } if err := utils.WriteJSON(pipe, &initError{Message: retErr.Error()}); err != nil { - fmt.Fprintln(os.Stderr, retErr) + fmt.Fprintln(os.Stderr, err) return } + // The error is sent, no need to also return it (or it will be reported twice). + retErr = nil }() + // Set up logging. This is used rarely, and mostly for init debugging. + + // Passing log level is optional; currently libcontainer/integration does not do it. + if levelStr := os.Getenv("_LIBCONTAINER_LOGLEVEL"); levelStr != "" { + logLevel, err := strconv.Atoi(levelStr) + if err != nil { + return fmt.Errorf("unable to convert _LIBCONTAINER_LOGLEVEL: %w", err) + } + logrus.SetLevel(logrus.Level(logLevel)) + } + + logFD, err := strconv.Atoi(os.Getenv("_LIBCONTAINER_LOGPIPE")) + if err != nil { + return fmt.Errorf("unable to convert _LIBCONTAINER_LOGPIPE: %w", err) + } + + logrus.SetOutput(os.NewFile(uintptr(logFD), "logpipe")) + logrus.SetFormatter(new(logrus.JSONFormatter)) + logrus.Debug("child process in init()") + // Only init processes have FIFOFD. fifofd := -1 envInitType := os.Getenv("_LIBCONTAINER_INITTYPE") @@ -133,12 +174,6 @@ func StartInitialization() (retErr error) { defer consoleSocket.Close() } - logPipeFdStr := os.Getenv("_LIBCONTAINER_LOGPIPE") - logPipeFd, err := strconv.Atoi(logPipeFdStr) - if err != nil { - return fmt.Errorf("unable to convert _LIBCONTAINER_LOGPIPE: %w", err) - } - // Get mount files (O_PATH). mountSrcFds, err := parseFdsFromEnv("_LIBCONTAINER_MOUNT_FDS") if err != nil { @@ -166,7 +201,7 @@ func StartInitialization() (retErr error) { }() // If init succeeds, it will not return, hence none of the defers will be called. - return containerInit(it, pipe, consoleSocket, fifofd, logPipeFd, mountFds{sourceFds: mountSrcFds, idmapFds: idmapFds}) + return containerInit(it, pipe, consoleSocket, fifofd, logFD, mountFds{sourceFds: mountSrcFds, idmapFds: idmapFds}) } func containerInit(t initType, pipe *os.File, consoleSocket *os.File, fifoFd, logFd int, mountFds mountFds) error { diff --git a/libcontainer/integration/init_test.go b/libcontainer/integration/init_test.go index efcbe72b0f1..a79ffe5c36f 100644 --- a/libcontainer/integration/init_test.go +++ b/libcontainer/integration/init_test.go @@ -1,9 +1,7 @@ package integration import ( - "fmt" "os" - "runtime" "testing" "github.com/opencontainers/runc/libcontainer" @@ -14,20 +12,9 @@ import ( // Same as ../../init.go but for libcontainer/integration. func init() { - if len(os.Args) < 2 || os.Args[1] != "init" { - return + if len(os.Args) > 1 && os.Args[1] == "init" { + libcontainer.Init() } - // This is the golang entry point for runc init, executed - // before TestMain() but after libcontainer/nsenter's nsexec(). - runtime.GOMAXPROCS(1) - runtime.LockOSThread() - if err := libcontainer.StartInitialization(); err != nil { - // logrus is not initialized - fmt.Fprintln(os.Stderr, err) - } - // Normally, StartInitialization() never returns, meaning - // if we are here, it had failed. - os.Exit(1) } func TestMain(m *testing.M) {