Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: fix panic when getting container logs #1911

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 23 additions & 6 deletions docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,11 @@ func (c *DockerContainer) CopyToContainer(ctx context.Context, fileContent []byt
// StartLogProducer will start a concurrent process that will continuously read logs
// from the container and will send them to each added LogConsumer
func (c *DockerContainer) StartLogProducer(ctx context.Context) error {
// Short cirtuit.
if ctx.Err() != nil {
return ctx.Err()
}

if c.stopProducer != nil {
return errors.New("log producer already started")
}
Expand All @@ -636,14 +641,18 @@ func (c *DockerContainer) StartLogProducer(ctx context.Context) error {

r, err := c.provider.client.ContainerLogs(ctx, c.GetContainerID(), options)
if err != nil {
// if we can't get the logs, panic, we can't return an error to anything
// from within this goroutine
panic(err)
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return
}
// If the context is not canceled and deadline is not exceeded then retry.
goto BEGIN
}
defer c.provider.Close()

for {
select {
case <-ctx.Done():
return
case <-stop:
err := r.Close()
if err != nil {
Expand Down Expand Up @@ -714,9 +723,17 @@ func (c *DockerContainer) StartLogProducer(ctx context.Context) error {
// and sending them to each added LogConsumer
func (c *DockerContainer) StopLogProducer() error {
if c.stopProducer != nil {
c.stopProducer <- true
// block until the producer is actually done in order to avoid strange races
<-c.producerDone
// Producer might have been stopped already in case of an error.
// In that case, we don't want to block so we check if something is still
// listening on stopProducer or if we're able to read from the producerDone
// channel (this would indicate it's closed, hence producer goroutine has
// finished).
select {
case c.stopProducer <- true:
case <-c.producerDone:
default:
}

c.stopProducer = nil
c.producerDone = nil
}
Expand Down
90 changes: 90 additions & 0 deletions docker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const (
nginxDelayedImage = "docker.io/menedev/delayed-nginx:1.15.2"
nginxImage = "docker.io/nginx"
nginxAlpineImage = "docker.io/nginx:alpine"
pauseImage = "registry.k8s.io/pause:3.9"
nginxDefaultPort = "80/tcp"
nginxHighPort = "8080/tcp"
daemonMaxVersion = "1.41"
Expand Down Expand Up @@ -2159,3 +2160,92 @@ func TestImageBuiltFromDockerfile_KeepBuiltImage(t *testing.T) {
})
}
}

func TestLogs(t *testing.T) {
ctx := context.Background()

testcases := []struct {
name string
test func(ctx context.Context, t *testing.T, c Container)
}{
{
name: "StartLogProducer doesn't panic when provided context is cancelled",
test: func(ctx context.Context, t *testing.T, c Container) {
ctx, cancel := context.WithCancel(ctx)
cancel()

require.ErrorIs(t, c.StartLogProducer(ctx), context.Canceled)
t.Cleanup(func() { assert.NoError(t, c.StopLogProducer()) })
},
},
{
name: "StartLogProducer doesn't panic when provided context gets cancelled",
test: func(ctx context.Context, t *testing.T, c Container) {
ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
defer cancel()

require.NoError(t, c.StartLogProducer(ctx))
t.Cleanup(func() { assert.NoError(t, c.StopLogProducer()) })
<-ctx.Done()
},
},
{
name: "StartLogProducer and StopLogProducer work",
test: func(ctx context.Context, t *testing.T, c Container) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()

require.NoError(t, c.StartLogProducer(ctx))
_, err := c.Logs(ctx)
require.NoError(t, err)
require.NoError(t, c.StopLogProducer())
_, err = c.Logs(ctx)
require.NoError(t, err)
},
},
{
name: "StartLogProducer doesn't panic when provided context exceeds deadline",
test: func(ctx context.Context, t *testing.T, c Container) {
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(time.Nanosecond))
defer cancel()

require.ErrorIs(t, c.StartLogProducer(ctx), context.DeadlineExceeded)
t.Cleanup(func() { assert.NoError(t, c.StopLogProducer()) })
},
},
{
name: "Logs doesn't panic when context is cancelled",
test: func(ctx context.Context, t *testing.T, c Container) {
ctx, cancel := context.WithCancel(ctx)
cancel()

_, err := c.Logs(ctx)
require.ErrorIs(t, err, context.Canceled)
},
},
}

for _, tc := range testcases {
tc := tc

t.Run(tc.name, func(t *testing.T) {
t.Parallel()

container, err := GenericContainer(ctx,
GenericContainerRequest{
ContainerRequest: ContainerRequest{
Image: pauseImage,
},
Started: true,
Logger: TestLogger(t),
})
require.NoError(t, err, container)

t.Cleanup(func() {
assert.NoError(t, container.Terminate(context.Background()))
})

tc.test(ctx, t, container)
})
}
}
Loading