Skip to content

Commit

Permalink
refactor: split home dir parsing and mounting
Browse files Browse the repository at this point in the history
Signed-off-by: Edita Kizinevic <edita.kizinevic@cern.ch>
  • Loading branch information
dtrudg authored and edytuk committed Jul 21, 2023
1 parent 454b0dc commit 434267a
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 81 deletions.
55 changes: 54 additions & 1 deletion internal/pkg/runtime/launcher/oci/launcher_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ var (
type Launcher struct {
cfg launcher.Options
apptainerConf *apptainerconf.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.
Expand All @@ -71,7 +77,17 @@ func NewLauncher(opts ...launcher.Option) (*Launcher, error) {
return nil, fmt.Errorf("apptainer configuration is not initialized")
}

return &Launcher{cfg: lo, apptainerConf: c}, nil
homeSrc, homeDest, err := parseHomeDir(lo.HomeDir, lo.CustomHome, lo.Fakeroot)
if err != nil {
return nil, err
}

return &Launcher{
cfg: lo,
apptainerConf: c,
homeSrc: homeSrc,
homeDest: homeDest,
}, nil
}

// checkOpts ensures that options set are supported by the oci.Launcher.
Expand Down Expand Up @@ -183,6 +199,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 (<src>:<dest>), 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 /
Expand Down
38 changes: 33 additions & 5 deletions internal/pkg/runtime/launcher/oci/launcher_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
package oci

import (
"os/user"
"reflect"
"testing"

Expand All @@ -28,23 +29,50 @@ func TestNewLauncher(t *testing.T) {
}
apptainerconf.SetCurrentConfig(sc)

u, err := user.Current()
if err != nil {
t.Fatalf("while getting current user: %s", err)
}

tests := []struct {
name string
opts []launcher.Option
want *Launcher
wantErr bool
}{
{
name: "default",
want: &Launcher{apptainerConf: sc},
name: "default",
want: &Launcher{
apptainerConf: sc,
homeSrc: "",
homeDest: u.HomeDir,
},
},
{
name: "homeDest",
opts: []launcher.Option{
launcher.OptHome("/home/dest", true, false),
},
want: &Launcher{
cfg: launcher.Options{HomeDir: "/home/dest", CustomHome: true},
apptainerConf: sc,
homeSrc: "",
homeDest: "/home/dest",
},
wantErr: false,
},
{
name: "validOption",
name: "homeSrcDest",
opts: []launcher.Option{
launcher.OptHome("/home/test", false, false),
launcher.OptHome("/home/src:/home/dest", true, false),
},
want: &Launcher{cfg: launcher.Options{HomeDir: "/home/test"}, apptainerConf: sc},
want: &Launcher{
cfg: launcher.Options{HomeDir: "/home/src:/home/dest", CustomHome: true},
apptainerConf: sc,
homeSrc: "/home/src",
homeDest: "/home/dest",
},
wantErr: false,
},
{
name: "unsupportedOption",
Expand Down
108 changes: 34 additions & 74 deletions internal/pkg/runtime/launcher/oci/mounts_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,101 +265,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.apptainerConf.MountHome {
sylog.Debugf("Skipping mount of $HOME due to apptainer.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.apptainerConf.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.apptainerConf.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.apptainerConf.SessiondirMaxSize),
fmt.Sprintf("uid=%d", pw.UID),
fmt.Sprintf("gid=%d", pw.GID),
},
Options: tmpfsOpt,
})

return nil
Expand Down
2 changes: 1 addition & 1 deletion internal/pkg/runtime/launcher/oci/process_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func (l *Launcher) getProcess(ctx context.Context, imgSpec imgspecv1.Image, imag

// Ensure HOME points to the required home directory, even if it is a custom one, unless the container explicitly specifies its USER, in which case we don't want to touch HOME.
if imgSpec.Config.User == "" {
rtEnv["HOME"] = l.cfg.HomeDir
rtEnv["HOME"] = l.homeDest
}

cwd, err := l.getProcessCwd()
Expand Down

0 comments on commit 434267a

Please sign in to comment.