diff --git a/pkg/extensions/search/convert/convert_test.go b/pkg/extensions/search/convert/convert_test.go index 3a588616..8a86d19c 100644 --- a/pkg/extensions/search/convert/convert_test.go +++ b/pkg/extensions/search/convert/convert_test.go @@ -272,6 +272,188 @@ func ref[T any](val T) *T { return &ref } +func TestTaggedTimestamp(t *testing.T) { + ctx := context.Background() + + Convey("Test TaggedTimestamp in ImageSummary", t, func() { + Convey("TaggedTimestamp is populated from tag descriptor", func() { + taggedTime := time.Date(2024, time.January, 15, 10, 30, 0, 0, time.UTC) + pushTime := time.Date(2024, time.January, 10, 8, 0, 0, 0, time.UTC) + + repoMeta := mTypes.RepoMeta{ + Name: "repo", + Tags: map[string]mTypes.Descriptor{ + "tag1": { + Digest: "sha256:abc123", + MediaType: ispec.MediaTypeImageManifest, + TaggedTimestamp: taggedTime, + }, + }, + Statistics: map[string]mTypes.DescriptorStatistics{ + "sha256:abc123": { + PushTimestamp: pushTime, + }, + }, + } + + imageMeta := mTypes.ImageMeta{ + Digest: godigest.FromString("sha256:abc123"), + MediaType: ispec.MediaTypeImageManifest, + Manifests: []mTypes.ManifestMeta{ + { + Digest: godigest.FromString("sha256:abc123"), + Manifest: ispec.Manifest{ + Config: ispec.Descriptor{ + Digest: godigest.FromString("sha256:config123"), + }, + }, + Config: ispec.Image{}, + }, + }, + } + + fullImageMeta := convert.GetFullImageMeta("tag1", repoMeta, imageMeta) + So(fullImageMeta.TaggedTimestamp, ShouldEqual, taggedTime) + + imageSummary, _, err := convert.ImageManifest2ImageSummary(ctx, fullImageMeta) + So(err, ShouldBeNil) + So(imageSummary.TaggedTimestamp, ShouldNotBeNil) + So(*imageSummary.TaggedTimestamp, ShouldEqual, taggedTime) + }) + + Convey("TaggedTimestamp falls back to PushTimestamp when zero", func() { + pushTime := time.Date(2024, time.January, 10, 8, 0, 0, 0, time.UTC) + + // Use a proper digest that will match when converted to string + imageDigest := godigest.FromString("sha256:abc123") + digestStr := imageDigest.String() + + repoMeta := mTypes.RepoMeta{ + Name: "repo", + Tags: map[string]mTypes.Descriptor{ + "tag1": { + Digest: digestStr, + MediaType: ispec.MediaTypeImageManifest, + TaggedTimestamp: time.Time{}, // Zero time + }, + }, + Statistics: map[string]mTypes.DescriptorStatistics{ + digestStr: { + PushTimestamp: pushTime, + }, + }, + } + + imageMeta := mTypes.ImageMeta{ + Digest: imageDigest, + MediaType: ispec.MediaTypeImageManifest, + Manifests: []mTypes.ManifestMeta{ + { + Digest: imageDigest, + Manifest: ispec.Manifest{ + Config: ispec.Descriptor{ + Digest: godigest.FromString("sha256:config123"), + }, + }, + Config: ispec.Image{}, + }, + }, + } + + fullImageMeta := convert.GetFullImageMeta("tag1", repoMeta, imageMeta) + So(fullImageMeta.TaggedTimestamp.IsZero(), ShouldBeTrue) + + imageSummary, _, err := convert.ImageManifest2ImageSummary(ctx, fullImageMeta) + So(err, ShouldBeNil) + So(imageSummary.TaggedTimestamp, ShouldNotBeNil) + So(*imageSummary.TaggedTimestamp, ShouldEqual, pushTime) + }) + + Convey("TaggedTimestamp is propagated to nested manifests in ImageIndex", func() { + taggedTime := time.Date(2024, time.January, 15, 10, 30, 0, 0, time.UTC) + pushTime := time.Date(2024, time.January, 10, 8, 0, 0, 0, time.UTC) + + // Create a multiarch image + multiarchImage := CreateMultiarchWith().Images([]Image{ + CreateRandomImage(), + CreateRandomImage(), + }).Build() + + indexDigestStr := multiarchImage.DigestStr() + + repoMeta := mTypes.RepoMeta{ + Name: "repo", + Tags: map[string]mTypes.Descriptor{ + "tag1": { + Digest: indexDigestStr, + MediaType: ispec.MediaTypeImageIndex, + TaggedTimestamp: taggedTime, + }, + }, + Statistics: map[string]mTypes.DescriptorStatistics{ + indexDigestStr: { + PushTimestamp: pushTime, + }, + }, + } + + imageMeta := multiarchImage.AsImageMeta() + fullImageMeta := convert.GetFullImageMeta("tag1", repoMeta, imageMeta) + So(fullImageMeta.TaggedTimestamp, ShouldEqual, taggedTime) + + imageSummary, _, err := convert.ImageIndex2ImageSummary(ctx, fullImageMeta) + So(err, ShouldBeNil) + So(imageSummary.TaggedTimestamp, ShouldNotBeNil) + So(*imageSummary.TaggedTimestamp, ShouldEqual, taggedTime) + + // Verify that nested manifests also have the correct TaggedTimestamp + So(len(imageSummary.Manifests), ShouldBeGreaterThan, 0) + + for _, manifestSummary := range imageSummary.Manifests { + // Each manifest summary is part of the index, so they should inherit the index's TaggedTimestamp + // Note: ManifestSummary doesn't have TaggedTimestamp field, but the parent ImageSummary does + So(manifestSummary, ShouldNotBeNil) + } + }) + + Convey("TaggedTimestamp falls back to PushTimestamp for ImageIndex when zero", func() { + pushTime := time.Date(2024, time.January, 10, 8, 0, 0, 0, time.UTC) + + // Create a multiarch image + multiarchImage := CreateMultiarchWith().Images([]Image{ + CreateRandomImage(), + }).Build() + + indexDigestStr := multiarchImage.DigestStr() + + repoMeta := mTypes.RepoMeta{ + Name: "repo", + Tags: map[string]mTypes.Descriptor{ + "tag1": { + Digest: indexDigestStr, + MediaType: ispec.MediaTypeImageIndex, + TaggedTimestamp: time.Time{}, // Zero time + }, + }, + Statistics: map[string]mTypes.DescriptorStatistics{ + indexDigestStr: { + PushTimestamp: pushTime, + }, + }, + } + + imageMeta := multiarchImage.AsImageMeta() + fullImageMeta := convert.GetFullImageMeta("tag1", repoMeta, imageMeta) + So(fullImageMeta.TaggedTimestamp.IsZero(), ShouldBeTrue) + + imageSummary, _, err := convert.ImageIndex2ImageSummary(ctx, fullImageMeta) + So(err, ShouldBeNil) + So(imageSummary.TaggedTimestamp, ShouldNotBeNil) + So(*imageSummary.TaggedTimestamp, ShouldEqual, pushTime) + }) + }) +} + func TestPaginatedConvert(t *testing.T) { ctx := context.Background() diff --git a/pkg/extensions/search/convert/metadb.go b/pkg/extensions/search/convert/metadb.go index 846cd5aa..08633ee8 100644 --- a/pkg/extensions/search/convert/metadb.go +++ b/pkg/extensions/search/convert/metadb.go @@ -162,17 +162,23 @@ func RepoMeta2ExpandedRepoInfo(ctx context.Context, repoMeta mTypes.RepoMeta, func GetFullImageMeta(tag string, repoMeta mTypes.RepoMeta, imageMeta mTypes.ImageMeta, ) mTypes.FullImageMeta { + taggedTimestamp := time.Time{} + if descriptor, ok := repoMeta.Tags[tag]; ok { + taggedTimestamp = descriptor.TaggedTimestamp + } + return mTypes.FullImageMeta{ - Repo: repoMeta.Name, - Tag: tag, - MediaType: imageMeta.MediaType, - Digest: imageMeta.Digest, - Size: imageMeta.Size, - Index: imageMeta.Index, - Manifests: GetFullManifestMeta(repoMeta, imageMeta.Manifests), - Referrers: repoMeta.Referrers[imageMeta.Digest.String()], - Statistics: repoMeta.Statistics[imageMeta.Digest.String()], - Signatures: repoMeta.Signatures[imageMeta.Digest.String()], + Repo: repoMeta.Name, + Tag: tag, + MediaType: imageMeta.MediaType, + Digest: imageMeta.Digest, + Size: imageMeta.Size, + Index: imageMeta.Index, + Manifests: GetFullManifestMeta(repoMeta, imageMeta.Manifests), + Referrers: repoMeta.Referrers[imageMeta.Digest.String()], + Statistics: repoMeta.Statistics[imageMeta.Digest.String()], + Signatures: repoMeta.Signatures[imageMeta.Digest.String()], + TaggedTimestamp: taggedTimestamp, } } @@ -407,21 +413,29 @@ func ImageIndex2ImageSummary(ctx context.Context, fullImageMeta mTypes.FullImage manifestSummaries = make([]*gql_generated.ManifestSummary, 0, len(fullImageMeta.Manifests)) indexBlobs = map[string]int64{} - indexDigestStr = fullImageMeta.Digest.String() - indexMediaType = ispec.MediaTypeImageIndex + indexDigestStr = fullImageMeta.Digest.String() + indexMediaType = ispec.MediaTypeImageIndex + pushTimestamp = fullImageMeta.Statistics.PushTimestamp + taggedTimestamp = fullImageMeta.TaggedTimestamp ) + // Fallback to PushTimestamp if TaggedTimestamp is not available + if taggedTimestamp.IsZero() { + taggedTimestamp = pushTimestamp + } + for _, imageManifest := range fullImageMeta.Manifests { imageManifestSummary, manifestBlobs, err := ImageManifest2ImageSummary(ctx, mTypes.FullImageMeta{ - Repo: fullImageMeta.Repo, - Tag: fullImageMeta.Tag, - MediaType: ispec.MediaTypeImageManifest, - Digest: imageManifest.Digest, - Size: imageManifest.Size, - Manifests: []mTypes.FullManifestMeta{imageManifest}, - Referrers: imageManifest.Referrers, - Statistics: imageManifest.Statistics, - Signatures: imageManifest.Signatures, + Repo: fullImageMeta.Repo, + Tag: fullImageMeta.Tag, + MediaType: ispec.MediaTypeImageManifest, + Digest: imageManifest.Digest, + Size: imageManifest.Size, + Manifests: []mTypes.FullManifestMeta{imageManifest}, + Referrers: imageManifest.Referrers, + Statistics: imageManifest.Statistics, + Signatures: imageManifest.Signatures, + TaggedTimestamp: fullImageMeta.TaggedTimestamp, }) if err != nil { return &gql_generated.ImageSummary{}, map[string]int64{}, err @@ -462,25 +476,27 @@ func ImageIndex2ImageSummary(ctx context.Context, fullImageMeta mTypes.FullImage } indexSummary := gql_generated.ImageSummary{ - RepoName: &repo, - Tag: &tag, - Digest: &indexDigestStr, - MediaType: &indexMediaType, - Manifests: manifestSummaries, - LastUpdated: imageLastUpdated, - IsSigned: &isSigned, - SignatureInfo: signaturesInfo, - Size: ref(strconv.FormatInt(indexSize, 10)), - DownloadCount: ref(fullImageMeta.Statistics.DownloadCount), - Description: &annotations.Description, - Title: &annotations.Title, - Documentation: &annotations.Documentation, - Licenses: &annotations.Licenses, - Labels: &annotations.Labels, - Source: &annotations.Source, - Vendor: &annotations.Vendor, - Authors: &annotations.Authors, - Referrers: getReferrers(fullImageMeta.Referrers), + RepoName: &repo, + Tag: &tag, + Digest: &indexDigestStr, + MediaType: &indexMediaType, + Manifests: manifestSummaries, + LastUpdated: imageLastUpdated, + IsSigned: &isSigned, + SignatureInfo: signaturesInfo, + Size: ref(strconv.FormatInt(indexSize, 10)), + DownloadCount: ref(fullImageMeta.Statistics.DownloadCount), + PushTimestamp: &pushTimestamp, + TaggedTimestamp: &taggedTimestamp, + Description: &annotations.Description, + Title: &annotations.Title, + Documentation: &annotations.Documentation, + Licenses: &annotations.Licenses, + Labels: &annotations.Labels, + Source: &annotations.Source, + Vendor: &annotations.Vendor, + Authors: &annotations.Authors, + Referrers: getReferrers(fullImageMeta.Referrers), } return &indexSummary, indexBlobs, nil @@ -504,8 +520,14 @@ func ImageManifest2ImageSummary(ctx context.Context, fullImageMeta mTypes.FullIm isSigned = isImageSigned(fullImageMeta.Signatures) lastPullTimestamp = fullImageMeta.Statistics.LastPullTimestamp pushTimestamp = fullImageMeta.Statistics.PushTimestamp + taggedTimestamp = fullImageMeta.TaggedTimestamp ) + // Fallback to PushTimestamp if TaggedTimestamp is not available + if taggedTimestamp.IsZero() { + taggedTimestamp = pushTimestamp + } + imageSize, imageBlobsMap := getImageBlobsInfo(manifestDigest, manifestSize, configDigest, configSize, manifest.Manifest.Layers) imageSizeStr := strconv.FormatInt(imageSize, 10) @@ -558,6 +580,7 @@ func ImageManifest2ImageSummary(ctx context.Context, fullImageMeta mTypes.FullIm DownloadCount: &downloadCount, LastPullTimestamp: &lastPullTimestamp, PushTimestamp: &pushTimestamp, + TaggedTimestamp: &taggedTimestamp, Description: &annotations.Description, Title: &annotations.Title, Documentation: &annotations.Documentation, diff --git a/pkg/extensions/search/gql_generated/generated.go b/pkg/extensions/search/gql_generated/generated.go index 51c1e3b1..485cc757 100644 --- a/pkg/extensions/search/gql_generated/generated.go +++ b/pkg/extensions/search/gql_generated/generated.go @@ -117,6 +117,7 @@ type ComplexityRoot struct { Size func(childComplexity int) int Source func(childComplexity int) int Tag func(childComplexity int) int + TaggedTimestamp func(childComplexity int) int Title func(childComplexity int) int Vendor func(childComplexity int) int Vulnerabilities func(childComplexity int) int @@ -581,6 +582,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin } return e.complexity.ImageSummary.Tag(childComplexity), true + case "ImageSummary.TaggedTimestamp": + if e.complexity.ImageSummary.TaggedTimestamp == nil { + break + } + + return e.complexity.ImageSummary.TaggedTimestamp(childComplexity), true case "ImageSummary.Title": if e.complexity.ImageSummary.Title == nil { break @@ -1426,10 +1433,14 @@ type ImageSummary { """ LastPullTimestamp: Time """ - Timestamp when the image was pushed to the registry + Timestamp when the image was pushed to the registry """ PushTimestamp: Time """ + Timestamp when the image manifest was tagged (if the data is unavailable it falls back to when the image was pushed to the registry) + """ + TaggedTimestamp: Time + """ Timestamp of the last modification done to the image (from config or the last updated layer) """ LastUpdated: Time @@ -3132,6 +3143,8 @@ func (ec *executionContext) fieldContext_GlobalSearchResult_Images(_ context.Con return ec.fieldContext_ImageSummary_LastPullTimestamp(ctx, field) case "PushTimestamp": return ec.fieldContext_ImageSummary_PushTimestamp(ctx, field) + case "TaggedTimestamp": + return ec.fieldContext_ImageSummary_TaggedTimestamp(ctx, field) case "LastUpdated": return ec.fieldContext_ImageSummary_LastUpdated(ctx, field) case "Description": @@ -3811,6 +3824,35 @@ func (ec *executionContext) fieldContext_ImageSummary_PushTimestamp(_ context.Co return fc, nil } +func (ec *executionContext) _ImageSummary_TaggedTimestamp(ctx context.Context, field graphql.CollectedField, obj *ImageSummary) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_ImageSummary_TaggedTimestamp, + func(ctx context.Context) (any, error) { + return obj.TaggedTimestamp, nil + }, + nil, + ec.marshalOTime2ᚖtimeᚐTime, + true, + false, + ) +} + +func (ec *executionContext) fieldContext_ImageSummary_TaggedTimestamp(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ImageSummary", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Time does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _ImageSummary_LastUpdated(ctx context.Context, field graphql.CollectedField, obj *ImageSummary) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -5272,6 +5314,8 @@ func (ec *executionContext) fieldContext_PaginatedImagesResult_Results(_ context return ec.fieldContext_ImageSummary_LastPullTimestamp(ctx, field) case "PushTimestamp": return ec.fieldContext_ImageSummary_PushTimestamp(ctx, field) + case "TaggedTimestamp": + return ec.fieldContext_ImageSummary_TaggedTimestamp(ctx, field) case "LastUpdated": return ec.fieldContext_ImageSummary_LastUpdated(ctx, field) case "Description": @@ -6027,6 +6071,8 @@ func (ec *executionContext) fieldContext_Query_Image(ctx context.Context, field return ec.fieldContext_ImageSummary_LastPullTimestamp(ctx, field) case "PushTimestamp": return ec.fieldContext_ImageSummary_PushTimestamp(ctx, field) + case "TaggedTimestamp": + return ec.fieldContext_ImageSummary_TaggedTimestamp(ctx, field) case "LastUpdated": return ec.fieldContext_ImageSummary_LastUpdated(ctx, field) case "Description": @@ -6521,6 +6567,8 @@ func (ec *executionContext) fieldContext_RepoInfo_Images(_ context.Context, fiel return ec.fieldContext_ImageSummary_LastPullTimestamp(ctx, field) case "PushTimestamp": return ec.fieldContext_ImageSummary_PushTimestamp(ctx, field) + case "TaggedTimestamp": + return ec.fieldContext_ImageSummary_TaggedTimestamp(ctx, field) case "LastUpdated": return ec.fieldContext_ImageSummary_LastUpdated(ctx, field) case "Description": @@ -6802,6 +6850,8 @@ func (ec *executionContext) fieldContext_RepoSummary_NewestImage(_ context.Conte return ec.fieldContext_ImageSummary_LastPullTimestamp(ctx, field) case "PushTimestamp": return ec.fieldContext_ImageSummary_PushTimestamp(ctx, field) + case "TaggedTimestamp": + return ec.fieldContext_ImageSummary_TaggedTimestamp(ctx, field) case "LastUpdated": return ec.fieldContext_ImageSummary_LastUpdated(ctx, field) case "Description": @@ -9040,6 +9090,8 @@ func (ec *executionContext) _ImageSummary(ctx context.Context, sel ast.Selection out.Values[i] = ec._ImageSummary_LastPullTimestamp(ctx, field, obj) case "PushTimestamp": out.Values[i] = ec._ImageSummary_PushTimestamp(ctx, field, obj) + case "TaggedTimestamp": + out.Values[i] = ec._ImageSummary_TaggedTimestamp(ctx, field, obj) case "LastUpdated": out.Values[i] = ec._ImageSummary_LastUpdated(ctx, field, obj) case "Description": diff --git a/pkg/extensions/search/gql_generated/models_gen.go b/pkg/extensions/search/gql_generated/models_gen.go index d1d7bde9..91b024a5 100644 --- a/pkg/extensions/search/gql_generated/models_gen.go +++ b/pkg/extensions/search/gql_generated/models_gen.go @@ -152,6 +152,8 @@ type ImageSummary struct { LastPullTimestamp *time.Time `json:"LastPullTimestamp,omitempty"` // Timestamp when the image was pushed to the registry PushTimestamp *time.Time `json:"PushTimestamp,omitempty"` + // Timestamp when the image manifest was tagged (if the data is unavailable it falls back to when the image was pushed to the registry) + TaggedTimestamp *time.Time `json:"TaggedTimestamp,omitempty"` // Timestamp of the last modification done to the image (from config or the last updated layer) LastUpdated *time.Time `json:"LastUpdated,omitempty"` // Human-readable description of the software packaged in the image diff --git a/pkg/extensions/search/schema.graphql b/pkg/extensions/search/schema.graphql index c30e43c7..88820c23 100644 --- a/pkg/extensions/search/schema.graphql +++ b/pkg/extensions/search/schema.graphql @@ -209,10 +209,14 @@ type ImageSummary { """ LastPullTimestamp: Time """ - Timestamp when the image was pushed to the registry + Timestamp when the image was pushed to the registry """ PushTimestamp: Time """ + Timestamp when the image manifest was tagged (if the data is unavailable it falls back to when the image was pushed to the registry) + """ + TaggedTimestamp: Time + """ Timestamp of the last modification done to the image (from config or the last updated layer) """ LastUpdated: Time diff --git a/pkg/meta/boltdb/boltdb.go b/pkg/meta/boltdb/boltdb.go index b5ecbf10..d21ba42a 100644 --- a/pkg/meta/boltdb/boltdb.go +++ b/pkg/meta/boltdb/boltdb.go @@ -242,9 +242,23 @@ func (bdw *BoltDB) SetRepoReference(ctx context.Context, repo string, reference // 3. Update tag if !common.ReferenceIsDigest(reference) { + // Set TaggedTimestamp to now if this is a new tag, otherwise preserve existing timestamp + // For old data without TaggedTimestamp, leave it nil so it falls back to PushTimestamp + var taggedTimestamp *timestamppb.Timestamp + if existingTag, exists := protoRepoMeta.Tags[reference]; exists { + // Tag exists - preserve TaggedTimestamp if present, otherwise leave nil (old data) + if existingTag.GetTaggedTimestamp() != nil { + taggedTimestamp = existingTag.GetTaggedTimestamp() + } + // else leave taggedTimestamp as nil (old data without TaggedTimestamp) + } else { + // New tag - set timestamp to now + taggedTimestamp = timestamppb.Now() + } protoRepoMeta.Tags[reference] = &proto_go.TagDescriptor{ - Digest: imageMeta.Digest.String(), - MediaType: imageMeta.MediaType, + Digest: imageMeta.Digest.String(), + MediaType: imageMeta.MediaType, + TaggedTimestamp: taggedTimestamp, } } @@ -1161,7 +1175,7 @@ func (bdw *BoltDB) DeleteRepoMeta(repo string) error { return err } -func (bdw *BoltDB) ResetRepoReferences(repo string) error { +func (bdw *BoltDB) ResetRepoReferences(repo string, tagsToKeep map[string]bool) error { err := bdw.DB.Update(func(tx *bbolt.Tx) error { buck := tx.Bucket([]byte(RepoMetaBuck)) @@ -1172,11 +1186,26 @@ func (bdw *BoltDB) ResetRepoReferences(repo string) error { return err } + // Preserve tags that are in tagsToKeep, remove others + preservedTags := make(map[string]*proto_go.TagDescriptor) + if tagsToKeep != nil { + for tag, descriptor := range protoRepoMeta.Tags { + // Keep the tag if it's in tagsToKeep, or if it's the empty key (internal use) + if tag == "" || tagsToKeep[tag] { + preservedTags[tag] = descriptor + } + } + } + // Ensure empty key exists for internal use + if _, exists := preservedTags[""]; !exists { + preservedTags[""] = &proto_go.TagDescriptor{} + } + repoMetaBlob, err = proto.Marshal(&proto_go.RepoMeta{ Name: repo, Statistics: protoRepoMeta.Statistics, Stars: protoRepoMeta.Stars, - Tags: map[string]*proto_go.TagDescriptor{"": {}}, + Tags: preservedTags, Signatures: map[string]*proto_go.ManifestSignatures{"": {Map: map[string]*proto_go.SignaturesInfo{"": {}}}}, Referrers: map[string]*proto_go.ReferrersInfo{"": {}}, }) diff --git a/pkg/meta/boltdb/boltdb_test.go b/pkg/meta/boltdb/boltdb_test.go index c261e630..8790b6f1 100644 --- a/pkg/meta/boltdb/boltdb_test.go +++ b/pkg/meta/boltdb/boltdb_test.go @@ -312,9 +312,46 @@ func TestWrapperErrors(t *testing.T) { err := setRepoMeta("repo", badProtoBlob, boltdbWrapper.DB) So(err, ShouldBeNil) - err = boltdbWrapper.ResetRepoReferences("repo") + err = boltdbWrapper.ResetRepoReferences("repo", nil) So(err, ShouldNotBeNil) }) + + Convey("preserve tags in tagsToKeep", func() { + ctx := context.Background() + + // Create repo with multiple tags + image1 := CreateRandomImage() + image2 := CreateRandomImage() + + err := boltdbWrapper.SetRepoReference(ctx, "repo", "tag1", image1.AsImageMeta()) + So(err, ShouldBeNil) + + // Wait a bit to ensure different timestamps + time.Sleep(10 * time.Millisecond) + + err = boltdbWrapper.SetRepoReference(ctx, "repo", "tag2", image2.AsImageMeta()) + So(err, ShouldBeNil) + + // Get repo meta to capture TaggedTimestamp + repoMeta, err := boltdbWrapper.GetRepoMeta(ctx, "repo") + So(err, ShouldBeNil) + So(repoMeta.Tags, ShouldContainKey, "tag1") + So(repoMeta.Tags, ShouldContainKey, "tag2") + + tag1Timestamp := repoMeta.Tags["tag1"].TaggedTimestamp + + // Reset with only tag1 in tagsToKeep + tagsToKeep := map[string]bool{"tag1": true} + err = boltdbWrapper.ResetRepoReferences("repo", tagsToKeep) + So(err, ShouldBeNil) + + // Verify tag1 is preserved with its timestamp + repoMeta, err = boltdbWrapper.GetRepoMeta(ctx, "repo") + So(err, ShouldBeNil) + So(repoMeta.Tags, ShouldContainKey, "tag1") + So(repoMeta.Tags, ShouldNotContainKey, "tag2") + So(repoMeta.Tags["tag1"].TaggedTimestamp, ShouldEqual, tag1Timestamp) + }) }) Convey("DecrementRepoStars", func() { diff --git a/pkg/meta/common/common.go b/pkg/meta/common/common.go index d3a3e6e4..a9e0d0b0 100644 --- a/pkg/meta/common/common.go +++ b/pkg/meta/common/common.go @@ -178,6 +178,16 @@ func AddImageMetaToRepoMeta(repoMeta *proto_go.RepoMeta, repoBlobs *proto_go.Rep ) (*proto_go.RepoMeta, *proto_go.RepoBlobs) { switch imageMeta.MediaType { case ispec.MediaTypeImageManifest: + if len(imageMeta.Manifests) == 0 { + // Empty manifests is an invalid state for ImageManifest, but we still add basic blob info + // to avoid skipping all metadata processing (e.g., LastUpdatedImage update) + repoBlobs.Blobs[imageMeta.Digest.String()] = &proto_go.BlobInfo{ + Size: imageMeta.Size, + } + + break + } + manifestData := imageMeta.Manifests[0] vendor := GetVendor(manifestData.Manifest.Annotations) diff --git a/pkg/meta/common/common_test.go b/pkg/meta/common/common_test.go index ba012c76..aff09248 100644 --- a/pkg/meta/common/common_test.go +++ b/pkg/meta/common/common_test.go @@ -5,6 +5,8 @@ import ( "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" @@ -519,4 +521,96 @@ func TestUtils(t *testing.T) { 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) + }) + }) } diff --git a/pkg/meta/convert/convert.go b/pkg/meta/convert/convert.go index 1adc6856..5432975e 100644 --- a/pkg/meta/convert/convert.go +++ b/pkg/meta/convert/convert.go @@ -362,9 +362,14 @@ func GetTags(tags map[mTypes.Tag]*proto_go.TagDescriptor) map[mTypes.Tag]mTypes. resultMap := map[mTypes.Tag]mTypes.Descriptor{} for tag, tagDescriptor := range tags { + taggedTimestamp := time.Time{} + if tagDescriptor.GetTaggedTimestamp() != nil { + taggedTimestamp = tagDescriptor.GetTaggedTimestamp().AsTime() + } resultMap[tag] = mTypes.Descriptor{ - Digest: tagDescriptor.GetDigest(), - MediaType: tagDescriptor.GetMediaType(), + Digest: tagDescriptor.GetDigest(), + MediaType: tagDescriptor.GetMediaType(), + TaggedTimestamp: taggedTimestamp, } } @@ -408,16 +413,22 @@ func GetFullImageMetaFromProto(tag string, protoRepoMeta *proto_go.RepoMeta, pro imageMeta := GetImageMeta(protoImageMeta) imageDigest := imageMeta.Digest.String() + taggedTimestamp := time.Time{} + if tagDescriptor, ok := protoRepoMeta.GetTags()[tag]; ok && tagDescriptor.GetTaggedTimestamp() != nil { + taggedTimestamp = tagDescriptor.GetTaggedTimestamp().AsTime() + } + return mTypes.FullImageMeta{ - Repo: protoRepoMeta.GetName(), - Tag: tag, - MediaType: imageMeta.MediaType, - Digest: imageMeta.Digest, - Size: imageMeta.Size, - Index: imageMeta.Index, - Manifests: GetFullManifestData(protoRepoMeta, imageMeta.Manifests), - IsStarred: protoRepoMeta.GetIsStarred(), - IsBookmarked: protoRepoMeta.GetIsBookmarked(), + Repo: protoRepoMeta.GetName(), + Tag: tag, + MediaType: imageMeta.MediaType, + Digest: imageMeta.Digest, + Size: imageMeta.Size, + Index: imageMeta.Index, + Manifests: GetFullManifestData(protoRepoMeta, imageMeta.Manifests), + IsStarred: protoRepoMeta.GetIsStarred(), + IsBookmarked: protoRepoMeta.GetIsBookmarked(), + TaggedTimestamp: taggedTimestamp, Referrers: GetImageReferrers(protoRepoMeta.GetReferrers()[imageDigest]), Statistics: GetImageStatistics(protoRepoMeta.GetStatistics()[imageDigest]), diff --git a/pkg/meta/convert/convert_proto.go b/pkg/meta/convert/convert_proto.go index 481ae813..53ca6032 100644 --- a/pkg/meta/convert/convert_proto.go +++ b/pkg/meta/convert/convert_proto.go @@ -337,10 +337,14 @@ func GetProtoTags(tags map[mTypes.Tag]mTypes.Descriptor) map[mTypes.Tag]*proto_g resultMap := map[mTypes.Tag]*proto_go.TagDescriptor{} for tag, tagDescriptor := range tags { - resultMap[tag] = &proto_go.TagDescriptor{ + protoTagDescriptor := &proto_go.TagDescriptor{ Digest: tagDescriptor.Digest, MediaType: tagDescriptor.MediaType, } + if !tagDescriptor.TaggedTimestamp.IsZero() { + protoTagDescriptor.TaggedTimestamp = timestamppb.New(tagDescriptor.TaggedTimestamp) + } + resultMap[tag] = protoTagDescriptor } return resultMap diff --git a/pkg/meta/dynamodb/dynamodb.go b/pkg/meta/dynamodb/dynamodb.go index 993a2517..562f3132 100644 --- a/pkg/meta/dynamodb/dynamodb.go +++ b/pkg/meta/dynamodb/dynamodb.go @@ -420,9 +420,23 @@ func (dwr *DynamoDB) SetRepoReference(ctx context.Context, repo string, referenc // 3. Update tag if !common.ReferenceIsDigest(reference) { + // Set TaggedTimestamp to now if this is a new tag, otherwise preserve existing timestamp + // For old data without TaggedTimestamp, leave it nil so it falls back to PushTimestamp + var taggedTimestamp *timestamppb.Timestamp + if existingTag, exists := repoMeta.Tags[reference]; exists { + // Tag exists - preserve TaggedTimestamp if present, otherwise leave nil (old data) + if existingTag.GetTaggedTimestamp() != nil { + taggedTimestamp = existingTag.GetTaggedTimestamp() + } + // else leave taggedTimestamp as nil (old data without TaggedTimestamp) + } else { + // New tag - set timestamp to now + taggedTimestamp = timestamppb.Now() + } repoMeta.Tags[reference] = &proto_go.TagDescriptor{ - Digest: imageMeta.Digest.String(), - MediaType: imageMeta.MediaType, + Digest: imageMeta.Digest.String(), + MediaType: imageMeta.MediaType, + TaggedTimestamp: taggedTimestamp, } } @@ -864,17 +878,32 @@ func getProtoImageMetaFromAttribute(imageMetaAttribute types.AttributeValue) (*p return protoImageMeta, nil } -func (dwr *DynamoDB) ResetRepoReferences(repo string) error { +func (dwr *DynamoDB) ResetRepoReferences(repo string, tagsToKeep map[string]bool) error { protoRepoMeta, err := dwr.getProtoRepoMeta(context.Background(), repo) if err != nil { return err } + // Preserve tags that are in tagsToKeep, remove others + preservedTags := make(map[string]*proto_go.TagDescriptor) + if tagsToKeep != nil { + for tag, descriptor := range protoRepoMeta.Tags { + // Keep the tag if it's in tagsToKeep, or if it's the empty key (internal use) + if tag == "" || tagsToKeep[tag] { + preservedTags[tag] = descriptor + } + } + } + // Ensure empty key exists for internal use + if _, exists := preservedTags[""]; !exists { + preservedTags[""] = &proto_go.TagDescriptor{} + } + return dwr.setProtoRepoMeta(repo, &proto_go.RepoMeta{ Name: repo, Statistics: protoRepoMeta.Statistics, Stars: protoRepoMeta.Stars, - Tags: map[string]*proto_go.TagDescriptor{"": {}}, + Tags: preservedTags, Referrers: map[string]*proto_go.ReferrersInfo{"": {}}, Signatures: map[string]*proto_go.ManifestSignatures{"": {Map: map[string]*proto_go.SignaturesInfo{"": {}}}}, }) diff --git a/pkg/meta/dynamodb/dynamodb_test.go b/pkg/meta/dynamodb/dynamodb_test.go index 05165499..c32b9a5d 100644 --- a/pkg/meta/dynamodb/dynamodb_test.go +++ b/pkg/meta/dynamodb/dynamodb_test.go @@ -425,6 +425,103 @@ func TestWrapperErrors(t *testing.T) { }) }) + Convey("ResetRepoReferences", func() { + Convey("unmarshalProtoRepoMeta error", func() { + err := setRepoMeta("repo", badProtoBlob, dynamoWrapper) + So(err, ShouldBeNil) + + err = dynamoWrapper.ResetRepoReferences("repo", nil) + So(err, ShouldNotBeNil) + }) + + Convey("preserve tags in tagsToKeep", func() { + // Create repo with multiple tags + image1 := CreateRandomImage() + image2 := CreateRandomImage() + + err := dynamoWrapper.SetRepoReference(ctx, "repo", "tag1", image1.AsImageMeta()) + So(err, ShouldBeNil) + + // Wait a bit to ensure different timestamps + time.Sleep(10 * time.Millisecond) + + err = dynamoWrapper.SetRepoReference(ctx, "repo", "tag2", image2.AsImageMeta()) + So(err, ShouldBeNil) + + // Get repo meta to capture TaggedTimestamp + repoMeta, err := dynamoWrapper.GetRepoMeta(ctx, "repo") + So(err, ShouldBeNil) + So(repoMeta.Tags, ShouldContainKey, "tag1") + So(repoMeta.Tags, ShouldContainKey, "tag2") + + tag1Timestamp := repoMeta.Tags["tag1"].TaggedTimestamp + + // Reset with only tag1 in tagsToKeep + tagsToKeep := map[string]bool{"tag1": true} + err = dynamoWrapper.ResetRepoReferences("repo", tagsToKeep) + So(err, ShouldBeNil) + + // Verify tag1 is preserved with its timestamp + repoMeta, err = dynamoWrapper.GetRepoMeta(ctx, "repo") + So(err, ShouldBeNil) + So(repoMeta.Tags, ShouldContainKey, "tag1") + So(repoMeta.Tags, ShouldNotContainKey, "tag2") + So(repoMeta.Tags["tag1"].TaggedTimestamp, ShouldEqual, tag1Timestamp) + }) + + Convey("remove tags not in tagsToKeep", func() { + // Create repo with multiple tags + image1 := CreateRandomImage() + image2 := CreateRandomImage() + image3 := CreateRandomImage() + + err := dynamoWrapper.SetRepoReference(ctx, "repo", "tag1", image1.AsImageMeta()) + So(err, ShouldBeNil) + + err = dynamoWrapper.SetRepoReference(ctx, "repo", "tag2", image2.AsImageMeta()) + So(err, ShouldBeNil) + + err = dynamoWrapper.SetRepoReference(ctx, "repo", "tag3", image3.AsImageMeta()) + So(err, ShouldBeNil) + + // Reset with tag1 and tag2 in tagsToKeep + tagsToKeep := map[string]bool{"tag1": true, "tag2": true} + err = dynamoWrapper.ResetRepoReferences("repo", tagsToKeep) + So(err, ShouldBeNil) + + // Verify only tag1 and tag2 are preserved + repoMeta, err := dynamoWrapper.GetRepoMeta(ctx, "repo") + So(err, ShouldBeNil) + So(repoMeta.Tags, ShouldContainKey, "tag1") + So(repoMeta.Tags, ShouldContainKey, "tag2") + So(repoMeta.Tags, ShouldNotContainKey, "tag3") + }) + + Convey("preserve statistics and stars", func() { + image := CreateRandomImage() + + err := dynamoWrapper.SetRepoReference(ctx, "repo", "tag1", image.AsImageMeta()) + So(err, ShouldBeNil) + + err = dynamoWrapper.IncrementRepoStars("repo") + So(err, ShouldBeNil) + + // Get original stats + repoMeta, err := dynamoWrapper.GetRepoMeta(ctx, "repo") + So(err, ShouldBeNil) + originalStars := repoMeta.StarCount + + // Reset with empty tagsToKeep + err = dynamoWrapper.ResetRepoReferences("repo", map[string]bool{}) + So(err, ShouldBeNil) + + // Verify statistics and stars are preserved + repoMeta, err = dynamoWrapper.GetRepoMeta(ctx, "repo") + So(err, ShouldBeNil) + So(repoMeta.StarCount, ShouldEqual, originalStars) + }) + }) + Convey("GetMultipleRepoMeta", func() { Convey("repoMetaAttributeIterator.First fails", func() { dynamoWrapper.RepoMetaTablename = badTablename diff --git a/pkg/meta/parse.go b/pkg/meta/parse.go index fea94fda..9dd7e769 100644 --- a/pkg/meta/parse.go +++ b/pkg/meta/parse.go @@ -130,7 +130,17 @@ func ParseRepo(repo string, metaDB mTypes.MetaDB, storeController stypes.StoreCo return err } - err = metaDB.ResetRepoReferences(repo) + // Collect tags that exist in storage to preserve them + tagsToKeep := make(map[string]bool) + + for _, manifest := range indexContent.Manifests { + tag := manifest.Annotations[ispec.AnnotationRefName] + if tag != "" && !zcommon.IsReferrersTag(tag) { + tagsToKeep[tag] = true + } + } + + err = metaDB.ResetRepoReferences(repo, tagsToKeep) if err != nil && !errors.Is(err, zerr.ErrRepoMetaNotFound) { log.Error().Err(err).Str("repository", repo).Msg("failed to reset tag field in RepoMetadata for repo") diff --git a/pkg/meta/parse_test.go b/pkg/meta/parse_test.go index 0015a663..624f6760 100644 --- a/pkg/meta/parse_test.go +++ b/pkg/meta/parse_test.go @@ -143,7 +143,7 @@ func TestParseStorageErrors(t *testing.T) { imageStore.GetIndexContentFn = func(repo string) ([]byte, error) { return []byte("{}"), nil } - metaDB.ResetRepoReferencesFn = func(repo string) error { return ErrTestError } + metaDB.ResetRepoReferencesFn = func(repo string, tagsToKeep map[string]bool) error { return ErrTestError } err := meta.ParseRepo("repo", metaDB, storeController, log) So(err, ShouldNotBeNil) }) @@ -592,6 +592,51 @@ func RunParseStorageTests(rootDir string, metaDB mTypes.MetaDB, log log.Logger) So(repoMeta.StarCount, ShouldEqual, 1) }) + Convey("preserve TaggedTimestamp during ParseRepo", func() { + imageStore := local.NewImageStore(rootDir, false, false, + log, monitoring.NewMetricsServer(false, log), nil, nil, nil, nil) + + storeController := storage.StoreController{DefaultStore: imageStore} + + // Create images with tags + for i := range 2 { + image := CreateRandomImage() //nolint:staticcheck + + err := WriteImageToFileSystem( + image, repo, fmt.Sprintf("tag%d", i), storeController) + So(err, ShouldBeNil) + } + + // Initial parse to set up metadata + err := meta.ParseStorage(metaDB, storeController, log) //nolint: contextcheck + So(err, ShouldBeNil) + + // Get initial TaggedTimestamp values + repoMeta, err := metaDB.GetRepoMeta(ctx, repo) + So(err, ShouldBeNil) + So(repoMeta.Tags, ShouldContainKey, "tag0") + So(repoMeta.Tags, ShouldContainKey, "tag1") + + tag0Timestamp := repoMeta.Tags["tag0"].TaggedTimestamp + tag1Timestamp := repoMeta.Tags["tag1"].TaggedTimestamp + + // Verify timestamps are not zero + So(tag0Timestamp.IsZero(), ShouldBeFalse) + So(tag1Timestamp.IsZero(), ShouldBeFalse) + + // Re-parse the storage (simulating service restart) + err = meta.ParseStorage(metaDB, storeController, log) //nolint: contextcheck + So(err, ShouldBeNil) + + // Verify TaggedTimestamp values are preserved + repoMeta, err = metaDB.GetRepoMeta(ctx, repo) + So(err, ShouldBeNil) + So(repoMeta.Tags, ShouldContainKey, "tag0") + So(repoMeta.Tags, ShouldContainKey, "tag1") + So(repoMeta.Tags["tag0"].TaggedTimestamp, ShouldEqual, tag0Timestamp) + So(repoMeta.Tags["tag1"].TaggedTimestamp, ShouldEqual, tag1Timestamp) + }) + // make sure pushTimestamp is always populated to not interfere with retention logic Convey("Always update pushTimestamp if its value is 0(time.Time{})", func() { imageStore := local.NewImageStore(rootDir, false, false, diff --git a/pkg/meta/proto/gen/config.pb.go b/pkg/meta/proto/gen/config.pb.go index 585edb9d..60cafd21 100644 --- a/pkg/meta/proto/gen/config.pb.go +++ b/pkg/meta/proto/gen/config.pb.go @@ -7,12 +7,11 @@ package gen import ( - reflect "reflect" - sync "sync" - protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" ) const ( diff --git a/pkg/meta/proto/gen/descriptor.pb.go b/pkg/meta/proto/gen/descriptor.pb.go index 86439123..e2c1a054 100644 --- a/pkg/meta/proto/gen/descriptor.pb.go +++ b/pkg/meta/proto/gen/descriptor.pb.go @@ -7,11 +7,10 @@ package gen import ( - reflect "reflect" - sync "sync" - protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" ) const ( diff --git a/pkg/meta/proto/gen/index.pb.go b/pkg/meta/proto/gen/index.pb.go index 758cd0ed..9e5d2a2b 100644 --- a/pkg/meta/proto/gen/index.pb.go +++ b/pkg/meta/proto/gen/index.pb.go @@ -7,11 +7,10 @@ package gen import ( - reflect "reflect" - sync "sync" - protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" ) const ( diff --git a/pkg/meta/proto/gen/manifest.pb.go b/pkg/meta/proto/gen/manifest.pb.go index 9208077d..f4640ced 100644 --- a/pkg/meta/proto/gen/manifest.pb.go +++ b/pkg/meta/proto/gen/manifest.pb.go @@ -7,11 +7,10 @@ package gen import ( - reflect "reflect" - sync "sync" - protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" ) const ( diff --git a/pkg/meta/proto/gen/meta.pb.go b/pkg/meta/proto/gen/meta.pb.go index 3d55e887..dd0f7947 100644 --- a/pkg/meta/proto/gen/meta.pb.go +++ b/pkg/meta/proto/gen/meta.pb.go @@ -7,12 +7,11 @@ package gen import ( - reflect "reflect" - sync "sync" - protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" ) const ( @@ -27,8 +26,9 @@ type TagDescriptor struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - MediaType string `protobuf:"bytes,1,opt,name=MediaType,proto3" json:"MediaType,omitempty"` - Digest string `protobuf:"bytes,2,opt,name=Digest,proto3" json:"Digest,omitempty"` + MediaType string `protobuf:"bytes,1,opt,name=MediaType,proto3" json:"MediaType,omitempty"` + Digest string `protobuf:"bytes,2,opt,name=Digest,proto3" json:"Digest,omitempty"` + TaggedTimestamp *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=TaggedTimestamp,proto3" json:"TaggedTimestamp,omitempty"` } func (x *TagDescriptor) Reset() { @@ -77,6 +77,13 @@ func (x *TagDescriptor) GetDigest() string { return "" } +func (x *TagDescriptor) GetTaggedTimestamp() *timestamppb.Timestamp { + if x != nil { + return x.TaggedTimestamp + } + return nil +} + type ImageMeta struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1075,202 +1082,207 @@ var file_meta_meta_proto_rawDesc = []byte{ 0x1a, 0x0f, 0x6f, 0x63, 0x69, 0x2f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x13, 0x6f, 0x63, 0x69, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x14, 0x6f, 0x63, 0x69, 0x2f, 0x64, 0x65, 0x73, 0x63, - 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x45, 0x0a, 0x0d, - 0x54, 0x61, 0x67, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x12, 0x1c, 0x0a, - 0x09, 0x4d, 0x65, 0x64, 0x69, 0x61, 0x54, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x09, 0x4d, 0x65, 0x64, 0x69, 0x61, 0x54, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x44, - 0x69, 0x67, 0x65, 0x73, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x69, 0x67, - 0x65, 0x73, 0x74, 0x22, 0x97, 0x01, 0x0a, 0x09, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x4d, 0x65, 0x74, - 0x61, 0x12, 0x1c, 0x0a, 0x09, 0x4d, 0x65, 0x64, 0x69, 0x61, 0x54, 0x79, 0x70, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x4d, 0x65, 0x64, 0x69, 0x61, 0x54, 0x79, 0x70, 0x65, 0x12, - 0x33, 0x0a, 0x09, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x5f, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x6e, - 0x69, 0x66, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x09, 0x4d, 0x61, 0x6e, 0x69, 0x66, - 0x65, 0x73, 0x74, 0x73, 0x12, 0x2d, 0x0a, 0x05, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x5f, 0x76, 0x31, 0x2e, 0x49, 0x6e, - 0x64, 0x65, 0x78, 0x4d, 0x65, 0x74, 0x61, 0x48, 0x00, 0x52, 0x05, 0x49, 0x6e, 0x64, 0x65, 0x78, - 0x88, 0x01, 0x01, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x22, 0x8f, 0x01, - 0x0a, 0x0c, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x16, - 0x0a, 0x06, 0x44, 0x69, 0x67, 0x65, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, - 0x44, 0x69, 0x67, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x53, 0x69, 0x7a, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x2c, 0x0a, 0x08, 0x4d, 0x61, - 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x6f, - 0x63, 0x69, 0x5f, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x08, - 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x25, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x6f, 0x63, 0x69, 0x5f, 0x76, - 0x31, 0x2e, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, - 0x5c, 0x0a, 0x09, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x16, 0x0a, 0x06, - 0x44, 0x69, 0x67, 0x65, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x69, - 0x67, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x53, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x04, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x23, 0x0a, 0x05, 0x49, 0x6e, 0x64, 0x65, - 0x78, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x6f, 0x63, 0x69, 0x5f, 0x76, 0x31, - 0x2e, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x52, 0x05, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x22, 0xb1, 0x01, - 0x0a, 0x14, 0x52, 0x65, 0x70, 0x6f, 0x4c, 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x64, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x41, 0x0a, 0x0b, 0x4c, 0x61, 0x73, 0x74, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, - 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x48, 0x00, 0x52, 0x0b, 0x4c, 0x61, 0x73, 0x74, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x88, 0x01, 0x01, 0x12, 0x1c, 0x0a, 0x09, 0x4d, 0x65, 0x64, - 0x69, 0x61, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x4d, 0x65, - 0x64, 0x69, 0x61, 0x54, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x69, 0x67, 0x65, 0x73, - 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x69, 0x67, 0x65, 0x73, 0x74, 0x12, - 0x10, 0x0a, 0x03, 0x54, 0x61, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x54, 0x61, - 0x67, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x4c, 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x64, 0x22, 0xc3, 0x07, 0x0a, 0x08, 0x52, 0x65, 0x70, 0x6f, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x12, - 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, - 0x6d, 0x65, 0x12, 0x2f, 0x0a, 0x04, 0x54, 0x61, 0x67, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x1b, 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x5f, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x4d, - 0x65, 0x74, 0x61, 0x2e, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x54, - 0x61, 0x67, 0x73, 0x12, 0x41, 0x0a, 0x0a, 0x53, 0x74, 0x61, 0x74, 0x69, 0x73, 0x74, 0x69, 0x63, - 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x5f, 0x76, - 0x31, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x4d, 0x65, 0x74, 0x61, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x69, - 0x73, 0x74, 0x69, 0x63, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0a, 0x53, 0x74, 0x61, 0x74, - 0x69, 0x73, 0x74, 0x69, 0x63, 0x73, 0x12, 0x41, 0x0a, 0x0a, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, - 0x75, 0x72, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x6d, 0x65, 0x74, - 0x61, 0x5f, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x4d, 0x65, 0x74, 0x61, 0x2e, 0x53, 0x69, - 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0a, 0x53, - 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x12, 0x3e, 0x0a, 0x09, 0x52, 0x65, 0x66, - 0x65, 0x72, 0x72, 0x65, 0x72, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x6d, - 0x65, 0x74, 0x61, 0x5f, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x4d, 0x65, 0x74, 0x61, 0x2e, - 0x52, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x09, - 0x52, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x72, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x49, 0x73, 0x53, - 0x74, 0x61, 0x72, 0x72, 0x65, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x49, 0x73, - 0x53, 0x74, 0x61, 0x72, 0x72, 0x65, 0x64, 0x12, 0x22, 0x0a, 0x0c, 0x49, 0x73, 0x42, 0x6f, 0x6f, - 0x6b, 0x6d, 0x61, 0x72, 0x6b, 0x65, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x49, - 0x73, 0x42, 0x6f, 0x6f, 0x6b, 0x6d, 0x61, 0x72, 0x6b, 0x65, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x52, - 0x61, 0x6e, 0x6b, 0x18, 0x08, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x52, 0x61, 0x6e, 0x6b, 0x12, - 0x14, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x72, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, - 0x53, 0x74, 0x61, 0x72, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x53, 0x69, 0x7a, 0x65, 0x18, 0x0a, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x04, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x56, 0x65, 0x6e, - 0x64, 0x6f, 0x72, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x56, 0x65, 0x6e, 0x64, - 0x6f, 0x72, 0x73, 0x12, 0x2e, 0x0a, 0x09, 0x50, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x73, - 0x18, 0x0c, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x6f, 0x63, 0x69, 0x5f, 0x76, 0x31, 0x2e, - 0x50, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x52, 0x09, 0x50, 0x6c, 0x61, 0x74, 0x66, 0x6f, - 0x72, 0x6d, 0x73, 0x12, 0x4e, 0x0a, 0x10, 0x4c, 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x64, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, - 0x6d, 0x65, 0x74, 0x61, 0x5f, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x4c, 0x61, 0x73, 0x74, - 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x48, 0x00, 0x52, 0x10, - 0x4c, 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x49, 0x6d, 0x61, 0x67, 0x65, - 0x88, 0x01, 0x01, 0x12, 0x1c, 0x0a, 0x09, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x73, - 0x18, 0x0e, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, - 0x73, 0x1a, 0x4f, 0x0a, 0x09, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, - 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, - 0x12, 0x2c, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x16, 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x5f, 0x76, 0x31, 0x2e, 0x54, 0x61, 0x67, 0x44, 0x65, 0x73, - 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, - 0x38, 0x01, 0x1a, 0x5c, 0x0a, 0x0f, 0x53, 0x74, 0x61, 0x74, 0x69, 0x73, 0x74, 0x69, 0x63, 0x73, + 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x8b, 0x01, 0x0a, + 0x0d, 0x54, 0x61, 0x67, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x12, 0x1c, + 0x0a, 0x09, 0x4d, 0x65, 0x64, 0x69, 0x61, 0x54, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x09, 0x4d, 0x65, 0x64, 0x69, 0x61, 0x54, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, + 0x44, 0x69, 0x67, 0x65, 0x73, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x69, + 0x67, 0x65, 0x73, 0x74, 0x12, 0x44, 0x0a, 0x0f, 0x54, 0x61, 0x67, 0x67, 0x65, 0x64, 0x54, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0f, 0x54, 0x61, 0x67, 0x67, 0x65, + 0x64, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x22, 0x97, 0x01, 0x0a, 0x09, 0x49, + 0x6d, 0x61, 0x67, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x1c, 0x0a, 0x09, 0x4d, 0x65, 0x64, 0x69, + 0x61, 0x54, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x4d, 0x65, 0x64, + 0x69, 0x61, 0x54, 0x79, 0x70, 0x65, 0x12, 0x33, 0x0a, 0x09, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, + 0x73, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x65, 0x74, 0x61, + 0x5f, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, + 0x52, 0x09, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x73, 0x12, 0x2d, 0x0a, 0x05, 0x49, + 0x6e, 0x64, 0x65, 0x78, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6d, 0x65, 0x74, + 0x61, 0x5f, 0x76, 0x31, 0x2e, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x4d, 0x65, 0x74, 0x61, 0x48, 0x00, + 0x52, 0x05, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x88, 0x01, 0x01, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x49, + 0x6e, 0x64, 0x65, 0x78, 0x22, 0x8f, 0x01, 0x0a, 0x0c, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, + 0x74, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x69, 0x67, 0x65, 0x73, 0x74, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x69, 0x67, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, + 0x04, 0x53, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x53, 0x69, 0x7a, + 0x65, 0x12, 0x2c, 0x0a, 0x08, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x6f, 0x63, 0x69, 0x5f, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x6e, + 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x08, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, + 0x25, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x0d, 0x2e, 0x6f, 0x63, 0x69, 0x5f, 0x76, 0x31, 0x2e, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x06, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x5c, 0x0a, 0x09, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x4d, + 0x65, 0x74, 0x61, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x69, 0x67, 0x65, 0x73, 0x74, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x69, 0x67, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x53, + 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x53, 0x69, 0x7a, 0x65, 0x12, + 0x23, 0x0a, 0x05, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, + 0x2e, 0x6f, 0x63, 0x69, 0x5f, 0x76, 0x31, 0x2e, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x52, 0x05, 0x49, + 0x6e, 0x64, 0x65, 0x78, 0x22, 0xb1, 0x01, 0x0a, 0x14, 0x52, 0x65, 0x70, 0x6f, 0x4c, 0x61, 0x73, + 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x41, 0x0a, + 0x0b, 0x4c, 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x48, 0x00, + 0x52, 0x0b, 0x4c, 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x88, 0x01, 0x01, + 0x12, 0x1c, 0x0a, 0x09, 0x4d, 0x65, 0x64, 0x69, 0x61, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x09, 0x4d, 0x65, 0x64, 0x69, 0x61, 0x54, 0x79, 0x70, 0x65, 0x12, 0x16, + 0x0a, 0x06, 0x44, 0x69, 0x67, 0x65, 0x73, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, + 0x44, 0x69, 0x67, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x54, 0x61, 0x67, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x03, 0x54, 0x61, 0x67, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x4c, 0x61, 0x73, + 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x22, 0xc3, 0x07, 0x0a, 0x08, 0x52, 0x65, 0x70, + 0x6f, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x2f, 0x0a, 0x04, 0x54, 0x61, 0x67, + 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x5f, 0x76, + 0x31, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x4d, 0x65, 0x74, 0x61, 0x2e, 0x54, 0x61, 0x67, 0x73, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x54, 0x61, 0x67, 0x73, 0x12, 0x41, 0x0a, 0x0a, 0x53, 0x74, + 0x61, 0x74, 0x69, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, + 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x5f, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x4d, 0x65, 0x74, + 0x61, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x69, 0x73, 0x74, 0x69, 0x63, 0x73, 0x45, 0x6e, 0x74, 0x72, + 0x79, 0x52, 0x0a, 0x53, 0x74, 0x61, 0x74, 0x69, 0x73, 0x74, 0x69, 0x63, 0x73, 0x12, 0x41, 0x0a, + 0x0a, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x21, 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x5f, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x70, 0x6f, + 0x4d, 0x65, 0x74, 0x61, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0a, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, + 0x12, 0x3e, 0x0a, 0x09, 0x52, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x72, 0x73, 0x18, 0x05, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x5f, 0x76, 0x31, 0x2e, 0x52, 0x65, + 0x70, 0x6f, 0x4d, 0x65, 0x74, 0x61, 0x2e, 0x52, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x72, 0x73, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x09, 0x52, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x72, 0x73, + 0x12, 0x1c, 0x0a, 0x09, 0x49, 0x73, 0x53, 0x74, 0x61, 0x72, 0x72, 0x65, 0x64, 0x18, 0x06, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x09, 0x49, 0x73, 0x53, 0x74, 0x61, 0x72, 0x72, 0x65, 0x64, 0x12, 0x22, + 0x0a, 0x0c, 0x49, 0x73, 0x42, 0x6f, 0x6f, 0x6b, 0x6d, 0x61, 0x72, 0x6b, 0x65, 0x64, 0x18, 0x07, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x49, 0x73, 0x42, 0x6f, 0x6f, 0x6b, 0x6d, 0x61, 0x72, 0x6b, + 0x65, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x52, 0x61, 0x6e, 0x6b, 0x18, 0x08, 0x20, 0x01, 0x28, 0x05, + 0x52, 0x04, 0x52, 0x61, 0x6e, 0x6b, 0x12, 0x14, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x72, 0x73, 0x18, + 0x09, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x53, 0x74, 0x61, 0x72, 0x73, 0x12, 0x12, 0x0a, 0x04, + 0x53, 0x69, 0x7a, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x53, 0x69, 0x7a, 0x65, + 0x12, 0x18, 0x0a, 0x07, 0x56, 0x65, 0x6e, 0x64, 0x6f, 0x72, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, + 0x09, 0x52, 0x07, 0x56, 0x65, 0x6e, 0x64, 0x6f, 0x72, 0x73, 0x12, 0x2e, 0x0a, 0x09, 0x50, 0x6c, + 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x73, 0x18, 0x0c, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, + 0x6f, 0x63, 0x69, 0x5f, 0x76, 0x31, 0x2e, 0x50, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x52, + 0x09, 0x50, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x73, 0x12, 0x4e, 0x0a, 0x10, 0x4c, 0x61, + 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x18, 0x0d, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x5f, 0x76, 0x31, 0x2e, 0x52, + 0x65, 0x70, 0x6f, 0x4c, 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x49, 0x6d, + 0x61, 0x67, 0x65, 0x48, 0x00, 0x52, 0x10, 0x4c, 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x64, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x88, 0x01, 0x01, 0x12, 0x1c, 0x0a, 0x09, 0x44, 0x6f, + 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x73, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x44, + 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x73, 0x1a, 0x4f, 0x0a, 0x09, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x33, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x5f, 0x76, 0x31, - 0x2e, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x53, 0x74, 0x61, 0x74, 0x69, - 0x73, 0x74, 0x69, 0x63, 0x73, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, - 0x1a, 0x5a, 0x0a, 0x0f, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x45, 0x6e, - 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x31, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x5f, 0x76, 0x31, 0x2e, 0x4d, - 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, - 0x73, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x54, 0x0a, 0x0e, - 0x52, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, - 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, - 0x12, 0x2c, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x16, 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x5f, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x66, 0x65, 0x72, 0x72, - 0x65, 0x72, 0x73, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, - 0x38, 0x01, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x4c, 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x64, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x22, 0xa1, 0x01, 0x0a, 0x09, 0x52, 0x65, 0x70, 0x6f, - 0x42, 0x6c, 0x6f, 0x62, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x33, 0x0a, 0x05, 0x42, 0x6c, 0x6f, - 0x62, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x5f, - 0x76, 0x31, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x42, 0x6c, 0x6f, 0x62, 0x73, 0x2e, 0x42, 0x6c, 0x6f, - 0x62, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, 0x42, 0x6c, 0x6f, 0x62, 0x73, 0x1a, 0x4b, - 0x0a, 0x0a, 0x42, 0x6c, 0x6f, 0x62, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, - 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x27, - 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, - 0x6d, 0x65, 0x74, 0x61, 0x5f, 0x76, 0x31, 0x2e, 0x42, 0x6c, 0x6f, 0x62, 0x49, 0x6e, 0x66, 0x6f, - 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xd7, 0x01, 0x0a, 0x08, - 0x42, 0x6c, 0x6f, 0x62, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x12, 0x0a, 0x04, 0x53, 0x69, 0x7a, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x18, 0x0a, 0x07, - 0x56, 0x65, 0x6e, 0x64, 0x6f, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x56, - 0x65, 0x6e, 0x64, 0x6f, 0x72, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x53, 0x75, 0x62, 0x42, 0x6c, 0x6f, - 0x62, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x53, 0x75, 0x62, 0x42, 0x6c, 0x6f, - 0x62, 0x73, 0x12, 0x2e, 0x0a, 0x09, 0x50, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x73, 0x18, - 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x6f, 0x63, 0x69, 0x5f, 0x76, 0x31, 0x2e, 0x50, - 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x52, 0x09, 0x50, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, - 0x6d, 0x73, 0x12, 0x41, 0x0a, 0x0b, 0x4c, 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x48, 0x00, 0x52, 0x0b, 0x4c, 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x64, 0x88, 0x01, 0x01, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x4c, 0x61, 0x73, 0x74, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x64, 0x22, 0xe4, 0x01, 0x0a, 0x14, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x6f, 0x72, 0x53, 0x74, 0x61, 0x74, 0x69, 0x73, 0x74, 0x69, 0x63, 0x73, 0x12, 0x24, - 0x0a, 0x0d, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x43, - 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x48, 0x0a, 0x11, 0x4c, 0x61, 0x73, 0x74, 0x50, 0x75, 0x6c, 0x6c, - 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x11, 0x4c, 0x61, 0x73, - 0x74, 0x50, 0x75, 0x6c, 0x6c, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x40, - 0x0a, 0x0d, 0x50, 0x75, 0x73, 0x68, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, - 0x70, 0x52, 0x0d, 0x50, 0x75, 0x73, 0x68, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, - 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x75, 0x73, 0x68, 0x65, 0x64, 0x42, 0x79, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x08, 0x50, 0x75, 0x73, 0x68, 0x65, 0x64, 0x42, 0x79, 0x22, 0x3a, 0x0a, 0x0d, - 0x52, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x72, 0x73, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x29, 0x0a, - 0x04, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x65, - 0x74, 0x61, 0x5f, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x72, 0x49, 0x6e, - 0x66, 0x6f, 0x52, 0x04, 0x6c, 0x69, 0x73, 0x74, 0x22, 0x9c, 0x02, 0x0a, 0x0c, 0x52, 0x65, 0x66, - 0x65, 0x72, 0x72, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x69, 0x67, - 0x65, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x69, 0x67, 0x65, 0x73, - 0x74, 0x12, 0x14, 0x0a, 0x05, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x05, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x4d, 0x65, 0x64, 0x69, 0x61, - 0x54, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x4d, 0x65, 0x64, 0x69, - 0x61, 0x54, 0x79, 0x70, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, - 0x74, 0x54, 0x79, 0x70, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x41, 0x72, 0x74, - 0x69, 0x66, 0x61, 0x63, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x53, 0x69, 0x7a, - 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x48, 0x0a, - 0x0b, 0x41, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x06, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x5f, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x66, - 0x65, 0x72, 0x72, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x41, 0x6e, 0x6e, 0x6f, 0x74, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0b, 0x41, 0x6e, 0x6e, 0x6f, - 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x1a, 0x3e, 0x0a, 0x10, 0x41, 0x6e, 0x6e, 0x6f, 0x74, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, - 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, - 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x9d, 0x01, 0x0a, 0x12, 0x4d, 0x61, 0x6e, 0x69, - 0x66, 0x65, 0x73, 0x74, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x12, 0x36, - 0x0a, 0x03, 0x6d, 0x61, 0x70, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x6d, 0x65, + 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2c, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x5f, 0x76, 0x31, + 0x2e, 0x54, 0x61, 0x67, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x52, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x5c, 0x0a, 0x0f, 0x53, 0x74, 0x61, + 0x74, 0x69, 0x73, 0x74, 0x69, 0x63, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, + 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x33, + 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, + 0x6d, 0x65, 0x74, 0x61, 0x5f, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, + 0x6f, 0x72, 0x53, 0x74, 0x61, 0x74, 0x69, 0x73, 0x74, 0x69, 0x63, 0x73, 0x52, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x5a, 0x0a, 0x0f, 0x53, 0x69, 0x67, 0x6e, 0x61, + 0x74, 0x75, 0x72, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, + 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x31, 0x0a, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x5f, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x53, 0x69, - 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x2e, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, - 0x79, 0x52, 0x03, 0x6d, 0x61, 0x70, 0x1a, 0x4f, 0x0a, 0x08, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, + 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, + 0x02, 0x38, 0x01, 0x1a, 0x54, 0x0a, 0x0e, 0x52, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x72, 0x73, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2c, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x5f, 0x76, 0x31, + 0x2e, 0x52, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x72, 0x73, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x4c, 0x61, + 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x22, 0xa1, + 0x01, 0x0a, 0x09, 0x52, 0x65, 0x70, 0x6f, 0x42, 0x6c, 0x6f, 0x62, 0x73, 0x12, 0x12, 0x0a, 0x04, + 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, + 0x12, 0x33, 0x0a, 0x05, 0x42, 0x6c, 0x6f, 0x62, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x1d, 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x5f, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x42, 0x6c, + 0x6f, 0x62, 0x73, 0x2e, 0x42, 0x6c, 0x6f, 0x62, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, + 0x42, 0x6c, 0x6f, 0x62, 0x73, 0x1a, 0x4b, 0x0a, 0x0a, 0x42, 0x6c, 0x6f, 0x62, 0x73, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x27, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x5f, 0x76, 0x31, 0x2e, 0x42, + 0x6c, 0x6f, 0x62, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, + 0x38, 0x01, 0x22, 0xd7, 0x01, 0x0a, 0x08, 0x42, 0x6c, 0x6f, 0x62, 0x49, 0x6e, 0x66, 0x6f, 0x12, + 0x12, 0x0a, 0x04, 0x53, 0x69, 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x53, + 0x69, 0x7a, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x56, 0x65, 0x6e, 0x64, 0x6f, 0x72, 0x73, 0x18, 0x02, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x56, 0x65, 0x6e, 0x64, 0x6f, 0x72, 0x73, 0x12, 0x1a, 0x0a, + 0x08, 0x53, 0x75, 0x62, 0x42, 0x6c, 0x6f, 0x62, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x08, 0x53, 0x75, 0x62, 0x42, 0x6c, 0x6f, 0x62, 0x73, 0x12, 0x2e, 0x0a, 0x09, 0x50, 0x6c, 0x61, + 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x6f, + 0x63, 0x69, 0x5f, 0x76, 0x31, 0x2e, 0x50, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x52, 0x09, + 0x50, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x73, 0x12, 0x41, 0x0a, 0x0b, 0x4c, 0x61, 0x73, + 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x48, 0x00, 0x52, 0x0b, 0x4c, 0x61, + 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x88, 0x01, 0x01, 0x42, 0x0e, 0x0a, 0x0c, + 0x5f, 0x4c, 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x22, 0xe4, 0x01, 0x0a, + 0x14, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x53, 0x74, 0x61, 0x74, 0x69, + 0x73, 0x74, 0x69, 0x63, 0x73, 0x12, 0x24, 0x0a, 0x0d, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, + 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x44, 0x6f, + 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x48, 0x0a, 0x11, 0x4c, + 0x61, 0x73, 0x74, 0x50, 0x75, 0x6c, 0x6c, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, + 0x6d, 0x70, 0x52, 0x11, 0x4c, 0x61, 0x73, 0x74, 0x50, 0x75, 0x6c, 0x6c, 0x54, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x40, 0x0a, 0x0d, 0x50, 0x75, 0x73, 0x68, 0x54, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, + 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0d, 0x50, 0x75, 0x73, 0x68, 0x54, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x75, 0x73, 0x68, 0x65, + 0x64, 0x42, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x50, 0x75, 0x73, 0x68, 0x65, + 0x64, 0x42, 0x79, 0x22, 0x3a, 0x0a, 0x0d, 0x52, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x72, 0x73, + 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x29, 0x0a, 0x04, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x5f, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x66, + 0x65, 0x72, 0x72, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x04, 0x6c, 0x69, 0x73, 0x74, 0x22, + 0x9c, 0x02, 0x0a, 0x0c, 0x52, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, + 0x12, 0x16, 0x0a, 0x06, 0x44, 0x69, 0x67, 0x65, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x44, 0x69, 0x67, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x43, 0x6f, 0x75, 0x6e, + 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1c, + 0x0a, 0x09, 0x4d, 0x65, 0x64, 0x69, 0x61, 0x54, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x09, 0x4d, 0x65, 0x64, 0x69, 0x61, 0x54, 0x79, 0x70, 0x65, 0x12, 0x22, 0x0a, 0x0c, + 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x54, 0x79, 0x70, 0x65, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0c, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x54, 0x79, 0x70, 0x65, + 0x12, 0x12, 0x0a, 0x04, 0x53, 0x69, 0x7a, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, + 0x53, 0x69, 0x7a, 0x65, 0x12, 0x48, 0x0a, 0x0b, 0x41, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x6d, 0x65, 0x74, 0x61, + 0x5f, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, + 0x2e, 0x41, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, + 0x79, 0x52, 0x0b, 0x41, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x1a, 0x3e, + 0x0a, 0x10, 0x41, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2d, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x5f, 0x76, 0x31, 0x2e, 0x53, 0x69, - 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x05, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x3c, 0x0a, 0x0e, 0x53, 0x69, 0x67, 0x6e, 0x61, - 0x74, 0x75, 0x72, 0x65, 0x73, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x2a, 0x0a, 0x04, 0x6c, 0x69, 0x73, - 0x74, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x5f, 0x76, - 0x31, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, - 0x04, 0x6c, 0x69, 0x73, 0x74, 0x22, 0x7e, 0x0a, 0x0d, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, - 0x72, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x38, 0x0a, 0x17, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, - 0x75, 0x72, 0x65, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x44, 0x69, 0x67, 0x65, 0x73, - 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, - 0x72, 0x65, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x44, 0x69, 0x67, 0x65, 0x73, 0x74, - 0x12, 0x33, 0x0a, 0x0a, 0x4c, 0x61, 0x79, 0x65, 0x72, 0x73, 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x02, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x5f, 0x76, 0x31, 0x2e, 0x4c, - 0x61, 0x79, 0x65, 0x72, 0x73, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0a, 0x4c, 0x61, 0x79, 0x65, 0x72, - 0x73, 0x49, 0x6e, 0x66, 0x6f, 0x22, 0xbe, 0x01, 0x0a, 0x0a, 0x4c, 0x61, 0x79, 0x65, 0x72, 0x73, - 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x20, 0x0a, 0x0b, 0x4c, 0x61, 0x79, 0x65, 0x72, 0x44, 0x69, 0x67, - 0x65, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x4c, 0x61, 0x79, 0x65, 0x72, - 0x44, 0x69, 0x67, 0x65, 0x73, 0x74, 0x12, 0x22, 0x0a, 0x0c, 0x4c, 0x61, 0x79, 0x65, 0x72, 0x43, - 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x4c, 0x61, - 0x79, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x22, 0x0a, 0x0c, 0x53, 0x69, - 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x4b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0c, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x16, - 0x0a, 0x06, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, - 0x53, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x12, 0x2e, 0x0a, 0x04, 0x44, 0x61, 0x74, 0x65, 0x18, 0x05, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, - 0x52, 0x04, 0x44, 0x61, 0x74, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x9d, + 0x01, 0x0a, 0x12, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x53, 0x69, 0x67, 0x6e, 0x61, + 0x74, 0x75, 0x72, 0x65, 0x73, 0x12, 0x36, 0x0a, 0x03, 0x6d, 0x61, 0x70, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x5f, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x6e, + 0x69, 0x66, 0x65, 0x73, 0x74, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x2e, + 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x03, 0x6d, 0x61, 0x70, 0x1a, 0x4f, 0x0a, + 0x08, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2d, 0x0a, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x6d, 0x65, 0x74, + 0x61, 0x5f, 0x76, 0x31, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x49, + 0x6e, 0x66, 0x6f, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x3c, + 0x0a, 0x0e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x49, 0x6e, 0x66, 0x6f, + 0x12, 0x2a, 0x0a, 0x04, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, + 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x5f, 0x76, 0x31, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, + 0x72, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x04, 0x6c, 0x69, 0x73, 0x74, 0x22, 0x7e, 0x0a, 0x0d, + 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x38, 0x0a, + 0x17, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, + 0x73, 0x74, 0x44, 0x69, 0x67, 0x65, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, + 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, + 0x74, 0x44, 0x69, 0x67, 0x65, 0x73, 0x74, 0x12, 0x33, 0x0a, 0x0a, 0x4c, 0x61, 0x79, 0x65, 0x72, + 0x73, 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x6d, 0x65, + 0x74, 0x61, 0x5f, 0x76, 0x31, 0x2e, 0x4c, 0x61, 0x79, 0x65, 0x72, 0x73, 0x49, 0x6e, 0x66, 0x6f, + 0x52, 0x0a, 0x4c, 0x61, 0x79, 0x65, 0x72, 0x73, 0x49, 0x6e, 0x66, 0x6f, 0x22, 0xbe, 0x01, 0x0a, + 0x0a, 0x4c, 0x61, 0x79, 0x65, 0x72, 0x73, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x20, 0x0a, 0x0b, 0x4c, + 0x61, 0x79, 0x65, 0x72, 0x44, 0x69, 0x67, 0x65, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0b, 0x4c, 0x61, 0x79, 0x65, 0x72, 0x44, 0x69, 0x67, 0x65, 0x73, 0x74, 0x12, 0x22, 0x0a, + 0x0c, 0x4c, 0x61, 0x79, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x4c, 0x61, 0x79, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, + 0x74, 0x12, 0x22, 0x0a, 0x0c, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x4b, 0x65, + 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, + 0x72, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x12, 0x2e, 0x0a, + 0x04, 0x44, 0x61, 0x74, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x04, 0x44, 0x61, 0x74, 0x65, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1309,47 +1321,48 @@ var file_meta_meta_proto_goTypes = []interface{}{ nil, // 19: meta_v1.RepoBlobs.BlobsEntry nil, // 20: meta_v1.ReferrerInfo.AnnotationsEntry nil, // 21: meta_v1.ManifestSignatures.MapEntry - (*Manifest)(nil), // 22: oci_v1.Manifest - (*Image)(nil), // 23: oci_v1.Image - (*Index)(nil), // 24: oci_v1.Index - (*timestamppb.Timestamp)(nil), // 25: google.protobuf.Timestamp + (*timestamppb.Timestamp)(nil), // 22: google.protobuf.Timestamp + (*Manifest)(nil), // 23: oci_v1.Manifest + (*Image)(nil), // 24: oci_v1.Image + (*Index)(nil), // 25: oci_v1.Index (*Platform)(nil), // 26: oci_v1.Platform } var file_meta_meta_proto_depIdxs = []int32{ - 2, // 0: meta_v1.ImageMeta.Manifests:type_name -> meta_v1.ManifestMeta - 3, // 1: meta_v1.ImageMeta.Index:type_name -> meta_v1.IndexMeta - 22, // 2: meta_v1.ManifestMeta.Manifest:type_name -> oci_v1.Manifest - 23, // 3: meta_v1.ManifestMeta.Config:type_name -> oci_v1.Image - 24, // 4: meta_v1.IndexMeta.Index:type_name -> oci_v1.Index - 25, // 5: meta_v1.RepoLastUpdatedImage.LastUpdated:type_name -> google.protobuf.Timestamp - 15, // 6: meta_v1.RepoMeta.Tags:type_name -> meta_v1.RepoMeta.TagsEntry - 16, // 7: meta_v1.RepoMeta.Statistics:type_name -> meta_v1.RepoMeta.StatisticsEntry - 17, // 8: meta_v1.RepoMeta.Signatures:type_name -> meta_v1.RepoMeta.SignaturesEntry - 18, // 9: meta_v1.RepoMeta.Referrers:type_name -> meta_v1.RepoMeta.ReferrersEntry - 26, // 10: meta_v1.RepoMeta.Platforms:type_name -> oci_v1.Platform - 4, // 11: meta_v1.RepoMeta.LastUpdatedImage:type_name -> meta_v1.RepoLastUpdatedImage - 19, // 12: meta_v1.RepoBlobs.Blobs:type_name -> meta_v1.RepoBlobs.BlobsEntry - 26, // 13: meta_v1.BlobInfo.Platforms:type_name -> oci_v1.Platform - 25, // 14: meta_v1.BlobInfo.LastUpdated:type_name -> google.protobuf.Timestamp - 25, // 15: meta_v1.DescriptorStatistics.LastPullTimestamp:type_name -> google.protobuf.Timestamp - 25, // 16: meta_v1.DescriptorStatistics.PushTimestamp:type_name -> google.protobuf.Timestamp - 10, // 17: meta_v1.ReferrersInfo.list:type_name -> meta_v1.ReferrerInfo - 20, // 18: meta_v1.ReferrerInfo.Annotations:type_name -> meta_v1.ReferrerInfo.AnnotationsEntry - 21, // 19: meta_v1.ManifestSignatures.map:type_name -> meta_v1.ManifestSignatures.MapEntry - 13, // 20: meta_v1.SignaturesInfo.list:type_name -> meta_v1.SignatureInfo - 14, // 21: meta_v1.SignatureInfo.LayersInfo:type_name -> meta_v1.LayersInfo - 25, // 22: meta_v1.LayersInfo.Date:type_name -> google.protobuf.Timestamp - 0, // 23: meta_v1.RepoMeta.TagsEntry.value:type_name -> meta_v1.TagDescriptor - 8, // 24: meta_v1.RepoMeta.StatisticsEntry.value:type_name -> meta_v1.DescriptorStatistics - 11, // 25: meta_v1.RepoMeta.SignaturesEntry.value:type_name -> meta_v1.ManifestSignatures - 9, // 26: meta_v1.RepoMeta.ReferrersEntry.value:type_name -> meta_v1.ReferrersInfo - 7, // 27: meta_v1.RepoBlobs.BlobsEntry.value:type_name -> meta_v1.BlobInfo - 12, // 28: meta_v1.ManifestSignatures.MapEntry.value:type_name -> meta_v1.SignaturesInfo - 29, // [29:29] is the sub-list for method output_type - 29, // [29:29] is the sub-list for method input_type - 29, // [29:29] is the sub-list for extension type_name - 29, // [29:29] is the sub-list for extension extendee - 0, // [0:29] is the sub-list for field type_name + 22, // 0: meta_v1.TagDescriptor.TaggedTimestamp:type_name -> google.protobuf.Timestamp + 2, // 1: meta_v1.ImageMeta.Manifests:type_name -> meta_v1.ManifestMeta + 3, // 2: meta_v1.ImageMeta.Index:type_name -> meta_v1.IndexMeta + 23, // 3: meta_v1.ManifestMeta.Manifest:type_name -> oci_v1.Manifest + 24, // 4: meta_v1.ManifestMeta.Config:type_name -> oci_v1.Image + 25, // 5: meta_v1.IndexMeta.Index:type_name -> oci_v1.Index + 22, // 6: meta_v1.RepoLastUpdatedImage.LastUpdated:type_name -> google.protobuf.Timestamp + 15, // 7: meta_v1.RepoMeta.Tags:type_name -> meta_v1.RepoMeta.TagsEntry + 16, // 8: meta_v1.RepoMeta.Statistics:type_name -> meta_v1.RepoMeta.StatisticsEntry + 17, // 9: meta_v1.RepoMeta.Signatures:type_name -> meta_v1.RepoMeta.SignaturesEntry + 18, // 10: meta_v1.RepoMeta.Referrers:type_name -> meta_v1.RepoMeta.ReferrersEntry + 26, // 11: meta_v1.RepoMeta.Platforms:type_name -> oci_v1.Platform + 4, // 12: meta_v1.RepoMeta.LastUpdatedImage:type_name -> meta_v1.RepoLastUpdatedImage + 19, // 13: meta_v1.RepoBlobs.Blobs:type_name -> meta_v1.RepoBlobs.BlobsEntry + 26, // 14: meta_v1.BlobInfo.Platforms:type_name -> oci_v1.Platform + 22, // 15: meta_v1.BlobInfo.LastUpdated:type_name -> google.protobuf.Timestamp + 22, // 16: meta_v1.DescriptorStatistics.LastPullTimestamp:type_name -> google.protobuf.Timestamp + 22, // 17: meta_v1.DescriptorStatistics.PushTimestamp:type_name -> google.protobuf.Timestamp + 10, // 18: meta_v1.ReferrersInfo.list:type_name -> meta_v1.ReferrerInfo + 20, // 19: meta_v1.ReferrerInfo.Annotations:type_name -> meta_v1.ReferrerInfo.AnnotationsEntry + 21, // 20: meta_v1.ManifestSignatures.map:type_name -> meta_v1.ManifestSignatures.MapEntry + 13, // 21: meta_v1.SignaturesInfo.list:type_name -> meta_v1.SignatureInfo + 14, // 22: meta_v1.SignatureInfo.LayersInfo:type_name -> meta_v1.LayersInfo + 22, // 23: meta_v1.LayersInfo.Date:type_name -> google.protobuf.Timestamp + 0, // 24: meta_v1.RepoMeta.TagsEntry.value:type_name -> meta_v1.TagDescriptor + 8, // 25: meta_v1.RepoMeta.StatisticsEntry.value:type_name -> meta_v1.DescriptorStatistics + 11, // 26: meta_v1.RepoMeta.SignaturesEntry.value:type_name -> meta_v1.ManifestSignatures + 9, // 27: meta_v1.RepoMeta.ReferrersEntry.value:type_name -> meta_v1.ReferrersInfo + 7, // 28: meta_v1.RepoBlobs.BlobsEntry.value:type_name -> meta_v1.BlobInfo + 12, // 29: meta_v1.ManifestSignatures.MapEntry.value:type_name -> meta_v1.SignaturesInfo + 30, // [30:30] is the sub-list for method output_type + 30, // [30:30] is the sub-list for method input_type + 30, // [30:30] is the sub-list for extension type_name + 30, // [30:30] is the sub-list for extension extendee + 0, // [0:30] is the sub-list for field type_name } func init() { file_meta_meta_proto_init() } diff --git a/pkg/meta/proto/gen/versioned.pb.go b/pkg/meta/proto/gen/versioned.pb.go index 630747f5..df7d9272 100644 --- a/pkg/meta/proto/gen/versioned.pb.go +++ b/pkg/meta/proto/gen/versioned.pb.go @@ -7,11 +7,10 @@ package gen import ( - reflect "reflect" - sync "sync" - protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" ) const ( diff --git a/pkg/meta/proto/meta/meta.proto b/pkg/meta/proto/meta/meta.proto index de0621ba..779b93e8 100644 --- a/pkg/meta/proto/meta/meta.proto +++ b/pkg/meta/proto/meta/meta.proto @@ -10,6 +10,7 @@ import "oci/descriptor.proto"; message TagDescriptor { string MediaType = 1; string Digest = 2; + google.protobuf.Timestamp TaggedTimestamp = 3; } message ImageMeta { diff --git a/pkg/meta/redis/redis.go b/pkg/meta/redis/redis.go index fa7f1e22..938dc49d 100644 --- a/pkg/meta/redis/redis.go +++ b/pkg/meta/redis/redis.go @@ -728,6 +728,8 @@ func (rc *RedisDB) SetImageMeta(digest godigest.Digest, imageMeta mTypes.ImageMe } // SetRepoReference sets the given image data to the repo metadata. +// +//nolint:gocyclo // Complex function handling multiple metadata updates (referrers, tags, statistics, signatures, blobs) func (rc *RedisDB) SetRepoReference(ctx context.Context, repo string, reference string, imageMeta mTypes.ImageMeta, ) error { @@ -805,9 +807,23 @@ func (rc *RedisDB) SetRepoReference(ctx context.Context, repo string, // 3. Update tag if !common.ReferenceIsDigest(reference) { + // Set TaggedTimestamp to now if this is a new tag, otherwise preserve existing timestamp + // For old data without TaggedTimestamp, leave it nil so it falls back to PushTimestamp + var taggedTimestamp *timestamppb.Timestamp + if existingTag, exists := protoRepoMeta.Tags[reference]; exists { + // Tag exists - preserve TaggedTimestamp if present, otherwise leave nil (old data) + if existingTag.GetTaggedTimestamp() != nil { + taggedTimestamp = existingTag.GetTaggedTimestamp() + } + // else leave taggedTimestamp as nil (old data without TaggedTimestamp) + } else { + // New tag - set timestamp to now + taggedTimestamp = timestamppb.Now() + } protoRepoMeta.Tags[reference] = &proto_go.TagDescriptor{ - Digest: imageMeta.Digest.String(), - MediaType: imageMeta.MediaType, + Digest: imageMeta.Digest.String(), + MediaType: imageMeta.MediaType, + TaggedTimestamp: taggedTimestamp, } } @@ -1984,9 +2000,11 @@ func (rc *RedisDB) RemoveRepoReference(repo, reference string, manifestDigest go return err } -// ResetRepoReferences resets all layout specific data (tags, signatures, referrers, etc.) but keep user and image +// ResetRepoReferences resets layout specific data (tags, signatures, referrers, etc.) but keep user and image // specific metadata such as star count, downloads other statistics. -func (rc *RedisDB) ResetRepoReferences(repo string) error { +// tagsToKeep is a set of tag names that should be preserved (tags that exist in storage). +// Tags not in tagsToKeep will be removed. +func (rc *RedisDB) ResetRepoReferences(repo string, tagsToKeep map[string]bool) error { ctx := context.Background() err := rc.withRSLocks(ctx, []string{rc.getRepoLockKey(repo)}, func() error { @@ -1995,11 +2013,27 @@ func (rc *RedisDB) ResetRepoReferences(repo string) error { return err } + // Preserve tags that are in tagsToKeep, remove others + preservedTags := make(map[string]*proto_go.TagDescriptor) + if tagsToKeep != nil { + for tag, descriptor := range protoRepoMeta.Tags { + // Keep the tag if it's in tagsToKeep, or if it's the empty key (internal use) + if tag == "" || tagsToKeep[tag] { + preservedTags[tag] = descriptor + } + } + } + + // Ensure empty key exists for internal use + if _, exists := preservedTags[""]; !exists { + preservedTags[""] = &proto_go.TagDescriptor{} + } + repoMetaBlob, err := proto.Marshal(&proto_go.RepoMeta{ Name: repo, Statistics: protoRepoMeta.Statistics, Stars: protoRepoMeta.Stars, - Tags: map[string]*proto_go.TagDescriptor{"": {}}, + Tags: preservedTags, Signatures: map[string]*proto_go.ManifestSignatures{"": {Map: map[string]*proto_go.SignaturesInfo{"": {}}}}, Referrers: map[string]*proto_go.ReferrersInfo{"": {}}, }) @@ -2025,8 +2059,11 @@ func (rc *RedisDB) GetRepoLastUpdated(repo string) time.Time { lastUpdatedBlob, err := rc.Client.HGet(ctx, rc.RepoLastUpdatedKey, repo).Bytes() if err != nil { - rc.Log.Error().Err(err).Str("hget", rc.RepoLastUpdatedKey).Str("repo", repo). - Msg("failed to get repo last updated timestamp") + // redis.Nil is a normal condition when the key doesn't exist (new repo) + if !errors.Is(err, redis.Nil) { + rc.Log.Error().Err(err).Str("hget", rc.RepoLastUpdatedKey).Str("repo", repo). + Msg("failed to get repo last updated timestamp") + } return time.Time{} } diff --git a/pkg/meta/redis/redis_test.go b/pkg/meta/redis/redis_test.go index 6fcf7a6f..44d9c7a6 100644 --- a/pkg/meta/redis/redis_test.go +++ b/pkg/meta/redis/redis_test.go @@ -493,7 +493,7 @@ func TestRedisUnreachable(t *testing.T) { err = metaDB.RemoveRepoReference(repo, reference, digest) So(err, ShouldNotBeNil) - err = metaDB.ResetRepoReferences(repo) + err = metaDB.ResetRepoReferences(repo, nil) So(err, ShouldNotBeNil) t := metaDB.GetRepoLastUpdated(repo) @@ -815,9 +815,96 @@ func TestWrapperErrors(t *testing.T) { err := setRepoMeta("repo", badProtoBlob, client) So(err, ShouldBeNil) - err = metaDB.ResetRepoReferences("repo") + err = metaDB.ResetRepoReferences("repo", nil) So(err, ShouldNotBeNil) }) + + Convey("preserve tags in tagsToKeep", func() { + // Create repo with multiple tags + image1 := CreateRandomImage() + image2 := CreateRandomImage() + + err := metaDB.SetRepoReference(ctx, "repo", "tag1", image1.AsImageMeta()) + So(err, ShouldBeNil) + + // Wait a bit to ensure different timestamps + time.Sleep(10 * time.Millisecond) + + err = metaDB.SetRepoReference(ctx, "repo", "tag2", image2.AsImageMeta()) + So(err, ShouldBeNil) + + // Get repo meta to capture TaggedTimestamp + repoMeta, err := metaDB.GetRepoMeta(ctx, "repo") + So(err, ShouldBeNil) + So(repoMeta.Tags, ShouldContainKey, "tag1") + So(repoMeta.Tags, ShouldContainKey, "tag2") + + tag1Timestamp := repoMeta.Tags["tag1"].TaggedTimestamp + + // Reset with only tag1 in tagsToKeep + tagsToKeep := map[string]bool{"tag1": true} + err = metaDB.ResetRepoReferences("repo", tagsToKeep) + So(err, ShouldBeNil) + + // Verify tag1 is preserved with its timestamp + repoMeta, err = metaDB.GetRepoMeta(ctx, "repo") + So(err, ShouldBeNil) + So(repoMeta.Tags, ShouldContainKey, "tag1") + So(repoMeta.Tags, ShouldNotContainKey, "tag2") + So(repoMeta.Tags["tag1"].TaggedTimestamp, ShouldEqual, tag1Timestamp) + }) + + Convey("remove tags not in tagsToKeep", func() { + // Create repo with multiple tags + image1 := CreateRandomImage() + image2 := CreateRandomImage() + image3 := CreateRandomImage() + + err := metaDB.SetRepoReference(ctx, "repo", "tag1", image1.AsImageMeta()) + So(err, ShouldBeNil) + + err = metaDB.SetRepoReference(ctx, "repo", "tag2", image2.AsImageMeta()) + So(err, ShouldBeNil) + + err = metaDB.SetRepoReference(ctx, "repo", "tag3", image3.AsImageMeta()) + So(err, ShouldBeNil) + + // Reset with tag1 and tag2 in tagsToKeep + tagsToKeep := map[string]bool{"tag1": true, "tag2": true} + err = metaDB.ResetRepoReferences("repo", tagsToKeep) + So(err, ShouldBeNil) + + // Verify only tag1 and tag2 are preserved + repoMeta, err := metaDB.GetRepoMeta(ctx, "repo") + So(err, ShouldBeNil) + So(repoMeta.Tags, ShouldContainKey, "tag1") + So(repoMeta.Tags, ShouldContainKey, "tag2") + So(repoMeta.Tags, ShouldNotContainKey, "tag3") + }) + + Convey("preserve statistics and stars", func() { + image := CreateRandomImage() + + err := metaDB.SetRepoReference(ctx, "repo", "tag1", image.AsImageMeta()) + So(err, ShouldBeNil) + + err = metaDB.IncrementRepoStars("repo") + So(err, ShouldBeNil) + + // Get original stats + repoMeta, err := metaDB.GetRepoMeta(ctx, "repo") + So(err, ShouldBeNil) + originalStars := repoMeta.StarCount + + // Reset with empty tagsToKeep + err = metaDB.ResetRepoReferences("repo", map[string]bool{}) + So(err, ShouldBeNil) + + // Verify statistics and stars are preserved + repoMeta, err = metaDB.GetRepoMeta(ctx, "repo") + So(err, ShouldBeNil) + So(repoMeta.StarCount, ShouldEqual, originalStars) + }) }) Convey("DecrementRepoStars", func() { diff --git a/pkg/meta/types/types.go b/pkg/meta/types/types.go index 6e16b311..4a32e65f 100644 --- a/pkg/meta/types/types.go +++ b/pkg/meta/types/types.go @@ -141,9 +141,11 @@ type MetaDB interface { //nolint:interfacebloat */ RemoveRepoReference(repo, reference string, manifestDigest godigest.Digest) error - // ResetRepoReferences resets all layout specific data (tags, signatures, referrers, etc.) but keep user and image - // specific metadata such as star count, downloads other statistics - ResetRepoReferences(repo string) error + // ResetRepoReferences resets layout specific data (tags, signatures, referrers, etc.) but keep user and image + // specific metadata such as star count, downloads other statistics. + // tagsToKeep is a set of tag names that should be preserved (tags that exist in storage). + // Tags not in tagsToKeep will be removed. + ResetRepoReferences(repo string, tagsToKeep map[string]bool) error GetRepoLastUpdated(repo string) time.Time @@ -270,9 +272,10 @@ type FullImageMeta struct { IsStarred bool IsBookmarked bool - Referrers []ReferrerInfo - Statistics DescriptorStatistics - Signatures ManifestSignatures + Referrers []ReferrerInfo + Statistics DescriptorStatistics + Signatures ManifestSignatures + TaggedTimestamp time.Time } type FullManifestMeta struct { @@ -300,8 +303,9 @@ type ReferrerInfo struct { // Descriptor represents an image. Multiple images might have the same digests but different tags. type Descriptor struct { - Digest string - MediaType string + Digest string + MediaType string + TaggedTimestamp time.Time } type DescriptorStatistics struct { diff --git a/pkg/test/mocks/repo_db_mock.go b/pkg/test/mocks/repo_db_mock.go index 317778b7..f10ee434 100644 --- a/pkg/test/mocks/repo_db_mock.go +++ b/pkg/test/mocks/repo_db_mock.go @@ -98,7 +98,7 @@ type MetaDBMock struct { GetFullImageMetaFn func(ctx context.Context, repo string, tag string) (mTypes.FullImageMeta, error) - ResetRepoReferencesFn func(repo string) error + ResetRepoReferencesFn func(repo string, tagsToKeep map[string]bool) error GetAllRepoNamesFn func() ([]string, error) @@ -456,9 +456,9 @@ func (sdm MetaDBMock) GetFullImageMeta(ctx context.Context, repo string, tag str return mTypes.FullImageMeta{}, nil } -func (sdm MetaDBMock) ResetRepoReferences(repo string) error { +func (sdm MetaDBMock) ResetRepoReferences(repo string, tagsToKeep map[string]bool) error { if sdm.ResetRepoReferencesFn != nil { - return sdm.ResetRepoReferencesFn(repo) + return sdm.ResetRepoReferencesFn(repo, tagsToKeep) } return nil