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:
@ -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
286
types/blob/blob_test.go
Normal 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
|
||||||
|
}
|
@ -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 {
|
||||||
|
Reference in New Issue
Block a user