Skip to content

Commit

Permalink
Merge pull request #235 from edytuk/sylabs1208
Browse files Browse the repository at this point in the history
feat: oci: honor USER in image config, from sylabs 1208
  • Loading branch information
edytuk authored Feb 20, 2023
2 parents 9a7f5b9 + b1451d5 commit 8c9de2b
Show file tree
Hide file tree
Showing 12 changed files with 732 additions and 439 deletions.
2 changes: 1 addition & 1 deletion LICENSE.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ reserved.

Copyright (c) 2017, SingularityWare, LLC. All rights reserved.

Copyright (c) 2018-2022, Sylabs, Inc. All rights reserved.
Copyright (c) 2018-2023, Sylabs, Inc. All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
Expand Down
1 change: 1 addition & 0 deletions LICENSE_THIRD_PARTY.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ The source files:

* `pkg/sypgp/testdata_test.go`
* `internal/pkg/util/user/cgo_lookup_unix.go`
* `internal/pkg/util/passwdfile/passwdfile_unix.go`

Contain code from the Go project.

Expand Down
51 changes: 50 additions & 1 deletion e2e/docker/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// 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) 2019-2022 Sylabs Inc. All rights reserved.
// Copyright (c) 2019-2023 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.
Expand Down Expand Up @@ -827,6 +827,54 @@ func (c ctx) testDockerCMDENTRYPOINT(t *testing.T) {
}
}

// Check that the USER in a docker container is honored under --oci mode
func (c ctx) testDockerUSER(t *testing.T) {
tests := []struct {
name string
expectOutput string
profile e2e.Profile
}{
// Sanity check apptainer native engine... no support for USER
{
name: "default",
profile: e2e.UserProfile,
expectOutput: fmt.Sprintf("uid=%d(%s) gid=%d",
e2e.UserProfile.ContainerUser(t).UID,
e2e.UserProfile.ContainerUser(t).Name,
e2e.UserProfile.ContainerUser(t).GID),
},
// `--oci` modes (USER honored by default)
{
name: "OCIUser",
profile: e2e.OCIUserProfile,
expectOutput: `uid=2000(testuser) gid=2000(testgroup)`,
},
{
name: "OCIFakeroot",
profile: e2e.OCIFakerootProfile,
expectOutput: `uid=0(root) gid=0(root)`,
},
{
name: "OCIRoot",
profile: e2e.OCIRootProfile,
expectOutput: `uid=2000(testuser) gid=2000(testgroup)`,
},
}

for _, tt := range tests {
c.env.RunApptainer(
t,
e2e.AsSubtest(tt.name),
e2e.WithProfile(tt.profile),
e2e.WithCommand("run"),
e2e.WithArgs("docker://ghcr.io/apptainer/docker-user"),
e2e.ExpectExit(0,
e2e.ExpectOutput(e2e.ContainMatch, tt.expectOutput),
),
)
}
}

// E2ETests is the main func to trigger the test suite
func E2ETests(env e2e.TestEnv) testhelper.Tests {
c := ctx{
Expand All @@ -848,6 +896,7 @@ func E2ETests(env e2e.TestEnv) testhelper.Tests {
t.Run("entrypoint", c.testDockerENTRYPOINT)
t.Run("cmdentrypoint", c.testDockerCMDENTRYPOINT)
t.Run("cmd quotes", c.testDockerCMDQuotes)
t.Run("user", c.testDockerUSER)
// Regressions
t.Run("issue 4524", c.issue4524)
},
Expand Down
139 changes: 87 additions & 52 deletions internal/pkg/runtime/launcher/oci/launcher_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// 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.
// Copyright (c) 2022-2023, 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.
Expand All @@ -19,14 +19,15 @@ import (
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"syscall"

"github.com/apptainer/apptainer/internal/pkg/buildcfg"
"github.com/apptainer/apptainer/internal/pkg/cache"
"github.com/apptainer/apptainer/internal/pkg/runtime/launcher"
"github.com/apptainer/apptainer/internal/pkg/util/fs/files"
"github.com/apptainer/apptainer/internal/pkg/util/user"
"github.com/apptainer/apptainer/pkg/ocibundle"
"github.com/apptainer/apptainer/pkg/ocibundle/native"
"github.com/apptainer/apptainer/pkg/ocibundle/tools"
"github.com/apptainer/apptainer/pkg/syfs"
Expand All @@ -36,7 +37,6 @@ import (
"github.com/containers/image/v5/types"
"github.com/google/uuid"
"github.com/opencontainers/runtime-spec/specs-go"
"golang.org/x/term"
)

var (
Expand Down Expand Up @@ -216,52 +216,103 @@ func checkOpts(lo launcher.Options) error {
return nil
}

// createSpec produces an OCI runtime specification, suitable to launch a
// container. This spec excludes ProcessArgs and Env, as these have to be
// computed where the image config is available, to account for the image's CMD
// / ENTRYPOINT / ENV.
// 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 /
// ENTRYPOINT / ENV / USER.
func (l *Launcher) createSpec() (*specs.Spec, error) {
spec := minimalSpec()

// Override the default Process.Terminal to false if our stdin is not a terminal.
if !term.IsTerminal(syscall.Stdin) {
spec.Process.Terminal = false
spec = addNamespaces(spec, l.cfg.Namespaces)

mounts, err := l.getMounts()
if err != nil {
return nil, err
}
spec.Mounts = mounts

spec.Process.User = l.getProcessUser()
return &spec, nil
}

// If we are *not* requesting fakeroot, then we need to map the container
// uid back to host uid, through the initial fakeroot userns.
if !l.cfg.Fakeroot && os.Getuid() != 0 {
uidMap, gidMap, err := l.getReverseUserMaps()
// finalizeSpec updates the bundle config, filling in Process config that depends on the image spec.
func (l *Launcher) finalizeSpec(ctx context.Context, b ocibundle.Bundle, spec *specs.Spec, image string, process string, args []string) (err error) {
imgSpec := b.ImageSpec()
if imgSpec == nil {
return fmt.Errorf("bundle has no image spec")
}

// In the absence of a USER in the OCI image config, we will run the
// container process as our current user / group.
currentUID := uint32(os.Getuid())
currentGID := uint32(os.Getgid())
targetUID := currentUID
targetGID := currentGID
containerUser := false

// If the OCI image config specifies a USER we will:
// * When unprivileged - run as that user, via nested subuid/gid mappings (host user -> userns root -> OCI USER)
// * When privileged - directly run as that user, as a host uid/gid.
if imgSpec.Config.User != "" {
imgUser, err := tools.BundleUser(b.Path(), imgSpec.Config.User)
if err != nil {
return err
}
imgUID, err := strconv.ParseUint(imgUser.Uid, 10, 32)
if err != nil {
return nil, err
return err
}
imgGID, err := strconv.ParseUint(imgUser.Gid, 10, 32)
if err != nil {
return err
}
targetUID = uint32(imgUID)
targetGID = uint32(imgGID)
containerUser = true
sylog.Debugf("Running as USER specified in OCI image config %d:%d", targetUID, targetGID)
}

// Fakeroot always overrides to give us root in the container (via userns & idmap if unprivileged).
if l.cfg.Fakeroot {
targetUID = 0
targetGID = 0
}

if targetUID != 0 && currentUID != 0 {
uidMap, gidMap, err := l.getReverseUserMaps(targetUID, targetGID)
if err != nil {
return err
}
spec.Linux.UIDMappings = uidMap
spec.Linux.GIDMappings = gidMap
}

spec = addNamespaces(spec, l.cfg.Namespaces)

cwd, err := l.getProcessCwd()
if err != nil {
return nil, err
u := specs.User{
UID: targetUID,
GID: targetGID,
}
spec.Process.Cwd = cwd

mounts, err := l.getMounts()
specProcess, err := l.getProcess(ctx, *imgSpec, image, b.Path(), process, args, u)
if err != nil {
return nil, err
return err
}
spec.Process = specProcess
if err := b.Update(ctx, spec); err != nil {
return err
}
spec.Mounts = mounts

return &spec, nil
// If we are entering as root, or a USER defined in the container, then passwd/group
// information should be present already.
if targetUID == 0 || containerUser {
return nil
}
// Otherewise, add to the passwd and group files in the container.
if err := l.updatePasswdGroup(tools.RootFs(b.Path()).Path(), targetUID, targetGID); err != nil {
return err
}
return nil
}

func (l *Launcher) updatePasswdGroup(rootfs string) error {
uid := os.Getuid()
gid := os.Getgid()

func (l *Launcher) updatePasswdGroup(rootfs string, uid, gid uint32) error {
if os.Getuid() == 0 || l.cfg.Fakeroot {
return nil
}
Expand All @@ -275,7 +326,7 @@ func (l *Launcher) updatePasswdGroup(rootfs string) error {
}

sylog.Debugf("Updating passwd file: %s", containerPasswd)
content, err := files.Passwd(containerPasswd, pw.Dir, uid)
content, err := files.Passwd(containerPasswd, pw.Dir, int(uid))
if err != nil {
return fmt.Errorf("while creating passwd file: %w", err)
}
Expand All @@ -284,7 +335,7 @@ func (l *Launcher) updatePasswdGroup(rootfs string) error {
}

sylog.Debugf("Updating group file: %s", containerGroup)
content, err = files.Group(containerGroup, uid, []int{gid})
content, err = files.Group(containerGroup, int(uid), []int{int(gid)})
if err != nil {
return fmt.Errorf("while creating group file: %w", err)
}
Expand Down Expand Up @@ -338,44 +389,28 @@ func (l *Launcher) Exec(ctx context.Context, image string, process string, args
}
}

// Create OCI runtime spec, excluding the Process settings which must consider the image spec.
spec, err := l.createSpec()
if err != nil {
return fmt.Errorf("while creating OCI spec: %w", err)
}

// Assemble the runtime & user-requested environment, which will be merged
// with the image ENV and set in the container at runtime.
rtEnv := defaultEnv(image, bundleDir)
// APPTAINERENV_ has lowest priority
rtEnv = mergeMap(rtEnv, apptainerEnvMap())
// --env-file can override APPTAINERENV_
if l.cfg.EnvFile != "" {
e, err := envFileMap(ctx, l.cfg.EnvFile)
if err != nil {
return err
}
rtEnv = mergeMap(rtEnv, e)
}
// --env flag can override --env-file and APPTAINERENV_
rtEnv = mergeMap(rtEnv, l.cfg.Env)

// Create a bundle - obtain and extract the image.
b, err := native.New(
native.OptBundlePath(bundleDir),
native.OptImageRef(image),
native.OptSysCtx(sysCtx),
native.OptImgCache(imgCache),
native.OptProcessArgs(process, args),
native.OptProcessEnv(rtEnv),
)
if err != nil {
return err
}

if err := b.Create(ctx, spec); err != nil {
return err
}

if err := l.updatePasswdGroup(tools.RootFs(b.Path()).Path()); err != nil {
// With reference to the bundle's image spec, now set the process configuration.
if err := l.finalizeSpec(ctx, b, spec, image, process, args); err != nil {
return err
}

Expand Down
Loading

0 comments on commit 8c9de2b

Please sign in to comment.