blob: 05210d0dc73c8c4a903f298a0fd249fc209bcf3c [file] [edit]
package internal
import (
"context"
"encoding/json"
"errors"
"io"
"iter"
"sync"
"github.com/containerd/errdefs/pkg/errhttp"
"github.com/moby/moby/api/types/jsonstream"
)
func NewJSONMessageStream(rc io.ReadCloser) Stream {
if rc == nil {
panic("nil io.ReadCloser")
}
return Stream{
rc: rc,
close: sync.OnceValue(rc.Close),
}
}
type Stream struct {
rc io.ReadCloser
close func() error
}
// Read implements io.ReadCloser
func (r Stream) Read(p []byte) (n int, err error) {
if r.rc == nil {
return 0, io.EOF
}
return r.rc.Read(p)
}
// Close implements io.ReadCloser
func (r Stream) Close() error {
if r.close == nil {
return nil
}
return r.close()
}
var _ io.ReadCloser = Stream{}
// JSONMessages decodes the response stream as a sequence of [jsonstream.Message].
// The underlying [io.Reader] is closed when the stream ends or if the context
// is cancelled.
func (r Stream) JSONMessages(ctx context.Context) iter.Seq2[jsonstream.Message, error] {
stop := context.AfterFunc(ctx, func() {
_ = r.Close()
})
return func(yield func(jsonstream.Message, error) bool) {
defer func() {
stop() // unregister AfterFunc
_ = r.Close()
}()
dec := json.NewDecoder(r)
for {
var jm jsonstream.Message
if err := dec.Decode(&jm); err != nil {
if errors.Is(err, io.EOF) {
return
}
if err := ctx.Err(); err != nil {
// Do not return decoding errors if the context was
// cancelled, because the decoding errors may be due
// to the context being cancelled.
yield(jsonstream.Message{}, err)
return
}
yield(jsonstream.Message{}, err)
return
}
if !yield(jm, nil) {
return
}
}
}
}
// Wait consumes the stream until completion.
//
// It returns nil if the operation completes successfully. Errors are
// returned if the context is canceled, a decoding/transport failure
// occurs, or a JSON message reports an error ([jsonstream.Message.Error]).
func (r Stream) Wait(ctx context.Context) error {
for jm, err := range r.JSONMessages(ctx) {
if err != nil {
// decode, transport and context cancellation errors.
return err
}
if jm.Error != nil {
// push/pull failures.
return httpErrorFromStatusCode(jm.Error, jm.Error.Code)
}
}
return nil
}
type httpError struct {
err error
errdef error
}
func (e *httpError) Error() string {
return e.err.Error()
}
func (e *httpError) Unwrap() error {
return e.err
}
func (e *httpError) Is(target error) bool {
return errors.Is(e.errdef, target)
}
// httpErrorFromStatusCode creates an errdef error, based on the provided HTTP status-code
//
// TODO(thaJeztah): unify with the implementation in client and move to an internal package
// see https://github.com/moby/moby/blob/client/v0.4.0/client/errors.go#L76-L114
func httpErrorFromStatusCode(err error, statusCode int) error {
if err == nil {
return nil
}
return &httpError{
err: err,
errdef: errhttp.ToNative(statusCode),
}
}