Skip to content

Commit b81614c

Browse files
authored
Merge pull request #1014 from AkihiroSuda/run-i-t-take2
Fix `flag -t needs -i to be specified together` restriction
2 parents a7addf0 + 18ea81f commit b81614c

File tree

10 files changed

+144
-17
lines changed

10 files changed

+144
-17
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,7 @@ Usage: `nerdctl run [OPTIONS] IMAGE [COMMAND] [ARG...]`
356356
Basic flags:
357357
- :whale: :blue_square: `-i, --interactive`: Keep STDIN open even if not attached"
358358
- :whale: :blue_square: `-t, --tty`: Allocate a pseudo-TTY
359-
- :warning: WIP: currently `-t` requires `-i`, and conflicts with `-d`
359+
- :warning: WIP: currently `-t` conflicts with `-d`
360360
- :whale: :blue_square: `-d, --detach`: Run container in background and print container ID
361361
- :whale: `--restart=(no|always)`: Restart policy to apply when a container exits
362362
- Default: "no"
@@ -611,7 +611,7 @@ Usage: `nerdctl exec [OPTIONS] CONTAINER COMMAND [ARG...]`
611611
Flags:
612612
- :whale: `-i, --interactive`: Keep STDIN open even if not attached
613613
- :whale: `-t, --tty`: Allocate a pseudo-TTY
614-
- :warning: WIP: currently `-t` requires `-i`, and conflicts with `-d`
614+
- :warning: WIP: currently `-t` conflicts with `-d`
615615
- :whale: `-d, --detach`: Detached mode: run command in the background
616616
- :whale: `-w, --workdir`: Working directory inside the container
617617
- :whale: `-e, --env`: Set environment variables

cmd/nerdctl/exec.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,6 @@ func execActionWithContainer(ctx context.Context, cmd *cobra.Command, args []str
121121
if flagD {
122122
return errors.New("currently flag -t and -d cannot be specified together (FIXME)")
123123
}
124-
if !flagI {
125-
return errors.New("currently flag -t needs -i to be specified together (FIXME)")
126-
}
127124
}
128125

129126
pspec, err := generateExecProcessSpec(ctx, cmd, args, container, client)

cmd/nerdctl/exec_linux_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,24 @@ func TestExecWithUser(t *testing.T) {
4848
base.Cmd(cmd...).AssertOutContains(expected)
4949
}
5050
}
51+
52+
func TestExecTTY(t *testing.T) {
53+
t.Parallel()
54+
base := testutil.NewBase(t)
55+
if testutil.GetTarget() == testutil.Nerdctl {
56+
testutil.RequireDaemonVersion(base, ">= 1.6.0-0")
57+
}
58+
59+
testContainer := testutil.Identifier(t)
60+
defer base.Cmd("rm", "-f", testContainer).Run()
61+
base.Cmd("run", "-d", "--name", testContainer, testutil.CommonImage, "sleep", "infinity").AssertOK()
62+
63+
const sttyPartialOutput = "speed 38400 baud"
64+
// unbuffer(1) emulates tty, which is required by `nerdctl run -t`.
65+
// unbuffer(1) can be installed with `apt-get install expect`.
66+
unbuffer := []string{"unbuffer"}
67+
base.CmdWithHelper(unbuffer, "exec", "-it", testContainer, "stty").AssertOutContains(sttyPartialOutput)
68+
base.CmdWithHelper(unbuffer, "exec", "-t", testContainer, "stty").AssertOutContains(sttyPartialOutput)
69+
base.Cmd("exec", "-i", testContainer, "stty").AssertFail()
70+
base.Cmd("exec", testContainer, "stty").AssertFail()
71+
}

cmd/nerdctl/exec_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package main
1818

1919
import (
20+
"strings"
2021
"testing"
2122

2223
"github.com/containerd/nerdctl/pkg/testutil"
@@ -44,3 +45,21 @@ func TestExecWithDoubleDash(t *testing.T) {
4445

4546
base.Cmd("exec", testContainer, "--", "echo", "success").AssertOutExactly("success\n")
4647
}
48+
49+
func TestExecStdin(t *testing.T) {
50+
t.Parallel()
51+
base := testutil.NewBase(t)
52+
if testutil.GetTarget() == testutil.Nerdctl {
53+
testutil.RequireDaemonVersion(base, ">= 1.6.0-0")
54+
}
55+
56+
testContainer := testutil.Identifier(t)
57+
defer base.Cmd("rm", "-f", testContainer).Run()
58+
base.Cmd("run", "-d", "--name", testContainer, testutil.CommonImage, "sleep", "1h").AssertOK()
59+
60+
const testStr = "test-exec-stdin"
61+
opts := []func(*testutil.Cmd){
62+
testutil.WithStdin(strings.NewReader(testStr)),
63+
}
64+
base.Cmd("exec", "-i", testContainer, "cat").CmdOption(opts...).AssertOutExactly(testStr)
65+
}

cmd/nerdctl/run.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -452,9 +452,6 @@ func createContainer(cmd *cobra.Command, ctx context.Context, client *containerd
452452
if flagD {
453453
return nil, "", nil, errors.New("currently flag -t and -d cannot be specified together (FIXME)")
454454
}
455-
if !flagI {
456-
return nil, "", nil, errors.New("currently flag -t needs -i to be specified together (FIXME)")
457-
}
458455
opts = append(opts, oci.WithTTY)
459456
}
460457

cmd/nerdctl/run_linux_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,3 +219,20 @@ ENTRYPOINT ["./main"]
219219
assert.Assert(logsCMD.Base.T, result.ExitCode == 0, stdoutContent)
220220
assert.Equal(logsCMD.Base.T, strings.Contains(stdoutContent, "signal: 15"), true)
221221
}
222+
223+
func TestRunTTY(t *testing.T) {
224+
t.Parallel()
225+
base := testutil.NewBase(t)
226+
if testutil.GetTarget() == testutil.Nerdctl {
227+
testutil.RequireDaemonVersion(base, ">= 1.6.0-0")
228+
}
229+
230+
const sttyPartialOutput = "speed 38400 baud"
231+
// unbuffer(1) emulates tty, which is required by `nerdctl run -t`.
232+
// unbuffer(1) can be installed with `apt-get install expect`.
233+
unbuffer := []string{"unbuffer"}
234+
base.CmdWithHelper(unbuffer, "run", "--rm", "-it", testutil.CommonImage, "stty").AssertOutContains(sttyPartialOutput)
235+
base.CmdWithHelper(unbuffer, "run", "--rm", "-t", testutil.CommonImage, "stty").AssertOutContains(sttyPartialOutput)
236+
base.Cmd("run", "--rm", "-i", testutil.CommonImage, "stty").AssertFail()
237+
base.Cmd("run", "--rm", testutil.CommonImage, "stty").AssertFail()
238+
}

cmd/nerdctl/run_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,3 +198,17 @@ func TestRunEnv(t *testing.T) {
198198
return nil
199199
})
200200
}
201+
202+
func TestRunStdin(t *testing.T) {
203+
t.Parallel()
204+
base := testutil.NewBase(t)
205+
if testutil.GetTarget() == testutil.Nerdctl {
206+
testutil.RequireDaemonVersion(base, ">= 1.6.0-0")
207+
}
208+
209+
const testStr = "test-run-stdin"
210+
opts := []func(*testutil.Cmd){
211+
testutil.WithStdin(strings.NewReader(testStr)),
212+
}
213+
base.Cmd("run", "--rm", "-i", testutil.CommonImage, "cat").CmdOption(opts...).AssertOutExactly(testStr)
214+
}

pkg/infoutil/infoutil.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@ package infoutil
1818

1919
import (
2020
"context"
21+
"fmt"
2122
"os"
2223
"runtime"
2324
"strings"
2425
"time"
2526

27+
"github.com/Masterminds/semver/v3"
2628
"github.com/containerd/containerd"
2729
"github.com/containerd/containerd/services/introspection"
2830
"github.com/containerd/nerdctl/pkg/inspecttypes/dockercompat"
@@ -135,3 +137,15 @@ func ServerVersion(ctx context.Context, client *containerd.Client) (*dockercompa
135137
}
136138
return v, nil
137139
}
140+
141+
func ServerSemVer(ctx context.Context, client *containerd.Client) (*semver.Version, error) {
142+
v, err := client.Version(ctx)
143+
if err != nil {
144+
return nil, err
145+
}
146+
sv, err := semver.NewVersion(v.Version)
147+
if err != nil {
148+
return nil, fmt.Errorf("failed to parse the containerd version %q: %w", v.Version, err)
149+
}
150+
return sv, nil
151+
}

pkg/taskutil/taskutil.go

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,23 +22,35 @@ import (
2222
"io"
2323
"net/url"
2424
"os"
25+
"runtime"
26+
"sync"
27+
"syscall"
2528

29+
"github.com/Masterminds/semver/v3"
2630
"github.com/containerd/console"
2731
"github.com/containerd/containerd"
2832
"github.com/containerd/containerd/cio"
33+
"github.com/containerd/nerdctl/pkg/infoutil"
34+
"github.com/sirupsen/logrus"
35+
"golang.org/x/term"
2936
)
3037

3138
// NewTask is from https://github.com/containerd/containerd/blob/v1.4.3/cmd/ctr/commands/tasks/tasks_unix.go#L70-L108
3239
func NewTask(ctx context.Context, client *containerd.Client, container containerd.Container, flagI, flagT, flagD bool, con console.Console, logURI string) (containerd.Task, error) {
33-
stdinC := &StdinCloser{
34-
Stdin: os.Stdin,
35-
}
3640
var ioCreator cio.Creator
3741
if flagT {
3842
if con == nil {
3943
return nil, errors.New("got nil con with flagT=true")
4044
}
41-
ioCreator = cio.NewCreator(cio.WithStreams(con, con, nil), cio.WithTerminal)
45+
var in io.Reader
46+
if flagI {
47+
// FIXME: check IsTerminal on Windows too
48+
if runtime.GOOS != "windows" && !term.IsTerminal(0) {
49+
return nil, errors.New("the input device is not a TTY")
50+
}
51+
in = con
52+
}
53+
ioCreator = cio.NewCreator(cio.WithStreams(in, con, nil), cio.WithTerminal)
4254
} else if flagD && logURI != "" {
4355
// TODO: support logURI for `nerdctl run -it`
4456
u, err := url.Parse(logURI)
@@ -49,6 +61,21 @@ func NewTask(ctx context.Context, client *containerd.Client, container container
4961
} else {
5062
var in io.Reader
5163
if flagI {
64+
if sv, err := infoutil.ServerSemVer(ctx, client); err != nil {
65+
logrus.Warn(err)
66+
} else if sv.LessThan(semver.MustParse("1.6.0-0")) {
67+
logrus.Warnf("`nerdctl (run|exec) -i` without `-t` expects containerd 1.6 or later, got containerd %v", sv)
68+
}
69+
stdinC := &StdinCloser{
70+
Stdin: os.Stdin,
71+
Closer: func() {
72+
if t, err := container.Task(ctx, nil); err != nil {
73+
logrus.WithError(err).Debugf("failed to get task for StdinCloser")
74+
} else {
75+
t.CloseIO(ctx, containerd.WithStdinCloser)
76+
}
77+
},
78+
}
5279
in = stdinC
5380
}
5481
ioCreator = cio.NewCreator(cio.WithStreams(in, os.Stdout, os.Stderr))
@@ -57,23 +84,28 @@ func NewTask(ctx context.Context, client *containerd.Client, container container
5784
if err != nil {
5885
return nil, err
5986
}
60-
stdinC.Closer = func() {
61-
t.CloseIO(ctx, containerd.WithStdinCloser)
62-
}
6387
return t, nil
6488
}
6589

6690
// StdinCloser is from https://github.com/containerd/containerd/blob/v1.4.3/cmd/ctr/commands/tasks/exec.go#L181-L194
6791
type StdinCloser struct {
92+
mu sync.Mutex
6893
Stdin *os.File
6994
Closer func()
95+
closed bool
7096
}
7197

7298
func (s *StdinCloser) Read(p []byte) (int, error) {
99+
s.mu.Lock()
100+
defer s.mu.Unlock()
101+
if s.closed {
102+
return 0, syscall.EBADF
103+
}
73104
n, err := s.Stdin.Read(p)
74-
if err == io.EOF {
105+
if err != nil {
75106
if s.Closer != nil {
76107
s.Closer()
108+
s.closed = true
77109
}
78110
}
79111
return n, err

pkg/testutil/testutil.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,12 @@ import (
2727
"testing"
2828
"time"
2929

30+
"github.com/Masterminds/semver/v3"
3031
"github.com/containerd/nerdctl/pkg/buildkitutil"
3132
"github.com/containerd/nerdctl/pkg/inspecttypes/dockercompat"
3233
"github.com/containerd/nerdctl/pkg/inspecttypes/native"
3334
"github.com/containerd/nerdctl/pkg/platformutil"
3435
"github.com/opencontainers/go-digest"
35-
3636
"gotest.tools/v3/assert"
3737
"gotest.tools/v3/icmd"
3838
)
@@ -406,6 +406,22 @@ func RequireExecPlatform(t testing.TB, ss ...string) {
406406
}
407407
}
408408

409+
func RequireDaemonVersion(b *Base, constraint string) {
410+
b.T.Helper()
411+
c, err := semver.NewConstraint(constraint)
412+
if err != nil {
413+
b.T.Fatal(err)
414+
}
415+
info := b.Info()
416+
sv, err := semver.NewVersion(info.ServerVersion)
417+
if err != nil {
418+
b.T.Skip(err)
419+
}
420+
if !c.Check(sv) {
421+
b.T.Skipf("version %v does not satisfy constraints %v", sv, c)
422+
}
423+
}
424+
409425
const Namespace = "nerdctl-test"
410426

411427
func NewBase(t *testing.T) *Base {

0 commit comments

Comments
 (0)