diff --git a/blob.go b/blob.go index 017f7df..13451ca 100644 --- a/blob.go +++ b/blob.go @@ -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, diff --git a/blob_test.go b/blob_test.go index d5ecf73..94cb15c 100644 --- a/blob_test.go +++ b/blob_test.go @@ -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") } }) diff --git a/scheme/ocidir/blob_test.go b/scheme/ocidir/blob_test.go index 5340a27..604b860 100644 --- a/scheme/ocidir/blob_test.go +++ b/scheme/ocidir/blob_test.go @@ -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) } diff --git a/scheme/reg/blob_test.go b/scheme/reg/blob_test.go index 1025999..1970be3 100644 --- a/scheme/reg/blob_test.go +++ b/scheme/reg/blob_test.go @@ -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") } }) diff --git a/types/blob/blob.go b/types/blob/blob.go index c31bfa7..16c5542 100644 --- a/types/blob/blob.go +++ b/types/blob/blob.go @@ -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 } } diff --git a/types/blob/blob_test.go b/types/blob/blob_test.go index eede24b..378d4eb 100644 --- a/types/blob/blob_test.go +++ b/types/blob/blob_test.go @@ -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 diff --git a/types/blob/common.go b/types/blob/common.go index 32c284f..bb9157b 100644 --- a/types/blob/common.go +++ b/types/blob/common.go @@ -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 diff --git a/types/blob/ociconfig.go b/types/blob/ociconfig.go index 3a5da7b..2ecfa13 100644 --- a/types/blob/ociconfig.go +++ b/types/blob/ociconfig.go @@ -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 +} diff --git a/types/blob/reader.go b/types/blob/reader.go index 608ed3d..992db05 100644 --- a/types/blob/reader.go +++ b/types/blob/reader.go @@ -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 }