Skip to content

Commit

Permalink
fix: fix panic when getting container logs
Browse files Browse the repository at this point in the history
  • Loading branch information
pmalek committed Nov 11, 2023
1 parent 83bc893 commit 1f20eaf
Show file tree
Hide file tree
Showing 2 changed files with 113 additions and 6 deletions.
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)
})
}
}

0 comments on commit 1f20eaf

Please sign in to comment.