Skip to content

Commit

Permalink
feat: oci: support --env option in --oci mode
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
dtrudg authored and edytuk committed Mar 31, 2023
1 parent bd72312 commit f8e1286
Show file tree
Hide file tree
Showing 4 changed files with 228 additions and 5 deletions.
23 changes: 18 additions & 5 deletions internal/pkg/runtime/launcher/oci/launcher_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
8 changes: 8 additions & 0 deletions internal/pkg/runtime/launcher/oci/process_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}
79 changes: 79 additions & 0 deletions pkg/ocibundle/native/bundle_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
}
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down
123 changes: 123 additions & 0 deletions pkg/ocibundle/native/bundle_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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)
}
})
}
}

0 comments on commit f8e1286

Please sign in to comment.