//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) }) }