Skip to content

Commit a45c25e

Browse files
committed
feat: support for -a and --attach in run
Signed-off-by: CodeChanning <[email protected]>
1 parent 77e6f18 commit a45c25e

File tree

6 files changed

+207
-7
lines changed

6 files changed

+207
-7
lines changed

cmd/nerdctl/container_run.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"errors"
2121
"fmt"
2222
"runtime"
23+
"strings"
2324

2425
"github.com/containerd/console"
2526
"github.com/containerd/log"
@@ -69,6 +70,7 @@ func newRunCommand() *cobra.Command {
6970
setCreateFlags(runCommand)
7071

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

7375
return runCommand
7476
}
@@ -304,6 +306,23 @@ func processCreateCommandFlagsInRun(cmd *cobra.Command) (opt types.ContainerCrea
304306
if err != nil {
305307
return
306308
}
309+
opt.Attach, err = cmd.Flags().GetStringSlice("attach")
310+
if err != nil {
311+
return
312+
}
313+
314+
validAttachFlag := true
315+
for i, str := range opt.Attach {
316+
opt.Attach[i] = strings.ToUpper(str)
317+
318+
if opt.Attach[i] != "STDIN" && opt.Attach[i] != "STDOUT" && opt.Attach[i] != "STDERR" {
319+
validAttachFlag = false
320+
}
321+
}
322+
if !validAttachFlag {
323+
return opt, fmt.Errorf("invalid stream specified with -a flag. Valid streams are STDIN, STDOUT, and STDERR.")
324+
}
325+
307326
return opt, nil
308327
}
309328

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

347+
if len(createOpt.Attach) > 0 && createOpt.Detach {
348+
return errors.New("flags -d and -a cannot be specified together")
349+
}
350+
328351
netFlags, err := loadNetworkFlags(cmd)
329352
if err != nil {
330353
return fmt.Errorf("failed to load networking flags: %s", err)
@@ -381,7 +404,7 @@ func runAction(cmd *cobra.Command, args []string) error {
381404
}
382405
logURI := lab[labels.LogURI]
383406
detachC := make(chan struct{})
384-
task, err := taskutil.NewTask(ctx, client, c, false, createOpt.Interactive, createOpt.TTY, createOpt.Detach,
407+
task, err := taskutil.NewTask(ctx, client, c, createOpt.Attach, createOpt.Interactive, createOpt.TTY, createOpt.Detach,
385408
con, logURI, createOpt.DetachKeys, createOpt.GOptions.Namespace, detachC)
386409
if err != nil {
387410
return err

cmd/nerdctl/container_run_test.go

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,3 +533,123 @@ func TestRunRmTime(t *testing.T) {
533533
t.Fatalf("expected to have completed in %v, took %v", deadline, took)
534534
}
535535
}
536+
537+
func runAttachStdin(t *testing.T, testStr string, args []string) string {
538+
if runtime.GOOS == "windows" {
539+
t.Skip("run attach test is not yet implemented on Windows")
540+
}
541+
542+
t.Parallel()
543+
base := testutil.NewBase(t)
544+
containerName := testutil.Identifier(t)
545+
546+
opts := []func(*testutil.Cmd){
547+
testutil.WithStdin(strings.NewReader("echo " + testStr + "\nexit\n")),
548+
}
549+
550+
fullArgs := []string{"run", "--rm", "-i"}
551+
fullArgs = append(fullArgs, args...)
552+
fullArgs = append(fullArgs,
553+
"--name",
554+
containerName,
555+
testutil.CommonImage,
556+
)
557+
558+
defer base.Cmd("rm", "-f", containerName).AssertOK()
559+
result := base.Cmd(fullArgs...).CmdOption(opts...).Run()
560+
561+
return result.Combined()
562+
}
563+
564+
func runAttach(t *testing.T, testStr string, args []string) string {
565+
if runtime.GOOS == "windows" {
566+
t.Skip("run attach test is not yet implemented on Windows")
567+
}
568+
569+
t.Parallel()
570+
base := testutil.NewBase(t)
571+
containerName := testutil.Identifier(t)
572+
573+
fullArgs := []string{"run"}
574+
fullArgs = append(fullArgs, args...)
575+
fullArgs = append(fullArgs,
576+
"--name",
577+
containerName,
578+
testutil.CommonImage,
579+
"sh",
580+
"-euxc",
581+
"echo "+testStr,
582+
)
583+
584+
defer base.Cmd("rm", "-f", containerName).AssertOK()
585+
result := base.Cmd(fullArgs...).Run()
586+
587+
return result.Combined()
588+
}
589+
590+
func TestRunAttachFlag(t *testing.T) {
591+
592+
type testCase struct {
593+
name string
594+
args []string
595+
testFunc func(t *testing.T, testStr string, args []string) string
596+
testStr string
597+
expectedOut string
598+
dockerOut string
599+
}
600+
testCases := []testCase{
601+
{
602+
name: "AttachFlagStdin",
603+
args: []string{"-a", "STDIN", "-a", "STDOUT"},
604+
testFunc: runAttachStdin,
605+
testStr: "test-run-stdio",
606+
expectedOut: "test-run-stdio",
607+
dockerOut: "test-run-stdio",
608+
},
609+
{
610+
name: "AttachFlagStdOut",
611+
args: []string{"-a", "STDOUT"},
612+
testFunc: runAttach,
613+
testStr: "foo",
614+
expectedOut: "foo",
615+
dockerOut: "foo",
616+
},
617+
{
618+
name: "AttachFlagMixedValue",
619+
args: []string{"-a", "STDIN", "-a", "invalid-value"},
620+
testFunc: runAttach,
621+
testStr: "foo",
622+
expectedOut: "invalid stream specified with -a flag. Valid streams are STDIN, STDOUT, and STDERR.",
623+
dockerOut: "valid streams are STDIN, STDOUT and STDERR",
624+
},
625+
{
626+
name: "AttachFlagInvalidValue",
627+
args: []string{"-a", "invalid-stream"},
628+
testFunc: runAttach,
629+
testStr: "foo",
630+
expectedOut: "invalid stream specified with -a flag. Valid streams are STDIN, STDOUT, and STDERR.",
631+
dockerOut: "valid streams are STDIN, STDOUT and STDERR",
632+
},
633+
{
634+
name: "AttachFlagCaseInsensitive",
635+
args: []string{"-a", "stdin", "-a", "stdout"},
636+
testFunc: runAttachStdin,
637+
testStr: "test-run-stdio",
638+
expectedOut: "test-run-stdio",
639+
dockerOut: "test-run-stdio",
640+
},
641+
}
642+
643+
for _, tc := range testCases {
644+
tc := tc
645+
t.Run(tc.name, func(t *testing.T) {
646+
actualOut := tc.testFunc(t, tc.testStr, tc.args)
647+
errorMsg := fmt.Sprintf("%s failed;\nExpected: '%s'\nActual: '%s'", tc.name, tc.expectedOut, actualOut)
648+
if testutil.GetTarget() == testutil.Docker {
649+
assert.Equal(t, true, strings.Contains(actualOut, tc.dockerOut), errorMsg)
650+
} else {
651+
assert.Equal(t, true, strings.Contains(actualOut, tc.expectedOut), errorMsg)
652+
}
653+
})
654+
}
655+
}

docs/command-reference.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ Usage: `nerdctl run [OPTIONS] IMAGE [COMMAND] [ARG...]`
137137

138138
Basic flags:
139139

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

389390
Unimplemented `docker run` flags:
390-
`--attach`, `--blkio-weight-device`, `--cpu-rt-*`, `--device-*`,
391+
`--blkio-weight-device`, `--cpu-rt-*`, `--device-*`,
391392
`--disable-content-trust`, `--domainname`, `--expose`, `--health-*`, `--isolation`, `--no-healthcheck`,
392393
`--link*`, `--publish-all`, `--storage-opt`,
393394
`--userns`, `--volume-driver`

pkg/api/types/container_types.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ type ContainerCreateOptions struct {
6868
Detach bool
6969
// The key sequence for detaching a container.
7070
DetachKeys string
71+
// Attach STDIN, STDOUT, or STDERR
72+
Attach []string
7173
// Restart specifies the policy to apply when a container exits
7274
Restart string
7375
// Rm specifies whether to remove the container automatically when it exits

pkg/containerutil/containerutil.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,13 @@ func Start(ctx context.Context, container containerd.Container, flagA bool, clie
272272
}
273273
}
274274
detachC := make(chan struct{})
275-
task, err := taskutil.NewTask(ctx, client, container, flagA, false, flagT, true, con, logURI, detachKeys, namespace, detachC)
275+
attachStreamOpt := []string{}
276+
if flagA {
277+
// In start, flagA attaches only STDOUT/STDERR
278+
// source: https://github.com/containerd/nerdctl/blob/main/docs/command-reference.md#whale-nerdctl-start
279+
attachStreamOpt = []string{"STDOUT", "STDERR"}
280+
}
281+
task, err := taskutil.NewTask(ctx, client, container, attachStreamOpt, false, flagT, true, con, logURI, detachKeys, namespace, detachC)
276282
if err != nil {
277283
return err
278284
}

pkg/taskutil/taskutil.go

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import (
2323
"net/url"
2424
"os"
2525
"runtime"
26+
"slices"
27+
"strings"
2628
"sync"
2729
"syscall"
2830

@@ -37,9 +39,9 @@ import (
3739
"golang.org/x/term"
3840
)
3941

40-
// NewTask is from https://github.com/containerd/containerd/blob/v1.4.3/cmd/ctr/commands/tasks/tasks_unix.go#L70-L108
4142
func NewTask(ctx context.Context, client *containerd.Client, container containerd.Container,
42-
flagA, flagI, flagT, flagD bool, con console.Console, logURI, detachKeys, namespace string, detachC chan<- struct{}) (containerd.Task, error) {
43+
attachStreamOpt []string, flagI, flagT, flagD bool, con console.Console, logURI, detachKeys, namespace string, detachC chan<- struct{}) (containerd.Task, error) {
44+
4345
var t containerd.Task
4446
closer := func() {
4547
if detachC != nil {
@@ -59,7 +61,7 @@ func NewTask(ctx context.Context, client *containerd.Client, container container
5961
io.Cancel()
6062
}
6163
var ioCreator cio.Creator
62-
if flagA {
64+
if len(attachStreamOpt) != 0 {
6365
log.G(ctx).Debug("attaching output instead of using the log-uri")
6466
if flagT {
6567
in, err := consoleutil.NewDetachableStdin(con, detachKeys, closer)
@@ -68,7 +70,8 @@ func NewTask(ctx context.Context, client *containerd.Client, container container
6870
}
6971
ioCreator = cio.NewCreator(cio.WithStreams(in, con, nil), cio.WithTerminal)
7072
} else {
71-
ioCreator = cio.NewCreator(cio.WithStdio)
73+
streams := processAttachStreamsOpt(attachStreamOpt)
74+
ioCreator = cio.NewCreator(cio.WithStreams(streams.stdIn, streams.stdOut, streams.stdErr))
7275
}
7376

7477
} else if flagT && flagD {
@@ -146,6 +149,51 @@ func NewTask(ctx context.Context, client *containerd.Client, container container
146149
return t, nil
147150
}
148151

152+
// struct used to store streams specified with attachStreamOpt (-a, --attach)
153+
type streams struct {
154+
stdIn *os.File
155+
stdOut *os.File
156+
stdErr *os.File
157+
}
158+
159+
func nullStream() *os.File {
160+
devNull, err := os.Open(os.DevNull)
161+
if err != nil {
162+
return nil
163+
}
164+
defer devNull.Close()
165+
166+
return devNull
167+
}
168+
169+
func processAttachStreamsOpt(streamsArr []string) streams {
170+
stdIn := os.Stdin
171+
stdOut := os.Stdout
172+
stdErr := os.Stderr
173+
174+
for i, str := range streamsArr {
175+
streamsArr[i] = strings.ToUpper(str)
176+
}
177+
178+
if !slices.Contains(streamsArr, "STDIN") {
179+
stdIn = nullStream()
180+
}
181+
182+
if !slices.Contains(streamsArr, "STDOUT") {
183+
stdOut = nullStream()
184+
}
185+
186+
if !slices.Contains(streamsArr, "STDERR") {
187+
stdErr = nullStream()
188+
}
189+
190+
return streams{
191+
stdIn: stdIn,
192+
stdOut: stdOut,
193+
stdErr: stdErr,
194+
}
195+
}
196+
149197
// StdinCloser is from https://github.com/containerd/containerd/blob/v1.4.3/cmd/ctr/commands/tasks/exec.go#L181-L194
150198
type StdinCloser struct {
151199
mu sync.Mutex

0 commit comments

Comments
 (0)