zotregistry.dev/zot@v1.4.4-0.20240314164342-eec277e14d20/pkg/compliance/v1_0_0/check.go (about) 1 //nolint:dupl 2 package v1_0_0 //nolint:stylecheck,golint,revive 3 4 import ( 5 "bytes" 6 "encoding/json" 7 "fmt" 8 "io" 9 "net" 10 "net/http" 11 "os" 12 "path" 13 "strings" 14 "testing" 15 16 godigest "github.com/opencontainers/go-digest" 17 //nolint:golint,stylecheck,revive 18 . "github.com/smartystreets/goconvey/convey" 19 "github.com/smartystreets/goconvey/convey/reporting" 20 "gopkg.in/resty.v1" 21 22 "zotregistry.dev/zot/pkg/api" 23 "zotregistry.dev/zot/pkg/api/constants" 24 "zotregistry.dev/zot/pkg/compliance" 25 test "zotregistry.dev/zot/pkg/test/common" 26 "zotregistry.dev/zot/pkg/test/image-utils" 27 ) 28 29 func CheckWorkflows(t *testing.T, config *compliance.Config) { 30 t.Helper() 31 32 if config == nil || config.Address == "" || config.Port == "" { 33 t.Fatal("insufficient config") 34 } 35 36 if config.OutputJSON { 37 outputJSONEnter() 38 39 defer outputJSONExit() 40 } 41 42 baseURL := fmt.Sprintf("http://%s", net.JoinHostPort(config.Address, config.Port)) 43 44 storageInfo := config.StorageInfo 45 46 fmt.Println("------------------------------") 47 fmt.Println("Checking for v1.0.0 compliance") 48 fmt.Println("------------------------------") 49 50 Convey("Make API calls to the controller", t, func(c C) { 51 Convey("Check version", func() { 52 _, _ = Print("\nCheck version") 53 resp, err := resty.R().Get(baseURL + constants.RoutePrefix + "/") 54 So(err, ShouldBeNil) 55 So(resp.StatusCode(), ShouldEqual, http.StatusOK) 56 }) 57 58 Convey("Get repository catalog", func() { 59 _, _ = Print("\nGet repository catalog") 60 resp, err := resty.R().Get(baseURL + constants.RoutePrefix + constants.ExtCatalogPrefix) 61 So(err, ShouldBeNil) 62 So(resp.StatusCode(), ShouldEqual, http.StatusOK) 63 So(resp.String(), ShouldNotBeEmpty) 64 So(resp.Header().Get("Content-Type"), ShouldEqual, constants.DefaultMediaType) 65 var repoList api.RepositoryList 66 err = json.Unmarshal(resp.Body(), &repoList) 67 So(err, ShouldBeNil) 68 So(len(repoList.Repositories), ShouldEqual, 0) 69 70 // after newly created upload should succeed 71 resp, err = resty.R().Post(baseURL + "/v2/z/blobs/uploads/") 72 So(err, ShouldBeNil) 73 So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) 74 75 // after newly created upload should succeed 76 resp, err = resty.R().Post(baseURL + "/v2/a/b/c/d/blobs/uploads/") 77 So(err, ShouldBeNil) 78 So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) 79 80 resp, err = resty.R().SetResult(&api.RepositoryList{}).Get(baseURL + 81 constants.RoutePrefix + constants.ExtCatalogPrefix) 82 So(err, ShouldBeNil) 83 So(resp.StatusCode(), ShouldEqual, http.StatusOK) 84 So(resp.String(), ShouldNotBeEmpty) 85 result, ok := resp.Result().(*api.RepositoryList) 86 So(ok, ShouldBeTrue) 87 if !config.Compliance { 88 // stricter check for zot ci/cd 89 So(len(result.Repositories), ShouldBeGreaterThan, 0) 90 So(result.Repositories[0], ShouldEqual, "a/b/c/d") 91 So(result.Repositories[1], ShouldEqual, "z") 92 } 93 }) 94 95 Convey("Get images in a repository", func() { 96 _, _ = Print("\nGet images in a repository") 97 // non-existent repository should fail 98 resp, err := resty.R().Get(baseURL + "/v2/repo1/tags/list") 99 So(err, ShouldBeNil) 100 So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) 101 So(resp.String(), ShouldNotBeEmpty) 102 103 // after newly created upload should succeed 104 resp, err = resty.R().Post(baseURL + "/v2/repo1/blobs/uploads/") 105 So(err, ShouldBeNil) 106 So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) 107 108 resp, err = resty.R().Get(baseURL + "/v2/repo1/tags/list") 109 So(err, ShouldBeNil) 110 if !config.Compliance { 111 // stricter check for zot ci/cd 112 So(resp.StatusCode(), ShouldEqual, http.StatusOK) 113 So(resp.String(), ShouldNotBeEmpty) 114 } 115 }) 116 117 Convey("Monolithic blob upload", func() { 118 _, _ = Print("\nMonolithic blob upload") 119 resp, err := resty.R().Post(baseURL + "/v2/repo2/blobs/uploads/") 120 So(err, ShouldBeNil) 121 So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) 122 loc := test.Location(baseURL, resp) 123 So(loc, ShouldNotBeEmpty) 124 125 resp, err = resty.R().Get(loc) 126 So(err, ShouldBeNil) 127 So(resp.StatusCode(), ShouldEqual, http.StatusNoContent) 128 129 resp, err = resty.R().Get(baseURL + "/v2/repo2/tags/list") 130 So(err, ShouldBeNil) 131 if !config.Compliance { 132 // stricter check for zot ci/cd 133 So(resp.StatusCode(), ShouldEqual, http.StatusOK) 134 So(resp.String(), ShouldNotBeEmpty) 135 } 136 137 // without a "?digest=<>" should fail 138 content := []byte("this is a blob1") 139 digest := godigest.FromBytes(content) 140 So(digest, ShouldNotBeNil) 141 resp, err = resty.R().Put(loc) 142 So(err, ShouldBeNil) 143 So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest) 144 // without the Content-Length should fail 145 resp, err = resty.R().SetQueryParam("digest", digest.String()).Put(loc) 146 So(err, ShouldBeNil) 147 So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest) 148 // without any data to send, should fail 149 resp, err = resty.R().SetQueryParam("digest", digest.String()). 150 SetHeader("Content-Type", "application/octet-stream").Put(loc) 151 So(err, ShouldBeNil) 152 So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest) 153 // monolithic blob upload: success 154 resp, err = resty.R().SetQueryParam("digest", digest.String()). 155 SetHeader("Content-Type", "application/octet-stream").SetBody(content).Put(loc) 156 So(err, ShouldBeNil) 157 So(resp.StatusCode(), ShouldEqual, http.StatusCreated) 158 blobLoc := test.Location(baseURL, resp) 159 So(blobLoc, ShouldNotBeEmpty) 160 So(resp.Header().Get("Content-Length"), ShouldEqual, "0") 161 So(resp.Header().Get(constants.DistContentDigestKey), ShouldNotBeEmpty) 162 // upload reference should now be removed 163 resp, err = resty.R().Get(loc) 164 So(err, ShouldBeNil) 165 So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) 166 // blob reference should be accessible 167 resp, err = resty.R().Get(blobLoc) 168 So(err, ShouldBeNil) 169 So(resp.StatusCode(), ShouldEqual, http.StatusOK) 170 }) 171 172 Convey("Monolithic blob upload with body", func() { 173 _, _ = Print("\nMonolithic blob upload") 174 // create content 175 content := []byte("this is a blob2") 176 digest := godigest.FromBytes(content) 177 So(digest, ShouldNotBeNil) 178 // setting invalid URL params should fail 179 resp, err := resty.R(). 180 SetQueryParam("digest", digest.String()). 181 SetQueryParam("from", digest.String()). 182 SetHeader("Content-Type", "application/octet-stream"). 183 SetBody(content). 184 Post(baseURL + "/v2/repo2/blobs/uploads/") 185 So(err, ShouldBeNil) 186 So(resp.StatusCode(), ShouldEqual, http.StatusMethodNotAllowed) 187 // setting a "?digest=<>" but without body should fail 188 resp, err = resty.R(). 189 SetQueryParam("digest", digest.String()). 190 SetHeader("Content-Type", "application/octet-stream"). 191 Post(baseURL + "/v2/repo2/blobs/uploads/") 192 So(err, ShouldBeNil) 193 So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest) 194 // set a "?digest=<>" 195 resp, err = resty.R(). 196 SetQueryParam("digest", digest.String()). 197 SetHeader("Content-Type", "application/octet-stream"). 198 SetBody(content). 199 Post(baseURL + "/v2/repo2/blobs/uploads/") 200 So(err, ShouldBeNil) 201 So(resp.StatusCode(), ShouldEqual, http.StatusCreated) 202 loc := test.Location(baseURL, resp) 203 So(loc, ShouldNotBeEmpty) 204 // blob reference should be accessible 205 resp, err = resty.R().Get(loc) 206 So(err, ShouldBeNil) 207 So(resp.StatusCode(), ShouldEqual, http.StatusOK) 208 }) 209 210 Convey("Monolithic blob upload with multiple name components", func() { 211 _, _ = Print("\nMonolithic blob upload with multiple name components") 212 resp, err := resty.R().Post(baseURL + "/v2/repo10/repo20/repo30/blobs/uploads/") 213 So(err, ShouldBeNil) 214 So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) 215 loc := test.Location(baseURL, resp) 216 So(loc, ShouldNotBeEmpty) 217 218 resp, err = resty.R().Get(loc) 219 So(err, ShouldBeNil) 220 So(resp.StatusCode(), ShouldEqual, http.StatusNoContent) 221 222 resp, err = resty.R().Get(baseURL + "/v2/repo10/repo20/repo30/tags/list") 223 So(err, ShouldBeNil) 224 if !config.Compliance { 225 // stricter check for zot ci/cd 226 So(resp.StatusCode(), ShouldEqual, http.StatusOK) 227 So(resp.String(), ShouldNotBeEmpty) 228 } 229 230 // without a "?digest=<>" should fail 231 content := []byte("this is a blob3") 232 digest := godigest.FromBytes(content) 233 So(digest, ShouldNotBeNil) 234 resp, err = resty.R().Put(loc) 235 So(err, ShouldBeNil) 236 So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest) 237 // without the Content-Length should fail 238 resp, err = resty.R().SetQueryParam("digest", digest.String()).Put(loc) 239 So(err, ShouldBeNil) 240 So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest) 241 // without any data to send, should fail 242 resp, err = resty.R().SetQueryParam("digest", digest.String()). 243 SetHeader("Content-Type", "application/octet-stream").Put(loc) 244 So(err, ShouldBeNil) 245 So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest) 246 // monolithic blob upload: success 247 resp, err = resty.R().SetQueryParam("digest", digest.String()). 248 SetHeader("Content-Type", "application/octet-stream").SetBody(content).Put(loc) 249 So(err, ShouldBeNil) 250 So(resp.StatusCode(), ShouldEqual, http.StatusCreated) 251 blobLoc := test.Location(baseURL, resp) 252 So(blobLoc, ShouldNotBeEmpty) 253 So(resp.Header().Get("Content-Length"), ShouldEqual, "0") 254 So(resp.Header().Get(constants.DistContentDigestKey), ShouldNotBeEmpty) 255 // upload reference should now be removed 256 resp, err = resty.R().Get(loc) 257 So(err, ShouldBeNil) 258 So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) 259 // blob reference should be accessible 260 resp, err = resty.R().Get(blobLoc) 261 So(err, ShouldBeNil) 262 So(resp.StatusCode(), ShouldEqual, http.StatusOK) 263 }) 264 265 Convey("Chunked blob upload", func() { 266 _, _ = Print("\nChunked blob upload") 267 resp, err := resty.R().Post(baseURL + "/v2/repo3/blobs/uploads/") 268 So(err, ShouldBeNil) 269 So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) 270 loc := test.Location(baseURL, resp) 271 So(loc, ShouldNotBeEmpty) 272 273 var buf bytes.Buffer 274 chunk1 := []byte("this is the first chunk1") 275 nbytes, err := buf.Write(chunk1) 276 So(nbytes, ShouldEqual, len(chunk1)) 277 So(err, ShouldBeNil) 278 279 // write first chunk 280 contentRange := fmt.Sprintf("%d-%d", 0, len(chunk1)-1) 281 resp, err = resty.R().SetHeader("Content-Type", "application/octet-stream"). 282 SetHeader("Content-Range", contentRange).SetBody(chunk1).Patch(loc) 283 So(err, ShouldBeNil) 284 So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) 285 286 // check progress 287 resp, err = resty.R().Get(loc) 288 So(err, ShouldBeNil) 289 So(resp.StatusCode(), ShouldEqual, http.StatusNoContent) 290 r := resp.Header().Get("Range") 291 So(r, ShouldNotBeEmpty) 292 So(r, ShouldEqual, contentRange) 293 294 // write same chunk should fail 295 contentRange = fmt.Sprintf("%d-%d", 0, len(chunk1)-1) 296 resp, err = resty.R().SetHeader("Content-Type", "application/octet-stream"). 297 SetHeader("Content-Range", contentRange).SetBody(chunk1).Patch(loc) 298 So(err, ShouldBeNil) 299 So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable) 300 So(resp.String(), ShouldNotBeEmpty) 301 302 chunk2 := []byte("this is the second chunk1") 303 nbytes, err = buf.Write(chunk2) 304 So(nbytes, ShouldEqual, len(chunk2)) 305 So(err, ShouldBeNil) 306 307 digest := godigest.FromBytes(buf.Bytes()) 308 So(digest, ShouldNotBeNil) 309 310 // write final chunk 311 contentRange = fmt.Sprintf("%d-%d", len(chunk1), len(buf.Bytes())-1) 312 resp, err = resty.R().SetQueryParam("digest", digest.String()). 313 SetHeader("Content-Range", contentRange). 314 SetHeader("Content-Type", "application/octet-stream").SetBody(chunk2).Put(loc) 315 So(err, ShouldBeNil) 316 So(resp.StatusCode(), ShouldEqual, http.StatusCreated) 317 blobLoc := test.Location(baseURL, resp) 318 So(err, ShouldBeNil) 319 So(resp.StatusCode(), ShouldEqual, http.StatusCreated) 320 So(blobLoc, ShouldNotBeEmpty) 321 So(resp.Header().Get("Content-Length"), ShouldEqual, "0") 322 So(resp.Header().Get(constants.DistContentDigestKey), ShouldNotBeEmpty) 323 // upload reference should now be removed 324 resp, err = resty.R().Get(loc) 325 So(err, ShouldBeNil) 326 So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) 327 // blob reference should be accessible 328 resp, err = resty.R().Get(blobLoc) 329 So(err, ShouldBeNil) 330 So(resp.StatusCode(), ShouldEqual, http.StatusOK) 331 }) 332 333 Convey("Chunked blob upload with multiple name components", func() { 334 _, _ = Print("\nChunked blob upload with multiple name components") 335 resp, err := resty.R().Post(baseURL + "/v2/repo40/repo50/repo60/blobs/uploads/") 336 So(err, ShouldBeNil) 337 So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) 338 loc := test.Location(baseURL, resp) 339 So(loc, ShouldNotBeEmpty) 340 341 var buf bytes.Buffer 342 chunk1 := []byte("this is the first chunk2") 343 nbytes, err := buf.Write(chunk1) 344 So(nbytes, ShouldEqual, len(chunk1)) 345 So(err, ShouldBeNil) 346 347 // write first chunk 348 contentRange := fmt.Sprintf("%d-%d", 0, len(chunk1)-1) 349 resp, err = resty.R().SetHeader("Content-Type", "application/octet-stream"). 350 SetHeader("Content-Range", contentRange).SetBody(chunk1).Patch(loc) 351 So(err, ShouldBeNil) 352 So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) 353 354 // check progress 355 resp, err = resty.R().Get(loc) 356 So(err, ShouldBeNil) 357 So(resp.StatusCode(), ShouldEqual, http.StatusNoContent) 358 r := resp.Header().Get("Range") 359 So(r, ShouldNotBeEmpty) 360 So(r, ShouldEqual, contentRange) 361 362 // write same chunk should fail 363 contentRange = fmt.Sprintf("%d-%d", 0, len(chunk1)-1) 364 resp, err = resty.R().SetHeader("Content-Type", "application/octet-stream"). 365 SetHeader("Content-Range", contentRange).SetBody(chunk1).Patch(loc) 366 So(err, ShouldBeNil) 367 So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable) 368 So(resp.String(), ShouldNotBeEmpty) 369 370 chunk2 := []byte("this is the second chunk2") 371 nbytes, err = buf.Write(chunk2) 372 So(nbytes, ShouldEqual, len(chunk2)) 373 So(err, ShouldBeNil) 374 375 digest := godigest.FromBytes(buf.Bytes()) 376 So(digest, ShouldNotBeNil) 377 378 // write final chunk 379 contentRange = fmt.Sprintf("%d-%d", len(chunk1), len(buf.Bytes())-1) 380 resp, err = resty.R().SetQueryParam("digest", digest.String()). 381 SetHeader("Content-Range", contentRange). 382 SetHeader("Content-Type", "application/octet-stream").SetBody(chunk2).Put(loc) 383 So(err, ShouldBeNil) 384 So(resp.StatusCode(), ShouldEqual, http.StatusCreated) 385 blobLoc := test.Location(baseURL, resp) 386 So(err, ShouldBeNil) 387 So(resp.StatusCode(), ShouldEqual, http.StatusCreated) 388 So(blobLoc, ShouldNotBeEmpty) 389 So(resp.Header().Get("Content-Length"), ShouldEqual, "0") 390 So(resp.Header().Get(constants.DistContentDigestKey), ShouldNotBeEmpty) 391 // upload reference should now be removed 392 resp, err = resty.R().Get(loc) 393 So(err, ShouldBeNil) 394 So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) 395 // blob reference should be accessible 396 resp, err = resty.R().Get(blobLoc) 397 So(err, ShouldBeNil) 398 So(resp.StatusCode(), ShouldEqual, http.StatusOK) 399 }) 400 401 Convey("Create and delete uploads", func() { 402 _, _ = Print("\nCreate and delete uploads") 403 // create a upload 404 resp, err := resty.R().Post(baseURL + "/v2/repo4/blobs/uploads/") 405 So(err, ShouldBeNil) 406 So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) 407 loc := test.Location(baseURL, resp) 408 So(loc, ShouldNotBeEmpty) 409 410 // delete this upload 411 resp, err = resty.R().Delete(loc) 412 So(err, ShouldBeNil) 413 So(resp.StatusCode(), ShouldEqual, http.StatusNoContent) 414 }) 415 416 Convey("Create and delete blobs", func() { 417 _, _ = Print("\nCreate and delete blobs") 418 // create a upload 419 resp, err := resty.R().Post(baseURL + "/v2/repo5/blobs/uploads/") 420 So(err, ShouldBeNil) 421 So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) 422 loc := test.Location(baseURL, resp) 423 So(loc, ShouldNotBeEmpty) 424 425 content := []byte("this is a blob4") 426 digest := godigest.FromBytes(content) 427 So(digest, ShouldNotBeNil) 428 // monolithic blob upload 429 resp, err = resty.R().SetQueryParam("digest", digest.String()). 430 SetHeader("Content-Type", "application/octet-stream").SetBody(content).Put(loc) 431 So(err, ShouldBeNil) 432 So(resp.StatusCode(), ShouldEqual, http.StatusCreated) 433 blobLoc := test.Location(baseURL, resp) 434 So(blobLoc, ShouldNotBeEmpty) 435 So(resp.Header().Get(constants.DistContentDigestKey), ShouldNotBeEmpty) 436 437 // delete this blob 438 resp, err = resty.R().Delete(blobLoc) 439 So(err, ShouldBeNil) 440 So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) 441 So(resp.Header().Get("Content-Length"), ShouldEqual, "0") 442 }) 443 444 Convey("Mount blobs", func() { 445 _, _ = Print("\nMount blobs from another repository") 446 // create a upload 447 resp, err := resty.R().Post(baseURL + "/v2/repo6/blobs/uploads/?digest=\"abc\"&&from=\"xyz\"") 448 So(err, ShouldBeNil) 449 So(resp.StatusCode(), ShouldBeIn, []int{http.StatusCreated, http.StatusAccepted, http.StatusMethodNotAllowed}) 450 }) 451 452 Convey("Manifests", func() { 453 _, _ = Print("\nManifests") 454 // create a blob/layer 455 resp, err := resty.R().Post(baseURL + "/v2/repo7/blobs/uploads/") 456 So(err, ShouldBeNil) 457 So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) 458 loc := test.Location(baseURL, resp) 459 So(loc, ShouldNotBeEmpty) 460 461 // since we are not specifying any prefix i.e provided in config while starting server, 462 // so it should store repo7 to global root dir 463 _, err = os.Stat(path.Join(storageInfo[0], "repo7")) 464 So(err, ShouldBeNil) 465 466 resp, err = resty.R().Get(loc) 467 So(err, ShouldBeNil) 468 So(resp.StatusCode(), ShouldEqual, http.StatusNoContent) 469 content := []byte("this is a blob5") 470 digest := godigest.FromBytes(content) 471 So(digest, ShouldNotBeNil) 472 // monolithic blob upload: success 473 resp, err = resty.R().SetQueryParam("digest", digest.String()). 474 SetHeader("Content-Type", "application/octet-stream").SetBody(content).Put(loc) 475 So(err, ShouldBeNil) 476 So(resp.StatusCode(), ShouldEqual, http.StatusCreated) 477 blobLoc := resp.Header().Get("Location") 478 So(blobLoc, ShouldNotBeEmpty) 479 So(resp.Header().Get("Content-Length"), ShouldEqual, "0") 480 So(resp.Header().Get(constants.DistContentDigestKey), ShouldNotBeEmpty) 481 482 // check a non-existent manifest 483 resp, err = resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json"). 484 SetBody(content).Head(baseURL + "/v2/unknown/manifests/test:1.0") 485 So(err, ShouldBeNil) 486 So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) 487 488 img := image.CreateDefaultImage() 489 digest = img.ManifestDescriptor.Digest 490 491 repoName := "repo7" 492 err = image.UploadImage(img, baseURL, repoName, "test:1.0") 493 So(err, ShouldBeNil) 494 495 err = image.UploadImage(img, baseURL, repoName, "test:1.0.1") 496 So(err, ShouldBeNil) 497 498 err = image.UploadImage(img, baseURL, repoName, "test:2.0") 499 So(err, ShouldBeNil) 500 501 // check/get by tag 502 resp, err = resty.R().Head(baseURL + "/v2/repo7/manifests/test:1.0") 503 So(err, ShouldBeNil) 504 So(resp.StatusCode(), ShouldEqual, http.StatusOK) 505 So(resp.Header().Get("Content-Type"), ShouldNotBeEmpty) 506 resp, err = resty.R().Get(baseURL + "/v2/repo7/manifests/test:1.0") 507 So(err, ShouldBeNil) 508 So(resp.StatusCode(), ShouldEqual, http.StatusOK) 509 So(resp.Body(), ShouldNotBeEmpty) 510 // check/get by reference 511 resp, err = resty.R().Head(baseURL + "/v2/repo7/manifests/" + digest.String()) 512 So(err, ShouldBeNil) 513 So(resp.StatusCode(), ShouldEqual, http.StatusOK) 514 So(resp.Header().Get("Content-Type"), ShouldNotBeEmpty) 515 resp, err = resty.R().Get(baseURL + "/v2/repo7/manifests/" + digest.String()) 516 So(err, ShouldBeNil) 517 So(resp.StatusCode(), ShouldEqual, http.StatusOK) 518 So(resp.Body(), ShouldNotBeEmpty) 519 520 // delete manifest by tag should pass 521 resp, err = resty.R().Delete(baseURL + "/v2/repo7/manifests/test:1.0") 522 So(err, ShouldBeNil) 523 So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) 524 // delete manifest by digest (1.0 deleted but 1.0.1 has same reference) 525 resp, err = resty.R().Delete(baseURL + "/v2/repo7/manifests/" + digest.String()) 526 So(err, ShouldBeNil) 527 So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) 528 // delete manifest by digest 529 resp, err = resty.R().Delete(baseURL + "/v2/repo7/manifests/" + digest.String()) 530 So(err, ShouldBeNil) 531 So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) 532 // delete again should fail 533 resp, err = resty.R().Delete(baseURL + "/v2/repo7/manifests/" + digest.String()) 534 So(err, ShouldBeNil) 535 So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) 536 537 // check/get by tag 538 resp, err = resty.R().Head(baseURL + "/v2/repo7/manifests/test:1.0") 539 So(err, ShouldBeNil) 540 So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) 541 resp, err = resty.R().Get(baseURL + "/v2/repo7/manifests/test:1.0") 542 So(err, ShouldBeNil) 543 So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) 544 So(resp.Body(), ShouldNotBeEmpty) 545 resp, err = resty.R().Head(baseURL + "/v2/repo7/manifests/test:2.0") 546 So(err, ShouldBeNil) 547 So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) 548 resp, err = resty.R().Get(baseURL + "/v2/repo7/manifests/test:2.0") 549 So(err, ShouldBeNil) 550 So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) 551 So(resp.Body(), ShouldNotBeEmpty) 552 // check/get by reference 553 resp, err = resty.R().Head(baseURL + "/v2/repo7/manifests/" + digest.String()) 554 So(err, ShouldBeNil) 555 So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) 556 resp, err = resty.R().Get(baseURL + "/v2/repo7/manifests/" + digest.String()) 557 So(err, ShouldBeNil) 558 So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) 559 So(resp.Body(), ShouldNotBeEmpty) 560 }) 561 562 // pagination 563 Convey("Pagination", func() { 564 _, _ = Print("\nPagination") 565 566 img := image.CreateDefaultImage() 567 568 for index := 0; index <= 4; index++ { 569 repoName := "page0" 570 err := image.UploadImage( 571 img, baseURL, repoName, fmt.Sprintf("test:%d.0", index)) 572 So(err, ShouldBeNil) 573 } 574 575 resp, err := resty.R().Get(baseURL + "/v2/page0/tags/list") 576 So(err, ShouldBeNil) 577 So(resp.StatusCode(), ShouldEqual, http.StatusOK) 578 579 resp, err = resty.R().Get(baseURL + "/v2/page0/tags/list?n= ") 580 So(err, ShouldBeNil) 581 So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest) 582 583 resp, err = resty.R().Get(baseURL + "/v2/page0/tags/list?n=a") 584 So(err, ShouldBeNil) 585 So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest) 586 587 resp, err = resty.R().Get(baseURL + "/v2/page0/tags/list?n=0") 588 So(err, ShouldBeNil) 589 So(resp.StatusCode(), ShouldEqual, http.StatusOK) 590 591 resp, err = resty.R().Get(baseURL + "/v2/page0/tags/list?n=0&last=100") 592 So(err, ShouldBeNil) 593 So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) 594 595 resp, err = resty.R().Get(baseURL + "/v2/page0/tags/list?n=0&last=test:0.0") 596 So(err, ShouldBeNil) 597 So(resp.StatusCode(), ShouldEqual, http.StatusOK) 598 599 resp, err = resty.R().Get(baseURL + "/v2/page0/tags/list?n=3") 600 So(err, ShouldBeNil) 601 So(resp.StatusCode(), ShouldEqual, http.StatusOK) 602 next := resp.Header().Get("Link") 603 So(next, ShouldNotBeEmpty) 604 605 nextURL := strings.Split(next, ";")[0] 606 if strings.HasPrefix(nextURL, "<") || strings.HasPrefix(nextURL, "\"") { 607 nextURL = nextURL[1:] 608 } 609 if strings.HasSuffix(nextURL, ">") || strings.HasSuffix(nextURL, "\"") { 610 nextURL = nextURL[:len(nextURL)-1] 611 } 612 nextURL = baseURL + nextURL 613 614 resp, err = resty.R().Get(nextURL) 615 So(err, ShouldBeNil) 616 So(resp.StatusCode(), ShouldEqual, http.StatusOK) 617 next = resp.Header().Get("Link") 618 So(next, ShouldBeEmpty) 619 }) 620 621 // this is an additional test for repository names (alphanumeric) 622 Convey("Repository names", func() { 623 _, _ = Print("\nRepository names") 624 // create a blob/layer 625 resp, err := resty.R().Post(baseURL + "/v2/repotest/blobs/uploads/") 626 So(err, ShouldBeNil) 627 So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) 628 resp, err = resty.R().Post(baseURL + "/v2/repotest123/blobs/uploads/") 629 So(err, ShouldBeNil) 630 So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) 631 }) 632 633 Convey("Multiple Storage", func() { 634 // test APIS on subpath routes, default storage already tested above 635 // subpath route firsttest 636 resp, err := resty.R().Post(baseURL + "/v2/firsttest/first/blobs/uploads/") 637 So(err, ShouldBeNil) 638 So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) 639 firstloc := test.Location(baseURL, resp) 640 So(firstloc, ShouldNotBeEmpty) 641 642 resp, err = resty.R().Get(firstloc) 643 So(err, ShouldBeNil) 644 So(resp.StatusCode(), ShouldEqual, http.StatusNoContent) 645 646 // if firsttest route is used as prefix in url that means repo should be stored in subpaths["firsttest"] rootdir 647 _, err = os.Stat(path.Join(storageInfo[1], "firsttest/first")) 648 So(err, ShouldBeNil) 649 650 // subpath route secondtest 651 resp, err = resty.R().Post(baseURL + "/v2/secondtest/second/blobs/uploads/") 652 So(err, ShouldBeNil) 653 So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) 654 secondloc := test.Location(baseURL, resp) 655 So(secondloc, ShouldNotBeEmpty) 656 657 resp, err = resty.R().Get(secondloc) 658 So(err, ShouldBeNil) 659 So(resp.StatusCode(), ShouldEqual, http.StatusNoContent) 660 661 // if secondtest route is used as prefix in url that means repo should be stored in subpaths["secondtest"] rootdir 662 _, err = os.Stat(path.Join(storageInfo[2], "secondtest/second")) 663 So(err, ShouldBeNil) 664 665 content := []byte("this is a blob5") 666 digest := godigest.FromBytes(content) 667 So(digest, ShouldNotBeNil) 668 // monolithic blob upload: success 669 // first test 670 resp, err = resty.R().SetQueryParam("digest", digest.String()). 671 SetHeader("Content-Type", "application/octet-stream").SetBody(content).Put(firstloc) 672 So(err, ShouldBeNil) 673 So(resp.StatusCode(), ShouldEqual, http.StatusCreated) 674 firstblobLoc := resp.Header().Get("Location") 675 So(firstblobLoc, ShouldNotBeEmpty) 676 So(resp.Header().Get("Content-Length"), ShouldEqual, "0") 677 So(resp.Header().Get(constants.DistContentDigestKey), ShouldNotBeEmpty) 678 679 // second test 680 resp, err = resty.R().SetQueryParam("digest", digest.String()). 681 SetHeader("Content-Type", "application/octet-stream").SetBody(content).Put(secondloc) 682 So(err, ShouldBeNil) 683 So(resp.StatusCode(), ShouldEqual, http.StatusCreated) 684 secondblobLoc := resp.Header().Get("Location") 685 So(secondblobLoc, ShouldNotBeEmpty) 686 So(resp.Header().Get("Content-Length"), ShouldEqual, "0") 687 So(resp.Header().Get(constants.DistContentDigestKey), ShouldNotBeEmpty) 688 689 // check a non-existent manifest 690 resp, err = resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json"). 691 SetBody(content).Head(baseURL + "/v2/unknown/manifests/test:1.0") 692 So(err, ShouldBeNil) 693 So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) 694 695 resp, err = resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json"). 696 SetBody(content).Head(baseURL + "/v2/firsttest/unknown/manifests/test:1.0") 697 So(err, ShouldBeNil) 698 So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) 699 700 resp, err = resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json"). 701 SetBody(content).Head(baseURL + "/v2/secondtest/unknown/manifests/test:1.0") 702 So(err, ShouldBeNil) 703 So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) 704 705 img := image.CreateDefaultImage() 706 digest = img.ManifestDescriptor.Digest 707 708 // subpath firsttest 709 err = image.UploadImage(img, baseURL, "firsttest/first", "test:1.0") 710 So(err, ShouldBeNil) 711 712 // subpath secondtest 713 err = image.UploadImage(img, baseURL, "secondtest/second", "test:1.0") 714 So(err, ShouldBeNil) 715 716 // subpath firsttest 717 err = image.UploadImage(img, baseURL, "firsttest/first", "test:2.0") 718 So(err, ShouldBeNil) 719 720 // subpath secondtest 721 err = image.UploadImage(img, baseURL, "secondtest/second", "test:2.0") 722 So(err, ShouldBeNil) 723 724 // check/get by tag 725 resp, err = resty.R().Head(baseURL + "/v2/firsttest/first/manifests/test:1.0") 726 So(err, ShouldBeNil) 727 So(resp.StatusCode(), ShouldEqual, http.StatusOK) 728 resp, err = resty.R().Get(baseURL + "/v2/firsttest/first/manifests/test:1.0") 729 So(err, ShouldBeNil) 730 So(resp.StatusCode(), ShouldEqual, http.StatusOK) 731 So(resp.Body(), ShouldNotBeEmpty) 732 resp, err = resty.R().Head(baseURL + "/v2/secondtest/second/manifests/test:1.0") 733 So(err, ShouldBeNil) 734 So(resp.StatusCode(), ShouldEqual, http.StatusOK) 735 resp, err = resty.R().Get(baseURL + "/v2/secondtest/second/manifests/test:1.0") 736 So(err, ShouldBeNil) 737 So(resp.StatusCode(), ShouldEqual, http.StatusOK) 738 So(resp.Body(), ShouldNotBeEmpty) 739 740 // check/get by reference 741 resp, err = resty.R().Head(baseURL + "/v2/firsttest/first/manifests/" + digest.String()) 742 So(err, ShouldBeNil) 743 So(resp.StatusCode(), ShouldEqual, http.StatusOK) 744 resp, err = resty.R().Get(baseURL + "/v2/firsttest/first/manifests/" + digest.String()) 745 So(err, ShouldBeNil) 746 So(resp.StatusCode(), ShouldEqual, http.StatusOK) 747 So(resp.Body(), ShouldNotBeEmpty) 748 749 resp, err = resty.R().Head(baseURL + "/v2/secondtest/second/manifests/" + digest.String()) 750 So(err, ShouldBeNil) 751 So(resp.StatusCode(), ShouldEqual, http.StatusOK) 752 resp, err = resty.R().Get(baseURL + "/v2/secondtest/second/manifests/" + digest.String()) 753 So(err, ShouldBeNil) 754 So(resp.StatusCode(), ShouldEqual, http.StatusOK) 755 So(resp.Body(), ShouldNotBeEmpty) 756 757 // delete manifest by digest 758 resp, err = resty.R().Delete(baseURL + "/v2/firsttest/first/manifests/" + digest.String()) 759 So(err, ShouldBeNil) 760 So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) 761 762 resp, err = resty.R().Delete(baseURL + "/v2/secondtest/second/manifests/" + digest.String()) 763 So(err, ShouldBeNil) 764 So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) 765 766 // delete manifest by digest 767 resp, err = resty.R().Delete(baseURL + "/v2/firsttest/first/manifests/" + digest.String()) 768 So(err, ShouldBeNil) 769 So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) 770 771 resp, err = resty.R().Delete(baseURL + "/v2/secondtest/second/manifests/" + digest.String()) 772 So(err, ShouldBeNil) 773 So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) 774 775 // delete again should fail 776 resp, err = resty.R().Delete(baseURL + "/v2/firsttest/first/manifests/" + digest.String()) 777 So(err, ShouldBeNil) 778 So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) 779 780 resp, err = resty.R().Delete(baseURL + "/v2/secondtest/second/manifests/" + digest.String()) 781 So(err, ShouldBeNil) 782 So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) 783 784 // check/get by tag 785 resp, err = resty.R().Head(baseURL + "/v2/firsttest/first/manifests/test:1.0") 786 So(err, ShouldBeNil) 787 So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) 788 resp, err = resty.R().Get(baseURL + "/v2/firsttest/first/manifests/test:1.0") 789 So(err, ShouldBeNil) 790 So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) 791 So(resp.Body(), ShouldNotBeEmpty) 792 793 resp, err = resty.R().Head(baseURL + "/v2/secondtest/second/manifests/test:1.0") 794 So(err, ShouldBeNil) 795 So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) 796 resp, err = resty.R().Get(baseURL + "/v2/secondtest/second/manifests/test:1.0") 797 So(err, ShouldBeNil) 798 So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) 799 So(resp.Body(), ShouldNotBeEmpty) 800 801 resp, err = resty.R().Head(baseURL + "/v2/firsttest/first/repo7/manifests/test:2.0") 802 So(err, ShouldBeNil) 803 So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) 804 resp, err = resty.R().Get(baseURL + "/v2/firsttest/first/manifests/test:2.0") 805 So(err, ShouldBeNil) 806 So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) 807 So(resp.Body(), ShouldNotBeEmpty) 808 809 resp, err = resty.R().Head(baseURL + "/v2/secondtest/second/manifests/test:2.0") 810 So(err, ShouldBeNil) 811 So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) 812 resp, err = resty.R().Get(baseURL + "/v2/secondtest/second/manifests/test:2.0") 813 So(err, ShouldBeNil) 814 So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) 815 So(resp.Body(), ShouldNotBeEmpty) 816 817 // check/get by reference 818 resp, err = resty.R().Head(baseURL + "/v2/firsttest/first/manifests/" + digest.String()) 819 So(err, ShouldBeNil) 820 So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) 821 resp, err = resty.R().Get(baseURL + "/v2/firsttest/first/manifests/" + digest.String()) 822 So(err, ShouldBeNil) 823 So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) 824 So(resp.Body(), ShouldNotBeEmpty) 825 826 resp, err = resty.R().Head(baseURL + "/v2/secondtest/second/manifests/" + digest.String()) 827 So(err, ShouldBeNil) 828 So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) 829 resp, err = resty.R().Get(baseURL + "/v2/secondtest/second/manifests/" + digest.String()) 830 So(err, ShouldBeNil) 831 So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) 832 So(resp.Body(), ShouldNotBeEmpty) 833 }) 834 }) 835 } 836 837 //nolint:gochecknoglobals 838 var ( 839 old *os.File 840 r *os.File 841 w *os.File 842 outC chan string 843 ) 844 845 func outputJSONEnter() { 846 // this env var instructs goconvey to output results to JSON (stdout) 847 os.Setenv("GOCONVEY_REPORTER", "json") 848 849 // stdout capture copied from: https://stackoverflow.com/a/29339052 850 old = os.Stdout 851 // keep backup of the real stdout 852 r, w, _ = os.Pipe() 853 outC = make(chan string) 854 os.Stdout = w 855 856 // copy the output in a separate goroutine so printing can't block indefinitely 857 go func() { 858 var buf bytes.Buffer 859 860 _, err := io.Copy(&buf, r) 861 if err != nil { 862 panic(err) 863 } 864 865 outC <- buf.String() 866 }() 867 } 868 869 func outputJSONExit() { 870 // back to normal state 871 w.Close() 872 873 os.Stdout = old // restoring the real stdout 874 875 out := <-outC 876 877 // The output of JSON is combined with regular output, so we look for the 878 // first occurrence of the "{" character and take everything after that 879 rawJSON := "[{" + strings.Join(strings.Split(out, "{")[1:], "{") 880 rawJSON = strings.Replace(rawJSON, reporting.OpenJson, "", 1) 881 rawJSON = strings.Replace(rawJSON, reporting.CloseJson, "", 1) 882 tmp := strings.Split(rawJSON, ",") 883 rawJSON = strings.Join(tmp[0:len(tmp)-1], ",") + "]" 884 885 rawJSONMinified := validateMinifyRawJSON(rawJSON) 886 fmt.Println(rawJSONMinified) 887 } 888 889 func validateMinifyRawJSON(rawJSON string) string { 890 var jsonData interface{} 891 892 err := json.Unmarshal([]byte(rawJSON), &jsonData) 893 if err != nil { 894 panic(err) 895 } 896 897 rawJSONBytesMinified, err := json.Marshal(jsonData) 898 if err != nil { 899 panic(err) 900 } 901 902 return string(rawJSONBytesMinified) 903 }