From 67a1241f442f439486569b8551c99da5c6ffb224 Mon Sep 17 00:00:00 2001 From: David Trudgian Date: Mon, 5 Dec 2022 17:03:05 +0000 Subject: [PATCH] feat: oci: support --env option in --oci mode * Merge image config ENV and env vars requested by user with the --env CLI option. * Set default SINGULARITY_CONTAINER and SINGULARITY_NAME env variables. * Set default LD_LIBRARY_PATH to be used later for library injection (this is a singularity default). Fixes #1029 --- .../runtime/launcher/oci/launcher_linux.go | 23 ++++-- .../pkg/runtime/launcher/oci/process_linux.go | 8 ++ pkg/ocibundle/native/bundle_linux.go | 77 +++++++++++++++++++ 3 files changed, 103 insertions(+), 5 deletions(-) diff --git a/internal/pkg/runtime/launcher/oci/launcher_linux.go b/internal/pkg/runtime/launcher/oci/launcher_linux.go index 1d604ac820..8b3b68db9c 100644 --- a/internal/pkg/runtime/launcher/oci/launcher_linux.go +++ b/internal/pkg/runtime/launcher/oci/launcher_linux.go @@ -127,9 +127,6 @@ func checkOpts(lo launcher.Options) error { badOpt = append(badOpt, "Proot") } - if len(lo.Env) > 0 { - badOpt = append(badOpt, "Env") - } if lo.EnvFile != "" { badOpt = append(badOpt, "EnvFile") } @@ -230,8 +227,9 @@ func checkOpts(lo launcher.Options) error { } // createSpec produces an OCI runtime specification, suitable to launch a -// container. This spec excludes ProcessArgs, as these have to be computed where -// the image config is available, to account for the image's CMD / ENTRYPOINT. +// 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. func (l *Launcher) createSpec() (*specs.Spec, error) { spec := minimalSpec() @@ -318,12 +316,20 @@ func (l *Launcher) Exec(ctx context.Context, image string, process string, args 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) + // --env flag + rtEnv = mergeMap(rtEnv, l.cfg.Env) + // TODO - --env-file, SINGULARITYENV_ + 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 @@ -351,3 +357,10 @@ func (l *Launcher) Exec(ctx context.Context, image string, process string, args } return err } + +func mergeMap(a map[string]string, b map[string]string) map[string]string { + for k, v := range b { + a[k] = v + } + return a +} diff --git a/internal/pkg/runtime/launcher/oci/process_linux.go b/internal/pkg/runtime/launcher/oci/process_linux.go index 83d53b3f2a..598e97e373 100644 --- a/internal/pkg/runtime/launcher/oci/process_linux.go +++ b/internal/pkg/runtime/launcher/oci/process_linux.go @@ -137,3 +137,11 @@ func (l *Launcher) getReverseUserMaps() (uidMap, gidMap []specs.LinuxIDMapping, return uidMap, gidMap, nil } + +// defaultEnv returns default environment variables set in the container. +func defaultEnv(image, bundle string) map[string]string { + return map[string]string{ + "SINGULARITY_CONTAINER": bundle, + "SINGULARITY_NAME": image, + } +} diff --git a/pkg/ocibundle/native/bundle_linux.go b/pkg/ocibundle/native/bundle_linux.go index 6053401af3..8385f8c7e1 100644 --- a/pkg/ocibundle/native/bundle_linux.go +++ b/pkg/ocibundle/native/bundle_linux.go @@ -35,6 +35,8 @@ import ( "github.com/sylabs/singularity/pkg/sylog" ) +const singularityLibs = "/.singularity.d/libs" + // Bundle is a native OCI bundle, created from imageRef. type Bundle struct { // imageRef is the reference to the OCI image source, e.g. docker://ubuntu:latest. @@ -53,6 +55,8 @@ type Bundle struct { process string // args are the command arguments, which may override the image's CMD. args []string + // env is the container environment to set, which will be merged with the image's env. + env map[string]string // Generic bundle properties ocibundle.Bundle } @@ -104,6 +108,14 @@ func OptProcessArgs(process string, args []string) Option { } } +// OptEnv sets the environment to be set, merged with the image ENV. +func OptProcessEnv(env map[string]string) Option { + return func(b *Bundle) error { + b.env = env + return nil + } +} + // New returns a bundle interface to create/delete an OCI bundle from an OCI image ref. func New(opts ...Option) (ocibundle.Bundle, error) { b := Bundle{ @@ -156,6 +168,8 @@ func (b *Bundle) Create(ctx context.Context, ociConfig *specs.Spec) error { // consult the image Config to handle combining ENTRYPOINT/CMD with user // provided args. b.setProcessArgs(g) + // Ditto for environment handling (merge image and user/rt requested). + b.setProcessEnv(g) return b.writeConfig(g) } @@ -185,6 +199,69 @@ func (b *Bundle) setProcessArgs(g *generate.Generator) { g.SetProcessArgs(processArgs) } +// setProcessEnv compines the image config ENV with the ENV requested in the runtime provided spec. +// 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 (b *Bundle) setProcessEnv(g *generate.Generator) { + if g.Config == nil { + g.Config = &specs.Spec{} + } + if g.Config.Process == nil { + g.Config.Process = &specs.Process{} + } + g.Config.Process.Env = b.imageSpec.Config.Env + + path := "" + appendPath := "" + prependPath := "" + ldLibraryPath := "" + + // Obtain PATH, and LD_LIBRARY_PATH if set in the image config. + for _, env := range b.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 spec, except PATH and LD_LIBRARY_PATH releated. + for k, v := range b.env { + 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 + } + 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) +} + func (b *Bundle) writeConfig(g *generate.Generator) error { return tools.SaveBundleConfig(b.bundlePath, g) }