mirror of
https://github.com/project-zot/zot.git
synced 2026-06-18 05:28:07 +08:00
d93506909c
Signed-off-by: Vishwas Rajashekar <dev@vrajashkr.com>
289 lines
8.6 KiB
Go
289 lines
8.6 KiB
Go
//go:build sync
|
|
|
|
package sync
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
godigest "github.com/opencontainers/go-digest"
|
|
"github.com/regclient/regclient/types/descriptor"
|
|
rcManifest "github.com/regclient/regclient/types/manifest"
|
|
rcOCIV1 "github.com/regclient/regclient/types/oci/v1"
|
|
. "github.com/smartystreets/goconvey/convey"
|
|
|
|
zerr "zotregistry.dev/zot/v2/errors"
|
|
"zotregistry.dev/zot/v2/pkg/log"
|
|
)
|
|
|
|
type mockStreamTempStore struct {
|
|
blobPathFn func(godigest.Digest) string
|
|
}
|
|
|
|
func (m *mockStreamTempStore) BlobPath(dig godigest.Digest) string {
|
|
if m.blobPathFn != nil {
|
|
return m.blobPathFn(dig)
|
|
}
|
|
|
|
return "/nonexistent/dir/" + dig.Encoded()
|
|
}
|
|
|
|
func newTestChunkingStreamManager(dir string) *ChunkingStreamManager {
|
|
logger := log.NewTestLogger()
|
|
|
|
return &ChunkingStreamManager{
|
|
tempStore: NewLocalTempStore(dir, logger),
|
|
activeStreams: map[string]*ChunkedBlobReader{},
|
|
streamingRefs: map[string]rcManifest.Manifest{},
|
|
blobInfoMap: map[string]descriptor.Descriptor{},
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
func newTestOCIManifestWithBlobs(t *testing.T, configData, layerData []byte) rcManifest.Manifest {
|
|
t.Helper()
|
|
|
|
origMan := rcOCIV1.Manifest{
|
|
Versioned: rcOCIV1.ManifestSchemaVersion,
|
|
Config: descriptor.Descriptor{
|
|
MediaType: "application/vnd.oci.image.config.v1+json",
|
|
Digest: godigest.FromBytes(configData),
|
|
Size: int64(len(configData)),
|
|
},
|
|
Layers: []descriptor.Descriptor{
|
|
{
|
|
MediaType: "application/vnd.oci.image.layer.v1.tar+gzip",
|
|
Digest: godigest.FromBytes(layerData),
|
|
Size: int64(len(layerData)),
|
|
},
|
|
},
|
|
}
|
|
|
|
m, err := rcManifest.New(rcManifest.WithOrig(origMan))
|
|
if err != nil {
|
|
t.Fatalf("failed to create test OCI manifest: %v", err)
|
|
}
|
|
|
|
return m
|
|
}
|
|
|
|
func TestChunkingStreamManagerConnectClient(t *testing.T) {
|
|
Convey("ConnectClient", t, func() {
|
|
sm := newTestChunkingStreamManager(t.TempDir())
|
|
|
|
Convey("returns ErrBlobNotFoundInActiveStreams when blob is not active", func() {
|
|
digest := "sha256:" + strings.Repeat("a", 64)
|
|
copier, err := sm.ConnectClient(digest, &bytes.Buffer{})
|
|
So(errors.Is(err, zerr.ErrBlobNotFoundInActiveStreams), ShouldBeTrue)
|
|
So(copier, ShouldBeNil)
|
|
})
|
|
|
|
Convey("returns error for an unparseable blob digest", func() {
|
|
copier, err := sm.ConnectClient("not-a-valid-digest", &bytes.Buffer{})
|
|
So(err, ShouldNotBeNil)
|
|
So(copier, ShouldBeNil)
|
|
})
|
|
|
|
Convey("returns an InFlightBlobCopier for an active blob", func() {
|
|
blobData := []byte("test blob content")
|
|
desc := descriptor.Descriptor{
|
|
Digest: godigest.FromBytes(blobData),
|
|
Size: int64(len(blobData)),
|
|
MediaType: "application/octet-stream",
|
|
}
|
|
|
|
err := sm.prepareActiveStreamForBlob(desc)
|
|
So(err, ShouldBeNil)
|
|
|
|
copier, err := sm.ConnectClient(desc.Digest.String(), &bytes.Buffer{})
|
|
So(err, ShouldBeNil)
|
|
So(copier, ShouldNotBeNil)
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestChunkingStreamManagerCachedBlobInfo(t *testing.T) {
|
|
Convey("CachedBlobInfo", t, func() {
|
|
sm := newTestChunkingStreamManager(t.TempDir())
|
|
|
|
Convey("returns ErrBlobNotFound for an unknown blob", func() {
|
|
digest := "sha256:" + strings.Repeat("b", 64)
|
|
size, mt, err := sm.CachedBlobInfo(digest)
|
|
So(errors.Is(err, zerr.ErrBlobNotFound), ShouldBeTrue)
|
|
So(size, ShouldEqual, 0)
|
|
So(mt, ShouldBeEmpty)
|
|
})
|
|
|
|
Convey("returns size and media type for a known blob", func() {
|
|
blobData := []byte("cached blob data")
|
|
desc := descriptor.Descriptor{
|
|
Digest: godigest.FromBytes(blobData),
|
|
Size: int64(len(blobData)),
|
|
MediaType: "application/vnd.oci.image.layer.v1.tar+gzip",
|
|
}
|
|
|
|
sm.blobInfoMap[desc.Digest.String()] = desc
|
|
|
|
size, mt, err := sm.CachedBlobInfo(desc.Digest.String())
|
|
So(err, ShouldBeNil)
|
|
So(size, ShouldEqual, int64(len(blobData)))
|
|
So(mt, ShouldEqual, "application/vnd.oci.image.layer.v1.tar+gzip")
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestChunkingStreamManagerStreamingBlobReader(t *testing.T) {
|
|
Convey("StreamingBlobReader", t, func() {
|
|
sm := newTestChunkingStreamManager(t.TempDir())
|
|
|
|
Convey("returns ErrBlobReaderMissing when blob has no active stream", func() {
|
|
data := []byte("some blob")
|
|
reader := newTestBReader(data)
|
|
result, err := sm.StreamingBlobReader(reader)
|
|
So(errors.Is(err, zerr.ErrBlobReaderMissing), ShouldBeTrue)
|
|
So(result, ShouldBeNil)
|
|
})
|
|
|
|
Convey("initialises the chunked reader and returns a wrapped BReader for an active stream", func() {
|
|
data := []byte("streaming blob")
|
|
desc := descriptor.Descriptor{
|
|
Digest: godigest.FromBytes(data),
|
|
Size: int64(len(data)),
|
|
MediaType: "application/octet-stream",
|
|
}
|
|
|
|
err := sm.prepareActiveStreamForBlob(desc)
|
|
So(err, ShouldBeNil)
|
|
|
|
reader := newTestBReader(data)
|
|
result, err := sm.StreamingBlobReader(reader)
|
|
So(err, ShouldBeNil)
|
|
So(result, ShouldNotBeNil)
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestChunkingStreamManagerStoreImageForStreaming(t *testing.T) {
|
|
Convey("StoreImageForStreaming", t, func() {
|
|
sm := newTestChunkingStreamManager(t.TempDir())
|
|
|
|
configData := []byte("config-payload")
|
|
layerData := []byte("layer-payload")
|
|
manifest := newTestOCIManifestWithBlobs(t, configData, layerData)
|
|
|
|
Convey("stores manifest and prepares active streams for all blobs", func() {
|
|
err := sm.StoreImageForStreaming("myrepo", "v1.0", manifest)
|
|
So(err, ShouldBeNil)
|
|
|
|
// Manifest entry should be stored.
|
|
m, ok := sm.StreamingImageManifest("myrepo", "v1.0")
|
|
So(ok, ShouldBeTrue)
|
|
So(m, ShouldEqual, manifest)
|
|
|
|
// All three blobs (manifest, config, layer) should be active streams.
|
|
manifestDigest := manifest.GetDescriptor().Digest.String()
|
|
configDigest := godigest.FromBytes(configData).String()
|
|
layerDigest := godigest.FromBytes(layerData).String()
|
|
|
|
_, hasManifest := sm.activeStreams[manifestDigest]
|
|
So(hasManifest, ShouldBeTrue)
|
|
|
|
_, hasConfig := sm.activeStreams[configDigest]
|
|
So(hasConfig, ShouldBeTrue)
|
|
|
|
_, hasLayer := sm.activeStreams[layerDigest]
|
|
So(hasLayer, ShouldBeTrue)
|
|
})
|
|
|
|
Convey("storing the same repo:reference is idempotent", func() {
|
|
err := sm.StoreImageForStreaming("myrepo", "v1.0", manifest)
|
|
So(err, ShouldBeNil)
|
|
|
|
err = sm.StoreImageForStreaming("myrepo", "v1.0", manifest)
|
|
So(err, ShouldBeNil)
|
|
|
|
_, ok := sm.StreamingImageManifest("myrepo", "v1.0")
|
|
So(ok, ShouldBeTrue)
|
|
})
|
|
|
|
Convey("propagates error when the temp store cannot create a blob path", func() {
|
|
sm.tempStore = &mockStreamTempStore{
|
|
blobPathFn: func(_ godigest.Digest) string {
|
|
return "/nonexistent/dir/blob"
|
|
},
|
|
}
|
|
|
|
err := sm.StoreImageForStreaming("myrepo", "v1.0", manifest)
|
|
So(err, ShouldNotBeNil)
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestChunkingStreamManagerStreamingImageManifest(t *testing.T) {
|
|
Convey("StreamingImageManifest", t, func() {
|
|
sm := newTestChunkingStreamManager(t.TempDir())
|
|
|
|
manifest := newTestOCIManifestWithBlobs(t, []byte("cfg"), []byte("lyr"))
|
|
|
|
Convey("returns nil and false when no entry exists", func() {
|
|
m, ok := sm.StreamingImageManifest("repo", "tag")
|
|
So(ok, ShouldBeFalse)
|
|
So(m, ShouldBeNil)
|
|
})
|
|
|
|
Convey("returns the manifest and true after it has been stored", func() {
|
|
err := sm.StoreImageForStreaming("repo", "tag", manifest)
|
|
So(err, ShouldBeNil)
|
|
|
|
m, ok := sm.StreamingImageManifest("repo", "tag")
|
|
So(ok, ShouldBeTrue)
|
|
So(m, ShouldEqual, manifest)
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestChunkingStreamManagerRemoveStreamingImage(t *testing.T) {
|
|
Convey("RemoveStreamingImage", t, func() {
|
|
sm := newTestChunkingStreamManager(t.TempDir())
|
|
|
|
Convey("does not panic when no entry exists for the given repo:reference", func() {
|
|
So(func() { sm.RemoveStreamingImage("nothere", "v0") }, ShouldNotPanic)
|
|
})
|
|
|
|
Convey("removes manifest and all associated blobs after a successful store", func() {
|
|
configData := []byte("cfg-payload")
|
|
layerData := []byte("lyr-payload")
|
|
manifest := newTestOCIManifestWithBlobs(t, configData, layerData)
|
|
|
|
err := sm.StoreImageForStreaming("myrepo", "latest", manifest)
|
|
So(err, ShouldBeNil)
|
|
|
|
manifestDigest := manifest.GetDescriptor().Digest.String()
|
|
configDigest := godigest.FromBytes(configData).String()
|
|
layerDigest := godigest.FromBytes(layerData).String()
|
|
|
|
// Confirm blobs are active before removal.
|
|
_, ok := sm.activeStreams[manifestDigest]
|
|
So(ok, ShouldBeTrue)
|
|
|
|
sm.RemoveStreamingImage("myrepo", "latest")
|
|
|
|
// Manifest entry should be gone.
|
|
_, found := sm.StreamingImageManifest("myrepo", "latest")
|
|
So(found, ShouldBeFalse)
|
|
|
|
// All active streams should be cleaned up.
|
|
_, stillHasManifest := sm.activeStreams[manifestDigest]
|
|
So(stillHasManifest, ShouldBeFalse)
|
|
|
|
_, stillHasConfig := sm.activeStreams[configDigest]
|
|
So(stillHasConfig, ShouldBeFalse)
|
|
|
|
_, stillHasLayer := sm.activeStreams[layerDigest]
|
|
So(stillHasLayer, ShouldBeFalse)
|
|
})
|
|
})
|
|
}
|