drone/registry/app/pkg/docker/manifest_service.go
Arvind Choudhary fb65327419
feat: [AH-231]: Upstream features (#3560)
Added support for:
1. Default library/ prefix for docker hub if required.
2. Implemented manifest list support for upstream proxies. New table
introduced: `oci_image_index_mappings`
3. Fixed Secret issue on UI for both gitness and harness code
4. Code refactoring to bring controller inside wire and other minor
fixes around linting
5. Fixed few bugs around upstream proxy.
2024-09-25 01:16:26 -07:00

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
},
)
}