cmd/run: Implement the run command in Go

https://github.com/containers/toolbox/pull/318
This commit is contained in:
Harry Míchal 2020-04-27 13:06:56 +02:00 committed by Debarshi Ray
parent cf5c58ab00
commit 238f2451e7
1 changed files with 339 additions and 0 deletions

View File

@ -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
}