mirror of
https://github.com/project-zot/zot.git
synced 2024-12-16 21:56:37 -05:00
9cc990d7ca
Initial code was contributed by Bogdan BIVOLARU <104334+bogdanbiv@users.noreply.github.com> Moved implementation from a separate db to repodb by Andrei Aaron <aaaron@luxoft.com> Not done yet: - run/test dynamodb implementation, only boltdb was tested - add additional coverage for existing functionality - add web-based APIs to toggle the stars/bookmarks on/off Initially graphql mutation was discussed for the missing API but we decided REST endpoints would be better suited for configuration feat(userdb): complete functionality for userdb integration - dynamodb rollback changes to user starred repos in case increasing the total star count fails - dynamodb increment/decrement repostars in repometa when user stars/unstars a repo - dynamodb check anonymous user permissions are working as intendend - common test handle anonymous users - RepoMeta2RepoSummary set IsStarred and IsBookmarked feat(userdb): rest api calls for toggling stars/bookmarks on/off test(userdb): blackbox tests test(userdb): move preferences tests in a different file with specific build tags feat(repodb): add is-starred and is-bookmarked fields to repo-meta - removed duplicated logic for determining if a repo is starred/bookmarked Signed-off-by: Laurentiu Niculae <niculae.laurentiu1@gmail.com> Co-authored-by: Andrei Aaron <aaaron@luxoft.com>
377 lines
10 KiB
Go
377 lines
10 KiB
Go
//go:build search
|
|
// +build search
|
|
|
|
package search_test
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/url"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
godigest "github.com/opencontainers/go-digest"
|
|
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
|
. "github.com/smartystreets/goconvey/convey"
|
|
"gopkg.in/resty.v1"
|
|
|
|
"zotregistry.io/zot/pkg/api"
|
|
"zotregistry.io/zot/pkg/api/config"
|
|
"zotregistry.io/zot/pkg/api/constants"
|
|
extconf "zotregistry.io/zot/pkg/extensions/config"
|
|
"zotregistry.io/zot/pkg/meta/repodb"
|
|
. "zotregistry.io/zot/pkg/test"
|
|
)
|
|
|
|
type ImgResponseForDigest struct {
|
|
ImgListForDigest ImgListForDigest `json:"data"`
|
|
Errors []ErrorGQL `json:"errors"`
|
|
}
|
|
|
|
//nolint:tagliatelle // graphQL schema
|
|
type ImgListForDigest struct {
|
|
PaginatedImagesResultForDigest `json:"ImageListForDigest"`
|
|
}
|
|
|
|
//nolint:tagliatelle // graphQL schema
|
|
type ImgInfo struct {
|
|
RepoName string `json:"RepoName"`
|
|
Tag string `json:"Tag"`
|
|
ConfigDigest string `json:"ConfigDigest"`
|
|
Digest string `json:"Digest"`
|
|
Size string `json:"Size"`
|
|
}
|
|
|
|
type PaginatedImagesResultForDigest struct {
|
|
Results []ImgInfo `json:"results"`
|
|
Page repodb.PageInfo `json:"page"`
|
|
}
|
|
|
|
func TestDigestSearchHTTP(t *testing.T) {
|
|
Convey("Test image search by digest scanning", t, func() {
|
|
rootDir := t.TempDir()
|
|
|
|
port := GetFreePort()
|
|
baseURL := GetBaseURL(port)
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
conf.Storage.RootDirectory = rootDir
|
|
defaultVal := true
|
|
conf.Extensions = &extconf.ExtensionConfig{
|
|
Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}},
|
|
}
|
|
|
|
ctlr := api.NewController(conf)
|
|
ctrlManager := NewControllerManager(ctlr)
|
|
|
|
ctrlManager.StartAndWait(port)
|
|
|
|
// shut down server
|
|
defer ctrlManager.StopServer()
|
|
|
|
createdTime1 := time.Date(2009, 1, 1, 12, 0, 0, 0, time.UTC)
|
|
layers1 := [][]byte{
|
|
{3, 2, 2},
|
|
}
|
|
image1, err := GetImageWithComponents(
|
|
ispec.Image{
|
|
Created: &createdTime1,
|
|
History: []ispec.History{
|
|
{
|
|
Created: &createdTime1,
|
|
},
|
|
},
|
|
},
|
|
layers1,
|
|
)
|
|
So(err, ShouldBeNil)
|
|
|
|
const ver001 = "0.0.1"
|
|
image1.Reference = ver001
|
|
err = UploadImage(
|
|
image1,
|
|
baseURL,
|
|
"zot-cve-test",
|
|
)
|
|
So(err, ShouldBeNil)
|
|
|
|
createdTime2 := time.Date(2010, 1, 1, 12, 0, 0, 0, time.UTC)
|
|
image2, err := GetImageWithComponents(
|
|
ispec.Image{
|
|
History: []ispec.History{{Created: &createdTime2}},
|
|
Platform: ispec.Platform{
|
|
Architecture: "amd64",
|
|
OS: "linux",
|
|
},
|
|
},
|
|
[][]byte{
|
|
{0, 0, 2},
|
|
},
|
|
)
|
|
So(err, ShouldBeNil)
|
|
|
|
image2.Reference = ver001
|
|
manifestDigest, err := image2.Digest()
|
|
So(err, ShouldBeNil)
|
|
|
|
err = UploadImage(image2, baseURL, "zot-test")
|
|
So(err, ShouldBeNil)
|
|
|
|
configBlob, err := json.Marshal(image2.Config)
|
|
So(err, ShouldBeNil)
|
|
|
|
configDigest := godigest.FromBytes(configBlob)
|
|
|
|
resp, err := resty.R().Get(baseURL + "/v2/")
|
|
So(resp, ShouldNotBeNil)
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, 200)
|
|
|
|
resp, err = resty.R().Get(baseURL + constants.FullSearchPrefix)
|
|
So(resp, ShouldNotBeNil)
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, 422)
|
|
|
|
// "sha" should match all digests in all images
|
|
query := `{
|
|
ImageListForDigest(id:"sha") {
|
|
Results {
|
|
RepoName Tag
|
|
Manifests {
|
|
Digest ConfigDigest Size
|
|
Layers { Digest }
|
|
}
|
|
Size
|
|
}
|
|
}
|
|
}`
|
|
resp, err = resty.R().Get(
|
|
baseURL + constants.FullSearchPrefix + "?query=" + url.QueryEscape(query),
|
|
)
|
|
So(resp, ShouldNotBeNil)
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, 200)
|
|
|
|
var responseStruct ImgResponseForDigest
|
|
err = json.Unmarshal(resp.Body(), &responseStruct)
|
|
So(err, ShouldBeNil)
|
|
So(len(responseStruct.Errors), ShouldEqual, 0)
|
|
So(len(responseStruct.ImgListForDigest.Results), ShouldEqual, 2)
|
|
So(responseStruct.ImgListForDigest.Results[0].Tag, ShouldEqual, "0.0.1")
|
|
|
|
// Call should return {"data":{"ImageListForDigest":[{"Name":"zot-test","Tags":["0.0.1"]}]}}
|
|
// GetTestBlobDigest("zot-test", "manifest").Encoded() should match the manifest of 1 image
|
|
|
|
gqlQuery := url.QueryEscape(`{ImageListForDigest(id:"` + manifestDigest.Encoded() + `")
|
|
{Results{RepoName Tag Manifests {Digest ConfigDigest Size Layers { Digest }}}}}`)
|
|
targetURL := baseURL + constants.FullSearchPrefix + `?query=` + gqlQuery
|
|
|
|
resp, err = resty.R().Get(targetURL)
|
|
So(string(resp.Body()), ShouldNotBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, 200)
|
|
|
|
err = json.Unmarshal(resp.Body(), &responseStruct)
|
|
So(err, ShouldBeNil)
|
|
So(len(responseStruct.Errors), ShouldEqual, 0)
|
|
So(len(responseStruct.ImgListForDigest.Results), ShouldEqual, 1)
|
|
So(responseStruct.ImgListForDigest.Results[0].RepoName, ShouldEqual, "zot-test")
|
|
So(responseStruct.ImgListForDigest.Results[0].Tag, ShouldEqual, "0.0.1")
|
|
|
|
gqlQuery = url.QueryEscape(`{ImageListForDigest(id:"` + configDigest.Encoded() + `")
|
|
{Results{RepoName Tag Manifests {Digest ConfigDigest Size Layers { Digest }}}}}`)
|
|
|
|
targetURL = baseURL + constants.FullSearchPrefix + `?query=` + gqlQuery
|
|
resp, err = resty.R().Get(targetURL)
|
|
|
|
So(resp, ShouldNotBeNil)
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, 200)
|
|
|
|
err = json.Unmarshal(resp.Body(), &responseStruct)
|
|
So(err, ShouldBeNil)
|
|
So(len(responseStruct.Errors), ShouldEqual, 0)
|
|
So(len(responseStruct.ImgListForDigest.Results), ShouldEqual, 1)
|
|
So(responseStruct.ImgListForDigest.Results[0].RepoName, ShouldEqual, "zot-test")
|
|
So(responseStruct.ImgListForDigest.Results[0].Tag, ShouldEqual, "0.0.1")
|
|
|
|
// Call should return {"data":{"ImageListForDigest":[{"Name":"zot-cve-test","Tags":["0.0.1"]}]}}
|
|
// GetTestBlobDigest("zot-cve-test", "layer").Encoded() should match the layer of 1 image
|
|
layerDigest1 := godigest.FromBytes((layers1[0]))
|
|
gqlQuery = url.QueryEscape(`{ImageListForDigest(id:"` + layerDigest1.Encoded() + `")
|
|
{Results{RepoName Tag Manifests {Digest ConfigDigest Size Layers { Digest }}}}}`)
|
|
targetURL = baseURL + constants.FullSearchPrefix + `?query=` + gqlQuery
|
|
|
|
resp, err = resty.R().Get(
|
|
targetURL,
|
|
)
|
|
|
|
So(resp, ShouldNotBeNil)
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, 200)
|
|
var responseStruct2 ImgResponseForDigest
|
|
|
|
err = json.Unmarshal(resp.Body(), &responseStruct2)
|
|
So(err, ShouldBeNil)
|
|
So(len(responseStruct2.Errors), ShouldEqual, 0)
|
|
So(len(responseStruct2.ImgListForDigest.Results), ShouldEqual, 1)
|
|
So(responseStruct2.ImgListForDigest.Results[0].RepoName, ShouldEqual, "zot-cve-test")
|
|
So(responseStruct2.ImgListForDigest.Results[0].Tag, ShouldEqual, "0.0.1")
|
|
|
|
// Call should return {"data":{"ImageListForDigest":[]}}
|
|
// "1111111" should match 0 images
|
|
query = `
|
|
{
|
|
ImageListForDigest(id:"1111111") {
|
|
Results {
|
|
RepoName Tag
|
|
Manifests {
|
|
Digest ConfigDigest Size
|
|
Layers { Digest }
|
|
}
|
|
}
|
|
}
|
|
}`
|
|
resp, err = resty.R().Get(
|
|
baseURL + constants.FullSearchPrefix + "?query=" + url.QueryEscape(query),
|
|
)
|
|
So(resp, ShouldNotBeNil)
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, 200)
|
|
|
|
err = json.Unmarshal(resp.Body(), &responseStruct)
|
|
So(err, ShouldBeNil)
|
|
So(len(responseStruct.Errors), ShouldEqual, 0)
|
|
So(len(responseStruct.ImgListForDigest.Results), ShouldEqual, 0)
|
|
|
|
// Call should return {"errors": [{....}]", data":null}}
|
|
query = `{
|
|
ImageListForDigest(id:"1111111") {
|
|
Results {
|
|
RepoName Tag343s
|
|
}
|
|
}`
|
|
resp, err = resty.R().Get(
|
|
baseURL + constants.FullSearchPrefix + "?query=" + url.QueryEscape(query),
|
|
)
|
|
So(resp, ShouldNotBeNil)
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, 422)
|
|
|
|
err = json.Unmarshal(resp.Body(), &responseStruct)
|
|
So(err, ShouldBeNil)
|
|
So(len(responseStruct.Errors), ShouldEqual, 1)
|
|
})
|
|
}
|
|
|
|
func TestDigestSearchHTTPSubPaths(t *testing.T) {
|
|
Convey("Test image search by digest scanning using storage subpaths", t, func() {
|
|
subRootDir := t.TempDir()
|
|
|
|
port := GetFreePort()
|
|
baseURL := GetBaseURL(port)
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
defaultVal := true
|
|
conf.Extensions = &extconf.ExtensionConfig{
|
|
Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}},
|
|
}
|
|
|
|
ctlr := api.NewController(conf)
|
|
|
|
globalDir := t.TempDir()
|
|
defer os.RemoveAll(globalDir)
|
|
|
|
ctlr.Config.Storage.RootDirectory = globalDir
|
|
|
|
subPathMap := make(map[string]config.StorageConfig)
|
|
|
|
subPathMap["/a"] = config.StorageConfig{RootDirectory: subRootDir}
|
|
|
|
ctlr.Config.Storage.SubPaths = subPathMap
|
|
ctrlManager := NewControllerManager(ctlr)
|
|
|
|
ctrlManager.StartAndWait(port)
|
|
|
|
// shut down server
|
|
defer ctrlManager.StopServer()
|
|
|
|
config, layers, manifest, err := GetImageComponents(100)
|
|
So(err, ShouldBeNil)
|
|
|
|
err = PushTestImage("a/zot-cve-test", "0.0.1", baseURL,
|
|
manifest, config, layers)
|
|
So(err, ShouldBeNil)
|
|
|
|
err = PushTestImage("a/zot-test", "0.0.1", baseURL,
|
|
manifest, config, layers)
|
|
So(err, ShouldBeNil)
|
|
|
|
resp, err := resty.R().Get(baseURL + "/v2/")
|
|
So(resp, ShouldNotBeNil)
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, 200)
|
|
|
|
resp, err = resty.R().Get(baseURL + constants.FullSearchPrefix)
|
|
So(resp, ShouldNotBeNil)
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, 422)
|
|
|
|
query := `{
|
|
ImageListForDigest(id:"sha") {
|
|
Results {
|
|
RepoName Tag
|
|
Manifests {
|
|
Digest ConfigDigest Size
|
|
Layers { Digest }
|
|
}
|
|
}
|
|
}
|
|
}`
|
|
resp, err = resty.R().Get(
|
|
baseURL + constants.FullSearchPrefix + "?query=" + url.QueryEscape(query),
|
|
)
|
|
So(resp, ShouldNotBeNil)
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, 200)
|
|
|
|
var responseStruct ImgResponseForDigest
|
|
err = json.Unmarshal(resp.Body(), &responseStruct)
|
|
So(err, ShouldBeNil)
|
|
So(len(responseStruct.Errors), ShouldEqual, 0)
|
|
So(len(responseStruct.ImgListForDigest.Results), ShouldEqual, 2)
|
|
})
|
|
}
|
|
|
|
func TestDigestSearchDisabled(t *testing.T) {
|
|
Convey("Test disabling image search", t, func() {
|
|
var disabled bool
|
|
port := GetFreePort()
|
|
baseURL := GetBaseURL(port)
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
conf.Storage.RootDirectory = t.TempDir()
|
|
conf.Extensions = &extconf.ExtensionConfig{
|
|
Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &disabled}},
|
|
}
|
|
|
|
ctlr := api.NewController(conf)
|
|
ctrlManager := NewControllerManager(ctlr)
|
|
|
|
ctrlManager.StartAndWait(port)
|
|
|
|
// shut down server
|
|
defer ctrlManager.StopServer()
|
|
|
|
resp, err := resty.R().Get(baseURL + "/v2/")
|
|
So(resp, ShouldNotBeNil)
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, 200)
|
|
|
|
resp, err = resty.R().Get(baseURL + constants.FullSearchPrefix)
|
|
So(resp, ShouldNotBeNil)
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, 404)
|
|
})
|
|
}
|