client: Client.ContainerLogs: close reader on context cancellation

Use a cancelReadCloser to automatically close the reader when the context
is cancelled. Consumers are still recommended to manually close the reader,
but the cancelReadCloser makes the Close idempotent.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Sebastiaan van Stijn
2025-10-30 10:38:42 +01:00
parent cc9969bfed
commit d35d8ec81b
3 changed files with 11 additions and 37 deletions

View File

@@ -30,6 +30,8 @@ type ContainerLogsResult interface {
// ContainerLogs returns the logs generated by a container in an [io.ReadCloser]. // ContainerLogs returns the logs generated by a container in an [io.ReadCloser].
// It's up to the caller to close the stream. // It's up to the caller to close the stream.
// //
// The underlying [io.ReadCloser] is automatically closed if the context is canceled,
//
// The stream format on the response uses one of two formats: // The stream format on the response uses one of two formats:
// //
// - If the container is using a TTY, there is only a single stream (stdout) // - If the container is using a TTY, there is only a single stream (stdout)
@@ -102,30 +104,15 @@ func (cli *Client) ContainerLogs(ctx context.Context, containerID string, option
return nil, err return nil, err
} }
return &containerLogsResult{ return &containerLogsResult{
body: resp.Body, ReadCloser: newCancelReadCloser(ctx, resp.Body),
}, nil }, nil
} }
type containerLogsResult struct { type containerLogsResult struct {
// body must be closed to avoid a resource leak io.ReadCloser
body io.ReadCloser
} }
var ( var (
_ io.ReadCloser = (*containerLogsResult)(nil) _ io.ReadCloser = (*containerLogsResult)(nil)
_ ContainerLogsResult = (*containerLogsResult)(nil) _ ContainerLogsResult = (*containerLogsResult)(nil)
) )
func (r *containerLogsResult) Read(p []byte) (int, error) {
if r == nil || r.body == nil {
return 0, io.EOF
}
return r.body.Read(p)
}
func (r *containerLogsResult) Close() error {
if r == nil || r.body == nil {
return nil
}
return r.body.Close()
}

View File

@@ -100,14 +100,14 @@ func TestNetworkLoopbackNat(t *testing.T) {
poll.WaitOn(t, container.IsStopped(ctx, apiClient, cID)) poll.WaitOn(t, container.IsStopped(ctx, apiClient, cID))
body, err := apiClient.ContainerLogs(ctx, cID, client.ContainerLogsOptions{ logs, err := apiClient.ContainerLogs(ctx, cID, client.ContainerLogsOptions{
ShowStdout: true, ShowStdout: true,
}) })
assert.NilError(t, err) assert.NilError(t, err)
defer body.Close() defer logs.Close()
var b bytes.Buffer var b bytes.Buffer
_, err = io.Copy(&b, body) _, err = io.Copy(&b, logs)
assert.NilError(t, err) assert.NilError(t, err)
assert.Check(t, is.Equal(msg, strings.TrimSpace(b.String()))) assert.Check(t, is.Equal(msg, strings.TrimSpace(b.String())))

View File

@@ -30,6 +30,8 @@ type ContainerLogsResult interface {
// ContainerLogs returns the logs generated by a container in an [io.ReadCloser]. // ContainerLogs returns the logs generated by a container in an [io.ReadCloser].
// It's up to the caller to close the stream. // It's up to the caller to close the stream.
// //
// The underlying [io.ReadCloser] is automatically closed if the context is canceled,
//
// The stream format on the response uses one of two formats: // The stream format on the response uses one of two formats:
// //
// - If the container is using a TTY, there is only a single stream (stdout) // - If the container is using a TTY, there is only a single stream (stdout)
@@ -102,30 +104,15 @@ func (cli *Client) ContainerLogs(ctx context.Context, containerID string, option
return nil, err return nil, err
} }
return &containerLogsResult{ return &containerLogsResult{
body: resp.Body, ReadCloser: newCancelReadCloser(ctx, resp.Body),
}, nil }, nil
} }
type containerLogsResult struct { type containerLogsResult struct {
// body must be closed to avoid a resource leak io.ReadCloser
body io.ReadCloser
} }
var ( var (
_ io.ReadCloser = (*containerLogsResult)(nil) _ io.ReadCloser = (*containerLogsResult)(nil)
_ ContainerLogsResult = (*containerLogsResult)(nil) _ ContainerLogsResult = (*containerLogsResult)(nil)
) )
func (r *containerLogsResult) Read(p []byte) (int, error) {
if r == nil || r.body == nil {
return 0, io.EOF
}
return r.body.Read(p)
}
func (r *containerLogsResult) Close() error {
if r == nil || r.body == nil {
return nil
}
return r.body.Close()
}