From e794d9cbc211de15dbebfe76aa2303f16095542d Mon Sep 17 00:00:00 2001 From: David Trudgian Date: Fri, 16 Jun 2023 17:58:41 +0100 Subject: [PATCH] refactor: split home dir parsing and mounting --- .../runtime/launcher/oci/launcher_linux.go | 55 ++++++++- .../pkg/runtime/launcher/oci/mounts_linux.go | 108 ++++++------------ 2 files changed, 88 insertions(+), 75 deletions(-) diff --git a/internal/pkg/runtime/launcher/oci/launcher_linux.go b/internal/pkg/runtime/launcher/oci/launcher_linux.go index a6292a9599..816ce9866e 100644 --- a/internal/pkg/runtime/launcher/oci/launcher_linux.go +++ b/internal/pkg/runtime/launcher/oci/launcher_linux.go @@ -47,6 +47,12 @@ var ( type Launcher struct { cfg launcher.Options singularityConf *singularityconf.File + // homeSrc is the computed source (on the host) for the user's home directory. + // An empty value indicates there is no source on the host, and a tmpfs will be used. + homeSrc string + // homeDest is the computed destination (in the container) for the user's home directory. + // An empty value is not valid at mount time. + homeDest string } // NewLauncher returns a oci.Launcher with an initial configuration set by opts. @@ -67,7 +73,17 @@ func NewLauncher(opts ...launcher.Option) (*Launcher, error) { return nil, fmt.Errorf("singularity configuration is not initialized") } - return &Launcher{cfg: lo, singularityConf: c}, nil + homeSrc, homeDest, err := parseHomeDir(lo.HomeDir, lo.CustomHome, lo.Fakeroot) + if err != nil { + return nil, err + } + + return &Launcher{ + cfg: lo, + singularityConf: c, + homeSrc: homeSrc, + homeDest: homeDest, + }, nil } // checkOpts ensures that options set are supported by the oci.Launcher. @@ -186,6 +202,43 @@ func checkOpts(lo launcher.Options) error { return nil } +// parseHomeDir parses the homedir value passed from the CLI layer into a host source, and container dest. +// This includes handling fakeroot and custom --home dst, or --home src:dst specifications. +func parseHomeDir(homedir string, custom, fakeroot bool) (src, dest string, err error) { + // Get the host user's information, looking outside of a user namespace if necessary. + pw, err := rootless.GetUser() + if err != nil { + return "", "", err + } + + // By default in --oci mode there is no external source for $HOME, i.e. a `tmpfs` will be used. + src = "" + // By default the destination in the container matches the users's $HOME on the host. + dest = pw.HomeDir + + // --fakeroot means we are root in the container so $HOME=/root + if fakeroot { + dest = "/root" + } + + // If the user set a custom --home via the CLI, then override the defaults. + if custom { + homeSlice := strings.Split(homedir, ":") + if len(homeSlice) < 1 || len(homeSlice) > 2 { + return "", "", fmt.Errorf("home argument has incorrect number of elements: %v", homeSlice) + } + // A single path was provided, so we will be mounting a tmpfs on this custom destination. + dest = homeSlice[0] + + // Two paths provided (:), so we will be bind mounting from host to container. + if len(homeSlice) > 1 { + src = homeSlice[0] + dest = homeSlice[1] + } + } + return src, dest, nil +} + // createSpec creates an initial OCI runtime specification, suitable to launch a // container. This spec excludes the Process config, as this has to be computed // where the image config is available, to account for the image's CMD / diff --git a/internal/pkg/runtime/launcher/oci/mounts_linux.go b/internal/pkg/runtime/launcher/oci/mounts_linux.go index 32c3d7b1b0..f6eb022367 100644 --- a/internal/pkg/runtime/launcher/oci/mounts_linux.go +++ b/internal/pkg/runtime/launcher/oci/mounts_linux.go @@ -261,101 +261,61 @@ func (l *Launcher) addSysMount(mounts *[]specs.Mount) error { return nil } -// addHomeMount adds a user home directory as a tmpfs mount, and sets the -// container home directory. We are currently emulating `--compat` / -// `--containall`, so the user must specifically bind in their home directory -// from the host for it to be available. +// addHomeMount adds the user home directory to the container, according to the +// src and dest computed by parseHomeDir from launcher.New. func (l *Launcher) addHomeMount(mounts *[]specs.Mount) error { - // If the $HOME mount is skipped by config or --no-home, we still need to - // handle setting the correct $HOME dir, but just skip adding the mount. - skipMount := false if !l.singularityConf.MountHome { sylog.Debugf("Skipping mount of $HOME due to singularity.conf") - skipMount = true + return nil } if l.cfg.NoHome { sylog.Debugf("Skipping mount of $HOME due to --no-home") - skipMount = true + return nil } - // Get the host user's data - pw, err := user.CurrentOriginal() - if err != nil { - return err + if l.homeDest == "" { + return fmt.Errorf("cannot add home mount with empty destination") } - if l.cfg.CustomHome { - // Handle any user request to override the home directory source/dest - homeSlice := strings.Split(l.cfg.HomeDir, ":") - if len(homeSlice) < 1 || len(homeSlice) > 2 { - return fmt.Errorf("home argument has incorrect number of elements: %v", homeSlice) - } - homeSrc := homeSlice[0] - l.cfg.HomeDir = homeSrc - - // User requested more than just a custom home dir; they want to bind-mount a directory in the host to this custom home dir. - // This means the home dir, as viewed from inside the container, is actually the second member of the ":"-separated slice. The first member is actually just the source portion of the requested bind-mount. - if len(homeSlice) > 1 { - homeDest := homeSlice[1] - l.cfg.HomeDir = homeDest + // If l.homeSrc is set, then we are simply bind mounting from the host. + if l.homeSrc != "" { + return addBindMount(mounts, bind.Path{ + Source: l.homeSrc, + Destination: l.homeDest, + }) + } - if skipMount { - return nil - } + // Otherwise we setup a tmpfs, mounted onto l.homeDst. + tmpfsOpt := []string{ + "nosuid", + "relatime", + "mode=755", + fmt.Sprintf("size=%dm", l.singularityConf.SessiondirMaxSize), + } - // Since the home dir is a bind-mount in this case, we don't have to mount a tmpfs directory for the in-container home dir, and we can just do the bind-mount & return. - return addBindMount(mounts, bind.Path{ - Source: homeSrc, - Destination: homeDest, - }) + // If we aren't using fakeroot, ensure the tmpfs ownership is correct for our real uid/gid. + if !l.cfg.Fakeroot { + uid, err := rootless.Getuid() + if err != nil { + return fmt.Errorf("while fetching uid: %w", err) } - } else { - // If we're running in fake-root mode (and we haven't requested a custom home dir), we do need to create a tmpfs mount for the home dir, but it's a special case (because of its location & permissions), so we handle that here & return. - if l.cfg.Fakeroot { - l.cfg.HomeDir = "/root" - - if skipMount { - return nil - } - - *mounts = append(*mounts, - specs.Mount{ - Destination: "/root", - Type: "tmpfs", - Source: "tmpfs", - Options: []string{ - "nosuid", - "relatime", - "mode=755", - fmt.Sprintf("size=%dm", l.singularityConf.SessiondirMaxSize), - }, - }) - - return nil + gid, err := rootless.Getgid() + if err != nil { + return fmt.Errorf("while fetching gid: %w", err) } - // No fakeroot and no custom home dir - so the in-container home dir will be named the same as the host user's home dir. (Though note that it'll still be a tmpfs mount! It'll get mounted by the catch-all mount append, below.) - l.cfg.HomeDir = pw.Dir - } - - if skipMount { - return nil + tmpfsOpt = append(tmpfsOpt, + fmt.Sprintf("uid=%d", uid), + fmt.Sprintf("gid=%d", gid), + ) } - // If we've not hit a special case (bind-mounted custom home dir, or fakeroot), then create a tmpfs mount as a home dir in the requested location (whether it's custom or not; by this point, l.cfg.HomeDir will reflect the right value). *mounts = append(*mounts, specs.Mount{ - Destination: l.cfg.HomeDir, + Destination: l.homeDest, Type: "tmpfs", Source: "tmpfs", - Options: []string{ - "nosuid", - "relatime", - "mode=755", - fmt.Sprintf("size=%dm", l.singularityConf.SessiondirMaxSize), - fmt.Sprintf("uid=%d", pw.UID), - fmt.Sprintf("gid=%d", pw.GID), - }, + Options: tmpfsOpt, }) return nil