From f8e1286080f1b762ba31fd7aaac43d48c34d5169 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 sylabs/singularity#1029 Signed-off-by: Edita Kizinevic --- .../runtime/launcher/oci/launcher_linux.go | 23 +++- .../pkg/runtime/launcher/oci/process_linux.go | 8 ++ pkg/ocibundle/native/bundle_linux.go | 79 +++++++++++ pkg/ocibundle/native/bundle_linux_test.go | 123 ++++++++++++++++++ 4 files changed, 228 insertions(+), 5 deletions(-) diff --git a/internal/pkg/runtime/launcher/oci/launcher_linux.go b/internal/pkg/runtime/launcher/oci/launcher_linux.go index 67a00bd536..7c9f2db864 100644 --- a/internal/pkg/runtime/launcher/oci/launcher_linux.go +++ b/internal/pkg/runtime/launcher/oci/launcher_linux.go @@ -128,9 +128,6 @@ func checkOpts(lo launcher.Options) error { badOpt = append(badOpt, "ContainLibs") } - if len(lo.Env) > 0 { - badOpt = append(badOpt, "Env") - } if lo.EnvFile != "" { badOpt = append(badOpt, "EnvFile") } @@ -231,8 +228,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() @@ -319,12 +317,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, APPTAINERENV_ + 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 @@ -352,3 +358,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 21dd98fdc5..cf6780d686 100644 --- a/internal/pkg/runtime/launcher/oci/process_linux.go +++ b/internal/pkg/runtime/launcher/oci/process_linux.go @@ -141,3 +141,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{ + "APPTAINER_CONTAINER": bundle, + "APPTAINER_NAME": image, + } +} diff --git a/pkg/ocibundle/native/bundle_linux.go b/pkg/ocibundle/native/bundle_linux.go index 6573396a43..203eb89b65 100644 --- a/pkg/ocibundle/native/bundle_linux.go +++ b/pkg/ocibundle/native/bundle_linux.go @@ -39,6 +39,8 @@ import ( "github.com/opencontainers/umoci/pkg/idtools" ) +const apptainerLibs = "/.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. @@ -57,6 +59,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 } @@ -108,6 +112,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{ @@ -160,6 +172,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) } @@ -189,6 +203,71 @@ func (b *Bundle) setProcessArgs(g *generate.Generator) { g.SetProcessArgs(processArgs) } +// setProcessEnv combines the image config ENV with the ENV requested in the runtime provided spec. +// APPEND_PATH and PREPEND_PATH are honored as with the native apptainer runtime. +// LD_LIBRARY_PATH is modified to always include the apptainer 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.SetProcessEnv(k, v) + } + } + + // Compute and set optionally APPEND-ed / PREPEND-ed PATH. + if appendPath != "" { + path = path + ":" + appendPath + } + if prependPath != "" { + path = prependPath + ":" + path + } + if path != "" { + g.SetProcessEnv("PATH", path) + } + + // Ensure LD_LIBRARY_PATH always contains apptainer lib binding dir. + if !strings.Contains(ldLibraryPath, apptainerLibs) { + ldLibraryPath = strings.TrimPrefix(ldLibraryPath+":"+apptainerLibs, ":") + } + g.SetProcessEnv("LD_LIBRARY_PATH", ldLibraryPath) +} + func (b *Bundle) writeConfig(g *generate.Generator) error { return tools.SaveBundleConfig(b.bundlePath, g) } diff --git a/pkg/ocibundle/native/bundle_linux_test.go b/pkg/ocibundle/native/bundle_linux_test.go index 77e6b03a2f..ee29e9f7b9 100644 --- a/pkg/ocibundle/native/bundle_linux_test.go +++ b/pkg/ocibundle/native/bundle_linux_test.go @@ -21,6 +21,7 @@ import ( "github.com/apptainer/apptainer/internal/pkg/cache" "github.com/apptainer/apptainer/internal/pkg/runtime/engine/config/oci" + "github.com/apptainer/apptainer/internal/pkg/runtime/engine/config/oci/generate" v1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/opencontainers/runtime-tools/validate" ) @@ -268,3 +269,125 @@ func TestSetProcessArgs(t *testing.T) { }) } } + +func TestSetProcessEnv(t *testing.T) { + tests := []struct { + name string + imageEnv []string + bundleEnv map[string]string + wantEnv []string + }{ + { + name: "Default", + imageEnv: []string{}, + bundleEnv: map[string]string{}, + wantEnv: []string{"LD_LIBRARY_PATH=/.singularity.d/libs"}, + }, + { + name: "ImagePath", + imageEnv: []string{"PATH=/foo"}, + bundleEnv: map[string]string{}, + wantEnv: []string{ + "PATH=/foo", + "LD_LIBRARY_PATH=/.singularity.d/libs", + }, + }, + { + name: "OverridePath", + imageEnv: []string{"PATH=/foo"}, + bundleEnv: map[string]string{"PATH": "/bar"}, + wantEnv: []string{ + "PATH=/bar", + "LD_LIBRARY_PATH=/.singularity.d/libs", + }, + }, + { + name: "AppendPath", + imageEnv: []string{"PATH=/foo"}, + bundleEnv: map[string]string{"APPEND_PATH": "/bar"}, + wantEnv: []string{ + "PATH=/foo:/bar", + "LD_LIBRARY_PATH=/.singularity.d/libs", + }, + }, + { + name: "PrependPath", + imageEnv: []string{"PATH=/foo"}, + bundleEnv: map[string]string{"PREPEND_PATH": "/bar"}, + wantEnv: []string{ + "PATH=/bar:/foo", + "LD_LIBRARY_PATH=/.singularity.d/libs", + }, + }, + { + name: "ImageLdLibraryPath", + imageEnv: []string{"LD_LIBRARY_PATH=/foo"}, + bundleEnv: map[string]string{}, + wantEnv: []string{ + "LD_LIBRARY_PATH=/foo:/.singularity.d/libs", + }, + }, + { + name: "BundleLdLibraryPath", + imageEnv: []string{}, + bundleEnv: map[string]string{"LD_LIBRARY_PATH": "/foo"}, + wantEnv: []string{ + "LD_LIBRARY_PATH=/foo:/.singularity.d/libs", + }, + }, + { + name: "OverrideLdLibraryPath", + imageEnv: []string{"LD_LIBRARY_PATH=/foo"}, + bundleEnv: map[string]string{"LD_LIBRARY_PATH": "/bar"}, + wantEnv: []string{ + "LD_LIBRARY_PATH=/bar:/.singularity.d/libs", + }, + }, + { + name: "ImageVar", + imageEnv: []string{"FOO=bar"}, + bundleEnv: map[string]string{}, + wantEnv: []string{ + "FOO=bar", + "LD_LIBRARY_PATH=/.singularity.d/libs", + }, + }, + { + name: "ImageOverride", + imageEnv: []string{"FOO=bar"}, + bundleEnv: map[string]string{"FOO": "baz"}, + wantEnv: []string{ + "FOO=baz", + "LD_LIBRARY_PATH=/.singularity.d/libs", + }, + }, + { + name: "ImageAdditional", + imageEnv: []string{"FOO=bar"}, + bundleEnv: map[string]string{"ABC": "123"}, + wantEnv: []string{ + "FOO=bar", + "ABC=123", + "LD_LIBRARY_PATH=/.singularity.d/libs", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + imgSpec := &v1.Image{ + Config: v1.ImageConfig{Env: tt.imageEnv}, + } + + b := &Bundle{ + imageSpec: imgSpec, + env: tt.bundleEnv, + } + g := &generate.Generator{} + b.setProcessEnv(g) + + if !reflect.DeepEqual(g.Config.Process.Env, tt.wantEnv) { + t.Errorf("want: %v, got: %v", tt.wantEnv, g.Config.Process.Env) + } + }) + } +}