mirror of https://github.com/harness/drone.git
1132 lines
36 KiB
Go
1132 lines
36 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 (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
gas "github.com/harness/gitness/app/store"
|
|
"github.com/harness/gitness/registry/app/api/openapi/contracts/artifact"
|
|
"github.com/harness/gitness/registry/app/event"
|
|
"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/store"
|
|
"github.com/harness/gitness/registry/app/store/database/util"
|
|
"github.com/harness/gitness/registry/gc"
|
|
"github.com/harness/gitness/registry/types"
|
|
gitnessstore "github.com/harness/gitness/store"
|
|
db "github.com/harness/gitness/store/database"
|
|
"github.com/harness/gitness/store/database/dbtx"
|
|
|
|
"github.com/distribution/distribution/v3"
|
|
"github.com/distribution/distribution/v3/registry/api/errcode"
|
|
"github.com/opencontainers/go-digest"
|
|
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
type manifestService struct {
|
|
registryDao store.RegistryRepository
|
|
manifestDao store.ManifestRepository
|
|
layerDao store.LayerRepository
|
|
blobRepo store.BlobRepository
|
|
mtRepository store.MediaTypesRepository
|
|
tagDao store.TagRepository
|
|
imageDao store.ImageRepository
|
|
artifactDao store.ArtifactRepository
|
|
manifestRefDao store.ManifestReferenceRepository
|
|
ociImageIndexMappingDao store.OCIImageIndexMappingRepository
|
|
spacePathStore gas.SpacePathStore
|
|
gcService gc.Service
|
|
tx dbtx.Transactor
|
|
reporter event.Reporter
|
|
}
|
|
|
|
func NewManifestService(
|
|
registryDao store.RegistryRepository, manifestDao store.ManifestRepository,
|
|
blobRepo store.BlobRepository, mtRepository store.MediaTypesRepository, tagDao store.TagRepository,
|
|
imageDao store.ImageRepository, artifactDao store.ArtifactRepository,
|
|
layerDao store.LayerRepository, manifestRefDao store.ManifestReferenceRepository,
|
|
tx dbtx.Transactor, gcService gc.Service, reporter event.Reporter, spacePathStore gas.SpacePathStore,
|
|
ociImageIndexMappingDao store.OCIImageIndexMappingRepository,
|
|
) ManifestService {
|
|
return &manifestService{
|
|
registryDao: registryDao,
|
|
manifestDao: manifestDao,
|
|
layerDao: layerDao,
|
|
blobRepo: blobRepo,
|
|
mtRepository: mtRepository,
|
|
tagDao: tagDao,
|
|
artifactDao: artifactDao,
|
|
imageDao: imageDao,
|
|
manifestRefDao: manifestRefDao,
|
|
gcService: gcService,
|
|
tx: tx,
|
|
reporter: reporter,
|
|
spacePathStore: spacePathStore,
|
|
ociImageIndexMappingDao: ociImageIndexMappingDao,
|
|
}
|
|
}
|
|
|
|
type ManifestService interface {
|
|
// GetTags gets the tags of a repository
|
|
DBTag(
|
|
ctx context.Context,
|
|
mfst manifest.Manifest,
|
|
d digest.Digest,
|
|
tag string,
|
|
repoKey string,
|
|
headers *commons.ResponseHeaders,
|
|
info pkg.RegistryInfo,
|
|
) error
|
|
DBPut(
|
|
ctx context.Context,
|
|
mfst manifest.Manifest,
|
|
d digest.Digest,
|
|
repoKey string,
|
|
headers *commons.ResponseHeaders,
|
|
info pkg.RegistryInfo,
|
|
) error
|
|
DeleteTag(ctx context.Context, repoKey string, tag string, info pkg.RegistryInfo) (bool, error)
|
|
DeleteManifest(ctx context.Context, repoKey string, d digest.Digest, info pkg.RegistryInfo) error
|
|
AddManifestAssociation(ctx context.Context, repoKey string, digest digest.Digest, info pkg.RegistryInfo) error
|
|
DBFindRepositoryBlob(
|
|
ctx context.Context, desc manifest.Descriptor, repoID int64,
|
|
info pkg.RegistryInfo,
|
|
) (*types.Blob, error)
|
|
}
|
|
|
|
func (l *manifestService) DBTag(
|
|
ctx context.Context,
|
|
mfst manifest.Manifest,
|
|
d digest.Digest,
|
|
tag string,
|
|
repoKey string,
|
|
headers *commons.ResponseHeaders,
|
|
info pkg.RegistryInfo,
|
|
) error {
|
|
imageName := info.Image
|
|
|
|
if err := l.dbTagManifest(ctx, d, tag, imageName, info); err != nil {
|
|
log.Ctx(ctx).Error().Err(err).Msg("failed to create tag in database")
|
|
err2 := l.handleTagError(ctx, mfst, d, tag, repoKey, headers, info, err, imageName)
|
|
if err2 != nil {
|
|
return err2
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (l *manifestService) handleTagError(
|
|
ctx context.Context,
|
|
mfst manifest.Manifest,
|
|
d digest.Digest,
|
|
tag string,
|
|
repoKey string,
|
|
headers *commons.ResponseHeaders,
|
|
info pkg.RegistryInfo,
|
|
err error,
|
|
imageName string,
|
|
) error {
|
|
if errors.Is(err, util.ErrManifestNotFound) {
|
|
// If online GC was already reviewing the manifest that we want to tag, and that manifest had no
|
|
// tags before the review start, the API is unable to stop the GC from deleting the manifest (as
|
|
// the GC already acquired the lock on the corresponding queue row). This means that once the API
|
|
// is unblocked and tries to create the tag, a foreign key violation error will occur (because we're
|
|
// trying to create a tag for a manifest that no longer exists) and lead to this specific error.
|
|
// This should be extremely rare, if it ever occurs, but if it does, we should recreate the manifest
|
|
// and tag it, instead of returning a "manifest not found response" to clients. It's expected that
|
|
// this route handles the creation of a manifest if it doesn't exist already.
|
|
if err = l.DBPut(ctx, mfst, "", repoKey, headers, info); err != nil {
|
|
return fmt.Errorf("failed to recreate manifest in database: %w", err)
|
|
}
|
|
if err = l.dbTagManifest(ctx, d, tag, imageName, info); err != nil {
|
|
return fmt.Errorf("failed to create tag in database after manifest recreate: %w", err)
|
|
}
|
|
} else {
|
|
return fmt.Errorf("failed to create tag in database: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (l *manifestService) dbTagManifest(
|
|
ctx context.Context,
|
|
dgst digest.Digest,
|
|
tagName, imageName string,
|
|
info pkg.RegistryInfo,
|
|
) error {
|
|
dbRegistry, err := l.registryDao.GetByParentIDAndName(ctx, info.ParentID, info.RegIdentifier)
|
|
if err != nil {
|
|
return formatFailedToTagErr(err)
|
|
}
|
|
newDigest, err := types.NewDigest(dgst)
|
|
if err != nil {
|
|
return formatFailedToTagErr(err)
|
|
}
|
|
dbManifest, err := l.manifestDao.FindManifestByDigest(ctx, dbRegistry.ID, info.Image, newDigest)
|
|
if errors.Is(err, gitnessstore.ErrResourceNotFound) {
|
|
return fmt.Errorf("manifest %s not found in database", dgst)
|
|
}
|
|
if err != nil {
|
|
return formatFailedToTagErr(err)
|
|
}
|
|
|
|
err = l.tx.WithTx(ctx, func(ctx context.Context) error {
|
|
// Prevent long running transactions by setting an upper limit of manifestTagGCLockTimeout. 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 tag creation and let the client retry. This will bubble up and lead to a 503 Service Unavailable response.
|
|
// Set timeout for the transaction to prevent long-running operations
|
|
ctx, cancel := context.WithTimeout(ctx, manifestTagGCLockTimeout)
|
|
defer cancel()
|
|
|
|
// Attempt to find and lock the manifest for GC review
|
|
if err := l.lockManifestForGC(ctx, dbRegistry.ID, dbManifest.ID); err != nil {
|
|
return formatFailedToTagErr(err)
|
|
}
|
|
|
|
// Create or update artifact and tag records
|
|
if err := l.upsertArtifactAndTag(ctx, dbRegistry.ID, dbManifest.ID, imageName, tagName,
|
|
dgst); err != nil {
|
|
return formatFailedToTagErr(err)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return formatFailedToTagErr(err)
|
|
}
|
|
spacePath, packageType, err := l.getSpacePathAndPackageType(ctx, dbRegistry)
|
|
if err == nil {
|
|
l.reportEventAsync(ctx, info.RegIdentifier, imageName, tagName, packageType, spacePath)
|
|
} else {
|
|
log.Ctx(ctx).Err(err).Msg("Failed to find spacePath, not publishing event")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func formatFailedToTagErr(err error) error {
|
|
return fmt.Errorf("failed to tag manifest: %w", err)
|
|
}
|
|
|
|
// Locks the manifest for GC review.
|
|
func (l *manifestService) lockManifestForGC(ctx context.Context, repoID, manifestID int64) error {
|
|
_, err := l.gcService.ManifestFindAndLockBefore(
|
|
ctx, repoID, manifestID,
|
|
time.Now().Add(manifestTagGCReviewWindow),
|
|
)
|
|
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
|
// Use ProcessSQLErrorf for handling the SQL error abstraction
|
|
return db.ProcessSQLErrorf(
|
|
ctx,
|
|
err,
|
|
"failed to lock manifest for GC review [repoID: %d, manifestID: %d]", repoID, manifestID,
|
|
)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Creates or updates artifact and tag records.
|
|
func (l *manifestService) upsertArtifactAndTag(
|
|
ctx context.Context,
|
|
registryID,
|
|
manifestID int64,
|
|
imageName,
|
|
tagName string,
|
|
dgst digest.Digest,
|
|
) error {
|
|
image := &types.Image{
|
|
Name: imageName,
|
|
RegistryID: registryID,
|
|
Enabled: true,
|
|
}
|
|
|
|
if err := l.imageDao.CreateOrUpdate(ctx, image); err != nil {
|
|
return err
|
|
}
|
|
|
|
digest, err := types.NewDigest(dgst)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
artifact := &types.Artifact{
|
|
ImageID: image.ID,
|
|
Version: digest.String(),
|
|
}
|
|
|
|
if err := l.artifactDao.CreateOrUpdate(ctx, artifact); err != nil {
|
|
return err
|
|
}
|
|
|
|
tag := &types.Tag{
|
|
Name: tagName,
|
|
ImageName: imageName,
|
|
RegistryID: registryID,
|
|
ManifestID: manifestID,
|
|
}
|
|
|
|
return l.tagDao.CreateOrUpdate(ctx, tag)
|
|
}
|
|
|
|
// Retrieves the spacePath and packageType.
|
|
func (l *manifestService) getSpacePathAndPackageType(
|
|
ctx context.Context,
|
|
dbRepo *types.Registry,
|
|
) (string, event.PackageType, error) {
|
|
spacePath, err := l.spacePathStore.FindPrimaryBySpaceID(ctx, dbRepo.ParentID)
|
|
if err != nil {
|
|
log.Ctx(ctx).Err(err).Msg("Failed to find spacePath")
|
|
return "", event.PackageType(0), err
|
|
}
|
|
|
|
packageType, err := event.GetPackageTypeFromString(string(dbRepo.PackageType))
|
|
if err != nil {
|
|
log.Ctx(ctx).Err(err).Msg("Failed to find packageType")
|
|
return "", event.PackageType(0), err
|
|
}
|
|
|
|
return spacePath.Value, packageType, nil
|
|
}
|
|
|
|
// Reports event asynchronously.
|
|
func (l *manifestService) reportEventAsync(
|
|
ctx context.Context,
|
|
regID,
|
|
imageName,
|
|
tagName string,
|
|
packageType event.PackageType,
|
|
spacePath string,
|
|
) {
|
|
go l.reporter.ReportEvent(ctx, &event.ArtifactDetails{
|
|
RegistryID: regID,
|
|
ImagePath: imageName + ":" + tagName,
|
|
PackageType: packageType,
|
|
}, spacePath)
|
|
}
|
|
|
|
func (l *manifestService) DBPut(
|
|
ctx context.Context,
|
|
mfst manifest.Manifest,
|
|
d digest.Digest,
|
|
repoKey string,
|
|
headers *commons.ResponseHeaders,
|
|
info pkg.RegistryInfo,
|
|
) error {
|
|
_, payload, err := mfst.Payload()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = l.dbPutManifest(ctx, mfst, payload, d, repoKey, headers, info)
|
|
var mtErr util.UnknownMediaTypeError
|
|
if errors.As(err, &mtErr) {
|
|
return errcode.ErrorCodeManifestInvalid.WithDetail(mtErr.Error())
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (l *manifestService) dbPutManifest(
|
|
ctx context.Context,
|
|
manifest manifest.Manifest,
|
|
payload []byte,
|
|
d digest.Digest,
|
|
repoKey string,
|
|
headers *commons.ResponseHeaders,
|
|
info pkg.RegistryInfo,
|
|
) error {
|
|
switch reqManifest := manifest.(type) {
|
|
case *schema2.DeserializedManifest:
|
|
return l.dbPutManifestSchema2(ctx, reqManifest, payload, d, repoKey, headers, info)
|
|
case *ocischema.DeserializedManifest:
|
|
return l.dbPutManifestOCI(ctx, reqManifest, payload, d, repoKey, headers, info)
|
|
case *manifestlist.DeserializedManifestList:
|
|
return l.dbPutManifestList(ctx, reqManifest, payload, d, repoKey, headers, info)
|
|
case *ocischema.DeserializedImageIndex:
|
|
return l.dbPutImageIndex(ctx, reqManifest, payload, d, repoKey, headers, info)
|
|
default:
|
|
return errcode.ErrorCodeManifestInvalid.WithDetail("manifest type unsupported")
|
|
}
|
|
}
|
|
|
|
func (l *manifestService) dbPutManifestSchema2(
|
|
ctx context.Context,
|
|
manifest *schema2.DeserializedManifest,
|
|
payload []byte,
|
|
d digest.Digest,
|
|
repoKey string,
|
|
headers *commons.ResponseHeaders,
|
|
info pkg.RegistryInfo,
|
|
) error {
|
|
return l.dbPutManifestV2(ctx, manifest, payload, false, d, repoKey, headers, info)
|
|
}
|
|
|
|
func (l *manifestService) dbPutManifestV2(
|
|
ctx context.Context,
|
|
mfst manifest.ManifestV2,
|
|
payload []byte,
|
|
nonConformant bool,
|
|
digest digest.Digest,
|
|
repoKey string,
|
|
headers *commons.ResponseHeaders,
|
|
info pkg.RegistryInfo,
|
|
) error {
|
|
// find target repository
|
|
dbRepo, err := l.registryDao.GetByParentIDAndName(ctx, info.ParentID, repoKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if dbRepo == nil {
|
|
return errors.New("repository not found in database")
|
|
}
|
|
|
|
// Find the config now to ensure that the config's blob is associated with the repository.
|
|
dbCfgBlob, err := l.DBFindRepositoryBlob(ctx, mfst.Config(), dbRepo.ID, info)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
dgst, err := types.NewDigest(digest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
dbManifest, err := l.manifestDao.FindManifestByDigest(ctx, dbRepo.ID, info.Image, dgst)
|
|
if err != nil && !errors.Is(err, gitnessstore.ErrResourceNotFound) {
|
|
return err
|
|
}
|
|
|
|
if dbManifest != nil {
|
|
return nil
|
|
}
|
|
|
|
log.Debug().Msgf("manifest %s not found in database", dgst.String())
|
|
|
|
cfg := &types.Configuration{
|
|
MediaType: mfst.Config().MediaType,
|
|
Digest: dbCfgBlob.Digest,
|
|
BlobID: dbCfgBlob.ID,
|
|
}
|
|
|
|
//TODO: check if we need to store the config payload in the database
|
|
|
|
// skip retrieval and caching of config payload if its size is over the limit
|
|
/*if dbCfgBlob.Size <= datastore.ConfigSizeLimit {
|
|
// Since filesystem writes may be optional, We cannot be sure that the
|
|
// repository scoped filesystem blob service will have a link to the
|
|
// configuration blob; however, since we check for repository scoped access
|
|
// via the database above, we may retrieve the blob directly common storage.
|
|
cfgPayload, err := imh.blobProvider.Get(imh, dbCfgBlob.Digest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cfg.Payload = cfgPayload
|
|
}*/
|
|
|
|
m := &types.Manifest{
|
|
RegistryID: dbRepo.ID,
|
|
TotalSize: mfst.TotalSize(),
|
|
SchemaVersion: mfst.Version().SchemaVersion,
|
|
MediaType: mfst.Version().MediaType,
|
|
Digest: digest,
|
|
Payload: payload,
|
|
Configuration: cfg,
|
|
NonConformant: nonConformant,
|
|
ImageName: info.Image,
|
|
}
|
|
|
|
var artifactMediaType sql.NullString
|
|
ocim, ok := mfst.(manifest.ManifestOCI)
|
|
if ok {
|
|
subjectHandlingError := l.handleSubject(
|
|
ctx, ocim.Subject(), ocim.ArtifactType(),
|
|
ocim.Annotations(), dbRepo, m, headers, info,
|
|
)
|
|
if subjectHandlingError != nil {
|
|
return subjectHandlingError
|
|
}
|
|
if ocim.ArtifactType() != "" {
|
|
artifactMediaType.Valid = true
|
|
artifactMediaType.String = ocim.ArtifactType()
|
|
m.ArtifactType = artifactMediaType
|
|
}
|
|
} else if mfst.Config().MediaType != "" {
|
|
artifactMediaType.Valid = true
|
|
artifactMediaType.String = mfst.Config().MediaType
|
|
m.ArtifactType = artifactMediaType
|
|
}
|
|
|
|
// check if the manifest references non-distributable layers and mark it as such on the DB
|
|
ll := mfst.DistributableLayers()
|
|
m.NonDistributableLayers = len(ll) < len(mfst.Layers())
|
|
|
|
// Use CreateOrFind to prevent race conditions while pushing the same manifest with digest for different tags
|
|
if err := l.manifestDao.CreateOrFind(ctx, m); err != nil {
|
|
return err
|
|
}
|
|
|
|
dbManifest = m
|
|
|
|
// find and associate distributable manifest layer blobs
|
|
for _, reqLayer := range mfst.DistributableLayers() {
|
|
dbBlob, err := l.DBFindRepositoryBlob(ctx, reqLayer, dbRepo.ID, info)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Overwrite the media type from common blob storage with the one
|
|
// specified in the manifest json for the layer entity. The layer entity
|
|
// has a 1-1 relationship with with the manifest, so we want to reflect
|
|
// the manifest's description of the layer. Multiple manifest can reference
|
|
// the same blob, so the common blob storage should remain generic.
|
|
if ok2 := l.layerMediaTypeExists(ctx, reqLayer.MediaType); ok2 {
|
|
dbBlob.MediaType = reqLayer.MediaType
|
|
}
|
|
|
|
if err2 := l.layerDao.AssociateLayerBlob(ctx, dbManifest, dbBlob); err2 != nil {
|
|
return err2
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (l *manifestService) DBFindRepositoryBlob(
|
|
ctx context.Context, desc manifest.Descriptor,
|
|
repoID int64, info pkg.RegistryInfo,
|
|
) (*types.Blob, error) {
|
|
image := info.Image
|
|
b, err := l.blobRepo.FindByDigestAndRepoID(ctx, desc.Digest, repoID, image)
|
|
if err != nil {
|
|
if errors.Is(err, gitnessstore.ErrResourceNotFound) {
|
|
return nil, fmt.Errorf("blob not found in database")
|
|
}
|
|
return nil, err
|
|
}
|
|
return b, nil
|
|
}
|
|
|
|
// AddManifestAssociation This updates the manifestRefs for all new childDigests to their already existing parent
|
|
// manifests. This is used when a manifest from a manifest list is pulled from the remote and manifest list already
|
|
// exists in the database.
|
|
func (l *manifestService) AddManifestAssociation(
|
|
ctx context.Context, repoKey string, childDigest digest.Digest, info pkg.RegistryInfo,
|
|
) error {
|
|
newDigest, err2 := types.NewDigest(childDigest)
|
|
if err2 != nil {
|
|
return fmt.Errorf("failed to create digest: %s %w", childDigest, err2)
|
|
}
|
|
r, err := l.registryDao.GetByParentIDAndName(ctx, info.ParentID, repoKey)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get registry: %s %w", repoKey, err)
|
|
}
|
|
childManifest, err2 := l.manifestDao.FindManifestByDigest(ctx, r.ID, info.Image, newDigest)
|
|
if err2 != nil {
|
|
return fmt.Errorf("failed to find manifest by digest. Repo: %d Image: %s %w", r.ID, info.Image, err2)
|
|
}
|
|
mappings, err := l.ociImageIndexMappingDao.GetAllByChildDigest(ctx, r.ID, childManifest.ImageName, newDigest)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get oci image index mappings. Repo: %d Image: %s %w",
|
|
r.ID,
|
|
childManifest.ImageName,
|
|
err)
|
|
}
|
|
for _, mapping := range mappings {
|
|
parentManifest, err := l.manifestDao.Get(ctx, mapping.ParentManifestID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get manifest with ID: %d %w", mapping.ParentManifestID, err)
|
|
}
|
|
if err := l.manifestRefDao.AssociateManifest(ctx, parentManifest, childManifest); err != nil {
|
|
if errors.Is(err, util.ErrRefManifestNotFound) {
|
|
// This can only happen if the online GC deleted one
|
|
// of the referenced manifests (because they were
|
|
// untagged/unreferenced) between the call to
|
|
// `FindAndLockNBefore` and `AssociateManifest`. For now
|
|
// we need to return this error to mimic the behaviour
|
|
// of the corresponding filesystem validation.
|
|
log.Error().
|
|
Msgf("Failed to associate manifest Ref Manifest not found. parentDigest:%s childDigest:%s %v",
|
|
parentManifest.Digest.String(),
|
|
childManifest.Digest.String(),
|
|
err)
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (l *manifestService) handleSubject(
|
|
ctx context.Context, subject manifest.Descriptor,
|
|
artifactType string, annotations map[string]string, dbRepo *types.Registry,
|
|
m *types.Manifest, headers *commons.ResponseHeaders, info pkg.RegistryInfo,
|
|
) error {
|
|
if subject.Digest.String() != "" {
|
|
// Fetch subject_id from digest
|
|
subjectDigest, err := types.NewDigest(subject.Digest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dbSubject, err := l.manifestDao.FindManifestByDigest(ctx, dbRepo.ID, info.Image, subjectDigest)
|
|
if err != nil && !errors.Is(err, gitnessstore.ErrResourceNotFound) {
|
|
return err
|
|
}
|
|
|
|
if errors.Is(err, gitnessstore.ErrResourceNotFound) {
|
|
// in case something happened to the referenced manifest after validation
|
|
// return distribution.ManifestBlobUnknownError{Digest: subject.Digest}
|
|
log.Ctx(ctx).Warn().Msgf("subject manifest not found in database")
|
|
} else {
|
|
m.SubjectID.Int64 = dbSubject.ID
|
|
m.SubjectID.Valid = true
|
|
}
|
|
m.SubjectDigest = subject.Digest
|
|
headers.Headers["OCI-Subject"] = subject.Digest.String()
|
|
}
|
|
|
|
if artifactType != "" {
|
|
m.ArtifactType.String = artifactType
|
|
m.ArtifactType.Valid = true
|
|
}
|
|
m.Annotations = annotations
|
|
return nil
|
|
}
|
|
|
|
func (l *manifestService) dbPutManifestOCI(
|
|
ctx context.Context,
|
|
manifest *ocischema.DeserializedManifest,
|
|
payload []byte,
|
|
d digest.Digest,
|
|
repoKey string,
|
|
headers *commons.ResponseHeaders,
|
|
info pkg.RegistryInfo,
|
|
) error {
|
|
return l.dbPutManifestV2(ctx, manifest, payload, false, d, repoKey, headers, info)
|
|
}
|
|
|
|
func (l *manifestService) dbPutManifestList(
|
|
ctx context.Context,
|
|
manifestList *manifestlist.DeserializedManifestList,
|
|
payload []byte,
|
|
digest digest.Digest,
|
|
repoKey string,
|
|
headers *commons.ResponseHeaders,
|
|
info pkg.RegistryInfo,
|
|
) error {
|
|
if LikelyBuildxCache(manifestList) {
|
|
return l.dbPutBuildkitIndex(ctx, manifestList, payload, digest, repoKey, headers, info)
|
|
}
|
|
|
|
r, err := l.registryDao.GetByParentIDAndName(ctx, info.ParentID, repoKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if r == nil {
|
|
return errors.New("repository not found in database")
|
|
}
|
|
|
|
dgst, err := types.NewDigest(digest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ml, err := l.manifestDao.FindManifestByDigest(ctx, r.ID, info.Image, dgst)
|
|
if err != nil && !errors.Is(err, gitnessstore.ErrResourceNotFound) {
|
|
return err
|
|
}
|
|
|
|
// Media type can be either Docker (`application/vnd.docker.distribution.manifest.list.v2+json`)
|
|
// or OCI (empty).
|
|
// We need to make it explicit if empty, otherwise we're not able to distinguish between media types.
|
|
mediaType := manifestList.MediaType
|
|
if mediaType == "" {
|
|
mediaType = v1.MediaTypeImageIndex
|
|
}
|
|
|
|
ml = &types.Manifest{
|
|
RegistryID: r.ID,
|
|
SchemaVersion: manifestList.SchemaVersion,
|
|
MediaType: mediaType,
|
|
Digest: digest,
|
|
Payload: payload,
|
|
ImageName: info.Image,
|
|
}
|
|
|
|
mm, ids, err2 := l.validateManifestList(ctx, manifestList, r, info)
|
|
if err2 != nil {
|
|
return err2
|
|
}
|
|
|
|
err = l.tx.WithTx(
|
|
ctx, func(ctx context.Context) error {
|
|
// Prevent long running transactions by setting an upper limit of
|
|
// manifestListCreateGCLockTimeout. 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 request and let the client retry.
|
|
// This will bubble up and lead to a 503 Service
|
|
// Unavailable response.
|
|
ctx, cancel := context.WithTimeout(ctx, manifestListCreateGCLockTimeout)
|
|
defer cancel()
|
|
|
|
if _, err := l.gcService.ManifestFindAndLockNBefore(
|
|
ctx, r.ID, ids,
|
|
time.Now().Add(manifestListCreateGCReviewWindow),
|
|
); err != nil {
|
|
return err
|
|
}
|
|
|
|
// use CreateOrFind to prevent race conditions when the same digest is used by different tags
|
|
// and pushed at the same time
|
|
if err := l.manifestDao.CreateOrFind(ctx, ml); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Associate manifests to the manifest list.
|
|
for _, m := range mm {
|
|
if err := l.manifestRefDao.AssociateManifest(ctx, ml, m); err != nil {
|
|
if errors.Is(err, util.ErrRefManifestNotFound) {
|
|
// This can only happen if the online GC deleted one
|
|
// of the referenced manifests (because they were
|
|
// untagged/unreferenced) between the call to
|
|
// `FindAndLockNBefore` and `AssociateManifest`. For now
|
|
// we need to return this error to mimic the behaviour
|
|
// of the corresponding filesystem validation.
|
|
return distribution.ErrManifestVerification{
|
|
distribution.ErrManifestBlobUnknown{Digest: m.Digest},
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
}
|
|
|
|
err = l.mapManifestList(ctx, ml.ID, manifestList, r)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to map manifest list: %w", err)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
)
|
|
|
|
if err != nil {
|
|
log.Ctx(ctx).Error().Err(err).Msgf("failed to create manifest list in database: %v", err)
|
|
return fmt.Errorf("failed to create manifest list in database: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (l *manifestService) validateManifestIndex(
|
|
ctx context.Context, manifestList *ocischema.DeserializedImageIndex, r *types.Registry, info pkg.RegistryInfo,
|
|
) ([]*types.Manifest, []int64, error) {
|
|
mm := make([]*types.Manifest, 0, len(manifestList.Manifests))
|
|
ids := make([]int64, 0, len(manifestList.Manifests))
|
|
for _, desc := range manifestList.Manifests {
|
|
m, err := l.dbFindManifestListManifest(ctx, r, info.Image, desc.Digest)
|
|
if errors.Is(err, gitnessstore.ErrResourceNotFound) && r.Type == artifact.RegistryTypeUPSTREAM {
|
|
continue
|
|
}
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
mm = append(mm, m)
|
|
ids = append(ids, m.ID)
|
|
}
|
|
log.Ctx(ctx).Debug().Msgf("validated %d / %d manifests in index", len(mm), len(manifestList.Manifests))
|
|
return mm, ids, nil
|
|
}
|
|
|
|
func (l *manifestService) mapManifestIndex(
|
|
ctx context.Context, mi int64, manifestList *ocischema.DeserializedImageIndex, r *types.Registry,
|
|
) error {
|
|
if r.Type != artifact.RegistryTypeUPSTREAM {
|
|
return nil
|
|
}
|
|
for _, desc := range manifestList.Manifests {
|
|
err := l.ociImageIndexMappingDao.Create(ctx, &types.OCIImageIndexMapping{
|
|
ParentManifestID: mi,
|
|
ChildManifestDigest: desc.Digest,
|
|
})
|
|
if err != nil {
|
|
log.Ctx(ctx).Error().Err(err).
|
|
Msgf("failed to create oci image index manifest for digest %s", desc.Digest)
|
|
return fmt.Errorf("failed to create oci image index manifest: %w", err)
|
|
}
|
|
}
|
|
log.Ctx(ctx).Debug().Msgf("successfully mapped manifest index %d with its manifests", mi)
|
|
return nil
|
|
}
|
|
|
|
func (l *manifestService) validateManifestList(
|
|
ctx context.Context, manifestList *manifestlist.DeserializedManifestList, r *types.Registry, info pkg.RegistryInfo,
|
|
) ([]*types.Manifest, []int64, error) {
|
|
mm := make([]*types.Manifest, 0, len(manifestList.Manifests))
|
|
ids := make([]int64, 0, len(manifestList.Manifests))
|
|
for _, desc := range manifestList.Manifests {
|
|
m, err := l.dbFindManifestListManifest(ctx, r, info.Image, desc.Digest)
|
|
if errors.Is(err, gitnessstore.ErrResourceNotFound) && r.Type == artifact.RegistryTypeUPSTREAM {
|
|
continue
|
|
}
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
mm = append(mm, m)
|
|
ids = append(ids, m.ID)
|
|
}
|
|
log.Ctx(ctx).Debug().Msgf("validated %d / %d manifests in list", len(mm), len(manifestList.Manifests))
|
|
return mm, ids, nil
|
|
}
|
|
|
|
func (l *manifestService) mapManifestList(
|
|
ctx context.Context, mi int64, manifestList *manifestlist.DeserializedManifestList, r *types.Registry,
|
|
) error {
|
|
if r.Type != artifact.RegistryTypeUPSTREAM {
|
|
return nil
|
|
}
|
|
for _, desc := range manifestList.Manifests {
|
|
err := l.ociImageIndexMappingDao.Create(ctx, &types.OCIImageIndexMapping{
|
|
ParentManifestID: mi,
|
|
ChildManifestDigest: desc.Digest,
|
|
})
|
|
if err != nil {
|
|
log.Ctx(ctx).Error().Err(err).Msgf("failed to create oci image index manifest for digest %s", desc.Digest)
|
|
return fmt.Errorf("failed to create oci image index manifest: %w", err)
|
|
}
|
|
}
|
|
log.Ctx(ctx).Debug().Msgf("successfully mapped manifest list %d with its manifests", mi)
|
|
return nil
|
|
}
|
|
|
|
func (l *manifestService) dbPutImageIndex(
|
|
ctx context.Context,
|
|
imageIndex *ocischema.DeserializedImageIndex,
|
|
payload []byte,
|
|
digest digest.Digest,
|
|
repoKey string,
|
|
headers *commons.ResponseHeaders,
|
|
info pkg.RegistryInfo,
|
|
) error {
|
|
r, err := l.registryDao.GetByParentIDAndName(ctx, info.ParentID, repoKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if r == nil {
|
|
return errors.New("repository not found in database")
|
|
}
|
|
|
|
dgst, err := types.NewDigest(digest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
mi, err := l.manifestDao.FindManifestByDigest(ctx, r.ID, info.Image, dgst)
|
|
if err != nil && !errors.Is(err, gitnessstore.ErrResourceNotFound) {
|
|
return err
|
|
}
|
|
|
|
// Media type can be either Docker (`application/vnd.docker.distribution.manifest.list.v2+json`)
|
|
// or OCI (empty).
|
|
// We need to make it explicit if empty, otherwise we're not able to distinguish
|
|
// between media types.
|
|
mediaType := imageIndex.MediaType
|
|
if mediaType == "" {
|
|
mediaType = v1.MediaTypeImageIndex
|
|
}
|
|
|
|
mi = &types.Manifest{
|
|
RegistryID: r.ID,
|
|
SchemaVersion: imageIndex.SchemaVersion,
|
|
MediaType: mediaType,
|
|
Digest: digest,
|
|
Payload: payload,
|
|
ImageName: info.Image,
|
|
}
|
|
|
|
subjectHandlingError := l.handleSubject(
|
|
ctx, imageIndex.Subject(), imageIndex.ArtifactType(),
|
|
imageIndex.Annotations(), r, mi, headers, info,
|
|
)
|
|
if subjectHandlingError != nil {
|
|
return subjectHandlingError
|
|
}
|
|
|
|
mm, ids, err := l.validateManifestIndex(ctx, imageIndex, r, info)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to map manifest index: %w", err)
|
|
}
|
|
|
|
err = l.tx.WithTx(
|
|
ctx, func(ctx context.Context) error {
|
|
// Prevent long running transactions by setting an upper limit of
|
|
// manifestListCreateGCLockTimeout. 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 request and let the client retry.
|
|
// This will bubble up and lead to a 503 Service
|
|
// Unavailable response.
|
|
ctx, cancel := context.WithTimeout(ctx, manifestListCreateGCLockTimeout)
|
|
defer cancel()
|
|
|
|
if _, err := l.gcService.ManifestFindAndLockNBefore(
|
|
ctx, r.ID, ids,
|
|
time.Now().Add(manifestListCreateGCReviewWindow),
|
|
); err != nil {
|
|
return err
|
|
}
|
|
// use CreateOrFind to prevent race conditions when the same digest is used by different tags
|
|
// and pushed at the same time
|
|
if err := l.manifestDao.CreateOrFind(ctx, mi); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Associate manifests to the manifest list.
|
|
for _, m := range mm {
|
|
if err := l.manifestRefDao.AssociateManifest(ctx, mi, m); err != nil {
|
|
if errors.Is(err, util.ErrRefManifestNotFound) {
|
|
// This can only happen if the online GC deleted one of the
|
|
// referenced manifests (because they were
|
|
// untagged/unreferenced) between the call to
|
|
// `FindAndLockNBefore` and `AssociateManifest`. For now
|
|
// we need to return this error to mimic the behaviour
|
|
// of the corresponding filesystem validation.
|
|
return distribution.ErrManifestVerification{
|
|
distribution.ErrManifestBlobUnknown{Digest: m.Digest},
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
}
|
|
|
|
err = l.mapManifestIndex(ctx, mi.ID, imageIndex, r)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to map manifest index: %w", err)
|
|
}
|
|
return nil
|
|
},
|
|
)
|
|
|
|
if err != nil {
|
|
log.Ctx(ctx).Error().Err(err).Msgf("failed to create image index in database")
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (l *manifestService) dbPutBuildkitIndex(
|
|
ctx context.Context,
|
|
ml *manifestlist.DeserializedManifestList,
|
|
payload []byte,
|
|
digest digest.Digest,
|
|
repoKey string,
|
|
headers *commons.ResponseHeaders,
|
|
info pkg.RegistryInfo,
|
|
) error {
|
|
// convert to OCI manifest and process as if it was one
|
|
m, err := OCIManifestFromBuildkitIndex(ml)
|
|
if err != nil {
|
|
return fmt.Errorf("converting buildkit index to manifest: %w", err)
|
|
}
|
|
|
|
// Note that `payload` is not the deserialized manifest (`m`) payload but
|
|
// rather the index payload, untouched.
|
|
// Within dbPutManifestOCIOrSchema2 we use this value for the
|
|
// `manifests.payload` column and source the value for
|
|
// the `manifests.digest` column from `imh.Digest`, and not from `m`.
|
|
// Therefore, we keep behavioral consistency for
|
|
// the outside world by preserving the index payload and digest while
|
|
// storing things internally as an OCI manifest.
|
|
return l.dbPutManifestV2(ctx, m, payload, true, digest, repoKey, headers, info)
|
|
}
|
|
|
|
func (l *manifestService) dbFindManifestListManifest(
|
|
ctx context.Context,
|
|
repository *types.Registry, imageName string, digest digest.Digest,
|
|
) (*types.Manifest, error) {
|
|
dgst, err := types.NewDigest(digest)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
dbManifest, err := l.manifestDao.FindManifestByDigest(
|
|
ctx, repository.ID,
|
|
imageName, dgst,
|
|
)
|
|
if err != nil {
|
|
if errors.Is(err, gitnessstore.ErrResourceNotFound) {
|
|
return nil, fmt.Errorf(
|
|
"manifest %s not found for %s/%s: %w", digest.String(),
|
|
repository.Name, imageName, err,
|
|
)
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
return dbManifest, nil
|
|
}
|
|
|
|
func (l *manifestService) layerMediaTypeExists(ctx context.Context, mt string) bool {
|
|
exists, err := l.mtRepository.MediaTypeExists(ctx, mt)
|
|
if err != nil {
|
|
log.Ctx(ctx).Error().Stack().Err(err).Msgf("error checking for existence of media type: %v", err)
|
|
return false
|
|
}
|
|
|
|
if exists {
|
|
return true
|
|
}
|
|
|
|
log.Ctx(ctx).Warn().Msgf("unknown layer media type")
|
|
|
|
return false
|
|
}
|
|
|
|
func (l *manifestService) DeleteTag(
|
|
ctx context.Context,
|
|
repoKey string,
|
|
tag string,
|
|
info pkg.RegistryInfo,
|
|
) (bool, error) {
|
|
// Fetch the registry by parent ID and name
|
|
registry, err := l.registryDao.GetByParentIDAndName(ctx, info.ParentID, repoKey)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
found, err := l.tagDao.DeleteTagByName(ctx, registry.ID, tag)
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to delete tag in database: %w", err)
|
|
}
|
|
if !found {
|
|
return false, distribution.ErrTagUnknown{Tag: tag}
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func (l *manifestService) DeleteTagsByManifestID(
|
|
ctx context.Context,
|
|
repoKey string,
|
|
manifestID int64,
|
|
info pkg.RegistryInfo,
|
|
) (bool, error) {
|
|
registry, err := l.registryDao.GetByParentIDAndName(ctx, info.ParentID, repoKey)
|
|
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
return l.tagDao.DeleteTagByManifestID(ctx, registry.ID, manifestID)
|
|
}
|
|
|
|
func (l *manifestService) DeleteManifest(
|
|
ctx context.Context,
|
|
repoKey string,
|
|
d digest.Digest,
|
|
info pkg.RegistryInfo,
|
|
) error {
|
|
log.Ctx(ctx).Debug().Msg("deleting manifest from repository in database")
|
|
|
|
registry, err := l.registryDao.GetByParentIDAndName(ctx, info.ParentID, repoKey)
|
|
imageName := info.Image
|
|
|
|
if registry == nil || err != nil {
|
|
return fmt.Errorf("repository not found in database: %w", err)
|
|
}
|
|
|
|
// We need to find the manifest first and then lookup for any manifest
|
|
// it references (if it's a manifest list). This
|
|
// is needed to ensure we lock any related online GC tasks to prevent
|
|
// race conditions around the delete.
|
|
newDigest, err := types.NewDigest(d)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
m, err := l.manifestDao.FindManifestByDigest(ctx, registry.ID, imageName, newDigest)
|
|
if err != nil {
|
|
if errors.Is(err, gitnessstore.ErrResourceNotFound) {
|
|
return util.ErrManifestNotFound
|
|
}
|
|
return err
|
|
}
|
|
|
|
return l.tx.WithTx(
|
|
ctx, func(ctx context.Context) error {
|
|
switch m.MediaType {
|
|
case manifestlist.MediaTypeManifestList, v1.MediaTypeImageIndex:
|
|
mm, err := l.manifestDao.References(ctx, m)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// This should never happen, as it's not possible to delete a
|
|
// child manifest if it's referenced by a list, which
|
|
// means that we'll always have at least one child manifest here.
|
|
// Nevertheless, log error if this ever happens.
|
|
if len(mm) == 0 {
|
|
log.Ctx(ctx).Error().Stack().Err(err).Msgf("stored manifest list has no references")
|
|
break
|
|
}
|
|
ids := make([]int64, 0, len(mm))
|
|
for _, m := range mm {
|
|
ids = append(ids, m.ID)
|
|
}
|
|
|
|
// Prevent long running transactions by setting an upper limit of
|
|
// manifestDeleteGCLockTimeout. 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 manifest delete and let the client retry.
|
|
// This will bubble up and lead to a 503
|
|
// Service Unavailable response.
|
|
ctx, cancel := context.WithTimeout(ctx, manifestDeleteGCLockTimeout)
|
|
defer cancel()
|
|
|
|
if _, err := l.gcService.ManifestFindAndLockNBefore(
|
|
ctx, registry.ID,
|
|
ids, time.Now().Add(manifestDeleteGCReviewWindow),
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
found, err := l.manifestDao.DeleteManifest(ctx, registry.ID, imageName, d)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !found {
|
|
return util.ErrManifestNotFound
|
|
}
|
|
|
|
return nil
|
|
},
|
|
)
|
|
}
|