Skip to content

Commit

Permalink
chore: refactor runtime spec handling out of bundle
Browse files Browse the repository at this point in the history
Bring all handling of the runtime spec out of the native oci bundle
package, up into the oci launcher.

We obtain the bundle's image spec after it is downloaded / extracted.
The launcher then computes the correct Process config and updates the
bundle with it.

This is required so that we can handle an image USER in the launcher.
  • Loading branch information
dtrudg committed Jan 9, 2023
1 parent 1a1a90b commit f0299d3
Show file tree
Hide file tree
Showing 8 changed files with 410 additions and 395 deletions.
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
44 changes: 15 additions & 29 deletions internal/pkg/runtime/launcher/oci/launcher_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import (
"os/exec"
"path/filepath"
"strings"
"syscall"

"github.com/containers/image/v5/types"
"github.com/google/uuid"
Expand All @@ -33,7 +32,6 @@ import (
"github.com/sylabs/singularity/pkg/sylog"
"github.com/sylabs/singularity/pkg/util/singularityconf"
useragent "github.com/sylabs/singularity/pkg/util/user-agent"
"golang.org/x/term"
)

var (
Expand Down Expand Up @@ -213,19 +211,12 @@ func checkOpts(lo launcher.Options) error {
}

// createSpec produces an OCI runtime specification, suitable to launch a
// container. This spec excludes ProcessArgs and Env, as these have to be
// container. This spec excludes the Process config, as this have to be
// computed where the image config is available, to account for the image's CMD
// / ENTRYPOINT / ENV.
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.Process.User = l.getProcessUser()

// 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 {
Expand Down Expand Up @@ -343,43 +334,38 @@ 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)
// SINGULARITYENV_ has lowest priority
rtEnv = mergeMap(rtEnv, singularityEnvMap())
// --env-file can override SINGULARITYENV_
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 SINGULARITYENV_
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
}

// With reference to the bundle's image spec, now set the process configuration.
imgSpec := b.ImageSpec()
if imgSpec == nil {
return fmt.Errorf("bundle has no image spec")
}
specProcess, err := l.getProcess(ctx, *imgSpec, image, b.Path(), process, args)
if err != nil {
return err
}
spec.Process = specProcess
b.Update(ctx, spec)

if err := l.updatePasswdGroup(tools.RootFs(b.Path()).Path()); err != nil {
return err
}
Expand Down
133 changes: 133 additions & 0 deletions internal/pkg/runtime/launcher/oci/process_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,84 @@ import (
"fmt"
"os"
"strings"
"syscall"

imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/opencontainers/runtime-spec/specs-go"
"github.com/sylabs/singularity/internal/pkg/fakeroot"
"github.com/sylabs/singularity/internal/pkg/runtime/engine/config/oci/generate"
"github.com/sylabs/singularity/internal/pkg/util/env"
"github.com/sylabs/singularity/internal/pkg/util/shell/interpreter"
"github.com/sylabs/singularity/internal/pkg/util/user"
"golang.org/x/term"
)

const singularityLibs = "/.singularity.d/libs"

func (l *Launcher) getProcess(ctx context.Context, imgSpec imgspecv1.Image, image, bundle, process string, args []string) (*specs.Process, error) {
// Assemble the runtime & user-requested environment, which will be merged
// with the image ENV and set in the container at runtime.
rtEnv := defaultEnv(image, bundle)
// SINGULARITYENV_ has lowest priority
rtEnv = mergeMap(rtEnv, singularityEnvMap())
// --env-file can override SINGULARITYENV_
if l.cfg.EnvFile != "" {
e, err := envFileMap(ctx, l.cfg.EnvFile)
if err != nil {
return nil, err
}
rtEnv = mergeMap(rtEnv, e)
}
// --env flag can override --env-file and SINGULARITYENV_
rtEnv = mergeMap(rtEnv, l.cfg.Env)

cwd, err := l.getProcessCwd()
if err != nil {
return nil, err
}

p := specs.Process{
Args: getProcessArgs(imgSpec, process, args),
Cwd: cwd,
Env: getProcessEnv(imgSpec, rtEnv),
User: l.getProcessUser(),
Terminal: getProcessTerminal(),
}

return &p, nil
}

// getProcessTerminal determines whether the container process should run with a terminal.
func getProcessTerminal() bool {
// Override the default Process.Terminal to false if our stdin is not a terminal.
if term.IsTerminal(syscall.Stdin) {
return true
}
return false
}

// getProcessArgs returns the process args for a container, with reference to the OCI Image Spec.
// The process and image parameters may override the image CMD and/or ENTRYPOINT.
func getProcessArgs(imageSpec imgspecv1.Image, process string, args []string) []string {
var processArgs []string

if process != "" {
processArgs = []string{process}
} else {
processArgs = imageSpec.Config.Entrypoint
}

if len(args) > 0 {
processArgs = append(processArgs, args...)
} else {
if process == "" {
processArgs = append(processArgs, imageSpec.Config.Cmd...)
}
}

return processArgs
}

// getProcessUser computes the uid/gid(s) to be set on process execution.
// Currently this only supports the same uid / primary gid as on the host.
// TODO - expand for fakeroot, and arbitrary mapped user.
Expand Down Expand Up @@ -142,6 +212,69 @@ func (l *Launcher) getReverseUserMaps() (uidMap, gidMap []specs.LinuxIDMapping,
return uidMap, gidMap, nil
}

// getProcessEnv combines the image config ENV with the ENV requested at runtime.
// APPEND_PATH and PREPEND_PATH are honored as with the native singularity runtime.
// LD_LIBRARY_PATH is modified to always include the singularity lib bind directory.
func getProcessEnv(imageSpec imgspecv1.Image, runtimeEnv map[string]string) []string {
path := ""
appendPath := ""
prependPath := ""
ldLibraryPath := ""

// Start with the environment from the image config.
g := generate.New(nil)
g.Config.Process = &specs.Process{Env: imageSpec.Config.Env}

// Obtain PATH, and LD_LIBRARY_PATH if set in the image config, for special handling.
for _, env := range imageSpec.Config.Env {
e := strings.SplitN(env, "=", 2)
if len(e) < 2 {
continue
}
if e[0] == "PATH" {
path = e[1]
}
if e[0] == "LD_LIBRARY_PATH" {
ldLibraryPath = e[1]
}
}

// Apply env vars from runtime, except PATH and LD_LIBRARY_PATH releated.
for k, v := range runtimeEnv {
switch k {
case "PATH":
path = v
case "APPEND_PATH":
appendPath = v
case "PREPEND_PATH":
prependPath = v
case "LD_LIBRARY_PATH":
ldLibraryPath = v
default:
g.AddProcessEnv(k, v)
}
}

// Compute and set optionally APPEND-ed / PREPEND-ed PATH.
if appendPath != "" {
path = path + ":" + appendPath
}
if prependPath != "" {
path = prependPath + ":" + path
}
if path != "" {
g.AddProcessEnv("PATH", path)
}

// Ensure LD_LIBRARY_PATH always contains singularity lib binding dir.
if !strings.Contains(ldLibraryPath, singularityLibs) {
ldLibraryPath = strings.TrimPrefix(ldLibraryPath+":"+singularityLibs, ":")
}
g.AddProcessEnv("LD_LIBRARY_PATH", ldLibraryPath)

return g.Config.Process.Env
}

// defaultEnv returns default environment variables set in the container.
func defaultEnv(image, bundle string) map[string]string {
return map[string]string{
Expand Down
Loading

0 comments on commit f0299d3

Please sign in to comment.