diff --git a/pkg/cli/image_cmd_test.go b/pkg/cli/image_cmd_test.go index cc292647..b1eb541a 100644 --- a/pkg/cli/image_cmd_test.go +++ b/pkg/cli/image_cmd_test.go @@ -483,7 +483,7 @@ func TestDerivedImageList(t *testing.T) { cm.StartAndWait(conf.HTTP.Port) defer cm.StopServer() - err := uploadManifest(url) + err := uploadManifestDerivedBase(url) if err != nil { panic(err) } @@ -493,7 +493,7 @@ func TestDerivedImageList(t *testing.T) { Convey("Test from real server", t, func() { Convey("Test derived images list working", func() { t.Logf("%s", ctlr.Config.Storage.RootDirectory) - args := []string{"imagetest", "--derived-images", "repo7:test:1.0"} + args := []string{"imagetest", "--derived-images", "repo7:test:2.0"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url)) defer os.Remove(configPath) cmd := NewImageCommand(new(searchService)) @@ -507,19 +507,11 @@ func TestDerivedImageList(t *testing.T) { str := space.ReplaceAllString(buff.String(), " ") actual := strings.TrimSpace(str) So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST SIGNED SIZE") - So(actual, ShouldContainSubstring, "repo7 test:2.0 883fc0c5 false 492B") + So(actual, ShouldContainSubstring, "repo7 test:1.0 2694fdb0 false 824B") }) - Convey("Test derived images fail", func() { - t.Logf("%s", ctlr.Config.Storage.RootDirectory) - err = os.Chmod(ctlr.Config.Storage.RootDirectory, 0o000) - So(err, ShouldBeNil) - - defer func() { - err := os.Chmod(ctlr.Config.Storage.RootDirectory, 0o755) - So(err, ShouldBeNil) - }() - args := []string{"imagetest", "--derived-images", "repo7:test:1.0"} + Convey("Test derived images list fails", func() { + args := []string{"imagetest", "--derived-images", "repo7:test:missing"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url)) defer os.Remove(configPath) cmd := NewImageCommand(new(searchService)) @@ -533,7 +525,7 @@ func TestDerivedImageList(t *testing.T) { Convey("Test derived images list cannot print", func() { t.Logf("%s", ctlr.Config.Storage.RootDirectory) - args := []string{"imagetest", "--derived-images", "repo7:test:1.0", "-o", "random"} + args := []string{"imagetest", "--derived-images", "repo7:test:2.0", "-o", "random"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url)) defer os.Remove(configPath) cmd := NewImageCommand(new(searchService)) @@ -564,7 +556,7 @@ func TestBaseImageList(t *testing.T) { cm.StartAndWait(conf.HTTP.Port) defer cm.StopServer() - err := uploadManifest(url) + err := uploadManifestDerivedBase(url) if err != nil { panic(err) } @@ -588,19 +580,11 @@ func TestBaseImageList(t *testing.T) { str := space.ReplaceAllString(buff.String(), " ") actual := strings.TrimSpace(str) So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST SIGNED SIZE") - So(actual, ShouldContainSubstring, "repo7 test:2.0 883fc0c5 false 492B") + So(actual, ShouldContainSubstring, "repo7 test:2.0 3fc80493 false 494B") }) - Convey("Test base images fail", func() { - t.Logf("%s", ctlr.Config.Storage.RootDirectory) - err = os.Chmod(ctlr.Config.Storage.RootDirectory, 0o000) - So(err, ShouldBeNil) - - defer func() { - err := os.Chmod(ctlr.Config.Storage.RootDirectory, 0o755) - So(err, ShouldBeNil) - }() - args := []string{"imagetest", "--base-images", "repo7:test:1.0"} + Convey("Test base images list fail", func() { + args := []string{"imagetest", "--base-images", "repo7:test:missing"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url)) defer os.Remove(configPath) cmd := NewImageCommand(new(searchService)) @@ -1481,6 +1465,98 @@ func uploadManifest(url string) error { return nil } +func uploadManifestDerivedBase(url string) error { + // create a blob/layer + _, _ = resty.R().Post(url + "/v2/repo7/blobs/uploads/") + + content1 := []byte("this is a blob5.0") + content2 := []byte("this is a blob5.1") + content3 := []byte("this is a blob5.2") + digest1 := godigest.FromBytes(content1) + digest2 := godigest.FromBytes(content2) + digest3 := godigest.FromBytes(content3) + _, _ = resty.R().SetQueryParam("digest", digest1.String()). + SetHeader("Content-Type", "application/octet-stream").SetBody(content1).Post(url + "/v2/repo7/blobs/uploads/") + _, _ = resty.R().SetQueryParam("digest", digest2.String()). + SetHeader("Content-Type", "application/octet-stream").SetBody(content2).Post(url + "/v2/repo7/blobs/uploads/") + _, _ = resty.R().SetQueryParam("digest", digest3.String()). + SetHeader("Content-Type", "application/octet-stream").SetBody(content3).Post(url + "/v2/repo7/blobs/uploads/") + + // upload image config blob + resp, _ := resty.R().Post(url + "/v2/repo7/blobs/uploads/") + loc := test.Location(url, resp) + cblob, cdigest := test.GetImageConfig() + + _, _ = resty.R(). + SetContentLength(true). + SetHeader("Content-Length", fmt.Sprintf("%d", len(cblob))). + SetHeader("Content-Type", "application/octet-stream"). + SetQueryParam("digest", cdigest.String()). + SetBody(cblob). + Put(loc) + + // create a manifest + manifest := ispec.Manifest{ + Config: ispec.Descriptor{ + MediaType: "application/vnd.oci.image.config.v1+json", + Digest: cdigest, + Size: int64(len(cblob)), + }, + Layers: []ispec.Descriptor{ + { + MediaType: "application/vnd.oci.image.layer.v1.tar", + Digest: digest1, + Size: int64(len(content1)), + }, { + MediaType: "application/vnd.oci.image.layer.v1.tar", + Digest: digest2, + Size: int64(len(content2)), + }, { + MediaType: "application/vnd.oci.image.layer.v1.tar", + Digest: digest3, + Size: int64(len(content3)), + }, + }, + } + manifest.SchemaVersion = 2 + + content, err := json.Marshal(manifest) + if err != nil { + return err + } + + _, _ = resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json"). + SetBody(content).Put(url + "/v2/repo7/manifests/test:1.0") + + content1 = []byte("this is a blob5.0") + digest1 = godigest.FromBytes(content1) + // create a manifest with one common layer blob + manifest = ispec.Manifest{ + Config: ispec.Descriptor{ + MediaType: "application/vnd.oci.image.config.v1+json", + Digest: cdigest, + Size: int64(len(cblob)), + }, + Layers: []ispec.Descriptor{ + { + MediaType: "application/vnd.oci.image.layer.v1.tar", + Digest: digest1, + Size: int64(len(content1)), + }, + }, + } + manifest.SchemaVersion = 2 + + content, err = json.Marshal(manifest) + if err != nil { + return err + } + _, _ = resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json"). + SetBody(content).Put(url + "/v2/repo7/manifests/test:2.0") + + return nil +} + type mockService struct{} func (service mockService) getRepos(ctx context.Context, config searchConfig, username, @@ -1501,7 +1577,7 @@ func (service mockService) getDerivedImageListGQL(ctx context.Context, config se derivedImage string, ) (*imageListStructForDerivedImagesGQL, error) { imageListGQLResponse := &imageListStructForDerivedImagesGQL{} - imageListGQLResponse.Data.ImageList = []imageStruct{ + imageListGQLResponse.Data.ImageList.Results = []imageStruct{ { RepoName: "dummyImageName", Tag: "tag", @@ -1519,7 +1595,7 @@ func (service mockService) getBaseImageListGQL(ctx context.Context, config searc derivedImage string, ) (*imageListStructForBaseImagesGQL, error) { imageListGQLResponse := &imageListStructForBaseImagesGQL{} - imageListGQLResponse.Data.ImageList = []imageStruct{ + imageListGQLResponse.Data.ImageList.Results = []imageStruct{ { RepoName: "dummyImageName", Tag: "tag", diff --git a/pkg/cli/searcher.go b/pkg/cli/searcher.go index 4486b466..914eac62 100644 --- a/pkg/cli/searcher.go +++ b/pkg/cli/searcher.go @@ -241,7 +241,7 @@ func (search derivedImageListSearcherGQL) search(config searchConfig) (bool, err return true, err } - if err := printResult(config, imageList.Data.ImageList); err != nil { + if err := printResult(config, imageList.Data.ImageList.Results); err != nil { return true, err } @@ -266,7 +266,7 @@ func (search baseImageListSearcherGQL) search(config searchConfig) (bool, error) return true, err } - if err := printResult(config, imageList.Data.ImageList); err != nil { + if err := printResult(config, imageList.Data.ImageList.Results); err != nil { return true, err } diff --git a/pkg/cli/service.go b/pkg/cli/service.go index f79d3bf8..4c919b33 100644 --- a/pkg/cli/service.go +++ b/pkg/cli/service.go @@ -71,14 +71,16 @@ func (service searchService) getDerivedImageListGQL(ctx context.Context, config query := fmt.Sprintf(` { DerivedImageList(image:"%s"){ - RepoName, - Tag, - Digest, - ConfigDigest, - Layers {Size Digest}, - LastUpdated, - IsSigned, - Size + Results{ + RepoName, + Tag, + Digest, + ConfigDigest, + Layers {Size Digest}, + LastUpdated, + IsSigned, + Size + } } }`, derivedImage) @@ -98,14 +100,16 @@ func (service searchService) getBaseImageListGQL(ctx context.Context, config sea query := fmt.Sprintf(` { BaseImageList(image:"%s"){ - RepoName, - Tag, - Digest, - ConfigDigest, - Layers {Size Digest}, - LastUpdated, - IsSigned, - Size + Results{ + RepoName, + Tag, + Digest, + ConfigDigest, + Layers {Size Digest}, + LastUpdated, + IsSigned, + Size + } } }`, baseImage) @@ -862,6 +866,13 @@ type imageStruct struct { IsSigned bool `json:"isSigned"` } +type DerivedImageList struct { + Results []imageStruct `json:"results"` +} +type BaseImageList struct { + Results []imageStruct `json:"results"` +} + type imageListStructGQL struct { Errors []errorGraphQL `json:"errors"` Data struct { @@ -879,14 +890,14 @@ type imageListStructForDigestGQL struct { type imageListStructForDerivedImagesGQL struct { Errors []errorGraphQL `json:"errors"` Data struct { - ImageList []imageStruct `json:"DerivedImageList"` //nolint:tagliatelle + ImageList DerivedImageList `json:"DerivedImageList"` //nolint:tagliatelle } `json:"data"` } type imageListStructForBaseImagesGQL struct { Errors []errorGraphQL `json:"errors"` Data struct { - ImageList []imageStruct `json:"BaseImageList"` //nolint:tagliatelle + ImageList BaseImageList `json:"BaseImageList"` //nolint:tagliatelle } `json:"data"` } diff --git a/pkg/extensions/search/common/common_test.go b/pkg/extensions/search/common/common_test.go index 6fcf8729..6f49d4ad 100644 --- a/pkg/extensions/search/common/common_test.go +++ b/pkg/extensions/search/common/common_test.go @@ -127,6 +127,11 @@ type PaginatedReposResult struct { Page repodb.PageInfo `json:"page"` } +type PaginatedImagesResult struct { + Results []common.ImageSummary `json:"results"` + Page repodb.PageInfo `json:"page"` +} + //nolint:tagliatelle // graphQL schema type RepoListWithNewestImage struct { PaginatedReposResult `json:"RepoListWithNewestImage"` @@ -1504,6 +1509,7 @@ func TestDerivedImageList(t *testing.T) { {10, 10, 10, 10}, {10, 10, 10, 11}, {11, 11, 10, 10}, + {11, 10, 10, 10}, } manifest = ispec.Manifest{ @@ -1558,39 +1564,129 @@ func TestDerivedImageList(t *testing.T) { ) So(err, ShouldBeNil) - query := ` - { - DerivedImageList(image:"test-repo:latest"){ - RepoName, - Tag, - Digest, - ConfigDigest, - LastUpdated, - IsSigned, - Size - } - }` + manifest = ispec.Manifest{ + Versioned: specs.Versioned{ + SchemaVersion: 2, + }, + Config: ispec.Descriptor{ + MediaType: "application/vnd.oci.image.config.v1+json", + Digest: configDigest, + Size: int64(len(configBlob)), + }, + Layers: []ispec.Descriptor{ + { + MediaType: "application/vnd.oci.image.layer.v1.tar", + Digest: godigest.FromBytes(layers[0]), + Size: int64(len(layers[0])), + }, + { + MediaType: "application/vnd.oci.image.layer.v1.tar", + Digest: godigest.FromBytes(layers[1]), + Size: int64(len(layers[1])), + }, + { + MediaType: "application/vnd.oci.image.layer.v1.tar", + Digest: godigest.FromBytes(layers[2]), + Size: int64(len(layers[2])), + }, + { + MediaType: "application/vnd.oci.image.layer.v1.tar", + Digest: godigest.FromBytes(layers[3]), + Size: int64(len(layers[3])), + }, + { + MediaType: "application/vnd.oci.image.layer.v1.tar", + Digest: godigest.FromBytes(layers[4]), + Size: int64(len(layers[4])), + }, + { + MediaType: "application/vnd.oci.image.layer.v1.tar", + Digest: godigest.FromBytes(layers[5]), + Size: int64(len(layers[5])), + }, + }, + } - resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) - So(resp, ShouldNotBeNil) - So(strings.Contains(string(resp.Body()), "same-layers"), ShouldBeTrue) //nolint:goconst - So(strings.Contains(string(resp.Body()), "missing-layers"), ShouldBeFalse) - So(strings.Contains(string(resp.Body()), "more-layers"), ShouldBeTrue) + repoName = "all-layers" + + err = UploadImage( + Image{ + Manifest: manifest, + Config: config, + Layers: layers, + Tag: "latest", + }, + baseURL, + repoName, + ) So(err, ShouldBeNil) - So(resp.StatusCode(), ShouldEqual, 200) + + Convey("non paginated query", func() { + query := ` + { + DerivedImageList(image:"test-repo:latest"){ + Results{ + RepoName, + Tag, + Digest, + ConfigDigest, + LastUpdated, + IsSigned, + Size + } + } + }` + + resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) + So(resp, ShouldNotBeNil) + So(strings.Contains(string(resp.Body()), "same-layers"), ShouldBeFalse) //nolint:goconst + So(strings.Contains(string(resp.Body()), "missing-layers"), ShouldBeFalse) + So(strings.Contains(string(resp.Body()), "more-layers"), ShouldBeTrue) + So(strings.Contains(string(resp.Body()), "all-layers"), ShouldBeTrue) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + }) + + Convey("paginated query", func() { + query := ` + { + DerivedImageList(image:"test-repo:latest", requestedPage:{limit: 1, offset: 0, sortBy:ALPHABETIC_ASC}){ + Results{ + RepoName, + Tag, + Digest, + ConfigDigest, + LastUpdated, + IsSigned, + Size + } + } + }` + + resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) + So(resp, ShouldNotBeNil) + So(strings.Contains(string(resp.Body()), "same-layers"), ShouldBeFalse) //nolint:goconst + So(strings.Contains(string(resp.Body()), "missing-layers"), ShouldBeFalse) + So(strings.Contains(string(resp.Body()), "more-layers"), ShouldBeFalse) + So(strings.Contains(string(resp.Body()), "all-layers"), ShouldBeTrue) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + }) }) Convey("Inexistent repository", t, func() { query := ` { DerivedImageList(image:"inexistent-image:latest"){ - RepoName, - Tag, - Digest, - ConfigDigest, - LastUpdated, - IsSigned, - Size + Results { + RepoName, + Tag, + Digest, + ConfigDigest, + LastUpdated, + IsSigned, + Size + } } }` @@ -1603,13 +1699,15 @@ func TestDerivedImageList(t *testing.T) { query := ` { DerivedImageList(image:"inexistent-image"){ - RepoName, - Tag, - Digest, - ConfigDigest, - LastUpdated, - IsSigned, - Size + Results { + RepoName, + Tag, + Digest, + ConfigDigest, + LastUpdated, + IsSigned, + Size + } } }` @@ -1628,28 +1726,6 @@ func TestDerivedImageList(t *testing.T) { } So(contains, ShouldBeTrue) }) - - Convey("Failed to get manifest", t, func() { - err := os.Mkdir(path.Join(rootDir, "fail-image"), 0o000) - So(err, ShouldBeNil) - - query := ` - { - DerivedImageList(image:"fail-image:latest"){ - RepoName, - Tag, - Digest, - ConfigDigest, - LastUpdated, - IsSigned, - Size - } - }` - - resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) - So(strings.Contains(string(resp.Body()), "permission denied"), ShouldBeTrue) - So(err, ShouldBeNil) - }) } //nolint:dupl @@ -1676,19 +1752,25 @@ func TestDerivedImageListNoRepos(t *testing.T) { query := ` { - DerivedImageList(image:"test-image:latest"){ - RepoName, - Tag, - Digest, - ConfigDigest, - LastUpdated, - IsSigned, - Size + DerivedImageList(image:"test-image"){ + Results{ + RepoName, + Tag, + Digest, + ConfigDigest, + LastUpdated, + IsSigned, + Size + } } }` resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) - So(strings.Contains(string(resp.Body()), "{\"data\":{\"DerivedImageList\":[]}}"), ShouldBeTrue) + So(resp, ShouldNotBeNil) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + + So(strings.Contains(string(resp.Body()), "no reference provided"), ShouldBeTrue) So(err, ShouldBeNil) }) } @@ -1915,6 +1997,43 @@ func TestBaseImageList(t *testing.T) { ) So(err, ShouldBeNil) + // create image with one layer, which is also present in the given image + layers = [][]byte{ + {10, 11, 10, 11}, + } + + manifest = ispec.Manifest{ + Versioned: specs.Versioned{ + SchemaVersion: 2, + }, + Config: ispec.Descriptor{ + MediaType: "application/vnd.oci.image.config.v1+json", + Digest: configDigest, + Size: int64(len(configBlob)), + }, + Layers: []ispec.Descriptor{ + { + MediaType: "application/vnd.oci.image.layer.v1.tar", + Digest: godigest.FromBytes(layers[0]), + Size: int64(len(layers[0])), + }, + }, + } + + repoName = "one-layer" + + err = UploadImage( + Image{ + Manifest: manifest, + Config: config, + Layers: layers, + Tag: "latest", + }, + baseURL, + repoName, + ) + So(err, ShouldBeNil) + // create image with less layers than the given image, but one layer isn't in the given image layers = [][]byte{ {10, 11, 10, 11}, @@ -2062,42 +2181,78 @@ func TestBaseImageList(t *testing.T) { ) So(err, ShouldBeNil) - query := ` - { - BaseImageList(image:"test-repo:latest"){ - RepoName, - Tag, - Digest, - ConfigDigest, - LastUpdated, - IsSigned, - Size - } - }` + Convey("non paginated query", func() { + query := ` + { + BaseImageList(image:"test-repo:latest"){ + Results{ + RepoName, + Tag, + Digest, + ConfigDigest, + LastUpdated, + IsSigned, + Size + } + } + }` - resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) - So(resp, ShouldNotBeNil) - So(strings.Contains(string(resp.Body()), "same-layers"), ShouldBeTrue) //nolint:goconst - So(strings.Contains(string(resp.Body()), "less-layers"), ShouldBeTrue) - So(strings.Contains(string(resp.Body()), "less-layers-false"), ShouldBeFalse) - So(strings.Contains(string(resp.Body()), "more-layers"), ShouldBeFalse) - So(strings.Contains(string(resp.Body()), "diff-layers"), ShouldBeFalse) - So(strings.Contains(string(resp.Body()), "test-repo"), ShouldBeFalse) //nolint:goconst // should not list given image - So(err, ShouldBeNil) - So(resp.StatusCode(), ShouldEqual, 200) + resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) + So(resp, ShouldNotBeNil) + So(strings.Contains(string(resp.Body()), "less-layers"), ShouldBeTrue) + So(strings.Contains(string(resp.Body()), "one-layer"), ShouldBeTrue) + So(strings.Contains(string(resp.Body()), "same-layers"), ShouldBeFalse) //nolint:goconst + So(strings.Contains(string(resp.Body()), "less-layers-false"), ShouldBeFalse) + So(strings.Contains(string(resp.Body()), "more-layers"), ShouldBeFalse) + So(strings.Contains(string(resp.Body()), "diff-layers"), ShouldBeFalse) + So(strings.Contains(string(resp.Body()), "test-repo"), ShouldBeFalse) //nolint:goconst // should not list given image + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + }) + + Convey("paginated query", func() { + query := ` + { + BaseImageList(image:"test-repo:latest", requestedPage:{limit: 1, offset: 0, sortBy:RELEVANCE}){ + Results{ + RepoName, + Tag, + Digest, + ConfigDigest, + LastUpdated, + IsSigned, + Size + } + } + }` + + resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) + So(resp, ShouldNotBeNil) + So(strings.Contains(string(resp.Body()), "less-layers"), ShouldBeTrue) + So(strings.Contains(string(resp.Body()), "one-layer"), ShouldBeFalse) + So(strings.Contains(string(resp.Body()), "same-layers"), ShouldBeFalse) //nolint:goconst + So(strings.Contains(string(resp.Body()), "less-layers-false"), ShouldBeFalse) + So(strings.Contains(string(resp.Body()), "more-layers"), ShouldBeFalse) + So(strings.Contains(string(resp.Body()), "diff-layers"), ShouldBeFalse) + So(strings.Contains(string(resp.Body()), "test-repo"), ShouldBeFalse) //nolint:goconst // should not list given image + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + }) }) Convey("Nonexistent repository", t, func() { query := ` { BaseImageList(image:"nonexistent-image:latest"){ - RepoName, - Tag, - Digest, - ConfigDigest, - LastUpdated, - IsSigned, - Size + Results{ + RepoName, + Tag, + Digest, + ConfigDigest, + LastUpdated, + IsSigned, + Size + } } }` @@ -2110,13 +2265,15 @@ func TestBaseImageList(t *testing.T) { query := ` { BaseImageList(image:"nonexistent-image"){ - RepoName, - Tag, - Digest, - ConfigDigest, - LastUpdated, - IsSigned, - Size + Results{ + RepoName, + Tag, + Digest, + ConfigDigest, + LastUpdated, + IsSigned, + Size + } } }` @@ -2135,28 +2292,6 @@ func TestBaseImageList(t *testing.T) { } So(contains, ShouldBeTrue) }) - - Convey("Failed to get manifest", t, func() { - err := os.Mkdir(path.Join(rootDir, "fail-image"), 0o000) - So(err, ShouldBeNil) - - query := ` - { - BaseImageList(image:"fail-image:latest"){ - RepoName, - Tag, - Digest, - ConfigDigest, - LastUpdated, - IsSigned, - Size - } - }` - - resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) - So(strings.Contains(string(resp.Body()), "permission denied"), ShouldBeTrue) - So(err, ShouldBeNil) - }) } //nolint:dupl @@ -2183,19 +2318,21 @@ func TestBaseImageListNoRepos(t *testing.T) { query := ` { - BaseImageList(image:"test-image:latest"){ - RepoName, - Tag, - Digest, - ConfigDigest, - LastUpdated, - IsSigned, - Size + BaseImageList(image:"test-image"){ + Results{ + RepoName, + Tag, + Digest, + ConfigDigest, + LastUpdated, + IsSigned, + Size + } } }` resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) - So(strings.Contains(string(resp.Body()), "{\"data\":{\"BaseImageList\":[]}}"), ShouldBeTrue) + So(strings.Contains(string(resp.Body()), "no reference provided"), ShouldBeTrue) So(err, ShouldBeNil) }) } diff --git a/pkg/extensions/search/gql_generated/generated.go b/pkg/extensions/search/gql_generated/generated.go index 53605281..e2a59189 100644 --- a/pkg/extensions/search/gql_generated/generated.go +++ b/pkg/extensions/search/gql_generated/generated.go @@ -144,9 +144,9 @@ type ComplexityRoot struct { } Query struct { - BaseImageList func(childComplexity int, image string) int + BaseImageList func(childComplexity int, image string, requestedPage *PageInput) int CVEListForImage func(childComplexity int, image string, requestedPage *PageInput) int - DerivedImageList func(childComplexity int, image string) int + DerivedImageList func(childComplexity int, image string, requestedPage *PageInput) int ExpandedRepoInfo func(childComplexity int, repo string) int GlobalSearch func(childComplexity int, query string, filter *Filter, requestedPage *PageInput) int Image func(childComplexity int, image string) int @@ -195,8 +195,8 @@ type QueryResolver interface { ImageList(ctx context.Context, repo string, requestedPage *PageInput) ([]*ImageSummary, error) ExpandedRepoInfo(ctx context.Context, repo string) (*RepoInfo, error) GlobalSearch(ctx context.Context, query string, filter *Filter, requestedPage *PageInput) (*GlobalSearchResult, error) - DerivedImageList(ctx context.Context, image string) ([]*ImageSummary, error) - BaseImageList(ctx context.Context, image string) ([]*ImageSummary, error) + DerivedImageList(ctx context.Context, image string, requestedPage *PageInput) (*PaginatedImagesResult, error) + BaseImageList(ctx context.Context, image string, requestedPage *PageInput) (*PaginatedImagesResult, error) Image(ctx context.Context, image string) (*ImageSummary, error) Referrers(ctx context.Context, repo string, digest string, typeArg string) ([]*Referrer, error) } @@ -632,7 +632,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return 0, false } - return e.complexity.Query.BaseImageList(childComplexity, args["image"].(string)), true + return e.complexity.Query.BaseImageList(childComplexity, args["image"].(string), args["requestedPage"].(*PageInput)), true case "Query.CVEListForImage": if e.complexity.Query.CVEListForImage == nil { @@ -656,7 +656,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return 0, false } - return e.complexity.Query.DerivedImageList(childComplexity, args["image"].(string)), true + return e.complexity.Query.DerivedImageList(childComplexity, args["image"].(string), args["requestedPage"].(*PageInput)), true case "Query.ExpandedRepoInfo": if e.complexity.Query.ExpandedRepoInfo == nil { @@ -1193,12 +1193,12 @@ type Query { """ List of images which use the argument image """ - DerivedImageList(image: String!): [ImageSummary!] + DerivedImageList(image: String!, requestedPage: PageInput): PaginatedImagesResult! """ List of images on which the argument image depends on """ - BaseImageList(image: String!): [ImageSummary!] + BaseImageList(image: String!, requestedPage: PageInput): PaginatedImagesResult! """ Search for a specific image using its name @@ -1231,6 +1231,15 @@ func (ec *executionContext) field_Query_BaseImageList_args(ctx context.Context, } } args["image"] = arg0 + var arg1 *PageInput + if tmp, ok := rawArgs["requestedPage"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("requestedPage")) + arg1, err = ec.unmarshalOPageInput2ᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐPageInput(ctx, tmp) + if err != nil { + return nil, err + } + } + args["requestedPage"] = arg1 return args, nil } @@ -1270,6 +1279,15 @@ func (ec *executionContext) field_Query_DerivedImageList_args(ctx context.Contex } } args["image"] = arg0 + var arg1 *PageInput + if tmp, ok := rawArgs["requestedPage"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("requestedPage")) + arg1, err = ec.unmarshalOPageInput2ᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐPageInput(ctx, tmp) + if err != nil { + return nil, err + } + } + args["requestedPage"] = arg1 return args, nil } @@ -4814,18 +4832,21 @@ func (ec *executionContext) _Query_DerivedImageList(ctx context.Context, field g }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Query().DerivedImageList(rctx, fc.Args["image"].(string)) + return ec.resolvers.Query().DerivedImageList(rctx, fc.Args["image"].(string), fc.Args["requestedPage"].(*PageInput)) }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } return graphql.Null } - res := resTmp.([]*ImageSummary) + res := resTmp.(*PaginatedImagesResult) fc.Result = res - return ec.marshalOImageSummary2ᚕᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐImageSummaryᚄ(ctx, field.Selections, res) + return ec.marshalNPaginatedImagesResult2ᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐPaginatedImagesResult(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_Query_DerivedImageList(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -4836,50 +4857,12 @@ func (ec *executionContext) fieldContext_Query_DerivedImageList(ctx context.Cont IsResolver: true, 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 "Digest": - return ec.fieldContext_ImageSummary_Digest(ctx, field) - case "ConfigDigest": - return ec.fieldContext_ImageSummary_ConfigDigest(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) - case "DownloadCount": - return ec.fieldContext_ImageSummary_DownloadCount(ctx, field) - case "Layers": - return ec.fieldContext_ImageSummary_Layers(ctx, field) - case "Description": - return ec.fieldContext_ImageSummary_Description(ctx, field) - case "Licenses": - return ec.fieldContext_ImageSummary_Licenses(ctx, field) - case "Labels": - return ec.fieldContext_ImageSummary_Labels(ctx, field) - case "Title": - return ec.fieldContext_ImageSummary_Title(ctx, field) - case "Source": - return ec.fieldContext_ImageSummary_Source(ctx, field) - case "Documentation": - return ec.fieldContext_ImageSummary_Documentation(ctx, field) - case "History": - return ec.fieldContext_ImageSummary_History(ctx, field) - case "Vulnerabilities": - return ec.fieldContext_ImageSummary_Vulnerabilities(ctx, field) - case "Authors": - return ec.fieldContext_ImageSummary_Authors(ctx, field) + case "Page": + return ec.fieldContext_PaginatedImagesResult_Page(ctx, field) + case "Results": + return ec.fieldContext_PaginatedImagesResult_Results(ctx, field) } - return nil, fmt.Errorf("no field named %q was found under type ImageSummary", field.Name) + return nil, fmt.Errorf("no field named %q was found under type PaginatedImagesResult", field.Name) }, } defer func() { @@ -4910,18 +4893,21 @@ func (ec *executionContext) _Query_BaseImageList(ctx context.Context, field grap }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Query().BaseImageList(rctx, fc.Args["image"].(string)) + return ec.resolvers.Query().BaseImageList(rctx, fc.Args["image"].(string), fc.Args["requestedPage"].(*PageInput)) }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } return graphql.Null } - res := resTmp.([]*ImageSummary) + res := resTmp.(*PaginatedImagesResult) fc.Result = res - return ec.marshalOImageSummary2ᚕᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐImageSummaryᚄ(ctx, field.Selections, res) + return ec.marshalNPaginatedImagesResult2ᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐPaginatedImagesResult(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_Query_BaseImageList(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -4932,50 +4918,12 @@ func (ec *executionContext) fieldContext_Query_BaseImageList(ctx context.Context IsResolver: true, 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 "Digest": - return ec.fieldContext_ImageSummary_Digest(ctx, field) - case "ConfigDigest": - return ec.fieldContext_ImageSummary_ConfigDigest(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) - case "DownloadCount": - return ec.fieldContext_ImageSummary_DownloadCount(ctx, field) - case "Layers": - return ec.fieldContext_ImageSummary_Layers(ctx, field) - case "Description": - return ec.fieldContext_ImageSummary_Description(ctx, field) - case "Licenses": - return ec.fieldContext_ImageSummary_Licenses(ctx, field) - case "Labels": - return ec.fieldContext_ImageSummary_Labels(ctx, field) - case "Title": - return ec.fieldContext_ImageSummary_Title(ctx, field) - case "Source": - return ec.fieldContext_ImageSummary_Source(ctx, field) - case "Documentation": - return ec.fieldContext_ImageSummary_Documentation(ctx, field) - case "History": - return ec.fieldContext_ImageSummary_History(ctx, field) - case "Vulnerabilities": - return ec.fieldContext_ImageSummary_Vulnerabilities(ctx, field) - case "Authors": - return ec.fieldContext_ImageSummary_Authors(ctx, field) + case "Page": + return ec.fieldContext_PaginatedImagesResult_Page(ctx, field) + case "Results": + return ec.fieldContext_PaginatedImagesResult_Results(ctx, field) } - return nil, fmt.Errorf("no field named %q was found under type ImageSummary", field.Name) + return nil, fmt.Errorf("no field named %q was found under type PaginatedImagesResult", field.Name) }, } defer func() { @@ -8757,6 +8705,9 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr } }() res = ec._Query_DerivedImageList(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&invalids, 1) + } return res } @@ -8777,6 +8728,9 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr } }() res = ec._Query_BaseImageList(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&invalids, 1) + } return res } @@ -9459,6 +9413,20 @@ func (ec *executionContext) marshalNInt2int(ctx context.Context, sel ast.Selecti return res } +func (ec *executionContext) marshalNPaginatedImagesResult2zotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐPaginatedImagesResult(ctx context.Context, sel ast.SelectionSet, v PaginatedImagesResult) graphql.Marshaler { + return ec._PaginatedImagesResult(ctx, sel, &v) +} + +func (ec *executionContext) marshalNPaginatedImagesResult2ᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐPaginatedImagesResult(ctx context.Context, sel ast.SelectionSet, v *PaginatedImagesResult) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._PaginatedImagesResult(ctx, sel, v) +} + func (ec *executionContext) marshalNPaginatedReposResult2zotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐPaginatedReposResult(ctx context.Context, sel ast.SelectionSet, v PaginatedReposResult) graphql.Marshaler { return ec._PaginatedReposResult(ctx, sel, &v) } diff --git a/pkg/extensions/search/resolver.go b/pkg/extensions/search/resolver.go index c03fe817..e8def46d 100644 --- a/pkg/extensions/search/resolver.go +++ b/pkg/extensions/search/resolver.go @@ -137,7 +137,7 @@ func getImageListForDigest(ctx context.Context, digest string, repoDB repodb.Rep } // get all repos - reposMeta, manifestMetaMap, err := repoDB.FilterTags(ctx, FilterByDigest(digest), pageInput) + reposMeta, manifestMetaMap, _, err := repoDB.FilterTags(ctx, FilterByDigest(digest), pageInput) if err != nil { return []*gql_generated.ImageSummary{}, err } @@ -336,7 +336,7 @@ func getImageListForCVE( } // get all repos - reposMeta, manifestMetaMap, err := repoDB.FilterTags(ctx, FilterByTagInfo(affectedImages), pageInput) + reposMeta, manifestMetaMap, _, err := repoDB.FilterTags(ctx, FilterByTagInfo(affectedImages), pageInput) if err != nil { return []*gql_generated.ImageSummary{}, err } @@ -388,7 +388,7 @@ func getImageListWithCVEFixed( } // get all repos - reposMeta, manifestMetaMap, err := repoDB.FilterTags(ctx, FilterByTagInfo(tagsInfo), pageInput) + reposMeta, manifestMetaMap, _, err := repoDB.FilterTags(ctx, FilterByTagInfo(tagsInfo), pageInput) if err != nil { return []*gql_generated.ImageSummary{}, err } @@ -542,6 +542,225 @@ func canSkipField(preloads map[string]bool, s string) bool { return !fieldIsPresent } +func derivedImageList(ctx context.Context, image string, repoDB repodb.RepoDB, + requestedPage *gql_generated.PageInput, + cveInfo cveinfo.CveInfo, log log.Logger, +) (*gql_generated.PaginatedImagesResult, error) { + derivedList := make([]*gql_generated.ImageSummary, 0) + + if requestedPage == nil { + requestedPage = &gql_generated.PageInput{} + } + + pageInput := repodb.PageInput{ + Limit: safeDerefferencing(requestedPage.Limit, 0), + Offset: safeDerefferencing(requestedPage.Offset, 0), + SortBy: repodb.SortCriteria( + safeDerefferencing(requestedPage.SortBy, gql_generated.SortCriteriaUpdateTime), + ), + } + + skip := convert.SkipQGLField{ + Vulnerabilities: canSkipField(convert.GetPreloads(ctx), "Vulnerabilities"), + } + + imageRepo, imageTag := common.GetImageDirAndTag(image) + if imageTag == "" { + return &gql_generated.PaginatedImagesResult{}, gqlerror.Errorf("no reference provided") + } + + searchedImage, err := getImageSummary(ctx, imageRepo, imageTag, repoDB, cveInfo, log) + if err != nil { + if errors.Is(err, zerr.ErrRepoMetaNotFound) { + return &gql_generated.PaginatedImagesResult{}, gqlerror.Errorf("repository: not found") + } + + return &gql_generated.PaginatedImagesResult{}, err + } + + // we need all available tags + reposMeta, manifestMetaMap, pageInfo, err := repoDB.FilterTags(ctx, + filterDerivedImages(searchedImage), + pageInput) + if err != nil { + return &gql_generated.PaginatedImagesResult{}, err + } + + for _, repoMeta := range reposMeta { + summary := convert.RepoMeta2ImageSummaries(ctx, repoMeta, manifestMetaMap, skip, cveInfo) + derivedList = append(derivedList, summary...) + } + + if len(derivedList) == 0 { + log.Info().Msg("no images found") + + return &gql_generated.PaginatedImagesResult{ + Page: &gql_generated.PageInfo{}, + Results: derivedList, + }, nil + } + + return &gql_generated.PaginatedImagesResult{ + Results: derivedList, + Page: &gql_generated.PageInfo{ + TotalCount: pageInfo.TotalCount, + ItemCount: pageInfo.ItemCount, + }, + }, nil +} + +func filterDerivedImages(image *gql_generated.ImageSummary) repodb.FilterFunc { + return func(repoMeta repodb.RepoMetadata, manifestMeta repodb.ManifestMetadata) bool { + var addImageToList bool + + var imageManifest ispec.Manifest + + err := json.Unmarshal(manifestMeta.ManifestBlob, &imageManifest) + if err != nil { + return false + } + + manifestDigest := godigest.FromBytes(manifestMeta.ManifestBlob).String() + if manifestDigest == *image.Digest { + return false + } + + imageLayers := image.Layers + + addImageToList = false + layers := imageManifest.Layers + + sameLayer := 0 + + for _, l := range imageLayers { + for _, k := range layers { + if k.Digest.String() == *l.Digest { + sameLayer++ + } + } + } + + // if all layers are the same + if sameLayer == len(imageLayers) { + // it's a derived image + addImageToList = true + } + + return addImageToList + } +} + +func baseImageList(ctx context.Context, image string, repoDB repodb.RepoDB, + requestedPage *gql_generated.PageInput, + cveInfo cveinfo.CveInfo, log log.Logger, +) (*gql_generated.PaginatedImagesResult, error) { + imageSummaries := make([]*gql_generated.ImageSummary, 0) + + if requestedPage == nil { + requestedPage = &gql_generated.PageInput{} + } + + pageInput := repodb.PageInput{ + Limit: safeDerefferencing(requestedPage.Limit, 0), + Offset: safeDerefferencing(requestedPage.Offset, 0), + SortBy: repodb.SortCriteria( + safeDerefferencing(requestedPage.SortBy, gql_generated.SortCriteriaUpdateTime), + ), + } + + skip := convert.SkipQGLField{ + Vulnerabilities: canSkipField(convert.GetPreloads(ctx), "Vulnerabilities"), + } + + imageRepo, imageTag := common.GetImageDirAndTag(image) + + if imageTag == "" { + return &gql_generated.PaginatedImagesResult{}, gqlerror.Errorf("no reference provided") + } + + searchedImage, err := getImageSummary(ctx, imageRepo, imageTag, repoDB, cveInfo, log) + if err != nil { + if errors.Is(err, zerr.ErrRepoMetaNotFound) { + return &gql_generated.PaginatedImagesResult{}, gqlerror.Errorf("repository: not found") + } + + return &gql_generated.PaginatedImagesResult{}, err + } + + // we need all available tags + reposMeta, manifestMetaMap, pageInfo, err := repoDB.FilterTags(ctx, + filterBaseImages(searchedImage), + pageInput) + if err != nil { + return &gql_generated.PaginatedImagesResult{}, err + } + + for _, repoMeta := range reposMeta { + summary := convert.RepoMeta2ImageSummaries(ctx, repoMeta, manifestMetaMap, skip, cveInfo) + imageSummaries = append(imageSummaries, summary...) + } + + if len(imageSummaries) == 0 { + log.Info().Msg("no images found") + + return &gql_generated.PaginatedImagesResult{ + Results: imageSummaries, + Page: &gql_generated.PageInfo{}, + }, nil + } + + return &gql_generated.PaginatedImagesResult{ + Page: &gql_generated.PageInfo{ + TotalCount: pageInfo.TotalCount, + ItemCount: pageInfo.ItemCount, + }, + Results: imageSummaries, + }, nil +} + +func filterBaseImages(image *gql_generated.ImageSummary) repodb.FilterFunc { + return func(repoMeta repodb.RepoMetadata, manifestMeta repodb.ManifestMetadata) bool { + var addImageToList bool + + var imageManifest ispec.Manifest + + err := json.Unmarshal(manifestMeta.ManifestBlob, &imageManifest) + if err != nil { + return false + } + + manifestDigest := godigest.FromBytes(manifestMeta.ManifestBlob).String() + if manifestDigest == *image.Digest { + return false + } + + imageLayers := image.Layers + + addImageToList = true + layers := imageManifest.Layers + + for _, l := range layers { + foundLayer := false + + for _, k := range imageLayers { + if l.Digest.String() == *k.Digest { + foundLayer = true + + break + } + } + + if !foundLayer { + addImageToList = false + + break + } + } + + return addImageToList + } +} + func validateGlobalSearchInput(query string, filter *gql_generated.Filter, requestedPage *gql_generated.PageInput, ) error { @@ -765,7 +984,7 @@ func getImageList(ctx context.Context, repo string, repoDB repodb.RepoDB, cveInf } // reposMeta, manifestMetaMap, err := repoDB.SearchRepos(ctx, repo, repodb.Filter{}, pageInput) - reposMeta, manifestMetaMap, err := repoDB.FilterTags(ctx, + reposMeta, manifestMetaMap, _, err := repoDB.FilterTags(ctx, func(repoMeta repodb.RepoMetadata, manifestMeta repodb.ManifestMetadata) bool { return true }, diff --git a/pkg/extensions/search/resolver_test.go b/pkg/extensions/search/resolver_test.go index 429493cb..e77ea6d1 100644 --- a/pkg/extensions/search/resolver_test.go +++ b/pkg/extensions/search/resolver_test.go @@ -11,6 +11,7 @@ import ( "github.com/99designs/gqlgen/graphql" godigest "github.com/opencontainers/go-digest" + "github.com/opencontainers/image-spec/specs-go" ispec "github.com/opencontainers/image-spec/specs-go/v1" . "github.com/smartystreets/goconvey/convey" @@ -562,8 +563,8 @@ func TestImageListForDigest(t *testing.T) { mockSearchDB := mocks.RepoDBMock{ FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, requestedPage repodb.PageInput, - ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, error) { - return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, ErrTestError + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, repodb.PageInfo{}, ErrTestError }, } @@ -577,7 +578,7 @@ func TestImageListForDigest(t *testing.T) { mockSearchDB := mocks.RepoDBMock{ FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, requestedPage repodb.PageInput, - ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, error) { + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { repos := []repodb.RepoMetadata{ { Name: "test", @@ -604,7 +605,7 @@ func TestImageListForDigest(t *testing.T) { }, } - return repos, manifestMetaDatas, nil + return repos, manifestMetaDatas, repodb.PageInfo{}, nil }, } @@ -625,7 +626,7 @@ func TestImageListForDigest(t *testing.T) { mockSearchDB := mocks.RepoDBMock{ FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, requestedPage repodb.PageInput, - ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, error) { + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { repos := []repodb.RepoMetadata{ { Name: "test", @@ -658,7 +659,7 @@ func TestImageListForDigest(t *testing.T) { repos[0].Tags = matchedTags - return repos, manifestMetaDatas, nil + return repos, manifestMetaDatas, repodb.PageInfo{}, nil }, } @@ -698,7 +699,7 @@ func TestImageListForDigest(t *testing.T) { mockSearchDB := mocks.RepoDBMock{ FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, requestedPage repodb.PageInput, - ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, error) { + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { repos := []repodb.RepoMetadata{ { Name: "test", @@ -736,7 +737,7 @@ func TestImageListForDigest(t *testing.T) { repos[0].Tags = matchedTags - return repos, manifestMetaDatas, nil + return repos, manifestMetaDatas, repodb.PageInfo{}, nil }, } @@ -772,7 +773,7 @@ func TestImageListForDigest(t *testing.T) { mockSearchDB := mocks.RepoDBMock{ FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, requestedPage repodb.PageInput, - ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, error) { + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { repos := []repodb.RepoMetadata{ { Name: "test", @@ -812,7 +813,7 @@ func TestImageListForDigest(t *testing.T) { repos[0].Tags = matchedTags - return repos, manifestMetaDatas, nil + return repos, manifestMetaDatas, repodb.PageInfo{}, nil }, } @@ -846,7 +847,7 @@ func TestImageListForDigest(t *testing.T) { mockSearchDB := mocks.RepoDBMock{ FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, requestedPage repodb.PageInput, - ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, error) { + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { repos := []repodb.RepoMetadata{ { Name: "test", @@ -881,7 +882,7 @@ func TestImageListForDigest(t *testing.T) { repos[i].Tags = matchedTags } - return repos, manifestMetaDatas, nil + return repos, manifestMetaDatas, repodb.PageInfo{}, nil }, } @@ -915,10 +916,10 @@ func TestImageListForDigest(t *testing.T) { mockSearchDB := mocks.RepoDBMock{ FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, requestedPage repodb.PageInput, - ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, error) { + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { pageFinder, err := repodb.NewBaseImagePageFinder(requestedPage.Limit, requestedPage.Offset, requestedPage.SortBy) if err != nil { - return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, err + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, repodb.PageInfo{}, err } repos := []repodb.RepoMetadata{ @@ -961,7 +962,7 @@ func TestImageListForDigest(t *testing.T) { repos, _ = pageFinder.Page() - return repos, manifestMetaDatas, nil + return repos, manifestMetaDatas, repodb.PageInfo{}, nil }, } @@ -992,8 +993,8 @@ func TestImageList(t *testing.T) { mockSearchDB := mocks.RepoDBMock{ FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, requestedPage repodb.PageInput, - ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, error) { - return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, ErrTestError + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, repodb.PageInfo{}, ErrTestError }, } @@ -1008,7 +1009,7 @@ func TestImageList(t *testing.T) { mockSearchDB := mocks.RepoDBMock{ FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, requestedPage repodb.PageInput, - ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, error) { + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { repos := []repodb.RepoMetadata{ { Name: "test", @@ -1052,7 +1053,7 @@ func TestImageList(t *testing.T) { }, } - return repos, manifestMetaDatas, nil + return repos, manifestMetaDatas, repodb.PageInfo{}, nil }, } @@ -1273,6 +1274,42 @@ func TestQueryResolverErrors(t *testing.T) { log := log.NewLogger("debug", "") ctx := context.Background() + Convey("GlobalSearch error bad requested page", func() { + resolverConfig := NewResolver( + log, + storage.StoreController{}, + mocks.RepoDBMock{}, + mocks.CveInfoMock{}, + ) + + resolver := queryResolver{ + resolverConfig, + } + + limit := -1 + offset := 0 + sortCriteria := gql_generated.SortCriteriaAlphabeticAsc + pageInput := gql_generated.PageInput{ + Limit: &limit, + Offset: &offset, + SortBy: &sortCriteria, + } + + _, err := resolver.GlobalSearch(ctx, "some_string", &gql_generated.Filter{}, &pageInput) + So(err, ShouldNotBeNil) + + limit = 0 + offset = -1 + pageInput = gql_generated.PageInput{ + Limit: &limit, + Offset: &offset, + SortBy: &sortCriteria, + } + + _, err = resolver.GlobalSearch(ctx, "some_string", &gql_generated.Filter{}, &pageInput) + So(err, ShouldNotBeNil) + }) + Convey("ImageListForCve error in GetMultipleRepoMeta", func() { resolverConfig := NewResolver( log, @@ -1306,8 +1343,8 @@ func TestQueryResolverErrors(t *testing.T) { mocks.RepoDBMock{ FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, requestedPage repodb.PageInput, - ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, error) { - return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, ErrTestError + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, repodb.PageInfo{}, ErrTestError }, }, mocks.CveInfoMock{}, @@ -1330,8 +1367,8 @@ func TestQueryResolverErrors(t *testing.T) { mocks.RepoDBMock{ FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, requestedPage repodb.PageInput, - ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, error) { - return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, ErrTestError + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, repodb.PageInfo{}, ErrTestError }, }, mocks.CveInfoMock{}, @@ -1398,8 +1435,8 @@ func TestQueryResolverErrors(t *testing.T) { mocks.RepoDBMock{ FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, requestedPage repodb.PageInput, - ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, error) { - return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, ErrTestError + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, repodb.PageInfo{}, ErrTestError }, }, mocks.CveInfoMock{}, @@ -1438,7 +1475,7 @@ func TestQueryResolverErrors(t *testing.T) { resolverConfig, } - _, err := qr.DerivedImageList(ctx, "repo:tag") + _, err := qr.DerivedImageList(ctx, "repo:tag", &gql_generated.PageInput{}) So(err, ShouldNotBeNil) }) @@ -1467,7 +1504,60 @@ func TestQueryResolverErrors(t *testing.T) { resolverConfig, } - _, err := qr.BaseImageList(ctx, "repo:tag") + _, err := qr.BaseImageList(ctx, "repo:tag", &gql_generated.PageInput{}) + So(err, ShouldNotBeNil) + }) + + Convey("DerivedImageList and BaseImage List FilterTags() errors", func() { + configBlob, err := json.Marshal(ispec.Image{ + Config: ispec.ImageConfig{ + Labels: map[string]string{}, + }, + }) + So(err, ShouldBeNil) + + manifest := ispec.Manifest{} + + manifestBlob, err := json.Marshal(manifest) + So(err, ShouldBeNil) + + manifestDigest := godigest.FromBytes(manifestBlob) + + resolverConfig := NewResolver( + log, + storage.StoreController{}, + mocks.RepoDBMock{ + FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, + requestedPage repodb.PageInput, + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, repodb.PageInfo{}, ErrTestError + }, + GetRepoMetaFn: func(repo string) (repodb.RepoMetadata, error) { + return repodb.RepoMetadata{ + Name: "repo", + Tags: map[string]repodb.Descriptor{ + "tag": {Digest: manifestDigest.String(), MediaType: ispec.MediaTypeImageManifest}, + }, + }, nil + }, + GetManifestMetaFn: func(repo string, manifestDigest godigest.Digest) (repodb.ManifestMetadata, error) { + return repodb.ManifestMetadata{ + ManifestBlob: manifestBlob, + ConfigBlob: configBlob, + }, nil + }, + }, + mocks.CveInfoMock{}, + ) + + resolver := queryResolver{ + resolverConfig, + } + + _, err = resolver.DerivedImageList(ctx, "repo:tag", &gql_generated.PageInput{}) + So(err, ShouldNotBeNil) + + _, err = resolver.BaseImageList(ctx, "repo:tag", &gql_generated.PageInput{}) So(err, ShouldNotBeNil) }) @@ -2182,3 +2272,675 @@ func getPageInput(limit int, offset int) *gql_generated.PageInput { SortBy: &sortCriteria, } } + +func TestDerivedImageList(t *testing.T) { + Convey("RepoDB FilterTags error", t, func() { + mockSearchDB := mocks.RepoDBMock{ + FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, + requestedPage repodb.PageInput, + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { + return make([]repodb.RepoMetadata, 0), make(map[string]repodb.ManifestMetadata), repodb.PageInfo{}, ErrTestError + }, + GetRepoMetaFn: func(repo string) (repodb.RepoMetadata, error) { + return repodb.RepoMetadata{}, ErrTestError + }, + GetManifestMetaFn: func(repo string, manifestDigest godigest.Digest) (repodb.ManifestMetadata, error) { + return repodb.ManifestMetadata{}, ErrTestError + }, + } + responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, + graphql.DefaultRecover) + + mockCve := mocks.CveInfoMock{} + images, err := derivedImageList(responseContext, "repo1:1.0.1", mockSearchDB, &gql_generated.PageInput{}, + mockCve, log.NewLogger("debug", "")) + So(err, ShouldNotBeNil) + So(images.Results, ShouldBeEmpty) + }) + + //nolint: dupl + Convey("RepoDB FilterTags no repo available", t, func() { + configBlob, err := json.Marshal(ispec.Image{ + Config: ispec.ImageConfig{ + Labels: map[string]string{}, + }, + }) + So(err, ShouldBeNil) + + manifest := ispec.Manifest{} + + manifestBlob, err := json.Marshal(manifest) + So(err, ShouldBeNil) + + manifestDigest := godigest.FromBytes(manifestBlob) + + mockSearchDB := mocks.RepoDBMock{ + FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, + requestedPage repodb.PageInput, + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { + return make([]repodb.RepoMetadata, 0), make(map[string]repodb.ManifestMetadata), repodb.PageInfo{}, nil + }, + GetRepoMetaFn: func(repo string) (repodb.RepoMetadata, error) { + return repodb.RepoMetadata{ + Name: "repo1", + Tags: map[string]repodb.Descriptor{ + "1.0.1": {Digest: manifestDigest.String(), MediaType: ispec.MediaTypeImageManifest}, + }, + }, nil + }, + GetManifestMetaFn: func(repo string, manifestDigest godigest.Digest) (repodb.ManifestMetadata, error) { + return repodb.ManifestMetadata{ + ManifestBlob: manifestBlob, + ConfigBlob: configBlob, + }, nil + }, + } + responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, + graphql.DefaultRecover) + + mockCve := mocks.CveInfoMock{} + images, err := derivedImageList(responseContext, "repo1:1.0.1", mockSearchDB, &gql_generated.PageInput{}, + mockCve, log.NewLogger("debug", "")) + So(err, ShouldBeNil) + So(images.Results, ShouldBeEmpty) + }) + + //nolint: dupl + Convey("derived image list working", t, func() { + configBlob, err := json.Marshal(ispec.Image{ + Config: ispec.ImageConfig{ + Labels: map[string]string{}, + }, + }) + So(err, ShouldBeNil) + + configDigest := godigest.FromBytes(configBlob) + + layers := [][]byte{ + {10, 11, 10, 11}, + {11, 11, 11, 11}, + {10, 10, 10, 11}, + {13, 14, 15, 11}, + } + + manifestBlob, err := json.Marshal(ispec.Manifest{ + Versioned: specs.Versioned{ + SchemaVersion: 2, + }, + Config: ispec.Descriptor{ + MediaType: "application/vnd.oci.image.config.v1+json", + Digest: configDigest, + Size: int64(len(configBlob)), + }, + Layers: []ispec.Descriptor{ + { + MediaType: "application/vnd.oci.image.layer.v1.tar", + Digest: godigest.FromBytes(layers[0]), + Size: int64(len(layers[0])), + }, + { + MediaType: "application/vnd.oci.image.layer.v1.tar", + Digest: godigest.FromBytes(layers[1]), + Size: int64(len(layers[1])), + }, + { + MediaType: "application/vnd.oci.image.layer.v1.tar", + Digest: godigest.FromBytes(layers[2]), + Size: int64(len(layers[2])), + }, + }, + }) + So(err, ShouldBeNil) + + derivedManifestBlob, err := json.Marshal(ispec.Manifest{ + Versioned: specs.Versioned{ + SchemaVersion: 2, + }, + Config: ispec.Descriptor{ + MediaType: "application/vnd.oci.image.config.v1+json", + Digest: configDigest, + Size: int64(len(configBlob)), + }, + Layers: []ispec.Descriptor{ + { + MediaType: "application/vnd.oci.image.layer.v1.tar", + Digest: godigest.FromBytes(layers[0]), + Size: int64(len(layers[0])), + }, + { + MediaType: "application/vnd.oci.image.layer.v1.tar", + Digest: godigest.FromBytes(layers[1]), + Size: int64(len(layers[1])), + }, + { + MediaType: "application/vnd.oci.image.layer.v1.tar", + Digest: godigest.FromBytes(layers[2]), + Size: int64(len(layers[2])), + }, + { + MediaType: "application/vnd.oci.image.layer.v1.tar", + Digest: godigest.FromBytes(layers[3]), + Size: int64(len(layers[3])), + }, + }, + }) + So(err, ShouldBeNil) + + manifestMetas := map[string]repodb.ManifestMetadata{ + "digestTag1.0.1": { + ManifestBlob: manifestBlob, + ConfigBlob: configBlob, + DownloadCount: 100, + Signatures: make(repodb.ManifestSignatures), + }, + "digestTag1.0.2": { + ManifestBlob: derivedManifestBlob, + ConfigBlob: configBlob, + DownloadCount: 100, + Signatures: make(repodb.ManifestSignatures), + }, + "digestTag1.0.3": { + ManifestBlob: derivedManifestBlob, + ConfigBlob: configBlob, + DownloadCount: 100, + Signatures: make(repodb.ManifestSignatures), + }, + } + manifestDigest := godigest.FromBytes(manifestBlob) + + mockSearchDB := mocks.RepoDBMock{ + GetRepoMetaFn: func(repo string) (repodb.RepoMetadata, error) { + return repodb.RepoMetadata{ + Name: "repo1", + Tags: map[string]repodb.Descriptor{ + "1.0.1": {Digest: manifestDigest.String(), MediaType: ispec.MediaTypeImageManifest}, + }, + }, nil + }, + GetManifestMetaFn: func(repo string, manifestDigest godigest.Digest) (repodb.ManifestMetadata, error) { + return repodb.ManifestMetadata{ + ManifestBlob: manifestBlob, + ConfigBlob: configBlob, + }, nil + }, + FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, + requestedPage repodb.PageInput, + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { + pageFinder, err := repodb.NewBaseImagePageFinder(requestedPage.Limit, requestedPage.Offset, requestedPage.SortBy) + So(err, ShouldBeNil) + + repos := []repodb.RepoMetadata{ + { + Name: "repo1", + Tags: map[string]repodb.Descriptor{ + "1.0.1": {Digest: "digestTag1.0.1", MediaType: ispec.MediaTypeImageManifest}, + "1.0.2": {Digest: "digestTag1.0.2", MediaType: ispec.MediaTypeImageManifest}, + "1.0.3": {Digest: "digestTag1.0.3", MediaType: ispec.MediaTypeImageManifest}, + }, + Stars: 100, + }, + } + + for i, repo := range repos { + matchedTags := repo.Tags + + for tag, descriptor := range repo.Tags { + if !filter(repo, manifestMetas[descriptor.Digest]) { + delete(matchedTags, tag) + delete(manifestMetas, descriptor.Digest) + + continue + } + } + + repos[i].Tags = matchedTags + + pageFinder.Add(repodb.DetailedRepoMeta{ + RepoMeta: repo, + }) + } + repos, pageInfo := pageFinder.Page() + + return repos, manifestMetas, pageInfo, nil + }, + } + + responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, + graphql.DefaultRecover) + + mockCve := mocks.CveInfoMock{} + + Convey("valid derivedImageList, results not affected by pageInput", func() { + images, err := derivedImageList(responseContext, "repo1:1.0.1", mockSearchDB, &gql_generated.PageInput{}, + mockCve, log.NewLogger("debug", "")) + So(err, ShouldBeNil) + So(images.Results, ShouldNotBeEmpty) + So(len(images.Results), ShouldEqual, 2) + }) + + Convey("valid derivedImageList, results affected by pageInput", func() { + limit := 1 + offset := 0 + sortCriteria := gql_generated.SortCriteriaAlphabeticAsc + pageInput := gql_generated.PageInput{ + Limit: &limit, + Offset: &offset, + SortBy: &sortCriteria, + } + + images, err := derivedImageList(responseContext, "repo1:1.0.1", mockSearchDB, &pageInput, + mockCve, log.NewLogger("debug", "")) + So(err, ShouldBeNil) + So(images.Results, ShouldNotBeEmpty) + So(len(images.Results), ShouldEqual, limit) + }) + }) +} + +func TestBaseImageList(t *testing.T) { + Convey("RepoDB FilterTags error", t, func() { + mockSearchDB := mocks.RepoDBMock{ + FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, + requestedPage repodb.PageInput, + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { + return make([]repodb.RepoMetadata, 0), make(map[string]repodb.ManifestMetadata), repodb.PageInfo{}, ErrTestError + }, + GetRepoMetaFn: func(repo string) (repodb.RepoMetadata, error) { + return repodb.RepoMetadata{}, ErrTestError + }, + GetManifestMetaFn: func(repo string, manifestDigest godigest.Digest) (repodb.ManifestMetadata, error) { + return repodb.ManifestMetadata{}, ErrTestError + }, + } + responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, + graphql.DefaultRecover) + + mockCve := mocks.CveInfoMock{} + images, err := baseImageList(responseContext, "repo1:1.0.2", mockSearchDB, &gql_generated.PageInput{}, + mockCve, log.NewLogger("debug", "")) + So(err, ShouldNotBeNil) + So(images.Results, ShouldBeEmpty) + }) + + //nolint: dupl + Convey("RepoDB FilterTags no repo available", t, func() { + configBlob, err := json.Marshal(ispec.Image{ + Config: ispec.ImageConfig{ + Labels: map[string]string{}, + }, + }) + So(err, ShouldBeNil) + + manifest := ispec.Manifest{} + + manifestBlob, err := json.Marshal(manifest) + So(err, ShouldBeNil) + + manifestDigest := godigest.FromBytes(manifestBlob) + + mockSearchDB := mocks.RepoDBMock{ + FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, + requestedPage repodb.PageInput, + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { + return make([]repodb.RepoMetadata, 0), make(map[string]repodb.ManifestMetadata), repodb.PageInfo{}, nil + }, + GetRepoMetaFn: func(repo string) (repodb.RepoMetadata, error) { + return repodb.RepoMetadata{ + Name: "repo1", + Tags: map[string]repodb.Descriptor{ + "1.0.2": {Digest: manifestDigest.String(), MediaType: ispec.MediaTypeImageManifest}, + }, + }, nil + }, + GetManifestMetaFn: func(repo string, manifestDigest godigest.Digest) (repodb.ManifestMetadata, error) { + return repodb.ManifestMetadata{ + ManifestBlob: manifestBlob, + ConfigBlob: configBlob, + }, nil + }, + } + responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, + graphql.DefaultRecover) + + mockCve := mocks.CveInfoMock{} + images, err := baseImageList(responseContext, "repo1:1.0.2", mockSearchDB, &gql_generated.PageInput{}, + mockCve, log.NewLogger("debug", "")) + So(err, ShouldBeNil) + So(images.Results, ShouldBeEmpty) + }) + + //nolint: dupl + Convey("base image list working", t, func() { + configBlob, err := json.Marshal(ispec.Image{ + Config: ispec.ImageConfig{ + Labels: map[string]string{}, + }, + }) + So(err, ShouldBeNil) + + configDigest := godigest.FromBytes(configBlob) + + layers := [][]byte{ + {10, 11, 10, 11}, + {11, 11, 11, 11}, + {10, 10, 10, 11}, + {13, 14, 15, 11}, + } + + manifestBlob, err := json.Marshal(ispec.Manifest{ + Versioned: specs.Versioned{ + SchemaVersion: 2, + }, + Config: ispec.Descriptor{ + MediaType: "application/vnd.oci.image.config.v1+json", + Digest: configDigest, + Size: int64(len(configBlob)), + }, + Layers: []ispec.Descriptor{ + { + MediaType: "application/vnd.oci.image.layer.v1.tar", + Digest: godigest.FromBytes(layers[0]), + Size: int64(len(layers[0])), + }, + { + MediaType: "application/vnd.oci.image.layer.v1.tar", + Digest: godigest.FromBytes(layers[1]), + Size: int64(len(layers[1])), + }, + { + MediaType: "application/vnd.oci.image.layer.v1.tar", + Digest: godigest.FromBytes(layers[2]), + Size: int64(len(layers[2])), + }, + }, + }) + So(err, ShouldBeNil) + + derivedManifestBlob, err := json.Marshal(ispec.Manifest{ + Versioned: specs.Versioned{ + SchemaVersion: 2, + }, + Config: ispec.Descriptor{ + MediaType: "application/vnd.oci.image.config.v1+json", + Digest: configDigest, + Size: int64(len(configBlob)), + }, + Layers: []ispec.Descriptor{ + { + MediaType: "application/vnd.oci.image.layer.v1.tar", + Digest: godigest.FromBytes(layers[0]), + Size: int64(len(layers[0])), + }, + { + MediaType: "application/vnd.oci.image.layer.v1.tar", + Digest: godigest.FromBytes(layers[1]), + Size: int64(len(layers[1])), + }, + { + MediaType: "application/vnd.oci.image.layer.v1.tar", + Digest: godigest.FromBytes(layers[2]), + Size: int64(len(layers[2])), + }, + { + MediaType: "application/vnd.oci.image.layer.v1.tar", + Digest: godigest.FromBytes(layers[3]), + Size: int64(len(layers[3])), + }, + }, + }) + So(err, ShouldBeNil) + + manifestMetas := map[string]repodb.ManifestMetadata{ + "digestTag1.0.1": { + ManifestBlob: manifestBlob, + ConfigBlob: configBlob, + DownloadCount: 100, + Signatures: make(repodb.ManifestSignatures), + }, + "digestTag1.0.2": { + ManifestBlob: derivedManifestBlob, + ConfigBlob: configBlob, + DownloadCount: 100, + Signatures: make(repodb.ManifestSignatures), + }, + } + derivedManifestDigest := godigest.FromBytes(derivedManifestBlob) + + mockSearchDB := mocks.RepoDBMock{ + GetRepoMetaFn: func(repo string) (repodb.RepoMetadata, error) { + return repodb.RepoMetadata{ + Name: "repo1", + Tags: map[string]repodb.Descriptor{ + "1.0.2": {Digest: derivedManifestDigest.String(), MediaType: ispec.MediaTypeImageManifest}, + }, + }, nil + }, + GetManifestMetaFn: func(repo string, manifestDigest godigest.Digest) (repodb.ManifestMetadata, error) { + return repodb.ManifestMetadata{ + ManifestBlob: derivedManifestBlob, + ConfigBlob: configBlob, + }, nil + }, + FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, + requestedPage repodb.PageInput, + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { + pageFinder, err := repodb.NewBaseImagePageFinder(requestedPage.Limit, requestedPage.Offset, requestedPage.SortBy) + So(err, ShouldBeNil) + + repos := []repodb.RepoMetadata{ + { + Name: "repo1", + Tags: map[string]repodb.Descriptor{ + "1.0.1": {Digest: "digestTag1.0.1", MediaType: ispec.MediaTypeImageManifest}, + "1.0.3": {Digest: "digestTag1.0.1", MediaType: ispec.MediaTypeImageManifest}, + "1.0.2": {Digest: "digestTag1.0.2", MediaType: ispec.MediaTypeImageManifest}, + }, + Stars: 100, + }, + } + + for i, repo := range repos { + matchedTags := repo.Tags + + for tag, descriptor := range repo.Tags { + if !filter(repo, manifestMetas[descriptor.Digest]) { + delete(matchedTags, tag) + delete(manifestMetas, descriptor.Digest) + + continue + } + } + + repos[i].Tags = matchedTags + + pageFinder.Add(repodb.DetailedRepoMeta{ + RepoMeta: repo, + }) + } + + repos, pageInfo := pageFinder.Page() + + return repos, manifestMetas, pageInfo, nil + }, + } + responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, + graphql.DefaultRecover) + + mockCve := mocks.CveInfoMock{} + + Convey("valid baseImageList, results not affected by pageInput", func() { + images, err := baseImageList(responseContext, "repo1:1.0.2", mockSearchDB, + &gql_generated.PageInput{}, mockCve, log.NewLogger("debug", "")) + So(err, ShouldBeNil) + So(images.Results, ShouldNotBeEmpty) + So(len(images.Results), ShouldEqual, 2) + expectedTags := []string{"1.0.1", "1.0.3"} + So(expectedTags, ShouldContain, *images.Results[0].Tag) + So(expectedTags, ShouldContain, *images.Results[1].Tag) + }) + + Convey("valid baseImageList, results affected by pageInput", func() { + limit := 1 + offset := 0 + sortCriteria := gql_generated.SortCriteriaAlphabeticAsc + pageInput := gql_generated.PageInput{ + Limit: &limit, + Offset: &offset, + SortBy: &sortCriteria, + } + + images, err := baseImageList(responseContext, "repo1:1.0.2", mockSearchDB, + &pageInput, mockCve, log.NewLogger("debug", "")) + So(err, ShouldBeNil) + So(images.Results, ShouldNotBeEmpty) + So(len(images.Results), ShouldEqual, limit) + So(*images.Results[0].Tag, ShouldEqual, "1.0.1") + }) + }) + + //nolint: dupl + Convey("filterTags working, no base image list found", t, func() { + configBlob, err := json.Marshal(ispec.Image{ + Config: ispec.ImageConfig{ + Labels: map[string]string{}, + }, + }) + So(err, ShouldBeNil) + + configDigest := godigest.FromBytes(configBlob) + + layers := [][]byte{ + {10, 11, 10, 11}, + {11, 11, 11, 11}, + {10, 10, 10, 11}, + {13, 14, 15, 11}, + } + + manifestBlob, err := json.Marshal(ispec.Manifest{ + Versioned: specs.Versioned{ + SchemaVersion: 2, + }, + Config: ispec.Descriptor{ + MediaType: "application/vnd.oci.image.config.v1+json", + Digest: configDigest, + Size: int64(len(configBlob)), + }, + Layers: []ispec.Descriptor{ + { + MediaType: "application/vnd.oci.image.layer.v1.tar", + Digest: godigest.FromBytes(layers[0]), + Size: int64(len(layers[0])), + }, + { + MediaType: "application/vnd.oci.image.layer.v1.tar", + Digest: godigest.FromBytes(layers[1]), + Size: int64(len(layers[1])), + }, + { + MediaType: "application/vnd.oci.image.layer.v1.tar", + Digest: godigest.FromBytes(layers[2]), + Size: int64(len(layers[2])), + }, + }, + }) + So(err, ShouldBeNil) + + derivedManifestBlob, err := json.Marshal(ispec.Manifest{ + Versioned: specs.Versioned{ + SchemaVersion: 2, + }, + Config: ispec.Descriptor{ + MediaType: "application/vnd.oci.image.config.v1+json", + Digest: configDigest, + Size: int64(len(configBlob)), + }, + Layers: []ispec.Descriptor{ + { + MediaType: "application/vnd.oci.image.layer.v1.tar", + Digest: godigest.FromBytes(layers[3]), + Size: int64(len(layers[3])), + }, + }, + }) + So(err, ShouldBeNil) + + manifestMetas := map[string]repodb.ManifestMetadata{ + "digestTag1.0.1": { + ManifestBlob: manifestBlob, + ConfigBlob: configBlob, + DownloadCount: 100, + Signatures: make(repodb.ManifestSignatures), + }, + "digestTag1.0.2": { + ManifestBlob: derivedManifestBlob, + ConfigBlob: configBlob, + DownloadCount: 100, + Signatures: make(repodb.ManifestSignatures), + }, + } + derivedManifestDigest := godigest.FromBytes(derivedManifestBlob) + + mockSearchDB := mocks.RepoDBMock{ + GetRepoMetaFn: func(repo string) (repodb.RepoMetadata, error) { + return repodb.RepoMetadata{ + Name: "repo1", + Tags: map[string]repodb.Descriptor{ + "1.0.2": {Digest: derivedManifestDigest.String(), MediaType: ispec.MediaTypeImageManifest}, + }, + }, nil + }, + GetManifestMetaFn: func(repo string, manifestDigest godigest.Digest) (repodb.ManifestMetadata, error) { + return repodb.ManifestMetadata{ + ManifestBlob: derivedManifestBlob, + ConfigBlob: configBlob, + }, nil + }, + FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, + requestedPage repodb.PageInput, + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { + pageFinder, err := repodb.NewBaseImagePageFinder(requestedPage.Limit, requestedPage.Offset, requestedPage.SortBy) + So(err, ShouldBeNil) + + repos := []repodb.RepoMetadata{ + { + Name: "repo1", + Tags: map[string]repodb.Descriptor{ + "1.0.1": {Digest: "digestTag1.0.1", MediaType: ispec.MediaTypeImageManifest}, + "1.0.2": {Digest: "digestTag1.0.2", MediaType: ispec.MediaTypeImageManifest}, + }, + Stars: 100, + }, + } + + for i, repo := range repos { + matchedTags := repo.Tags + + for tag, descriptor := range repo.Tags { + if !filter(repo, manifestMetas[descriptor.Digest]) { + delete(matchedTags, tag) + delete(manifestMetas, descriptor.Digest) + + continue + } + } + + repos[i].Tags = matchedTags + + pageFinder.Add(repodb.DetailedRepoMeta{ + RepoMeta: repo, + }) + } + + return repos, manifestMetas, repodb.PageInfo{}, nil + }, + } + responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, + graphql.DefaultRecover) + + mockCve := mocks.CveInfoMock{} + images, err := baseImageList(responseContext, "repo1:1.0.2", mockSearchDB, &gql_generated.PageInput{}, + mockCve, log.NewLogger("debug", "")) + So(err, ShouldBeNil) + So(images.Results, ShouldBeEmpty) + }) +} diff --git a/pkg/extensions/search/schema.graphql b/pkg/extensions/search/schema.graphql index a109a2ba..e8e8396a 100644 --- a/pkg/extensions/search/schema.graphql +++ b/pkg/extensions/search/schema.graphql @@ -244,12 +244,12 @@ type Query { """ List of images which use the argument image """ - DerivedImageList(image: String!): [ImageSummary!] + DerivedImageList(image: String!, requestedPage: PageInput): PaginatedImagesResult! """ List of images on which the argument image depends on """ - BaseImageList(image: String!): [ImageSummary!] + BaseImageList(image: String!, requestedPage: PageInput): PaginatedImagesResult! """ Search for a specific image using its name diff --git a/pkg/extensions/search/schema.resolvers.go b/pkg/extensions/search/schema.resolvers.go index 39c0c871..cd3781f6 100644 --- a/pkg/extensions/search/schema.resolvers.go +++ b/pkg/extensions/search/schema.resolvers.go @@ -104,157 +104,17 @@ func (r *queryResolver) GlobalSearch(ctx context.Context, query string, filter * } // DependencyListForImage is the resolver for the DependencyListForImage field. -func (r *queryResolver) DerivedImageList(ctx context.Context, image string) ([]*gql_generated.ImageSummary, error) { - layoutUtils := common.NewBaseOciLayoutUtils(r.storeController, r.log) - imageList := make([]*gql_generated.ImageSummary, 0) +func (r *queryResolver) DerivedImageList(ctx context.Context, image string, requestedPage *gql_generated.PageInput) (*gql_generated.PaginatedImagesResult, error) { + derivedList, err := derivedImageList(ctx, image, r.repoDB, requestedPage, r.cveInfo, r.log) - repoList, err := layoutUtils.GetRepositories() - if err != nil { - r.log.Error().Err(err).Msg("unable to get repositories list") - - return nil, err - } - - if len(repoList) == 0 { - r.log.Info().Msg("no repositories found") - - return imageList, nil - } - - imageDir, imageTag := common.GetImageDirAndTag(image) - - if imageTag == "" { - return []*gql_generated.ImageSummary{}, gqlerror.Errorf("no reference provided") - } - - imageManifest, _, err := layoutUtils.GetImageManifest(imageDir, imageTag) - if err != nil { - r.log.Info().Str("image", image).Msg("image not found") - - return imageList, err - } - - imageLayers := imageManifest.Layers - - for _, repo := range repoList { - repoInfo, err := r.ExpandedRepoInfo(ctx, repo) - if err != nil { - r.log.Error().Err(err).Msg("unable to get image list") - - return nil, err - } - - imageSummaries := repoInfo.Images - - // verify every image - for _, imageSummary := range imageSummaries { - if imageTag == *imageSummary.Tag && imageDir == repo { - continue - } - - layers := imageSummary.Layers - - sameLayer := 0 - - for _, l := range imageLayers { - for _, k := range layers { - if *k.Digest == l.Digest.String() { - sameLayer++ - } - } - } - - // if all layers are the same - if sameLayer == len(imageLayers) { - // add to returned list - imageList = append(imageList, imageSummary) - } - } - } - - return imageList, nil + return derivedList, err } // BaseImageList is the resolver for the BaseImageList field. -func (r *queryResolver) BaseImageList(ctx context.Context, image string) ([]*gql_generated.ImageSummary, error) { - layoutUtils := common.NewBaseOciLayoutUtils(r.storeController, r.log) - imageList := make([]*gql_generated.ImageSummary, 0) +func (r *queryResolver) BaseImageList(ctx context.Context, image string, requestedPage *gql_generated.PageInput) (*gql_generated.PaginatedImagesResult, error) { + imageList, err := baseImageList(ctx, image, r.repoDB, requestedPage, r.cveInfo, r.log) - repoList, err := layoutUtils.GetRepositories() - if err != nil { - r.log.Error().Err(err).Msg("unable to get repositories list") - - return nil, err - } - - if len(repoList) == 0 { - r.log.Info().Msg("no repositories found") - - return imageList, nil - } - - imageDir, imageTag := common.GetImageDirAndTag(image) - - if imageTag == "" { - return []*gql_generated.ImageSummary{}, gqlerror.Errorf("no reference provided") - } - - imageManifest, _, err := layoutUtils.GetImageManifest(imageDir, imageTag) - if err != nil { - r.log.Info().Str("image", image).Msg("image not found") - - return imageList, err - } - - imageLayers := imageManifest.Layers - - // This logic may not scale well in the future as we need to read all the - // manifest files from the disk when the call is made, we should improve in a future PR - for _, repo := range repoList { - repoInfo, err := r.ExpandedRepoInfo(ctx, repo) - if err != nil { - r.log.Error().Err(err).Msg("unable to get image list") - - return nil, err - } - - imageSummaries := repoInfo.Images - - var addImageToList bool - // verify every image - for _, imageSummary := range imageSummaries { - if imageTag == *imageSummary.Tag && imageDir == repo { - continue - } - - addImageToList = true - layers := imageSummary.Layers - - for _, l := range layers { - foundLayer := false - - for _, k := range imageLayers { - if *l.Digest == k.Digest.String() { - foundLayer = true - - break - } - } - - if !foundLayer { - addImageToList = false - - break - } - } - - if addImageToList { - imageList = append(imageList, imageSummary) - } - } - } - - return imageList, nil + return imageList, err } // Image is the resolver for the Image field. diff --git a/pkg/meta/repodb/boltdb-wrapper/boltdb_wrapper.go b/pkg/meta/repodb/boltdb-wrapper/boltdb_wrapper.go index bcacefcc..820725bb 100644 --- a/pkg/meta/repodb/boltdb-wrapper/boltdb_wrapper.go +++ b/pkg/meta/repodb/boltdb-wrapper/boltdb_wrapper.go @@ -746,16 +746,17 @@ func (bdw DBWrapper) SearchRepos(ctx context.Context, searchText string, filter func (bdw DBWrapper) FilterTags(ctx context.Context, filter repodb.FilterFunc, requestedPage repodb.PageInput, -) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, error) { +) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { var ( foundRepos = make([]repodb.RepoMetadata, 0) foundManifestMetadataMap = make(map[string]repodb.ManifestMetadata) pageFinder repodb.PageFinder + pageInfo repodb.PageInfo ) pageFinder, err := repodb.NewBaseImagePageFinder(requestedPage.Limit, requestedPage.Offset, requestedPage.SortBy) if err != nil { - return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, err + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, repodb.PageInfo{}, err } err = bdw.DB.View(func(tx *bolt.Tx) error { @@ -836,7 +837,7 @@ func (bdw DBWrapper) FilterTags(ctx context.Context, filter repodb.FilterFunc, }) } - foundRepos, _ = pageFinder.Page() + foundRepos, pageInfo = pageFinder.Page() // keep just the manifestMeta we need for _, repoMeta := range foundRepos { @@ -848,7 +849,7 @@ func (bdw DBWrapper) FilterTags(ctx context.Context, filter repodb.FilterFunc, return nil }) - return foundRepos, foundManifestMetadataMap, err + return foundRepos, foundManifestMetadataMap, pageInfo, err } func (bdw DBWrapper) SearchTags(ctx context.Context, searchText string, filter repodb.Filter, diff --git a/pkg/meta/repodb/dynamodb-wrapper/dynamo_test.go b/pkg/meta/repodb/dynamodb-wrapper/dynamo_test.go index 8d8294b5..a30e793b 100644 --- a/pkg/meta/repodb/dynamodb-wrapper/dynamo_test.go +++ b/pkg/meta/repodb/dynamodb-wrapper/dynamo_test.go @@ -398,7 +398,7 @@ func TestWrapperErrors(t *testing.T) { err = setBadRepoMeta(dynamoWrapper.Client, repoMetaTablename, "repo") //nolint:contextcheck So(err, ShouldBeNil) - _, _, err = dynamoWrapper.FilterTags( + _, _, _, err = dynamoWrapper.FilterTags( ctx, func(repoMeta repodb.RepoMetadata, manifestMeta repodb.ManifestMetadata) bool { return true @@ -413,7 +413,7 @@ func TestWrapperErrors(t *testing.T) { err := dynamoWrapper.SetRepoTag("repo", "tag1", "manifestNotFound", "") //nolint:contextcheck So(err, ShouldBeNil) - _, _, err = dynamoWrapper.FilterTags( + _, _, _, err = dynamoWrapper.FilterTags( ctx, func(repoMeta repodb.RepoMetadata, manifestMeta repodb.ManifestMetadata) bool { return true @@ -431,7 +431,7 @@ func TestWrapperErrors(t *testing.T) { err = setBadManifestData(dynamoWrapper.Client, manifestDataTablename, "dig") //nolint:contextcheck So(err, ShouldBeNil) - _, _, err = dynamoWrapper.FilterTags( + _, _, _, err = dynamoWrapper.FilterTags( ctx, func(repoMeta repodb.RepoMetadata, manifestMeta repodb.ManifestMetadata) bool { return true @@ -452,7 +452,7 @@ func TestWrapperErrors(t *testing.T) { }) So(err, ShouldBeNil) - _, _, err = dynamoWrapper.FilterTags( + _, _, _, err = dynamoWrapper.FilterTags( ctx, func(repoMeta repodb.RepoMetadata, manifestMeta repodb.ManifestMetadata) bool { return true diff --git a/pkg/meta/repodb/dynamodb-wrapper/dynamo_wrapper.go b/pkg/meta/repodb/dynamodb-wrapper/dynamo_wrapper.go index 4a75034b..beb7a6a3 100644 --- a/pkg/meta/repodb/dynamodb-wrapper/dynamo_wrapper.go +++ b/pkg/meta/repodb/dynamodb-wrapper/dynamo_wrapper.go @@ -653,12 +653,13 @@ func (dwr DBWrapper) SearchRepos(ctx context.Context, searchText string, filter func (dwr DBWrapper) FilterTags(ctx context.Context, filter repodb.FilterFunc, requestedPage repodb.PageInput, -) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, error) { +) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { var ( foundManifestMetadataMap = make(map[string]repodb.ManifestMetadata) manifestMetadataMap = make(map[string]repodb.ManifestMetadata) pageFinder repodb.PageFinder repoMetaAttributeIterator iterator.AttributesIterator + pageInfo repodb.PageInfo ) repoMetaAttributeIterator = iterator.NewBaseDynamoAttributesIterator( @@ -667,7 +668,7 @@ func (dwr DBWrapper) FilterTags(ctx context.Context, filter repodb.FilterFunc, pageFinder, err := repodb.NewBaseImagePageFinder(requestedPage.Limit, requestedPage.Offset, requestedPage.SortBy) if err != nil { - return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, err + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, pageInfo, err } repoMetaAttribute, err := repoMetaAttributeIterator.First(ctx) @@ -675,14 +676,14 @@ func (dwr DBWrapper) FilterTags(ctx context.Context, filter repodb.FilterFunc, for ; repoMetaAttribute != nil; repoMetaAttribute, err = repoMetaAttributeIterator.Next(ctx) { if err != nil { // log - return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, err + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, pageInfo, err } var repoMeta repodb.RepoMetadata err := attributevalue.Unmarshal(repoMetaAttribute, &repoMeta) if err != nil { - return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, err + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, pageInfo, err } if ok, err := localCtx.RepoIsUserAvailable(ctx, repoMeta.Name); !ok || err != nil { @@ -701,7 +702,7 @@ func (dwr DBWrapper) FilterTags(ctx context.Context, filter repodb.FilterFunc, if !manifestExists { manifestMeta, err := dwr.GetManifestMeta(repoMeta.Name, godigest.Digest(manifestDigest)) //nolint:contextcheck if err != nil { - return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, pageInfo, errors.Wrapf(err, "repodb: error while unmashaling manifest metadata for digest %s", manifestDigest) } @@ -709,7 +710,7 @@ func (dwr DBWrapper) FilterTags(ctx context.Context, filter repodb.FilterFunc, err = json.Unmarshal(manifestMeta.ConfigBlob, &configContent) if err != nil { - return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, pageInfo, errors.Wrapf(err, "repodb: error while unmashaling config for manifest with digest %s", manifestDigest) } } @@ -734,7 +735,7 @@ func (dwr DBWrapper) FilterTags(ctx context.Context, filter repodb.FilterFunc, }) } - foundRepos, _ := pageFinder.Page() + foundRepos, pageInfo := pageFinder.Page() // keep just the manifestMeta we need for _, repoMeta := range foundRepos { @@ -743,7 +744,7 @@ func (dwr DBWrapper) FilterTags(ctx context.Context, filter repodb.FilterFunc, } } - return foundRepos, foundManifestMetadataMap, err + return foundRepos, foundManifestMetadataMap, pageInfo, err } func (dwr DBWrapper) SearchTags(ctx context.Context, searchText string, filter repodb.Filter, diff --git a/pkg/meta/repodb/repodb.go b/pkg/meta/repodb/repodb.go index d3fefdfe..2da3ab34 100644 --- a/pkg/meta/repodb/repodb.go +++ b/pkg/meta/repodb/repodb.go @@ -78,7 +78,7 @@ type RepoDB interface { //nolint:interfacebloat // FilterTags filters for images given a filter function FilterTags(ctx context.Context, filter FilterFunc, - requestedPage PageInput) ([]RepoMetadata, map[string]ManifestMetadata, error) + requestedPage PageInput) ([]RepoMetadata, map[string]ManifestMetadata, PageInfo, error) PatchDB() error } diff --git a/pkg/meta/repodb/repodb_test.go b/pkg/meta/repodb/repodb_test.go index 57573195..8f3d018d 100644 --- a/pkg/meta/repodb/repodb_test.go +++ b/pkg/meta/repodb/repodb_test.go @@ -1335,7 +1335,7 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { So(err, ShouldBeNil) Convey("Return all tags", func() { - repos, manifesMetaMap, err := repoDB.FilterTags( + repos, manifesMetaMap, pageInfo, err := repoDB.FilterTags( ctx, func(repoMeta repodb.RepoMetadata, manifestMeta repodb.ManifestMetadata) bool { return true @@ -1358,10 +1358,12 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { So(manifesMetaMap, ShouldContainKey, manifestDigest1.String()) So(manifesMetaMap, ShouldContainKey, manifestDigest2.String()) So(manifesMetaMap, ShouldContainKey, manifestDigest3.String()) + So(pageInfo.ItemCount, ShouldEqual, 6) + So(pageInfo.TotalCount, ShouldEqual, 6) }) Convey("Return all tags in a specific repo", func() { - repos, manifesMetaMap, err := repoDB.FilterTags( + repos, manifesMetaMap, pageInfo, err := repoDB.FilterTags( ctx, func(repoMeta repodb.RepoMetadata, manifestMeta repodb.ManifestMetadata) bool { return repoMeta.Name == repo1 @@ -1381,10 +1383,12 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { So(manifesMetaMap, ShouldContainKey, manifestDigest1.String()) So(manifesMetaMap, ShouldContainKey, manifestDigest2.String()) So(manifesMetaMap, ShouldContainKey, manifestDigest3.String()) + So(pageInfo.ItemCount, ShouldEqual, 5) + So(pageInfo.TotalCount, ShouldEqual, 5) }) Convey("Filter everything out", func() { - repos, manifesMetaMap, err := repoDB.FilterTags( + repos, manifesMetaMap, pageInfo, err := repoDB.FilterTags( ctx, func(repoMeta repodb.RepoMetadata, manifestMeta repodb.ManifestMetadata) bool { return false @@ -1395,6 +1399,8 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { So(err, ShouldBeNil) So(len(repos), ShouldEqual, 0) So(len(manifesMetaMap), ShouldEqual, 0) + So(pageInfo.ItemCount, ShouldEqual, 0) + So(pageInfo.TotalCount, ShouldEqual, 0) }) Convey("Search with access control", func() { @@ -1409,7 +1415,7 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { authzCtxKey := localCtx.GetContextKey() ctx := context.WithValue(context.Background(), authzCtxKey, acCtx) - repos, manifesMetaMap, err := repoDB.FilterTags( + repos, manifesMetaMap, pageInfo, err := repoDB.FilterTags( ctx, func(repoMeta repodb.RepoMetadata, manifestMeta repodb.ManifestMetadata) bool { return true @@ -1423,10 +1429,12 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { So(len(repos[0].Tags), ShouldEqual, 1) So(repos[0].Tags, ShouldContainKey, "0.0.1") So(manifesMetaMap, ShouldContainKey, manifestDigest3.String()) + So(pageInfo.ItemCount, ShouldEqual, 1) + So(pageInfo.TotalCount, ShouldEqual, 1) }) Convey("With wrong pagination input", func() { - repos, _, err := repoDB.FilterTags( + repos, _, _, err := repoDB.FilterTags( ctx, func(repoMeta repodb.RepoMetadata, manifestMeta repodb.ManifestMetadata) bool { return true diff --git a/pkg/test/mocks/repo_db_mock.go b/pkg/test/mocks/repo_db_mock.go index c79fd283..c6c7559d 100644 --- a/pkg/test/mocks/repo_db_mock.go +++ b/pkg/test/mocks/repo_db_mock.go @@ -50,7 +50,7 @@ type RepoDBMock struct { FilterTagsFn func(ctx context.Context, filter repodb.FilterFunc, requestedPage repodb.PageInput, - ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, error) + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) SearchDigestsFn func(ctx context.Context, searchText string, requestedPage repodb.PageInput) ( []repodb.RepoMetadata, map[string]repodb.ManifestMetadata, error) @@ -223,12 +223,12 @@ func (sdm RepoDBMock) SearchTags(ctx context.Context, searchText string, filter func (sdm RepoDBMock) FilterTags(ctx context.Context, filter repodb.FilterFunc, requestedPage repodb.PageInput, -) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, error) { +) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { if sdm.FilterTagsFn != nil { return sdm.FilterTagsFn(ctx, filter, requestedPage) } - return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, nil + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, repodb.PageInfo{}, nil } func (sdm RepoDBMock) SearchDigests(ctx context.Context, searchText string, requestedPage repodb.PageInput,