cmd/create: Implement the create command

The Go version supports specifying the name of the new toolbox
container as 'toolbox create NAME', in addition to the existing
'toolbox create --container NAME' form.

https://github.com/containers/toolbox/pull/318
This commit is contained in:
Harry Míchal 2020-04-16 15:11:13 +02:00 committed by Debarshi Ray
parent 49146028bc
commit 8f30d71806
3 changed files with 559 additions and 0 deletions

View file

@ -17,10 +17,19 @@
package cmd
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/briandowns/spinner"
"github.com/containers/toolbox/pkg/podman"
"github.com/containers/toolbox/pkg/shell"
"github.com/containers/toolbox/pkg/utils"
systemd "github.com/coreos/go-systemd/v22/dbus"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
@ -30,6 +39,14 @@ var (
image string
release string
}
createToolboxShMounts = []struct {
containerPath string
source string
}{
{"/etc/profile.d/toolbox.sh", "/etc/profile.d/toolbox.sh"},
{"/etc/profile.d/toolbox.sh", "/usr/share/profile.d/toolbox.sh"},
}
)
var createCmd = &cobra.Command{
@ -64,6 +81,326 @@ func init() {
}
func create(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 container string
var containerArg string
if len(args) != 0 {
container = args[0]
containerArg = "CONTAINER"
} else if createFlags.container != "" {
container = createFlags.container
containerArg = "--container"
}
if container != "" {
if _, err := utils.IsContainerNameValid(container); err != nil {
var builder strings.Builder
fmt.Fprintf(&builder, "invalid argument for '%s'\n", containerArg)
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 createFlags.release != "" {
var err error
release, err = utils.ParseRelease(createFlags.release)
if err != nil {
err := utils.CreateErrorInvalidRelease(executableBase)
return err
}
}
container, image, release, err := utils.ResolveContainerAndImageNames(container,
createFlags.image,
release)
if err != nil {
return err
}
if err := createContainer(container, image, release, true); err != nil {
return err
}
return nil
}
func createContainer(container, image, release string, showCommandToEnter bool) error {
if container == "" {
panic("container not specified")
}
if image == "" {
panic("image not specified")
}
if release == "" {
panic("release not specified")
}
enterCommand := getEnterCommand(container, release)
logrus.Debugf("Checking if container %s already exists", container)
if exists, _ := podman.ContainerExists(container); exists {
var builder strings.Builder
fmt.Fprintf(&builder, "container %s already exists\n", container)
fmt.Fprintf(&builder, "Enter with: %s\n", enterCommand)
fmt.Fprintf(&builder, "Run '%s --help' for usage.", executableBase)
errMsg := builder.String()
return errors.New(errMsg)
}
pulled, err := pullImage(image, release)
if err != nil {
return err
}
if !pulled {
return nil
}
imageFull, err := getFullyQualifiedImageName(image)
if err != nil {
return err
}
toolboxPath := os.Getenv("TOOLBOX_PATH")
toolboxPathEnvArg := "TOOLBOX_PATH=" + toolboxPath
toolboxPathMountArg := toolboxPath + ":/usr/bin/toolbox:ro"
sudoGroup, err := utils.GetGroupForSudo()
if err != nil {
return err
}
logrus.Debug("Checking if 'podman create' supports '--ulimit host'")
var ulimitHost []string
if podman.CheckVersion("1.5.0") {
logrus.Debug("'podman create' supports '--ulimit host'")
ulimitHost = []string{"--ulimit", "host"}
}
dbusSystemSocket, err := getDBusSystemSocket()
if err != nil {
return err
}
dbusSystemSocketMountArg := dbusSystemSocket + ":" + dbusSystemSocket
flatpakHelperMonitorPath, err := utils.CallFlatpakSessionHelper()
if err != nil {
return err
}
flatpakHelperMonitorMountArg := flatpakHelperMonitorPath + ":/run/host/monitor"
homeDirEvaled, err := filepath.EvalSymlinks(currentUser.HomeDir)
if err != nil {
return fmt.Errorf("failed to canonicalize %s", currentUser.HomeDir)
}
logrus.Debugf("%s canonicalized to %s", currentUser.HomeDir, homeDirEvaled)
homeDirMountArg := homeDirEvaled + ":" + homeDirEvaled + ":rslave"
usrMountFlags := "ro"
isUsrReadWrite, err := isUsrReadWrite()
if err != nil {
return err
}
if isUsrReadWrite {
usrMountFlags = "rw"
}
usrMountArg := "/usr:/run/host/usr:" + usrMountFlags + ",rslave"
xdgRuntimeDir := os.Getenv("XDG_RUNTIME_DIR")
xdgRuntimeDirMountArg := xdgRuntimeDir + ":" + xdgRuntimeDir
var kcmSocketMount []string
kcmSocket, err := getKCMSocket()
if err != nil {
logrus.Debug(err)
}
if kcmSocket != "" {
kcmSocketMountArg := kcmSocket + ":" + kcmSocket
kcmSocketMount = []string{"--volume", kcmSocketMountArg}
}
var mediaLink []string
var mediaMount []string
if utils.PathExists("/media") {
logrus.Debug("Checking if /media is a symbolic link to /run/media")
mediaPath, _ := filepath.EvalSymlinks("/media")
if mediaPath == "/run/media" {
logrus.Debug("/media is a symbolic link to /run/media")
mediaLink = []string{"--media-link"}
} else {
mediaMount = []string{"--volume", "/media:/media:rslave"}
}
}
logrus.Debug("Checking if /mnt is a symbolic link to /var/mnt")
var mntLink []string
var mntMount []string
mntPath, _ := filepath.EvalSymlinks("/mnt")
if mntPath == "/var/mnt" {
logrus.Debug("/mnt is a symbolic link to /var/mnt")
mntLink = []string{"--mnt-link"}
} else {
mntMount = []string{"--volume", "/mnt:/mnt:rslave"}
}
var runMediaMount []string
if utils.PathExists("/run/media") {
runMediaMount = []string{"--volume", "/run/media:/run/media:rslave"}
}
logrus.Debug("Looking for toolbox.sh")
var toolboxShMount []string
for _, mount := range createToolboxShMounts {
if utils.PathExists(mount.source) {
logrus.Debugf("Found %s", mount.source)
toolboxShMountArg := mount.source + ":" + mount.containerPath + ":ro"
toolboxShMount = []string{"--volume", toolboxShMountArg}
break
}
}
logrus.Debug("Checking if /home is a symbolic link to /var/home")
var slashHomeLink []string
slashHomeEvaled, _ := filepath.EvalSymlinks("/home")
if slashHomeEvaled == "/var/home" {
logrus.Debug("/home is a symbolic link to /var/home")
slashHomeLink = []string{"--home-link"}
}
logLevelString := podman.LogLevel.String()
userShell := os.Getenv("SHELL")
if userShell == "" {
return errors.New("failed to get the current user's default shell")
}
entryPoint := []string{
"toolbox", "--verbose",
"init-container",
"--home", currentUser.HomeDir,
}
entryPoint = append(entryPoint, slashHomeLink...)
entryPoint = append(entryPoint, mediaLink...)
entryPoint = append(entryPoint, mntLink...)
entryPoint = append(entryPoint, []string{
"--monitor-host",
"--shell", userShell,
"--uid", currentUser.Uid,
"--user", currentUser.Username,
}...)
createArgs := []string{
"--log-level", logLevelString,
"create",
"--dns", "none",
"--env", toolboxPathEnvArg,
"--group-add", sudoGroup,
"--hostname", "toolbox",
"--ipc", "host",
"--label", "com.github.containers.toolbox=true",
"--label", "com.github.debarshiray.toolbox=true",
"--name", container,
"--network", "host",
"--no-hosts",
"--pid", "host",
"--privileged",
"--security-opt", "label=disable",
}
createArgs = append(createArgs, ulimitHost...)
createArgs = append(createArgs, []string{
"--userns=keep-id",
"--user", "root:root",
"--volume", "/etc:/run/host/etc",
"--volume", "/dev:/dev:rslave",
"--volume", "/run:/run/host/run:rslave",
"--volume", "/tmp:/run/host/tmp:rslave",
"--volume", "/var:/run/host/var:rslave",
"--volume", dbusSystemSocketMountArg,
"--volume", flatpakHelperMonitorMountArg,
"--volume", homeDirMountArg,
"--volume", toolboxPathMountArg,
"--volume", usrMountArg,
"--volume", xdgRuntimeDirMountArg,
}...)
createArgs = append(createArgs, kcmSocketMount...)
createArgs = append(createArgs, mediaMount...)
createArgs = append(createArgs, mntMount...)
createArgs = append(createArgs, runMediaMount...)
createArgs = append(createArgs, toolboxShMount...)
createArgs = append(createArgs, []string{
imageFull,
}...)
createArgs = append(createArgs, entryPoint...)
logrus.Debugf("Creating container %s:", container)
logrus.Debug("podman")
for _, arg := range createArgs {
logrus.Debugf("%s", arg)
}
s := spinner.New(spinner.CharSets[9], 500*time.Millisecond)
if logLevel := logrus.GetLevel(); logLevel < logrus.DebugLevel {
s.Prefix = fmt.Sprintf("Creating container %s: ", container)
s.Writer = os.Stdout
s.Start()
defer s.Stop()
}
if err := shell.Run("podman", nil, nil, nil, createArgs...); err != nil {
return fmt.Errorf("failed to create container %s", container)
}
s.Stop()
if showCommandToEnter {
fmt.Printf("Created container: %s\n", container)
fmt.Printf("Enter with: %s\n", enterCommand)
}
return nil
}
@ -87,3 +424,211 @@ func createHelp(cmd *cobra.Command, args []string) {
return
}
}
func getDBusSystemSocket() (string, error) {
logrus.Debug("Resolving path to the D-Bus system socket")
address := os.Getenv("DBUS_SYSTEM_BUS_ADDRESS")
if address == "" {
address = "unix:path=/var/run/dbus/system_bus_socket"
}
addressSplit := strings.Split(address, "=")
if len(addressSplit) != 2 {
return "", errors.New("failed to get the path to the D-Bus system socket")
}
path := addressSplit[1]
pathEvaled, err := filepath.EvalSymlinks(path)
if err != nil {
return "", errors.New("failed to resolve the path to the D-Bus system socket")
}
return pathEvaled, nil
}
func getEnterCommand(container, release string) string {
var enterCommand string
containerNamePrefixDefaultWithRelease := utils.ContainerNamePrefixDefault + "-" + release
switch container {
case utils.ContainerNameDefault:
enterCommand = fmt.Sprintf("%s enter", executableBase)
case containerNamePrefixDefaultWithRelease:
enterCommand = fmt.Sprintf("%s enter --release %s", executableBase, release)
default:
enterCommand = fmt.Sprintf("%s enter --container %s", executableBase, container)
}
return enterCommand
}
func getFullyQualifiedImageName(image string) (string, error) {
logrus.Debugf("Resolving fully qualified name for image %s", image)
var imageFull string
if utils.ImageReferenceHasDomain(image) {
imageFull = image
} else {
info, err := podman.Inspect("image", image)
if err != nil {
return "", fmt.Errorf("failed to inspect image %s", image)
}
if info["RepoTags"] == nil {
return "", fmt.Errorf("missing RepoTag for image %s", image)
}
repoTags := info["RepoTags"].([]interface{})
if len(repoTags) == 0 {
return "", fmt.Errorf("empty RepoTag for image %s", image)
}
imageFull = repoTags[0].(string)
}
logrus.Debugf("Resolved image %s to %s", image, imageFull)
return imageFull, nil
}
func getKCMSocket() (string, error) {
logrus.Debug("Resolving path to the KCM socket")
connection, err := systemd.NewSystemConnection()
if err != nil {
return "", errors.New("failed to connect to the D-Bus system instance")
}
defer connection.Close()
properties, err := connection.GetAllProperties("sssd-kcm.socket")
if err != nil {
return "", errors.New("failed to get the properties of sssd-kcm.socket")
}
value := properties["Listen"]
if value == nil {
return "", errors.New("failed to find the Listen property of sssd-kcm.socket")
}
sockets := value.([][]interface{})
for _, socket := range sockets {
if socket[0] == "Stream" {
path := socket[1].(string)
if !strings.HasPrefix(path, "/") {
continue
}
pathEvaled, err := filepath.EvalSymlinks(path)
if err != nil {
continue
}
return pathEvaled, nil
}
}
return "", errors.New("failed to find a SOCK_STREAM socket for sssd-kcm.socket")
}
func isUsrReadWrite() (bool, error) {
logrus.Debug("Checking if /usr is mounted read-only or read-write")
mountPoint, err := utils.GetMountPoint("/usr")
if err != nil {
return false, fmt.Errorf("failed to get the mount-point of /usr: %s", err)
}
logrus.Debugf("Mount-point of /usr is %s", mountPoint)
mountFlags, err := utils.GetMountOptions(mountPoint)
if err != nil {
return false, fmt.Errorf("failed to get the mount options of %s: %s", mountPoint, err)
}
logrus.Debugf("Mount flags of /usr on the host are %s", mountFlags)
if !strings.Contains(mountFlags, "ro") {
return true, nil
}
return false, nil
}
func pullImage(image, release string) (bool, error) {
if _, err := utils.ImageReferenceCanBeID(image); err == nil {
logrus.Debugf("Looking for image %s", image)
if _, err := podman.ImageExists(image); err == nil {
return true, nil
}
}
hasDomain := utils.ImageReferenceHasDomain(image)
if !hasDomain {
imageLocal := "localhost/" + image
logrus.Debugf("Looking for image %s", imageLocal)
if _, err := podman.ImageExists(imageLocal); err == nil {
return true, nil
}
}
var imageFull string
if hasDomain {
imageFull = image
} else {
imageFull = fmt.Sprintf("registry.fedoraproject.org/f%s/%s", release, image)
}
logrus.Debugf("Looking for image %s", imageFull)
if _, err := podman.ImageExists(imageFull); err == nil {
return true, nil
}
domain := utils.ImageReferenceGetDomain(imageFull)
if domain == "" {
panicMsg := fmt.Sprintf("failed to get domain from %s", imageFull)
panic(panicMsg)
}
promptForDownload := true
var shouldPullImage bool
if rootFlags.assumeYes || domain == "localhost" {
promptForDownload = false
shouldPullImage = true
}
if promptForDownload {
fmt.Println("Image required to create toolbox container.")
prompt := fmt.Sprintf("Download %s (500MB)? [y/N]:", imageFull)
shouldPullImage = utils.AskForConfirmation(prompt)
}
if !shouldPullImage {
return false, nil
}
logrus.Debugf("Pulling image %s", imageFull)
if logLevel := logrus.GetLevel(); logLevel < logrus.DebugLevel {
s := spinner.New(spinner.CharSets[9], 500*time.Millisecond)
s.Prefix = fmt.Sprintf("Pulling %s: ", imageFull)
s.Writer = os.Stdout
s.Start()
defer s.Stop()
}
if err := podman.Pull(imageFull); err != nil {
return false, fmt.Errorf("failed to pull image %s", imageFull)
}
return true, nil
}

View file

@ -5,6 +5,8 @@ go 1.13
require (
github.com/HarryMichal/go-version v1.0.0
github.com/acobaugh/osrelease v0.0.0-20181218015638-a93a0a55a249
github.com/briandowns/spinner v1.10.0
github.com/coreos/go-systemd/v22 v22.0.0
github.com/godbus/dbus/v5 v5.0.3
github.com/sirupsen/logrus v1.5.0
github.com/spf13/cobra v0.0.6

View file

@ -10,18 +10,25 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/briandowns/spinner v1.10.0 h1:753NIJC2NHmPyVoPVWS+wh9eDx5umqe2U+JgX+KoTag=
github.com/briandowns/spinner v1.10.0/go.mod h1:QOuQk7x+EaDASo80FEXwlwiA+j/PPIcX3FScO+3/ZPQ=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e h1:Wf6HqHfScWJN9/ZjdUKyjop4mf3Qdd+1TvvltAvM3m8=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd/v22 v22.0.0 h1:XJIw/+VlJ+87J+doOxznsAWIdmWuViOVhkQamW5YV28=
github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
@ -57,6 +64,10 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
@ -120,6 +131,7 @@ golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=