forked from forgejo/forgejo
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>
This commit is contained in:
parent
2bce1ea986
commit
1d332342db
197 changed files with 18563 additions and 55 deletions
45
routers/api/packages/container/auth.go
Normal file
45
routers/api/packages/container/auth.go
Normal file
|
@ -0,0 +1,45 @@
|
|||
// Copyright 2022 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 (
|
||||
"net/http"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/services/auth"
|
||||
"code.gitea.io/gitea/services/packages"
|
||||
)
|
||||
|
||||
type Auth struct{}
|
||||
|
||||
func (a *Auth) Name() string {
|
||||
return "container"
|
||||
}
|
||||
|
||||
// Verify extracts the user from the Bearer token
|
||||
// If it's an anonymous session a ghost user is returned
|
||||
func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataStore, sess auth.SessionStore) *user_model.User {
|
||||
uid, err := packages.ParseAuthorizationToken(req)
|
||||
if err != nil {
|
||||
log.Trace("ParseAuthorizationToken: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if uid == 0 {
|
||||
return nil
|
||||
}
|
||||
if uid == -1 {
|
||||
return user_model.NewGhostUser()
|
||||
}
|
||||
|
||||
u, err := user_model.GetUserByID(uid)
|
||||
if err != nil {
|
||||
log.Error("GetUserByID: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return u
|
||||
}
|
136
routers/api/packages/container/blob.go
Normal file
136
routers/api/packages/container/blob.go
Normal file
|
@ -0,0 +1,136 @@
|
|||
// Copyright 2022 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 (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
container_model "code.gitea.io/gitea/models/packages/container"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
container_module "code.gitea.io/gitea/modules/packages/container"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
)
|
||||
|
||||
// saveAsPackageBlob creates a package blob from an upload
|
||||
// The uploaded blob gets stored in a special upload version to link them to the package/image
|
||||
func saveAsPackageBlob(hsr packages_module.HashedSizeReader, pi *packages_service.PackageInfo) (*packages_model.PackageBlob, error) {
|
||||
pb := packages_service.NewPackageBlob(hsr)
|
||||
|
||||
exists := false
|
||||
|
||||
contentStore := packages_module.NewContentStore()
|
||||
|
||||
err := db.WithTx(func(ctx context.Context) error {
|
||||
p := &packages_model.Package{
|
||||
OwnerID: pi.Owner.ID,
|
||||
Type: packages_model.TypeContainer,
|
||||
Name: strings.ToLower(pi.Name),
|
||||
LowerName: strings.ToLower(pi.Name),
|
||||
}
|
||||
var err error
|
||||
if p, err = packages_model.TryInsertPackage(ctx, p); err != nil {
|
||||
if err != packages_model.ErrDuplicatePackage {
|
||||
log.Error("Error inserting package: %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
pv := &packages_model.PackageVersion{
|
||||
PackageID: p.ID,
|
||||
CreatorID: pi.Owner.ID,
|
||||
Version: container_model.UploadVersion,
|
||||
LowerVersion: container_model.UploadVersion,
|
||||
IsInternal: true,
|
||||
MetadataJSON: "null",
|
||||
}
|
||||
if pv, err = packages_model.GetOrInsertVersion(ctx, pv); err != nil {
|
||||
if err != packages_model.ErrDuplicatePackageVersion {
|
||||
log.Error("Error inserting package: %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
pb, exists, err = packages_model.GetOrInsertBlob(ctx, pb)
|
||||
if err != nil {
|
||||
log.Error("Error inserting package blob: %v", err)
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
if err := contentStore.Save(packages_module.BlobHash256Key(pb.HashSHA256), hsr, hsr.Size()); err != nil {
|
||||
log.Error("Error saving package blob in content store: %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
filename := strings.ToLower(fmt.Sprintf("sha256_%s", pb.HashSHA256))
|
||||
|
||||
pf := &packages_model.PackageFile{
|
||||
VersionID: pv.ID,
|
||||
BlobID: pb.ID,
|
||||
Name: filename,
|
||||
LowerName: filename,
|
||||
CompositeKey: packages_model.EmptyFileKey,
|
||||
}
|
||||
if pf, err = packages_model.TryInsertFile(ctx, pf); err != nil {
|
||||
if err == packages_model.ErrDuplicatePackageFile {
|
||||
return nil
|
||||
}
|
||||
log.Error("Error inserting package file: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypeFile, pf.ID, container_module.PropertyDigest, digestFromPackageBlob(pb)); err != nil {
|
||||
log.Error("Error setting package file property: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
if !exists {
|
||||
if err := contentStore.Delete(packages_module.BlobHash256Key(pb.HashSHA256)); err != nil {
|
||||
log.Error("Error deleting package blob from content store: %v", err)
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pb, nil
|
||||
}
|
||||
|
||||
func deleteBlob(ownerID int64, image, digest string) error {
|
||||
return db.WithTx(func(ctx context.Context) error {
|
||||
pfds, err := container_model.GetContainerBlobs(ctx, &container_model.BlobSearchOptions{
|
||||
OwnerID: ownerID,
|
||||
Image: image,
|
||||
Digest: digest,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, file := range pfds {
|
||||
if err := packages_service.DeletePackageFile(ctx, file.File); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func digestFromHashSummer(h packages_module.HashSummer) string {
|
||||
_, _, hashSHA256, _ := h.Sums()
|
||||
return "sha256:" + hex.EncodeToString(hashSHA256)
|
||||
}
|
||||
|
||||
func digestFromPackageBlob(pb *packages_model.PackageBlob) string {
|
||||
return "sha256:" + pb.HashSHA256
|
||||
}
|
613
routers/api/packages/container/container.go
Normal file
613
routers/api/packages/container/container.go
Normal file
|
@ -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,
|
||||
})
|
||||
}
|
53
routers/api/packages/container/errors.go
Normal file
53
routers/api/packages/container/errors.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
// Copyright 2022 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 (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#error-codes
|
||||
var (
|
||||
errBlobUnknown = &namedError{Code: "BLOB_UNKNOWN", StatusCode: http.StatusNotFound}
|
||||
errBlobUploadInvalid = &namedError{Code: "BLOB_UPLOAD_INVALID", StatusCode: http.StatusBadRequest}
|
||||
errBlobUploadUnknown = &namedError{Code: "BLOB_UPLOAD_UNKNOWN", StatusCode: http.StatusNotFound}
|
||||
errDigestInvalid = &namedError{Code: "DIGEST_INVALID", StatusCode: http.StatusBadRequest}
|
||||
errManifestBlobUnknown = &namedError{Code: "MANIFEST_BLOB_UNKNOWN", StatusCode: http.StatusNotFound}
|
||||
errManifestInvalid = &namedError{Code: "MANIFEST_INVALID", StatusCode: http.StatusBadRequest}
|
||||
errManifestUnknown = &namedError{Code: "MANIFEST_UNKNOWN", StatusCode: http.StatusNotFound}
|
||||
errNameInvalid = &namedError{Code: "NAME_INVALID", StatusCode: http.StatusBadRequest}
|
||||
errNameUnknown = &namedError{Code: "NAME_UNKNOWN", StatusCode: http.StatusNotFound}
|
||||
errSizeInvalid = &namedError{Code: "SIZE_INVALID", StatusCode: http.StatusBadRequest}
|
||||
errUnauthorized = &namedError{Code: "UNAUTHORIZED", StatusCode: http.StatusUnauthorized}
|
||||
errUnsupported = &namedError{Code: "UNSUPPORTED", StatusCode: http.StatusNotImplemented}
|
||||
)
|
||||
|
||||
type namedError struct {
|
||||
Code string
|
||||
StatusCode int
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *namedError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
// WithMessage creates a new instance of the error with a different message
|
||||
func (e *namedError) WithMessage(message string) *namedError {
|
||||
return &namedError{
|
||||
Code: e.Code,
|
||||
StatusCode: e.StatusCode,
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
|
||||
// WithStatusCode creates a new instance of the error with a different status code
|
||||
func (e *namedError) WithStatusCode(statusCode int) *namedError {
|
||||
return &namedError{
|
||||
Code: e.Code,
|
||||
StatusCode: statusCode,
|
||||
Message: e.Message,
|
||||
}
|
||||
}
|
408
routers/api/packages/container/manifest.go
Normal file
408
routers/api/packages/container/manifest.go
Normal file
|
@ -0,0 +1,408 @@
|
|||
// Copyright 2022 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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
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/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"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
)
|
||||
|
||||
// manifestCreationInfo describes a manifest to create
|
||||
type manifestCreationInfo struct {
|
||||
MediaType oci.MediaType
|
||||
Owner *user_model.User
|
||||
Creator *user_model.User
|
||||
Image string
|
||||
Reference string
|
||||
IsTagged bool
|
||||
Properties map[string]string
|
||||
}
|
||||
|
||||
func processManifest(mci *manifestCreationInfo, buf *packages_module.HashedBuffer) (string, error) {
|
||||
var schema oci.SchemaMediaBase
|
||||
if err := json.NewDecoder(buf).Decode(&schema); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if schema.SchemaVersion != 2 {
|
||||
return "", errUnsupported.WithMessage("Schema version is not supported")
|
||||
}
|
||||
|
||||
if _, err := buf.Seek(0, io.SeekStart); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if !mci.MediaType.IsValid() {
|
||||
mci.MediaType = schema.MediaType
|
||||
if !mci.MediaType.IsValid() {
|
||||
return "", errManifestInvalid.WithMessage("MediaType not recognized")
|
||||
}
|
||||
}
|
||||
|
||||
if mci.MediaType.IsImageManifest() {
|
||||
d, err := processImageManifest(mci, buf)
|
||||
return d, err
|
||||
} else if mci.MediaType.IsImageIndex() {
|
||||
d, err := processImageManifestIndex(mci, buf)
|
||||
return d, err
|
||||
}
|
||||
return "", errManifestInvalid
|
||||
}
|
||||
|
||||
func processImageManifest(mci *manifestCreationInfo, buf *packages_module.HashedBuffer) (string, error) {
|
||||
manifestDigest := ""
|
||||
|
||||
err := func() error {
|
||||
var manifest oci.Manifest
|
||||
if err := json.NewDecoder(buf).Decode(&manifest); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := buf.Seek(0, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, committer, err := db.TxContext()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
configDescriptor, err := container_model.GetContainerBlob(ctx, &container_model.BlobSearchOptions{
|
||||
OwnerID: mci.Owner.ID,
|
||||
Image: mci.Image,
|
||||
Digest: string(manifest.Config.Digest),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configReader, err := packages_module.NewContentStore().Get(packages_module.BlobHash256Key(configDescriptor.Blob.HashSHA256))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer configReader.Close()
|
||||
|
||||
metadata, err := container_module.ParseImageConfig(manifest.Config.MediaType, configReader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
blobReferences := make([]*blobReference, 0, 1+len(manifest.Layers))
|
||||
|
||||
blobReferences = append(blobReferences, &blobReference{
|
||||
Digest: manifest.Config.Digest,
|
||||
MediaType: manifest.Config.MediaType,
|
||||
File: configDescriptor,
|
||||
ExpectedSize: manifest.Config.Size,
|
||||
})
|
||||
|
||||
for _, layer := range manifest.Layers {
|
||||
pfd, err := container_model.GetContainerBlob(ctx, &container_model.BlobSearchOptions{
|
||||
OwnerID: mci.Owner.ID,
|
||||
Image: mci.Image,
|
||||
Digest: string(layer.Digest),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
blobReferences = append(blobReferences, &blobReference{
|
||||
Digest: layer.Digest,
|
||||
MediaType: layer.MediaType,
|
||||
File: pfd,
|
||||
ExpectedSize: layer.Size,
|
||||
})
|
||||
}
|
||||
|
||||
pv, err := createPackageAndVersion(ctx, mci, metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uploadVersion, err := packages_model.GetInternalVersionByNameAndVersion(ctx, mci.Owner.ID, packages_model.TypeContainer, mci.Image, container_model.UploadVersion)
|
||||
if err != nil && err != packages_model.ErrPackageNotExist {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, ref := range blobReferences {
|
||||
if err := createFileFromBlobReference(ctx, pv, uploadVersion, ref); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
pb, created, digest, err := createManifestBlob(ctx, mci, pv, buf)
|
||||
removeBlob := false
|
||||
defer func() {
|
||||
if removeBlob {
|
||||
contentStore := packages_module.NewContentStore()
|
||||
if err := contentStore.Delete(packages_module.BlobHash256Key(pb.HashSHA256)); err != nil {
|
||||
log.Error("Error deleting package blob from content store: %v", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
if err != nil {
|
||||
removeBlob = created
|
||||
return err
|
||||
}
|
||||
|
||||
if err := committer.Commit(); err != nil {
|
||||
removeBlob = created
|
||||
return err
|
||||
}
|
||||
|
||||
manifestDigest = digest
|
||||
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return manifestDigest, nil
|
||||
}
|
||||
|
||||
func processImageManifestIndex(mci *manifestCreationInfo, buf *packages_module.HashedBuffer) (string, error) {
|
||||
manifestDigest := ""
|
||||
|
||||
err := func() error {
|
||||
var index oci.Index
|
||||
if err := json.NewDecoder(buf).Decode(&index); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := buf.Seek(0, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, committer, err := db.TxContext()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
metadata := &container_module.Metadata{
|
||||
Type: container_module.TypeOCI,
|
||||
MultiArch: make(map[string]string),
|
||||
}
|
||||
|
||||
for _, manifest := range index.Manifests {
|
||||
if !manifest.MediaType.IsImageManifest() {
|
||||
return errManifestInvalid
|
||||
}
|
||||
|
||||
platform := container_module.DefaultPlatform
|
||||
if manifest.Platform != nil {
|
||||
platform = fmt.Sprintf("%s/%s", manifest.Platform.OS, manifest.Platform.Architecture)
|
||||
if manifest.Platform.Variant != "" {
|
||||
platform = fmt.Sprintf("%s/%s", platform, manifest.Platform.Variant)
|
||||
}
|
||||
}
|
||||
|
||||
_, err := container_model.GetContainerBlob(ctx, &container_model.BlobSearchOptions{
|
||||
OwnerID: mci.Owner.ID,
|
||||
Image: mci.Image,
|
||||
Digest: string(manifest.Digest),
|
||||
IsManifest: true,
|
||||
})
|
||||
if err != nil {
|
||||
if err == container_model.ErrContainerBlobNotExist {
|
||||
return errManifestBlobUnknown
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
metadata.MultiArch[platform] = string(manifest.Digest)
|
||||
}
|
||||
|
||||
pv, err := createPackageAndVersion(ctx, mci, metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pb, created, digest, err := createManifestBlob(ctx, mci, pv, buf)
|
||||
removeBlob := false
|
||||
defer func() {
|
||||
if removeBlob {
|
||||
contentStore := packages_module.NewContentStore()
|
||||
if err := contentStore.Delete(packages_module.BlobHash256Key(pb.HashSHA256)); err != nil {
|
||||
log.Error("Error deleting package blob from content store: %v", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
if err != nil {
|
||||
removeBlob = created
|
||||
return err
|
||||
}
|
||||
|
||||
if err := committer.Commit(); err != nil {
|
||||
removeBlob = created
|
||||
return err
|
||||
}
|
||||
|
||||
manifestDigest = digest
|
||||
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return manifestDigest, nil
|
||||
}
|
||||
|
||||
func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, metadata *container_module.Metadata) (*packages_model.PackageVersion, error) {
|
||||
p := &packages_model.Package{
|
||||
OwnerID: mci.Owner.ID,
|
||||
Type: packages_model.TypeContainer,
|
||||
Name: strings.ToLower(mci.Image),
|
||||
LowerName: strings.ToLower(mci.Image),
|
||||
}
|
||||
var err error
|
||||
if p, err = packages_model.TryInsertPackage(ctx, p); err != nil {
|
||||
if err != packages_model.ErrDuplicatePackage {
|
||||
log.Error("Error inserting package: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
metadata.IsTagged = mci.IsTagged
|
||||
|
||||
metadataJSON, err := json.Marshal(metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_pv := &packages_model.PackageVersion{
|
||||
PackageID: p.ID,
|
||||
CreatorID: mci.Creator.ID,
|
||||
Version: strings.ToLower(mci.Reference),
|
||||
LowerVersion: strings.ToLower(mci.Reference),
|
||||
MetadataJSON: string(metadataJSON),
|
||||
}
|
||||
var pv *packages_model.PackageVersion
|
||||
if pv, err = packages_model.GetOrInsertVersion(ctx, _pv); err != nil {
|
||||
if err == packages_model.ErrDuplicatePackageVersion {
|
||||
if err := packages_service.DeletePackageVersionAndReferences(ctx, pv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if pv, err = packages_model.GetOrInsertVersion(ctx, _pv); err != nil {
|
||||
log.Error("Error inserting package: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
log.Error("Error inserting package: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if mci.IsTagged {
|
||||
if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestTagged, ""); err != nil {
|
||||
log.Error("Error setting package version property: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
for _, digest := range metadata.MultiArch {
|
||||
if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestReference, digest); err != nil {
|
||||
log.Error("Error setting package version property: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return pv, nil
|
||||
}
|
||||
|
||||
type blobReference struct {
|
||||
Digest oci.Digest
|
||||
MediaType oci.MediaType
|
||||
Name string
|
||||
File *packages_model.PackageFileDescriptor
|
||||
ExpectedSize int64
|
||||
IsLead bool
|
||||
}
|
||||
|
||||
func createFileFromBlobReference(ctx context.Context, pv, uploadVersion *packages_model.PackageVersion, ref *blobReference) error {
|
||||
if ref.File.Blob.Size != ref.ExpectedSize {
|
||||
return errSizeInvalid
|
||||
}
|
||||
|
||||
if ref.Name == "" {
|
||||
ref.Name = strings.ToLower(fmt.Sprintf("sha256_%s", ref.File.Blob.HashSHA256))
|
||||
}
|
||||
|
||||
pf := &packages_model.PackageFile{
|
||||
VersionID: pv.ID,
|
||||
BlobID: ref.File.Blob.ID,
|
||||
Name: ref.Name,
|
||||
LowerName: ref.Name,
|
||||
IsLead: ref.IsLead,
|
||||
}
|
||||
var err error
|
||||
if pf, err = packages_model.TryInsertFile(ctx, pf); err != nil {
|
||||
log.Error("Error inserting package file: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
props := map[string]string{
|
||||
container_module.PropertyMediaType: string(ref.MediaType),
|
||||
container_module.PropertyDigest: string(ref.Digest),
|
||||
}
|
||||
for name, value := range props {
|
||||
if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypeFile, pf.ID, name, value); err != nil {
|
||||
log.Error("Error setting package file property: %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the file from the blob upload version
|
||||
if uploadVersion != nil && ref.File.File != nil && uploadVersion.ID == ref.File.File.VersionID {
|
||||
if err := packages_service.DeletePackageFile(ctx, ref.File.File); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createManifestBlob(ctx context.Context, mci *manifestCreationInfo, pv *packages_model.PackageVersion, buf *packages_module.HashedBuffer) (*packages_model.PackageBlob, bool, string, error) {
|
||||
pb, exists, err := packages_model.GetOrInsertBlob(ctx, packages_service.NewPackageBlob(buf))
|
||||
if err != nil {
|
||||
log.Error("Error inserting package blob: %v", err)
|
||||
return nil, false, "", err
|
||||
}
|
||||
if !exists {
|
||||
contentStore := packages_module.NewContentStore()
|
||||
if err := contentStore.Save(packages_module.BlobHash256Key(pb.HashSHA256), buf, buf.Size()); err != nil {
|
||||
log.Error("Error saving package blob in content store: %v", err)
|
||||
return nil, false, "", err
|
||||
}
|
||||
}
|
||||
|
||||
manifestDigest := digestFromHashSummer(buf)
|
||||
err = createFileFromBlobReference(ctx, pv, nil, &blobReference{
|
||||
Digest: oci.Digest(manifestDigest),
|
||||
MediaType: mci.MediaType,
|
||||
Name: container_model.ManifestFilename,
|
||||
File: &packages_model.PackageFileDescriptor{Blob: pb},
|
||||
ExpectedSize: pb.Size,
|
||||
IsLead: true,
|
||||
})
|
||||
|
||||
return pb, !exists, manifestDigest, err
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue