aboutsummaryrefslogtreecommitdiffstats
path: root/routers/api/packages/container/container.go
diff options
context:
space:
mode:
Diffstat (limited to 'routers/api/packages/container/container.go')
-rw-r--r--routers/api/packages/container/container.go613
1 files changed, 613 insertions, 0 deletions
diff --git a/routers/api/packages/container/container.go b/routers/api/packages/container/container.go
new file mode 100644
index 0000000000..f0b1fafd26
--- /dev/null
+++ b/routers/api/packages/container/container.go
@@ -0,0 +1,613 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package container
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "regexp"
+ "strconv"
+ "strings"
+
+ packages_model "code.gitea.io/gitea/models/packages"
+ container_model "code.gitea.io/gitea/models/packages/container"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/json"
+ "code.gitea.io/gitea/modules/log"
+ packages_module "code.gitea.io/gitea/modules/packages"
+ container_module "code.gitea.io/gitea/modules/packages/container"
+ "code.gitea.io/gitea/modules/packages/container/oci"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/routers/api/packages/helper"
+ packages_service "code.gitea.io/gitea/services/packages"
+ container_service "code.gitea.io/gitea/services/packages/container"
+)
+
+// maximum size of a container manifest
+// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-manifests
+const maxManifestSize = 10 * 1024 * 1024
+
+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.ContentLength != 0 {
+ resp.Header().Set("Content-Length", strconv.FormatInt(h.ContentLength, 10))
+ }
+ 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))
+ }
+ resp.Header().Set("Docker-Distribution-Api-Version", "registry/2.0")
+ resp.WriteHeader(h.Status)
+}
+
+func jsonResponse(ctx *context.Context, status int, obj interface{}) {
+ setResponseHeaders(ctx.Resp, &containerHeaders{
+ Status: status,
+ ContentType: "application/json",
+ })
+ if err := json.NewEncoder(ctx.Resp).Encode(obj); err != nil {
+ log.Error("JSON encode: %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 *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,
+ },
+ },
+ })
+}
+
+// ReqContainerAccess is a middleware which checks the current user valid (real user or ghost for anonymous access)
+func ReqContainerAccess(ctx *context.Context) {
+ if ctx.Doer == nil {
+ ctx.Resp.Header().Add("WWW-Authenticate", `Bearer realm="`+setting.AppURL+`v2/token"`)
+ ctx.Resp.Header().Add("WWW-Authenticate", `Basic`)
+ apiErrorDefined(ctx, errUnauthorized)
+ }
+}
+
+// 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, 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
+func Authenticate(ctx *context.Context) {
+ u := ctx.Doer
+ if u == nil {
+ u = user_model.NewGhostUser()
+ }
+
+ token, err := packages_service.CreateAuthorizationToken(u)
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ ctx.JSON(http.StatusOK, map[string]string{
+ "token": token,
+ })
+}
+
+// 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_model.GetContainerBlob(ctx, &container_model.BlobSearchOptions{
+ Image: from,
+ Digest: mount,
+ })
+ if blob != nil {
+ 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, 32*1024*1024)
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+ defer buf.Close()
+
+ if digest != digestFromHashSummer(buf) {
+ apiErrorDefined(ctx, errDigestInvalid)
+ return
+ }
+
+ if _, err := saveAsPackageBlob(buf, &packages_service.PackageInfo{Owner: ctx.Package.Owner, Name: image}); 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,
+ })
+ 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://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, 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, errBlobUploadInvalid)
+ return
+ }
+
+ if int64(start) != uploader.Size() {
+ apiErrorDefined(ctx, errBlobUploadInvalid.WithStatusCode(http.StatusRequestedRangeNotSatisfiable))
+ return
+ }
+ } else if uploader.Size() != 0 {
+ apiErrorDefined(ctx, 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, errDigestInvalid)
+ return
+ }
+
+ uploader, err := container_service.NewBlobUploader(ctx, ctx.Params("uuid"))
+ if err != nil {
+ if err == packages_model.ErrPackageBlobUploadNotExist {
+ apiErrorDefined(ctx, errBlobUploadUnknown)
+ } else {
+ apiError(ctx, http.StatusInternalServerError, err)
+ }
+ return
+ }
+ close := true
+ defer func() {
+ if close {
+ 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 != digestFromHashSummer(uploader) {
+ apiErrorDefined(ctx, errDigestInvalid)
+ return
+ }
+
+ if _, err := saveAsPackageBlob(uploader, &packages_service.PackageInfo{Owner: ctx.Package.Owner, Name: image}); err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ if err := uploader.Close(); err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+ close = false
+
+ 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,
+ })
+}
+
+func getBlobFromContext(ctx *context.Context) (*packages_model.PackageFileDescriptor, error) {
+ digest := ctx.Params("digest")
+
+ if !oci.Digest(digest).Validate() {
+ return nil, container_model.ErrContainerBlobNotExist
+ }
+
+ return container_model.GetContainerBlob(ctx, &container_model.BlobSearchOptions{
+ OwnerID: ctx.Package.Owner.ID,
+ Image: ctx.Params("image"),
+ Digest: digest,
+ })
+}
+
+// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#checking-if-content-exists-in-the-registry
+func HeadBlob(ctx *context.Context) {
+ blob, err := getBlobFromContext(ctx)
+ if err != nil {
+ if err == container_model.ErrContainerBlobNotExist {
+ apiErrorDefined(ctx, 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 := getBlobFromContext(ctx)
+ if err != nil {
+ if err == container_model.ErrContainerBlobNotExist {
+ apiErrorDefined(ctx, errBlobUnknown)
+ } else {
+ apiError(ctx, http.StatusInternalServerError, err)
+ }
+ return
+ }
+
+ s, err := packages_module.NewContentStore().Get(packages_module.BlobHash256Key(blob.Blob.HashSHA256))
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+ defer s.Close()
+
+ setResponseHeaders(ctx.Resp, &containerHeaders{
+ ContentDigest: blob.Properties.GetByName(container_module.PropertyDigest),
+ ContentType: blob.Properties.GetByName(container_module.PropertyMediaType),
+ ContentLength: blob.Blob.Size,
+ Status: http.StatusOK,
+ })
+ if _, err := io.Copy(ctx.Resp, s); err != nil {
+ log.Error("Error whilst copying content to response: %v", err)
+ }
+}
+
+// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#deleting-blobs
+func DeleteBlob(ctx *context.Context) {
+ digest := ctx.Params("digest")
+
+ if !oci.Digest(digest).Validate() {
+ apiErrorDefined(ctx, errBlobUnknown)
+ return
+ }
+
+ if err := deleteBlob(ctx.Package.Owner.ID, ctx.Params("image"), digest); 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) {
+ reference := ctx.Params("reference")
+
+ mci := &manifestCreationInfo{
+ MediaType: oci.MediaType(ctx.Req.Header.Get("Content-Type")),
+ Owner: ctx.Package.Owner,
+ Creator: ctx.Doer,
+ Image: ctx.Params("image"),
+ Reference: reference,
+ IsTagged: !oci.Digest(reference).Validate(),
+ }
+
+ if mci.IsTagged && !oci.Reference(reference).Validate() {
+ apiErrorDefined(ctx, errManifestInvalid.WithMessage("Tag is invalid"))
+ return
+ }
+
+ maxSize := maxManifestSize + 1
+ buf, err := packages_module.CreateHashedBufferFromReader(&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() > maxManifestSize {
+ apiErrorDefined(ctx, errManifestInvalid.WithMessage("Manifest exceeds maximum size").WithStatusCode(http.StatusRequestEntityTooLarge))
+ return
+ }
+
+ digest, err := processManifest(mci, buf)
+ if err != nil {
+ var namedError *namedError
+ if errors.As(err, &namedError) {
+ apiErrorDefined(ctx, namedError)
+ } else if errors.Is(err, container_model.ErrContainerBlobNotExist) {
+ apiErrorDefined(ctx, errBlobUnknown)
+ } else {
+ apiError(ctx, http.StatusInternalServerError, err)
+ }
+ return
+ }
+
+ setResponseHeaders(ctx.Resp, &containerHeaders{
+ Location: fmt.Sprintf("/v2/%s/%s/manifests/%s", ctx.Package.Owner.LowerName, mci.Image, reference),
+ ContentDigest: digest,
+ Status: http.StatusCreated,
+ })
+}
+
+func getManifestFromContext(ctx *context.Context) (*packages_model.PackageFileDescriptor, error) {
+ reference := ctx.Params("reference")
+
+ opts := &container_model.BlobSearchOptions{
+ OwnerID: ctx.Package.Owner.ID,
+ Image: ctx.Params("image"),
+ IsManifest: true,
+ }
+ if oci.Digest(reference).Validate() {
+ opts.Digest = reference
+ } else if oci.Reference(reference).Validate() {
+ opts.Tag = reference
+ } else {
+ return nil, container_model.ErrContainerBlobNotExist
+ }
+
+ return container_model.GetContainerBlob(ctx, opts)
+}
+
+// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#checking-if-content-exists-in-the-registry
+func HeadManifest(ctx *context.Context) {
+ manifest, err := getManifestFromContext(ctx)
+ if err != nil {
+ if err == container_model.ErrContainerBlobNotExist {
+ apiErrorDefined(ctx, 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 := getManifestFromContext(ctx)
+ if err != nil {
+ if err == container_model.ErrContainerBlobNotExist {
+ apiErrorDefined(ctx, errManifestUnknown)
+ } else {
+ apiError(ctx, http.StatusInternalServerError, err)
+ }
+ return
+ }
+
+ s, err := packages_module.NewContentStore().Get(packages_module.BlobHash256Key(manifest.Blob.HashSHA256))
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+ defer s.Close()
+
+ 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,
+ })
+ if _, err := io.Copy(ctx.Resp, s); err != nil {
+ log.Error("Error whilst copying content to response: %v", err)
+ }
+}
+
+// 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) {
+ reference := ctx.Params("reference")
+
+ opts := &container_model.BlobSearchOptions{
+ OwnerID: ctx.Package.Owner.ID,
+ Image: ctx.Params("image"),
+ IsManifest: true,
+ }
+ if oci.Digest(reference).Validate() {
+ opts.Digest = reference
+ } else if oci.Reference(reference).Validate() {
+ opts.Tag = reference
+ } else {
+ apiErrorDefined(ctx, errManifestUnknown)
+ return
+ }
+
+ pvs, err := container_model.GetManifestVersions(ctx, opts)
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ if len(pvs) == 0 {
+ apiErrorDefined(ctx, errManifestUnknown)
+ return
+ }
+
+ for _, pv := range pvs {
+ if err := packages_service.RemovePackageVersion(ctx.Doer, pv); 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#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 err == packages_model.ErrPackageNotExist {
+ apiErrorDefined(ctx, errNameUnknown)
+ } else {
+ apiError(ctx, http.StatusInternalServerError, err)
+ }
+ return
+ }
+
+ n := -1
+ if ctx.FormTrim("n") != "" {
+ n = ctx.FormInt("n")
+ }
+ last := ctx.FormTrim("last")
+
+ tags, err := container_model.GetImageTags(ctx, ctx.Package.Owner.ID, image, n, last)
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ type TagList struct {
+ Name string `json:"name"`
+ Tags []string `json:"tags"`
+ }
+
+ if len(tags) > 0 {
+ v := url.Values{}
+ if n > 0 {
+ v.Add("n", strconv.Itoa(n))
+ }
+ v.Add("last", tags[len(tags)-1])
+
+ ctx.Resp.Header().Set("Link", fmt.Sprintf(`</v2/%s/%s/tags/list?%s>; rel="next"`, ctx.Package.Owner.LowerName, image, v.Encode()))
+ }
+
+ jsonResponse(ctx, http.StatusOK, TagList{
+ Name: strings.ToLower(ctx.Package.Owner.LowerName + "/" + image),
+ Tags: tags,
+ })
+}