diff --git a/src/cmd/run.go b/src/cmd/run.go index 0779155..250275c 100644 --- a/src/cmd/run.go +++ b/src/cmd/run.go @@ -17,10 +17,14 @@ package cmd import ( + "errors" "fmt" "os" + "strings" + "time" "github.com/containers/toolbox/pkg/podman" + "github.com/containers/toolbox/pkg/shell" "github.com/containers/toolbox/pkg/utils" "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -60,6 +64,265 @@ func init() { } func run(cmd *cobra.Command, args []string) error { + if utils.IsInsideContainer() { + if !utils.IsInsideToolboxContainer() { + return errors.New("this is not a toolbox container") + } + + if _, err := utils.ForwardToHost(); err != nil { + return err + } + + return nil + } + + var nonDefaultContainer bool + + if runFlags.container != "" { + nonDefaultContainer = true + + if _, err := utils.IsContainerNameValid(runFlags.container); err != nil { + var builder strings.Builder + fmt.Fprintf(&builder, "invalid argument for '--container'\n") + fmt.Fprintf(&builder, "Container names must match '%s'\n", utils.ContainerNameRegexp) + fmt.Fprintf(&builder, "Run '%s --help' for usage.", executableBase) + + errMsg := builder.String() + return errors.New(errMsg) + } + } + + var release string + if runFlags.release != "" { + nonDefaultContainer = true + + var err error + release, err = utils.ParseRelease(runFlags.release) + if err != nil { + err := utils.CreateErrorInvalidRelease(executableBase) + return err + } + } + + if len(args) == 0 { + var builder strings.Builder + fmt.Fprintf(&builder, "missing argument for \"run\"\n") + fmt.Fprintf(&builder, "Run '%s --help' for usage.", executableBase) + + errMsg := builder.String() + return errors.New(errMsg) + } + + command := args + + container, image, release, err := utils.ResolveContainerAndImageNames(runFlags.container, "", release) + if err != nil { + return err + } + + if err := runCommand(container, + !nonDefaultContainer, + image, + release, + command, + false, + false, + true); err != nil { + return err + } + + return nil +} + +func runCommand(container string, + defaultContainer bool, + image, release string, + command []string, + emitEscapeSequence, fallbackToBash, pedantic bool) error { + if !pedantic { + if image == "" { + panic("image not specified") + } + + if release == "" { + panic("release not specified") + } + } + + logrus.Debugf("Checking if container %s exists", container) + + if _, err := podman.ContainerExists(container); err != nil { + logrus.Debugf("Container %s not found", container) + + if pedantic { + err := utils.CreateErrorContainerNotFound(container, executableBase) + return err + } + + containers, err := listContainers() + if err != nil { + err := utils.CreateErrorContainerNotFound(container, executableBase) + return err + } + + containersCount := len(containers) + logrus.Debugf("Found %d containers", containersCount) + + if containersCount == 0 { + var shouldCreateContainer bool + promptForCreate := true + + if rootFlags.assumeYes { + shouldCreateContainer = true + promptForCreate = false + } + + if promptForCreate { + prompt := "No toolbox containers found. Create now? [y/N]" + shouldCreateContainer = utils.AskForConfirmation(prompt) + } + + if !shouldCreateContainer { + fmt.Printf("A container can be created later with the 'create' command.\n") + fmt.Printf("Run '%s --help' for usage.\n", executableBase) + return nil + } + + if err := createContainer(container, image, release, false); err != nil { + return err + } + } else if containersCount == 1 && defaultContainer { + fmt.Fprintf(os.Stderr, "Error: container %s not found\n", container) + + container = containers[0]["Names"].(string) + fmt.Fprintf(os.Stderr, "Entering container %s instead.\n", container) + fmt.Fprintf(os.Stderr, "Use the 'create' command to create a different toolbox.\n") + fmt.Fprintf(os.Stderr, "Run '%s --help' for usage.\n", executableBase) + } else { + var builder strings.Builder + fmt.Fprintf(&builder, "container %s not found\n", container) + fmt.Fprintf(&builder, "Use the '--container' option to select a toolbox.\n") + fmt.Fprintf(&builder, "Run '%s --help' for usage.", executableBase) + + errMsg := builder.String() + return errors.New(errMsg) + } + } + + if _, err := utils.CallFlatpakSessionHelper(); err != nil { + return err + } + + logrus.Debugf("Starting container %s", container) + if err := startContainer(container); err != nil { + return err + } + + entryPoint, entryPointPID, err := getEntryPointAndPID(container) + if err != nil { + return err + } + + if entryPoint != "toolbox" { + var builder strings.Builder + fmt.Fprintf(&builder, "container %s is too old and no longer supported \n", container) + fmt.Fprintf(&builder, "Recreate it with Toolbox version 0.0.17 or newer.\n") + + errMsg := builder.String() + return errors.New(errMsg) + } + + if entryPointPID <= 0 { + return fmt.Errorf("invalid entry point PID of container %s", container) + } + + logrus.Debugf("Waiting for container %s to finish initializing", container) + + runtimeDirectory := os.Getenv("XDG_RUNTIME_DIR") + toolboxRuntimeDirectory := runtimeDirectory + "/toolbox" + initializedStamp := fmt.Sprintf("%s/container-initialized-%d", toolboxRuntimeDirectory, entryPointPID) + + logrus.Debugf("Checking if initialization stamp %s exists", initializedStamp) + + initializedTimeout := 25 // seconds + for i := 0; !utils.PathExists(initializedStamp); i++ { + if i == initializedTimeout { + return fmt.Errorf("failed to initialize container %s", container) + } + + time.Sleep(time.Second) + } + + logrus.Debugf("Container %s is initialized", container) + + if _, err := isCommandPresent(container, command[0]); err != nil { + if fallbackToBash { + logrus.Debugf("command %s not found in container %s; using /bin/bash instead", + command[0], + container) + + command = []string{"/bin/bash"} + } else { + return fmt.Errorf("command %s not found in container %s", command[0], container) + } + } + + envOptions := utils.GetEnvOptionsForPreservedVariables() + logLevelString := podman.LogLevel.String() + + execArgs := []string{ + "--log-level", logLevelString, + "exec", + "--interactive", + "--tty", + "--user", currentUser.Username, + "--workdir", workingDirectory, + } + + execArgs = append(execArgs, envOptions...) + + execArgs = append(execArgs, []string{ + container, + "capsh", "--caps=", "--", "-c", "exec \"$@\"", "/bin/sh", + }...) + + execArgs = append(execArgs, command...) + + if emitEscapeSequence { + fmt.Printf("\033]777;container;push;%s;toolbox\033\\", container) + } + + logrus.Debugf("Running in container %s:", container) + logrus.Debug("podman") + for _, arg := range execArgs { + logrus.Debugf("%s", arg) + } + + exitCode, err := shell.RunWithExitCode("podman", os.Stdin, os.Stdout, nil, execArgs...) + + if emitEscapeSequence { + fmt.Print("\033]777;container;pop;;\033\\") + } + + switch exitCode { + case 0: + if err != nil { + panic("unexpected error: 'podman exec' finished successfully") + } + case 125: + err = fmt.Errorf("failed to invoke 'podman exec' in container %s", container) + case 126: + err = fmt.Errorf("failed to invoke command %s in container %s", command[0], container) + case 127: + err = fmt.Errorf("command %s not found in container %s", command[0], container) + default: + err = nil + } + + if err != nil { + return err + } + return nil } @@ -115,3 +378,79 @@ func getEntryPointAndPID(container string) (string, int, error) { return entryPoint, entryPointPIDInt, nil } + +func isCommandPresent(container, command string) (bool, error) { + logrus.Debugf("Looking for command %s in container %s", command, container) + + logLevelString := podman.LogLevel.String() + args := []string{ + "--log-level", logLevelString, + "exec", + "--user", currentUser.Username, + container, + "sh", "-c", "command -v \"$1\"", "sh", command, + } + + if err := shell.Run("podman", nil, nil, nil, args...); err != nil { + return false, err + } + + return true, nil +} + +func startContainer(container string) error { + var stderr strings.Builder + if err := podman.Start(container, &stderr); err == nil { + return nil + } + + errString := stderr.String() + if !strings.Contains(errString, "use system migrate to mitigate") { + return fmt.Errorf("failed to start container %s", container) + } + + logrus.Debug("Checking if 'podman system migrate' supports '--new-runtime'") + + if !podman.CheckVersion("1.6.2") { + var builder strings.Builder + + fmt.Fprintf(&builder, + "container %s doesn't support cgroups v%d\n", + container, + cgroupsVersion) + + fmt.Fprintf(&builder, "Update Podman to version 1.6.2 or newer.\n") + + errMsg := builder.String() + return errors.New(errMsg) + } + + logrus.Debug("'podman system migrate' supports '--new-runtime'") + + ociRuntimeRequired := "runc" + if cgroupsVersion == 2 { + ociRuntimeRequired = "crun" + } + + logrus.Debugf("Migrating containers to OCI runtime %s", ociRuntimeRequired) + + if err := podman.SystemMigrate(ociRuntimeRequired); err != nil { + var builder strings.Builder + fmt.Fprintf(&builder, "failed to migrate containers to OCI runtime %s\n", ociRuntimeRequired) + fmt.Fprintf(&builder, "Factory reset with: podman system reset") + + errMsg := builder.String() + return errors.New(errMsg) + } + + if err := podman.Start(container, nil); err != nil { + var builder strings.Builder + fmt.Fprintf(&builder, "container %s doesn't support cgroups v%d\n", container, cgroupsVersion) + fmt.Fprintf(&builder, "Factory reset with: podman system reset") + + errMsg := builder.String() + return errors.New(errMsg) + } + + return nil +}