diff --git a/namespaces/execin.go b/namespaces/execin.go index 369431e8..054d3685 100644 --- a/namespaces/execin.go +++ b/namespaces/execin.go @@ -3,70 +3,79 @@ package namespaces import ( - "encoding/json" + "io" "os" + "os/exec" "strconv" + "syscall" "github.com/docker/libcontainer" "github.com/docker/libcontainer/label" + "github.com/docker/libcontainer/syncpipe" "github.com/docker/libcontainer/system" ) -// ExecIn uses an existing pid and joins the pid's namespaces with the new command. -func ExecIn(container *libcontainer.Config, state *libcontainer.State, args []string) error { - // Enter the namespace and then finish setup - args, err := GetNsEnterCommand(strconv.Itoa(state.InitPid), container, "", args) - if err != nil { - return err - } +func ExecIn(container *libcontainer.Config, state *libcontainer.State, userArgs []string, initPath string, + stdin io.Reader, stdout, stderr io.Writer, console string, startCallback func(*exec.Cmd)) (int, error) { - finalArgs := append([]string{os.Args[0]}, args...) - - if err := system.Execv(finalArgs[0], finalArgs[0:], os.Environ()); err != nil { - return err - } - - panic("unreachable") -} - -func getContainerJson(container *libcontainer.Config) (string, error) { - // TODO(vmarmol): If this gets too long, send it over a pipe to the child. - // Marshall the container into JSON since it won't be available in the namespace. - containerJson, err := json.Marshal(container) - if err != nil { - return "", err - } - return string(containerJson), nil -} - -func GetNsEnterCommand(initPid string, container *libcontainer.Config, console string, args []string) ([]string, error) { - containerJson, err := getContainerJson(container) - if err != nil { - return nil, err - } - - out := []string{ - "--nspid", initPid, - "--containerjson", containerJson, - } + args := []string{"--nspid", strconv.Itoa(state.InitPid)} if console != "" { - out = append(out, "--console", console) + args = append(args, "--console", console) } - out = append(out, "nsenter") - out = append(out, "--") - out = append(out, args...) - return out, nil + args = append(args, "nsenter", "--") + args = append(args, userArgs...) + + cmd := exec.Command(initPath, args...) + + pipe, err := syncpipe.NewSyncPipe() + if err != nil { + return -1, err + } + defer pipe.Close() + + // Note: these are only used in non-tty mode + // if there is a tty for the container it will be opened within the namespace and the + // fds will be duped to stdin, stdiout, and stderr + cmd.Stdin = stdin + cmd.Stdout = stdout + cmd.Stderr = stderr + + cmd.ExtraFiles = []*os.File{pipe.Child()} + + if err := cmd.Start(); err != nil { + return -1, err + } + pipe.CloseChild() + + if err := pipe.SendToChild(container); err != nil { + cmd.Process.Kill() + cmd.Wait() + return -1, err + } + + if startCallback != nil { + startCallback(cmd) + } + + if err := cmd.Wait(); err != nil { + if _, ok := err.(*exec.ExitError); !ok { + return -1, err + } + } + + return cmd.ProcessState.Sys().(syscall.WaitStatus).ExitStatus(), nil } -// Run a command in a container after entering the namespace. -func NsEnter(container *libcontainer.Config, args []string) error { - // clear the current processes env and replace it with the environment - // defined on the container +// Finalize expects that the setns calls have been setup and that is has joined an +// existing namespace +func FinalizeSetns(container *libcontainer.Config, args []string) error { + // clear the current processes env and replace it with the environment defined on the container if err := LoadContainerEnvironment(container); err != nil { return err } + if err := FinalizeNamespace(container); err != nil { return err } @@ -80,5 +89,6 @@ func NsEnter(container *libcontainer.Config, args []string) error { if err := system.Execv(args[0], args[0:], container.Env); err != nil { return err } + panic("unreachable") } diff --git a/namespaces/nsenter.c b/namespaces/nsenter.c index 28be5ff8..489d6db5 100644 --- a/namespaces/nsenter.c +++ b/namespaces/nsenter.c @@ -96,7 +96,7 @@ void nsenter() } static const struct option longopts[] = { {"nspid", required_argument, NULL, 'n'}, - {"containerjson", required_argument, NULL, 'c'}, + {"containerjson", optional_argument, NULL, 'c'}, {"console", optional_argument, NULL, 't'}, {NULL, 0, NULL, 0} }; @@ -126,7 +126,7 @@ void nsenter() return; } - if (container_json == NULL || init_pid_str == NULL) { + if (init_pid_str == NULL) { print_usage(); exit(1); } @@ -147,6 +147,7 @@ void nsenter() fprintf(stderr, "setsid failed. Error: %s\n", strerror(errno)); exit(1); } + // before we setns we need to dup the console int consolefd = -1; if (console != NULL) { @@ -158,6 +159,7 @@ void nsenter() exit(1); } } + // Setns on all supported namespaces. char ns_dir[PATH_MAX]; memset(ns_dir, 0, PATH_MAX); @@ -211,6 +213,7 @@ void nsenter() exit(1); } } + // Finish executing, let the Go runtime take over. return; } else { @@ -222,12 +225,14 @@ void nsenter() strerror(errno)); exit(1); } + // Forward the child's exit code or re-send its death signal. if (WIFEXITED(status)) { exit(WEXITSTATUS(status)); } else if (WIFSIGNALED(status)) { kill(getpid(), WTERMSIG(status)); } + exit(1); } diff --git a/nsinit/cli.go b/nsinit/cli.go index 1c770edb..f467d2f8 100644 --- a/nsinit/cli.go +++ b/nsinit/cli.go @@ -26,9 +26,11 @@ func NsInit() { app.Author = "libcontainer maintainers" app.Flags = []cli.Flag{ cli.StringFlag{Name: "nspid"}, - cli.StringFlag{Name: "containerjson"}, - cli.StringFlag{Name: "console"}} + cli.StringFlag{Name: "console"}, + } + app.Before = preload + app.Commands = []cli.Command{ execCommand, initCommand, diff --git a/nsinit/exec.go b/nsinit/exec.go index abb245bd..cef4fcbd 100644 --- a/nsinit/exec.go +++ b/nsinit/exec.go @@ -36,7 +36,7 @@ func execAction(context *cli.Context) { } if state != nil { - err = namespaces.ExecIn(container, state, []string(context.Args())) + exitCode, err = startInExistingContainer(container, state, context) } else { exitCode, err = startContainer(container, dataPath, []string(context.Args())) } @@ -48,6 +48,63 @@ func execAction(context *cli.Context) { os.Exit(exitCode) } +// the process for execing a new process inside an existing container is that we have to exec ourself +// with the nsenter argument so that the C code can setns an the namespaces that we require. Then that +// code path will drop us into the path that we can do the file setup of the namespace and exec the users +// application. +func startInExistingContainer(config *libcontainer.Config, state *libcontainer.State, context *cli.Context) (int, error) { + var ( + master *os.File + console string + err error + + sigc = make(chan os.Signal, 10) + + stdin = os.Stdin + stdout = os.Stdout + stderr = os.Stderr + ) + signal.Notify(sigc) + + if config.Tty { + stdin = nil + stdout = nil + stderr = nil + + master, console, err = consolepkg.CreateMasterAndConsole() + if err != nil { + return -1, err + } + + go io.Copy(master, os.Stdin) + go io.Copy(os.Stdout, master) + + state, err := term.SetRawTerminal(os.Stdin.Fd()) + if err != nil { + return -1, err + } + + defer term.RestoreTerminal(os.Stdin.Fd(), state) + } + + startCallback := func(cmd *exec.Cmd) { + go func() { + resizeTty(master) + + for sig := range sigc { + switch sig { + case syscall.SIGWINCH: + resizeTty(master) + default: + cmd.Process.Signal(sig) + } + } + }() + } + + return namespaces.ExecIn(config, state, context.Args(), os.Args[0], stdin, stdout, stderr, console, startCallback) +} + // startContainer starts the container. Returns the exit status or -1 and an // error. // diff --git a/nsinit/nsenter.go b/nsinit/nsenter.go index 323706c2..29e3441b 100644 --- a/nsinit/nsenter.go +++ b/nsinit/nsenter.go @@ -2,10 +2,11 @@ package nsinit import ( "log" - "strconv" "github.com/codegangsta/cli" + "github.com/docker/libcontainer" "github.com/docker/libcontainer/namespaces" + "github.com/docker/libcontainer/syncpipe" ) var nsenterCommand = cli.Command{ @@ -14,24 +15,20 @@ var nsenterCommand = cli.Command{ Action: nsenterAction, } +// this expects that we already have our namespaces setup by the C initializer +// we are expected to finalize the namespace and exec the user's application func nsenterAction(context *cli.Context) { - args := context.Args() - - if len(args) == 0 { - args = []string{"/bin/bash"} - } - - container, err := loadContainerFromJson(context.GlobalString("containerjson")) + syncPipe, err := syncpipe.NewSyncPipeFromFd(0, 3) if err != nil { - log.Fatalf("unable to load container: %s", err) + log.Fatalf("unable to create sync pipe: %s", err) } - nspid, err := strconv.Atoi(context.GlobalString("nspid")) - if nspid <= 0 || err != nil { - log.Fatalf("cannot enter into namespaces without valid pid: %q - %s", nspid, err) + var config *libcontainer.Config + if err := syncPipe.ReadFromParent(&config); err != nil { + log.Fatalf("reading container config from parent: %s", err) } - if err := namespaces.NsEnter(container, args); err != nil { + if err := namespaces.FinalizeSetns(config, context.Args()); err != nil { log.Fatalf("failed to nsenter: %s", err) } }