zotregistry.dev/zot@v1.4.4-0.20240314164342-eec277e14d20/pkg/extensions/search/digest_test.go (about) 1 //go:build search 2 // +build search 3 4 package search_test 5 6 import ( 7 "encoding/json" 8 "net/url" 9 "os" 10 "testing" 11 "time" 12 13 godigest "github.com/opencontainers/go-digest" 14 ispec "github.com/opencontainers/image-spec/specs-go/v1" 15 . "github.com/smartystreets/goconvey/convey" 16 "gopkg.in/resty.v1" 17 18 "zotregistry.dev/zot/pkg/api" 19 "zotregistry.dev/zot/pkg/api/config" 20 "zotregistry.dev/zot/pkg/api/constants" 21 "zotregistry.dev/zot/pkg/common" 22 extconf "zotregistry.dev/zot/pkg/extensions/config" 23 . "zotregistry.dev/zot/pkg/test/common" 24 . "zotregistry.dev/zot/pkg/test/image-utils" 25 ) 26 27 type ImgResponseForDigest struct { 28 ImgListForDigest ImgListForDigest `json:"data"` 29 Errors []common.ErrorGQL `json:"errors"` 30 } 31 32 //nolint:tagliatelle // graphQL schema 33 type ImgListForDigest struct { 34 PaginatedImagesResultForDigest `json:"ImageListForDigest"` 35 } 36 37 //nolint:tagliatelle // graphQL schema 38 type ImgInfo struct { 39 RepoName string `json:"RepoName"` 40 Tag string `json:"Tag"` 41 ConfigDigest string `json:"ConfigDigest"` 42 Digest string `json:"Digest"` 43 Size string `json:"Size"` 44 } 45 46 type PaginatedImagesResultForDigest struct { 47 Results []ImgInfo `json:"results"` 48 Page common.PageInfo `json:"page"` 49 } 50 51 func TestDigestSearchHTTP(t *testing.T) { 52 Convey("Test image search by digest scanning", t, func() { 53 rootDir := t.TempDir() 54 55 port := GetFreePort() 56 baseURL := GetBaseURL(port) 57 conf := config.New() 58 conf.HTTP.Port = port 59 conf.Storage.RootDirectory = rootDir 60 defaultVal := true 61 conf.Extensions = &extconf.ExtensionConfig{ 62 Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}}, 63 } 64 65 ctlr := api.NewController(conf) 66 ctrlManager := NewControllerManager(ctlr) 67 68 ctrlManager.StartAndWait(port) 69 70 // shut down server 71 defer ctrlManager.StopServer() 72 73 createdTime1 := time.Date(2009, 1, 1, 12, 0, 0, 0, time.UTC) 74 layers1 := [][]byte{ 75 {3, 2, 2}, 76 } 77 78 image1 := CreateImageWith(). 79 LayerBlobs(layers1). 80 ImageConfig(ispec.Image{ 81 Created: &createdTime1, 82 History: []ispec.History{ 83 { 84 Created: &createdTime1, 85 }, 86 }, 87 }).Build() 88 89 const ver001 = "0.0.1" 90 91 err := UploadImage(image1, baseURL, "zot-cve-test", ver001) 92 So(err, ShouldBeNil) 93 94 createdTime2 := time.Date(2010, 1, 1, 12, 0, 0, 0, time.UTC) 95 96 image2 := CreateImageWith(). 97 LayerBlobs([][]byte{{0, 0, 2}}). 98 ImageConfig(ispec.Image{ 99 History: []ispec.History{{Created: &createdTime2}}, 100 Platform: ispec.Platform{ 101 Architecture: "amd64", 102 OS: "linux", 103 }, 104 }).Build() 105 106 manifestDigest := image2.Digest() 107 108 err = UploadImage(image2, baseURL, "zot-test", ver001) 109 So(err, ShouldBeNil) 110 111 configBlob, err := json.Marshal(image2.Config) 112 So(err, ShouldBeNil) 113 114 configDigest := godigest.FromBytes(configBlob) 115 116 resp, err := resty.R().Get(baseURL + "/v2/") 117 So(resp, ShouldNotBeNil) 118 So(err, ShouldBeNil) 119 So(resp.StatusCode(), ShouldEqual, 200) 120 121 resp, err = resty.R().Get(baseURL + constants.FullSearchPrefix) 122 So(resp, ShouldNotBeNil) 123 So(err, ShouldBeNil) 124 So(resp.StatusCode(), ShouldEqual, 422) 125 126 // "sha" should match all digests in all images 127 query := `{ 128 ImageListForDigest(id:"sha") { 129 Results { 130 RepoName Tag 131 Manifests { 132 Digest ConfigDigest Size 133 Layers { Digest } 134 } 135 Size 136 } 137 } 138 }` 139 resp, err = resty.R().Get( 140 baseURL + constants.FullSearchPrefix + "?query=" + url.QueryEscape(query), 141 ) 142 So(resp, ShouldNotBeNil) 143 So(err, ShouldBeNil) 144 So(resp.StatusCode(), ShouldEqual, 200) 145 146 var responseStruct ImgResponseForDigest 147 err = json.Unmarshal(resp.Body(), &responseStruct) 148 So(err, ShouldBeNil) 149 So(len(responseStruct.Errors), ShouldEqual, 0) 150 So(len(responseStruct.ImgListForDigest.Results), ShouldEqual, 2) 151 So(responseStruct.ImgListForDigest.Results[0].Tag, ShouldEqual, "0.0.1") 152 153 // Call should return {"data":{"ImageListForDigest":[{"Name":"zot-test","Tags":["0.0.1"]}]}} 154 // GetTestBlobDigest("zot-test", "manifest").Encoded() should match the manifest of 1 image 155 156 gqlQuery := url.QueryEscape(`{ImageListForDigest(id:"` + manifestDigest.Encoded() + `") 157 {Results{RepoName Tag Manifests {Digest ConfigDigest Size Layers { Digest }}}}}`) 158 targetURL := baseURL + constants.FullSearchPrefix + `?query=` + gqlQuery 159 160 resp, err = resty.R().Get(targetURL) 161 So(string(resp.Body()), ShouldNotBeNil) 162 So(resp, ShouldNotBeNil) 163 So(err, ShouldBeNil) 164 So(resp.StatusCode(), ShouldEqual, 200) 165 166 err = json.Unmarshal(resp.Body(), &responseStruct) 167 So(err, ShouldBeNil) 168 So(len(responseStruct.Errors), ShouldEqual, 0) 169 So(len(responseStruct.ImgListForDigest.Results), ShouldEqual, 1) 170 So(responseStruct.ImgListForDigest.Results[0].RepoName, ShouldEqual, "zot-test") 171 So(responseStruct.ImgListForDigest.Results[0].Tag, ShouldEqual, "0.0.1") 172 173 gqlQuery = url.QueryEscape(`{ImageListForDigest(id:"` + configDigest.Encoded() + `") 174 {Results{RepoName Tag Manifests {Digest ConfigDigest Size Layers { Digest }}}}}`) 175 176 targetURL = baseURL + constants.FullSearchPrefix + `?query=` + gqlQuery 177 resp, err = resty.R().Get(targetURL) 178 179 So(resp, ShouldNotBeNil) 180 So(err, ShouldBeNil) 181 So(resp.StatusCode(), ShouldEqual, 200) 182 183 err = json.Unmarshal(resp.Body(), &responseStruct) 184 So(err, ShouldBeNil) 185 So(len(responseStruct.Errors), ShouldEqual, 0) 186 So(len(responseStruct.ImgListForDigest.Results), ShouldEqual, 1) 187 So(responseStruct.ImgListForDigest.Results[0].RepoName, ShouldEqual, "zot-test") 188 So(responseStruct.ImgListForDigest.Results[0].Tag, ShouldEqual, "0.0.1") 189 190 // Call should return {"data":{"ImageListForDigest":[{"Name":"zot-cve-test","Tags":["0.0.1"]}]}} 191 // GetTestBlobDigest("zot-cve-test", "layer").Encoded() should match the layer of 1 image 192 layerDigest1 := godigest.FromBytes((layers1[0])) 193 gqlQuery = url.QueryEscape(`{ImageListForDigest(id:"` + layerDigest1.Encoded() + `") 194 {Results{RepoName Tag Manifests {Digest ConfigDigest Size Layers { Digest }}}}}`) 195 targetURL = baseURL + constants.FullSearchPrefix + `?query=` + gqlQuery 196 197 resp, err = resty.R().Get( 198 targetURL, 199 ) 200 201 So(resp, ShouldNotBeNil) 202 So(err, ShouldBeNil) 203 So(resp.StatusCode(), ShouldEqual, 200) 204 var responseStruct2 ImgResponseForDigest 205 206 err = json.Unmarshal(resp.Body(), &responseStruct2) 207 So(err, ShouldBeNil) 208 So(len(responseStruct2.Errors), ShouldEqual, 0) 209 So(len(responseStruct2.ImgListForDigest.Results), ShouldEqual, 1) 210 So(responseStruct2.ImgListForDigest.Results[0].RepoName, ShouldEqual, "zot-cve-test") 211 So(responseStruct2.ImgListForDigest.Results[0].Tag, ShouldEqual, "0.0.1") 212 213 // Call should return {"data":{"ImageListForDigest":[]}} 214 // "1111111" should match 0 images 215 query = ` 216 { 217 ImageListForDigest(id:"1111111") { 218 Results { 219 RepoName Tag 220 Manifests { 221 Digest ConfigDigest Size 222 Layers { Digest } 223 } 224 } 225 } 226 }` 227 resp, err = resty.R().Get( 228 baseURL + constants.FullSearchPrefix + "?query=" + url.QueryEscape(query), 229 ) 230 So(resp, ShouldNotBeNil) 231 So(err, ShouldBeNil) 232 So(resp.StatusCode(), ShouldEqual, 200) 233 234 err = json.Unmarshal(resp.Body(), &responseStruct) 235 So(err, ShouldBeNil) 236 So(len(responseStruct.Errors), ShouldEqual, 0) 237 So(len(responseStruct.ImgListForDigest.Results), ShouldEqual, 0) 238 239 // Call should return {"errors": [{....}]", data":null}} 240 query = `{ 241 ImageListForDigest(id:"1111111") { 242 Results { 243 RepoName Tag343s 244 } 245 }` 246 resp, err = resty.R().Get( 247 baseURL + constants.FullSearchPrefix + "?query=" + url.QueryEscape(query), 248 ) 249 So(resp, ShouldNotBeNil) 250 So(err, ShouldBeNil) 251 So(resp.StatusCode(), ShouldEqual, 422) 252 253 err = json.Unmarshal(resp.Body(), &responseStruct) 254 So(err, ShouldBeNil) 255 So(len(responseStruct.Errors), ShouldEqual, 1) 256 }) 257 } 258 259 func TestDigestSearchHTTPSubPaths(t *testing.T) { 260 Convey("Test image search by digest scanning using storage subpaths", t, func() { 261 subRootDir := t.TempDir() 262 263 port := GetFreePort() 264 baseURL := GetBaseURL(port) 265 conf := config.New() 266 conf.HTTP.Port = port 267 defaultVal := true 268 conf.Extensions = &extconf.ExtensionConfig{ 269 Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}}, 270 } 271 272 ctlr := api.NewController(conf) 273 274 globalDir := t.TempDir() 275 defer os.RemoveAll(globalDir) 276 277 ctlr.Config.Storage.RootDirectory = globalDir 278 279 subPathMap := make(map[string]config.StorageConfig) 280 281 subPathMap["/a"] = config.StorageConfig{RootDirectory: subRootDir} 282 283 ctlr.Config.Storage.SubPaths = subPathMap 284 ctrlManager := NewControllerManager(ctlr) 285 286 ctrlManager.StartAndWait(port) 287 288 // shut down server 289 defer ctrlManager.StopServer() 290 291 image := CreateDefaultImage() 292 293 err := UploadImage(image, baseURL, "a/zot-cve-test", "0.0.1") 294 So(err, ShouldBeNil) 295 296 err = UploadImage(image, baseURL, "a/zot-test", "0.0.1") 297 So(err, ShouldBeNil) 298 299 resp, err := resty.R().Get(baseURL + "/v2/") 300 So(resp, ShouldNotBeNil) 301 So(err, ShouldBeNil) 302 So(resp.StatusCode(), ShouldEqual, 200) 303 304 resp, err = resty.R().Get(baseURL + constants.FullSearchPrefix) 305 So(resp, ShouldNotBeNil) 306 So(err, ShouldBeNil) 307 So(resp.StatusCode(), ShouldEqual, 422) 308 309 query := `{ 310 ImageListForDigest(id:"sha") { 311 Results { 312 RepoName Tag 313 Manifests { 314 Digest ConfigDigest Size 315 Layers { Digest } 316 } 317 } 318 } 319 }` 320 resp, err = resty.R().Get( 321 baseURL + constants.FullSearchPrefix + "?query=" + url.QueryEscape(query), 322 ) 323 So(resp, ShouldNotBeNil) 324 So(err, ShouldBeNil) 325 So(resp.StatusCode(), ShouldEqual, 200) 326 327 var responseStruct ImgResponseForDigest 328 err = json.Unmarshal(resp.Body(), &responseStruct) 329 So(err, ShouldBeNil) 330 So(len(responseStruct.Errors), ShouldEqual, 0) 331 So(len(responseStruct.ImgListForDigest.Results), ShouldEqual, 2) 332 }) 333 } 334 335 func TestDigestSearchDisabled(t *testing.T) { 336 Convey("Test disabling image search", t, func() { 337 var disabled bool 338 port := GetFreePort() 339 baseURL := GetBaseURL(port) 340 conf := config.New() 341 conf.HTTP.Port = port 342 conf.Storage.RootDirectory = t.TempDir() 343 conf.Extensions = &extconf.ExtensionConfig{ 344 Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &disabled}}, 345 } 346 347 ctlr := api.NewController(conf) 348 ctrlManager := NewControllerManager(ctlr) 349 350 ctrlManager.StartAndWait(port) 351 352 // shut down server 353 defer ctrlManager.StopServer() 354 355 resp, err := resty.R().Get(baseURL + "/v2/") 356 So(resp, ShouldNotBeNil) 357 So(err, ShouldBeNil) 358 So(resp.StatusCode(), ShouldEqual, 200) 359 360 resp, err = resty.R().Get(baseURL + constants.FullSearchPrefix) 361 So(resp, ShouldNotBeNil) 362 So(err, ShouldBeNil) 363 So(resp.StatusCode(), ShouldEqual, 404) 364 }) 365 }