Skip to content

Commit

Permalink
feat: support for -a and --attach in run
Browse files Browse the repository at this point in the history
Signed-off-by: CodeChanning <chxgaddy@amazon.com>
  • Loading branch information
CodeChanning committed Jul 16, 2024
1 parent 17558b3 commit cf0e6e6
Show file tree
Hide file tree
Showing 6 changed files with 207 additions and 6 deletions.
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

0 comments on commit cf0e6e6

Please sign in to comment.