fix(search): expose LastPullTimestamp and PushedBy on index ImageSummary (#3865)

ImageIndex2ImageSummary was missing LastPullTimestamp assignment, causing
multi-arch image queries to always return null for this field. Also adds
the PushedBy field (already stored in MetaDB) to the GraphQL schema and
both conversion paths (manifest and index).

Signed-off-by: cainydev <wajo432@gmail.com>
This commit is contained in:
John Wagner
2026-03-11 00:40:54 +01:00
committed by GitHub
parent 2ba0525f01
commit 8f0bf18d7e
5 changed files with 103 additions and 28 deletions
+14 -3
View File
@@ -279,19 +279,21 @@ func TestTaggedTimestamp(t *testing.T) {
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)
digestStr := godigest.FromString("sha256:abc123").String()
repoMeta := mTypes.RepoMeta{
Name: "repo",
Tags: map[string]mTypes.Descriptor{
"tag1": {
Digest: "sha256:abc123",
Digest: digestStr,
MediaType: ispec.MediaTypeImageManifest,
TaggedTimestamp: taggedTime,
},
},
Statistics: map[string]mTypes.DescriptorStatistics{
"sha256:abc123": {
digestStr: {
PushTimestamp: pushTime,
PushedBy: "testuser",
},
},
}
@@ -319,6 +321,8 @@ func TestTaggedTimestamp(t *testing.T) {
So(err, ShouldBeNil)
So(imageSummary.TaggedTimestamp, ShouldNotBeNil)
So(*imageSummary.TaggedTimestamp, ShouldEqual, taggedTime)
So(imageSummary.PushedBy, ShouldNotBeNil)
So(*imageSummary.PushedBy, ShouldEqual, "testuser")
})
Convey("TaggedTimestamp falls back to PushTimestamp when zero", func() {
@@ -372,6 +376,7 @@ func TestTaggedTimestamp(t *testing.T) {
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)
lastPullTime := time.Date(2024, time.February, 1, 12, 0, 0, 0, time.UTC)
// Create a multiarch image
multiarchImage := CreateMultiarchWith().Images([]Image{
@@ -392,7 +397,9 @@ func TestTaggedTimestamp(t *testing.T) {
},
Statistics: map[string]mTypes.DescriptorStatistics{
indexDigestStr: {
PushTimestamp: pushTime,
PushTimestamp: pushTime,
LastPullTimestamp: lastPullTime,
PushedBy: "indexuser",
},
},
}
@@ -405,6 +412,10 @@ func TestTaggedTimestamp(t *testing.T) {
So(err, ShouldBeNil)
So(imageSummary.TaggedTimestamp, ShouldNotBeNil)
So(*imageSummary.TaggedTimestamp, ShouldEqual, taggedTime)
So(imageSummary.LastPullTimestamp, ShouldNotBeNil)
So(*imageSummary.LastPullTimestamp, ShouldEqual, lastPullTime)
So(imageSummary.PushedBy, ShouldNotBeNil)
So(*imageSummary.PushedBy, ShouldEqual, "indexuser")
// Verify that nested manifests also have the correct TaggedTimestamp
So(len(imageSummary.Manifests), ShouldBeGreaterThan, 0)
+31 -25
View File
@@ -427,10 +427,12 @@ 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
pushTimestamp = fullImageMeta.Statistics.PushTimestamp
taggedTimestamp = fullImageMeta.TaggedTimestamp
indexDigestStr = fullImageMeta.Digest.String()
indexMediaType = ispec.MediaTypeImageIndex
lastPullTimestamp = fullImageMeta.Statistics.LastPullTimestamp
pushTimestamp = fullImageMeta.Statistics.PushTimestamp
pushedBy = fullImageMeta.Statistics.PushedBy
taggedTimestamp = fullImageMeta.TaggedTimestamp
)
// Fallback to PushTimestamp if TaggedTimestamp is not available
@@ -490,27 +492,29 @@ 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),
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),
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),
LastPullTimestamp: &lastPullTimestamp,
PushTimestamp: &pushTimestamp,
PushedBy: &pushedBy,
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
@@ -534,6 +538,7 @@ func ImageManifest2ImageSummary(ctx context.Context, fullImageMeta mTypes.FullIm
isSigned = isImageSigned(fullImageMeta.Signatures)
lastPullTimestamp = fullImageMeta.Statistics.LastPullTimestamp
pushTimestamp = fullImageMeta.Statistics.PushTimestamp
pushedBy = fullImageMeta.Statistics.PushedBy
taggedTimestamp = fullImageMeta.TaggedTimestamp
)
@@ -594,6 +599,7 @@ func ImageManifest2ImageSummary(ctx context.Context, fullImageMeta mTypes.FullIm
DownloadCount: &downloadCount,
LastPullTimestamp: &lastPullTimestamp,
PushTimestamp: &pushTimestamp,
PushedBy: &pushedBy,
TaggedTimestamp: &taggedTimestamp,
Description: &annotations.Description,
Title: &annotations.Title,
@@ -100,6 +100,7 @@ type ComplexityRoot struct {
Manifests func(childComplexity int) int
MediaType func(childComplexity int) int
PushTimestamp func(childComplexity int) int
PushedBy func(childComplexity int) int
Referrers func(childComplexity int) int
RepoName func(childComplexity int) int
SignatureInfo func(childComplexity int) int
@@ -530,6 +531,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
}
return e.ComplexityRoot.ImageSummary.PushTimestamp(childComplexity), true
case "ImageSummary.PushedBy":
if e.ComplexityRoot.ImageSummary.PushedBy == nil {
break
}
return e.ComplexityRoot.ImageSummary.PushedBy(childComplexity), true
case "ImageSummary.Referrers":
if e.ComplexityRoot.ImageSummary.Referrers == nil {
break
@@ -1400,6 +1407,10 @@ type ImageSummary {
"""
PushTimestamp: Time
"""
The user who pushed the image to the registry
"""
PushedBy: String
"""
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
@@ -3106,6 +3117,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 "PushedBy":
return ec.fieldContext_ImageSummary_PushedBy(ctx, field)
case "TaggedTimestamp":
return ec.fieldContext_ImageSummary_TaggedTimestamp(ctx, field)
case "LastUpdated":
@@ -3787,6 +3800,35 @@ func (ec *executionContext) fieldContext_ImageSummary_PushTimestamp(_ context.Co
return fc, nil
}
func (ec *executionContext) _ImageSummary_PushedBy(ctx context.Context, field graphql.CollectedField, obj *ImageSummary) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
ec.OperationContext,
field,
ec.fieldContext_ImageSummary_PushedBy,
func(ctx context.Context) (any, error) {
return obj.PushedBy, nil
},
nil,
ec.marshalOString2ᚖstring,
true,
false,
)
}
func (ec *executionContext) fieldContext_ImageSummary_PushedBy(_ 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 String does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _ImageSummary_TaggedTimestamp(ctx context.Context, field graphql.CollectedField, obj *ImageSummary) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
@@ -5277,6 +5319,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 "PushedBy":
return ec.fieldContext_ImageSummary_PushedBy(ctx, field)
case "TaggedTimestamp":
return ec.fieldContext_ImageSummary_TaggedTimestamp(ctx, field)
case "LastUpdated":
@@ -6034,6 +6078,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 "PushedBy":
return ec.fieldContext_ImageSummary_PushedBy(ctx, field)
case "TaggedTimestamp":
return ec.fieldContext_ImageSummary_TaggedTimestamp(ctx, field)
case "LastUpdated":
@@ -6530,6 +6576,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 "PushedBy":
return ec.fieldContext_ImageSummary_PushedBy(ctx, field)
case "TaggedTimestamp":
return ec.fieldContext_ImageSummary_TaggedTimestamp(ctx, field)
case "LastUpdated":
@@ -6813,6 +6861,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 "PushedBy":
return ec.fieldContext_ImageSummary_PushedBy(ctx, field)
case "TaggedTimestamp":
return ec.fieldContext_ImageSummary_TaggedTimestamp(ctx, field)
case "LastUpdated":
@@ -9065,6 +9115,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 "PushedBy":
out.Values[i] = ec._ImageSummary_PushedBy(ctx, field, obj)
case "TaggedTimestamp":
out.Values[i] = ec._ImageSummary_TaggedTimestamp(ctx, field, obj)
case "LastUpdated":
@@ -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"`
// The user who pushed the image to the registry
PushedBy *string `json:"PushedBy,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)
+4
View File
@@ -213,6 +213,10 @@ type ImageSummary {
"""
PushTimestamp: Time
"""
The user who pushed the image to the registry
"""
PushedBy: String
"""
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