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