libct/init: unify init, fix its error logic

This commit does two things:

1. Consolidate StartInitialization calling logic into Init().
2. Fix init error handling logic.

The main issues at hand are:
- the "unable to convert _LIBCONTAINER_INITPIPE" error from
  StartInitialization is never shown;
- errors from WriteSync and WriteJSON are never shown;
- the StartInit calling code is triplicated;
- using panic is questionable.

Generally, our goals are:
 - if there's any error, do our best to show it;
 - but only show each error once;
 - simplify the code, unify init implementations.

Signed-off-by: Kir Kolyshkin <kolyshkin@gmail.com>
This commit is contained in:
Kir Kolyshkin
2023-04-19 14:52:30 -07:00
parent 789a73db22
commit 883aef789b
4 changed files with 37 additions and 49 deletions
+1 -10
View File
@@ -2,7 +2,6 @@ package main
import ( import (
"os" "os"
"runtime"
"github.com/opencontainers/runc/libcontainer" "github.com/opencontainers/runc/libcontainer"
_ "github.com/opencontainers/runc/libcontainer/nsenter" _ "github.com/opencontainers/runc/libcontainer/nsenter"
@@ -12,14 +11,6 @@ func init() {
if len(os.Args) > 1 && os.Args[1] == "init" { if len(os.Args) > 1 && os.Args[1] == "init" {
// This is the golang entry point for runc init, executed // This is the golang entry point for runc init, executed
// before main() but after libcontainer/nsenter's nsexec(). // before main() but after libcontainer/nsenter's nsexec().
runtime.GOMAXPROCS(1) libcontainer.Init()
runtime.LockOSThread()
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")
} }
} }
+3 -16
View File
@@ -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 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). [nsenter](https://github.com/opencontainers/runc/blob/master/libcontainer/nsenter/README.md).
```go For details on how runc implements such "init", see
import ( [init.go](https://github.com/opencontainers/runc/blob/master/init.go)
_ "github.com/opencontainers/runc/libcontainer/nsenter" and [libcontainer/init_linux.go](https://github.com/opencontainers/runc/blob/master/libcontainer/init_linux.go).
)
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--")
}
}
```
Then to create a container you first have to create a configuration 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: struct describing how the container is to be created. A sample would look similar to this:
+31 -8
View File
@@ -8,6 +8,7 @@ import (
"io" "io"
"net" "net"
"os" "os"
"runtime"
"runtime/debug" "runtime/debug"
"strconv" "strconv"
"strings" "strings"
@@ -84,10 +85,30 @@ type initConfig struct {
Cgroup2Path string `json:"cgroup2_path,omitempty"` Cgroup2Path string `json:"cgroup2_path,omitempty"`
} }
// StartInitialization loads a container by opening the pipe fd from the parent // Init is part of "runc init" implementation.
// to read the configuration and state. This is a low level implementation func Init() {
// detail of the reexec and should not be consumed externally. runtime.GOMAXPROCS(1)
func StartInitialization() (retErr error) { 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. // Get the INITPIPE.
envInitPipe := os.Getenv("_LIBCONTAINER_INITPIPE") envInitPipe := os.Getenv("_LIBCONTAINER_INITPIPE")
pipefd, err := strconv.Atoi(envInitPipe) pipefd, err := strconv.Atoi(envInitPipe)
@@ -98,16 +119,18 @@ func StartInitialization() (retErr error) {
defer pipe.Close() defer pipe.Close()
defer func() { defer func() {
// We have an error during the initialization of the container's init, // If this defer is ever called, this means initialization has failed.
// send it back to the parent process in the form of an initError. // Send the error back to the parent process in the form of an initError.
if err := writeSync(pipe, procError); err != nil { if err := writeSync(pipe, procError); err != nil {
fmt.Fprintln(os.Stderr, retErr) fmt.Fprintln(os.Stderr, err)
return return
} }
if err := utils.WriteJSON(pipe, &initError{Message: retErr.Error()}); err != nil { if err := utils.WriteJSON(pipe, &initError{Message: retErr.Error()}); err != nil {
fmt.Fprintln(os.Stderr, retErr) fmt.Fprintln(os.Stderr, err)
return 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. // Set up logging. This is used rarely, and mostly for init debugging.
+2 -15
View File
@@ -1,9 +1,7 @@
package integration package integration
import ( import (
"fmt"
"os" "os"
"runtime"
"testing" "testing"
"github.com/opencontainers/runc/libcontainer" "github.com/opencontainers/runc/libcontainer"
@@ -14,20 +12,9 @@ import (
// Same as ../../init.go but for libcontainer/integration. // Same as ../../init.go but for libcontainer/integration.
func init() { func init() {
if len(os.Args) < 2 || os.Args[1] != "init" { if len(os.Args) > 1 && os.Args[1] == "init" {
return 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) { func TestMain(m *testing.M) {