0
Fork 0
mirror of https://github.com/project-zot/zot.git synced 2024-12-16 21:56:37 -05:00
zot/pkg/extensions/search/resolver_test.go
Andrei Aaron e8e7c343ad
feat(repodb): add pagination for ImageListForDigest and implement FilterTags (#1102)
* feat(repodb): add pagination for ImageListForDigest and implement FilterTags

ImageListForDigest can now return paginated results, directly from DB.
It uses FilterTags, a new method to filter tags (obviously) based on
the criteria provided in the filter function.
Pagination of tags is now slightly different, it shows all results if
no limit and offset are provided.

Signed-off-by: Alex Stan <alexandrustan96@yahoo.ro>

bug(tests): cli tests for digests expecting wrong size

Signed-off-by: Andrei Aaron <aaaron@luxoft.com>
(cherry picked from commit 369216df931a4053c18278a8d89f86d2e1e6a436)

fix(repodb): do not include repo metadata in search results if no matching tags are identified

Signed-off-by: Andrei Aaron <aaaron@luxoft.com>

* fix(repodb): Fix an issue in FilterTags where repometa was not proceesed correctly

The filter function was called only once per manifest digest.
The function is supposed to also take into consideration repometa,
but only the first repometa-manifestmeta pair was processed.

Also increase code coverage.

Signed-off-by: Andrei Aaron <aaaron@luxoft.com>

Signed-off-by: Andrei Aaron <aaaron@luxoft.com>
Co-authored-by: Alex Stan <alexandrustan96@yahoo.ro>
2023-01-18 00:31:54 +02:00

1474 lines
42 KiB
Go

package search //nolint
import (
"context"
"encoding/json"
"errors"
"strings"
"testing"
"time"
"github.com/99designs/gqlgen/graphql"
godigest "github.com/opencontainers/go-digest"
ispec "github.com/opencontainers/image-spec/specs-go/v1"
. "github.com/smartystreets/goconvey/convey"
"zotregistry.io/zot/pkg/extensions/search/common"
cveinfo "zotregistry.io/zot/pkg/extensions/search/cve"
"zotregistry.io/zot/pkg/extensions/search/gql_generated"
"zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/meta/repodb"
localCtx "zotregistry.io/zot/pkg/requestcontext"
"zotregistry.io/zot/pkg/storage"
"zotregistry.io/zot/pkg/test/mocks"
)
var ErrTestError = errors.New("TestError")
func TestGlobalSearch(t *testing.T) {
Convey("globalSearch", t, func() {
const query = "repo1"
Convey("RepoDB SearchRepos error", func() {
mockRepoDB := mocks.RepoDBMock{
SearchReposFn: func(ctx context.Context, searchText string, filter repodb.Filter, requestedPage repodb.PageInput,
) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, error) {
return make([]repodb.RepoMetadata, 0), make(map[string]repodb.ManifestMetadata), ErrTestError
},
}
responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter,
graphql.DefaultRecover)
mockCve := mocks.CveInfoMock{}
repos, images, layers, err := globalSearch(responseContext, query, mockRepoDB, &gql_generated.Filter{},
&gql_generated.PageInput{}, mockCve, log.NewLogger("debug", ""))
So(err, ShouldNotBeNil)
So(images, ShouldBeEmpty)
So(layers, ShouldBeEmpty)
So(repos, ShouldBeEmpty)
})
Convey("RepoDB SearchRepo is successful", func() {
mockRepoDB := mocks.RepoDBMock{
SearchReposFn: func(ctx context.Context, searchText string, filter repodb.Filter, requestedPage repodb.PageInput,
) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, error) {
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,
},
}
createTime := time.Now()
configBlob1, err := json.Marshal(ispec.Image{
Config: ispec.ImageConfig{
Labels: map[string]string{
ispec.AnnotationVendor: "TestVendor1",
},
},
Created: &createTime,
})
So(err, ShouldBeNil)
configBlob2, err := json.Marshal(ispec.Image{
Config: ispec.ImageConfig{
Labels: map[string]string{
ispec.AnnotationVendor: "TestVendor2",
},
},
})
So(err, ShouldBeNil)
manifestBlob, err := json.Marshal(ispec.Manifest{})
So(err, ShouldBeNil)
manifestMetas := map[string]repodb.ManifestMetadata{
"digestTag1.0.1": {
ManifestBlob: manifestBlob,
ConfigBlob: configBlob1,
},
"digestTag1.0.2": {
ManifestBlob: manifestBlob,
ConfigBlob: configBlob2,
},
}
return repos, manifestMetas, nil
},
}
const query = "repo1"
limit := 1
offset := 0
sortCriteria := gql_generated.SortCriteriaAlphabeticAsc
pageInput := gql_generated.PageInput{
Limit: &limit,
Offset: &offset,
SortBy: &sortCriteria,
}
responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter,
graphql.DefaultRecover)
mockCve := mocks.CveInfoMock{}
repos, images, layers, err := globalSearch(responseContext, query, mockRepoDB,
&gql_generated.Filter{}, &pageInput, mockCve, log.NewLogger("debug", ""))
So(err, ShouldBeNil)
So(images, ShouldBeEmpty)
So(layers, ShouldBeEmpty)
So(repos, ShouldNotBeEmpty)
So(len(repos[0].Vendors), ShouldEqual, 2)
})
Convey("RepoDB SearchRepo Bad manifest referenced", func() {
mockRepoDB := mocks.RepoDBMock{
SearchReposFn: func(ctx context.Context, searchText string, filter repodb.Filter, requestedPage repodb.PageInput,
) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, error) {
repos := []repodb.RepoMetadata{
{
Name: "repo1",
Tags: map[string]repodb.Descriptor{
"1.0.1": {
Digest: "digestTag1.0.1",
MediaType: ispec.MediaTypeImageManifest,
},
},
Stars: 100,
},
}
configBlob, err := json.Marshal(ispec.Image{})
So(err, ShouldBeNil)
manifestMetas := map[string]repodb.ManifestMetadata{
"digestTag1.0.1": {
ManifestBlob: []byte("bad manifest blob"),
ConfigBlob: configBlob,
},
}
return repos, manifestMetas, nil
},
}
query := "repo1"
limit := 1
offset := 0
sortCriteria := gql_generated.SortCriteriaAlphabeticAsc
pageInput := gql_generated.PageInput{
Limit: &limit,
Offset: &offset,
SortBy: &sortCriteria,
}
responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter,
graphql.DefaultRecover)
mockCve := mocks.CveInfoMock{}
repos, images, layers, err := globalSearch(responseContext, query, mockRepoDB,
&gql_generated.Filter{}, &pageInput, mockCve, log.NewLogger("debug", ""))
So(err, ShouldBeNil)
So(images, ShouldBeEmpty)
So(layers, ShouldBeEmpty)
So(repos, ShouldNotBeEmpty)
query = "repo1:1.0.1"
responseContext = graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter,
graphql.DefaultRecover)
repos, images, layers, err = globalSearch(responseContext, query, mockRepoDB,
&gql_generated.Filter{}, &pageInput, mockCve, log.NewLogger("debug", ""))
So(err, ShouldBeNil)
So(images, ShouldBeEmpty)
So(layers, ShouldBeEmpty)
So(repos, ShouldBeEmpty)
})
Convey("RepoDB SearchRepo good manifest referenced and bad config blob", func() {
mockRepoDB := mocks.RepoDBMock{
SearchReposFn: func(ctx context.Context, searchText string, filter repodb.Filter, requestedPage repodb.PageInput,
) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, error) {
repos := []repodb.RepoMetadata{
{
Name: "repo1",
Tags: map[string]repodb.Descriptor{
"1.0.1": {
Digest: "digestTag1.0.1",
MediaType: ispec.MediaTypeImageManifest,
},
},
Stars: 100,
},
}
manifestBlob, err := json.Marshal(ispec.Manifest{})
So(err, ShouldBeNil)
manifestMetas := map[string]repodb.ManifestMetadata{
"digestTag1.0.1": {
ManifestBlob: manifestBlob,
ConfigBlob: []byte("bad config blob"),
},
}
return repos, manifestMetas, nil
},
}
query := "repo1"
limit := 1
offset := 0
sortCriteria := gql_generated.SortCriteriaAlphabeticAsc
pageInput := gql_generated.PageInput{
Limit: &limit,
Offset: &offset,
SortBy: &sortCriteria,
}
mockCve := mocks.CveInfoMock{}
responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter,
graphql.DefaultRecover)
repos, images, layers, err := globalSearch(responseContext, query, mockRepoDB,
&gql_generated.Filter{}, &pageInput, mockCve, log.NewLogger("debug", ""))
So(err, ShouldBeNil)
So(images, ShouldBeEmpty)
So(layers, ShouldBeEmpty)
So(repos, ShouldNotBeEmpty)
query = "repo1:1.0.1"
responseContext = graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter,
graphql.DefaultRecover)
repos, images, layers, err = globalSearch(responseContext, query, mockRepoDB,
&gql_generated.Filter{}, &pageInput, mockCve, log.NewLogger("debug", ""))
So(err, ShouldBeNil)
So(images, ShouldBeEmpty)
So(layers, ShouldBeEmpty)
So(repos, ShouldBeEmpty)
})
Convey("RepoDB SearchTags gives error", func() {
mockRepoDB := mocks.RepoDBMock{
SearchTagsFn: func(ctx context.Context, searchText string, filter repodb.Filter, requestedPage repodb.PageInput,
) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, error) {
return make([]repodb.RepoMetadata, 0), make(map[string]repodb.ManifestMetadata), ErrTestError
},
}
const query = "repo1:1.0.1"
mockCve := mocks.CveInfoMock{}
responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter,
graphql.DefaultRecover)
repos, images, layers, err := globalSearch(responseContext, query, mockRepoDB, &gql_generated.Filter{},
&gql_generated.PageInput{}, mockCve, log.NewLogger("debug", ""))
So(err, ShouldNotBeNil)
So(images, ShouldBeEmpty)
So(layers, ShouldBeEmpty)
So(repos, ShouldBeEmpty)
})
Convey("RepoDB SearchTags is successful", func() {
mockRepoDB := mocks.RepoDBMock{
SearchTagsFn: func(ctx context.Context, searchText string, filter repodb.Filter, requestedPage repodb.PageInput,
) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, error) {
repos := []repodb.RepoMetadata{
{
Name: "repo1",
Tags: map[string]repodb.Descriptor{
"1.0.1": {
Digest: "digestTag1.0.1",
MediaType: ispec.MediaTypeImageManifest,
},
},
Stars: 100,
},
}
configBlob1, err := json.Marshal(ispec.Image{
Config: ispec.ImageConfig{
Labels: map[string]string{
ispec.AnnotationVendor: "TestVendor1",
},
},
})
So(err, ShouldBeNil)
configBlob2, err := json.Marshal(ispec.Image{
Config: ispec.ImageConfig{
Labels: map[string]string{
ispec.AnnotationVendor: "TestVendor2",
},
},
})
So(err, ShouldBeNil)
manifestBlob, err := json.Marshal(ispec.Manifest{})
So(err, ShouldBeNil)
manifestMetas := map[string]repodb.ManifestMetadata{
"digestTag1.0.1": {
ManifestBlob: manifestBlob,
ConfigBlob: configBlob1,
},
"digestTag1.0.2": {
ManifestBlob: manifestBlob,
ConfigBlob: configBlob2,
},
}
return repos, manifestMetas, nil
},
}
const query = "repo1:1.0.1"
limit := 1
offset := 0
sortCriteria := gql_generated.SortCriteriaAlphabeticAsc
pageInput := gql_generated.PageInput{
Limit: &limit,
Offset: &offset,
SortBy: &sortCriteria,
}
mockCve := mocks.CveInfoMock{}
responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter,
graphql.DefaultRecover)
repos, images, layers, err := globalSearch(responseContext, query, mockRepoDB,
&gql_generated.Filter{}, &pageInput, mockCve, log.NewLogger("debug", ""))
So(err, ShouldBeNil)
So(images, ShouldNotBeEmpty)
So(layers, ShouldBeEmpty)
So(repos, ShouldBeEmpty)
})
})
}
func TestRepoListWithNewestImage(t *testing.T) {
Convey("RepoListWithNewestImage", t, func() {
Convey("RepoDB SearchRepos error", func() {
mockRepoDB := mocks.RepoDBMock{
SearchReposFn: func(ctx context.Context, searchText string, filter repodb.Filter, requestedPage repodb.PageInput,
) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, error) {
return make([]repodb.RepoMetadata, 0), make(map[string]repodb.ManifestMetadata), ErrTestError
},
}
responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter,
graphql.DefaultRecover)
mockCve := mocks.CveInfoMock{}
limit := 1
offset := 0
sortCriteria := gql_generated.SortCriteriaUpdateTime
pageInput := gql_generated.PageInput{
Limit: &limit,
Offset: &offset,
SortBy: &sortCriteria,
}
repos, err := repoListWithNewestImage(responseContext, mockCve, log.NewLogger("debug", ""), &pageInput, mockRepoDB)
So(err, ShouldNotBeNil)
So(repos, ShouldBeEmpty)
})
Convey("RepoDB SearchRepo Bad manifest referenced", func() {
mockRepoDB := mocks.RepoDBMock{
SearchReposFn: func(ctx context.Context, searchText string, filter repodb.Filter, requestedPage repodb.PageInput,
) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, error) {
repos := []repodb.RepoMetadata{
{
Name: "repo1",
Tags: map[string]repodb.Descriptor{
"1.0.1": {
Digest: "digestTag1.0.1",
MediaType: ispec.MediaTypeImageManifest,
},
},
Stars: 100,
},
{
Name: "repo2",
Tags: map[string]repodb.Descriptor{
"1.0.2": {
Digest: "digestTag1.0.2",
MediaType: ispec.MediaTypeImageManifest,
},
},
Stars: 100,
},
}
configBlob1, err := json.Marshal(ispec.Image{
Config: ispec.ImageConfig{
Labels: map[string]string{},
},
})
So(err, ShouldBeNil)
manifestMetas := map[string]repodb.ManifestMetadata{
"digestTag1.0.1": {
ManifestBlob: []byte("bad manifest blob"),
ConfigBlob: configBlob1,
},
"digestTag1.0.2": {
ManifestBlob: []byte("bad manifest blob"),
ConfigBlob: configBlob1,
},
}
return repos, manifestMetas, nil
},
}
responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter,
graphql.DefaultRecover)
mockCve := mocks.CveInfoMock{}
limit := 1
offset := 0
sortCriteria := gql_generated.SortCriteriaUpdateTime
pageInput := gql_generated.PageInput{
Limit: &limit,
Offset: &offset,
SortBy: &sortCriteria,
}
repos, err := repoListWithNewestImage(responseContext, mockCve, log.NewLogger("debug", ""), &pageInput, mockRepoDB)
So(err, ShouldBeNil)
So(repos, ShouldNotBeEmpty)
})
Convey("Working SearchRepo function", func() {
createTime := time.Now()
createTime2 := createTime.Add(time.Second)
mockRepoDB := mocks.RepoDBMock{
SearchReposFn: func(ctx context.Context, searchText string, filter repodb.Filter, requestedPage repodb.PageInput,
) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, error) {
pageFinder, err := repodb.NewBaseRepoPageFinder(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,
},
},
Stars: 100,
},
{
Name: "repo2",
Tags: map[string]repodb.Descriptor{
"1.0.2": {
Digest: "digestTag1.0.2",
MediaType: ispec.MediaTypeImageManifest,
},
},
Stars: 100,
},
}
for _, repoMeta := range repos {
pageFinder.Add(repodb.DetailedRepoMeta{
RepoMeta: repoMeta,
UpdateTime: createTime,
})
createTime = createTime.Add(time.Second)
}
repos = pageFinder.Page()
configBlob1, err := json.Marshal(ispec.Image{
Config: ispec.ImageConfig{
Labels: map[string]string{},
},
Created: &createTime,
})
So(err, ShouldBeNil)
configBlob2, err := json.Marshal(ispec.Image{
Config: ispec.ImageConfig{
Labels: map[string]string{},
},
Created: &createTime2,
})
So(err, ShouldBeNil)
manifestBlob, err := json.Marshal(ispec.Manifest{})
So(err, ShouldBeNil)
manifestMetas := map[string]repodb.ManifestMetadata{
"digestTag1.0.1": {
ManifestBlob: manifestBlob,
ConfigBlob: configBlob1,
},
"digestTag1.0.2": {
ManifestBlob: manifestBlob,
ConfigBlob: configBlob2,
},
}
return repos, manifestMetas, nil
},
}
Convey("RepoDB missing requestedPage", func() {
responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter,
graphql.DefaultRecover)
mockCve := mocks.CveInfoMock{}
repos, err := repoListWithNewestImage(responseContext, mockCve, log.NewLogger("debug", ""), nil, mockRepoDB)
So(err, ShouldBeNil)
So(repos, ShouldNotBeEmpty)
})
Convey("RepoDB SearchRepo is successful", func() {
limit := 2
offset := 0
sortCriteria := gql_generated.SortCriteriaUpdateTime
pageInput := gql_generated.PageInput{
Limit: &limit,
Offset: &offset,
SortBy: &sortCriteria,
}
responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter,
graphql.DefaultRecover)
mockCve := mocks.CveInfoMock{}
repos, err := repoListWithNewestImage(responseContext, mockCve,
log.NewLogger("debug", ""), &pageInput, mockRepoDB)
So(err, ShouldBeNil)
So(repos, ShouldNotBeEmpty)
So(len(repos), ShouldEqual, 2)
So(*repos[0].Name, ShouldEqual, "repo2")
So(*repos[0].LastUpdated, ShouldEqual, createTime2)
})
})
})
}
func TestImageListForDigest(t *testing.T) {
Convey("getImageList", t, func() {
Convey("no page requested, FilterTagsFn returns error", func() {
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
},
}
responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter,
graphql.DefaultRecover)
_, err := getImageListForDigest(responseContext, "invalid", mockSearchDB, mocks.CveInfoMock{}, nil)
So(err, ShouldNotBeNil)
})
Convey("invalid manifest blob", func() {
mockSearchDB := mocks.RepoDBMock{
FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc,
requestedPage repodb.PageInput,
) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, error) {
repos := []repodb.RepoMetadata{
{
Name: "test",
Tags: map[string]repodb.Descriptor{
"1.0.1": {Digest: "digestTag1.0.1", MediaType: ispec.MediaTypeImageManifest},
},
Stars: 100,
},
}
configBlob, err := json.Marshal(ispec.Image{
Config: ispec.ImageConfig{
Labels: map[string]string{},
},
})
So(err, ShouldBeNil)
manifestBlob := []byte("invalid")
manifestMetaDatas := map[string]repodb.ManifestMetadata{
"digestTag1.0.1": {
ManifestBlob: manifestBlob,
ConfigBlob: configBlob,
DownloadCount: 0,
},
}
return repos, manifestMetaDatas, nil
},
}
responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter,
graphql.DefaultRecover)
imageList, err := getImageListForDigest(responseContext, "test", mockSearchDB, mocks.CveInfoMock{}, nil)
So(err, ShouldBeNil)
So(imageList, ShouldBeEmpty)
})
Convey("valid imageListForDigest returned for matching manifest digest", func() {
manifestBlob, err := json.Marshal(ispec.Manifest{})
So(err, ShouldBeNil)
manifestDigest := godigest.FromBytes(manifestBlob).String()
mockSearchDB := mocks.RepoDBMock{
FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc,
requestedPage repodb.PageInput,
) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, error) {
repos := []repodb.RepoMetadata{
{
Name: "test",
Tags: map[string]repodb.Descriptor{
"1.0.1": {Digest: manifestDigest, MediaType: ispec.MediaTypeImageManifest},
},
Stars: 100,
},
}
configBlob, err := json.Marshal(ispec.ImageConfig{})
So(err, ShouldBeNil)
manifestMetaDatas := map[string]repodb.ManifestMetadata{
manifestDigest: {
ManifestBlob: manifestBlob,
ConfigBlob: configBlob,
DownloadCount: 0,
},
}
matchedTags := repos[0].Tags
for tag, descriptor := range repos[0].Tags {
if !filter(repos[0], manifestMetaDatas[descriptor.Digest]) {
delete(matchedTags, tag)
delete(manifestMetaDatas, descriptor.Digest)
continue
}
}
repos[0].Tags = matchedTags
return repos, manifestMetaDatas, nil
},
}
limit := 1
offset := 0
sortCriteria := gql_generated.SortCriteriaAlphabeticAsc
pageInput := gql_generated.PageInput{
Limit: &limit,
Offset: &offset,
SortBy: &sortCriteria,
}
responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter,
graphql.DefaultRecover)
imageSummaries, err := getImageListForDigest(responseContext, manifestDigest,
mockSearchDB, mocks.CveInfoMock{}, &pageInput)
So(err, ShouldBeNil)
So(len(imageSummaries), ShouldEqual, 1)
imageSummaries, err = getImageListForDigest(responseContext, "invalid",
mockSearchDB, mocks.CveInfoMock{}, &pageInput)
So(err, ShouldBeNil)
So(len(imageSummaries), ShouldEqual, 0)
})
Convey("valid imageListForDigest returned for matching config digest", func() {
manifestBlob, err := json.Marshal(ispec.Manifest{})
So(err, ShouldBeNil)
manifestDigest := godigest.FromBytes(manifestBlob).String()
configBlob, err := json.Marshal(ispec.Image{})
So(err, ShouldBeNil)
configDigest := godigest.FromBytes(configBlob)
mockSearchDB := mocks.RepoDBMock{
FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc,
requestedPage repodb.PageInput,
) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, error) {
repos := []repodb.RepoMetadata{
{
Name: "test",
Tags: map[string]repodb.Descriptor{
"1.0.1": {Digest: manifestDigest, MediaType: ispec.MediaTypeImageManifest},
},
Stars: 100,
},
}
manifestBlob, err := json.Marshal(ispec.Manifest{
Config: ispec.Descriptor{
Digest: configDigest,
},
})
So(err, ShouldBeNil)
manifestMetaDatas := map[string]repodb.ManifestMetadata{
manifestDigest: {
ManifestBlob: manifestBlob,
ConfigBlob: configBlob,
DownloadCount: 0,
},
}
matchedTags := repos[0].Tags
for tag, descriptor := range repos[0].Tags {
if !filter(repos[0], manifestMetaDatas[descriptor.Digest]) {
delete(matchedTags, tag)
delete(manifestMetaDatas, descriptor.Digest)
continue
}
}
repos[0].Tags = matchedTags
return repos, manifestMetaDatas, nil
},
}
limit := 1
offset := 0
sortCriteria := gql_generated.SortCriteriaAlphabeticAsc
pageInput := gql_generated.PageInput{
Limit: &limit,
Offset: &offset,
SortBy: &sortCriteria,
}
responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter,
graphql.DefaultRecover)
imageSummaries, err := getImageListForDigest(responseContext, configDigest.String(),
mockSearchDB, mocks.CveInfoMock{}, &pageInput)
So(err, ShouldBeNil)
So(len(imageSummaries), ShouldEqual, 1)
})
Convey("valid imageListForDigest returned for matching layer digest", func() {
manifestBlob, err := json.Marshal(ispec.Manifest{})
So(err, ShouldBeNil)
manifestDigest := godigest.FromBytes(manifestBlob).String()
configBlob, err := json.Marshal(ispec.Image{})
So(err, ShouldBeNil)
layerDigest := godigest.Digest("validDigest")
mockSearchDB := mocks.RepoDBMock{
FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc,
requestedPage repodb.PageInput,
) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, error) {
repos := []repodb.RepoMetadata{
{
Name: "test",
Tags: map[string]repodb.Descriptor{
"1.0.1": {Digest: manifestDigest, MediaType: ispec.MediaTypeImageManifest},
},
Stars: 100,
},
}
manifestBlob, err := json.Marshal(ispec.Manifest{
Layers: []ispec.Descriptor{
{
Digest: layerDigest,
},
},
})
So(err, ShouldBeNil)
manifestMetaDatas := map[string]repodb.ManifestMetadata{
manifestDigest: {
ManifestBlob: manifestBlob,
ConfigBlob: configBlob,
DownloadCount: 0,
},
}
matchedTags := repos[0].Tags
for tag, descriptor := range repos[0].Tags {
if !filter(repos[0], manifestMetaDatas[descriptor.Digest]) {
delete(matchedTags, tag)
delete(manifestMetaDatas, descriptor.Digest)
continue
}
}
repos[0].Tags = matchedTags
return repos, manifestMetaDatas, nil
},
}
limit := 1
offset := 0
sortCriteria := gql_generated.SortCriteriaAlphabeticAsc
pageInput := gql_generated.PageInput{
Limit: &limit,
Offset: &offset,
SortBy: &sortCriteria,
}
responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter,
graphql.DefaultRecover)
imageSummaries, err := getImageListForDigest(responseContext, layerDigest.String(),
mockSearchDB, mocks.CveInfoMock{}, &pageInput)
So(err, ShouldBeNil)
So(len(imageSummaries), ShouldEqual, 1)
})
Convey("valid imageListForDigest, multiple matching tags", func() {
manifestBlob, err := json.Marshal(ispec.Manifest{})
So(err, ShouldBeNil)
manifestDigest := godigest.FromBytes(manifestBlob).String()
configBlob, err := json.Marshal(ispec.Image{})
So(err, ShouldBeNil)
mockSearchDB := mocks.RepoDBMock{
FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc,
requestedPage repodb.PageInput,
) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, error) {
repos := []repodb.RepoMetadata{
{
Name: "test",
Tags: map[string]repodb.Descriptor{
"1.0.1": {Digest: manifestDigest, MediaType: ispec.MediaTypeImageManifest},
"1.0.2": {Digest: manifestDigest, MediaType: ispec.MediaTypeImageManifest},
},
Stars: 100,
},
}
manifestMetaDatas := map[string]repodb.ManifestMetadata{
manifestDigest: {
ManifestBlob: manifestBlob,
ConfigBlob: configBlob,
DownloadCount: 0,
},
}
for i, repo := range repos {
matchedTags := repo.Tags
for tag, descriptor := range repo.Tags {
if !filter(repo, manifestMetaDatas[descriptor.Digest]) {
delete(matchedTags, tag)
delete(manifestMetaDatas, descriptor.Digest)
continue
}
}
repos[i].Tags = matchedTags
}
return repos, manifestMetaDatas, nil
},
}
limit := 1
offset := 0
sortCriteria := gql_generated.SortCriteriaAlphabeticAsc
pageInput := gql_generated.PageInput{
Limit: &limit,
Offset: &offset,
SortBy: &sortCriteria,
}
responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter,
graphql.DefaultRecover)
imageSummaries, err := getImageListForDigest(responseContext, manifestDigest,
mockSearchDB, mocks.CveInfoMock{}, &pageInput)
So(err, ShouldBeNil)
So(len(imageSummaries), ShouldEqual, 2)
})
Convey("valid imageListForDigest, multiple matching tags limited by pageInput", func() {
manifestBlob, err := json.Marshal(ispec.Manifest{})
So(err, ShouldBeNil)
manifestDigest := godigest.FromBytes(manifestBlob).String()
configBlob, err := json.Marshal(ispec.Image{})
So(err, ShouldBeNil)
mockSearchDB := mocks.RepoDBMock{
FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc,
requestedPage repodb.PageInput,
) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, error) {
pageFinder, err := repodb.NewBaseImagePageFinder(requestedPage.Limit, requestedPage.Offset, requestedPage.SortBy)
if err != nil {
return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, err
}
repos := []repodb.RepoMetadata{
{
Name: "test",
Tags: map[string]repodb.Descriptor{
"1.0.1": {Digest: manifestDigest, MediaType: ispec.MediaTypeImageManifest},
"1.0.2": {Digest: manifestDigest, MediaType: ispec.MediaTypeImageManifest},
},
Stars: 100,
},
}
manifestMetaDatas := map[string]repodb.ManifestMetadata{
manifestDigest: {
ManifestBlob: manifestBlob,
ConfigBlob: configBlob,
DownloadCount: 0,
},
}
for i, repo := range repos {
matchedTags := repo.Tags
for tag, descriptor := range repo.Tags {
if !filter(repo, manifestMetaDatas[descriptor.Digest]) {
delete(matchedTags, tag)
delete(manifestMetaDatas, descriptor.Digest)
continue
}
}
repos[i].Tags = matchedTags
pageFinder.Add(repodb.DetailedRepoMeta{
RepoMeta: repo,
})
}
repos = pageFinder.Page()
return repos, manifestMetaDatas, nil
},
}
limit := 1
offset := 0
sortCriteria := gql_generated.SortCriteriaAlphabeticAsc
pageInput := gql_generated.PageInput{
Limit: &limit,
Offset: &offset,
SortBy: &sortCriteria,
}
responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter,
graphql.DefaultRecover)
imageSummaries, err := getImageListForDigest(responseContext, manifestDigest,
mockSearchDB, mocks.CveInfoMock{}, &pageInput)
So(err, ShouldBeNil)
So(len(imageSummaries), ShouldEqual, 1)
})
})
}
func TestGetReferrers(t *testing.T) {
Convey("getReferrers", t, func() {
Convey("GetReferrers returns error", func() {
testLogger := log.NewLogger("debug", "")
mockedStore := mocks.MockedImageStore{
GetReferrersFn: func(repo string, digest godigest.Digest, artifactType string) (ispec.Index, error) {
return ispec.Index{}, ErrTestError
},
}
_, err := getReferrers(mockedStore, "test", "", "", testLogger)
So(err, ShouldNotBeNil)
})
Convey("GetReferrers return index of descriptors", func() {
testLogger := log.NewLogger("debug", "")
referrerDescriptor := ispec.Descriptor{
MediaType: ispec.MediaTypeArtifactManifest,
ArtifactType: "com.artifact.test",
Size: 403,
Digest: godigest.FromString("test"),
Annotations: map[string]string{
"key": "value",
},
}
mockedStore := mocks.MockedImageStore{
GetReferrersFn: func(repo string, digest godigest.Digest, artifactType string) (ispec.Index, error) {
return ispec.Index{
Manifests: []ispec.Descriptor{
referrerDescriptor,
},
}, nil
},
}
referrers, err := getReferrers(mockedStore, "test", "", "", testLogger)
So(err, ShouldBeNil)
So(*referrers[0].ArtifactType, ShouldEqual, referrerDescriptor.ArtifactType)
So(*referrers[0].MediaType, ShouldEqual, referrerDescriptor.MediaType)
So(*referrers[0].Size, ShouldEqual, referrerDescriptor.Size)
So(*referrers[0].Digest, ShouldEqual, referrerDescriptor.Digest)
So(*referrers[0].Annotations[0].Value, ShouldEqual, referrerDescriptor.Annotations["key"])
})
})
}
func TestExtractImageDetails(t *testing.T) {
Convey("repoListWithNewestImage", t, func() {
// log := log.Logger{Logger: zerolog.New(os.Stdout)}
content := []byte("this is a blob5")
testLogger := log.NewLogger("debug", "")
layerDigest := godigest.FromBytes(content)
config := ispec.Image{
Platform: ispec.Platform{
Architecture: "amd64",
OS: "linux",
},
RootFS: ispec.RootFS{
Type: "layers",
DiffIDs: []godigest.Digest{},
},
Author: "some author",
}
ctx := context.TODO()
authzCtxKey := localCtx.GetContextKey()
ctx = context.WithValue(ctx, authzCtxKey,
localCtx.AccessControlContext{
ReadGlobPatterns: map[string]bool{"*": true, "**": true},
Username: "jane_doe",
})
configBlobContent, _ := json.MarshalIndent(&config, "", "\t")
configDigest := godigest.FromBytes(configBlobContent)
localTestManifest := ispec.Manifest{
Config: ispec.Descriptor{
MediaType: "application/vnd.oci.image.config.v1+json",
Digest: configDigest,
Size: int64(len(configBlobContent)),
},
Layers: []ispec.Descriptor{
{
MediaType: "application/vnd.oci.image.layer.v1.tar",
Digest: layerDigest,
Size: int64(len(content)),
},
},
}
localTestDigestTry, _ := json.Marshal(localTestManifest)
localTestDigest := godigest.FromBytes(localTestDigestTry)
Convey("extractImageDetails good workflow", func() {
mockOlum := mocks.OciLayoutUtilsMock{
GetImageConfigInfoFn: func(repo string, digest godigest.Digest) (
ispec.Image, error,
) {
return config, nil
},
GetImageManifestFn: func(repo string, tag string) (
ispec.Manifest, godigest.Digest, error,
) {
return localTestManifest, localTestDigest, nil
},
}
resDigest, resManifest, resIspecImage, resErr := extractImageDetails(ctx,
mockOlum, "zot-test", "latest", testLogger)
So(string(resDigest), ShouldContainSubstring, "sha256:d004018b9f")
So(resManifest.Config.Digest.String(), ShouldContainSubstring, configDigest.Encoded())
So(resIspecImage.Architecture, ShouldContainSubstring, "amd64")
So(resErr, ShouldBeNil)
})
Convey("extractImageDetails bad ispec.ImageManifest", func() {
mockOlum := mocks.OciLayoutUtilsMock{
GetImageConfigInfoFn: func(repo string, digest godigest.Digest) (
ispec.Image, error,
) {
return config, nil
},
GetImageManifestFn: func(repo string, tag string) (
ispec.Manifest, godigest.Digest, error,
) {
return ispec.Manifest{}, localTestDigest, ErrTestError
},
}
resDigest, resManifest, resIspecImage, resErr := extractImageDetails(ctx,
mockOlum, "zot-test", "latest", testLogger)
So(resErr, ShouldEqual, ErrTestError)
So(string(resDigest), ShouldEqual, "")
So(resManifest, ShouldBeNil)
So(resIspecImage, ShouldBeNil)
})
Convey("extractImageDetails bad imageConfig", func() {
mockOlum := mocks.OciLayoutUtilsMock{
GetImageConfigInfoFn: func(repo string, digest godigest.Digest) (
ispec.Image, error,
) {
return config, nil
},
GetImageManifestFn: func(repo string, tag string) (
ispec.Manifest, godigest.Digest, error,
) {
return localTestManifest, localTestDigest, ErrTestError
},
}
resDigest, resManifest, resIspecImage, resErr := extractImageDetails(ctx,
mockOlum, "zot-test", "latest", testLogger)
So(string(resDigest), ShouldEqual, "")
So(resManifest, ShouldBeNil)
So(resIspecImage, ShouldBeNil)
So(resErr, ShouldEqual, ErrTestError)
})
Convey("extractImageDetails without proper authz", func() {
ctx = context.WithValue(ctx, authzCtxKey,
localCtx.AccessControlContext{
ReadGlobPatterns: map[string]bool{},
Username: "jane_doe",
})
mockOlum := mocks.OciLayoutUtilsMock{
GetImageConfigInfoFn: func(repo string, digest godigest.Digest) (
ispec.Image, error,
) {
return config, nil
},
GetImageManifestFn: func(repo string, tag string) (
ispec.Manifest, godigest.Digest, error,
) {
return localTestManifest, localTestDigest, ErrTestError
},
}
resDigest, resManifest, resIspecImage, resErr := extractImageDetails(ctx,
mockOlum, "zot-test", "latest", testLogger)
So(string(resDigest), ShouldEqual, "")
So(resManifest, ShouldBeNil)
So(resIspecImage, ShouldBeNil)
So(resErr, ShouldNotBeNil)
So(strings.ToLower(resErr.Error()), ShouldContainSubstring, "unauthorized access")
})
})
}
func TestQueryResolverErrors(t *testing.T) {
Convey("Errors", t, func() {
log := log.NewLogger("debug", "")
ctx := context.Background()
Convey("ImageListForCve olu.GetRepositories() errors", func() {
resolverConfig := NewResolver(
log,
storage.StoreController{
DefaultStore: mocks.MockedImageStore{
GetRepositoriesFn: func() ([]string, error) {
return nil, ErrTestError
},
},
},
mocks.RepoDBMock{},
mocks.CveInfoMock{},
)
qr := queryResolver{
resolverConfig,
}
_, err := qr.ImageListForCve(ctx, "id")
So(err, ShouldNotBeNil)
})
Convey("ImageListForCve cveInfo.GetImageListForCVE() errors", func() {
resolverConfig := NewResolver(
log,
storage.StoreController{
DefaultStore: mocks.MockedImageStore{
GetRepositoriesFn: func() ([]string, error) {
return []string{"repo"}, nil
},
},
},
mocks.RepoDBMock{},
mocks.CveInfoMock{
GetImageListForCVEFn: func(repo, cveID string) ([]cveinfo.ImageInfoByCVE, error) {
return nil, ErrTestError
},
},
)
qr := queryResolver{
resolverConfig,
}
_, err := qr.ImageListForCve(ctx, "a")
So(err, ShouldNotBeNil)
})
Convey("ImageListForCve olu.GetImageConfigInfo() errors", func() {
resolverConfig := NewResolver(
log,
storage.StoreController{
DefaultStore: mocks.MockedImageStore{
GetRepositoriesFn: func() ([]string, error) {
return []string{"repo"}, nil
},
GetBlobContentFn: func(repo string, digest godigest.Digest) ([]byte, error) {
return nil, ErrTestError
},
},
},
mocks.RepoDBMock{},
mocks.CveInfoMock{
GetImageListForCVEFn: func(repo, cveID string) ([]cveinfo.ImageInfoByCVE, error) {
return []cveinfo.ImageInfoByCVE{{}}, nil
},
},
)
qr := queryResolver{
resolverConfig,
}
_, err := qr.ImageListForCve(ctx, "a")
So(err, ShouldNotBeNil)
})
Convey("RepoListWithNewestImage repoListWithNewestImage errors", func() {
resolverConfig := NewResolver(
log,
storage.StoreController{
DefaultStore: mocks.MockedImageStore{},
},
mocks.RepoDBMock{
SearchReposFn: func(ctx context.Context, searchText string, filter repodb.Filter,
requestedPage repodb.PageInput,
) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, error) {
return nil, nil, ErrTestError
},
},
mocks.CveInfoMock{},
)
qr := queryResolver{
resolverConfig,
}
_, err := qr.RepoListWithNewestImage(ctx, &gql_generated.PageInput{})
So(err, ShouldNotBeNil)
})
Convey("ImageListWithCVEFixed olu.GetImageBlobManifest() errors", func() {
resolverConfig := NewResolver(
log,
storage.StoreController{
DefaultStore: mocks.MockedImageStore{
GetBlobContentFn: func(repo string, digest godigest.Digest) ([]byte, error) {
return nil, ErrTestError
},
},
},
mocks.RepoDBMock{},
mocks.CveInfoMock{
GetImageListWithCVEFixedFn: func(repo, cveID string) ([]common.TagInfo, error) {
return []common.TagInfo{{}}, nil
},
},
)
qr := queryResolver{
resolverConfig,
}
_, err := qr.ImageListWithCVEFixed(ctx, "a", "d")
So(err, ShouldNotBeNil)
})
Convey("ImageListWithCVEFixed olu.GetImageConfigInfo() errors", func() {
getBlobContentCallCounter := 0
resolverConfig := NewResolver(
log,
storage.StoreController{
DefaultStore: mocks.MockedImageStore{
GetBlobContentFn: func(repo string, digest godigest.Digest) ([]byte, error) {
if getBlobContentCallCounter == 1 {
getBlobContentCallCounter++
return nil, ErrTestError
}
getBlobContentCallCounter++
return []byte("{}"), nil
},
},
},
mocks.RepoDBMock{},
mocks.CveInfoMock{
GetImageListWithCVEFixedFn: func(repo, cveID string) ([]common.TagInfo, error) {
return []common.TagInfo{{}}, nil
},
},
)
qr := queryResolver{
resolverConfig,
}
_, err := qr.ImageListWithCVEFixed(ctx, "a", "d")
So(err, ShouldNotBeNil)
})
Convey("RepoListWithNewestImage repoListWithNewestImage() errors", func() {
resolverConfig := NewResolver(
log,
storage.StoreController{},
mocks.RepoDBMock{
SearchReposFn: func(ctx context.Context, searchText string,
filter repodb.Filter, requestedPage repodb.PageInput,
) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, error) {
return nil, nil, ErrTestError
},
},
mocks.CveInfoMock{},
)
qr := queryResolver{
resolverConfig,
}
_, err := qr.RepoListWithNewestImage(ctx, &gql_generated.PageInput{})
So(err, ShouldNotBeNil)
})
Convey("ImageList getImageList() errors", func() {
resolverConfig := NewResolver(
log,
storage.StoreController{
DefaultStore: mocks.MockedImageStore{
GetRepositoriesFn: func() ([]string, error) {
return nil, ErrTestError
},
},
},
mocks.RepoDBMock{},
mocks.CveInfoMock{},
)
qr := queryResolver{
resolverConfig,
}
_, err := qr.ImageList(ctx, "repo")
So(err, ShouldNotBeNil)
})
Convey("ImageList subpaths getImageList() errors", func() {
resolverConfig := NewResolver(
log,
storage.StoreController{
DefaultStore: mocks.MockedImageStore{
GetRepositoriesFn: func() ([]string, error) {
return []string{"sub1/repo"}, nil
},
},
SubStore: map[string]storage.ImageStore{
"/sub1": mocks.MockedImageStore{
GetRepositoriesFn: func() ([]string, error) {
return nil, ErrTestError
},
},
},
},
mocks.RepoDBMock{},
mocks.CveInfoMock{},
)
qr := queryResolver{
resolverConfig,
}
_, err := qr.ImageList(ctx, "repo")
So(err, ShouldNotBeNil)
})
Convey("DerivedImageList ExpandedRepoInfo() errors", func() {
resolverConfig := NewResolver(
log,
storage.StoreController{
DefaultStore: mocks.MockedImageStore{
GetRepositoriesFn: func() ([]string, error) {
return []string{"sub1/repo"}, nil
},
GetImageManifestFn: func(repo, reference string) ([]byte, godigest.Digest, string, error) {
return []byte("{}"), "digest", "str", nil
},
},
},
mocks.RepoDBMock{
GetRepoMetaFn: func(repo string) (repodb.RepoMetadata, error) {
return repodb.RepoMetadata{}, ErrTestError
},
},
mocks.CveInfoMock{},
)
qr := queryResolver{
resolverConfig,
}
_, err := qr.DerivedImageList(ctx, "repo:tag")
So(err, ShouldNotBeNil)
})
Convey("BaseImageList ExpandedRepoInfo() errors", func() {
resolverConfig := NewResolver(
log,
storage.StoreController{
DefaultStore: mocks.MockedImageStore{
GetRepositoriesFn: func() ([]string, error) {
return []string{"sub1/repo"}, nil
},
GetImageManifestFn: func(repo, reference string) ([]byte, godigest.Digest, string, error) {
return []byte("{}"), "digest", "str", nil
},
},
},
mocks.RepoDBMock{
GetRepoMetaFn: func(repo string) (repodb.RepoMetadata, error) {
return repodb.RepoMetadata{}, ErrTestError
},
},
mocks.CveInfoMock{},
)
qr := queryResolver{
resolverConfig,
}
_, err := qr.BaseImageList(ctx, "repo:tag")
So(err, ShouldNotBeNil)
})
})
}