From 3c7d5a5f1d40eb996ba1d56f28be57a3b77510af Mon Sep 17 00:00:00 2001 From: Andrei Aaron Date: Fri, 30 Jan 2026 23:05:14 +0200 Subject: [PATCH] feat: add TaggedTimestamp to ImageSummary returned by graphql API (#3731) feat(meta): add TaggedTimestamp field and preserve during re-parsing Add TaggedTimestamp field to track when image tags were created, exposed through GraphQL API. Previously, when zot restarted and re-parsed storage, ResetRepoReferences would clear all tags, causing timestamp information to be lost and reset to the service restart time for existing images. This change adds TaggedTimestamp support and modifies ResetRepoReferences to selectively preserve tags that still exist in storage, maintaining their TaggedTimestamp values. Tags that no longer exist in storage are removed as before. Changes: - Add TaggedTimestamp field to GraphQL ImageSummary schema - Update GraphQL conversion functions to populate TaggedTimestamp with fallback to PushTimestamp when unavailable - Updated ResetRepoReferences interface to accept tagsToKeep parameter - Modified ParseRepo to collect tags from storage before resetting - Updated all backend implementations (Redis, DynamoDB, BoltDB) to preserve tags in tagsToKeep instead of clearing all tags - Updated tests and mocks to match new signature This ensures TaggedTimestamp accurately reflects when tags were originally created, and exposes this information through the GraphQL API. Signed-off-by: Andrei Aaron --- pkg/extensions/search/convert/convert_test.go | 182 +++++++ pkg/extensions/search/convert/metadb.go | 103 ++-- .../search/gql_generated/generated.go | 54 +- .../search/gql_generated/models_gen.go | 2 + pkg/extensions/search/schema.graphql | 6 +- pkg/meta/boltdb/boltdb.go | 37 +- pkg/meta/boltdb/boltdb_test.go | 39 +- pkg/meta/common/common.go | 10 + pkg/meta/common/common_test.go | 94 ++++ pkg/meta/convert/convert.go | 33 +- pkg/meta/convert/convert_proto.go | 6 +- pkg/meta/dynamodb/dynamodb.go | 37 +- pkg/meta/dynamodb/dynamodb_test.go | 97 ++++ pkg/meta/parse.go | 12 +- pkg/meta/parse_test.go | 47 +- pkg/meta/proto/gen/config.pb.go | 5 +- pkg/meta/proto/gen/descriptor.pb.go | 5 +- pkg/meta/proto/gen/index.pb.go | 5 +- pkg/meta/proto/gen/manifest.pb.go | 5 +- pkg/meta/proto/gen/meta.pb.go | 485 +++++++++--------- pkg/meta/proto/gen/versioned.pb.go | 5 +- pkg/meta/proto/meta/meta.proto | 1 + pkg/meta/redis/redis.go | 51 +- pkg/meta/redis/redis_test.go | 91 +++- pkg/meta/types/types.go | 20 +- pkg/test/mocks/repo_db_mock.go | 6 +- 26 files changed, 1102 insertions(+), 336 deletions(-) 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