From b5f27c5b50098b3606f6e69a9eba5e30517df8a8 Mon Sep 17 00:00:00 2001 From: Andrei Aaron Date: Fri, 22 Jul 2022 20:01:38 +0000 Subject: [PATCH] RepoSummary has a new attribute NewestTag of type ImageSummary ImageListWithLatestTag currently returns a list of ImageInfo objects. It needs to return consistent results with the API used for Global search as the same information will be used by the UI in the same type or cards. So we need to update RepoSummary to include the data which right now is present in ImageInfo, but missing from RepoSummary (information on the latest tag in that specific repo). Will update return type of ImageListWithLatestTag in a later PR (issue tracked in a separate GH issue) Closes #666 Signed-off-by: Andrei Aaron --- pkg/extensions/search/common/common_test.go | 95 +++++++++++++++++-- pkg/extensions/search/common/oci_layout.go | 8 +- .../search/gql_generated/generated.go | 74 +++++++++++++++ .../search/gql_generated/models_gen.go | 13 +-- pkg/extensions/search/resolver.go | 19 +++- pkg/extensions/search/resolver_test.go | 5 +- pkg/extensions/search/schema.graphql | 1 + pkg/test/mocks/oci_mock.go | 6 +- 8 files changed, 190 insertions(+), 31 deletions(-) diff --git a/pkg/extensions/search/common/common_test.go b/pkg/extensions/search/common/common_test.go index 88ee3d78..e26d865d 100644 --- a/pkg/extensions/search/common/common_test.go +++ b/pkg/extensions/search/common/common_test.go @@ -87,15 +87,17 @@ type ImageSummary struct { Platform OsArch `json:"platform"` Vendor string `json:"vendor"` Score int `json:"score"` + IsSigned bool `json:"isSigned"` } type RepoSummary struct { - Name string `json:"name"` - LastUpdated time.Time `json:"lastUpdated"` - Size string `json:"size"` - Platforms []OsArch `json:"platforms"` - Vendors []string `json:"vendors"` - Score int `json:"score"` + Name string `json:"name"` + LastUpdated time.Time `json:"lastUpdated"` + Size string `json:"size"` + Platforms []OsArch `json:"platforms"` + Vendors []string `json:"vendors"` + Score int `json:"score"` + NewestTag ImageSummary `json:"newestTag"` } type LayerSummary struct { @@ -797,18 +799,37 @@ func TestGlobalSearch(t *testing.T) { Tag LastUpdated Size + IsSigned + Vendor Score + Platform { + Os + Arch + } } Repos { Name LastUpdated Size Platforms { - Os - Arch + Os + Arch } Vendors Score + NewestTag { + RepoName + Tag + LastUpdated + Size + IsSigned + Vendor + Score + Platform { + Os + Arch + } + } } Layers { Digest @@ -826,11 +847,65 @@ func TestGlobalSearch(t *testing.T) { err = json.Unmarshal(resp.Body(), responseStruct) So(err, ShouldBeNil) + // There are 2 repos: zot-cve-test and zot-test, each having an image with tag 0.0.1 + imageStore := ctlr.StoreController.DefaultStore + + repos, err := imageStore.GetRepositories() + So(err, ShouldBeNil) + expectedRepoCount := len(repos) + + allExpectedTagMap := make(map[string][]string, expectedRepoCount) + expectedImageCount := 0 + for _, repo := range repos { + tags, err := imageStore.GetImageTags(repo) + So(err, ShouldBeNil) + + allExpectedTagMap[repo] = tags + expectedImageCount += len(tags) + } + + // Make sure the repo/image counts match before comparing actual content So(responseStruct.GlobalSearchResult.GlobalSearch.Images, ShouldNotBeNil) - So(len(responseStruct.GlobalSearchResult.GlobalSearch.Images), ShouldNotBeEmpty) - So(len(responseStruct.GlobalSearchResult.GlobalSearch.Repos), ShouldNotBeEmpty) + t.Logf("returned images: %v", responseStruct.GlobalSearchResult.GlobalSearch.Images) + So(len(responseStruct.GlobalSearchResult.GlobalSearch.Images), ShouldEqual, expectedImageCount) + t.Logf("returned repos: %v", responseStruct.GlobalSearchResult.GlobalSearch.Repos) + So(len(responseStruct.GlobalSearchResult.GlobalSearch.Repos), ShouldEqual, expectedRepoCount) + t.Logf("returned layers: %v", responseStruct.GlobalSearchResult.GlobalSearch.Layers) So(len(responseStruct.GlobalSearchResult.GlobalSearch.Layers), ShouldNotBeEmpty) + newestImageMap := make(map[string]ImageSummary) + for _, image := range responseStruct.GlobalSearchResult.GlobalSearch.Images { + // Make sure all returned results are supposed to be in the repo + So(allExpectedTagMap[image.RepoName], ShouldContain, image.Tag) + // Identify the newest image in each repo + if newestImage, ok := newestImageMap[image.RepoName]; ok { + if newestImage.LastUpdated.Before(image.LastUpdated) { + newestImageMap[image.RepoName] = image + } + } else { + newestImageMap[image.RepoName] = image + } + } + t.Logf("expected results for newest images in repos: %v", newestImageMap) + + for _, repo := range responseStruct.GlobalSearchResult.GlobalSearch.Repos { + image := newestImageMap[repo.Name] + So(repo.Name, ShouldEqual, image.RepoName) + So(repo.LastUpdated, ShouldEqual, image.LastUpdated) + So(repo.Size, ShouldEqual, image.Size) + So(repo.Vendors[0], ShouldEqual, image.Vendor) + So(repo.Platforms[0].Os, ShouldEqual, image.Platform.Os) + So(repo.Platforms[0].Arch, ShouldEqual, image.Platform.Arch) + So(repo.NewestTag.RepoName, ShouldEqual, image.RepoName) + So(repo.NewestTag.Tag, ShouldEqual, image.Tag) + So(repo.NewestTag.LastUpdated, ShouldEqual, image.LastUpdated) + So(repo.NewestTag.Size, ShouldEqual, image.Size) + So(repo.NewestTag.IsSigned, ShouldEqual, image.IsSigned) + So(repo.NewestTag.Vendor, ShouldEqual, image.Vendor) + So(repo.NewestTag.Platform.Os, ShouldEqual, image.Platform.Os) + So(repo.NewestTag.Platform.Arch, ShouldEqual, image.Platform.Arch) + } + // GetRepositories fail err = os.Chmod(rootDir, 0o333) diff --git a/pkg/extensions/search/common/oci_layout.go b/pkg/extensions/search/common/oci_layout.go index 40d1d2b7..a9223b3c 100644 --- a/pkg/extensions/search/common/oci_layout.go +++ b/pkg/extensions/search/common/oci_layout.go @@ -30,7 +30,7 @@ type OciLayoutUtils interface { GetImagePlatform(imageInfo ispec.Image) (string, string) GetImageVendor(imageInfo ispec.Image) string GetImageManifestSize(repo string, manifestDigest godigest.Digest) int64 - GetRepoLastUpdated(repo string) (time.Time, error) + GetRepoLastUpdated(repo string) (TagInfo, error) GetExpandedRepoInfo(name string) (RepoInfo, error) GetImageConfigInfo(repo string, manifestDigest godigest.Digest) (ispec.Image, error) } @@ -323,15 +323,15 @@ func (olu BaseOciLayoutUtils) GetImageConfigSize(repo string, manifestDigest god return imageBlobManifest.Config.Size } -func (olu BaseOciLayoutUtils) GetRepoLastUpdated(repo string) (time.Time, error) { +func (olu BaseOciLayoutUtils) GetRepoLastUpdated(repo string) (TagInfo, error) { tagsInfo, err := olu.GetImageTagsWithTimestamp(repo) if err != nil || len(tagsInfo) == 0 { - return time.Time{}, err + return TagInfo{}, err } latestTag := GetLatestTag(tagsInfo) - return latestTag.Timestamp, nil + return latestTag, nil } func (olu BaseOciLayoutUtils) GetExpandedRepoInfo(name string) (RepoInfo, error) { diff --git a/pkg/extensions/search/gql_generated/generated.go b/pkg/extensions/search/gql_generated/generated.go index 0261ee6f..40916282 100644 --- a/pkg/extensions/search/gql_generated/generated.go +++ b/pkg/extensions/search/gql_generated/generated.go @@ -143,6 +143,7 @@ type ComplexityRoot struct { RepoSummary struct { LastUpdated func(childComplexity int) int Name func(childComplexity int) int + NewestTag func(childComplexity int) int Platforms func(childComplexity int) int Score func(childComplexity int) int Size func(childComplexity int) int @@ -596,6 +597,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.RepoSummary.Name(childComplexity), true + case "RepoSummary.NewestTag": + if e.complexity.RepoSummary.NewestTag == nil { + break + } + + return e.complexity.RepoSummary.NewestTag(childComplexity), true + case "RepoSummary.Platforms": if e.complexity.RepoSummary.Platforms == nil { break @@ -794,6 +802,7 @@ type RepoSummary { Platforms: [OsArch] Vendors: [String] Score: Int + NewestTag: ImageSummary } # Currently the same as LayerInfo, we can refactor later @@ -1392,6 +1401,8 @@ func (ec *executionContext) fieldContext_GlobalSearchResult_Repos(ctx context.Co return ec.fieldContext_RepoSummary_Vendors(ctx, field) case "Score": return ec.fieldContext_RepoSummary_Score(ctx, field) + case "NewestTag": + return ec.fieldContext_RepoSummary_NewestTag(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type RepoSummary", field.Name) }, @@ -3740,6 +3751,65 @@ func (ec *executionContext) fieldContext_RepoSummary_Score(ctx context.Context, return fc, nil } +func (ec *executionContext) _RepoSummary_NewestTag(ctx context.Context, field graphql.CollectedField, obj *RepoSummary) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_RepoSummary_NewestTag(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.NewestTag, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*ImageSummary) + fc.Result = res + return ec.marshalOImageSummary2ᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐImageSummary(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_RepoSummary_NewestTag(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "RepoSummary", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "RepoName": + return ec.fieldContext_ImageSummary_RepoName(ctx, field) + case "Tag": + return ec.fieldContext_ImageSummary_Tag(ctx, field) + case "LastUpdated": + return ec.fieldContext_ImageSummary_LastUpdated(ctx, field) + case "IsSigned": + return ec.fieldContext_ImageSummary_IsSigned(ctx, field) + case "Size": + return ec.fieldContext_ImageSummary_Size(ctx, field) + case "Platform": + return ec.fieldContext_ImageSummary_Platform(ctx, field) + case "Vendor": + return ec.fieldContext_ImageSummary_Vendor(ctx, field) + case "Score": + return ec.fieldContext_ImageSummary_Score(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type ImageSummary", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _TagInfo_Name(ctx context.Context, field graphql.CollectedField, obj *TagInfo) (ret graphql.Marshaler) { fc, err := ec.fieldContext_TagInfo_Name(ctx, field) if err != nil { @@ -6338,6 +6408,10 @@ func (ec *executionContext) _RepoSummary(ctx context.Context, sel ast.SelectionS out.Values[i] = ec._RepoSummary_Score(ctx, field, obj) + case "NewestTag": + + out.Values[i] = ec._RepoSummary_NewestTag(ctx, field, obj) + default: panic("unknown field " + strconv.Quote(field.Name)) } diff --git a/pkg/extensions/search/gql_generated/models_gen.go b/pkg/extensions/search/gql_generated/models_gen.go index fc637222..dd80da46 100644 --- a/pkg/extensions/search/gql_generated/models_gen.go +++ b/pkg/extensions/search/gql_generated/models_gen.go @@ -95,12 +95,13 @@ type RepoInfo struct { } type RepoSummary struct { - Name *string `json:"Name"` - LastUpdated *time.Time `json:"LastUpdated"` - Size *string `json:"Size"` - Platforms []*OsArch `json:"Platforms"` - Vendors []*string `json:"Vendors"` - Score *int `json:"Score"` + Name *string `json:"Name"` + LastUpdated *time.Time `json:"LastUpdated"` + Size *string `json:"Size"` + Platforms []*OsArch `json:"Platforms"` + Vendors []*string `json:"Vendors"` + Score *int `json:"Score"` + NewestTag *ImageSummary `json:"NewestTag"` } type TagInfo struct { diff --git a/pkg/extensions/search/resolver.go b/pkg/extensions/search/resolver.go index 6b21939b..38874ce8 100644 --- a/pkg/extensions/search/resolver.go +++ b/pkg/extensions/search/resolver.go @@ -211,9 +211,9 @@ func globalSearch(repoList []string, name, tag string, olu common.OciLayoutUtils // made up of all manifests, configs and image layers repoSize := int64(0) - lastUpdate, err := olu.GetRepoLastUpdated(repo) + lastUpdatedTag, err := olu.GetRepoLastUpdated(repo) if err != nil { - log.Error().Err(err).Msgf("can't find latest update timestamp for repo: %s", repo) + log.Error().Err(err).Msgf("can't find latest updated tag for repo: %s", repo) } tagsInfo, err := olu.GetImageTagsWithTimestamp(repo) @@ -230,6 +230,8 @@ func globalSearch(repoList []string, name, tag string, olu common.OciLayoutUtils continue } + var lastUpdatedImageSummary gql_generated.ImageSummary + repoPlatforms := make([]*gql_generated.OsArch, 0, len(tagsInfo)) repoVendors := make([]*string, 0, len(repoInfo.Manifests)) @@ -307,7 +309,7 @@ func globalSearch(repoList []string, name, tag string, olu common.OciLayoutUtils repoPlatforms = append(repoPlatforms, osArch) repoVendors = append(repoVendors, &vendor) - images = append(images, &gql_generated.ImageSummary{ + imageSummary := gql_generated.ImageSummary{ RepoName: &repo, Tag: &tag, LastUpdated: &lastUpdated, @@ -316,7 +318,13 @@ func globalSearch(repoList []string, name, tag string, olu common.OciLayoutUtils Platform: osArch, Vendor: &vendor, Score: &score, - }) + } + + if tagsInfo[i].Digest == lastUpdatedTag.Digest { + lastUpdatedImageSummary = imageSummary + } + + images = append(images, &imageSummary) } } @@ -329,11 +337,12 @@ func globalSearch(repoList []string, name, tag string, olu common.OciLayoutUtils repos = append(repos, &gql_generated.RepoSummary{ Name: &repo, - LastUpdated: &lastUpdate, + LastUpdated: &lastUpdatedTag.Timestamp, Size: &repoSize, Platforms: repoPlatforms, Vendors: repoVendors, Score: &index, + NewestTag: &lastUpdatedImageSummary, }) } } diff --git a/pkg/extensions/search/resolver_test.go b/pkg/extensions/search/resolver_test.go index cc5dd6fa..b094e918 100644 --- a/pkg/extensions/search/resolver_test.go +++ b/pkg/extensions/search/resolver_test.go @@ -4,7 +4,6 @@ import ( "errors" "strings" "testing" - "time" godigest "github.com/opencontainers/go-digest" . "github.com/smartystreets/goconvey/convey" @@ -19,8 +18,8 @@ func TestGlobalSearch(t *testing.T) { Convey("globalSearch", t, func() { Convey("GetRepoLastUpdated fail", func() { mockOlum := mocks.OciLayoutUtilsMock{ - GetRepoLastUpdatedFn: func(repo string) (time.Time, error) { - return time.Time{}, ErrTestError + GetRepoLastUpdatedFn: func(repo string) (common.TagInfo, error) { + return common.TagInfo{}, ErrTestError }, } diff --git a/pkg/extensions/search/schema.graphql b/pkg/extensions/search/schema.graphql index 9db10c4b..4a223b44 100644 --- a/pkg/extensions/search/schema.graphql +++ b/pkg/extensions/search/schema.graphql @@ -95,6 +95,7 @@ type RepoSummary { Platforms: [OsArch] Vendors: [String] Score: Int + NewestTag: ImageSummary } # Currently the same as LayerInfo, we can refactor later diff --git a/pkg/test/mocks/oci_mock.go b/pkg/test/mocks/oci_mock.go index 965017cd..98e6c515 100644 --- a/pkg/test/mocks/oci_mock.go +++ b/pkg/test/mocks/oci_mock.go @@ -20,7 +20,7 @@ type OciLayoutUtilsMock struct { GetImageVendorFn func(imageInfo ispec.Image) string GetImageManifestSizeFn func(repo string, manifestDigest godigest.Digest) int64 GetImageConfigSizeFn func(repo string, manifestDigest godigest.Digest) int64 - GetRepoLastUpdatedFn func(repo string) (time.Time, error) + GetRepoLastUpdatedFn func(repo string) (common.TagInfo, error) GetExpandedRepoInfoFn func(name string) (common.RepoInfo, error) GetImageConfigInfoFn func(repo string, manifestDigest godigest.Digest) (ispec.Image, error) } @@ -105,12 +105,12 @@ func (olum OciLayoutUtilsMock) GetImageConfigSize(repo string, manifestDigest go return 0 } -func (olum OciLayoutUtilsMock) GetRepoLastUpdated(repo string) (time.Time, error) { +func (olum OciLayoutUtilsMock) GetRepoLastUpdated(repo string) (common.TagInfo, error) { if olum.GetRepoLastUpdatedFn != nil { return olum.GetRepoLastUpdatedFn(repo) } - return time.Time{}, nil + return common.TagInfo{}, nil } func (olum OciLayoutUtilsMock) GetExpandedRepoInfo(name string) (common.RepoInfo, error) {