drone/registry/app/pkg/docker/local.go

1887 lines
52 KiB
Go

// Source: https://gitlab.com/gitlab-org/container-registry
// Copyright 2019 Gitlab Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package docker
import (
"bytes"
"context"
"crypto/hmac"
"crypto/sha256"
"database/sql"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"mime"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/harness/gitness/app/api/request"
"github.com/harness/gitness/registry/app/dist_temp/dcontext"
"github.com/harness/gitness/registry/app/dist_temp/errcode"
"github.com/harness/gitness/registry/app/manifest"
"github.com/harness/gitness/registry/app/manifest/manifestlist"
"github.com/harness/gitness/registry/app/manifest/ocischema"
"github.com/harness/gitness/registry/app/manifest/schema2"
"github.com/harness/gitness/registry/app/pkg"
"github.com/harness/gitness/registry/app/pkg/commons"
"github.com/harness/gitness/registry/app/storage"
"github.com/harness/gitness/registry/app/store"
"github.com/harness/gitness/registry/app/store/database/util"
"github.com/harness/gitness/registry/gc"
"github.com/harness/gitness/registry/types"
store2 "github.com/harness/gitness/store"
"github.com/harness/gitness/store/database/dbtx"
gitnesstypes "github.com/harness/gitness/types"
"github.com/distribution/distribution/v3"
"github.com/distribution/reference"
"github.com/opencontainers/go-digest"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/rs/zerolog/log"
)
const (
defaultArch = "amd64"
defaultOS = "linux"
imageClass = "image"
)
type storageType int
const (
manifestSchema2 storageType = iota // 0
manifestlistSchema // 1
ociSchema // 2
ociImageIndexSchema // 3
numStorageTypes // 4
)
const (
manifestListCreateGCReviewWindow = 1 * time.Hour
manifestListCreateGCLockTimeout = 10 * time.Second
manifestTagGCLockTimeout = 10 * time.Second
tagDeleteGCLockTimeout = 10 * time.Second
manifestTagGCReviewWindow = 1 * time.Hour
manifestDeleteGCReviewWindow = 1 * time.Hour
manifestDeleteGCLockTimeout = 10 * time.Second
blobExistsGCLockTimeout = 10 * time.Second
blobExistsGCReviewWindow = 1 * time.Hour
DefaultMaximumReturnedEntries = 100
)
const (
ReferrersSchemaVersion = 2
ReferrersMediaType = "application/vnd.oci.image.index.v1+json"
ArtifactTypeLocalRegistry = "Local Registry"
)
type CatalogAPIResponse struct {
Repositories []string `json:"repositories"`
}
type S3Store interface {
}
var errInvalidSecret = fmt.Errorf("invalid secret")
type hmacKey string
func NewLocalRegistry(
app *App, ms ManifestService, manifestDao store.ManifestRepository,
registryDao store.RegistryRepository, registryBlobDao store.RegistryBlobRepository,
blobRepo store.BlobRepository, mtRepository store.MediaTypesRepository,
tagDao store.TagRepository, imageDao store.ImageRepository, artifactDao store.ArtifactRepository,
bandwidthStatDao store.BandwidthStatRepository, downloadStatDao store.DownloadStatRepository,
gcService gc.Service, tx dbtx.Transactor,
) Registry {
return &LocalRegistry{
App: app,
ms: ms,
registryDao: registryDao,
manifestDao: manifestDao,
registryBlobDao: registryBlobDao,
blobRepo: blobRepo,
mtRepository: mtRepository,
tagDao: tagDao,
imageDao: imageDao,
artifactDao: artifactDao,
bandwidthStatDao: bandwidthStatDao,
downloadStatDao: downloadStatDao,
gcService: gcService,
tx: tx,
}
}
type LocalRegistry struct {
App *App
ms ManifestService
registryDao store.RegistryRepository
manifestDao store.ManifestRepository
registryBlobDao store.RegistryBlobRepository
blobRepo store.BlobRepository
mtRepository store.MediaTypesRepository
tagDao store.TagRepository
imageDao store.ImageRepository
artifactDao store.ArtifactRepository
bandwidthStatDao store.BandwidthStatRepository
downloadStatDao store.DownloadStatRepository
gcService gc.Service
tx dbtx.Transactor
}
func (r *LocalRegistry) Base() error {
return nil
}
func (r *LocalRegistry) CanBeMount() (mount bool, repository string, err error) {
// TODO implement me
panic("implement me")
}
func (r *LocalRegistry) GetArtifactType() string {
return ArtifactTypeLocalRegistry
}
func (r *LocalRegistry) getManifest(
ctx context.Context,
manifestDigest digest.Digest,
repoKey string,
imageName string,
info pkg.RegistryInfo,
) (manifest.Manifest, error) {
dbRepo, err := r.registryDao.GetByParentIDAndName(ctx, info.ParentID, repoKey)
if err != nil {
return nil, err
}
if dbRepo == nil {
return nil, manifest.RegistryUnknownError{Name: repoKey}
}
log.Ctx(ctx).Debug().Msgf("getting manifest by digest from database")
dig, _ := types.NewDigest(manifestDigest)
// Find manifest by its digest
dbManifest, err := r.manifestDao.FindManifestByDigest(ctx, dbRepo.ID, imageName, dig)
if err != nil {
if errors.Is(err, store2.ErrResourceNotFound) {
return nil, manifest.UnknownRevisionError{
Name: repoKey,
Revision: manifestDigest,
}
}
return nil, err
}
return DBManifestToManifest(dbManifest)
}
func DBManifestToManifest(dbm *types.Manifest) (manifest.Manifest, error) {
if dbm.SchemaVersion == 1 {
return nil, manifest.ErrSchemaV1Unsupported
}
if dbm.SchemaVersion != 2 {
return nil, fmt.Errorf("unrecognized manifest schema version %d", dbm.SchemaVersion)
}
mediaType := dbm.MediaType
if dbm.NonConformant {
// parse payload and get real media type
var versioned manifest.Versioned
if err := json.Unmarshal(dbm.Payload, &versioned); err != nil {
return nil, fmt.Errorf("failed to unmarshal manifest payload: %w", err)
}
mediaType = versioned.MediaType
}
// This can be an image manifest or a manifest list
switch mediaType {
case schema2.MediaTypeManifest:
m := &schema2.DeserializedManifest{}
if err := m.UnmarshalJSON(dbm.Payload); err != nil {
return nil, err
}
return m, nil
case v1.MediaTypeImageManifest:
m := &ocischema.DeserializedManifest{}
if err := m.UnmarshalJSON(dbm.Payload); err != nil {
return nil, err
}
return m, nil
case manifestlist.MediaTypeManifestList, v1.MediaTypeImageIndex:
m := &manifestlist.DeserializedManifestList{}
if err := m.UnmarshalJSON(dbm.Payload); err != nil {
return nil, err
}
return m, nil
case "":
// OCI image or image index - no media type in the content
// First see if it looks like an image index
resIndex := &manifestlist.DeserializedManifestList{}
if err := resIndex.UnmarshalJSON(dbm.Payload); err != nil {
return nil, err
}
if resIndex.Manifests != nil {
return resIndex, nil
}
// Otherwise, assume it must be an image manifest
m := &ocischema.DeserializedManifest{}
if err := m.UnmarshalJSON(dbm.Payload); err != nil {
return nil, err
}
return m, nil
default:
return nil,
manifest.VerificationErrors{
fmt.Errorf("unrecognized manifest content type %s", dbm.MediaType),
}
}
}
func (r *LocalRegistry) getTag(ctx context.Context, info pkg.RegistryInfo) (*manifest.Descriptor, error) {
dbRepo, err := r.registryDao.GetByParentIDAndName(ctx, info.ParentID, info.RegIdentifier)
if err != nil {
return nil, err
}
if dbRepo == nil {
return nil, manifest.RegistryUnknownError{Name: info.RegIdentifier}
}
log.Ctx(ctx).Info().Msgf("getting manifest by tag from database")
dbManifest, err := r.manifestDao.FindManifestByTagName(ctx, dbRepo.ID, info.Image, info.Tag)
if err != nil {
// at the DB level a tag has a FK to manifests, so a tag cannot exist
// unless it points to an existing manifest
if errors.Is(err, store2.ErrResourceNotFound) {
return nil, manifest.TagUnknownError{Tag: info.Tag}
}
return nil, err
}
return &manifest.Descriptor{Digest: dbManifest.Digest}, nil
}
func etagMatch(headers []string, etag string) bool {
for _, headerVal := range headers {
if headerVal == etag || headerVal == fmt.Sprintf(`"%s"`, etag) {
// allow quoted or unquoted
return true
}
}
return false
}
// copyFullPayload copies the payload of an HTTP request to destWriter. If it
// receives less content than expected, and the client disconnected during the
// upload, it avoids sending a 400 error to keep the logs cleaner.
//
// The copy will be limited to `limit` bytes, if limit is greater than zero.
func copyFullPayload(
ctx context.Context, length int64, body io.ReadCloser,
destWriter io.Writer, action string,
) error {
// Get a channel that tells us if the client disconnects
clientClosed := ctx.Done()
// Read in the data, if any.
copied, err := io.Copy(destWriter, body)
if clientClosed != nil && (err != nil || (length > 0 && copied < length)) {
// Didn't receive as much content as expected. Did the client
// disconnect during the request? If so, avoid returning a 400
// error to keep the logs cleaner.
select {
case <-clientClosed:
// Set the response Code to "499 Client Closed Request"
// Even though the connection has already been closed,
// this causes the logger to pick up a 499 error
// instead of showing 0 for the HTTP status.
// responseWriter.WriteHeader(499)
dcontext.GetLoggerWithFields(
ctx, log.Error(), map[interface{}]interface{}{
"error": err,
"copied": copied,
"contentLength": length,
}, "error", "copied", "contentLength",
).Msg("client disconnected during " + action)
return errors.New("client disconnected")
default:
}
}
if err != nil {
dcontext.GetLogger(ctx, log.Error()).Msgf("unknown error reading request payload: %v", err)
return err
}
return nil
}
func (r *LocalRegistry) HeadBlob(
ctx2 context.Context,
artInfo pkg.RegistryInfo,
) (
responseHeaders *commons.ResponseHeaders, fr *storage.FileReader, size int64, readCloser io.ReadCloser,
redirectURL string, errs []error,
) {
return r.fetchBlobInternal(ctx2, http.MethodHead, artInfo)
}
func (r *LocalRegistry) GetBlob(
ctx2 context.Context,
artInfo pkg.RegistryInfo,
) (
responseHeaders *commons.ResponseHeaders, fr *storage.FileReader, size int64,
readCloser io.ReadCloser, redirectURL string, errs []error,
) {
return r.fetchBlobInternal(ctx2, http.MethodGet, artInfo)
}
func (r *LocalRegistry) fetchBlobInternal(
ctx2 context.Context, method string, info pkg.RegistryInfo,
) (*commons.ResponseHeaders, *storage.FileReader, int64, io.ReadCloser, string, []error) {
ctx := r.App.GetBlobsContext(ctx2, info)
responseHeaders := &commons.ResponseHeaders{
Code: 0,
Headers: make(map[string]string),
}
errs := make([]error, 0)
var dgst digest.Digest
blobs := ctx.OciBlobStore
if err := r.dbBlobLinkExists(ctx, ctx.Digest, info.RegIdentifier, info); err != nil {
errs = append(errs, errcode.FromUnknownError(err))
return responseHeaders, nil, -1, nil, "", errs
}
dgst = ctx.Digest
headers := make(map[string]string)
fileReader, redirectURL, size, err := blobs.ServeBlobInternal(
ctx.Context,
info.RootIdentifier,
dgst,
headers,
method,
)
if err != nil {
if fileReader != nil {
fileReader.Close()
}
if errors.Is(err, storage.ErrBlobUnknown) {
errs = append(errs, errcode.ErrCodeBlobUnknown.WithDetail(ctx.Digest))
} else {
errs = append(errs, errcode.FromUnknownError(err))
}
return responseHeaders, nil, -1, nil, "", errs
}
if http.MethodGet == method {
// This GoRoutine is used to update the bandwidth stat of the artifact
go func(art pkg.RegistryInfo, dgst digest.Digest) {
// Cloning Context.
session, _ := request.AuthSessionFrom(ctx)
ctx3 := request.WithAuthSession(context.Background(), session)
err := r.dbBlobDownloadComplete(ctx3, dgst, info)
if err != nil {
log.Ctx(ctx3).Error().Stack().Str("goRoutine",
"UpdateBandwidth").Err(err).Msgf("error while putting bandwidth stat of artifact, %v",
err)
return
}
log.Ctx(ctx3).Info().Str("goRoutine",
"UpdateBandwidth").Msgf("Successfully updated the bandwidth stat metrics %s", art.Digest)
}(info, dgst)
}
if redirectURL != "" {
return responseHeaders, nil, -1, nil, redirectURL, errs
}
for key, value := range headers {
responseHeaders.Headers[key] = value
}
return responseHeaders, fileReader, size, nil, "", errs
}
func (r *LocalRegistry) PullManifest(
ctx context.Context,
artInfo pkg.RegistryInfo,
acceptHeaders []string,
ifNoneMatchHeader []string,
) (responseHeaders *commons.ResponseHeaders, descriptor manifest.Descriptor, manifest manifest.Manifest, errs []error) {
responseHeaders, descriptor, manifest, errs = r.ManifestExist(ctx, artInfo, acceptHeaders, ifNoneMatchHeader)
// This GoRoutine is used to update the download stat of the artifact when manifest is pulled
go func(art pkg.RegistryInfo) {
// Cloning Context.
session, _ := request.AuthSessionFrom(ctx)
ctx2 := request.WithAuthSession(context.Background(), session)
ctx2 = log.Ctx(ctx2).With().
Str("goRoutine", "UpdateDownload").
Logger().WithContext(ctx2)
err := r.dbGetManifestComplete(ctx2, artInfo)
if err != nil {
log.Ctx(ctx2).Error().Str("goRoutine",
"UpdateDownload").Stack().Err(err).Msgf("error while putting download stat of artifact, %v", err)
return
}
log.Ctx(ctx2).Info().Str("goRoutine",
"UpdateDownload").Msgf("Successfully updated the download stat metrics %s", art.Digest)
}(artInfo)
return responseHeaders, descriptor, manifest, errs
}
func (r *LocalRegistry) getDigestByTag(ctx context.Context, artInfo pkg.RegistryInfo) (digest.Digest, error) {
desc, err := r.getTag(ctx, artInfo)
if err != nil {
var tagUnknownError manifest.TagUnknownError
if errors.As(err, &tagUnknownError) {
return "", errcode.ErrCodeManifestUnknown.WithDetail(err)
}
return "", err
}
return desc.Digest, nil
}
func getDigestFromInfo(artInfo pkg.RegistryInfo) digest.Digest {
if artInfo.Digest != "" {
return digest.Digest(artInfo.Digest)
}
return digest.Digest(artInfo.Reference)
}
func (r *LocalRegistry) getDigest(ctx context.Context, artInfo pkg.RegistryInfo) (digest.Digest, error) {
if artInfo.Tag != "" {
return r.getDigestByTag(ctx, artInfo)
}
return getDigestFromInfo(artInfo), nil
}
func (r *LocalRegistry) ManifestExist(
ctx context.Context,
artInfo pkg.RegistryInfo,
acceptHeaders []string,
ifNoneMatchHeader []string,
) (
responseHeaders *commons.ResponseHeaders, descriptor manifest.Descriptor, manifestResult manifest.Manifest,
errs []error,
) {
tag := artInfo.Tag
supports := r.getSupportsList(acceptHeaders)
d, err := r.getDigest(ctx, artInfo)
if err != nil {
return responseHeaders, descriptor, manifestResult, []error{err}
}
if etagMatch(ifNoneMatchHeader, d.String()) {
r2 := &commons.ResponseHeaders{
Code: http.StatusNotModified,
}
return r2, manifest.Descriptor{Digest: d}, nil, nil
}
manifestResult, err = r.getManifest(ctx, d, artInfo.RegIdentifier, artInfo.Image, artInfo)
if err != nil {
var manifestUnknownRevisionError manifest.UnknownRevisionError
if errors.As(err, &manifestUnknownRevisionError) {
errs = append(errs, errcode.ErrCodeManifestUnknown.WithDetail(err))
}
return responseHeaders, descriptor, manifestResult, errs
}
// determine the type of the returned manifest
manifestType := manifestSchema2
manifestList, isManifestList := manifestResult.(*manifestlist.DeserializedManifestList)
if _, isOCImanifest := manifestResult.(*ocischema.DeserializedManifest); isOCImanifest {
manifestType = ociSchema
} else if isManifestList {
if manifestList.MediaType == manifestlist.MediaTypeManifestList {
manifestType = manifestlistSchema
} else if manifestList.MediaType == v1.MediaTypeImageIndex {
manifestType = ociImageIndexSchema
}
}
if manifestType == ociSchema && !supports[ociSchema] {
errs = append(
errs,
errcode.ErrCodeManifestUnknown.WithMessage(
"OCI manifest found, but accept header does not support OCI manifests",
),
)
return responseHeaders, descriptor, manifestResult, errs
}
if manifestType == ociImageIndexSchema && !supports[ociImageIndexSchema] {
errs = append(
errs,
errcode.ErrCodeManifestUnknown.WithMessage(
"OCI index found, but accept header does not support OCI indexes",
),
)
return responseHeaders, descriptor, manifestResult, errs
}
if tag != "" && manifestType == manifestlistSchema && !supports[manifestlistSchema] {
d, manifestResult, err = r.rewriteManifest(ctx, artInfo, d, manifestList, supports)
if err != nil {
errs = append(errs, err)
return responseHeaders, descriptor, manifestResult, errs
}
}
ct, p, err := manifestResult.Payload()
if err != nil {
return responseHeaders, descriptor, manifestResult, errs
}
r2 := &commons.ResponseHeaders{
Headers: map[string]string{
"Content-Type": ct,
"Content-Length": fmt.Sprint(len(p)),
"Docker-Content-Digest": d.String(),
"Etag": fmt.Sprintf(`"%s"`, d),
},
}
return r2, manifest.Descriptor{Digest: d}, manifestResult, nil
}
func (r *LocalRegistry) rewriteManifest(
ctx context.Context, artInfo pkg.RegistryInfo, d digest.Digest, manifestList *manifestlist.DeserializedManifestList,
supports [4]bool,
) (digest.Digest, manifest.Manifest, error) {
// Rewrite manifest in schema1 format
log.Ctx(ctx).Info().Msgf(
"rewriting manifest list %s in schema1 format to support old client", d.String(),
)
// Find the image manifest corresponding to the default
// platform
var manifestDigest digest.Digest
for _, manifestDescriptor := range manifestList.Manifests {
if manifestDescriptor.Platform.Architecture == defaultArch &&
manifestDescriptor.Platform.OS == defaultOS {
manifestDigest = manifestDescriptor.Digest
break
}
}
if manifestDigest == "" {
return "", nil, errcode.ErrCodeManifestUnknown
}
manifestResult, err := r.getManifest(
ctx, manifestDigest,
artInfo.RegIdentifier, artInfo.Image, artInfo,
)
if err != nil {
var manifestUnknownRevisionError manifest.UnknownRevisionError
if errors.As(err, &manifestUnknownRevisionError) {
return "", nil, errcode.ErrCodeManifestUnknown.WithDetail(err)
}
return "", nil, err
}
if _, isSchema2 := manifestResult.(*schema2.DeserializedManifest); isSchema2 && !supports[manifestSchema2] {
return "", manifestResult, errcode.ErrCodeManifestInvalid.WithMessage("Schema 2 manifest not supported by client")
}
d = manifestDigest
return d, manifestResult, nil
}
func (r *LocalRegistry) getSupportsList(acceptHeaders []string) [4]bool {
var supports [numStorageTypes]bool
// this parsing of Accept Headers is not quite as full-featured as
// godoc.org's parser, but we don't care about "q=" values
// https://github.com/golang/gddo/blob/
// e91d4165076d7474d20abda83f92d15c7ebc3e81/httputil/header/header.go#L165-L202
for _, acceptHeader := range acceptHeaders {
// r.Header[...] is a slice in case the request contains
// the same header more than once
// if the header isn't set, we'll get the zero value,
// which "range" will handle gracefully
// we need to split each header value on "," to get the full
// list of "Accept" values (per RFC 2616)
// https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
for _, mediaType := range strings.Split(acceptHeader, ",") {
mediaType = strings.TrimSpace(mediaType)
if _, _, err := mime.ParseMediaType(mediaType); err != nil {
continue
}
switch mediaType {
case schema2.MediaTypeManifest:
supports[manifestSchema2] = true
case manifestlist.MediaTypeManifestList:
supports[manifestlistSchema] = true
case v1.MediaTypeImageManifest:
supports[ociSchema] = true
case v1.MediaTypeImageIndex:
supports[ociImageIndexSchema] = true
}
}
}
return supports
}
func (r *LocalRegistry) appendPutError(err error, errList []error) []error {
// TODO: Move this error list inside the context
if errors.Is(err, manifest.ErrUnsupported) {
errList = append(errList, errcode.ErrCodeUnsupported)
return errList
}
if errors.Is(err, manifest.ErrAccessDenied) {
errList = append(errList, errcode.ErrCodeDenied)
return errList
}
if errors.Is(err, manifest.ErrSchemaV1Unsupported) {
errList = append(
errList,
errcode.ErrCodeManifestInvalid.WithDetail(
"manifest type unsupported",
),
)
return errList
}
if errors.Is(err, digest.ErrDigestInvalidFormat) {
errList = append(errList, errcode.ErrCodeDigestInvalid.WithDetail(err))
return errList
}
switch {
case errors.As(err, &manifest.VerificationErrors{}):
var verificationError manifest.VerificationErrors
errors.As(err, &verificationError)
for _, verificationError := range verificationError {
switch {
case errors.As(verificationError, &manifest.BlobUnknownError{}):
var manifestBlobUnknownError manifest.BlobUnknownError
errors.As(verificationError, &manifestBlobUnknownError)
errList = append(
errList, errcode.ErrCodeManifestBlobUnknown.WithDetail(
manifestBlobUnknownError.Digest,
),
)
case errors.As(verificationError, &manifest.NameInvalidError{}):
errList = append(
errList, errcode.ErrCodeNameInvalid.WithDetail(err),
)
case errors.As(verificationError, &manifest.UnverifiedError{}):
errList = append(errList, errcode.ErrCodeManifestUnverified)
case errors.As(verificationError, &manifest.ReferencesExceedLimitError{}):
errList = append(
errList, errcode.ErrCodeManifestReferenceLimit.WithDetail(err),
)
case errors.As(verificationError, &manifest.PayloadSizeExceedsLimitError{}):
errList = append(
errList, errcode.ErrCodeManifestPayloadSizeLimit.WithDetail(err.Error()),
)
default:
if errors.Is(verificationError, digest.ErrDigestInvalidFormat) {
errList = append(errList, errcode.ErrCodeDigestInvalid)
} else {
errList = append(errList, errcode.FromUnknownError(verificationError))
}
}
}
case errors.As(err, &errcode.Error{}):
errList = append(errList, err)
default:
errList = append(errList, errcode.FromUnknownError(err))
}
return errList
}
func (r *LocalRegistry) PutManifest(
ctx context.Context,
artInfo pkg.RegistryInfo,
mediaType string,
body io.ReadCloser,
length int64,
) (responseHeaders *commons.ResponseHeaders, errs []error) {
var jsonBuf bytes.Buffer
d, _ := digest.Parse(artInfo.Digest)
tag := artInfo.Tag
log.Ctx(ctx).Info().Msgf("Pushing manifest %s %s / %s", artInfo.RegIdentifier, d, tag)
responseHeaders = &commons.ResponseHeaders{
Headers: map[string]string{},
Code: http.StatusCreated,
}
if err := copyFullPayload(ctx, length, body, &jsonBuf, "image manifest PUT"); err != nil {
// copyFullPayload reports the error if necessary
errs = append(errs, errcode.ErrCodeManifestInvalid.WithDetail(err.Error()))
return responseHeaders, errs
}
unmarshalManifest, desc, err := manifest.UnmarshalManifest(mediaType, jsonBuf.Bytes())
if err != nil {
errs = append(errs, errcode.ErrCodeManifestInvalid.WithDetail(err))
return responseHeaders, errs
}
if d != "" {
if desc.Digest != d {
log.Ctx(ctx).Error().Stack().Err(err).Msgf("payload digest does not match: %q != %q", desc.Digest, d)
errs = append(errs, errcode.ErrCodeDigestInvalid)
return responseHeaders, errs
}
} else {
if tag != "" {
d = desc.Digest
} else {
errs = append(errs, errcode.ErrCodeTagInvalid.WithDetail("no tag or digest specified"))
return responseHeaders, errs
}
}
isAnOCIManifest := mediaType == v1.MediaTypeImageManifest ||
mediaType == v1.MediaTypeImageIndex
if isAnOCIManifest {
log.Ctx(ctx).Debug().Msg("Putting an OCI Manifest!")
} else {
log.Ctx(ctx).Debug().Msg("Putting a Docker Manifest!")
}
// We don't need to store manifest file in S3 storage
// manifestServicePut(ctx, _manifest, options...)
if err = r.ms.DBPut(
ctx, unmarshalManifest, d, artInfo.RegIdentifier,
responseHeaders, artInfo,
); err != nil {
errs = r.appendPutError(err, errs)
return responseHeaders, errs
}
// Tag this manifest
if tag != "" {
if err = r.ms.DBTag(
ctx, unmarshalManifest, d, tag, artInfo.RegIdentifier,
responseHeaders, artInfo,
); err != nil {
errs = r.appendPutError(err, errs)
return responseHeaders, errs
}
}
if err != nil {
return r.handlePutManifestErrors(err, errs, responseHeaders)
}
// Tag this manifest
if tag != "" {
err = tagserviceTag()
// err = tags.Tag(imh, Tag, desc)
if err != nil {
errs = append(errs, errcode.ErrCodeUnknown.WithDetail(err))
return responseHeaders, errs
}
}
// Construct a canonical url for the uploaded manifest.
name, _ := reference.WithName(fmt.Sprintf("%s/%s", artInfo.RegIdentifier, artInfo.Image))
canonicalRef, err := reference.WithDigest(name, d)
if err != nil {
errs = append(errs, errcode.ErrCodeUnknown.WithDetail(err))
return responseHeaders, errs
}
builder := artInfo.URLBuilder
location, err := builder.BuildManifestURL(canonicalRef)
if err != nil {
log.Ctx(ctx).Error().Stack().Err(
err,
).Msgf("error building manifest url from digest: %v", err)
}
responseHeaders.Headers["Location"] = location
responseHeaders.Headers["Docker-Content-Digest"] = d.String()
responseHeaders.Code = http.StatusCreated
log.Ctx(ctx).Debug().Msgf("Succeeded in putting manifest: %s", d.String())
return responseHeaders, errs
}
func (r *LocalRegistry) handlePutManifestErrors(
err error, errs []error, responseHeaders *commons.ResponseHeaders,
) (*commons.ResponseHeaders, []error) {
if errors.Is(err, manifest.ErrUnsupported) {
errs = append(errs, errcode.ErrCodeUnsupported)
return responseHeaders, errs
}
if errors.Is(err, manifest.ErrAccessDenied) {
errs = append(errs, errcode.ErrCodeDenied)
return responseHeaders, errs
}
switch {
case errors.As(err, &manifest.VerificationErrors{}):
var verificationError manifest.VerificationErrors
errors.As(err, &verificationError)
for _, verificationError := range verificationError {
switch {
case errors.As(verificationError, &manifest.BlobUnknownError{}):
var manifestBlobUnknownError manifest.BlobUnknownError
errors.As(verificationError, &manifestBlobUnknownError)
dgst := manifestBlobUnknownError.Digest
errs = append(
errs,
errcode.ErrCodeManifestBlobUnknown.WithDetail(
dgst,
),
)
case errors.As(verificationError, &manifest.NameInvalidError{}):
errs = append(
errs,
errcode.ErrCodeNameInvalid.WithDetail(err),
)
case errors.As(verificationError, &manifest.UnverifiedError{}):
errs = append(errs, errcode.ErrCodeManifestUnverified)
default:
if errors.Is(verificationError, digest.ErrDigestInvalidFormat) {
errs = append(errs, errcode.ErrCodeDigestInvalid)
} else {
errs = append(errs, errcode.ErrCodeUnknown, verificationError)
}
}
}
case errors.As(err, &errcode.Error{}):
errs = append(errs, err)
default:
errs = append(errs, errcode.ErrCodeUnknown.WithDetail(err))
}
return responseHeaders, errs
}
func tagserviceTag() error {
// TODO: implement this
return nil
}
func (r *LocalRegistry) PushBlobMonolith(
_ context.Context,
_ pkg.RegistryInfo,
_ int64,
_ io.Reader,
) error {
return nil
}
func (r *LocalRegistry) InitBlobUpload(
ctx2 context.Context,
artInfo pkg.RegistryInfo,
fromRepo, mountDigest string,
) (*commons.ResponseHeaders, []error) {
blobCtx := r.App.GetBlobsContext(ctx2, artInfo)
var errList []error
responseHeaders := &commons.ResponseHeaders{
Headers: make(map[string]string),
Code: 0,
}
digest := digest.Digest(mountDigest)
if mountDigest != "" && fromRepo != "" {
err := r.dbMountBlob(blobCtx, fromRepo, artInfo.RegIdentifier, digest, artInfo)
if err != nil {
e := fmt.Errorf("failed to mount blob in database: %w", err)
errList = append(errList, errcode.FromUnknownError(e))
}
if err = writeBlobCreatedHeaders(
blobCtx, digest,
responseHeaders, artInfo,
); err != nil {
errList = append(errList, errcode.ErrCodeUnknown.WithDetail(err))
}
return responseHeaders, errList
}
blobs := blobCtx.OciBlobStore
upload, err := blobs.Create(blobCtx.Context)
if err != nil {
if errors.Is(err, storage.ErrUnsupported) {
errList = append(errList, errcode.ErrCodeUnsupported)
} else {
errList = append(errList, errcode.ErrCodeUnknown.WithDetail(err))
}
return responseHeaders, errList
}
blobCtx.Upload = upload
if err = blobUploadResponse(
blobCtx, responseHeaders,
artInfo.RegIdentifier, artInfo,
); err != nil {
errList = append(errList, errcode.ErrCodeUnknown.WithDetail(err))
return responseHeaders, errList
}
responseHeaders.Headers[commons.HeaderDockerUploadUUID] = blobCtx.Upload.ID()
responseHeaders.Code = http.StatusAccepted
return responseHeaders, nil
}
func (r *LocalRegistry) PushBlobMonolithWithDigest(
_ context.Context,
_ pkg.RegistryInfo,
_ int64,
_ io.Reader,
) error {
return nil
}
func (r *LocalRegistry) PushBlobChunk(
ctx *Context,
artInfo pkg.RegistryInfo,
contentType string,
contentRange string,
contentLength string,
body io.ReadCloser,
contentLengthFromRequest int64,
) (responseHeaders *commons.ResponseHeaders, errs []error) {
responseHeaders = &commons.ResponseHeaders{
Code: 0,
Headers: make(map[string]string),
}
errs = make([]error, 0)
if ctx.Upload == nil {
e := errcode.ErrCodeBlobUploadUnknown
errs = append(errs, e)
return responseHeaders, errs
}
if contentType != "" && contentType != "application/octet-stream" {
e := errcode.ErrCodeUnknown.WithDetail(fmt.Errorf("bad Content-Type"))
errs = append(errs, e)
return responseHeaders, errs
}
if contentRange != "" && contentLength != "" {
start, end, err := parseContentRange(contentRange)
if err != nil {
errs = append(errs, errcode.ErrCodeUnknown.WithDetail(err.Error()))
return responseHeaders, errs
}
if start > end || start != ctx.Upload.Size() {
errs = append(errs, errcode.ErrCodeRangeInvalid)
return responseHeaders, errs
}
clInt, err := strconv.ParseInt(contentLength, 10, 64)
if err != nil {
errs = append(errs, errcode.ErrCodeUnknown.WithDetail(err.Error()))
return responseHeaders, errs
}
if clInt != (end-start)+1 {
errs = append(errs, errcode.ErrCodeSizeInvalid)
return responseHeaders, errs
}
}
if err := copyFullPayload(
ctx, contentLengthFromRequest, body, ctx.Upload,
"blob PATCH",
); err != nil {
errs = append(
errs,
errcode.ErrCodeUnknown.WithDetail(err.Error()),
)
return responseHeaders, errs
}
if err := blobUploadResponse(
ctx, responseHeaders, artInfo.RegIdentifier,
artInfo,
); err != nil {
errs = append(errs, errcode.ErrCodeUnknown.WithDetail(err))
return responseHeaders, errs
}
responseHeaders.Code = http.StatusAccepted
return responseHeaders, errs
}
func (r *LocalRegistry) PushBlob(
ctx2 context.Context,
artInfo pkg.RegistryInfo,
body io.ReadCloser,
contentLength int64,
stateToken string,
) (responseHeaders *commons.ResponseHeaders, errs []error) {
errs = make([]error, 0)
responseHeaders = &commons.ResponseHeaders{
Code: 0,
Headers: make(map[string]string),
}
ctx := r.App.GetBlobsContext(ctx2, artInfo)
if ctx.UUID != "" {
resumeErrs := ResumeBlobUpload(ctx, stateToken)
errs = append(errs, resumeErrs...)
}
if ctx.Upload == nil {
err := errcode.ErrCodeBlobUploadUnknown
errs = append(errs, err)
return responseHeaders, errs
}
defer ctx.Upload.Close()
if artInfo.Digest == "" {
// no digest? return error, but allow retry.
err := errcode.ErrCodeDigestInvalid.WithDetail("digest missing")
errs = append(errs, err)
return responseHeaders, errs
}
dgst, err := digest.Parse(artInfo.Digest)
if err != nil {
// no digest? return error, but allow retry.
errs = append(
errs,
errcode.ErrCodeDigestInvalid.WithDetail(
"digest parsing failed",
),
)
return responseHeaders, errs
}
if err := copyFullPayload(
ctx, contentLength, body, ctx.Upload,
"blob PUT",
); err != nil {
errs = append(
errs,
errcode.ErrCodeUnknown.WithDetail(err.Error()),
)
return responseHeaders, errs
}
desc, err := ctx.Upload.Commit(
ctx, artInfo.RootIdentifier, manifest.Descriptor{
Digest: dgst,
},
)
if err != nil {
switch {
case errors.As(err, &storage.BlobInvalidDigestError{}):
errs = append(
errs,
errcode.ErrCodeDigestInvalid.WithDetail(err),
)
case errors.As(err, &errcode.Error{}):
errs = append(errs, err)
default:
switch {
case errors.Is(err, storage.ErrAccessDenied):
errs = append(errs, errcode.ErrCodeDenied)
case errors.Is(err, storage.ErrUnsupported):
errs = append(
errs,
errcode.ErrCodeUnsupported,
)
case errors.Is(err, storage.ErrBlobInvalidLength), errors.Is(err, storage.ErrBlobDigestUnsupported):
errs = append(
errs,
errcode.ErrCodeBlobUploadInvalid.WithDetail(err),
)
default:
dcontext.GetLogger(ctx, log.Error()).Msgf("unknown error completing upload: %v", err)
errs = append(errs, errcode.ErrCodeUnknown.WithDetail(err))
}
}
// Clean up the backend blob data if there was an error.
if err := ctx.Upload.Cancel(ctx); err != nil {
// If the cleanup fails, all we can do is observe and report.
log.Error().Stack().Err(
err,
).Msgf("error canceling upload after error: %v", err)
}
return responseHeaders, errs
}
err = r.dbPutBlobUploadComplete(
ctx,
artInfo.RegIdentifier,
"application/octet-stream",
artInfo.Digest,
int(desc.Size),
artInfo,
)
if err != nil {
errs = append(errs, err)
log.Error().Stack().Err(err).Msgf(
"ensure blob %s failed, error: %v",
artInfo.Digest, err,
)
return responseHeaders, errs
}
if err := writeBlobCreatedHeaders(
ctx, desc.Digest,
responseHeaders, artInfo,
); err != nil {
errs = append(errs, errcode.ErrCodeUnknown.WithDetail(err))
return responseHeaders, errs
}
return responseHeaders, errs
}
func (r *LocalRegistry) ListTags(
c context.Context,
lastEntry string,
maxEntries int,
origURL string,
artInfo pkg.RegistryInfo,
) (*commons.ResponseHeaders, []string, error) {
filters := types.FilterParams{
LastEntry: lastEntry,
MaxEntries: maxEntries,
}
tags, moreEntries, err := r.dbGetTags(c, filters, artInfo)
if err != nil {
return nil, nil, err
}
if len(tags) == 0 {
// If no tags are found, the current implementation (`else`)
// returns a nil slice instead of an empty one,
// so we have to enforce the same behavior here, for consistency.
tags = nil
}
responseHeaders := &commons.ResponseHeaders{
Headers: map[string]string{"Content-Type": "application/json"},
Code: 0,
}
// Add a link header if there are more entries to retrieve
// (only supported by the metadata database backend)
if moreEntries {
filters.LastEntry = tags[len(tags)-1]
urlStr, err := CreateLinkEntry(origURL, filters, "", "")
if err != nil {
return responseHeaders, nil, errcode.ErrCodeUnknown.WithDetail(err)
}
if urlStr != "" {
responseHeaders.Headers["Link"] = urlStr
}
}
return responseHeaders, tags, nil
}
func (r *LocalRegistry) ListFilteredTags(
_ context.Context,
_ int,
_, _ string,
_ pkg.RegistryInfo,
) (tags []string, err error) {
return nil, nil
}
func (r *LocalRegistry) DeleteManifest(
ctx context.Context,
artInfo pkg.RegistryInfo,
) (errs []error, responseHeaders *commons.ResponseHeaders) {
log.Debug().Msg("DeleteImageManifest")
var tag = artInfo.Tag
var d = artInfo.Digest
responseHeaders = &commons.ResponseHeaders{}
// TODO: If Tag is not empty, we just untag the tag, nothing more!
if tag != "" {
log.Debug().Msg("DeleteImageTag")
_, err := r.ms.DeleteTag(ctx, artInfo.RegIdentifier, tag, artInfo)
if err != nil {
errs = append(errs, err)
return errs, responseHeaders
}
responseHeaders.Code = http.StatusAccepted
return errs, responseHeaders
}
err := r.ms.DeleteManifest(
ctx, artInfo.RegIdentifier,
digest.Digest(d), artInfo,
)
if err != nil {
switch {
case errors.Is(err, digest.ErrDigestUnsupported):
case errors.Is(err, digest.ErrDigestInvalidFormat):
errs = append(errs, errcode.ErrCodeDigestInvalid)
case errors.Is(err, storage.ErrBlobUnknown):
errs = append(errs, errcode.ErrCodeManifestUnknown)
case errors.Is(err, manifest.ErrUnsupported):
errs = append(errs, errcode.ErrCodeUnsupported)
case errors.Is(err, util.ErrManifestNotFound):
errs = append(errs, errcode.ErrCodeManifestUnknown)
case errors.Is(err, util.ErrManifestReferencedInList):
errs = append(errs, errcode.ErrCodeManifestReferencedInList)
default:
errs = append(errs, errcode.ErrCodeUnknown)
}
return errs, responseHeaders
}
responseHeaders.Code = http.StatusAccepted
return errs, responseHeaders
}
func (r *LocalRegistry) DeleteBlob(
ctx *Context,
artInfo pkg.RegistryInfo,
) (responseHeaders *commons.ResponseHeaders, errs []error) {
responseHeaders = &commons.ResponseHeaders{
Code: 0,
Headers: make(map[string]string),
}
errs = make([]error, 0)
err := r.dbDeleteBlob(ctx, r.App.Config, artInfo.RegIdentifier, digest.Digest(artInfo.Digest), artInfo)
if err != nil {
switch {
case errors.Is(err, storage.ErrUnsupported):
errs = append(errs, errcode.ErrCodeUnsupported)
case errors.Is(err, storage.ErrBlobUnknown):
errs = append(errs, errcode.ErrCodeBlobUnknown)
case errors.Is(err, storage.RegistryUnknownError{Name: artInfo.RegIdentifier}):
errs = append(errs, errcode.ErrCodeNameUnknown)
default:
errs = append(errs, errcode.FromUnknownError(err))
log.Error().Stack().Msg("failed to delete blob")
}
return
}
responseHeaders.Headers["Content-Length"] = "0"
responseHeaders.Code = http.StatusAccepted
return
}
func (r *LocalRegistry) MountBlob(
_ context.Context,
_ pkg.RegistryInfo,
_, _ string,
) (err error) {
return nil
}
func (r *LocalRegistry) ListReferrers(
ctx context.Context,
artInfo pkg.RegistryInfo,
artifactType string,
) (index *v1.Index, responseHeaders *commons.ResponseHeaders, err error) {
mfs := make([]v1.Descriptor, 0)
rsHeaders := &commons.ResponseHeaders{
Headers: map[string]string{"Content-Type": ReferrersMediaType},
Code: 0,
}
if artifactType != "" {
rsHeaders.Headers["OCI-Filters-Applied"] = "artifactType"
}
registry, err := r.registryDao.GetByParentIDAndName(ctx, artInfo.ParentID, artInfo.RegIdentifier)
if err != nil {
return nil, rsHeaders, err
}
if registry == nil {
err := errcode.ErrCodeNameUnknown.WithDetail(artInfo.RegIdentifier)
return nil, rsHeaders, err
}
subjectDigest, err := types.NewDigest(digest.Digest(artInfo.Digest))
if err != nil {
return nil, rsHeaders, err
}
manifests, err := r.manifestDao.ListManifestsBySubjectDigest(
ctx, registry.ID, subjectDigest,
)
if err != nil && !errors.Is(err, store2.ErrResourceNotFound) {
return nil, rsHeaders, err
}
for _, m := range manifests {
mf := v1.Descriptor{
MediaType: m.MediaType,
Size: m.TotalSize,
Digest: m.Digest,
Annotations: m.Annotations,
}
if m.ArtifactType.Valid {
mf.ArtifactType = m.ArtifactType.String
} else {
mf.ArtifactType = m.Configuration.MediaType
}
// filter by the artifactType since the artifactType is
// actually the config media type of the artifact.
if artifactType != "" {
if mf.ArtifactType == artifactType {
mfs = append(mfs, mf)
}
} else {
mfs = append(mfs, mf)
}
}
// Populate index manifest
result := &v1.Index{}
result.SchemaVersion = ReferrersSchemaVersion
result.MediaType = ReferrersMediaType
result.Manifests = mfs
return result, rsHeaders, nil
}
func (r *LocalRegistry) GetBlobUploadStatus(
ctx *Context,
artInfo pkg.RegistryInfo,
_ string,
) (*commons.ResponseHeaders, []error) {
responseHeaders := &commons.ResponseHeaders{
Code: 0,
Headers: make(map[string]string),
}
errList := make([]error, 0)
log.Debug().Msgf("GetBlobUploadStatus")
if ctx.Upload == nil {
blobs := ctx.OciBlobStore
upload, err := blobs.Resume(ctx, ctx.UUID)
if err != nil {
if errors.Is(err, distribution.ErrBlobUploadUnknown) {
errList = append(
errList,
errcode.ErrCodeBlobUploadUnknown.WithDetail(err),
)
} else {
errList = append(
errList,
errcode.ErrCodeUnknown.WithDetail(err),
)
}
return responseHeaders, errList
}
ctx.Upload = upload
}
if err := blobUploadResponse(
ctx, responseHeaders,
artInfo.RegIdentifier, artInfo,
); err != nil {
errList = append(
errList,
errcode.ErrCodeUnknown.WithDetail(err),
)
return responseHeaders, errList
}
responseHeaders.Code = http.StatusNoContent
return responseHeaders, nil
}
func (r *LocalRegistry) GetCatalog() (repositories []string, err error) {
return nil, nil
}
func (r *LocalRegistry) DeleteTag(
_, _ string,
_ pkg.RegistryInfo,
) error {
return nil
}
func (r *LocalRegistry) PullBlobChunk(
_, _ string,
_, _, _ int64,
_ pkg.RegistryInfo,
) (size int64, blob io.ReadCloser, err error) {
return 0, nil, nil
}
// WriteBlobCreatedHeaders writes the standard Headers
//
// describing a newly
//
// created blob. A 201 Created is written as well as the
//
// canonical URL and
//
// blob digest.
func writeBlobCreatedHeaders(
context *Context,
digest digest.Digest,
headers *commons.ResponseHeaders,
info pkg.RegistryInfo,
) error {
path, err := reference.WithName(fmt.Sprintf("%s/%s/%s", info.RootIdentifier, info.RegIdentifier, info.Image))
if err != nil {
return err
}
ref, err := reference.WithDigest(path, digest)
if err != nil {
return err
}
blobURL, err := context.URLBuilder.BuildBlobURL(ref)
if err != nil {
return err
}
headers.Headers = map[string]string{
"Location": blobURL,
"Docker-Content-Digest": digest.String(),
"Content-Length": "0",
}
headers.Code = http.StatusCreated
return nil
}
func blobUploadResponse(
context *Context,
headers *commons.ResponseHeaders,
repoKey string,
info pkg.RegistryInfo,
) error {
context.State.Path = context.OciBlobStore.Path()
context.State.UUID = context.Upload.ID()
context.Upload.Close()
context.State.Offset = context.Upload.Size()
token, err := hmacKey(
context.Config.Registry.HTTP.Secret,
).packUploadState(
context.State,
)
if err != nil {
log.Info().Msgf("error building upload state token: %s", err)
return err
}
image := info.Image
path, err := reference.WithName(fmt.Sprintf("%s/%s/%s", info.RootIdentifier, repoKey, image))
if err != nil {
return err
}
uploadURL, err := context.URLBuilder.BuildBlobUploadChunkURL(
path, context.Upload.ID(),
url.Values{
"_state": []string{token},
},
)
if err != nil {
log.Info().Msgf("error building upload url: %s", err)
return err
}
endRange := context.Upload.Size()
if endRange > 0 {
endRange--
}
headers.Headers["Docker-Upload-UUID"] = context.UUID
headers.Headers["Location"] = uploadURL
headers.Headers["Content-Length"] = "0"
headers.Headers["Range"] = fmt.Sprintf("0-%d", endRange)
return nil
}
// packUploadState packs the upload state signed with and hmac digest using
// the hmacKey secret, encoding to url safe base64. The resulting token can be
// used to share data with minimized risk of external tampering.
func (secret hmacKey) packUploadState(lus BlobUploadState) (string, error) {
mac := hmac.New(sha256.New, []byte(secret))
p, err := json.Marshal(lus)
if err != nil {
return "", err
}
mac.Write(p)
return base64.URLEncoding.EncodeToString(append(mac.Sum(nil), p...)), nil
}
func parseContentRange(cr string) (start int64, end int64, err error) {
rStart, rEnd, ok := strings.Cut(cr, "-")
if !ok {
return -1, -1, fmt.Errorf("invalid content range format, %s", cr)
}
start, err = strconv.ParseInt(rStart, 10, 64)
if err != nil {
return -1, -1, err
}
end, err = strconv.ParseInt(rEnd, 10, 64)
if err != nil {
return -1, -1, err
}
return start, end, nil
}
func (r *LocalRegistry) dbBlobLinkExists(
ctx context.Context, dgst digest.Digest, repoKey string,
info pkg.RegistryInfo,
) error {
reg, err := r.registryDao.GetByParentIDAndName(ctx, info.ParentID, repoKey)
if err != nil {
return err
}
if r == nil {
err := errcode.ErrCodeNameUnknown.WithDetail(repoKey)
return err
}
blob, err := r.blobRepo.FindByDigestAndRootParentID(ctx, dgst, info.RootParentID)
if err != nil {
if errors.Is(err, store2.ErrResourceNotFound) {
err = errcode.ErrCodeBlobUnknown.WithDetail(dgst)
}
return err
}
err = r.tx.WithTx(
ctx, func(ctx context.Context) error {
// Prevent long running transactions by setting an upper limit of blobExistsGCLockTimeout. If the GC is holding
// the lock of a related review record, the processing there should be fast enough to avoid this. Regardless, we
// should not let transactions open (and clients waiting) for too long. If this sensible timeout is exceeded, abort
// the operation and let the client retry. This will bubble up and lead to a 503 Service Unavailable response.
ctx, cancel := context.WithTimeout(ctx, blobExistsGCLockTimeout)
defer cancel()
bt, err := r.gcService.BlobFindAndLockBefore(ctx, blob.ID, time.Now().Add(blobExistsGCReviewWindow))
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return err
}
if bt != nil {
err = r.gcService.BlobReschedule(ctx, bt, 24*time.Hour)
if err != nil {
return err
}
}
found, err := r.blobRepo.ExistsBlob(ctx, reg.ID, dgst, info.Image)
if err != nil {
return err
}
if !found {
err := errcode.ErrCodeBlobUnknown.WithDetail(dgst)
return err
}
return nil
},
)
if err != nil {
return fmt.Errorf("committing database transaction: %w", err)
}
return nil
}
func (r *LocalRegistry) dbBlobDownloadComplete(
ctx context.Context,
dgst digest.Digest,
info pkg.RegistryInfo,
) error {
err := r.tx.WithTx(
ctx, func(ctx context.Context) error {
registry, err := r.registryDao.GetByParentIDAndName(ctx, info.ParentID, info.RegIdentifier)
if err != nil {
return err
}
blob, err := r.blobRepo.FindByDigestAndRootParentID(ctx, dgst, info.RootParentID)
if err != nil {
return err
}
image, err := r.imageDao.GetByName(ctx, registry.ID, info.Image)
if err != nil {
return err
}
bandwidthStat := &types.BandwidthStat{
ImageID: image.ID,
Type: types.BandwidthTypeDOWNLOAD,
Bytes: blob.Size,
}
if err := r.bandwidthStatDao.Create(ctx, bandwidthStat); err != nil {
return err
}
return nil
}, dbtx.TxDefault,
)
if err != nil {
log.Error().Msgf("failed to put download bandwidth stat in database: %v", err)
return fmt.Errorf("committing database transaction: %w", err)
}
return nil
}
func (r *LocalRegistry) dbGetManifestComplete(
ctx context.Context,
info pkg.RegistryInfo,
) error {
// FIXME: Update logic incase requests are internal. Currently, we are updating the stats for all requests.
if info.Digest == "" {
return nil
}
err := r.tx.WithTx(
ctx, func(ctx context.Context) error {
registry, err := r.registryDao.GetByParentIDAndName(ctx, info.ParentID, info.RegIdentifier)
if err != nil {
return err
}
image, err := r.imageDao.GetByName(ctx, registry.ID, info.Image)
if err != nil {
return err
}
newDigest, err := types.NewDigest(digest.Digest(info.Digest))
if err != nil {
log.Ctx(ctx).Error().Stack().Err(err).Msgf("error parsing digest: %s %v", info.Digest, err)
}
artifact, err := r.artifactDao.GetByName(ctx, image.ID, newDigest.String())
if err != nil {
return err
}
downloadStat := &types.DownloadStat{
ArtifactID: artifact.ID,
}
if err := r.downloadStatDao.Create(ctx, downloadStat); err != nil {
return err
}
return nil
}, dbtx.TxDefault,
)
if err != nil {
log.Error().Msgf("failed to put download stat in database: %v", err)
return fmt.Errorf("committing database transaction: %w", err)
}
return nil
}
func (r *LocalRegistry) dbPutBlobUploadComplete(
ctx context.Context,
repoName string,
mediaType string,
digestVal string,
size int,
info pkg.RegistryInfo,
) error {
blob := &types.Blob{
RootParentID: info.RootParentID,
Digest: digest.Digest(digestVal),
MediaType: mediaType,
Size: int64(size),
}
err := r.tx.WithTx(
ctx, func(ctx context.Context) error {
registry, err := r.registryDao.GetByParentIDAndName(ctx, info.ParentID, repoName)
if err != nil {
return err
}
storedBlob, err := r.blobRepo.CreateOrFind(ctx, blob)
if err != nil && !errors.Is(err, store2.ErrResourceNotFound) {
return err
}
// link blob to repository
if err := r.registryBlobDao.LinkBlob(ctx, info.Image, registry, storedBlob.ID); err != nil {
return err
}
image := &types.Image{
Name: info.Image,
RegistryID: registry.ID,
Enabled: false,
}
if err := r.imageDao.CreateOrUpdate(ctx, image); err != nil {
return err
}
bandwidthStat := &types.BandwidthStat{
ImageID: image.ID,
Type: types.BandwidthTypeUPLOAD,
Bytes: int64(size),
}
if err := r.bandwidthStatDao.Create(ctx, bandwidthStat); err != nil {
return err
}
return nil
}, dbtx.TxDefault,
)
if err != nil {
log.Error().Msgf("failed to put blob in database: %v", err)
return fmt.Errorf("committing database transaction: %w", err)
}
return nil
}
// dbDeleteBlob does not actually delete a blob from the database
// (that's GC's responsibility), it only unlinks it from
// a repository.
func (r *LocalRegistry) dbDeleteBlob(
ctx *Context, config *gitnesstypes.Config,
repoName string, d digest.Digest, info pkg.RegistryInfo,
) error {
log.Debug().Msgf("deleting blob from repository in database")
if !config.Registry.Storage.S3Storage.Delete {
return storage.ErrUnsupported
}
reg, err := r.registryDao.GetByParentIDAndName(ctx, info.ParentID, repoName)
if err != nil {
return err
}
if r == nil {
return storage.RegistryUnknownError{Name: repoName}
}
blob, err := r.blobRepo.FindByDigestAndRepoID(ctx, d, reg.ID, info.Image)
if err != nil {
if errors.Is(err, store2.ErrResourceNotFound) {
return storage.ErrBlobUnknown
}
return err
}
found, err := r.registryBlobDao.UnlinkBlob(ctx, info.Image, reg, blob.ID)
if err != nil {
return err
}
if !found {
return storage.ErrBlobUnknown
}
return nil
}
func (r *LocalRegistry) dbGetTags(
ctx context.Context, filters types.FilterParams,
info pkg.RegistryInfo,
) ([]string, bool, error) {
log.Debug().Msgf("finding tags in database")
reg, err := r.registryDao.GetByParentIDAndName(ctx, info.ParentID, info.RegIdentifier)
if err != nil {
return nil, false, err
}
if r == nil {
return nil, false,
errcode.ErrCodeNameUnknown.WithDetail(map[string]string{"name": info.RegIdentifier})
}
tt, err := r.tagDao.TagsPaginated(ctx, reg.ID, info.Image, filters)
if err != nil {
return nil, false, err
}
tags := make([]string, 0, len(tt))
for _, t := range tt {
tags = append(tags, t.Name)
}
var moreEntries bool
if len(tt) > 0 {
filters.LastEntry = tt[len(tt)-1].Name
moreEntries, err = r.tagDao.HasTagsAfterName(ctx, reg.ID, filters)
if err != nil {
return nil, false, err
}
}
return tags, moreEntries, nil
}
func (r *LocalRegistry) dbMountBlob(
ctx context.Context, fromRepo, toRepo string,
d digest.Digest, info pkg.RegistryInfo,
) error {
log.Debug().Msgf("cross repository blob mounting")
destRepo, err := r.registryDao.GetByParentIDAndName(ctx, info.ParentID, toRepo)
if err != nil {
return err
}
if destRepo == nil {
return fmt.Errorf(
"destination repository: [%s] not found in database",
toRepo,
)
}
sourceRepo, err := r.registryDao.GetByParentIDAndName(ctx, info.ParentID, fromRepo)
if err != nil {
return err
}
if sourceRepo == nil {
return fmt.Errorf(
"source repository: [%s] not found in database",
fromRepo,
)
}
b, err := r.ms.DBFindRepositoryBlob(
ctx, manifest.Descriptor{Digest: d},
sourceRepo.ID, info,
)
if err != nil {
return err
}
return r.registryBlobDao.LinkBlob(ctx, info.Image, destRepo, b.ID)
}