| 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), |
| } |
| } |