mirror of
https://github.com/project-zot/zot.git
synced 2024-12-30 22:34:13 -05:00
4170d2adbc
Moved boltdb to a driver implementation for such interface Added CreateCacheDatabaseDriver in controller Fixed default directory creation (boltDB will only create the file, not the dir Added coverage tests Added example config for boltdb Re-added caching on subpaths, rewrote CreateCacheDatabaseDriver Fix tests Made cacheDriver argument mandatory for NewImageStore, added more validation, added defaults Moved cache interface to own file, removed useRelPaths from config Got rid of cache config, refactored Moved cache to own package and folder Renamed + removed cache factory to backend, replaced CloudCache to RemoteCache Moved storage constants back to storage package moved cache interface and factory to storage package, changed remoteCache defaulting Signed-off-by: Catalin Hofnar <catalin.hofnar@gmail.com>
408 lines
12 KiB
Go
408 lines
12 KiB
Go
//go:build search
|
|
// +build search
|
|
|
|
//nolint:gochecknoinits
|
|
package digestinfo_test
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/url"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
. "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/extensions/monitoring"
|
|
digestinfo "zotregistry.io/zot/pkg/extensions/search/digest"
|
|
"zotregistry.io/zot/pkg/log"
|
|
"zotregistry.io/zot/pkg/storage"
|
|
"zotregistry.io/zot/pkg/storage/local"
|
|
. "zotregistry.io/zot/pkg/test"
|
|
)
|
|
|
|
type ImgResponseForDigest struct {
|
|
ImgListForDigest ImgListForDigest `json:"data"`
|
|
Errors []ErrorGQL `json:"errors"`
|
|
}
|
|
|
|
//nolint:tagliatelle // graphQL schema
|
|
type ImgListForDigest struct {
|
|
Images []ImgInfo `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 ErrorGQL struct {
|
|
Message string `json:"message"`
|
|
Path []string `json:"path"`
|
|
}
|
|
|
|
func testSetup(t *testing.T) (string, string, *digestinfo.DigestInfo) {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
subDir := t.TempDir()
|
|
|
|
rootDir := dir
|
|
|
|
subRootDir := subDir
|
|
|
|
// Test images used/copied:
|
|
// IMAGE NAME TAG DIGEST CONFIG LAYERS SIZE
|
|
// zot-test 0.0.1 2bacca16 adf3bb6c 76MB
|
|
// 2d473b07 76MB
|
|
// zot-cve-test 0.0.1 63a795ca 8dd57e17 75MB
|
|
// 7a0437f0 75MB
|
|
|
|
err := os.Mkdir(subDir+"/a", 0o700)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
err = CopyFiles("../../../../test/data", rootDir)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
err = CopyFiles("../../../../test/data", subDir+"/a/")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
log := log.NewLogger("debug", "")
|
|
metrics := monitoring.NewMetricsServer(false, log)
|
|
storeController := storage.StoreController{
|
|
DefaultStore: local.NewImageStore(rootDir, false, storage.DefaultGCDelay, false, false, log, metrics, nil, nil),
|
|
}
|
|
|
|
digestInfo := digestinfo.NewDigestInfo(storeController, log)
|
|
|
|
return rootDir, subRootDir, digestInfo
|
|
}
|
|
|
|
func TestDigestInfo(t *testing.T) {
|
|
Convey("Test image tag", t, func() {
|
|
_, _, digestInfo := testSetup(t)
|
|
|
|
// Search by manifest digest
|
|
imageTags, err := digestInfo.GetImageTagsByDigest("zot-cve-test",
|
|
GetTestBlobDigest("zot-cve-test", "manifest").Encoded())
|
|
So(err, ShouldBeNil)
|
|
So(len(imageTags), ShouldEqual, 1)
|
|
So(imageTags[0].Tag, ShouldEqual, "0.0.1")
|
|
|
|
// Search by config digest
|
|
imageTags, err = digestInfo.GetImageTagsByDigest("zot-test", GetTestBlobDigest("zot-test", "config").Encoded())
|
|
So(err, ShouldBeNil)
|
|
So(len(imageTags), ShouldEqual, 1)
|
|
So(imageTags[0].Tag, ShouldEqual, "0.0.1")
|
|
|
|
// Search by layer digest
|
|
imageTags, err = digestInfo.GetImageTagsByDigest("zot-cve-test", GetTestBlobDigest("zot-cve-test", "layer").Encoded())
|
|
So(err, ShouldBeNil)
|
|
So(len(imageTags), ShouldEqual, 1)
|
|
So(imageTags[0].Tag, ShouldEqual, "0.0.1")
|
|
|
|
// Search by non-existent image
|
|
imageTags, err = digestInfo.GetImageTagsByDigest("zot-tes", GetTestBlobDigest("zot-test", "manifest").Encoded())
|
|
So(err, ShouldNotBeNil)
|
|
So(len(imageTags), ShouldEqual, 0)
|
|
|
|
// Search by non-existent digest
|
|
imageTags, err = digestInfo.GetImageTagsByDigest("zot-test", "111")
|
|
So(err, ShouldBeNil)
|
|
So(len(imageTags), ShouldEqual, 0)
|
|
})
|
|
}
|
|
|
|
func TestDigestSearchHTTP(t *testing.T) {
|
|
Convey("Test image search by digest scanning", t, func() {
|
|
rootDir, _, _ := testSetup(t)
|
|
|
|
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)
|
|
|
|
go func() {
|
|
// this blocks
|
|
if err := ctlr.Run(context.Background()); err != nil {
|
|
return
|
|
}
|
|
}()
|
|
|
|
// wait till ready
|
|
for {
|
|
_, err := resty.R().Get(baseURL)
|
|
if err == nil {
|
|
break
|
|
}
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
|
|
// shut down server
|
|
defer func() {
|
|
ctx := context.Background()
|
|
_ = ctlr.Server.Shutdown(ctx)
|
|
}()
|
|
|
|
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
|
|
resp, err = resty.R().Get(
|
|
baseURL + constants.FullSearchPrefix + `?query={ImageListForDigest(id:"sha")` +
|
|
`{RepoName%20Tag%20Digest%20ConfigDigest%20Size%20Layers%20{%20Digest}}}`,
|
|
)
|
|
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.Images), ShouldEqual, 2)
|
|
So(responseStruct.ImgListForDigest.Images[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:"` + GetTestBlobDigest("zot-test", "manifest").Encoded() + `")
|
|
{RepoName Tag 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.Images), ShouldEqual, 1)
|
|
So(responseStruct.ImgListForDigest.Images[0].RepoName, ShouldEqual, "zot-test")
|
|
So(responseStruct.ImgListForDigest.Images[0].Tag, ShouldEqual, "0.0.1")
|
|
|
|
// GetTestBlobDigest("zot-test", "config").Encoded() should match the config of 1 image.
|
|
gqlQuery = url.QueryEscape(`{ImageListForDigest(id:"` + GetTestBlobDigest("zot-test", "config").Encoded() + `")
|
|
{RepoName Tag 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.Images), ShouldEqual, 1)
|
|
So(responseStruct.ImgListForDigest.Images[0].RepoName, ShouldEqual, "zot-test")
|
|
So(responseStruct.ImgListForDigest.Images[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
|
|
gqlQuery = url.QueryEscape(`{ImageListForDigest(id:"` + GetTestBlobDigest("zot-cve-test", "layer").Encoded() + `")
|
|
{RepoName Tag 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.Images), ShouldEqual, 1)
|
|
So(responseStruct2.ImgListForDigest.Images[0].RepoName, ShouldEqual, "zot-cve-test")
|
|
So(responseStruct2.ImgListForDigest.Images[0].Tag, ShouldEqual, "0.0.1")
|
|
|
|
// Call should return {"data":{"ImageListForDigest":[]}}
|
|
// "1111111" should match 0 images
|
|
resp, err = resty.R().Get(
|
|
baseURL + constants.FullSearchPrefix + `?query={ImageListForDigest(id:"1111111")` +
|
|
`{RepoName%20Tag%20Digest%20ConfigDigest%20Size%20Layers%20{%20Digest}}}`,
|
|
)
|
|
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.Images), ShouldEqual, 0)
|
|
|
|
// Call should return {"errors": [{....}]", data":null}}
|
|
resp, err = resty.R().Get(
|
|
baseURL + constants.FullSearchPrefix + `?query={ImageListForDigest(id:"1111111")` +
|
|
`{RepoName%20Tag343s}}`,
|
|
)
|
|
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, _ := testSetup(t)
|
|
|
|
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, err := os.MkdirTemp("", "digest_test")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
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
|
|
|
|
go func() {
|
|
// this blocks
|
|
if err := ctlr.Run(context.Background()); err != nil {
|
|
return
|
|
}
|
|
}()
|
|
|
|
// wait till ready
|
|
for {
|
|
_, err := resty.R().Get(baseURL)
|
|
if err == nil {
|
|
break
|
|
}
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
|
|
// shut down server
|
|
defer func() {
|
|
ctx := context.Background()
|
|
_ = ctlr.Server.Shutdown(ctx)
|
|
}()
|
|
|
|
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)
|
|
|
|
resp, err = resty.R().Get(
|
|
baseURL + constants.FullSearchPrefix + `?query={ImageListForDigest(id:"sha")` +
|
|
`{RepoName%20Tag%20Digest%20ConfigDigest%20Size%20Layers%20{%20Digest}}}`,
|
|
)
|
|
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.Images), 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)
|
|
|
|
go func() {
|
|
// this blocks
|
|
if err := ctlr.Run(context.Background()); err != nil {
|
|
return
|
|
}
|
|
}()
|
|
|
|
// wait till ready
|
|
for {
|
|
_, err := resty.R().Get(baseURL)
|
|
if err == nil {
|
|
break
|
|
}
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
|
|
// shut down server
|
|
defer func() {
|
|
ctx := context.Background()
|
|
_ = ctlr.Server.Shutdown(ctx)
|
|
}()
|
|
|
|
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)
|
|
})
|
|
}
|