code.gitea.io/gitea@v1.22.3/routers/api/packages/container/container.go (about) 1 // Copyright 2021 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package container 5 6 import ( 7 "errors" 8 "fmt" 9 "io" 10 "net/http" 11 "net/url" 12 "os" 13 "regexp" 14 "strconv" 15 "strings" 16 17 auth_model "code.gitea.io/gitea/models/auth" 18 packages_model "code.gitea.io/gitea/models/packages" 19 container_model "code.gitea.io/gitea/models/packages/container" 20 user_model "code.gitea.io/gitea/models/user" 21 "code.gitea.io/gitea/modules/httplib" 22 "code.gitea.io/gitea/modules/json" 23 "code.gitea.io/gitea/modules/log" 24 packages_module "code.gitea.io/gitea/modules/packages" 25 container_module "code.gitea.io/gitea/modules/packages/container" 26 "code.gitea.io/gitea/modules/setting" 27 "code.gitea.io/gitea/modules/sync" 28 "code.gitea.io/gitea/modules/util" 29 "code.gitea.io/gitea/routers/api/packages/helper" 30 auth_service "code.gitea.io/gitea/services/auth" 31 "code.gitea.io/gitea/services/context" 32 packages_service "code.gitea.io/gitea/services/packages" 33 container_service "code.gitea.io/gitea/services/packages/container" 34 35 digest "github.com/opencontainers/go-digest" 36 ) 37 38 // maximum size of a container manifest 39 // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-manifests 40 const maxManifestSize = 10 * 1024 * 1024 41 42 var ( 43 imageNamePattern = regexp.MustCompile(`\A[a-z0-9]+([._-][a-z0-9]+)*(/[a-z0-9]+([._-][a-z0-9]+)*)*\z`) 44 referencePattern = regexp.MustCompile(`\A[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}\z`) 45 ) 46 47 type containerHeaders struct { 48 Status int 49 ContentDigest string 50 UploadUUID string 51 Range string 52 Location string 53 ContentType string 54 ContentLength int64 55 } 56 57 // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#legacy-docker-support-http-headers 58 func setResponseHeaders(resp http.ResponseWriter, h *containerHeaders) { 59 if h.Location != "" { 60 resp.Header().Set("Location", h.Location) 61 } 62 if h.Range != "" { 63 resp.Header().Set("Range", h.Range) 64 } 65 if h.ContentType != "" { 66 resp.Header().Set("Content-Type", h.ContentType) 67 } 68 if h.ContentLength != 0 { 69 resp.Header().Set("Content-Length", strconv.FormatInt(h.ContentLength, 10)) 70 } 71 if h.UploadUUID != "" { 72 resp.Header().Set("Docker-Upload-Uuid", h.UploadUUID) 73 } 74 if h.ContentDigest != "" { 75 resp.Header().Set("Docker-Content-Digest", h.ContentDigest) 76 resp.Header().Set("ETag", fmt.Sprintf(`"%s"`, h.ContentDigest)) 77 } 78 resp.Header().Set("Docker-Distribution-Api-Version", "registry/2.0") 79 resp.WriteHeader(h.Status) 80 } 81 82 func jsonResponse(ctx *context.Context, status int, obj any) { 83 setResponseHeaders(ctx.Resp, &containerHeaders{ 84 Status: status, 85 ContentType: "application/json", 86 }) 87 if err := json.NewEncoder(ctx.Resp).Encode(obj); err != nil { 88 log.Error("JSON encode: %v", err) 89 } 90 } 91 92 func apiError(ctx *context.Context, status int, err error) { 93 helper.LogAndProcessError(ctx, status, err, func(message string) { 94 setResponseHeaders(ctx.Resp, &containerHeaders{ 95 Status: status, 96 }) 97 }) 98 } 99 100 // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#error-codes 101 func apiErrorDefined(ctx *context.Context, err *namedError) { 102 type ContainerError struct { 103 Code string `json:"code"` 104 Message string `json:"message"` 105 } 106 107 type ContainerErrors struct { 108 Errors []ContainerError `json:"errors"` 109 } 110 111 jsonResponse(ctx, err.StatusCode, ContainerErrors{ 112 Errors: []ContainerError{ 113 { 114 Code: err.Code, 115 Message: err.Message, 116 }, 117 }, 118 }) 119 } 120 121 func apiUnauthorizedError(ctx *context.Context) { 122 // container registry requires that the "/v2" must be in the root, so the sub-path in AppURL should be removed 123 realmURL := httplib.GuessCurrentHostURL(ctx) + "/v2/token" 124 ctx.Resp.Header().Add("WWW-Authenticate", `Bearer realm="`+realmURL+`",service="container_registry",scope="*"`) 125 apiErrorDefined(ctx, errUnauthorized) 126 } 127 128 // ReqContainerAccess is a middleware which checks the current user valid (real user or ghost if anonymous access is enabled) 129 func ReqContainerAccess(ctx *context.Context) { 130 if ctx.Doer == nil || (setting.Service.RequireSignInView && ctx.Doer.IsGhost()) { 131 apiUnauthorizedError(ctx) 132 } 133 } 134 135 // VerifyImageName is a middleware which checks if the image name is allowed 136 func VerifyImageName(ctx *context.Context) { 137 if !imageNamePattern.MatchString(ctx.Params("image")) { 138 apiErrorDefined(ctx, errNameInvalid) 139 } 140 } 141 142 // DetermineSupport is used to test if the registry supports OCI 143 // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#determining-support 144 func DetermineSupport(ctx *context.Context) { 145 setResponseHeaders(ctx.Resp, &containerHeaders{ 146 Status: http.StatusOK, 147 }) 148 } 149 150 // Authenticate creates a token for the current user 151 // If the current user is anonymous, the ghost user is used unless RequireSignInView is enabled. 152 func Authenticate(ctx *context.Context) { 153 u := ctx.Doer 154 packageScope := auth_service.GetAccessScope(ctx.Data) 155 if u == nil { 156 if setting.Service.RequireSignInView { 157 apiUnauthorizedError(ctx) 158 return 159 } 160 161 u = user_model.NewGhostUser() 162 } else { 163 if has, err := packageScope.HasAnyScope( 164 auth_model.AccessTokenScopeReadPackage, 165 auth_model.AccessTokenScopeWritePackage, 166 auth_model.AccessTokenScopeAll, 167 ); !has { 168 if err != nil { 169 log.Error("Error checking access scope: %v", err) 170 } 171 apiUnauthorizedError(ctx) 172 return 173 } 174 } 175 176 token, err := packages_service.CreateAuthorizationToken(u, packageScope) 177 if err != nil { 178 apiError(ctx, http.StatusInternalServerError, err) 179 return 180 } 181 182 ctx.JSON(http.StatusOK, map[string]string{ 183 "token": token, 184 }) 185 } 186 187 // https://distribution.github.io/distribution/spec/auth/oauth/ 188 func AuthenticateNotImplemented(ctx *context.Context) { 189 // This optional endpoint can be used to authenticate a client. 190 // It must implement the specification described in: 191 // https://datatracker.ietf.org/doc/html/rfc6749 192 // https://distribution.github.io/distribution/spec/auth/oauth/ 193 // Purpose of this stub is to respond with 404 Not Found instead of 405 Method Not Allowed. 194 195 ctx.Status(http.StatusNotFound) 196 } 197 198 // https://docs.docker.com/registry/spec/api/#listing-repositories 199 func GetRepositoryList(ctx *context.Context) { 200 n := ctx.FormInt("n") 201 if n <= 0 || n > 100 { 202 n = 100 203 } 204 last := ctx.FormTrim("last") 205 206 repositories, err := container_model.GetRepositories(ctx, ctx.Doer, n, last) 207 if err != nil { 208 apiError(ctx, http.StatusInternalServerError, err) 209 return 210 } 211 212 type RepositoryList struct { 213 Repositories []string `json:"repositories"` 214 } 215 216 if len(repositories) == n { 217 v := url.Values{} 218 if n > 0 { 219 v.Add("n", strconv.Itoa(n)) 220 } 221 v.Add("last", repositories[len(repositories)-1]) 222 223 ctx.Resp.Header().Set("Link", fmt.Sprintf(`</v2/_catalog?%s>; rel="next"`, v.Encode())) 224 } 225 226 jsonResponse(ctx, http.StatusOK, RepositoryList{ 227 Repositories: repositories, 228 }) 229 } 230 231 // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#mounting-a-blob-from-another-repository 232 // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#single-post 233 // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks 234 func InitiateUploadBlob(ctx *context.Context) { 235 image := ctx.Params("image") 236 237 mount := ctx.FormTrim("mount") 238 from := ctx.FormTrim("from") 239 if mount != "" { 240 blob, _ := workaroundGetContainerBlob(ctx, &container_model.BlobSearchOptions{ 241 Repository: from, 242 Digest: mount, 243 }) 244 if blob != nil { 245 accessible, err := packages_model.IsBlobAccessibleForUser(ctx, blob.Blob.ID, ctx.Doer) 246 if err != nil { 247 apiError(ctx, http.StatusInternalServerError, err) 248 return 249 } 250 251 if accessible { 252 if err := mountBlob(ctx, &packages_service.PackageInfo{Owner: ctx.Package.Owner, Name: image}, blob.Blob); err != nil { 253 apiError(ctx, http.StatusInternalServerError, err) 254 return 255 } 256 257 setResponseHeaders(ctx.Resp, &containerHeaders{ 258 Location: fmt.Sprintf("/v2/%s/%s/blobs/%s", ctx.Package.Owner.LowerName, image, mount), 259 ContentDigest: mount, 260 Status: http.StatusCreated, 261 }) 262 return 263 } 264 } 265 } 266 267 digest := ctx.FormTrim("digest") 268 if digest != "" { 269 buf, err := packages_module.CreateHashedBufferFromReader(ctx.Req.Body) 270 if err != nil { 271 apiError(ctx, http.StatusInternalServerError, err) 272 return 273 } 274 defer buf.Close() 275 276 if digest != digestFromHashSummer(buf) { 277 apiErrorDefined(ctx, errDigestInvalid) 278 return 279 } 280 281 if _, err := saveAsPackageBlob(ctx, 282 buf, 283 &packages_service.PackageCreationInfo{ 284 PackageInfo: packages_service.PackageInfo{ 285 Owner: ctx.Package.Owner, 286 Name: image, 287 }, 288 Creator: ctx.Doer, 289 }, 290 ); err != nil { 291 switch err { 292 case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize: 293 apiError(ctx, http.StatusForbidden, err) 294 default: 295 apiError(ctx, http.StatusInternalServerError, err) 296 } 297 return 298 } 299 300 setResponseHeaders(ctx.Resp, &containerHeaders{ 301 Location: fmt.Sprintf("/v2/%s/%s/blobs/%s", ctx.Package.Owner.LowerName, image, digest), 302 ContentDigest: digest, 303 Status: http.StatusCreated, 304 }) 305 return 306 } 307 308 upload, err := packages_model.CreateBlobUpload(ctx) 309 if err != nil { 310 apiError(ctx, http.StatusInternalServerError, err) 311 return 312 } 313 314 setResponseHeaders(ctx.Resp, &containerHeaders{ 315 Location: fmt.Sprintf("/v2/%s/%s/blobs/uploads/%s", ctx.Package.Owner.LowerName, image, upload.ID), 316 Range: "0-0", 317 UploadUUID: upload.ID, 318 Status: http.StatusAccepted, 319 }) 320 } 321 322 // https://docs.docker.com/registry/spec/api/#get-blob-upload 323 func GetUploadBlob(ctx *context.Context) { 324 uuid := ctx.Params("uuid") 325 326 upload, err := packages_model.GetBlobUploadByID(ctx, uuid) 327 if err != nil { 328 if err == packages_model.ErrPackageBlobUploadNotExist { 329 apiErrorDefined(ctx, errBlobUploadUnknown) 330 } else { 331 apiError(ctx, http.StatusInternalServerError, err) 332 } 333 return 334 } 335 336 setResponseHeaders(ctx.Resp, &containerHeaders{ 337 Range: fmt.Sprintf("0-%d", upload.BytesReceived), 338 UploadUUID: upload.ID, 339 Status: http.StatusNoContent, 340 }) 341 } 342 343 // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks 344 func UploadBlob(ctx *context.Context) { 345 image := ctx.Params("image") 346 347 uploader, err := container_service.NewBlobUploader(ctx, ctx.Params("uuid")) 348 if err != nil { 349 if err == packages_model.ErrPackageBlobUploadNotExist { 350 apiErrorDefined(ctx, errBlobUploadUnknown) 351 } else { 352 apiError(ctx, http.StatusInternalServerError, err) 353 } 354 return 355 } 356 defer uploader.Close() 357 358 contentRange := ctx.Req.Header.Get("Content-Range") 359 if contentRange != "" { 360 start, end := 0, 0 361 if _, err := fmt.Sscanf(contentRange, "%d-%d", &start, &end); err != nil { 362 apiErrorDefined(ctx, errBlobUploadInvalid) 363 return 364 } 365 366 if int64(start) != uploader.Size() { 367 apiErrorDefined(ctx, errBlobUploadInvalid.WithStatusCode(http.StatusRequestedRangeNotSatisfiable)) 368 return 369 } 370 } else if uploader.Size() != 0 { 371 apiErrorDefined(ctx, errBlobUploadInvalid.WithMessage("Stream uploads after first write are not allowed")) 372 return 373 } 374 375 if err := uploader.Append(ctx, ctx.Req.Body); err != nil { 376 apiError(ctx, http.StatusInternalServerError, err) 377 return 378 } 379 380 setResponseHeaders(ctx.Resp, &containerHeaders{ 381 Location: fmt.Sprintf("/v2/%s/%s/blobs/uploads/%s", ctx.Package.Owner.LowerName, image, uploader.ID), 382 Range: fmt.Sprintf("0-%d", uploader.Size()-1), 383 UploadUUID: uploader.ID, 384 Status: http.StatusAccepted, 385 }) 386 } 387 388 // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks 389 func EndUploadBlob(ctx *context.Context) { 390 image := ctx.Params("image") 391 392 digest := ctx.FormTrim("digest") 393 if digest == "" { 394 apiErrorDefined(ctx, errDigestInvalid) 395 return 396 } 397 398 uploader, err := container_service.NewBlobUploader(ctx, ctx.Params("uuid")) 399 if err != nil { 400 if err == packages_model.ErrPackageBlobUploadNotExist { 401 apiErrorDefined(ctx, errBlobUploadUnknown) 402 } else { 403 apiError(ctx, http.StatusInternalServerError, err) 404 } 405 return 406 } 407 doClose := true 408 defer func() { 409 if doClose { 410 uploader.Close() 411 } 412 }() 413 414 if ctx.Req.Body != nil { 415 if err := uploader.Append(ctx, ctx.Req.Body); err != nil { 416 apiError(ctx, http.StatusInternalServerError, err) 417 return 418 } 419 } 420 421 if digest != digestFromHashSummer(uploader) { 422 apiErrorDefined(ctx, errDigestInvalid) 423 return 424 } 425 426 if _, err := saveAsPackageBlob(ctx, 427 uploader, 428 &packages_service.PackageCreationInfo{ 429 PackageInfo: packages_service.PackageInfo{ 430 Owner: ctx.Package.Owner, 431 Name: image, 432 }, 433 Creator: ctx.Doer, 434 }, 435 ); err != nil { 436 switch err { 437 case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize: 438 apiError(ctx, http.StatusForbidden, err) 439 default: 440 apiError(ctx, http.StatusInternalServerError, err) 441 } 442 return 443 } 444 445 if err := uploader.Close(); err != nil { 446 apiError(ctx, http.StatusInternalServerError, err) 447 return 448 } 449 doClose = false 450 451 if err := container_service.RemoveBlobUploadByID(ctx, uploader.ID); err != nil { 452 apiError(ctx, http.StatusInternalServerError, err) 453 return 454 } 455 456 setResponseHeaders(ctx.Resp, &containerHeaders{ 457 Location: fmt.Sprintf("/v2/%s/%s/blobs/%s", ctx.Package.Owner.LowerName, image, digest), 458 ContentDigest: digest, 459 Status: http.StatusCreated, 460 }) 461 } 462 463 // https://docs.docker.com/registry/spec/api/#delete-blob-upload 464 func CancelUploadBlob(ctx *context.Context) { 465 uuid := ctx.Params("uuid") 466 467 _, err := packages_model.GetBlobUploadByID(ctx, uuid) 468 if err != nil { 469 if err == packages_model.ErrPackageBlobUploadNotExist { 470 apiErrorDefined(ctx, errBlobUploadUnknown) 471 } else { 472 apiError(ctx, http.StatusInternalServerError, err) 473 } 474 return 475 } 476 477 if err := container_service.RemoveBlobUploadByID(ctx, uuid); err != nil { 478 apiError(ctx, http.StatusInternalServerError, err) 479 return 480 } 481 482 setResponseHeaders(ctx.Resp, &containerHeaders{ 483 Status: http.StatusNoContent, 484 }) 485 } 486 487 func getBlobFromContext(ctx *context.Context) (*packages_model.PackageFileDescriptor, error) { 488 d := ctx.Params("digest") 489 490 if digest.Digest(d).Validate() != nil { 491 return nil, container_model.ErrContainerBlobNotExist 492 } 493 494 return workaroundGetContainerBlob(ctx, &container_model.BlobSearchOptions{ 495 OwnerID: ctx.Package.Owner.ID, 496 Image: ctx.Params("image"), 497 Digest: d, 498 }) 499 } 500 501 // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#checking-if-content-exists-in-the-registry 502 func HeadBlob(ctx *context.Context) { 503 blob, err := getBlobFromContext(ctx) 504 if err != nil { 505 if err == container_model.ErrContainerBlobNotExist { 506 apiErrorDefined(ctx, errBlobUnknown) 507 } else { 508 apiError(ctx, http.StatusInternalServerError, err) 509 } 510 return 511 } 512 513 setResponseHeaders(ctx.Resp, &containerHeaders{ 514 ContentDigest: blob.Properties.GetByName(container_module.PropertyDigest), 515 ContentLength: blob.Blob.Size, 516 Status: http.StatusOK, 517 }) 518 } 519 520 // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pulling-blobs 521 func GetBlob(ctx *context.Context) { 522 blob, err := getBlobFromContext(ctx) 523 if err != nil { 524 if err == container_model.ErrContainerBlobNotExist { 525 apiErrorDefined(ctx, errBlobUnknown) 526 } else { 527 apiError(ctx, http.StatusInternalServerError, err) 528 } 529 return 530 } 531 532 serveBlob(ctx, blob) 533 } 534 535 // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#deleting-blobs 536 func DeleteBlob(ctx *context.Context) { 537 d := ctx.Params("digest") 538 539 if digest.Digest(d).Validate() != nil { 540 apiErrorDefined(ctx, errBlobUnknown) 541 return 542 } 543 544 if err := deleteBlob(ctx, ctx.Package.Owner, ctx.Params("image"), d); err != nil { 545 apiError(ctx, http.StatusInternalServerError, err) 546 return 547 } 548 549 setResponseHeaders(ctx.Resp, &containerHeaders{ 550 Status: http.StatusAccepted, 551 }) 552 } 553 554 // TODO: use clustered lock 555 var lockManifest = sync.NewExclusivePool() 556 557 // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-manifests 558 func UploadManifest(ctx *context.Context) { 559 reference := ctx.Params("reference") 560 561 mci := &manifestCreationInfo{ 562 MediaType: ctx.Req.Header.Get("Content-Type"), 563 Owner: ctx.Package.Owner, 564 Creator: ctx.Doer, 565 Image: ctx.Params("image"), 566 Reference: reference, 567 IsTagged: digest.Digest(reference).Validate() != nil, 568 } 569 570 if mci.IsTagged && !referencePattern.MatchString(reference) { 571 apiErrorDefined(ctx, errManifestInvalid.WithMessage("Tag is invalid")) 572 return 573 } 574 575 maxSize := maxManifestSize + 1 576 buf, err := packages_module.CreateHashedBufferFromReaderWithSize(&io.LimitedReader{R: ctx.Req.Body, N: int64(maxSize)}, maxSize) 577 if err != nil { 578 apiError(ctx, http.StatusInternalServerError, err) 579 return 580 } 581 defer buf.Close() 582 583 if buf.Size() > maxManifestSize { 584 apiErrorDefined(ctx, errManifestInvalid.WithMessage("Manifest exceeds maximum size").WithStatusCode(http.StatusRequestEntityTooLarge)) 585 return 586 } 587 588 imagePath := ctx.Package.Owner.Name + "/" + ctx.Params("image") 589 lockManifest.CheckIn(imagePath) 590 defer lockManifest.CheckOut(imagePath) 591 592 digest, err := processManifest(ctx, mci, buf) 593 if err != nil { 594 var namedError *namedError 595 if errors.As(err, &namedError) { 596 apiErrorDefined(ctx, namedError) 597 } else if errors.Is(err, container_model.ErrContainerBlobNotExist) { 598 apiErrorDefined(ctx, errBlobUnknown) 599 } else { 600 switch err { 601 case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize: 602 apiError(ctx, http.StatusForbidden, err) 603 default: 604 apiError(ctx, http.StatusInternalServerError, err) 605 } 606 } 607 return 608 } 609 610 setResponseHeaders(ctx.Resp, &containerHeaders{ 611 Location: fmt.Sprintf("/v2/%s/%s/manifests/%s", ctx.Package.Owner.LowerName, mci.Image, reference), 612 ContentDigest: digest, 613 Status: http.StatusCreated, 614 }) 615 } 616 617 func getBlobSearchOptionsFromContext(ctx *context.Context) (*container_model.BlobSearchOptions, error) { 618 reference := ctx.Params("reference") 619 620 opts := &container_model.BlobSearchOptions{ 621 OwnerID: ctx.Package.Owner.ID, 622 Image: ctx.Params("image"), 623 IsManifest: true, 624 } 625 626 if digest.Digest(reference).Validate() == nil { 627 opts.Digest = reference 628 } else if referencePattern.MatchString(reference) { 629 opts.Tag = reference 630 } else { 631 return nil, container_model.ErrContainerBlobNotExist 632 } 633 634 return opts, nil 635 } 636 637 func getManifestFromContext(ctx *context.Context) (*packages_model.PackageFileDescriptor, error) { 638 opts, err := getBlobSearchOptionsFromContext(ctx) 639 if err != nil { 640 return nil, err 641 } 642 643 return workaroundGetContainerBlob(ctx, opts) 644 } 645 646 // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#checking-if-content-exists-in-the-registry 647 func HeadManifest(ctx *context.Context) { 648 manifest, err := getManifestFromContext(ctx) 649 if err != nil { 650 if err == container_model.ErrContainerBlobNotExist { 651 apiErrorDefined(ctx, errManifestUnknown) 652 } else { 653 apiError(ctx, http.StatusInternalServerError, err) 654 } 655 return 656 } 657 658 setResponseHeaders(ctx.Resp, &containerHeaders{ 659 ContentDigest: manifest.Properties.GetByName(container_module.PropertyDigest), 660 ContentType: manifest.Properties.GetByName(container_module.PropertyMediaType), 661 ContentLength: manifest.Blob.Size, 662 Status: http.StatusOK, 663 }) 664 } 665 666 // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pulling-manifests 667 func GetManifest(ctx *context.Context) { 668 manifest, err := getManifestFromContext(ctx) 669 if err != nil { 670 if err == container_model.ErrContainerBlobNotExist { 671 apiErrorDefined(ctx, errManifestUnknown) 672 } else { 673 apiError(ctx, http.StatusInternalServerError, err) 674 } 675 return 676 } 677 678 serveBlob(ctx, manifest) 679 } 680 681 // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#deleting-tags 682 // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#deleting-manifests 683 func DeleteManifest(ctx *context.Context) { 684 opts, err := getBlobSearchOptionsFromContext(ctx) 685 if err != nil { 686 apiErrorDefined(ctx, errManifestUnknown) 687 return 688 } 689 690 imagePath := ctx.Package.Owner.Name + "/" + ctx.Params("image") 691 lockManifest.CheckIn(imagePath) 692 defer lockManifest.CheckOut(imagePath) 693 694 pvs, err := container_model.GetManifestVersions(ctx, opts) 695 if err != nil { 696 apiError(ctx, http.StatusInternalServerError, err) 697 return 698 } 699 700 if len(pvs) == 0 { 701 apiErrorDefined(ctx, errManifestUnknown) 702 return 703 } 704 705 for _, pv := range pvs { 706 if err := packages_service.RemovePackageVersion(ctx, ctx.Doer, pv); err != nil { 707 apiError(ctx, http.StatusInternalServerError, err) 708 return 709 } 710 } 711 712 setResponseHeaders(ctx.Resp, &containerHeaders{ 713 Status: http.StatusAccepted, 714 }) 715 } 716 717 func serveBlob(ctx *context.Context, pfd *packages_model.PackageFileDescriptor) { 718 s, u, _, err := packages_service.GetPackageBlobStream(ctx, pfd.File, pfd.Blob) 719 if err != nil { 720 apiError(ctx, http.StatusInternalServerError, err) 721 return 722 } 723 724 headers := &containerHeaders{ 725 ContentDigest: pfd.Properties.GetByName(container_module.PropertyDigest), 726 ContentType: pfd.Properties.GetByName(container_module.PropertyMediaType), 727 ContentLength: pfd.Blob.Size, 728 Status: http.StatusOK, 729 } 730 731 if u != nil { 732 headers.Status = http.StatusTemporaryRedirect 733 headers.Location = u.String() 734 735 setResponseHeaders(ctx.Resp, headers) 736 return 737 } 738 739 defer s.Close() 740 741 setResponseHeaders(ctx.Resp, headers) 742 if _, err := io.Copy(ctx.Resp, s); err != nil { 743 log.Error("Error whilst copying content to response: %v", err) 744 } 745 } 746 747 // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#content-discovery 748 func GetTagList(ctx *context.Context) { 749 image := ctx.Params("image") 750 751 if _, err := packages_model.GetPackageByName(ctx, ctx.Package.Owner.ID, packages_model.TypeContainer, image); err != nil { 752 if err == packages_model.ErrPackageNotExist { 753 apiErrorDefined(ctx, errNameUnknown) 754 } else { 755 apiError(ctx, http.StatusInternalServerError, err) 756 } 757 return 758 } 759 760 n := -1 761 if ctx.FormTrim("n") != "" { 762 n = ctx.FormInt("n") 763 } 764 last := ctx.FormTrim("last") 765 766 tags, err := container_model.GetImageTags(ctx, ctx.Package.Owner.ID, image, n, last) 767 if err != nil { 768 apiError(ctx, http.StatusInternalServerError, err) 769 return 770 } 771 772 type TagList struct { 773 Name string `json:"name"` 774 Tags []string `json:"tags"` 775 } 776 777 if len(tags) > 0 { 778 v := url.Values{} 779 if n > 0 { 780 v.Add("n", strconv.Itoa(n)) 781 } 782 v.Add("last", tags[len(tags)-1]) 783 784 ctx.Resp.Header().Set("Link", fmt.Sprintf(`</v2/%s/%s/tags/list?%s>; rel="next"`, ctx.Package.Owner.LowerName, image, v.Encode())) 785 } 786 787 jsonResponse(ctx, http.StatusOK, TagList{ 788 Name: strings.ToLower(ctx.Package.Owner.LowerName + "/" + image), 789 Tags: tags, 790 }) 791 } 792 793 // FIXME: Workaround to be removed in v1.20 794 // https://github.com/go-gitea/gitea/issues/19586 795 func workaroundGetContainerBlob(ctx *context.Context, opts *container_model.BlobSearchOptions) (*packages_model.PackageFileDescriptor, error) { 796 blob, err := container_model.GetContainerBlob(ctx, opts) 797 if err != nil { 798 return nil, err 799 } 800 801 err = packages_module.NewContentStore().Has(packages_module.BlobHash256Key(blob.Blob.HashSHA256)) 802 if err != nil { 803 if errors.Is(err, util.ErrNotExist) || errors.Is(err, os.ErrNotExist) { 804 log.Debug("Package registry inconsistent: blob %s does not exist on file system", blob.Blob.HashSHA256) 805 return nil, container_model.ErrContainerBlobNotExist 806 } 807 return nil, err 808 } 809 810 return blob, nil 811 }