mirror of
https://github.com/moby/moby.git
synced 2022-11-09 12:21:53 -05:00
Merge pull request #37172 from zq-david-wang/resizefix2
Fix race condition between exec start and resize.
This commit is contained in:
commit
5e11f66cb6
6 changed files with 192 additions and 9 deletions
|
@ -16,7 +16,7 @@ import (
|
||||||
"github.com/docker/docker/pkg/pools"
|
"github.com/docker/docker/pkg/pools"
|
||||||
"github.com/docker/docker/pkg/signal"
|
"github.com/docker/docker/pkg/signal"
|
||||||
"github.com/docker/docker/pkg/term"
|
"github.com/docker/docker/pkg/term"
|
||||||
"github.com/opencontainers/runtime-spec/specs-go"
|
specs "github.com/opencontainers/runtime-spec/specs-go"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
@ -249,6 +249,9 @@ func (d *Daemon) ContainerExecStart(ctx context.Context, name string, stdin io.R
|
||||||
ec.Lock()
|
ec.Lock()
|
||||||
c.ExecCommands.Lock()
|
c.ExecCommands.Lock()
|
||||||
systemPid, err := d.containerd.Exec(ctx, c.ID, ec.ID, p, cStdin != nil, ec.InitializeStdio)
|
systemPid, err := d.containerd.Exec(ctx, c.ID, ec.ID, p, cStdin != nil, ec.InitializeStdio)
|
||||||
|
// the exec context should be ready, or error happened.
|
||||||
|
// close the chan to notify readiness
|
||||||
|
close(ec.Started)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ExecCommands.Unlock()
|
c.ExecCommands.Unlock()
|
||||||
ec.Unlock()
|
ec.Unlock()
|
||||||
|
|
|
@ -15,6 +15,7 @@ import (
|
||||||
// examined both during and after completion.
|
// examined both during and after completion.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
sync.Mutex
|
sync.Mutex
|
||||||
|
Started chan struct{}
|
||||||
StreamConfig *stream.Config
|
StreamConfig *stream.Config
|
||||||
ID string
|
ID string
|
||||||
Running bool
|
Running bool
|
||||||
|
@ -40,6 +41,7 @@ func NewConfig() *Config {
|
||||||
return &Config{
|
return &Config{
|
||||||
ID: stringid.GenerateNonCryptoID(),
|
ID: stringid.GenerateNonCryptoID(),
|
||||||
StreamConfig: stream.NewConfig(),
|
StreamConfig: stream.NewConfig(),
|
||||||
|
Started: make(chan struct{}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ package daemon // import "github.com/docker/docker/daemon"
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/docker/docker/libcontainerd"
|
"github.com/docker/docker/libcontainerd"
|
||||||
)
|
)
|
||||||
|
@ -37,5 +38,13 @@ func (daemon *Daemon) ContainerExecResize(name string, height, width int) error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return daemon.containerd.ResizeTerminal(context.Background(), ec.ContainerID, ec.ID, width, height)
|
// TODO: the timeout is hardcoded here, it would be more flexible to make it
|
||||||
|
// a parameter in resize request context, which would need API changes.
|
||||||
|
timeout := 10 * time.Second
|
||||||
|
select {
|
||||||
|
case <-ec.Started:
|
||||||
|
return daemon.containerd.ResizeTerminal(context.Background(), ec.ContainerID, ec.ID, width, height)
|
||||||
|
case <-time.After(timeout):
|
||||||
|
return fmt.Errorf("timeout waiting for exec session ready")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
103
daemon/resize_test.go
Normal file
103
daemon/resize_test.go
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
// +build linux
|
||||||
|
|
||||||
|
package daemon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/docker/container"
|
||||||
|
"github.com/docker/docker/daemon/exec"
|
||||||
|
"github.com/gotestyourself/gotestyourself/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This test simply verify that when a wrong ID used, a specific error should be returned for exec resize.
|
||||||
|
func TestExecResizeNoSuchExec(t *testing.T) {
|
||||||
|
n := "TestExecResize"
|
||||||
|
d := &Daemon{
|
||||||
|
execCommands: exec.NewStore(),
|
||||||
|
}
|
||||||
|
c := &container.Container{
|
||||||
|
ExecCommands: exec.NewStore(),
|
||||||
|
}
|
||||||
|
ec := &exec.Config{
|
||||||
|
ID: n,
|
||||||
|
}
|
||||||
|
d.registerExecCommand(c, ec)
|
||||||
|
err := d.ContainerExecResize("nil", 24, 8)
|
||||||
|
assert.ErrorContains(t, err, "No such exec instance")
|
||||||
|
}
|
||||||
|
|
||||||
|
type execResizeMockContainerdClient struct {
|
||||||
|
MockContainerdClient
|
||||||
|
ProcessID string
|
||||||
|
ContainerID string
|
||||||
|
Width int
|
||||||
|
Height int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *execResizeMockContainerdClient) ResizeTerminal(ctx context.Context, containerID, processID string, width, height int) error {
|
||||||
|
c.ProcessID = processID
|
||||||
|
c.ContainerID = containerID
|
||||||
|
c.Width = width
|
||||||
|
c.Height = height
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// This test is to make sure that when exec context is ready, resize should call ResizeTerminal via containerd client.
|
||||||
|
func TestExecResize(t *testing.T) {
|
||||||
|
n := "TestExecResize"
|
||||||
|
width := 24
|
||||||
|
height := 8
|
||||||
|
ec := &exec.Config{
|
||||||
|
ID: n,
|
||||||
|
ContainerID: n,
|
||||||
|
Started: make(chan struct{}),
|
||||||
|
}
|
||||||
|
close(ec.Started)
|
||||||
|
mc := &execResizeMockContainerdClient{}
|
||||||
|
d := &Daemon{
|
||||||
|
execCommands: exec.NewStore(),
|
||||||
|
containerd: mc,
|
||||||
|
containers: container.NewMemoryStore(),
|
||||||
|
}
|
||||||
|
c := &container.Container{
|
||||||
|
ExecCommands: exec.NewStore(),
|
||||||
|
State: &container.State{Running: true},
|
||||||
|
}
|
||||||
|
d.containers.Add(n, c)
|
||||||
|
d.registerExecCommand(c, ec)
|
||||||
|
err := d.ContainerExecResize(n, height, width)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
assert.Equal(t, mc.Width, width)
|
||||||
|
assert.Equal(t, mc.Height, height)
|
||||||
|
assert.Equal(t, mc.ProcessID, n)
|
||||||
|
assert.Equal(t, mc.ContainerID, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This test is to make sure that when exec context is not ready, a timeout error should happen.
|
||||||
|
// TODO: the expect running time for this test is 10s, which would be too long for unit test.
|
||||||
|
func TestExecResizeTimeout(t *testing.T) {
|
||||||
|
n := "TestExecResize"
|
||||||
|
width := 24
|
||||||
|
height := 8
|
||||||
|
ec := &exec.Config{
|
||||||
|
ID: n,
|
||||||
|
ContainerID: n,
|
||||||
|
Started: make(chan struct{}),
|
||||||
|
}
|
||||||
|
mc := &execResizeMockContainerdClient{}
|
||||||
|
d := &Daemon{
|
||||||
|
execCommands: exec.NewStore(),
|
||||||
|
containerd: mc,
|
||||||
|
containers: container.NewMemoryStore(),
|
||||||
|
}
|
||||||
|
c := &container.Container{
|
||||||
|
ExecCommands: exec.NewStore(),
|
||||||
|
State: &container.State{Running: true},
|
||||||
|
}
|
||||||
|
d.containers.Add(n, c)
|
||||||
|
d.registerExecCommand(c, ec)
|
||||||
|
err := d.ContainerExecResize(n, height, width)
|
||||||
|
assert.ErrorContains(t, err, "timeout waiting for exec session ready")
|
||||||
|
}
|
65
daemon/util_test.go
Normal file
65
daemon/util_test.go
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
// +build linux
|
||||||
|
|
||||||
|
package daemon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/containerd/containerd"
|
||||||
|
"github.com/docker/docker/libcontainerd"
|
||||||
|
specs "github.com/opencontainers/runtime-spec/specs-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Mock containerd client implementation, for unit tests.
|
||||||
|
type MockContainerdClient struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MockContainerdClient) Version(ctx context.Context) (containerd.Version, error) {
|
||||||
|
return containerd.Version{}, nil
|
||||||
|
}
|
||||||
|
func (c *MockContainerdClient) Restore(ctx context.Context, containerID string, attachStdio libcontainerd.StdioCallback) (alive bool, pid int, err error) {
|
||||||
|
return false, 0, nil
|
||||||
|
}
|
||||||
|
func (c *MockContainerdClient) Create(ctx context.Context, containerID string, spec *specs.Spec, runtimeOptions interface{}) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (c *MockContainerdClient) Start(ctx context.Context, containerID, checkpointDir string, withStdin bool, attachStdio libcontainerd.StdioCallback) (pid int, err error) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
func (c *MockContainerdClient) SignalProcess(ctx context.Context, containerID, processID string, signal int) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (c *MockContainerdClient) Exec(ctx context.Context, containerID, processID string, spec *specs.Process, withStdin bool, attachStdio libcontainerd.StdioCallback) (int, error) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
func (c *MockContainerdClient) ResizeTerminal(ctx context.Context, containerID, processID string, width, height int) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (c *MockContainerdClient) CloseStdin(ctx context.Context, containerID, processID string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (c *MockContainerdClient) Pause(ctx context.Context, containerID string) error { return nil }
|
||||||
|
func (c *MockContainerdClient) Resume(ctx context.Context, containerID string) error { return nil }
|
||||||
|
func (c *MockContainerdClient) Stats(ctx context.Context, containerID string) (*libcontainerd.Stats, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (c *MockContainerdClient) ListPids(ctx context.Context, containerID string) ([]uint32, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (c *MockContainerdClient) Summary(ctx context.Context, containerID string) ([]libcontainerd.Summary, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (c *MockContainerdClient) DeleteTask(ctx context.Context, containerID string) (uint32, time.Time, error) {
|
||||||
|
return 0, time.Time{}, nil
|
||||||
|
}
|
||||||
|
func (c *MockContainerdClient) Delete(ctx context.Context, containerID string) error { return nil }
|
||||||
|
func (c *MockContainerdClient) Status(ctx context.Context, containerID string) (libcontainerd.Status, error) {
|
||||||
|
return "null", nil
|
||||||
|
}
|
||||||
|
func (c *MockContainerdClient) UpdateResources(ctx context.Context, containerID string, resources *libcontainerd.Resources) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (c *MockContainerdClient) CreateCheckpoint(ctx context.Context, containerID, checkpointDir string, exit bool) error {
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -71,16 +71,17 @@ func (s *DockerSuite) TestExecResizeImmediatelyAfterExecStart(c *check.C) {
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
_, rc, err := request.Post(fmt.Sprintf("/exec/%s/resize?h=24&w=80", execID), request.ContentType("text/plain"))
|
_, rc, err := request.Post(fmt.Sprintf("/exec/%s/resize?h=24&w=80", execID), request.ContentType("text/plain"))
|
||||||
// It's probably a panic of the daemon if io.ErrUnexpectedEOF is returned.
|
if err != nil {
|
||||||
if err == io.ErrUnexpectedEOF {
|
// It's probably a panic of the daemon if io.ErrUnexpectedEOF is returned.
|
||||||
return fmt.Errorf("The daemon might have crashed.")
|
if err == io.ErrUnexpectedEOF {
|
||||||
|
return fmt.Errorf("The daemon might have crashed.")
|
||||||
|
}
|
||||||
|
// Other error happened, should be reported.
|
||||||
|
return fmt.Errorf("Fail to exec resize immediately after start. Error: %q", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if err == nil {
|
rc.Close()
|
||||||
rc.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// We only interested in the io.ErrUnexpectedEOF error, so we return nil otherwise.
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue