mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-05-15 23:40:26 +00:00
Enables and tests the usage of Authorized Integrations to access the package registries. Specific testing includes: - Container registry -- automated testing and manual testing - Generic registry, w/ detailed authorization tests -- automated testing - Conan registry -- automated testing (uses an "authenticate" endpoint that required updates) - npm registry -- manual testing with a Forgejo Action publishing packages For the container & conan registeries, where the client uses an authentication endpoint to request a temporary access token, the expiry of the temporary access token is restricted to the expiry of the authorized integration's JWT for the authorized integration in order to prevent an escalation of privileges. ## Checklist The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. All work and communication must conform to Forgejo's [AI Agreement](https://codeberg.org/forgejo/governance/src/branch/main/AIAgreement.md). There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org). ### Tests for Go changes - I added test coverage for Go changes... - [x] in their respective `*_test.go` for unit tests. - [x] in the `tests/integration` directory if it involves interactions with a live Forgejo server. - I ran... - [ ] `make pr-go` before pushing ### Documentation - [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change. - [x] I did not document these changes and I do not expect someone else to do it. ### Release notes - [x] This change will be noticed by a Forgejo user or admin (feature, bug fix, performance, etc.). I suggest to include a release note for this change. - [ ] This change is not visible to a Forgejo user or admin (refactor, dependency upgrade, etc.). I think there is no need to add a release note for this change. Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/12310 Reviewed-by: Andreas Ahlenstorf <aahlenst@noreply.codeberg.org>
707 lines
21 KiB
Go
707 lines
21 KiB
Go
// Copyright 2021 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package container
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"regexp"
|
|
"strconv"
|
|
|
|
packages_model "forgejo.org/models/packages"
|
|
container_model "forgejo.org/models/packages/container"
|
|
user_model "forgejo.org/models/user"
|
|
"forgejo.org/modules/json"
|
|
"forgejo.org/modules/log"
|
|
packages_module "forgejo.org/modules/packages"
|
|
container_module "forgejo.org/modules/packages/container"
|
|
"forgejo.org/modules/setting"
|
|
"forgejo.org/routers/api/packages/helper"
|
|
"forgejo.org/services/context"
|
|
packages_service "forgejo.org/services/packages"
|
|
container_service "forgejo.org/services/packages/container"
|
|
|
|
digest "github.com/opencontainers/go-digest"
|
|
)
|
|
|
|
var imageNamePattern = regexp.MustCompile(`\A[a-z0-9]+([._-][a-z0-9]+)*(/[a-z0-9]+([._-][a-z0-9]+)*)*\z`)
|
|
|
|
type containerHeaders struct {
|
|
Status int
|
|
ContentDigest string
|
|
UploadUUID string
|
|
Range string
|
|
Location string
|
|
ContentType string
|
|
ContentLength int64
|
|
}
|
|
|
|
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#legacy-docker-support-http-headers
|
|
func setResponseHeaders(resp http.ResponseWriter, h *containerHeaders) {
|
|
if h.Location != "" {
|
|
resp.Header().Set("Location", h.Location)
|
|
}
|
|
if h.Range != "" {
|
|
resp.Header().Set("Range", h.Range)
|
|
}
|
|
if h.ContentType != "" {
|
|
resp.Header().Set("Content-Type", h.ContentType)
|
|
}
|
|
if h.UploadUUID != "" {
|
|
resp.Header().Set("Docker-Upload-Uuid", h.UploadUUID)
|
|
}
|
|
if h.ContentDigest != "" {
|
|
resp.Header().Set("Docker-Content-Digest", h.ContentDigest)
|
|
resp.Header().Set("ETag", fmt.Sprintf(`"%s"`, h.ContentDigest))
|
|
}
|
|
if h.ContentLength >= 0 {
|
|
resp.Header().Set("Content-Length", strconv.FormatInt(h.ContentLength, 10))
|
|
}
|
|
resp.Header().Set("Docker-Distribution-Api-Version", "registry/2.0")
|
|
resp.WriteHeader(h.Status)
|
|
}
|
|
|
|
func jsonResponse(ctx *context.Context, status int, obj any) {
|
|
// Buffer the JSON content first to calculate correct Content-Length
|
|
var buf bytes.Buffer
|
|
if err := json.NewEncoder(&buf).Encode(obj); err != nil {
|
|
log.Error("JSON encode: %v", err)
|
|
return
|
|
}
|
|
|
|
setResponseHeaders(ctx.Resp, &containerHeaders{
|
|
Status: status,
|
|
ContentType: "application/json",
|
|
ContentLength: int64(buf.Len()),
|
|
})
|
|
|
|
if _, err := buf.WriteTo(ctx.Resp); err != nil {
|
|
log.Error("JSON write: %v", err)
|
|
}
|
|
}
|
|
|
|
func apiError(ctx *context.Context, status int, err error) {
|
|
helper.LogAndProcessError(ctx, status, err, func(message string) {
|
|
setResponseHeaders(ctx.Resp, &containerHeaders{
|
|
Status: status,
|
|
})
|
|
})
|
|
}
|
|
|
|
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#error-codes
|
|
func apiErrorDefined(ctx *context.Context, err *container_service.NamedError) {
|
|
type ContainerError struct {
|
|
Code string `json:"code"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
type ContainerErrors struct {
|
|
Errors []ContainerError `json:"errors"`
|
|
}
|
|
|
|
jsonResponse(ctx, err.StatusCode, ContainerErrors{
|
|
Errors: []ContainerError{
|
|
{
|
|
Code: err.Code,
|
|
Message: err.Message,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
func APIUnauthorizedError(ctx *context.Context) {
|
|
// Do not include more than one challenge in the same header field. That breaks clients even though the HTTP RFC
|
|
// allows it.
|
|
ctx.Resp.Header().Set("WWW-Authenticate", `Bearer realm="`+setting.AppURL+`v2/token",service="container_registry",scope="*"`)
|
|
apiErrorDefined(ctx, container_service.ErrUnauthorized)
|
|
}
|
|
|
|
// ReqContainerAccess is a middleware which checks the current user valid (real user or ghost if anonymous access is enabled)
|
|
func ReqContainerAccess(ctx *context.Context) {
|
|
if ctx.Doer == nil || (setting.Service.RequireSignInView && ctx.Doer.IsGhost()) {
|
|
APIUnauthorizedError(ctx)
|
|
}
|
|
}
|
|
|
|
// VerifyImageName is a middleware which checks if the image name is allowed
|
|
func VerifyImageName(ctx *context.Context) {
|
|
if !imageNamePattern.MatchString(ctx.Params("image")) {
|
|
apiErrorDefined(ctx, container_service.ErrNameInvalid)
|
|
}
|
|
}
|
|
|
|
// DetermineSupport is used to test if the registry supports OCI
|
|
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#determining-support
|
|
func DetermineSupport(ctx *context.Context) {
|
|
setResponseHeaders(ctx.Resp, &containerHeaders{
|
|
Status: http.StatusOK,
|
|
})
|
|
}
|
|
|
|
// Authenticate creates a token for the current user
|
|
// If the current user is anonymous, the ghost user is used unless RequireSignInView is enabled.
|
|
func Authenticate(ctx *context.Context) {
|
|
u := ctx.Doer
|
|
if u == nil {
|
|
if setting.Service.RequireSignInView {
|
|
APIUnauthorizedError(ctx)
|
|
return
|
|
}
|
|
|
|
u = user_model.NewGhostUser()
|
|
}
|
|
|
|
// If there's an API scope, ensure it propagates.
|
|
scope := ctx.Authentication.Scope().ValueOrZeroValue()
|
|
exp := ctx.Authentication.ExpiresAt()
|
|
|
|
token, err := packages_service.CreateAuthorizationToken(u, scope, exp)
|
|
if err != nil {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
ctx.JSON(http.StatusOK, map[string]string{
|
|
"token": token,
|
|
})
|
|
}
|
|
|
|
// https://distribution.github.io/distribution/spec/auth/oauth/
|
|
func AuthenticateNotImplemented(ctx *context.Context) {
|
|
// This optional endpoint can be used to authenticate a client.
|
|
// It must implement the specification described in:
|
|
// https://datatracker.ietf.org/doc/html/rfc6749
|
|
// https://distribution.github.io/distribution/spec/auth/oauth/
|
|
// Purpose of this stub is to respond with 404 Not Found instead of 405 Method Not Allowed.
|
|
|
|
ctx.Status(http.StatusNotFound)
|
|
}
|
|
|
|
// https://docs.docker.com/registry/spec/api/#listing-repositories
|
|
func GetRepositoryList(ctx *context.Context) {
|
|
n := ctx.FormInt("n")
|
|
if n <= 0 || n > 100 {
|
|
n = 100
|
|
}
|
|
last := ctx.FormTrim("last")
|
|
|
|
repositories, err := container_model.GetRepositories(ctx, ctx.Doer, n, last)
|
|
if err != nil {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
type RepositoryList struct {
|
|
Repositories []string `json:"repositories"`
|
|
}
|
|
|
|
if len(repositories) == n {
|
|
v := url.Values{}
|
|
if n > 0 {
|
|
v.Add("n", strconv.Itoa(n))
|
|
}
|
|
v.Add("last", repositories[len(repositories)-1])
|
|
|
|
ctx.Resp.Header().Set("Link", fmt.Sprintf(`</v2/_catalog?%s>; rel="next"`, v.Encode()))
|
|
}
|
|
|
|
jsonResponse(ctx, http.StatusOK, RepositoryList{
|
|
Repositories: repositories,
|
|
})
|
|
}
|
|
|
|
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#mounting-a-blob-from-another-repository
|
|
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#single-post
|
|
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks
|
|
func InitiateUploadBlob(ctx *context.Context) {
|
|
image := ctx.Params("image")
|
|
|
|
mount := ctx.FormTrim("mount")
|
|
from := ctx.FormTrim("from")
|
|
if mount != "" {
|
|
blob, _ := container_service.WorkaroundGetContainerBlob(ctx, &container_model.BlobSearchOptions{
|
|
Repository: from,
|
|
Digest: mount,
|
|
})
|
|
if blob != nil {
|
|
accessible, err := packages_model.IsBlobAccessibleForUser(ctx, blob.Blob.ID, ctx.Doer)
|
|
if err != nil {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
if accessible {
|
|
if err := container_service.MountBlob(ctx, &packages_service.PackageInfo{Owner: ctx.Package.Owner, Name: image}, blob.Blob); err != nil {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
setResponseHeaders(ctx.Resp, &containerHeaders{
|
|
Location: fmt.Sprintf("/v2/%s/%s/blobs/%s", ctx.Package.Owner.LowerName, image, mount),
|
|
ContentDigest: mount,
|
|
Status: http.StatusCreated,
|
|
})
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
digest := ctx.FormTrim("digest")
|
|
if digest != "" {
|
|
buf, err := packages_module.CreateHashedBufferFromReader(ctx.Req.Body)
|
|
if err != nil {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
defer buf.Close()
|
|
|
|
if digest != container_service.DigestFromHashSummer(buf) {
|
|
apiErrorDefined(ctx, container_service.ErrDigestInvalid)
|
|
return
|
|
}
|
|
|
|
if _, err := container_service.SaveAsPackageBlob(ctx,
|
|
buf,
|
|
&packages_service.PackageCreationInfo{
|
|
PackageInfo: packages_service.PackageInfo{
|
|
Owner: ctx.Package.Owner,
|
|
Name: image,
|
|
},
|
|
Creator: ctx.Doer,
|
|
},
|
|
); err != nil {
|
|
switch err {
|
|
case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
|
|
apiError(ctx, http.StatusForbidden, err)
|
|
default:
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
}
|
|
return
|
|
}
|
|
|
|
setResponseHeaders(ctx.Resp, &containerHeaders{
|
|
Location: fmt.Sprintf("/v2/%s/%s/blobs/%s", ctx.Package.Owner.LowerName, image, digest),
|
|
ContentDigest: digest,
|
|
Status: http.StatusCreated,
|
|
})
|
|
return
|
|
}
|
|
|
|
upload, err := packages_model.CreateBlobUpload(ctx)
|
|
if err != nil {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
setResponseHeaders(ctx.Resp, &containerHeaders{
|
|
Location: fmt.Sprintf("/v2/%s/%s/blobs/uploads/%s", ctx.Package.Owner.LowerName, image, upload.ID),
|
|
Range: "0-0",
|
|
UploadUUID: upload.ID,
|
|
Status: http.StatusAccepted,
|
|
})
|
|
}
|
|
|
|
// https://docs.docker.com/registry/spec/api/#get-blob-upload
|
|
func GetUploadBlob(ctx *context.Context) {
|
|
uuid := ctx.Params("uuid")
|
|
|
|
upload, err := packages_model.GetBlobUploadByID(ctx, uuid)
|
|
if err != nil {
|
|
if err == packages_model.ErrPackageBlobUploadNotExist {
|
|
apiErrorDefined(ctx, container_service.ErrBlobUploadUnknown)
|
|
} else {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
}
|
|
return
|
|
}
|
|
|
|
setResponseHeaders(ctx.Resp, &containerHeaders{
|
|
Range: fmt.Sprintf("0-%d", upload.BytesReceived),
|
|
UploadUUID: upload.ID,
|
|
Status: http.StatusNoContent,
|
|
})
|
|
}
|
|
|
|
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks
|
|
func UploadBlob(ctx *context.Context) {
|
|
image := ctx.Params("image")
|
|
|
|
uploader, err := container_service.NewBlobUploader(ctx, ctx.Params("uuid"))
|
|
if err != nil {
|
|
if err == packages_model.ErrPackageBlobUploadNotExist {
|
|
apiErrorDefined(ctx, container_service.ErrBlobUploadUnknown)
|
|
} else {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
}
|
|
return
|
|
}
|
|
defer uploader.Close()
|
|
|
|
contentRange := ctx.Req.Header.Get("Content-Range")
|
|
if contentRange != "" {
|
|
start, end := 0, 0
|
|
if _, err := fmt.Sscanf(contentRange, "%d-%d", &start, &end); err != nil {
|
|
apiErrorDefined(ctx, container_service.ErrBlobUploadInvalid)
|
|
return
|
|
}
|
|
|
|
if int64(start) != uploader.Size() {
|
|
apiErrorDefined(ctx, container_service.ErrBlobUploadInvalid.WithStatusCode(http.StatusRequestedRangeNotSatisfiable))
|
|
return
|
|
}
|
|
} else if uploader.Size() != 0 {
|
|
apiErrorDefined(ctx, container_service.ErrBlobUploadInvalid.WithMessage("Stream uploads after first write are not allowed"))
|
|
return
|
|
}
|
|
|
|
if err := uploader.Append(ctx, ctx.Req.Body); err != nil {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
setResponseHeaders(ctx.Resp, &containerHeaders{
|
|
Location: fmt.Sprintf("/v2/%s/%s/blobs/uploads/%s", ctx.Package.Owner.LowerName, image, uploader.ID),
|
|
Range: fmt.Sprintf("0-%d", uploader.Size()-1),
|
|
UploadUUID: uploader.ID,
|
|
Status: http.StatusAccepted,
|
|
})
|
|
}
|
|
|
|
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks
|
|
func EndUploadBlob(ctx *context.Context) {
|
|
image := ctx.Params("image")
|
|
|
|
digest := ctx.FormTrim("digest")
|
|
if digest == "" {
|
|
apiErrorDefined(ctx, container_service.ErrDigestInvalid)
|
|
return
|
|
}
|
|
|
|
uploader, err := container_service.NewBlobUploader(ctx, ctx.Params("uuid"))
|
|
if err != nil {
|
|
if err == packages_model.ErrPackageBlobUploadNotExist {
|
|
apiErrorDefined(ctx, container_service.ErrBlobUploadUnknown)
|
|
} else {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
}
|
|
return
|
|
}
|
|
defer uploader.Close()
|
|
|
|
if ctx.Req.Body != nil {
|
|
if err := uploader.Append(ctx, ctx.Req.Body); err != nil {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
}
|
|
|
|
if digest != container_service.DigestFromHashSummer(uploader) {
|
|
apiErrorDefined(ctx, container_service.ErrDigestInvalid)
|
|
return
|
|
}
|
|
|
|
if _, err := container_service.SaveAsPackageBlob(ctx,
|
|
uploader,
|
|
&packages_service.PackageCreationInfo{
|
|
PackageInfo: packages_service.PackageInfo{
|
|
Owner: ctx.Package.Owner,
|
|
Name: image,
|
|
},
|
|
Creator: ctx.Doer,
|
|
},
|
|
); err != nil {
|
|
switch err {
|
|
case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
|
|
apiError(ctx, http.StatusForbidden, err)
|
|
default:
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
}
|
|
return
|
|
}
|
|
|
|
if err := container_service.RemoveBlobUploadByID(ctx, uploader.ID); err != nil {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
setResponseHeaders(ctx.Resp, &containerHeaders{
|
|
Location: fmt.Sprintf("/v2/%s/%s/blobs/%s", ctx.Package.Owner.LowerName, image, digest),
|
|
ContentDigest: digest,
|
|
Status: http.StatusCreated,
|
|
})
|
|
}
|
|
|
|
// https://docs.docker.com/registry/spec/api/#delete-blob-upload
|
|
func CancelUploadBlob(ctx *context.Context) {
|
|
uuid := ctx.Params("uuid")
|
|
|
|
_, err := packages_model.GetBlobUploadByID(ctx, uuid)
|
|
if err != nil {
|
|
if err == packages_model.ErrPackageBlobUploadNotExist {
|
|
apiErrorDefined(ctx, container_service.ErrBlobUploadUnknown)
|
|
} else {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
}
|
|
return
|
|
}
|
|
|
|
if err := container_service.RemoveBlobUploadByID(ctx, uuid); err != nil {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
setResponseHeaders(ctx.Resp, &containerHeaders{
|
|
Status: http.StatusNoContent,
|
|
})
|
|
}
|
|
|
|
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#checking-if-content-exists-in-the-registry
|
|
func HeadBlob(ctx *context.Context) {
|
|
blob, err := container_service.GetLocalBlob(ctx, ctx.Package.Owner.ID, ctx.Params("digest"), ctx.Params("image"))
|
|
if err != nil {
|
|
if errors.Is(err, container_model.ErrContainerBlobNotExist) {
|
|
apiErrorDefined(ctx, container_service.ErrBlobUnknown)
|
|
} else {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
}
|
|
return
|
|
}
|
|
|
|
setResponseHeaders(ctx.Resp, &containerHeaders{
|
|
ContentDigest: blob.Properties.GetByName(container_module.PropertyDigest),
|
|
ContentLength: blob.Blob.Size,
|
|
Status: http.StatusOK,
|
|
})
|
|
}
|
|
|
|
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pulling-blobs
|
|
func GetBlob(ctx *context.Context) {
|
|
blob, err := container_service.GetLocalBlob(ctx, ctx.Package.Owner.ID, ctx.Params("digest"), ctx.Params("image"))
|
|
if err != nil {
|
|
if err == container_model.ErrContainerBlobNotExist {
|
|
apiErrorDefined(ctx, container_service.ErrBlobUnknown)
|
|
} else {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
}
|
|
return
|
|
}
|
|
|
|
serveBlob(ctx, blob)
|
|
}
|
|
|
|
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#deleting-blobs
|
|
func DeleteBlob(ctx *context.Context) {
|
|
d := ctx.Params("digest")
|
|
|
|
if digest.Digest(d).Validate() != nil {
|
|
apiErrorDefined(ctx, container_service.ErrBlobUnknown)
|
|
return
|
|
}
|
|
|
|
if err := container_service.DeleteBlob(ctx, ctx.Package.Owner.ID, ctx.Params("image"), d); err != nil {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
setResponseHeaders(ctx.Resp, &containerHeaders{
|
|
Status: http.StatusAccepted,
|
|
})
|
|
}
|
|
|
|
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-manifests
|
|
func UploadManifest(ctx *context.Context) {
|
|
mci, err := container_service.NewManifestCreationInfo(
|
|
ctx.Package.Owner,
|
|
ctx.Doer,
|
|
ctx.Req.Header.Get("Content-Type"),
|
|
ctx.Params("image"),
|
|
ctx.Params("reference"),
|
|
)
|
|
if err != nil {
|
|
apiErrorDefined(ctx, container_service.ErrManifestInvalid.WithMessage(err.Error()))
|
|
return
|
|
}
|
|
|
|
maxSize := container_service.MaxManifestSize + 1
|
|
buf, err := packages_module.CreateHashedBufferFromReaderWithSize(&io.LimitedReader{R: ctx.Req.Body, N: int64(maxSize)}, maxSize)
|
|
if err != nil {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
defer buf.Close()
|
|
|
|
if buf.Size() > container_service.MaxManifestSize {
|
|
apiErrorDefined(ctx, container_service.ErrManifestInvalid.WithMessage("Manifest exceeds maximum size").WithStatusCode(http.StatusRequestEntityTooLarge))
|
|
return
|
|
}
|
|
|
|
digest, err := container_service.ProcessManifest(ctx, *mci, buf)
|
|
if err != nil {
|
|
var namedError *container_service.NamedError
|
|
if errors.As(err, &namedError) {
|
|
apiErrorDefined(ctx, namedError)
|
|
} else if errors.Is(err, container_model.ErrContainerBlobNotExist) {
|
|
apiErrorDefined(ctx, container_service.ErrBlobUnknown)
|
|
} else {
|
|
switch err {
|
|
case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
|
|
apiError(ctx, http.StatusForbidden, err)
|
|
default:
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
setResponseHeaders(ctx.Resp, &containerHeaders{
|
|
Location: fmt.Sprintf("/v2/%s/%s/manifests/%s", ctx.Package.Owner.LowerName, mci.Image, mci.Reference),
|
|
ContentDigest: digest,
|
|
Status: http.StatusCreated,
|
|
})
|
|
}
|
|
|
|
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#checking-if-content-exists-in-the-registry
|
|
func HeadManifest(ctx *context.Context) {
|
|
manifest, err := container_service.GetLocalManifest(ctx, ctx.Package.Owner.ID, ctx.Params("image"), ctx.Params("reference"))
|
|
if err != nil {
|
|
if err == container_model.ErrContainerBlobNotExist {
|
|
apiErrorDefined(ctx, container_service.ErrManifestUnknown)
|
|
} else {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
}
|
|
return
|
|
}
|
|
|
|
setResponseHeaders(ctx.Resp, &containerHeaders{
|
|
ContentDigest: manifest.Properties.GetByName(container_module.PropertyDigest),
|
|
ContentType: manifest.Properties.GetByName(container_module.PropertyMediaType),
|
|
ContentLength: manifest.Blob.Size,
|
|
Status: http.StatusOK,
|
|
})
|
|
}
|
|
|
|
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pulling-manifests
|
|
func GetManifest(ctx *context.Context) {
|
|
manifest, err := container_service.GetLocalManifest(ctx, ctx.Package.Owner.ID, ctx.Params("image"), ctx.Params("reference"))
|
|
if err != nil {
|
|
if err == container_model.ErrContainerBlobNotExist {
|
|
apiErrorDefined(ctx, container_service.ErrManifestUnknown)
|
|
} else {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
}
|
|
return
|
|
}
|
|
|
|
serveBlob(ctx, manifest)
|
|
}
|
|
|
|
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#deleting-tags
|
|
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#deleting-manifests
|
|
func DeleteManifest(ctx *context.Context) {
|
|
opts, err := container_service.GetManifestSearchOptions(
|
|
ctx.Package.Owner.ID,
|
|
ctx.Params("image"),
|
|
ctx.Params("reference"),
|
|
)
|
|
if err != nil {
|
|
apiErrorDefined(ctx, container_service.ErrManifestUnknown)
|
|
return
|
|
}
|
|
|
|
pvs, err := container_model.GetManifestVersions(ctx, opts)
|
|
if err != nil {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
if len(pvs) == 0 {
|
|
apiErrorDefined(ctx, container_service.ErrManifestUnknown)
|
|
return
|
|
}
|
|
|
|
for _, pv := range pvs {
|
|
if err := packages_service.RemovePackageVersion(ctx, ctx.Doer, pv); err != nil {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
}
|
|
|
|
setResponseHeaders(ctx.Resp, &containerHeaders{
|
|
Status: http.StatusAccepted,
|
|
})
|
|
}
|
|
|
|
func serveBlob(ctx *context.Context, pfd *packages_model.PackageFileDescriptor) {
|
|
serveDirectReqParams := make(url.Values)
|
|
serveDirectReqParams.Set("response-content-type", pfd.Properties.GetByName(container_module.PropertyMediaType))
|
|
s, u, pf, err := packages_service.GetPackageBlobStream(ctx, pfd.File, pfd.Blob, serveDirectReqParams)
|
|
if err != nil {
|
|
if errors.Is(err, packages_model.ErrPackageFileNotExist) {
|
|
apiError(ctx, http.StatusNotFound, err)
|
|
return
|
|
}
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
opts := &context.ServeHeaderOptions{
|
|
ContentType: pfd.Properties.GetByName(container_module.PropertyMediaType),
|
|
RedirectStatusCode: http.StatusTemporaryRedirect,
|
|
AdditionalHeaders: map[string][]string{
|
|
"Docker-Distribution-Api-Version": {"registry/2.0"},
|
|
},
|
|
}
|
|
|
|
if d := pfd.Properties.GetByName(container_module.PropertyDigest); d != "" {
|
|
opts.AdditionalHeaders["Docker-Content-Digest"] = []string{d}
|
|
opts.AdditionalHeaders["ETag"] = []string{fmt.Sprintf(`"%s"`, d)}
|
|
}
|
|
|
|
helper.ServePackageFile(ctx, s, u, pf, opts)
|
|
}
|
|
|
|
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#content-discovery
|
|
func GetTagList(ctx *context.Context) {
|
|
image := ctx.Params("image")
|
|
|
|
if _, err := packages_model.GetPackageByName(ctx, ctx.Package.Owner.ID, packages_model.TypeContainer, image); err != nil {
|
|
if errors.Is(err, packages_model.ErrPackageNotExist) {
|
|
apiErrorDefined(ctx, container_service.ErrNameUnknown)
|
|
} else {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
}
|
|
return
|
|
}
|
|
|
|
n := -1
|
|
if ctx.FormTrim("n") != "" {
|
|
n = ctx.FormInt("n")
|
|
}
|
|
last := ctx.FormTrim("last")
|
|
|
|
tagList, vals, err := container_service.GetLocalTagList(ctx,
|
|
ctx.Package.Owner.LowerName,
|
|
image,
|
|
last,
|
|
n,
|
|
ctx.Package.Owner.ID)
|
|
|
|
if errors.Is(err, packages_model.ErrPackageNotExist) {
|
|
apiErrorDefined(ctx, container_service.ErrNameUnknown)
|
|
return
|
|
} else if err != nil {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
if len(tagList.Tags) > 0 {
|
|
ctx.Resp.Header().Set("Link", fmt.Sprintf(`</v2/%s/%s/tags/list?%s>; rel="next"`, ctx.Package.Owner.LowerName, image, vals.Encode()))
|
|
}
|
|
|
|
jsonResponse(ctx, http.StatusOK, tagList)
|
|
}
|