1
1
mirror of https://github.com/go-gitea/gitea synced 2024-11-18 16:14:26 +00:00
gitea/routers/api/packages/container/container.go

614 lines
18 KiB
Go
Raw Normal View History

Add Package Registry (#16510) * Added package store settings. * Added models. * Added generic package registry. * Added tests. * Added NuGet package registry. * Moved service index to api file. * Added NPM package registry. * Added Maven package registry. * Added PyPI package registry. * Summary is deprecated. * Changed npm name. * Sanitize project url. * Allow only scoped packages. * Added user interface. * Changed method name. * Added missing migration file. * Set page info. * Added documentation. * Added documentation links. * Fixed wrong error message. * Lint template files. * Fixed merge errors. * Fixed unit test storage path. * Switch to json module. * Added suggestions. * Added package webhook. * Add package api. * Fixed swagger file. * Fixed enum and comments. * Fixed NuGet pagination. * Print test names. * Added api tests. * Fixed access level. * Fix User unmarshal. * Added RubyGems package registry. * Fix lint. * Implemented io.Writer. * Added support for sha256/sha512 checksum files. * Improved maven-metadata.xml support. * Added support for symbol package uploads. * Added tests. * Added overview docs. * Added npm dependencies and keywords. * Added no-packages information. * Display file size. * Display asset count. * Fixed filter alignment. * Added package icons. * Formatted instructions. * Allow anonymous package downloads. * Fixed comments. * Fixed postgres test. * Moved file. * Moved models to models/packages. * Use correct error response format per client. * Use simpler search form. * Fixed IsProd. * Restructured data model. * Prevent empty filename. * Fix swagger. * Implemented user/org registry. * Implemented UI. * Use GetUserByIDCtx. * Use table for dependencies. * make svg * Added support for unscoped npm packages. * Add support for npm dist tags. * Added tests for npm tags. * Unlink packages if repository gets deleted. * Prevent user/org delete if a packages exist. * Use package unlink in repository service. * Added support for composer packages. * Restructured package docs. * Added missing tests. * Fixed generic content page. * Fixed docs. * Fixed swagger. * Added missing type. * Fixed ambiguous column. * Organize content store by sha256 hash. * Added admin package management. * Added support for sorting. * Add support for multiple identical versions/files. * Added missing repository unlink. * Added file properties. * make fmt * lint * Added Conan package registry. * Updated docs. * Unify package names. * Added swagger enum. * Use longer TEXT column type. * Removed version composite key. * Merged package and container registry. * Removed index. * Use dedicated package router. * Moved files to new location. * Updated docs. * Fixed JOIN order. * Fixed GROUP BY statement. * Fixed GROUP BY #2. * Added symbol server support. * Added more tests. * Set NOT NULL. * Added setting to disable package registries. * Moved auth into service. * refactor * Use ctx everywhere. * Added package cleanup task. * Changed packages path. * Added container registry. * Refactoring * Updated comparison. * Fix swagger. * Fixed table order. * Use token auth for npm routes. * Enabled ReverseProxy auth. * Added packages link for orgs. * Fixed anonymous org access. * Enable copy button for setup instructions. * Merge error * Added suggestions. * Fixed merge. * Handle "generic". * Added link for TODO. * Added suggestions. * Changed temporary buffer filename. * Added suggestions. * Apply suggestions from code review Co-authored-by: Thomas Boerger <thomas@webhippie.de> * Update docs/content/doc/packages/nuget.en-us.md Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: Thomas Boerger <thomas@webhippie.de>
2022-03-30 08:42:47 +00:00
// 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,
})
}