1
0
mirror of https://github.com/regclient/regclient.git synced 2025-07-29 09:01:11 +03:00

Blob changing from ReadCloser to Reader

Close and Seek methods are used when available.
Forcing a Closer resulted in a NoopCloser that blocked Seek.

Signed-off-by: Brandon Mitchell <git@bmitch.net>
This commit is contained in:
Brandon Mitchell
2022-01-16 16:14:21 -05:00
parent 6724eef624
commit 1e588c18ef
3 changed files with 314 additions and 11 deletions

View File

@ -19,7 +19,7 @@ type BlobConfig struct {
header http.Header header http.Header
image ociv1.Image image ociv1.Image
r ref.Ref r ref.Ref
rc io.ReadCloser rdr io.Reader
resp *http.Response resp *http.Response
} }
@ -41,9 +41,9 @@ func WithImage(image ociv1.Image) Opts {
bc.image = image bc.image = image
} }
} }
func WithReadCloser(rc io.ReadCloser) Opts { func WithReader(rc io.Reader) Opts {
return func(bc *BlobConfig) { return func(bc *BlobConfig) {
bc.rc = rc bc.rdr = rc
} }
} }
func WithRef(r ref.Ref) Opts { func WithRef(r ref.Ref) Opts {

286
types/blob/blob_test.go Normal file
View File

@ -0,0 +1,286 @@
package blob
import (
"bytes"
"errors"
"fmt"
"io"
"net/http"
"testing"
"github.com/opencontainers/go-digest"
ociv1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/regclient/regclient/types"
"github.com/regclient/regclient/types/ref"
)
var (
exRef, _ = ref.New("localhost:5000/library/alpine:latest")
exBlob = []byte(`
{
"created": "2021-11-24T20:19:40.483367546Z",
"architecture": "amd64",
"os": "linux",
"config": {
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Cmd": [
"/bin/sh"
]
},
"rootfs": {
"type": "layers",
"diff_ids": [
"sha256:8d3ac3489996423f53d6087c81180006263b79f206d3fdec9e66f0e27ceb8759"
]
},
"history": [
{
"created": "2021-11-24T20:19:40.199700946Z",
"created_by": "/bin/sh -c #(nop) ADD file:9233f6f2237d79659a9521f7e390df217cec49f1a8aa3a12147bbca1956acdb9 in / "
},
{
"created": "2021-11-24T20:19:40.483367546Z",
"created_by": "/bin/sh -c #(nop) CMD [\"/bin/sh\"]",
"empty_layer": true
}
]
}
`)
exLen = int64(len(exBlob))
exDigest = digest.FromBytes(exBlob)
exMT = types.MediaTypeDocker2ImageConfig
exHeaders = http.Header{
"Content-Type": {types.MediaTypeDocker2ImageConfig},
"Content-Length": {fmt.Sprintf("%d", exLen)},
"Docker-Content-Digest": {exDigest.String()},
}
exResp = http.Response{
Status: http.StatusText(http.StatusOK),
StatusCode: http.StatusOK,
Header: exHeaders,
ContentLength: exLen,
Body: io.NopCloser(bytes.NewReader(exBlob)),
}
)
func TestCommon(t *testing.T) {
// create test list
tests := []struct {
name string
opts []Opts
eBytes []byte
eDigest digest.Digest
eHeaders http.Header
eLen int64
eMT string
}{
{
name: "empty",
},
{
name: "reader",
opts: []Opts{WithReader(io.NopCloser(bytes.NewReader(exBlob)))},
eBytes: exBlob,
eDigest: exDigest,
eLen: exLen,
},
{
name: "descriptor",
opts: []Opts{
WithReader(io.NopCloser(bytes.NewReader(exBlob))),
WithDesc(ociv1.Descriptor{
MediaType: exMT,
Digest: exDigest,
Size: exLen,
}),
WithRef(exRef),
},
eBytes: exBlob,
eDigest: exDigest,
eLen: exLen,
eMT: exMT,
},
{
name: "headers",
opts: []Opts{
WithReader(io.NopCloser(bytes.NewReader(exBlob))),
WithHeader(exHeaders),
WithRef(exRef),
},
eBytes: exBlob,
eDigest: exDigest,
eHeaders: exHeaders,
eLen: exLen,
eMT: exMT,
},
{
name: "response",
opts: []Opts{
WithResp(&exResp),
WithRef(exRef),
},
eBytes: exBlob,
eDigest: exDigest,
eHeaders: exHeaders,
eLen: exLen,
eMT: exMT,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
b := NewReader(tt.opts...)
if len(tt.eBytes) > 0 {
bb, err := b.RawBody()
if err != nil {
t.Errorf("rawbody: %v", err)
return
}
if bytes.Compare(bb, tt.eBytes) != 0 {
t.Errorf("rawbody, expected %s, received %s", string(tt.eBytes), string(bb))
}
}
if tt.eDigest != "" && b.Digest() != tt.eDigest {
t.Errorf("digest, expected %s, received %s", tt.eDigest, b.Digest())
}
if tt.eLen > 0 && b.Length() != tt.eLen {
t.Errorf("length, expected %d, received %d", tt.eLen, b.Length())
}
if tt.eMT != "" && b.MediaType() != tt.eMT {
t.Errorf("media type, expected %s, received %s", tt.eMT, b.MediaType())
}
if tt.eHeaders != nil {
bHeader := b.RawHeaders()
for k, v := range tt.eHeaders {
if _, ok := bHeader[k]; !ok {
t.Errorf("missing header: %s", k)
} else if !cmpSliceString(v, bHeader[k]) {
t.Errorf("header mismatch for key %s, expected %v, received %v", k, v, bHeader[k])
}
}
}
})
}
}
func TestReader(t *testing.T) {
t.Run("empty", func(t *testing.T) {
// create empty blob
b := NewReader()
// test read, expect error
_, err := b.RawBody()
if err == nil {
t.Errorf("unexpected success")
return
}
if !errors.Is(err, io.ErrUnexpectedEOF) {
t.Errorf("unexpected err from rawbody: %v", err)
}
})
t.Run("readseek", func(t *testing.T) {
// create blob
b := NewReader(
WithReader(bytes.NewReader(exBlob)),
)
// test read and seek on blob1
bl := 500
bb := make([]byte, bl)
i, err := b.Read(bb)
if err != nil {
t.Errorf("read err: %v", err)
return
}
if i != bl {
t.Errorf("read length, expected %d, received %d", bl, i)
}
bSeek, ok := b.(io.Seeker)
if !ok {
t.Errorf("seek interface missing")
return
}
_, err = bSeek.Seek(5, io.SeekStart)
if err == nil {
t.Errorf("seek to non-zero position did not fail")
}
pos, err := bSeek.Seek(0, io.SeekStart)
if err != nil {
t.Errorf("seek err: %v", err)
return
}
if pos != 0 {
t.Errorf("seek pos, expected 0, received %d", pos)
}
bb, err = io.ReadAll(b)
if err != nil {
t.Errorf("readall: %v", err)
return
}
if b.Digest() != exDigest {
t.Errorf("digest mismatch, expected %s, received %s", exDigest, b.Digest())
}
if b.Length() != exLen {
t.Errorf("length mismatch, expected %d, received %d", exLen, b.Length())
}
})
t.Run("ociconfig", func(t *testing.T) {
// create blob
b := NewReader(
WithReader(io.NopCloser(bytes.NewReader(exBlob))),
WithDesc(ociv1.Descriptor{
MediaType: exMT,
Digest: exDigest,
Size: exLen,
}),
WithRef(exRef),
)
// test ToOCIConfig on blob 2
oc, err := b.ToOCIConfig()
if err != nil {
t.Errorf("ToOCIConfig: %v", err)
return
}
if exDigest != oc.Digest() {
t.Errorf("digest, expected %s, received %s", exDigest, oc.Digest())
}
ocb, err := oc.RawBody()
if err != nil {
t.Errorf("config rawbody: %v", err)
return
}
if bytes.Compare(exBlob, ocb) != 0 {
t.Errorf("config bytes, expected %s, received %s", string(exBlob), string(ocb))
}
})
t.Run("rawbytes", func(t *testing.T) {
// create blob
b := NewReader(
WithReader(io.NopCloser(bytes.NewReader(exBlob))),
)
// test RawBytes on blob 3
bb, err := b.RawBody()
if err != nil {
t.Errorf("rawbody: %v", err)
return
}
if bytes.Compare(exBlob, bb) != 0 {
t.Errorf("config bytes, expected %s, received %s", string(exBlob), string(bb))
}
})
}
func cmpSliceString(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}

View File

@ -23,7 +23,7 @@ type reader struct {
common common
readBytes int64 readBytes int64
reader io.Reader reader io.Reader
origRdr io.ReadCloser origRdr io.Reader
digester digest.Digester digester digest.Digester
} }
@ -35,15 +35,24 @@ func NewReader(opts ...Opts) Reader {
} }
if bc.resp != nil { if bc.resp != nil {
// extract headers and reader if other fields not passed // extract headers and reader if other fields not passed
if bc.header == nil {
bc.header = bc.resp.Header
}
if bc.rdr == nil {
bc.rdr = bc.resp.Body
}
}
if bc.header != nil {
// extract fields from header if descriptor not passed
if bc.desc.MediaType == "" { if bc.desc.MediaType == "" {
bc.desc.MediaType = bc.resp.Header.Get("Content-Type") bc.desc.MediaType = bc.header.Get("Content-Type")
} }
if bc.desc.Size == 0 { if bc.desc.Size == 0 {
cl, _ := strconv.Atoi(bc.resp.Header.Get("Content-Length")) cl, _ := strconv.Atoi(bc.header.Get("Content-Length"))
bc.desc.Size = int64(cl) bc.desc.Size = int64(cl)
} }
if bc.desc.Digest == "" { if bc.desc.Digest == "" {
bc.desc.Digest = digest.FromString(bc.resp.Header.Get("Docker-Content-Digest")) bc.desc.Digest, _ = digest.Parse(bc.header.Get("Docker-Content-Digest"))
} }
} }
c := common{ c := common{
@ -54,12 +63,12 @@ func NewReader(opts ...Opts) Reader {
} }
br := reader{ br := reader{
common: c, common: c,
origRdr: bc.rc, origRdr: bc.rdr,
} }
if bc.rc != nil { if bc.rdr != nil {
br.blobSet = true br.blobSet = true
br.digester = digest.Canonical.Digester() br.digester = digest.Canonical.Digester()
br.reader = io.TeeReader(bc.rc, br.digester.Hash()) br.reader = io.TeeReader(bc.rdr, br.digester.Hash())
} }
return &br return &br
} }
@ -68,7 +77,12 @@ func (b *reader) Close() error {
if b.origRdr == nil { if b.origRdr == nil {
return nil return nil
} }
return b.origRdr.Close() // attempt to close if available in original reader
bc, ok := b.origRdr.(io.Closer)
if !ok {
return nil
}
return bc.Close()
} }
// RawBody returns the original body from the request // RawBody returns the original body from the request
@ -78,6 +92,9 @@ func (b *reader) RawBody() ([]byte, error) {
// Read passes through the read operation while computing the digest and tracking the size // Read passes through the read operation while computing the digest and tracking the size
func (b *reader) Read(p []byte) (int, error) { func (b *reader) Read(p []byte) (int, error) {
if b.reader == nil {
return 0, fmt.Errorf("blob has no reader: %w", io.ErrUnexpectedEOF)
}
size, err := b.reader.Read(p) size, err := b.reader.Read(p)
b.readBytes = b.readBytes + int64(size) b.readBytes = b.readBytes + int64(size)
if err == io.EOF { if err == io.EOF {