diff --git a/cmd/nerdctl/container_run.go b/cmd/nerdctl/container_run.go index 9344cd2b2c1..f3bd5159d50 100644 --- a/cmd/nerdctl/container_run.go +++ b/cmd/nerdctl/container_run.go @@ -69,6 +69,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 } @@ -304,6 +305,10 @@ func processCreateCommandFlagsInRun(cmd *cobra.Command) (opt types.ContainerCrea if err != nil { return } + opt.Attach, err = cmd.Flags().GetStringSlice("attach") + if err != nil { + return + } return opt, nil } @@ -325,6 +330,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) @@ -381,7 +390,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 diff --git a/cmd/nerdctl/container_run_test.go b/cmd/nerdctl/container_run_test.go index fcf4305bcad..ef7f370681d 100644 --- a/cmd/nerdctl/container_run_test.go +++ b/cmd/nerdctl/container_run_test.go @@ -430,7 +430,6 @@ RUN echo '\ } \n\ }\n' >> main.go - RUN go mod init RUN go mod tidy RUN go build . @@ -533,3 +532,38 @@ func TestRunRmTime(t *testing.T) { t.Fatalf("expected to have completed in %v, took %v", deadline, took) } } + +func TestRunAttachStdin(t *testing.T) { + 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) + + const testStr = "test-run-stdio" + opts := []func(*testutil.Cmd){ + testutil.WithStdin(strings.NewReader("echo " + testStr + "\nexit\n")), + } + + defer base.Cmd("rm", "-f", containerName).AssertOK() + args := []string{"run", "--rm", "-a", "stdin", "-a", "stdout", "--name", containerName, testutil.CommonImage} + if testutil.GetTarget() == testutil.Docker { + args = append(args, "cat") + } + base.Cmd(args...).CmdOption(opts...).AssertOutContains(testStr) +} + +func TestRunAttachStdout(t *testing.T) { + 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) + + defer base.Cmd("rm", "-f", containerName).AssertOK() + base.Cmd("run", "-a", "stdout", "--name", containerName, testutil.CommonImage, "sh", "-euxc", "echo foo").AssertOutContains("foo") +} diff --git a/docs/command-reference.md b/docs/command-reference.md index 7c0c8c3ad76..29c17be1ed7 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -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` @@ -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` diff --git a/pkg/api/types/container_types.go b/pkg/api/types/container_types.go index 16fd41301d7..13e798cecad 100644 --- a/pkg/api/types/container_types.go +++ b/pkg/api/types/container_types.go @@ -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 diff --git a/pkg/containerutil/containerutil.go b/pkg/containerutil/containerutil.go index 85a4ddc53c9..36b09a13480 100644 --- a/pkg/containerutil/containerutil.go +++ b/pkg/containerutil/containerutil.go @@ -271,7 +271,11 @@ 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) + flagAStreams := []string{} + if flagA { + flagAStreams = []string{"STDOUT", "STDERR"} + } + task, err := taskutil.NewTask(ctx, client, container, flagAStreams, false, flagT, true, con, logURI, detachKeys, namespace, detachC) if err != nil { return err } diff --git a/pkg/taskutil/taskutil.go b/pkg/taskutil/taskutil.go index 101452f8b0e..6af686ab5b9 100644 --- a/pkg/taskutil/taskutil.go +++ b/pkg/taskutil/taskutil.go @@ -23,6 +23,8 @@ import ( "net/url" "os" "runtime" + "slices" + "strings" "sync" "syscall" @@ -37,9 +39,9 @@ import ( "golang.org/x/term" ) -// 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) { + flagA []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 { @@ -59,7 +61,7 @@ func NewTask(ctx context.Context, client *containerd.Client, container container io.Cancel() } var ioCreator cio.Creator - if flagA { + if len(flagA) != 0 { log.G(ctx).Debug("attaching output instead of using the log-uri") if flagT { in, err := consoleutil.NewDetachableStdin(con, detachKeys, closer) @@ -68,7 +70,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 := flagAStreams(flagA) + ioCreator = cio.NewCreator(cio.WithStreams(streams.stdIn, streams.stdOut, streams.stdErr)) } } else if flagT && flagD { @@ -146,6 +149,51 @@ func NewTask(ctx context.Context, client *containerd.Client, container container return t, nil } +// struct used to store streams specified with flagA (-a, --attach) +type streams struct { + stdIn *os.File + stdOut *os.File + stdErr *os.File +} + +func nullStream() *os.File { + devNull, err := os.Open("/dev/null") + if err != nil { + return nil + } + defer devNull.Close() + + return devNull +} + +func flagAStreams(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