1
0
mirror of https://github.com/regclient/regclient.git synced 2025-07-29 09:01:11 +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
}
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{
"err": err,
"src": refSrc.Reference,

View File

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

View File

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

View File

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

View File

@ -16,12 +16,13 @@ type Blob interface {
}
type blobConfig struct {
desc ociv1.Descriptor
header http.Header
image ociv1.Image
r ref.Ref
rdr io.Reader
resp *http.Response
desc ociv1.Descriptor
header http.Header
image *ociv1.Image
r ref.Ref
rdr io.Reader
resp *http.Response
rawBody []byte
}
type Opts func(*blobConfig)
@ -43,7 +44,14 @@ func WithHeader(header http.Header) Opts {
// WithImage provides the OCI Image config needed for config blobs
func WithImage(image ociv1.Image) Opts {
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 {
return func(bc *blobConfig) {
bc.resp = resp
if bc.header == nil {
if bc.header == nil && resp != nil {
bc.header = resp.Header
}
}

View File

@ -2,6 +2,7 @@ package blob
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
@ -63,6 +64,11 @@ var (
ContentLength: exLen,
Body: io.NopCloser(bytes.NewReader(exBlob)),
}
exDesc = ociv1.Descriptor{
MediaType: exMT,
Digest: exDigest,
Size: exLen,
}
)
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))
}
}
if tt.eDigest != "" && b.Digest() != tt.eDigest {
t.Errorf("digest, expected %s, received %s", tt.eDigest, b.Digest())
if tt.eDigest != "" && b.GetDescriptor().Digest != tt.eDigest {
t.Errorf("digest, expected %s, received %s", tt.eDigest, b.GetDescriptor().Digest)
}
if tt.eLen > 0 && b.Length() != tt.eLen {
t.Errorf("length, expected %d, received %d", tt.eLen, b.Length())
if tt.eLen > 0 && b.GetDescriptor().Size != tt.eLen {
t.Errorf("length, expected %d, received %d", tt.eLen, b.GetDescriptor().Size)
}
if tt.eMT != "" && b.MediaType() != tt.eMT {
t.Errorf("media type, expected %s, received %s", tt.eMT, b.MediaType())
if tt.eMT != "" && b.GetDescriptor().MediaType != tt.eMT {
t.Errorf("media type, expected %s, received %s", tt.eMT, b.GetDescriptor().MediaType)
}
if tt.eHeaders != nil {
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)
return
}
if b.Digest() != exDigest {
t.Errorf("digest mismatch, expected %s, received %s", exDigest, b.Digest())
if b.GetDescriptor().Digest != exDigest {
t.Errorf("digest mismatch, expected %s, received %s", exDigest, b.GetDescriptor().Digest)
}
if b.Length() != exLen {
t.Errorf("length mismatch, expected %d, received %d", exLen, b.Length())
if b.GetDescriptor().Size != exLen {
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)
return
}
if exDigest != oc.Digest() {
t.Errorf("digest, expected %s, received %s", exDigest, oc.Digest())
if exDigest != oc.GetDescriptor().Digest {
t.Errorf("digest, expected %s, received %s", exDigest, oc.GetDescriptor().Digest)
}
ocb, err := oc.RawBody()
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 {
if len(a) != len(b) {
return false

View File

@ -10,11 +10,13 @@ import (
// Common interface is provided by all Blob implementations
type Common interface {
Digest() digest.Digest
Length() int64
MediaType() string
GetDescriptor() ociv1.Descriptor
Response() *http.Response
RawHeaders() http.Header
Digest() digest.Digest // TODO: deprecate
Length() int64 // TODO: deprecate
MediaType() string // TODO: deprecate
}
type common struct {
@ -25,6 +27,11 @@ type common struct {
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
func (b *common) Digest() digest.Digest {
return b.desc.Digest

View File

@ -4,13 +4,16 @@ import (
"encoding/json"
"fmt"
"github.com/opencontainers/go-digest"
ociv1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/regclient/regclient/types"
)
// OCIConfig wraps an OCI Config struct extracted from a Blob
type OCIConfig interface {
Blob
GetConfig() ociv1.Image
SetConfig(ociv1.Image)
}
// ociConfig includes an OCI Config struct extracted from a Blob
@ -27,21 +30,47 @@ func NewOCIConfig(opts ...Opts) OCIConfig {
for _, opt := range opts {
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{
blobSet: true,
desc: bc.desc,
r: bc.r,
rawHeader: bc.header,
resp: bc.resp,
}
b := ociConfig{
common: c,
Image: bc.image,
common: c,
rawBody: bc.rawBody,
}
if bc.image != nil {
b.Image = *bc.image
b.blobSet = true
}
return &b
}
// GetConfig returns the original body from the request
// GetConfig returns OCI config
func (b *ociConfig) GetConfig() ociv1.Image {
return b.Image
}
@ -57,3 +86,15 @@ func (b *ociConfig) RawBody() ([]byte, error) {
}
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
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"strconv"
"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
@ -153,11 +151,11 @@ func (b *reader) ToOCIConfig() (OCIConfig, error) {
if err != nil {
return nil, fmt.Errorf("error reading image config for %s: %w", b.r.CommonName(), err)
}
var ociImage ociv1.Image
err = json.Unmarshal(blobBody, &ociImage)
if err != nil {
return nil, fmt.Errorf("error parsing image config for %s: %w", b.r.CommonName(), err)
}
// return the resulting blobOCIConfig, reuse blobCommon, setting rawBody read above, and the unmarshaled OCI image config
return &ociConfig{common: b.common, rawBody: blobBody, Image: ociImage}, nil
return NewOCIConfig(
WithDesc(b.desc),
WithHeader(b.rawHeader),
WithRawBody(blobBody),
WithRef(b.r),
WithResp(b.resp),
), nil
}