Skip to content

feat: support for -a and --attach in run #3157

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion cmd/nerdctl/container_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"errors"
"fmt"
"runtime"
"strings"

"github.com/containerd/console"
"github.com/containerd/log"
Expand Down Expand Up @@ -69,6 +70,7 @@ func newRunCommand() *cobra.Command {
setCreateFlags(runCommand)

runCommand.Flags().BoolP("detach", "d", false, "Run container in background and print container ID")
runCommand.Flags().StringSliceP("attach", "a", []string{}, "Attach STDIN, STDOUT, or STDERR")

return runCommand
}
Expand Down Expand Up @@ -304,6 +306,23 @@ func processCreateCommandFlagsInRun(cmd *cobra.Command) (opt types.ContainerCrea
if err != nil {
return
}
opt.Attach, err = cmd.Flags().GetStringSlice("attach")
if err != nil {
return
}

validAttachFlag := true
for i, str := range opt.Attach {
opt.Attach[i] = strings.ToUpper(str)

if opt.Attach[i] != "STDIN" && opt.Attach[i] != "STDOUT" && opt.Attach[i] != "STDERR" {
validAttachFlag = false
}
}
if !validAttachFlag {
return opt, fmt.Errorf("invalid stream specified with -a flag. Valid streams are STDIN, STDOUT, and STDERR")
}

return opt, nil
}

Expand All @@ -325,6 +344,10 @@ func runAction(cmd *cobra.Command, args []string) error {
return errors.New("flags -d and --rm cannot be specified together")
}

if len(createOpt.Attach) > 0 && createOpt.Detach {
return errors.New("flags -d and -a cannot be specified together")
}

netFlags, err := loadNetworkFlags(cmd)
if err != nil {
return fmt.Errorf("failed to load networking flags: %s", err)
Expand Down Expand Up @@ -381,7 +404,7 @@ func runAction(cmd *cobra.Command, args []string) error {
}
logURI := lab[labels.LogURI]
detachC := make(chan struct{})
task, err := taskutil.NewTask(ctx, client, c, false, createOpt.Interactive, createOpt.TTY, createOpt.Detach,
task, err := taskutil.NewTask(ctx, client, c, createOpt.Attach, createOpt.Interactive, createOpt.TTY, createOpt.Detach,
con, logURI, createOpt.DetachKeys, createOpt.GOptions.Namespace, detachC)
if err != nil {
return err
Expand Down
120 changes: 120 additions & 0 deletions cmd/nerdctl/container_run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -533,3 +533,123 @@ func TestRunRmTime(t *testing.T) {
t.Fatalf("expected to have completed in %v, took %v", deadline, took)
}
}

func runAttachStdin(t *testing.T, testStr string, args []string) string {
if runtime.GOOS == "windows" {
t.Skip("run attach test is not yet implemented on Windows")
}

t.Parallel()
base := testutil.NewBase(t)
containerName := testutil.Identifier(t)

opts := []func(*testutil.Cmd){
testutil.WithStdin(strings.NewReader("echo " + testStr + "\nexit\n")),
}

fullArgs := []string{"run", "--rm", "-i"}
fullArgs = append(fullArgs, args...)
fullArgs = append(fullArgs,
"--name",
containerName,
testutil.CommonImage,
)

defer base.Cmd("rm", "-f", containerName).AssertOK()
result := base.Cmd(fullArgs...).CmdOption(opts...).Run()

return result.Combined()
}

func runAttach(t *testing.T, testStr string, args []string) string {
if runtime.GOOS == "windows" {
t.Skip("run attach test is not yet implemented on Windows")
}

t.Parallel()
base := testutil.NewBase(t)
containerName := testutil.Identifier(t)

fullArgs := []string{"run"}
fullArgs = append(fullArgs, args...)
fullArgs = append(fullArgs,
"--name",
containerName,
testutil.CommonImage,
"sh",
"-euxc",
"echo "+testStr,
)

defer base.Cmd("rm", "-f", containerName).AssertOK()
result := base.Cmd(fullArgs...).Run()

return result.Combined()
}

func TestRunAttachFlag(t *testing.T) {

type testCase struct {
name string
args []string
testFunc func(t *testing.T, testStr string, args []string) string
testStr string
expectedOut string
dockerOut string
}
testCases := []testCase{
{
name: "AttachFlagStdin",
args: []string{"-a", "STDIN", "-a", "STDOUT"},
testFunc: runAttachStdin,
testStr: "test-run-stdio",
expectedOut: "test-run-stdio",
dockerOut: "test-run-stdio",
},
{
name: "AttachFlagStdOut",
args: []string{"-a", "STDOUT"},
testFunc: runAttach,
testStr: "foo",
expectedOut: "foo",
dockerOut: "foo",
},
{
name: "AttachFlagMixedValue",
args: []string{"-a", "STDIN", "-a", "invalid-value"},
testFunc: runAttach,
testStr: "foo",
expectedOut: "invalid stream specified with -a flag. Valid streams are STDIN, STDOUT, and STDERR",
dockerOut: "valid streams are STDIN, STDOUT and STDERR",
},
{
name: "AttachFlagInvalidValue",
args: []string{"-a", "invalid-stream"},
testFunc: runAttach,
testStr: "foo",
expectedOut: "invalid stream specified with -a flag. Valid streams are STDIN, STDOUT, and STDERR",
dockerOut: "valid streams are STDIN, STDOUT and STDERR",
},
{
name: "AttachFlagCaseInsensitive",
args: []string{"-a", "stdin", "-a", "stdout"},
testFunc: runAttachStdin,
testStr: "test-run-stdio",
expectedOut: "test-run-stdio",
dockerOut: "test-run-stdio",
},
}

for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
actualOut := tc.testFunc(t, tc.testStr, tc.args)
errorMsg := fmt.Sprintf("%s failed;\nExpected: '%s'\nActual: '%s'", tc.name, tc.expectedOut, actualOut)
if testutil.GetTarget() == testutil.Docker {
assert.Equal(t, true, strings.Contains(actualOut, tc.dockerOut), errorMsg)
} else {
assert.Equal(t, true, strings.Contains(actualOut, tc.expectedOut), errorMsg)
}
})
}
}
3 changes: 2 additions & 1 deletion docs/command-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ Usage: `nerdctl run [OPTIONS] IMAGE [COMMAND] [ARG...]`

Basic flags:

- :whale: `-a, --attach`: Attach STDIN, STDOUT, or STDERR
- :whale: :blue_square: `-i, --interactive`: Keep STDIN open even if not attached"
- :whale: :blue_square: `-t, --tty`: Allocate a pseudo-TTY
- :warning: WIP: currently `-t` conflicts with `-d`
Expand Down Expand Up @@ -387,7 +388,7 @@ IPFS flags:
- :nerd_face: `--ipfs-address`: Multiaddr of IPFS API (default uses `$IPFS_PATH` env variable if defined or local directory `~/.ipfs`)

Unimplemented `docker run` flags:
`--attach`, `--blkio-weight-device`, `--cpu-rt-*`, `--device-*`,
`--blkio-weight-device`, `--cpu-rt-*`, `--device-*`,
`--disable-content-trust`, `--domainname`, `--expose`, `--health-*`, `--isolation`, `--no-healthcheck`,
`--link*`, `--publish-all`, `--storage-opt`,
`--userns`, `--volume-driver`
Expand Down
2 changes: 2 additions & 0 deletions pkg/api/types/container_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ type ContainerCreateOptions struct {
Detach bool
// The key sequence for detaching a container.
DetachKeys string
// Attach STDIN, STDOUT, or STDERR
Attach []string
// Restart specifies the policy to apply when a container exits
Restart string
// Rm specifies whether to remove the container automatically when it exits
Expand Down
8 changes: 7 additions & 1 deletion pkg/containerutil/containerutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,13 @@ func Start(ctx context.Context, container containerd.Container, flagA bool, clie
}
}
detachC := make(chan struct{})
task, err := taskutil.NewTask(ctx, client, container, flagA, false, flagT, true, con, logURI, detachKeys, namespace, detachC)
attachStreamOpt := []string{}
if flagA {
// In start, flagA attaches only STDOUT/STDERR
// source: https://github.com/containerd/nerdctl/blob/main/docs/command-reference.md#whale-nerdctl-start
attachStreamOpt = []string{"STDOUT", "STDERR"}
}
task, err := taskutil.NewTask(ctx, client, container, attachStreamOpt, false, flagT, true, con, logURI, detachKeys, namespace, detachC)
if err != nil {
return err
}
Expand Down
55 changes: 52 additions & 3 deletions pkg/taskutil/taskutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import (
"net/url"
"os"
"runtime"
"slices"
"strings"
"sync"
"syscall"

Expand All @@ -39,7 +41,8 @@ import (

// NewTask is from https://github.com/containerd/containerd/blob/v1.4.3/cmd/ctr/commands/tasks/tasks_unix.go#L70-L108
func NewTask(ctx context.Context, client *containerd.Client, container containerd.Container,
flagA, flagI, flagT, flagD bool, con console.Console, logURI, detachKeys, namespace string, detachC chan<- struct{}) (containerd.Task, error) {
attachStreamOpt []string, flagI, flagT, flagD bool, con console.Console, logURI, detachKeys, namespace string, detachC chan<- struct{}) (containerd.Task, error) {

var t containerd.Task
closer := func() {
if detachC != nil {
Expand All @@ -59,7 +62,7 @@ func NewTask(ctx context.Context, client *containerd.Client, container container
io.Cancel()
}
var ioCreator cio.Creator
if flagA {
if len(attachStreamOpt) != 0 {
log.G(ctx).Debug("attaching output instead of using the log-uri")
if flagT {
in, err := consoleutil.NewDetachableStdin(con, detachKeys, closer)
Expand All @@ -68,7 +71,8 @@ func NewTask(ctx context.Context, client *containerd.Client, container container
}
ioCreator = cio.NewCreator(cio.WithStreams(in, con, nil), cio.WithTerminal)
} else {
ioCreator = cio.NewCreator(cio.WithStdio)
streams := processAttachStreamsOpt(attachStreamOpt)
ioCreator = cio.NewCreator(cio.WithStreams(streams.stdIn, streams.stdOut, streams.stdErr))
}

} else if flagT && flagD {
Expand Down Expand Up @@ -146,6 +150,51 @@ func NewTask(ctx context.Context, client *containerd.Client, container container
return t, nil
}

// struct used to store streams specified with attachStreamOpt (-a, --attach)
type streams struct {
stdIn *os.File
stdOut *os.File
stdErr *os.File
}

func nullStream() *os.File {
devNull, err := os.Open(os.DevNull)
if err != nil {
return nil
}
defer devNull.Close()

return devNull
}

func processAttachStreamsOpt(streamsArr []string) streams {
stdIn := os.Stdin
stdOut := os.Stdout
stdErr := os.Stderr

for i, str := range streamsArr {
streamsArr[i] = strings.ToUpper(str)
}

if !slices.Contains(streamsArr, "STDIN") {
stdIn = nullStream()
}

if !slices.Contains(streamsArr, "STDOUT") {
stdOut = nullStream()
}

if !slices.Contains(streamsArr, "STDERR") {
stdErr = nullStream()
}

return streams{
stdIn: stdIn,
stdOut: stdOut,
stdErr: stdErr,
}
}

// StdinCloser is from https://github.com/containerd/containerd/blob/v1.4.3/cmd/ctr/commands/tasks/exec.go#L181-L194
type StdinCloser struct {
mu sync.Mutex
Expand Down