1
0
mirror of https://github.com/regclient/regclient.git synced 2025-07-30 20:03:04 +03:00

Refactor blob like manifests, add SetConfig

Signed-off-by: Brandon Mitchell <git@bmitch.net>
This commit is contained in:
Brandon Mitchell
2022-02-22 20:42:37 -05:00
parent 64717baf53
commit 5c3f85f253
9 changed files with 212 additions and 52 deletions

View File

@ -59,7 +59,7 @@ func (rc *RegClient) BlobCopy(ctx context.Context, refSrc ref.Ref, refTgt ref.Re
return err return err
} }
defer blobIO.Close() defer blobIO.Close()
if _, _, err := rc.BlobPut(ctx, refTgt, d, blobIO, blobIO.Length()); err != nil { if _, _, err := rc.BlobPut(ctx, refTgt, d, blobIO, blobIO.GetDescriptor().Size); err != nil {
rc.log.WithFields(logrus.Fields{ rc.log.WithFields(logrus.Fields{
"err": err, "err": err,
"src": refSrc.Reference, "src": refSrc.Reference,

View File

@ -208,7 +208,7 @@ func TestBlobGet(t *testing.T) {
return return
} }
defer br.Close() defer br.Close()
if br.Length() != int64(blobLen) { if br.GetDescriptor().Size != int64(blobLen) {
t.Errorf("Failed comparing blob length") t.Errorf("Failed comparing blob length")
} }
}) })

View File

@ -48,13 +48,13 @@ func TestBlob(t *testing.T) {
t.Errorf("manifest get: %v", err) t.Errorf("manifest get: %v", err)
return return
} }
cd, err := m.GetConfigDigest() cd, err := m.GetConfig()
if err != nil { if err != nil {
t.Errorf("config digest: %v", err) t.Errorf("config digest: %v", err)
return return
} }
// blob head // blob head
bh, err := o.BlobHead(ctx, r, cd) bh, err := o.BlobHead(ctx, r, cd.Digest)
if err != nil { if err != nil {
t.Errorf("blob head: %v", err) t.Errorf("blob head: %v", err)
return return
@ -64,7 +64,7 @@ func TestBlob(t *testing.T) {
t.Errorf("blob head close: %v", err) t.Errorf("blob head close: %v", err)
} }
// blob get // blob get
bg, err := o.BlobGet(ctx, r, cd) bg, err := o.BlobGet(ctx, r, cd.Digest)
if err != nil { if err != nil {
t.Errorf("blob get: %v", err) t.Errorf("blob get: %v", err)
return return
@ -74,14 +74,14 @@ func TestBlob(t *testing.T) {
t.Errorf("blob readall: %v", err) t.Errorf("blob readall: %v", err)
return return
} }
if bg.Digest() != cd { if bg.GetDescriptor().Digest != cd.Digest {
t.Errorf("blob digest mismatch, expected %s, received %s", cd.String(), bg.Digest().String()) t.Errorf("blob digest mismatch, expected %s, received %s", cd.Digest.String(), bg.GetDescriptor().Digest.String())
} }
err = bg.Close() err = bg.Close()
if err != nil { if err != nil {
t.Errorf("blob get close: %v", err) t.Errorf("blob get close: %v", err)
} }
bFS, err := os.ReadFile(fmt.Sprintf("testdata/regctl/blobs/%s/%s", cd.Algorithm().String(), cd.Encoded())) bFS, err := os.ReadFile(fmt.Sprintf("testdata/regctl/blobs/%s/%s", cd.Digest.Algorithm().String(), cd.Digest.Encoded()))
if err != nil { if err != nil {
t.Errorf("blob read file: %v", err) t.Errorf("blob read file: %v", err)
} }
@ -90,7 +90,7 @@ func TestBlob(t *testing.T) {
} }
// toOCIConfig // toOCIConfig
bg, err = o.BlobGet(ctx, r, cd) bg, err = o.BlobGet(ctx, r, cd.Digest)
if err != nil { if err != nil {
t.Errorf("blob get 2: %v", err) t.Errorf("blob get 2: %v", err)
return return
@ -99,15 +99,15 @@ func TestBlob(t *testing.T) {
if err != nil { if err != nil {
t.Errorf("to oci config: %v", err) t.Errorf("to oci config: %v", err)
} }
if ociConf.Digest() != cd { if ociConf.GetDescriptor().Digest != cd.Digest {
t.Errorf("config digest mismatch, expected %s, received %s", cd.String(), ociConf.Digest().String()) t.Errorf("config digest mismatch, expected %s, received %s", cd.Digest.String(), ociConf.GetDescriptor().Digest.String())
} }
// blob put (to memfs) // blob put (to memfs)
fm := rwfs.MemNew() fm := rwfs.MemNew()
om := New(WithFS(fm)) om := New(WithFS(fm))
bRdr := bytes.NewReader(bBytes) bRdr := bytes.NewReader(bBytes)
bpd, bpl, err := om.BlobPut(ctx, r, cd, bRdr, int64(len(bBytes))) bpd, bpl, err := om.BlobPut(ctx, r, cd.Digest, bRdr, int64(len(bBytes)))
if err != nil { if err != nil {
t.Errorf("blob put: %v", err) t.Errorf("blob put: %v", err)
return return
@ -115,10 +115,10 @@ func TestBlob(t *testing.T) {
if bpl != int64(len(bBytes)) { if bpl != int64(len(bBytes)) {
t.Errorf("blob put length, expected %d, received %d", len(bBytes), bpl) t.Errorf("blob put length, expected %d, received %d", len(bBytes), bpl)
} }
if bpd != cd { if bpd != cd.Digest {
t.Errorf("blob put digest, expected %s, received %s", cd, bpd) t.Errorf("blob put digest, expected %s, received %s", cd.Digest, bpd)
} }
fd, err := fm.Open(fmt.Sprintf("testdata/regctl/blobs/%s/%s", cd.Algorithm().String(), cd.Encoded())) fd, err := fm.Open(fmt.Sprintf("testdata/regctl/blobs/%s/%s", cd.Digest.Algorithm().String(), cd.Digest.Encoded()))
if err != nil { if err != nil {
t.Errorf("blob put open file: %v", err) t.Errorf("blob put open file: %v", err)
} }

View File

@ -210,7 +210,7 @@ func TestBlobGet(t *testing.T) {
return return
} }
defer br.Close() defer br.Close()
if br.Length() != int64(blobLen) { if br.GetDescriptor().Size != int64(blobLen) {
t.Errorf("Failed comparing blob length") t.Errorf("Failed comparing blob length")
} }
}) })

View File

@ -16,12 +16,13 @@ type Blob interface {
} }
type blobConfig struct { type blobConfig struct {
desc ociv1.Descriptor desc ociv1.Descriptor
header http.Header header http.Header
image ociv1.Image image *ociv1.Image
r ref.Ref r ref.Ref
rdr io.Reader rdr io.Reader
resp *http.Response resp *http.Response
rawBody []byte
} }
type Opts func(*blobConfig) type Opts func(*blobConfig)
@ -43,7 +44,14 @@ func WithHeader(header http.Header) Opts {
// WithImage provides the OCI Image config needed for config blobs // WithImage provides the OCI Image config needed for config blobs
func WithImage(image ociv1.Image) Opts { func WithImage(image ociv1.Image) Opts {
return func(bc *blobConfig) { return func(bc *blobConfig) {
bc.image = image bc.image = &image
}
}
// WithRawBody defines the raw blob contents for OCIConfig
func WithRawBody(raw []byte) Opts {
return func(bc *blobConfig) {
bc.rawBody = raw
} }
} }
@ -65,7 +73,7 @@ func WithRef(r ref.Ref) Opts {
func WithResp(resp *http.Response) Opts { func WithResp(resp *http.Response) Opts {
return func(bc *blobConfig) { return func(bc *blobConfig) {
bc.resp = resp bc.resp = resp
if bc.header == nil { if bc.header == nil && resp != nil {
bc.header = resp.Header bc.header = resp.Header
} }
} }

View File

@ -2,6 +2,7 @@ package blob
import ( import (
"bytes" "bytes"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@ -63,6 +64,11 @@ var (
ContentLength: exLen, ContentLength: exLen,
Body: io.NopCloser(bytes.NewReader(exBlob)), Body: io.NopCloser(bytes.NewReader(exBlob)),
} }
exDesc = ociv1.Descriptor{
MediaType: exMT,
Digest: exDigest,
Size: exLen,
}
) )
func TestCommon(t *testing.T) { func TestCommon(t *testing.T) {
@ -141,14 +147,14 @@ func TestCommon(t *testing.T) {
t.Errorf("rawbody, expected %s, received %s", string(tt.eBytes), string(bb)) t.Errorf("rawbody, expected %s, received %s", string(tt.eBytes), string(bb))
} }
} }
if tt.eDigest != "" && b.Digest() != tt.eDigest { if tt.eDigest != "" && b.GetDescriptor().Digest != tt.eDigest {
t.Errorf("digest, expected %s, received %s", tt.eDigest, b.Digest()) t.Errorf("digest, expected %s, received %s", tt.eDigest, b.GetDescriptor().Digest)
} }
if tt.eLen > 0 && b.Length() != tt.eLen { if tt.eLen > 0 && b.GetDescriptor().Size != tt.eLen {
t.Errorf("length, expected %d, received %d", tt.eLen, b.Length()) t.Errorf("length, expected %d, received %d", tt.eLen, b.GetDescriptor().Size)
} }
if tt.eMT != "" && b.MediaType() != tt.eMT { if tt.eMT != "" && b.GetDescriptor().MediaType != tt.eMT {
t.Errorf("media type, expected %s, received %s", tt.eMT, b.MediaType()) t.Errorf("media type, expected %s, received %s", tt.eMT, b.GetDescriptor().MediaType)
} }
if tt.eHeaders != nil { if tt.eHeaders != nil {
bHeader := b.RawHeaders() bHeader := b.RawHeaders()
@ -160,6 +166,10 @@ func TestCommon(t *testing.T) {
} }
} }
} }
err := b.Close()
if err != nil {
t.Errorf("failed closing blob: %v", err)
}
}) })
} }
} }
@ -218,11 +228,11 @@ func TestReader(t *testing.T) {
t.Errorf("readall: %v", err) t.Errorf("readall: %v", err)
return return
} }
if b.Digest() != exDigest { if b.GetDescriptor().Digest != exDigest {
t.Errorf("digest mismatch, expected %s, received %s", exDigest, b.Digest()) t.Errorf("digest mismatch, expected %s, received %s", exDigest, b.GetDescriptor().Digest)
} }
if b.Length() != exLen { if b.GetDescriptor().Size != exLen {
t.Errorf("length mismatch, expected %d, received %d", exLen, b.Length()) t.Errorf("length mismatch, expected %d, received %d", exLen, b.GetDescriptor().Size)
} }
}) })
@ -243,8 +253,8 @@ func TestReader(t *testing.T) {
t.Errorf("ToOCIConfig: %v", err) t.Errorf("ToOCIConfig: %v", err)
return return
} }
if exDigest != oc.Digest() { if exDigest != oc.GetDescriptor().Digest {
t.Errorf("digest, expected %s, received %s", exDigest, oc.Digest()) t.Errorf("digest, expected %s, received %s", exDigest, oc.GetDescriptor().Digest)
} }
ocb, err := oc.RawBody() ocb, err := oc.RawBody()
if err != nil { if err != nil {
@ -273,6 +283,102 @@ func TestReader(t *testing.T) {
}) })
} }
func TestOCI(t *testing.T) {
ociConfig := ociv1.Image{}
err := json.Unmarshal(exBlob, &ociConfig)
if err != nil {
t.Errorf("failed to unmarshal exBlob: %v", err)
return
}
tests := []struct {
name string
opts []Opts
wantRaw []byte
wantDesc ociv1.Descriptor
}{
{
name: "RawBody",
opts: []Opts{
WithRawBody(exBlob),
WithDesc(exDesc),
},
wantDesc: exDesc,
wantRaw: exBlob,
},
{
name: "Config with Default Desc",
opts: []Opts{
WithImage(ociConfig),
},
wantDesc: ociv1.Descriptor{MediaType: types.MediaTypeOCI1ImageConfig},
},
{
name: "Config with Docker Desc",
opts: []Opts{
WithImage(ociConfig),
WithDesc(exDesc),
},
wantDesc: ociv1.Descriptor{MediaType: exMT},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
oc := NewOCIConfig(tt.opts...)
if tt.wantDesc.Digest != "" && tt.wantDesc.Digest != oc.GetDescriptor().Digest {
t.Errorf("digest, expected %s, received %s", tt.wantDesc.Digest, oc.GetDescriptor().Digest)
}
if tt.wantDesc.MediaType != "" && tt.wantDesc.MediaType != oc.GetDescriptor().MediaType {
t.Errorf("media type, expected %s, received %s", tt.wantDesc.MediaType, oc.GetDescriptor().MediaType)
}
if tt.wantDesc.Size > 0 && tt.wantDesc.Size != oc.GetDescriptor().Size {
t.Errorf("size, expected %d, received %d", tt.wantDesc.Size, oc.GetDescriptor().Size)
}
if len(tt.wantRaw) > 0 {
raw, err := oc.RawBody()
if err != nil {
t.Errorf("config rawbody: %v", err)
return
}
if !bytes.Equal(tt.wantRaw, raw) {
t.Errorf("config bytes, expected %s, received %s", string(tt.wantRaw), string(raw))
}
}
})
}
t.Run("ModConfig", func(t *testing.T) {
// create blob
oc := NewOCIConfig(
WithRawBody(exBlob),
WithDesc(ociv1.Descriptor{
MediaType: exMT,
Digest: exDigest,
Size: exLen,
}),
WithRef(exRef),
)
ociC := oc.GetConfig()
ociC.History = append(ociC.History, ociv1.History{Comment: "test", EmptyLayer: true})
oc.SetConfig(ociC)
// ensure digest and raw body change
if exDigest == oc.GetDescriptor().Digest {
t.Errorf("digest did not change, received %s", oc.GetDescriptor().Digest)
}
if exMT != oc.GetDescriptor().MediaType {
t.Errorf("media type changed, expected %s, received %s", exMT, oc.GetDescriptor().MediaType)
}
raw, err := oc.RawBody()
if err != nil {
t.Errorf("config rawbody: %v", err)
return
}
if bytes.Equal(exBlob, raw) {
t.Errorf("config bytes unchanged, received %s", string(raw))
}
})
}
func cmpSliceString(a, b []string) bool { func cmpSliceString(a, b []string) bool {
if len(a) != len(b) { if len(a) != len(b) {
return false return false

View File

@ -10,11 +10,13 @@ import (
// Common interface is provided by all Blob implementations // Common interface is provided by all Blob implementations
type Common interface { type Common interface {
Digest() digest.Digest GetDescriptor() ociv1.Descriptor
Length() int64
MediaType() string
Response() *http.Response Response() *http.Response
RawHeaders() http.Header RawHeaders() http.Header
Digest() digest.Digest // TODO: deprecate
Length() int64 // TODO: deprecate
MediaType() string // TODO: deprecate
} }
type common struct { type common struct {
@ -25,6 +27,11 @@ type common struct {
resp *http.Response resp *http.Response
} }
// GetDescriptor returns the descriptor associated with the blob
func (b *common) GetDescriptor() ociv1.Descriptor {
return b.desc
}
// Digest returns the provided or calculated digest of the blob // Digest returns the provided or calculated digest of the blob
func (b *common) Digest() digest.Digest { func (b *common) Digest() digest.Digest {
return b.desc.Digest return b.desc.Digest

View File

@ -4,13 +4,16 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/opencontainers/go-digest"
ociv1 "github.com/opencontainers/image-spec/specs-go/v1" ociv1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/regclient/regclient/types"
) )
// OCIConfig wraps an OCI Config struct extracted from a Blob // OCIConfig wraps an OCI Config struct extracted from a Blob
type OCIConfig interface { type OCIConfig interface {
Blob Blob
GetConfig() ociv1.Image GetConfig() ociv1.Image
SetConfig(ociv1.Image)
} }
// ociConfig includes an OCI Config struct extracted from a Blob // ociConfig includes an OCI Config struct extracted from a Blob
@ -27,21 +30,47 @@ func NewOCIConfig(opts ...Opts) OCIConfig {
for _, opt := range opts { for _, opt := range opts {
opt(&bc) opt(&bc)
} }
if bc.image != nil && len(bc.rawBody) == 0 {
var err error
bc.rawBody, err = json.Marshal(bc.image)
if err != nil {
bc.rawBody = []byte{}
}
}
if len(bc.rawBody) > 0 {
if bc.image == nil {
bc.image = &ociv1.Image{}
err := json.Unmarshal(bc.rawBody, bc.image)
if err != nil {
bc.image = nil
}
}
// force descriptor to match raw body, even if we generated the raw body
bc.desc.Digest = digest.FromBytes(bc.rawBody)
bc.desc.Size = int64(len(bc.rawBody))
if bc.desc.MediaType == "" {
bc.desc.MediaType = types.MediaTypeOCI1ImageConfig
}
}
c := common{ c := common{
blobSet: true,
desc: bc.desc, desc: bc.desc,
r: bc.r, r: bc.r,
rawHeader: bc.header, rawHeader: bc.header,
resp: bc.resp, resp: bc.resp,
} }
b := ociConfig{ b := ociConfig{
common: c, common: c,
Image: bc.image, rawBody: bc.rawBody,
}
if bc.image != nil {
b.Image = *bc.image
b.blobSet = true
} }
return &b return &b
} }
// GetConfig returns the original body from the request // GetConfig returns OCI config
func (b *ociConfig) GetConfig() ociv1.Image { func (b *ociConfig) GetConfig() ociv1.Image {
return b.Image return b.Image
} }
@ -57,3 +86,15 @@ func (b *ociConfig) RawBody() ([]byte, error) {
} }
return b.rawBody, err return b.rawBody, err
} }
// SetConfig updates the config, including raw body and descriptor
func (b *ociConfig) SetConfig(c ociv1.Image) {
b.Image = c
b.rawBody, _ = json.Marshal(b.Image)
if b.desc.MediaType == "" {
b.desc.MediaType = types.MediaTypeOCI1ImageConfig
}
b.desc.Digest = digest.FromBytes(b.rawBody)
b.desc.Size = int64(len(b.rawBody))
b.blobSet = true
}

View File

@ -1,14 +1,12 @@
package blob package blob
import ( import (
"encoding/json"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"strconv" "strconv"
"github.com/opencontainers/go-digest" "github.com/opencontainers/go-digest"
ociv1 "github.com/opencontainers/image-spec/specs-go/v1"
) )
// Reader is an unprocessed Blob with an available ReadCloser for reading the Blob // Reader is an unprocessed Blob with an available ReadCloser for reading the Blob
@ -153,11 +151,11 @@ func (b *reader) ToOCIConfig() (OCIConfig, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("error reading image config for %s: %w", b.r.CommonName(), err) return nil, fmt.Errorf("error reading image config for %s: %w", b.r.CommonName(), err)
} }
var ociImage ociv1.Image return NewOCIConfig(
err = json.Unmarshal(blobBody, &ociImage) WithDesc(b.desc),
if err != nil { WithHeader(b.rawHeader),
return nil, fmt.Errorf("error parsing image config for %s: %w", b.r.CommonName(), err) WithRawBody(blobBody),
} WithRef(b.r),
// return the resulting blobOCIConfig, reuse blobCommon, setting rawBody read above, and the unmarshaled OCI image config WithResp(b.resp),
return &ociConfig{common: b.common, rawBody: blobBody, Image: ociImage}, nil ), nil
} }