diff --git a/internal/pkg/runtime/launcher/oci/launcher_linux.go b/internal/pkg/runtime/launcher/oci/launcher_linux.go index 3454a346da..67a00bd536 100644 --- a/internal/pkg/runtime/launcher/oci/launcher_linux.go +++ b/internal/pkg/runtime/launcher/oci/launcher_linux.go @@ -100,15 +100,10 @@ func checkOpts(lo launcher.Options) error { badOpt = append(badOpt, "NoHome") } - if len(lo.BindPaths) > 0 { - badOpt = append(badOpt, "BindPaths") - } if len(lo.FuseMount) > 0 { badOpt = append(badOpt, "FuseMount") } - if len(lo.Mounts) > 0 { - badOpt = append(badOpt, "Mounts") - } + if len(lo.NoMount) > 0 { badOpt = append(badOpt, "NoMount") } diff --git a/internal/pkg/runtime/launcher/oci/mounts_linux.go b/internal/pkg/runtime/launcher/oci/mounts_linux.go index 434672bd26..5858f8958c 100644 --- a/internal/pkg/runtime/launcher/oci/mounts_linux.go +++ b/internal/pkg/runtime/launcher/oci/mounts_linux.go @@ -15,8 +15,12 @@ package oci import ( "fmt" "os" + "path/filepath" + "strings" "github.com/apptainer/apptainer/internal/pkg/util/user" + "github.com/apptainer/apptainer/pkg/sylog" + "github.com/apptainer/apptainer/pkg/util/bind" "github.com/opencontainers/runtime-spec/specs-go" ) @@ -25,15 +29,16 @@ func (l *Launcher) getMounts() ([]specs.Mount, error) { mounts := &[]specs.Mount{} l.addProcMount(mounts) l.addSysMount(mounts) - err := l.addDevMounts(mounts) - if err != nil { + if err := l.addDevMounts(mounts); err != nil { return nil, fmt.Errorf("while configuring devpts mount: %w", err) } l.addTmpMounts(mounts) - err = l.addHomeMount(mounts) - if err != nil { + if err := l.addHomeMount(mounts); err != nil { return nil, fmt.Errorf("while configuring home mount: %w", err) } + if err := l.addBindMounts(mounts); err != nil { + return nil, fmt.Errorf("while configuring bind mount(s): %w", err) + } return *mounts, nil } @@ -189,3 +194,60 @@ func (l *Launcher) addHomeMount(mounts *[]specs.Mount) error { }) return nil } + +func (l *Launcher) addBindMounts(mounts *[]specs.Mount) error { + // First get binds from -B/--bind and env var + binds, err := bind.ParseBindPath(strings.Join(l.cfg.BindPaths, ",")) + if err != nil { + return fmt.Errorf("while parsing bind path: %w", err) + } + // Now add binds from one or more --mount and env var. + for _, m := range l.cfg.Mounts { + bps, err := bind.ParseMountString(m) + if err != nil { + return fmt.Errorf("while parsing mount %q: %w", m, err) + } + binds = append(binds, bps...) + } + + for _, b := range binds { + if !l.apptainerConf.UserBindControl { + sylog.Warningf("Ignoring bind mount request: user bind control disabled by system administrator") + return nil + } + if err := addBindMount(mounts, b); err != nil { + return fmt.Errorf("while adding mount %q: %w", b.Source, err) + } + } + return nil +} + +func addBindMount(mounts *[]specs.Mount, b bind.BindPath) error { + if b.ID() != "" || b.ImageSrc() != "" { + return fmt.Errorf("image binds are not yet supported by the OCI runtime") + } + + opts := []string{"rbind", "nosuid", "nodev"} + if b.Readonly() { + opts = append(opts, "ro") + } + + absSource, err := filepath.Abs(b.Source) + if err != nil { + return fmt.Errorf("cannot determine absolute path of %s: %w", b.Source, err) + } + if _, err := os.Stat(absSource); err != nil { + return fmt.Errorf("cannot stat bind source %s: %w", b.Source, err) + } + + sylog.Debugf("Adding bind of %s to %s, with options %v", absSource, b.Destination, opts) + + *mounts = append(*mounts, + specs.Mount{ + Source: absSource, + Destination: b.Destination, + Type: "none", + Options: opts, + }) + return nil +} diff --git a/internal/pkg/runtime/launcher/oci/mounts_linux_test.go b/internal/pkg/runtime/launcher/oci/mounts_linux_test.go new file mode 100644 index 0000000000..93585faa20 --- /dev/null +++ b/internal/pkg/runtime/launcher/oci/mounts_linux_test.go @@ -0,0 +1,268 @@ +// Copyright (c) Contributors to the Apptainer project, established as +// Apptainer a Series of LF Projects LLC. +// For website terms of use, trademark policy, privacy policy and other +// project policies see https://lfprojects.org/policies +// Copyright (c) 2022, Sylabs Inc. All rights reserved. +// This software is licensed under a 3-clause BSD license. Please consult the +// LICENSE.md file distributed with the sources of this project regarding your +// rights to use or distribute this software. + +// Package oci implements a Launcher that will configure and launch a container +// with an OCI runtime. It also provides implementations of OCI state +// transitions that can be called directly, Create/Start/Kill etc. +package oci + +import ( + "reflect" + "testing" + + "github.com/apptainer/apptainer/internal/pkg/runtime/launcher" + "github.com/apptainer/apptainer/pkg/util/bind" + "github.com/apptainer/apptainer/pkg/util/apptainerconf" + "github.com/opencontainers/runtime-spec/specs-go" +) + +func Test_addBindMount(t *testing.T) { + tests := []struct { + name string + b bind.BindPath + wantMounts *[]specs.Mount + wantErr bool + }{ + { + name: "Valid", + b: bind.BindPath{ + Source: "/tmp", + Destination: "/tmp", + }, + wantMounts: &[]specs.Mount{ + { + Source: "/tmp", + Destination: "/tmp", + Type: "none", + Options: []string{"rbind", "nosuid", "nodev"}, + }, + }, + }, + { + name: "ValidRO", + b: bind.BindPath{ + Source: "/tmp", + Destination: "/tmp", + Options: map[string]*bind.BindOption{"ro": {}}, + }, + wantMounts: &[]specs.Mount{ + { + Source: "/tmp", + Destination: "/tmp", + Type: "none", + Options: []string{"rbind", "nosuid", "nodev", "ro"}, + }, + }, + }, + { + name: "BadSource", + b: bind.BindPath{ + Source: "doesnotexist!", + Destination: "/mnt", + }, + wantMounts: &[]specs.Mount{}, + wantErr: true, + }, + { + name: "ImageID", + b: bind.BindPath{ + Source: "/myimage.sif", + Destination: "/mnt", + Options: map[string]*bind.BindOption{"id": {Value: "4"}}, + }, + wantMounts: &[]specs.Mount{}, + wantErr: true, + }, + { + name: "ImageSrc", + b: bind.BindPath{ + Source: "/myimage.sif", + Destination: "/mnt", + Options: map[string]*bind.BindOption{"img-src": {Value: "/test"}}, + }, + wantMounts: &[]specs.Mount{}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mounts := &[]specs.Mount{} + err := addBindMount(mounts, tt.b) + if (err != nil) != tt.wantErr { + t.Errorf("addBindMount() error = %v, wantErr %v", err, tt.wantErr) + } + if !reflect.DeepEqual(mounts, tt.wantMounts) { + t.Errorf("addBindMount() want %v, got %v", tt.wantMounts, mounts) + } + }) + } +} + +func TestLauncher_addBindMounts(t *testing.T) { + tests := []struct { + name string + cfg launcher.Options + userbind bool + wantMounts *[]specs.Mount + wantErr bool + }{ + { + name: "Disabled", + cfg: launcher.Options{ + BindPaths: []string{"/tmp"}, + }, + wantMounts: &[]specs.Mount{}, + wantErr: false, + }, + { + name: "ValidBindSrc", + cfg: launcher.Options{ + BindPaths: []string{"/tmp"}, + }, + userbind: true, + wantMounts: &[]specs.Mount{ + { + Source: "/tmp", + Destination: "/tmp", + Type: "none", + Options: []string{"rbind", "nosuid", "nodev"}, + }, + }, + wantErr: false, + }, + { + name: "ValidBindSrcDst", + cfg: launcher.Options{ + BindPaths: []string{"/tmp:/mnt"}, + }, + userbind: true, + wantMounts: &[]specs.Mount{ + { + Source: "/tmp", + Destination: "/mnt", + Type: "none", + Options: []string{"rbind", "nosuid", "nodev"}, + }, + }, + wantErr: false, + }, + { + name: "ValidBindRO", + cfg: launcher.Options{ + BindPaths: []string{"/tmp:/mnt:ro"}, + }, + userbind: true, + wantMounts: &[]specs.Mount{ + { + Source: "/tmp", + Destination: "/mnt", + Type: "none", + Options: []string{"rbind", "nosuid", "nodev", "ro"}, + }, + }, + wantErr: false, + }, + { + name: "InvalidBindSrc", + cfg: launcher.Options{ + BindPaths: []string{"!doesnotexist"}, + }, + userbind: true, + wantMounts: &[]specs.Mount{}, + wantErr: true, + }, + { + name: "UnsupportedBindID", + cfg: launcher.Options{ + BindPaths: []string{"my.sif:/mnt:id=2"}, + }, + userbind: true, + wantMounts: &[]specs.Mount{}, + wantErr: true, + }, + { + name: "UnsupportedBindImgSrc", + cfg: launcher.Options{ + BindPaths: []string{"my.sif:/mnt:img-src=/test"}, + }, + userbind: true, + wantMounts: &[]specs.Mount{}, + wantErr: true, + }, + { + name: "ValidMount", + cfg: launcher.Options{ + Mounts: []string{"type=bind,source=/tmp,destination=/mnt"}, + }, + userbind: true, + wantMounts: &[]specs.Mount{ + { + Source: "/tmp", + Destination: "/mnt", + Type: "none", + Options: []string{"rbind", "nosuid", "nodev"}, + }, + }, + wantErr: false, + }, + { + name: "ValidMountRO", + cfg: launcher.Options{ + Mounts: []string{"type=bind,source=/tmp,destination=/mnt,ro"}, + }, + userbind: true, + wantMounts: &[]specs.Mount{ + { + Source: "/tmp", + Destination: "/mnt", + Type: "none", + Options: []string{"rbind", "nosuid", "nodev", "ro"}, + }, + }, + wantErr: false, + }, + { + name: "UnsupportedMountID", + cfg: launcher.Options{ + Mounts: []string{"type=bind,source=my.sif,destination=/mnt,id=2"}, + }, + userbind: true, + wantMounts: &[]specs.Mount{}, + wantErr: true, + }, + { + name: "UnsupportedMountImgSrc", + cfg: launcher.Options{ + Mounts: []string{"type=bind,source=my.sif,destination=/mnt,image-src=/test"}, + }, + userbind: true, + wantMounts: &[]specs.Mount{}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := &Launcher{ + cfg: tt.cfg, + apptainerConf: &apptainerconf.File{}, + } + if tt.userbind { + l.apptainerConf.UserBindControl = true + } + mounts := &[]specs.Mount{} + err := l.addBindMounts(mounts) + if (err != nil) != tt.wantErr { + t.Errorf("addBindMount() error = %v, wantErr %v", err, tt.wantErr) + } + if !reflect.DeepEqual(mounts, tt.wantMounts) { + t.Errorf("addBindMount() want %v, got %v", tt.wantMounts, mounts) + } + }) + } +}