Skip to content

Commit 370cab6

Browse files
authored
Merge pull request #3157 from CodeChanning/attach_run
feat: support for -a and --attach in run
2 parents 6c53c8a + cf0e6e6 commit 370cab6

File tree

6 files changed

+207
-6
lines changed

6 files changed

+207
-6
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 & 3 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

@@ -39,7 +41,8 @@ import (
3941

4042
// NewTask is from https://github.com/containerd/containerd/blob/v1.4.3/cmd/ctr/commands/tasks/tasks_unix.go#L70-L108
4143
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) {
44+
attachStreamOpt []string, flagI, flagT, flagD bool, con console.Console, logURI, detachKeys, namespace string, detachC chan<- struct{}) (containerd.Task, error) {
45+
4346
var t containerd.Task
4447
closer := func() {
4548
if detachC != nil {
@@ -59,7 +62,7 @@ func NewTask(ctx context.Context, client *containerd.Client, container container
5962
io.Cancel()
6063
}
6164
var ioCreator cio.Creator
62-
if flagA {
65+
if len(attachStreamOpt) != 0 {
6366
log.G(ctx).Debug("attaching output instead of using the log-uri")
6467
if flagT {
6568
in, err := consoleutil.NewDetachableStdin(con, detachKeys, closer)
@@ -68,7 +71,8 @@ func NewTask(ctx context.Context, client *containerd.Client, container container
6871
}
6972
ioCreator = cio.NewCreator(cio.WithStreams(in, con, nil), cio.WithTerminal)
7073
} else {
71-
ioCreator = cio.NewCreator(cio.WithStdio)
74+
streams := processAttachStreamsOpt(attachStreamOpt)
75+
ioCreator = cio.NewCreator(cio.WithStreams(streams.stdIn, streams.stdOut, streams.stdErr))
7276
}
7377

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

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

0 commit comments

Comments
 (0)