From f83a9138c4aa785bfe92c0eb94b23d0e72686686 Mon Sep 17 00:00:00 2001 From: Min Uk Lee Date: Tue, 4 Oct 2022 17:59:19 +0900 Subject: [PATCH] Support --pid=container:xxx for `nerdctl run` cmd - Add context and client to `setPlatformOptions()` - Add branch for platform-specific container opts - Add PIDContainer label into container's labels. It only works on linux platform. - Add reconfigPIDContainer() into container's start process. It recovers the pid namespace from its PIDContainer label. Signed-off-by: Min Uk Lee --- README.md | 2 +- cmd/nerdctl/restart_linux_test.go | 27 +++++++ cmd/nerdctl/run.go | 51 ++++++++++++- cmd/nerdctl/run_freebsd.go | 7 +- cmd/nerdctl/run_linux.go | 116 ++++++++++++++++++++++++++---- cmd/nerdctl/run_linux_test.go | 12 ++++ cmd/nerdctl/run_windows.go | 7 +- cmd/nerdctl/start.go | 41 +++++++++++ pkg/labels/labels.go | 3 + 9 files changed, 250 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 83d309d2e76..afd1c2396a8 100644 --- a/README.md +++ b/README.md @@ -372,7 +372,7 @@ Basic flags: - :whale: `--rm`: Automatically remove the container when it exits - :whale: `--pull=(always|missing|never)`: Pull image before running - Default: "missing" -- :whale: `--pid=(host)`: PID namespace to use +- :whale: `--pid=(host|container:)`: PID namespace to use - :whale: `--stop-signal`: Signal to stop a container (default "SIGTERM") - :whale: `--stop-timeout`: Timeout (in seconds) to stop a container diff --git a/cmd/nerdctl/restart_linux_test.go b/cmd/nerdctl/restart_linux_test.go index 9edfd5dba54..f25763ced96 100644 --- a/cmd/nerdctl/restart_linux_test.go +++ b/cmd/nerdctl/restart_linux_test.go @@ -17,6 +17,8 @@ package main import ( + "fmt" + "strings" "testing" "github.com/containerd/nerdctl/pkg/testutil" @@ -42,3 +44,28 @@ func TestRestart(t *testing.T) { newPid := newInspect.State.Pid assert.Assert(t, pid != newPid) } + +func TestRestartPIDContainer(t *testing.T) { + t.Parallel() + base := testutil.NewBase(t) + + baseContainerName := testutil.Identifier(t) + base.Cmd("run", "-d", "--name", baseContainerName, testutil.AlpineImage, "sleep", "infinity").Run() + defer base.Cmd("rm", "-f", baseContainerName).Run() + + sharedContainerName := testutil.Identifier(t) + base.Cmd("run", "-d", "--name", sharedContainerName, fmt.Sprintf("--pid=container:%s", baseContainerName), testutil.AlpineImage, "sleep", "infinity").Run() + defer base.Cmd("rm", "-f", sharedContainerName).Run() + + base.Cmd("restart", baseContainerName).AssertOK() + base.Cmd("restart", sharedContainerName).AssertOK() + + // output format : /proc/1/ns/pid + // example output: 4026532581 /proc/1/ns/pid + basePSResult := base.Cmd("exec", baseContainerName, "ls", "-Li", "/proc/1/ns/pid").Run() + baseOutput := strings.TrimSpace(basePSResult.Stdout()) + sharedPSResult := base.Cmd("exec", sharedContainerName, "ls", "-Li", "/proc/1/ns/pid").Run() + sharedOutput := strings.TrimSpace(sharedPSResult.Stdout()) + + assert.Equal(t, baseOutput, sharedOutput) +} diff --git a/cmd/nerdctl/run.go b/cmd/nerdctl/run.go index 1e399a6ec50..bf439655fde 100644 --- a/cmd/nerdctl/run.go +++ b/cmd/nerdctl/run.go @@ -50,6 +50,7 @@ import ( "github.com/containerd/nerdctl/pkg/netutil" "github.com/containerd/nerdctl/pkg/platformutil" "github.com/containerd/nerdctl/pkg/referenceutil" + "github.com/containerd/nerdctl/pkg/rootlessutil" "github.com/containerd/nerdctl/pkg/strutil" "github.com/containerd/nerdctl/pkg/taskutil" dopts "github.com/docker/cli/opts" @@ -431,7 +432,7 @@ func createContainer(cmd *cobra.Command, ctx context.Context, client *containerd oci.WithDefaultSpec(), ) - opts, err = setPlatformOptions(opts, cmd, id) + opts, err = setPlatformOptions(ctx, opts, cmd, client, id) if err != nil { return nil, nil, err } @@ -669,6 +670,11 @@ func createContainer(cmd *cobra.Command, ctx context.Context, client *containerd spec := containerd.WithSpec(&s, opts...) cOpts = append(cOpts, spec) + cOpts, err = setPlatformContainerOptions(ctx, cOpts, cmd, client, id) + if err != nil { + return nil, nil, err + } + container, err := client.NewContainer(ctx, id, cOpts...) if err != nil { gcContainer := func() { @@ -1141,3 +1147,46 @@ func parseEnvVars(paths []string) ([]string, error) { } return vars, nil } + +func generateSharingPIDOpts(ctx context.Context, targetCon containerd.Container) ([]oci.SpecOpts, error) { + opts := make([]oci.SpecOpts, 0) + + task, err := targetCon.Task(ctx, nil) + if err != nil { + return nil, err + } + status, err := task.Status(ctx) + if err != nil { + return nil, err + } + + if status.Status != containerd.Running { + return nil, fmt.Errorf("shared container is not running") + } + + spec, err := targetCon.Spec(ctx) + if err != nil { + return nil, err + } + + isHost := true + for _, n := range spec.Linux.Namespaces { + if n.Type == specs.PIDNamespace { + isHost = false + } + } + if isHost { + opts = append(opts, oci.WithHostNamespace(specs.PIDNamespace)) + if rootlessutil.IsRootless() { + opts = append(opts, withBindMountHostProcfs) + } + } else { + ns := specs.LinuxNamespace{ + Type: specs.PIDNamespace, + Path: fmt.Sprintf("/proc/%d/ns/pid", task.Pid()), + } + opts = append(opts, oci.WithLinuxNamespace(ns)) + } + + return opts, nil +} diff --git a/cmd/nerdctl/run_freebsd.go b/cmd/nerdctl/run_freebsd.go index d4717bd4887..3e2ed6723da 100644 --- a/cmd/nerdctl/run_freebsd.go +++ b/cmd/nerdctl/run_freebsd.go @@ -19,6 +19,7 @@ package main import ( "context" + "github.com/containerd/containerd" "github.com/containerd/containerd/containers" "github.com/containerd/containerd/oci" "github.com/spf13/cobra" @@ -38,6 +39,10 @@ func runShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]s return nil, cobra.ShellCompDirectiveNoFileComp } -func setPlatformOptions(opts []oci.SpecOpts, cmd *cobra.Command, id string) ([]oci.SpecOpts, error) { +func setPlatformOptions(ctx context.Context, opts []oci.SpecOpts, cmd *cobra.Command, client *containerd.Client, id string) ([]oci.SpecOpts, error) { return opts, nil } + +func setPlatformContainerOptions(ctx context.Context, cOpts []containerd.NewContainerOpts, cmd *cobra.Command, client *containerd.Client, id string) ([]containerd.NewContainerOpts, error) { + return cOpts, nil +} diff --git a/cmd/nerdctl/run_linux.go b/cmd/nerdctl/run_linux.go index b053f8e2274..c76170742e3 100644 --- a/cmd/nerdctl/run_linux.go +++ b/cmd/nerdctl/run_linux.go @@ -21,7 +21,10 @@ import ( "fmt" "strings" + "github.com/containerd/containerd" "github.com/containerd/nerdctl/pkg/bypass4netnsutil" + "github.com/containerd/nerdctl/pkg/idutil/containerwalker" + "github.com/containerd/nerdctl/pkg/labels" "github.com/containerd/nerdctl/pkg/rootlessutil" "github.com/containerd/nerdctl/pkg/strutil" "github.com/docker/go-units" @@ -55,7 +58,7 @@ func runShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]s } } -func setPlatformOptions(opts []oci.SpecOpts, cmd *cobra.Command, id string) ([]oci.SpecOpts, error) { +func setPlatformOptions(ctx context.Context, opts []oci.SpecOpts, cmd *cobra.Command, client *containerd.Client, id string) ([]oci.SpecOpts, error) { opts = append(opts, oci.WithDefaultUnixDevices, WithoutRunMount(), // unmount default tmpfs on "/run": https://github.com/containerd/nerdctl/issues/157) @@ -128,21 +131,15 @@ func setPlatformOptions(opts []oci.SpecOpts, cmd *cobra.Command, id string) ([]o opts = append(opts, oci.WithDevShmSize(shmBytes/1024)) } - pidNs, err := cmd.Flags().GetString("pid") + pid, err := cmd.Flags().GetString("pid") if err != nil { return nil, err } - pidNs = strings.ToLower(pidNs) - if pidNs != "" { - if pidNs != "host" { - return nil, fmt.Errorf("invalid pid namespace. Set --pid=host to enable host pid namespace") - } else { - opts = append(opts, oci.WithHostNamespace(specs.PIDNamespace)) - if rootlessutil.IsRootless() { - opts = append(opts, withBindMountHostProcfs) - } - } + pidOpts, err := generatePIDOpts(ctx, client, pid) + if err != nil { + return nil, err } + opts = append(opts, pidOpts...) ulimitOpts, err := generateUlimitsOpts(cmd) if err != nil { @@ -192,6 +189,21 @@ func setPlatformOptions(opts []oci.SpecOpts, cmd *cobra.Command, id string) ([]o return opts, nil } +func setPlatformContainerOptions(ctx context.Context, cOpts []containerd.NewContainerOpts, cmd *cobra.Command, client *containerd.Client, id string) ([]containerd.NewContainerOpts, error) { + pid, err := cmd.Flags().GetString("pid") + if err != nil { + return nil, err + } + + pidCOpts, err := generatePIDCOpts(ctx, client, pid) + if err != nil { + return nil, err + } + cOpts = append(cOpts, pidCOpts...) + + return cOpts, nil +} + func setOOMScoreAdj(opts []oci.SpecOpts, cmd *cobra.Command) ([]oci.SpecOpts, error) { if !cmd.Flags().Changed("oom-score-adj") { return opts, nil @@ -217,3 +229,83 @@ func withOOMScoreAdj(score int) oci.SpecOpts { return nil } } + +func generatePIDOpts(ctx context.Context, client *containerd.Client, pid string) ([]oci.SpecOpts, error) { + opts := make([]oci.SpecOpts, 0) + pid = strings.ToLower(pid) + + switch pid { + case "": + // do nothing + case "host": + opts = append(opts, oci.WithHostNamespace(specs.PIDNamespace)) + if rootlessutil.IsRootless() { + opts = append(opts, withBindMountHostProcfs) + } + default: // container: + parsed := strings.Split(pid, ":") + if len(parsed) < 2 || parsed[0] != "container" { + return nil, fmt.Errorf("invalid pid namespace. Set --pid=[host|container:") + } + + containerName := parsed[1] + walker := &containerwalker.ContainerWalker{ + Client: client, + OnFound: func(ctx context.Context, found containerwalker.Found) error { + if found.MatchCount > 1 { + return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req) + } + + o, err := generateSharingPIDOpts(ctx, found.Container) + if err != nil { + return err + } + opts = append(opts, o...) + + return nil + }, + } + matchedCount, err := walker.Walk(ctx, containerName) + if err != nil { + return nil, err + } + if matchedCount < 1 { + return nil, fmt.Errorf("no such container: %s", containerName) + } + } + + return opts, nil +} + +func generatePIDCOpts(ctx context.Context, client *containerd.Client, pid string) ([]containerd.NewContainerOpts, error) { + pid = strings.ToLower(pid) + + cOpts := make([]containerd.NewContainerOpts, 0) + parsed := strings.Split(pid, ":") + if len(parsed) < 2 || parsed[0] != "container" { + // no need to save pid options + return cOpts, nil + } + + containerName := parsed[1] + walker := &containerwalker.ContainerWalker{ + Client: client, + OnFound: func(ctx context.Context, found containerwalker.Found) error { + if found.MatchCount > 1 { + return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req) + } + cOpts = append(cOpts, containerd.WithAdditionalContainerLabels(map[string]string{ + labels.PIDContainer: containerName, + })) + return nil + }, + } + matchedCount, err := walker.Walk(ctx, containerName) + if err != nil { + return nil, err + } + if matchedCount < 1 { + return nil, fmt.Errorf("no such container: %s", containerName) + } + return cOpts, nil +} diff --git a/cmd/nerdctl/run_linux_test.go b/cmd/nerdctl/run_linux_test.go index 88255f369aa..7f5bb529de6 100644 --- a/cmd/nerdctl/run_linux_test.go +++ b/cmd/nerdctl/run_linux_test.go @@ -75,6 +75,18 @@ func TestRunPidHost(t *testing.T) { base.Cmd("run", "--rm", "--pid=host", testutil.AlpineImage, "ps", "auxw").AssertOutContains(strconv.Itoa(pid)) } +func TestRunPidContainer(t *testing.T) { + t.Parallel() + base := testutil.NewBase(t) + + sharedContainerResult := base.Cmd("run", "-d", testutil.AlpineImage, "sleep", "infinity").Run() + baseContainerID := strings.TrimSpace(sharedContainerResult.Stdout()) + defer base.Cmd("rm", "-f", baseContainerID).Run() + + base.Cmd("run", "--rm", fmt.Sprintf("--pid=container:%s", baseContainerID), + testutil.AlpineImage, "ps", "ax").AssertOutContains("sleep infinity") +} + func TestRunIpcHost(t *testing.T) { t.Parallel() base := testutil.NewBase(t) diff --git a/cmd/nerdctl/run_windows.go b/cmd/nerdctl/run_windows.go index 409b55af17e..915ddbf7975 100644 --- a/cmd/nerdctl/run_windows.go +++ b/cmd/nerdctl/run_windows.go @@ -20,6 +20,7 @@ import ( "context" "fmt" + "github.com/containerd/containerd" "github.com/containerd/containerd/containers" "github.com/containerd/containerd/oci" "github.com/docker/go-units" @@ -40,7 +41,7 @@ func runShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]s return nil, cobra.ShellCompDirectiveNoFileComp } -func setPlatformOptions(opts []oci.SpecOpts, cmd *cobra.Command, id string) ([]oci.SpecOpts, error) { +func setPlatformOptions(ctx context.Context, opts []oci.SpecOpts, cmd *cobra.Command, client *containerd.Client, id string) ([]oci.SpecOpts, error) { cpus, err := cmd.Flags().GetFloat64("cpus") if err != nil { return nil, err @@ -67,3 +68,7 @@ func setPlatformOptions(opts []oci.SpecOpts, cmd *cobra.Command, id string) ([]o return opts, nil } + +func setPlatformContainerOptions(ctx context.Context, cOpts []containerd.NewContainerOpts, cmd *cobra.Command, client *containerd.Client, id string) ([]containerd.NewContainerOpts, error) { + return cOpts, nil +} diff --git a/cmd/nerdctl/start.go b/cmd/nerdctl/start.go index 656b2afac5c..147ca221d31 100644 --- a/cmd/nerdctl/start.go +++ b/cmd/nerdctl/start.go @@ -19,9 +19,11 @@ package main import ( "context" "encoding/json" + "errors" "fmt" "net/url" "os" + "runtime" "strings" "github.com/containerd/containerd" @@ -110,6 +112,10 @@ func startContainer(ctx context.Context, container containerd.Container, flagA b return err } + if err := reconfigPIDContainer(ctx, container, client, lab); err != nil { + return err + } + taskCIO := cio.NullIO // Choosing the user selected option over the labels @@ -221,6 +227,41 @@ func reconfigNetContainer(ctx context.Context, c containerd.Container, client *c return nil } +func reconfigPIDContainer(ctx context.Context, c containerd.Container, client *containerd.Client, lab map[string]string) error { + targetContainerID, ok := lab[labels.PIDContainer] + if !ok { + return nil + } + + if runtime.GOOS != "linux" { + return errors.New("--pid only supported on linux") + } + + targetCon, err := client.LoadContainer(ctx, targetContainerID) + if err != nil { + return err + } + + opts, err := generateSharingPIDOpts(ctx, targetCon) + if err != nil { + return err + } + + spec, err := c.Spec(ctx) + if err != nil { + return err + } + + err = c.Update(ctx, containerd.UpdateContainerOpts( + containerd.WithSpec(spec, oci.Compose(opts...)), + )) + if err != nil { + return err + } + + return nil +} + func startShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // show non-running container names statusFilterFn := func(st containerd.ProcessStatus) bool { diff --git a/pkg/labels/labels.go b/pkg/labels/labels.go index d749f9fe828..7a7286e1b2f 100644 --- a/pkg/labels/labels.go +++ b/pkg/labels/labels.go @@ -86,6 +86,9 @@ const ( StopTimout = Prefix + "stop-timeout" MACAddress = Prefix + "mac-address" + + // PIDContainer is the `nerdctl run --pid` for restarting + PIDContainer = Prefix + "pid-container" ) var ShellCompletions = []string{