Skip to content

Commit

Permalink
feat: oci: enable bind mounts via --bind, --mount
Browse files Browse the repository at this point in the history
Implement support for bind mounts (rw & ro) specified using -B/--bind
and --mount on the singularity command line.

Fixes sylabs/singularity#1027

Signed-off-by: Edita Kizinevic <[email protected]>
  • Loading branch information
dtrudg authored and edytuk committed Jul 11, 2023
1 parent 986994a commit 7a221ca
Show file tree
Hide file tree
Showing 3 changed files with 334 additions and 10 deletions.
7 changes: 1 addition & 6 deletions internal/pkg/runtime/launcher/oci/launcher_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down
69 changes: 65 additions & 4 deletions internal/pkg/runtime/launcher/oci/mounts_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@ package oci
import (
"fmt"
"os"
"path/filepath"

"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"
)

Expand All @@ -25,15 +28,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
}

Expand Down Expand Up @@ -189,3 +193,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(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
}
268 changes: 268 additions & 0 deletions internal/pkg/runtime/launcher/oci/mounts_linux_test.go
Original file line number Diff line number Diff line change
@@ -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/apptainerconf"
"github.com/apptainer/apptainer/pkg/util/bind"
"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)
}
})
}
}

0 comments on commit 7a221ca

Please sign in to comment.