Skip to content

Commit

Permalink
Add --compat short-hand for improved OCI/Docker compatibility
Browse files Browse the repository at this point in the history
It is common for users to run docker containers that expect more
isolation than is default in Singularity, and that can create files on
startup. This is a simple short-hand to enable `--contain-all,
--no-init, --no-umask, --writable-tmpfs`. These options give the best
chance of an OCI/Docker container working as expected but *without*
requiring the user/uts/net namespaces that we can't rely on in all
installations / configurations of Singularity.

Fixes: sylabs/singularity#75
  • Loading branch information
dtrudg authored and DrDaveD committed Oct 6, 2021
1 parent e69dec7 commit 3e59058
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 10 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
- `--writable-tmpfs` can be used with `singularity build` to run
the `%test` section of the build with a ephemeral tmpfs overlay,
permitting tests that write to the container filesystem.
- `--compat` flag for actions is a new short-hand to enable a number of
options that increase OCI/Docker compatibility. Infers `--containall,
--no-init, --no-umask, --writable-tmpfs`. Does not use user, uts, or
network namespaces as these may not be supported on many installations.

### Changed defaults / behaviours

Expand Down
12 changes: 12 additions & 0 deletions cmd/internal/cli/action_flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ var (
IsBoot bool
IsFakeroot bool
IsCleanEnv bool
IsCompat bool
IsContained bool
IsContainAll bool
IsWritable bool
Expand Down Expand Up @@ -342,6 +343,16 @@ var actionCleanEnvFlag = cmdline.Flag{
EnvKeys: []string{"CLEANENV"},
}

// --compat
var actionCompatFlag = cmdline.Flag{
ID: "actionCompatFlag",
Value: &IsCompat,
DefaultValue: false,
Name: "compat",
Usage: "apply settings for increased OCI/Docker compatibility. Infers --containall, --no-init, --no-umask, --writable-tmpfs.",
EnvKeys: []string{"COMPAT"},
}

// -c|--contain
var actionContainFlag = cmdline.Flag{
ID: "actionContainFlag",
Expand Down Expand Up @@ -643,6 +654,7 @@ func init() {
cmdManager.RegisterFlagForCmd(&actionApplyCgroupsFlag, actionsInstanceCmd...)
cmdManager.RegisterFlagForCmd(&actionBindFlag, actionsInstanceCmd...)
cmdManager.RegisterFlagForCmd(&actionCleanEnvFlag, actionsInstanceCmd...)
cmdManager.RegisterFlagForCmd(&actionCompatFlag, actionsInstanceCmd...)
cmdManager.RegisterFlagForCmd(&actionContainAllFlag, actionsInstanceCmd...)
cmdManager.RegisterFlagForCmd(&actionContainFlag, actionsInstanceCmd...)
cmdManager.RegisterFlagForCmd(&actionContainLibsFlag, actionsInstanceCmd...)
Expand Down
10 changes: 10 additions & 0 deletions cmd/internal/cli/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,16 @@ func actionPreRun(cmd *cobra.Command, args []string) {
// set PATH after pulling images to be able to find potential
// docker credential helpers outside of standard paths
os.Setenv("PATH", defaultPath)

// --compat infers other options that give increased OCI / Docker compatibility
// Excludes uts/user/net namespaces as these are restrictive for many Singularity
// installs.
if IsCompat {
IsContainAll = true
IsWritableTmpfs = true
NoInit = true
NoUmask = true
}
}

func handleOCI(ctx context.Context, imgCache *cache.Handle, cmd *cobra.Command, pullFrom string) (string, error) {
Expand Down
76 changes: 66 additions & 10 deletions e2e/actions/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,19 +86,19 @@ func (c actionTests) actionExec(t *testing.T) {
user := e2e.CurrentUser(t)

// Create a temp testfile
testdata, err := fs.MakeTmpDir(c.env.TestDir, "testdata", 0755)
testdata, err := fs.MakeTmpDir(c.env.TestDir, "testdata", 0o755)
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(testdata)

testdataTmp := filepath.Join(testdata, "tmp")
if err := os.Mkdir(testdataTmp, 0755); err != nil {
if err := os.Mkdir(testdataTmp, 0o755); err != nil {
t.Fatal(err)
}

// Create a temp testfile
tmpfile, err := fs.MakeTmpFile(testdataTmp, "testSingularityExec.", 0644)
tmpfile, err := fs.MakeTmpFile(testdataTmp, "testSingularityExec.", 0o644)
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -1088,7 +1088,7 @@ func (c actionTests) actionBinds(t *testing.T) {
}
})(t)

if err := fs.Mkdir(hostCanaryDir, 0777); err != nil {
if err := fs.Mkdir(hostCanaryDir, 0o777); err != nil {
t.Fatalf("failed to create canary_dir: %s", err)
}
if err := fs.Touch(hostCanaryFile); err != nil {
Expand All @@ -1100,13 +1100,13 @@ func (c actionTests) actionBinds(t *testing.T) {
if err := fs.Touch(hostCanaryFileWithColon); err != nil {
t.Fatalf("failed to create canary_file_colon: %s", err)
}
if err := os.Chmod(hostCanaryFile, 0777); err != nil {
if err := os.Chmod(hostCanaryFile, 0o777); err != nil {
t.Fatalf("failed to apply permissions on canary_file: %s", err)
}
if err := fs.Mkdir(hostHomeDir, 0777); err != nil {
if err := fs.Mkdir(hostHomeDir, 0o777); err != nil {
t.Fatalf("failed to create workspace home directory: %s", err)
}
if err := fs.Mkdir(hostWorkDir, 0777); err != nil {
if err := fs.Mkdir(hostWorkDir, 0o777); err != nil {
t.Fatalf("failed to create workspace work directory: %s", err)
}
}
Expand Down Expand Up @@ -1607,7 +1607,7 @@ func (c actionTests) fuseMount(t *testing.T) {
t.Errorf("could not read ssh private key: %s", err)
return
}
if err := ioutil.WriteFile(userPrivKey, content, 0600); err != nil {
if err := ioutil.WriteFile(userPrivKey, content, 0o600); err != nil {
t.Errorf("could not write ssh user private key: %s", err)
return
}
Expand Down Expand Up @@ -1813,7 +1813,7 @@ func (c actionTests) bindImage(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if err := os.Chmod(squashDir, 0755); err != nil {
if err := os.Chmod(squashDir, 0o755); err != nil {
t.Fatal(err)
}

Expand Down Expand Up @@ -2118,7 +2118,6 @@ func (c actionTests) actionUmask(t *testing.T) {
e2e.ExpectOutput(e2e.ExactMatch, "0022"),
),
)

}

func (c actionTests) actionNoMount(t *testing.T) {
Expand Down Expand Up @@ -2237,6 +2236,62 @@ func (c actionTests) actionNoMount(t *testing.T) {
}
}

// actionCompat checks that the --compat flag sets up the expected environment
// for improved oci/docker compatibility
func (c actionTests) actionCompat(t *testing.T) {
e2e.EnsureImage(t, c.env)

type test struct {
name string
args []string
exitCode int
expect e2e.SingularityCmdResultOp
}

tests := []test{
{
name: "containall",
args: []string{"--compat", c.env.ImagePath, "sh", "-c", "ls -lah $HOME"},
exitCode: 0,
expect: e2e.ExpectOutput(e2e.ContainMatch, "total 0"),
},
{
name: "writable-tmpfs",
args: []string{"--compat", c.env.ImagePath, "sh", "-c", "touch /test"},
exitCode: 0,
},
{
name: "no-init",
args: []string{"--compat", c.env.ImagePath, "sh", "-c", "ps"},
exitCode: 0,
expect: e2e.ExpectOutput(e2e.UnwantedContainMatch, "sinit"),
},
{
name: "no-umask",
args: []string{"--compat", c.env.ImagePath, "sh", "-c", "umask"},
exitCode: 0,
expect: e2e.ExpectOutput(e2e.ContainMatch, "0022"),
},
}

oldUmask := syscall.Umask(0)
defer syscall.Umask(oldUmask)

for _, tt := range tests {
c.env.RunSingularity(
t,
e2e.AsSubtest(tt.name),
e2e.WithProfile(e2e.UserProfile),
e2e.WithCommand("exec"),
e2e.WithArgs(tt.args...),
e2e.ExpectExit(
tt.exitCode,
tt.expect,
),
)
}
}

// E2ETests is the main func to trigger the test suite
func E2ETests(env e2e.TestEnv) testhelper.Tests {
c := actionTests{
Expand Down Expand Up @@ -2278,6 +2333,7 @@ func E2ETests(env e2e.TestEnv) testhelper.Tests {
"bind image": c.bindImage, // test bind image
"umask": c.actionUmask, // test umask propagation
"no-mount": c.actionNoMount, // test --no-mount
"compat": c.actionCompat, // test --compat
"invalidRemote": np(c.invalidRemote), // GHSA-5mv9-q7fq-9394
}
}

0 comments on commit 3e59058

Please sign in to comment.