mirror of
https://github.com/project-zot/zot.git
synced 2026-06-17 21:17:58 +08:00
3c7d5a5f1d
feat(meta): add TaggedTimestamp field and preserve during re-parsing Add TaggedTimestamp field to track when image tags were created, exposed through GraphQL API. Previously, when zot restarted and re-parsed storage, ResetRepoReferences would clear all tags, causing timestamp information to be lost and reset to the service restart time for existing images. This change adds TaggedTimestamp support and modifies ResetRepoReferences to selectively preserve tags that still exist in storage, maintaining their TaggedTimestamp values. Tags that no longer exist in storage are removed as before. Changes: - Add TaggedTimestamp field to GraphQL ImageSummary schema - Update GraphQL conversion functions to populate TaggedTimestamp with fallback to PushTimestamp when unavailable - Updated ResetRepoReferences interface to accept tagsToKeep parameter - Modified ParseRepo to collect tags from storage before resetting - Updated all backend implementations (Redis, DynamoDB, BoltDB) to preserve tags in tagsToKeep instead of clearing all tags - Updated tests and mocks to match new signature This ensures TaggedTimestamp accurately reflects when tags were originally created, and exposes this information through the GraphQL API. Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com>
617 lines
19 KiB
Go
617 lines
19 KiB
Go
package common_test
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
godigest "github.com/opencontainers/go-digest"
|
|
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
|
. "github.com/smartystreets/goconvey/convey"
|
|
"google.golang.org/protobuf/types/known/timestamppb"
|
|
|
|
"zotregistry.dev/zot/v2/pkg/meta/common"
|
|
proto_go "zotregistry.dev/zot/v2/pkg/meta/proto/gen"
|
|
mTypes "zotregistry.dev/zot/v2/pkg/meta/types"
|
|
)
|
|
|
|
var ErrTestError = errors.New("test error")
|
|
|
|
func TestUtils(t *testing.T) {
|
|
Convey("GetPartialImageMeta", t, func() {
|
|
So(func() { common.GetPartialImageMeta(mTypes.ImageMeta{}, mTypes.ImageMeta{}) }, ShouldNotPanic)
|
|
})
|
|
|
|
Convey("MatchesArtifactTypes", t, func() {
|
|
res := common.MatchesArtifactTypes("", nil)
|
|
So(res, ShouldBeTrue)
|
|
|
|
res = common.MatchesArtifactTypes("type", []string{"someOtherType"})
|
|
So(res, ShouldBeFalse)
|
|
})
|
|
|
|
Convey("GetProtoPlatform", t, func() {
|
|
platform := common.GetProtoPlatform(nil)
|
|
So(platform, ShouldBeNil)
|
|
})
|
|
|
|
Convey("ValidateRepoReferenceInput", t, func() {
|
|
err := common.ValidateRepoReferenceInput("", "tag", "digest")
|
|
So(err, ShouldNotBeNil)
|
|
err = common.ValidateRepoReferenceInput("repo", "", "digest")
|
|
So(err, ShouldNotBeNil)
|
|
err = common.ValidateRepoReferenceInput("repo", "tag", "")
|
|
So(err, ShouldNotBeNil)
|
|
})
|
|
|
|
Convey("CheckImageLastUpdated", t, func() {
|
|
Convey("No image checked, it doesn't have time", func() {
|
|
repoLastUpdated := time.Time{}
|
|
isSigned := false
|
|
noImageChecked := true
|
|
manifestFilterData := mTypes.FilterData{
|
|
DownloadCount: 10,
|
|
LastUpdated: time.Time{},
|
|
IsSigned: true,
|
|
}
|
|
|
|
repoLastUpdated, noImageChecked, isSigned = common.CheckImageLastUpdated(repoLastUpdated, isSigned, noImageChecked,
|
|
manifestFilterData)
|
|
So(repoLastUpdated, ShouldResemble, manifestFilterData.LastUpdated)
|
|
So(isSigned, ShouldEqual, manifestFilterData.IsSigned)
|
|
So(noImageChecked, ShouldEqual, false)
|
|
})
|
|
|
|
Convey("First image checked, it has time", func() {
|
|
repoLastUpdated := time.Time{}
|
|
isSigned := false
|
|
noImageChecked := true
|
|
manifestFilterData := mTypes.FilterData{
|
|
DownloadCount: 10,
|
|
LastUpdated: time.Date(2000, 1, 1, 1, 1, 1, 1, time.UTC),
|
|
IsSigned: true,
|
|
}
|
|
|
|
repoLastUpdated, noImageChecked, isSigned = common.CheckImageLastUpdated(repoLastUpdated, isSigned, noImageChecked,
|
|
manifestFilterData)
|
|
So(repoLastUpdated, ShouldResemble, manifestFilterData.LastUpdated)
|
|
So(isSigned, ShouldEqual, manifestFilterData.IsSigned)
|
|
So(noImageChecked, ShouldEqual, false)
|
|
})
|
|
|
|
Convey("Not first image checked, current image is newer", func() {
|
|
repoLastUpdated := time.Date(2000, 1, 1, 1, 1, 1, 1, time.UTC)
|
|
isSigned := true
|
|
noImageChecked := false
|
|
manifestFilterData := mTypes.FilterData{
|
|
DownloadCount: 10,
|
|
LastUpdated: time.Date(2023, 1, 1, 1, 1, 1, 1, time.UTC),
|
|
IsSigned: false,
|
|
}
|
|
|
|
repoLastUpdated, noImageChecked, isSigned = common.CheckImageLastUpdated(repoLastUpdated, isSigned,
|
|
noImageChecked, manifestFilterData)
|
|
So(repoLastUpdated, ShouldResemble, manifestFilterData.LastUpdated)
|
|
So(isSigned, ShouldEqual, manifestFilterData.IsSigned)
|
|
So(noImageChecked, ShouldEqual, false)
|
|
})
|
|
|
|
Convey("Not first image checked, current image is older", func() {
|
|
repoLastUpdated := time.Date(2024, 1, 1, 1, 1, 1, 1, time.UTC)
|
|
isSigned := false
|
|
noImageChecked := false
|
|
manifestFilterData := mTypes.FilterData{
|
|
DownloadCount: 10,
|
|
LastUpdated: time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC),
|
|
IsSigned: true,
|
|
}
|
|
|
|
updatedRepoLastUpdated, noImageChecked, isSigned := common.CheckImageLastUpdated(repoLastUpdated, isSigned,
|
|
noImageChecked,
|
|
manifestFilterData)
|
|
So(updatedRepoLastUpdated, ShouldResemble, repoLastUpdated)
|
|
So(isSigned, ShouldEqual, false)
|
|
So(noImageChecked, ShouldEqual, false)
|
|
})
|
|
})
|
|
|
|
Convey("SignatureAlreadyExists", t, func() {
|
|
res := common.SignatureAlreadyExists(
|
|
[]mTypes.SignatureInfo{{SignatureManifestDigest: "digest"}},
|
|
mTypes.SignatureMetadata{SignatureDigest: "digest"},
|
|
)
|
|
|
|
So(res, ShouldEqual, true)
|
|
|
|
res = common.SignatureAlreadyExists(
|
|
[]mTypes.SignatureInfo{{SignatureManifestDigest: "digest"}},
|
|
mTypes.SignatureMetadata{SignatureDigest: "digest2"},
|
|
)
|
|
|
|
So(res, ShouldEqual, false)
|
|
})
|
|
|
|
Convey("RemoveImageFromRepoMeta", t, func() {
|
|
Convey("should handle nil blob info for descriptor digest and continue with other tags", func() {
|
|
now := time.Now()
|
|
repoMeta := &proto_go.RepoMeta{
|
|
Name: "test-repo",
|
|
Tags: map[string]*proto_go.TagDescriptor{
|
|
"tag1": {
|
|
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
|
Digest: "sha256:manifest1",
|
|
},
|
|
"tag-missing": {
|
|
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
|
Digest: "sha256:missing",
|
|
},
|
|
"tag2": {
|
|
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
|
Digest: "sha256:manifest2",
|
|
},
|
|
},
|
|
}
|
|
|
|
repoBlobs := &proto_go.RepoBlobs{
|
|
Blobs: map[string]*proto_go.BlobInfo{
|
|
"sha256:manifest1": {
|
|
Size: 1000,
|
|
LastUpdated: timestamppb.New(now),
|
|
SubBlobs: []string{"sha256:layer1"},
|
|
},
|
|
"sha256:layer1": {
|
|
Size: 500,
|
|
},
|
|
// Intentionally missing "sha256:missing" for tag-missing to test nil check
|
|
// The function should skip tag-missing and continue processing tag2
|
|
"sha256:manifest2": {
|
|
Size: 2000,
|
|
LastUpdated: timestamppb.New(now.Add(time.Hour)),
|
|
SubBlobs: []string{"sha256:layer2"},
|
|
},
|
|
"sha256:layer2": {
|
|
Size: 800,
|
|
},
|
|
},
|
|
}
|
|
|
|
// Remove tag1 (simulating actual usage pattern)
|
|
delete(repoMeta.Tags, "tag1")
|
|
|
|
// Should not panic when tag-missing has nil blob info
|
|
So(func() {
|
|
common.RemoveImageFromRepoMeta(repoMeta, repoBlobs, "tag1")
|
|
}, ShouldNotPanic)
|
|
|
|
resultMeta, resultBlobs := common.RemoveImageFromRepoMeta(repoMeta, repoBlobs, "tag1")
|
|
So(resultMeta, ShouldNotBeNil)
|
|
So(resultBlobs, ShouldNotBeNil)
|
|
|
|
// tag-missing remains in metadata but has no blobs (inconsistent state, acceptable in GC scenarios)
|
|
So(resultMeta.Tags["tag-missing"], ShouldNotBeNil)
|
|
|
|
// Should include blobs from tag2 (which has valid blob info)
|
|
So(len(resultBlobs.Blobs), ShouldEqual, 2)
|
|
So(resultBlobs.Blobs["sha256:manifest2"], ShouldNotBeNil)
|
|
So(resultBlobs.Blobs["sha256:layer2"], ShouldNotBeNil)
|
|
|
|
// Should have correct size from tag2 only
|
|
expectedSize := int64(2000 + 800)
|
|
So(resultMeta.Size, ShouldEqual, expectedSize)
|
|
|
|
// Should have updated last image from tag2
|
|
So(resultMeta.LastUpdatedImage, ShouldNotBeNil)
|
|
So(resultMeta.LastUpdatedImage.Digest, ShouldEqual, "sha256:manifest2")
|
|
})
|
|
|
|
Convey("should handle nil blob info in queue traversal and continue processing", func() {
|
|
now := time.Now()
|
|
repoMeta := &proto_go.RepoMeta{
|
|
Name: "test-repo",
|
|
Tags: map[string]*proto_go.TagDescriptor{
|
|
"tag1": {
|
|
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
|
Digest: "sha256:manifest1",
|
|
},
|
|
},
|
|
}
|
|
|
|
repoBlobs := &proto_go.RepoBlobs{
|
|
Blobs: map[string]*proto_go.BlobInfo{
|
|
"sha256:manifest1": {
|
|
Size: 1000,
|
|
LastUpdated: timestamppb.New(now),
|
|
// Mix of valid and missing sub-blobs to test that processing continues
|
|
SubBlobs: []string{"sha256:layer1", "sha256:missing-layer", "sha256:layer2"},
|
|
},
|
|
"sha256:layer1": {
|
|
Size: 500,
|
|
},
|
|
// Intentionally missing "sha256:missing-layer" to trigger nil check in queue traversal
|
|
// The function should skip it and continue processing layer2
|
|
"sha256:layer2": {
|
|
Size: 300,
|
|
SubBlobs: []string{"sha256:layer3"},
|
|
},
|
|
"sha256:layer3": {
|
|
Size: 200,
|
|
},
|
|
},
|
|
}
|
|
|
|
// Remove the tag before calling RemoveImageFromRepoMeta (as done in actual usage)
|
|
delete(repoMeta.Tags, "tag1")
|
|
|
|
// Should not panic when a sub-blob is nil
|
|
So(func() {
|
|
common.RemoveImageFromRepoMeta(repoMeta, repoBlobs, "tag1")
|
|
}, ShouldNotPanic)
|
|
|
|
resultMeta, resultBlobs := common.RemoveImageFromRepoMeta(repoMeta, repoBlobs, "tag1")
|
|
So(resultMeta, ShouldNotBeNil)
|
|
So(resultBlobs, ShouldNotBeNil)
|
|
|
|
// Verify tag1 was removed
|
|
So(resultMeta.Tags["tag1"], ShouldBeNil)
|
|
|
|
// After removing tag1, no blobs should remain
|
|
So(len(resultBlobs.Blobs), ShouldEqual, 0)
|
|
})
|
|
|
|
Convey("should handle multiple nil blobs in deeply nested structure", func() {
|
|
now := time.Now()
|
|
repoMeta := &proto_go.RepoMeta{
|
|
Name: "test-repo",
|
|
Tags: map[string]*proto_go.TagDescriptor{
|
|
"tag-valid": {
|
|
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
|
Digest: "sha256:manifest1",
|
|
},
|
|
},
|
|
}
|
|
|
|
repoBlobs := &proto_go.RepoBlobs{
|
|
Blobs: map[string]*proto_go.BlobInfo{
|
|
"sha256:manifest1": {
|
|
Size: 1000,
|
|
LastUpdated: timestamppb.New(now),
|
|
// Multiple missing sub-blobs interspersed with valid ones
|
|
SubBlobs: []string{
|
|
"sha256:missing1",
|
|
"sha256:layer1",
|
|
"sha256:missing2",
|
|
"sha256:layer2",
|
|
"sha256:missing3",
|
|
},
|
|
},
|
|
"sha256:layer1": {
|
|
Size: 500,
|
|
SubBlobs: []string{"sha256:missing4", "sha256:nested-layer"},
|
|
},
|
|
"sha256:layer2": {
|
|
Size: 300,
|
|
},
|
|
"sha256:nested-layer": {
|
|
Size: 100,
|
|
},
|
|
// Intentionally missing: missing1, missing2, missing3, missing4
|
|
},
|
|
}
|
|
|
|
// Should not panic with multiple missing blobs at various levels
|
|
So(func() {
|
|
common.RemoveImageFromRepoMeta(repoMeta, repoBlobs, "nonexistent")
|
|
}, ShouldNotPanic)
|
|
|
|
resultMeta, resultBlobs := common.RemoveImageFromRepoMeta(repoMeta, repoBlobs, "nonexistent")
|
|
So(resultMeta, ShouldNotBeNil)
|
|
So(resultBlobs, ShouldNotBeNil)
|
|
|
|
// Should only include the valid blobs that were successfully traversed
|
|
So(len(resultBlobs.Blobs), ShouldEqual, 4) // manifest1, layer1, layer2, nested-layer
|
|
So(resultBlobs.Blobs["sha256:manifest1"], ShouldNotBeNil)
|
|
So(resultBlobs.Blobs["sha256:layer1"], ShouldNotBeNil)
|
|
So(resultBlobs.Blobs["sha256:layer2"], ShouldNotBeNil)
|
|
So(resultBlobs.Blobs["sha256:nested-layer"], ShouldNotBeNil)
|
|
|
|
// Verify correct size calculation (only valid blobs)
|
|
expectedSize := int64(1000 + 500 + 300 + 100)
|
|
So(resultMeta.Size, ShouldEqual, expectedSize)
|
|
})
|
|
|
|
Convey("should work correctly with valid blob info", func() {
|
|
now := time.Now()
|
|
repoMeta := &proto_go.RepoMeta{
|
|
Name: "test-repo",
|
|
Tags: map[string]*proto_go.TagDescriptor{
|
|
"tag1": {
|
|
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
|
Digest: "sha256:manifest1",
|
|
},
|
|
"tag2": {
|
|
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
|
Digest: "sha256:manifest2",
|
|
},
|
|
},
|
|
}
|
|
|
|
repoBlobs := &proto_go.RepoBlobs{
|
|
Blobs: map[string]*proto_go.BlobInfo{
|
|
"sha256:manifest1": {
|
|
Size: 1000,
|
|
LastUpdated: timestamppb.New(now),
|
|
SubBlobs: []string{"sha256:layer1"},
|
|
Vendors: []string{"vendor1"},
|
|
Platforms: []*proto_go.Platform{{OS: "linux", Architecture: "amd64"}},
|
|
},
|
|
"sha256:layer1": {
|
|
Size: 500,
|
|
},
|
|
"sha256:manifest2": {
|
|
Size: 2000,
|
|
LastUpdated: timestamppb.New(now.Add(time.Hour)),
|
|
SubBlobs: []string{"sha256:layer2"},
|
|
},
|
|
"sha256:layer2": {
|
|
Size: 800,
|
|
},
|
|
},
|
|
}
|
|
|
|
// Remove the tag before calling RemoveImageFromRepoMeta (as done in actual usage)
|
|
delete(repoMeta.Tags, "tag1")
|
|
|
|
resultMeta, resultBlobs := common.RemoveImageFromRepoMeta(repoMeta, repoBlobs, "tag1")
|
|
So(resultMeta, ShouldNotBeNil)
|
|
So(resultBlobs, ShouldNotBeNil)
|
|
|
|
// Verify tag1 was removed
|
|
So(resultMeta.Tags["tag1"], ShouldBeNil)
|
|
|
|
// Should only include blobs from remaining tag2 (manifest2 and layer2)
|
|
So(len(resultBlobs.Blobs), ShouldEqual, 2)
|
|
So(resultBlobs.Blobs["sha256:manifest2"], ShouldNotBeNil)
|
|
So(resultBlobs.Blobs["sha256:layer2"], ShouldNotBeNil)
|
|
|
|
// Should calculate total size correctly (only tag2 blobs)
|
|
expectedSize := int64(2000 + 800)
|
|
So(resultMeta.Size, ShouldEqual, expectedSize)
|
|
|
|
// Should have updated last image
|
|
So(resultMeta.LastUpdatedImage, ShouldNotBeNil)
|
|
})
|
|
|
|
Convey("should handle empty tags", func() {
|
|
repoMeta := &proto_go.RepoMeta{
|
|
Name: "test-repo",
|
|
Tags: map[string]*proto_go.TagDescriptor{},
|
|
}
|
|
|
|
repoBlobs := &proto_go.RepoBlobs{
|
|
Blobs: map[string]*proto_go.BlobInfo{},
|
|
}
|
|
|
|
So(func() {
|
|
common.RemoveImageFromRepoMeta(repoMeta, repoBlobs, "tag1")
|
|
}, ShouldNotPanic)
|
|
|
|
resultMeta, resultBlobs := common.RemoveImageFromRepoMeta(repoMeta, repoBlobs, "tag1")
|
|
So(resultMeta, ShouldNotBeNil)
|
|
So(resultBlobs, ShouldNotBeNil)
|
|
So(resultMeta.Size, ShouldEqual, 0)
|
|
So(len(resultBlobs.Blobs), ShouldEqual, 0)
|
|
})
|
|
|
|
Convey("should skip tags with empty digest and continue processing", func() {
|
|
now := time.Now()
|
|
repoMeta := &proto_go.RepoMeta{
|
|
Name: "test-repo",
|
|
Tags: map[string]*proto_go.TagDescriptor{
|
|
"tag-empty": {
|
|
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
|
Digest: "", // Empty digest - should be skipped
|
|
},
|
|
"tag-valid": {
|
|
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
|
Digest: "sha256:manifest1",
|
|
},
|
|
},
|
|
}
|
|
|
|
repoBlobs := &proto_go.RepoBlobs{
|
|
Blobs: map[string]*proto_go.BlobInfo{
|
|
"sha256:manifest1": {
|
|
Size: 1000,
|
|
LastUpdated: timestamppb.New(now),
|
|
SubBlobs: []string{"sha256:layer1"},
|
|
},
|
|
"sha256:layer1": {
|
|
Size: 500,
|
|
},
|
|
},
|
|
}
|
|
|
|
So(func() {
|
|
common.RemoveImageFromRepoMeta(repoMeta, repoBlobs, "tag-empty")
|
|
}, ShouldNotPanic)
|
|
|
|
resultMeta, resultBlobs := common.RemoveImageFromRepoMeta(repoMeta, repoBlobs, "tag-empty")
|
|
So(resultMeta, ShouldNotBeNil)
|
|
So(resultBlobs, ShouldNotBeNil)
|
|
|
|
// Should skip tag-empty and process tag-valid
|
|
So(len(resultBlobs.Blobs), ShouldEqual, 2)
|
|
So(resultBlobs.Blobs["sha256:manifest1"], ShouldNotBeNil)
|
|
So(resultBlobs.Blobs["sha256:layer1"], ShouldNotBeNil)
|
|
|
|
expectedSize := int64(1000 + 500)
|
|
So(resultMeta.Size, ShouldEqual, expectedSize)
|
|
})
|
|
|
|
Convey("should handle combined edge cases - empty digest, nil descriptor blob, and nil queue blob", func() {
|
|
now := time.Now()
|
|
repoMeta := &proto_go.RepoMeta{
|
|
Name: "test-repo",
|
|
Tags: map[string]*proto_go.TagDescriptor{
|
|
"tag-empty": {
|
|
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
|
Digest: "", // Empty digest
|
|
},
|
|
"tag-nil-descriptor": {
|
|
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
|
Digest: "sha256:missing-descriptor",
|
|
},
|
|
"tag-nil-in-queue": {
|
|
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
|
Digest: "sha256:manifest-with-missing-blobs",
|
|
},
|
|
"tag-valid": {
|
|
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
|
Digest: "sha256:valid-manifest",
|
|
},
|
|
},
|
|
}
|
|
|
|
repoBlobs := &proto_go.RepoBlobs{
|
|
Blobs: map[string]*proto_go.BlobInfo{
|
|
// Missing "sha256:missing-descriptor" to trigger descriptor nil check
|
|
"sha256:manifest-with-missing-blobs": {
|
|
Size: 1000,
|
|
LastUpdated: timestamppb.New(now),
|
|
SubBlobs: []string{"sha256:missing-in-queue", "sha256:valid-layer1"},
|
|
},
|
|
// Missing "sha256:missing-in-queue" to trigger queue nil check
|
|
"sha256:valid-layer1": {
|
|
Size: 300,
|
|
},
|
|
"sha256:valid-manifest": {
|
|
Size: 2000,
|
|
LastUpdated: timestamppb.New(now.Add(2 * time.Hour)),
|
|
SubBlobs: []string{"sha256:valid-layer2"},
|
|
},
|
|
"sha256:valid-layer2": {
|
|
Size: 800,
|
|
},
|
|
},
|
|
}
|
|
|
|
// Should not panic with multiple types of issues
|
|
So(func() {
|
|
common.RemoveImageFromRepoMeta(repoMeta, repoBlobs, "nonexistent")
|
|
}, ShouldNotPanic)
|
|
|
|
resultMeta, resultBlobs := common.RemoveImageFromRepoMeta(repoMeta, repoBlobs, "nonexistent")
|
|
So(resultMeta, ShouldNotBeNil)
|
|
So(resultBlobs, ShouldNotBeNil)
|
|
|
|
// Should include only valid blobs:
|
|
// - tag-valid's blobs (valid-manifest + valid-layer2)
|
|
// - tag-nil-in-queue's valid blobs (manifest-with-missing-blobs + valid-layer1)
|
|
So(len(resultBlobs.Blobs), ShouldEqual, 4)
|
|
So(resultBlobs.Blobs["sha256:valid-manifest"], ShouldNotBeNil)
|
|
So(resultBlobs.Blobs["sha256:valid-layer2"], ShouldNotBeNil)
|
|
So(resultBlobs.Blobs["sha256:manifest-with-missing-blobs"], ShouldNotBeNil)
|
|
So(resultBlobs.Blobs["sha256:valid-layer1"], ShouldNotBeNil)
|
|
|
|
// Verify correct size (all valid blobs)
|
|
expectedSize := int64(2000 + 800 + 1000 + 300)
|
|
So(resultMeta.Size, ShouldEqual, expectedSize)
|
|
|
|
// Last updated should be from the most recent valid blob
|
|
So(resultMeta.LastUpdatedImage, ShouldNotBeNil)
|
|
})
|
|
})
|
|
|
|
Convey("AddImageMetaToRepoMeta", t, func() {
|
|
Convey("should handle ImageManifest with empty Manifests slice", func() {
|
|
repoMeta := &proto_go.RepoMeta{
|
|
Name: "test-repo",
|
|
Tags: map[string]*proto_go.TagDescriptor{},
|
|
}
|
|
|
|
repoBlobs := &proto_go.RepoBlobs{
|
|
Blobs: map[string]*proto_go.BlobInfo{},
|
|
}
|
|
|
|
testDigest := godigest.FromString("sha256:testdigest")
|
|
imageMeta := mTypes.ImageMeta{
|
|
MediaType: ispec.MediaTypeImageManifest,
|
|
Digest: testDigest,
|
|
Size: 1000,
|
|
Manifests: []mTypes.ManifestMeta{}, // Empty Manifests slice
|
|
}
|
|
|
|
// Should not panic
|
|
So(func() {
|
|
common.AddImageMetaToRepoMeta(repoMeta, repoBlobs, "tag1", imageMeta)
|
|
}, ShouldNotPanic)
|
|
|
|
resultMeta, resultBlobs := common.AddImageMetaToRepoMeta(repoMeta, repoBlobs, "tag1", imageMeta)
|
|
So(resultMeta, ShouldNotBeNil)
|
|
So(resultBlobs, ShouldNotBeNil)
|
|
|
|
// Should add basic blob info with just Size
|
|
digestStr := testDigest.String()
|
|
So(resultBlobs.Blobs[digestStr], ShouldNotBeNil)
|
|
So(resultBlobs.Blobs[digestStr].Size, ShouldEqual, 1000)
|
|
// Should not have SubBlobs, Vendors, Platforms, or LastUpdated since Manifests is empty
|
|
So(resultBlobs.Blobs[digestStr].SubBlobs, ShouldBeNil)
|
|
So(resultBlobs.Blobs[digestStr].Vendors, ShouldBeNil)
|
|
So(resultBlobs.Blobs[digestStr].Platforms, ShouldBeNil)
|
|
So(resultBlobs.Blobs[digestStr].LastUpdated, ShouldBeNil)
|
|
})
|
|
|
|
Convey("should handle ImageManifest with valid Manifests", func() {
|
|
repoMeta := &proto_go.RepoMeta{
|
|
Name: "test-repo",
|
|
Tags: map[string]*proto_go.TagDescriptor{},
|
|
}
|
|
|
|
repoBlobs := &proto_go.RepoBlobs{
|
|
Blobs: map[string]*proto_go.BlobInfo{},
|
|
}
|
|
|
|
testDigest := godigest.FromString("sha256:testdigest")
|
|
configDigest := godigest.FromString("sha256:configdigest")
|
|
layerDigest := godigest.FromString("sha256:layerdigest")
|
|
|
|
imageMeta := mTypes.ImageMeta{
|
|
MediaType: ispec.MediaTypeImageManifest,
|
|
Digest: testDigest,
|
|
Size: 1000,
|
|
Manifests: []mTypes.ManifestMeta{
|
|
{
|
|
Digest: testDigest,
|
|
Size: 1000,
|
|
Manifest: ispec.Manifest{
|
|
Config: ispec.Descriptor{
|
|
Digest: configDigest,
|
|
Size: 500,
|
|
},
|
|
Layers: []ispec.Descriptor{
|
|
{
|
|
Digest: layerDigest,
|
|
Size: 300,
|
|
},
|
|
},
|
|
},
|
|
Config: ispec.Image{},
|
|
},
|
|
},
|
|
}
|
|
|
|
resultMeta, resultBlobs := common.AddImageMetaToRepoMeta(repoMeta, repoBlobs, "tag1", imageMeta)
|
|
So(resultMeta, ShouldNotBeNil)
|
|
So(resultBlobs, ShouldNotBeNil)
|
|
|
|
// Should add full blob info including SubBlobs
|
|
digestStr := testDigest.String()
|
|
So(resultBlobs.Blobs[digestStr], ShouldNotBeNil)
|
|
So(resultBlobs.Blobs[digestStr].Size, ShouldEqual, 1000)
|
|
So(len(resultBlobs.Blobs[digestStr].SubBlobs), ShouldEqual, 2) // config + layer
|
|
So(resultBlobs.Blobs[configDigest.String()], ShouldNotBeNil)
|
|
So(resultBlobs.Blobs[layerDigest.String()], ShouldNotBeNil)
|
|
})
|
|
})
|
|
}
|