diff --git a/cmd/internal/cli/actions.go b/cmd/internal/cli/actions.go index fc9c470db8..5fd30b3dc5 100644 --- a/cmd/internal/cli/actions.go +++ b/cmd/internal/cli/actions.go @@ -243,10 +243,19 @@ var ShellCmd = &cobra.Command{ image := args[0] containerCmd := "/.singularity.d/actions/shell" containerArgs := []string{} - // OCI runtime does not use an action script + // OCI runtime does not use an action script, but must match behavior. + // See - internal/pkg/util/fs/files/action_scripts.go (case shell). if ociRuntime { - // TODO - needs to have bash -> sh fallback logic implemented somewhere. - containerCmd = "/bin/sh" + // APPTAINER_SHELL or --shell has priority + if shellPath != "" { + containerCmd = shellPath + // Clear the shellPath - not handled internally by the OCI runtime, as we exec directly without an action script. + shellPath = "" + } else { + // Otherwise try to exec /bin/bash --norc, falling back to /bin/sh + containerCmd = "/bin/sh" + containerArgs = []string{"-c", "test -x /bin/bash && PS1='Apptainer> ' exec /bin/bash --norc || PS1='Apptainer> ' exec /bin/sh"} + } } setVM(cmd) if vm { diff --git a/e2e/actions/actions.go b/e2e/actions/actions.go index 0359501aeb..18be6e73e3 100644 --- a/e2e/actions/actions.go +++ b/e2e/actions/actions.go @@ -2869,7 +2869,8 @@ func E2ETests(env e2e.TestEnv) testhelper.Tests { // // OCI Runtime Mode // - "ociRun": c.actionOciRun, // apptainer run --oci - "ociExec": c.actionOciExec, // apptainer exec --oci + "ociRun": c.actionOciRun, // apptainer run --oci + "ociExec": c.actionOciExec, // apptainer exec --oci + "ociShell": c.actionOciShell, // apptainer shell --oci } } diff --git a/e2e/actions/oci.go b/e2e/actions/oci.go index 2798a24ec1..67d51e38f3 100644 --- a/e2e/actions/oci.go +++ b/e2e/actions/oci.go @@ -146,3 +146,53 @@ func (c actionTests) actionOciExec(t *testing.T) { ) } } + +// Shell interaction tests +func (c actionTests) actionOciShell(t *testing.T) { + e2e.EnsureOCIImage(t, c.env) + + tests := []struct { + name string + argv []string + consoleOps []e2e.ApptainerConsoleOp + exit int + }{ + { + name: "ShellExit", + argv: []string{"oci-archive:" + c.env.OCIImagePath}, + consoleOps: []e2e.ApptainerConsoleOp{ + // "cd /" to work around issue where a long + // working directory name causes the test + // to fail because the "Apptainer" that + // we are looking for is chopped from the + // front. + // TODO(mem): This test was added back in 491a71716013654acb2276e4b37c2e015d2dfe09 + e2e.ConsoleSendLine("cd /"), + e2e.ConsoleExpect("Apptainer"), + e2e.ConsoleSendLine("exit"), + }, + exit: 0, + }, + { + name: "ShellBadCommand", + argv: []string{"oci-archive:" + c.env.OCIImagePath}, + consoleOps: []e2e.ApptainerConsoleOp{ + e2e.ConsoleSendLine("_a_fake_command"), + e2e.ConsoleSendLine("exit"), + }, + exit: 127, + }, + } + + for _, tt := range tests { + c.env.RunApptainer( + t, + e2e.AsSubtest(tt.name), + e2e.WithProfile(e2e.OCIUserProfile), + e2e.WithCommand("shell"), + e2e.WithArgs(tt.argv...), + e2e.ConsoleRun(tt.consoleOps...), + e2e.ExpectExit(tt.exit), + ) + } +} diff --git a/internal/pkg/runtime/launcher/launcher.go b/internal/pkg/runtime/launcher/launcher.go index e3c316bf04..812349835d 100644 --- a/internal/pkg/runtime/launcher/launcher.go +++ b/internal/pkg/runtime/launcher/launcher.go @@ -27,7 +27,7 @@ import "context" type Launcher interface { // 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 + // must be launched as a background instance, otherwise it must run // interactively, attached to the console. Exec(ctx context.Context, image string, process string, args []string, instanceName string) error }