Skip to content

Commit

Permalink
feat: exec / run args support for --oci mode
Browse files Browse the repository at this point in the history
When using `run` or `exec` with the `--oci` runtime mode, accept
arguments on the command line.

For `run`, the arguments override any CMD specified by the image.

For `exec`, the arguments replace ENTRYPOINT/CMD entirely, bypassing
the process configuration in the image config.

This mirrors the behavior of Singularity images today, via the exec
and run runscripts - but is implemented in the OCI bundle config,
rather than a script in the container.

Closes sylabs/singularity#1024

Closes sylabs/singularity#1092

Signed-off-by: Edita Kizinevic <[email protected]>
  • Loading branch information
dtrudg authored and edytuk committed Feb 23, 2023
1 parent 9b7bfa7 commit c20c184
Show file tree
Hide file tree
Showing 5 changed files with 185 additions and 27 deletions.
10 changes: 5 additions & 5 deletions internal/pkg/runtime/launcher/launcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ import "context"
// It will execute a runtime, such as Apptainer's native runtime (via the starter
// binary), or an external OCI runtime (e.g. runc).
type Launcher interface {
// Exec will execute the container image 'image', passing arguments 'args'
// the container#s initial process. If instanceName is specified, the
// container must be launched as a background instance, otherwist it must
// run interactively, attached to the console.
Exec(ctx context.Context, image string, cmd string, args []string, instanceName string) error
// Exec will execute the container image 'image', starting 'process', and
// passing arguments 'args'. If instanceName is specified, the container
// must be launched as a background instance, otherwist it must run
// interactively, attached to the console.
Exec(ctx context.Context, image string, process string, args []string, instanceName string) error
}
4 changes: 2 additions & 2 deletions internal/pkg/runtime/launcher/native/launcher_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ func NewLauncher(opts ...launcher.Option) (*Launcher, error) {
// This includes interactive containers, instances, and joining an existing instance.
//
//nolint:maintidx
func (l *Launcher) Exec(ctx context.Context, image string, cmd string, args []string, instanceName string) error {
func (l *Launcher) Exec(ctx context.Context, image string, process string, args []string, instanceName string) error {
var err error

var fakerootPath string
Expand Down Expand Up @@ -168,7 +168,7 @@ func (l *Launcher) Exec(ctx context.Context, image string, cmd string, args []st
}

// Native runtime expects command to execute as arg[0]
args = append([]string{cmd}, args...)
args = append([]string{process}, args...)

// Set arguments to pass to contained process.
l.generator.SetProcessArgs(args)
Expand Down
11 changes: 2 additions & 9 deletions internal/pkg/runtime/launcher/oci/launcher_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,19 +243,11 @@ func checkOpts(lo launcher.Options) error {

// Exec will interactively execute a container via the runc low-level runtime.
// image is a reference to an OCI image, e.g. docker://ubuntu or oci:/tmp/mycontainer
func (l *Launcher) Exec(ctx context.Context, image string, cmd string, args []string, instanceName string) error {
func (l *Launcher) Exec(ctx context.Context, image string, process string, args []string, instanceName string) error {
if instanceName != "" {
return fmt.Errorf("%w: instanceName", ErrNotImplemented)
}

if cmd != "" {
return fmt.Errorf("%w: cmd %v", ErrNotImplemented, cmd)
}

if len(args) > 0 {
return fmt.Errorf("%w: args %v", ErrNotImplemented, args)
}

bundleDir, err := os.MkdirTemp("", "oci-bundle")
if err != nil {
return nil
Expand Down Expand Up @@ -297,6 +289,7 @@ func (l *Launcher) Exec(ctx context.Context, image string, cmd string, args []st
native.OptImageRef(image),
native.OptSysCtx(sysCtx),
native.OptImgCache(imgCache),
native.OptProcessArgs(process, args),
)
if err != nil {
return err
Expand Down
60 changes: 49 additions & 11 deletions pkg/ocibundle/native/bundle_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ type Bundle struct {
// Note that we only use the 'blob' cache section. The 'oci-tmp' cache section holds
// OCI->SIF conversions, which are not used here.
imgCache *cache.Handle
// process is the command to execute, which may override the image's ENTRYPOINT / CMD.
process string
// args are the command arguments, which may override the image's CMD.
args []string
// Generic bundle properties
ocibundle.Bundle
}
Expand Down Expand Up @@ -95,6 +99,15 @@ func OptImgCache(ic *cache.Handle) Option {
}
}

// OptProcessArgs sets the command and arguments to run in the container.
func OptProcessArgs(process string, args []string) Option {
return func(b *Bundle) error {
b.process = process
b.args = args
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 @@ -144,27 +157,52 @@ func (b *Bundle) Create(ctx context.Context, ociConfig *specs.Spec) error {
return err
}

b.setProcessArgs(g)
// TODO - Handle custom env and user
b.setProcessEnv(g)
b.setProcessUser(g)

return b.writeConfig(g)
}

// Path returns the bundle's path on disk.
func (b *Bundle) Path() string {
return b.bundlePath
}

func (b *Bundle) setProcessUser(g *generate.Generator) {
// Set non-root uid/gid per Apptainer defaults
uid := uint32(os.Getuid())
if uid != 0 {
gid := uint32(os.Getgid())
g.Config.Process.User.UID = uid
g.Config.Process.User.GID = gid
}
// Set default ENV from image
}

func (b *Bundle) setProcessEnv(g *generate.Generator) {
// Set default ENV values from image
g.Config.Process.Env = append(g.Config.Process.Env, b.imageSpec.Config.Env...)
// Set default exec from image CMD & Entrypoint
if b.imageSpec == nil {
return fmt.Errorf("imageSpec cannot be nil")
}
args := append(b.imageSpec.Config.Entrypoint, b.imageSpec.Config.Cmd...)
g.SetProcessArgs(args)
return b.writeConfig(g)
}

// Path returns the bundle's path on disk.
func (b *Bundle) Path() string {
return b.bundlePath
func (b *Bundle) setProcessArgs(g *generate.Generator) {
var processArgs []string

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

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

g.SetProcessArgs(processArgs)
}

func (b *Bundle) writeConfig(g *generate.Generator) error {
Expand Down
127 changes: 127 additions & 0 deletions pkg/ocibundle/native/bundle_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@ import (
"net/http"
"os"
"os/exec"
"reflect"
"testing"

"github.com/apptainer/apptainer/internal/pkg/cache"
"github.com/apptainer/apptainer/internal/pkg/runtime/engine/config/oci"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/opencontainers/runtime-tools/validate"
)

Expand Down Expand Up @@ -141,3 +144,127 @@ func TestFromImageRef(t *testing.T) {
})
}
}

func TestSetProcessArgs(t *testing.T) {
tests := []struct {
name string
imgEntrypoint []string
imgCmd []string
bundleProcess string
bundleArgs []string
expectProcessArgs []string
}{
{
name: "imageEntrypointOnly",
imgEntrypoint: []string{"ENTRYPOINT"},
imgCmd: []string{},
bundleProcess: "",
bundleArgs: []string{},
expectProcessArgs: []string{"ENTRYPOINT"},
},
{
name: "imageCmdOnly",
imgEntrypoint: []string{},
imgCmd: []string{"CMD"},
bundleProcess: "",
bundleArgs: []string{},
expectProcessArgs: []string{"CMD"},
},
{
name: "imageEntrypointCMD",
imgEntrypoint: []string{"ENTRYPOINT"},
imgCmd: []string{"CMD"},
bundleProcess: "",
bundleArgs: []string{},
expectProcessArgs: []string{"ENTRYPOINT", "CMD"},
},
{
name: "ProcessOnly",
imgEntrypoint: []string{},
imgCmd: []string{},
bundleProcess: "PROCESS",
bundleArgs: []string{},
expectProcessArgs: []string{"PROCESS"},
},
{
name: "ArgsOnly",
imgEntrypoint: []string{},
imgCmd: []string{},
bundleProcess: "",
bundleArgs: []string{"ARGS"},
expectProcessArgs: []string{"ARGS"},
},
{
name: "ProcessArgs",
imgEntrypoint: []string{},
imgCmd: []string{},
bundleProcess: "PROCESS",
bundleArgs: []string{"ARGS"},
expectProcessArgs: []string{"PROCESS", "ARGS"},
},
{
name: "overrideEntrypointOnlyProcess",
imgEntrypoint: []string{"ENTRYPOINT"},
imgCmd: []string{},
bundleProcess: "PROCESS",
bundleArgs: []string{},
expectProcessArgs: []string{"PROCESS"},
},
{
name: "overrideCmdOnlyArgs",
imgEntrypoint: []string{},
imgCmd: []string{"CMD"},
bundleProcess: "",
bundleArgs: []string{"ARGS"},
expectProcessArgs: []string{"ARGS"},
},
{
name: "overrideBothProcess",
imgEntrypoint: []string{"ENTRYPOINT"},
imgCmd: []string{"CMD"},
bundleProcess: "PROCESS",
bundleArgs: []string{},
expectProcessArgs: []string{"PROCESS"},
},
{
name: "overrideBothArgs",
imgEntrypoint: []string{"ENTRYPOINT"},
imgCmd: []string{"CMD"},
bundleProcess: "",
bundleArgs: []string{"ARGS"},
expectProcessArgs: []string{"ENTRYPOINT", "ARGS"},
},
{
name: "overrideBothProcessArgs",
imgEntrypoint: []string{"ENTRYPOINT"},
imgCmd: []string{"CMD"},
bundleProcess: "PROCESS",
bundleArgs: []string{"ARGS"},
expectProcessArgs: []string{"PROCESS", "ARGS"},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
b := Bundle{
imageSpec: &v1.Image{
Config: v1.ImageConfig{
Entrypoint: tt.imgEntrypoint,
Cmd: tt.imgCmd,
},
},
process: tt.bundleProcess,
args: tt.bundleArgs,
}

g, err := oci.DefaultConfig()
if err != nil {
t.Fatal(err)
}
b.setProcessArgs(g)
if !reflect.DeepEqual(g.Config.Process.Args, tt.expectProcessArgs) {
t.Errorf("Expected: %v, Got: %v", tt.expectProcessArgs, g.Config.Process.Args)
}
})
}
}

0 comments on commit c20c184

Please sign in to comment.