func dbPutManifestList()

in registry/handlers/manifests.go [1143:1264]


func dbPutManifestList(imh *manifestHandler, manifestList *manifestlist.DeserializedManifestList, payload []byte) error {
	if mlcompat.LikelyBuildxCache(manifestList) {
		return dbPutBuildkitIndex(imh, manifestList, payload)
	}

	repoPath := imh.Repository.Named().Name()
	l := log.GetLogger(log.WithContext(imh)).WithFields(log.Fields{
		"repository":      repoPath,
		"manifest_digest": imh.Digest,
	})
	l.Debug("putting manifest list")

	var opts []datastore.RepositoryStoreOption
	if imh.GetRepoCache() != nil {
		opts = append(opts, datastore.WithRepositoryCache(imh.GetRepoCache()))
	}
	rStore := datastore.NewRepositoryStore(imh.App.db.Primary(), opts...)
	v := validation.NewManifestListValidator(
		&datastore.RepositoryManifestService{
			RepositoryReader: rStore,
			RepositoryPath:   repoPath,
		},
		&datastore.RepositoryBlobService{RepositoryReader: rStore, RepositoryPath: repoPath},
		imh.App.manifestRefLimit,
		imh.App.manifestPayloadSizeLimit,
	)

	if err := v.Validate(imh, manifestList); err != nil {
		return err
	}

	// create or find target repository
	r, err := rStore.CreateOrFindByPath(imh.Context, repoPath)
	if err != nil {
		return err
	}

	ml, err := rStore.FindManifestByDigest(imh.Context, r, imh.Digest)
	if err != nil {
		return err
	}
	if ml != nil {
		return nil
	}

	// 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 = &models.Manifest{
		NamespaceID:   r.NamespaceID,
		RepositoryID:  r.ID,
		SchemaVersion: manifestList.SchemaVersion,
		MediaType:     mediaType,
		Digest:        imh.Digest,
		Payload:       payload,
	}

	// We need to find and lock referenced manifests to ensure we lock any related online GC tasks to prevent race
	// conditions around the manifest list insert. See:
	// https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs/spec/gitlab/online-garbage-collection.md#creating-a-manifest-list-referencing-an-unreferenced-manifest
	mm := make([]*models.Manifest, 0, len(manifestList.Manifests))
	ids := make([]int64, 0, len(mm))
	for _, desc := range manifestList.Manifests {
		m, err := dbFindManifestListManifest(imh.Context, rStore, r, desc.Digest, r.Path)
		if err != nil {
			return err
		}
		mm = append(mm, m)
		ids = append(ids, m.ID)
	}

	tx, err := imh.db.Primary().BeginTx(imh.Context, nil)
	if err != nil {
		return fmt.Errorf("creating database transaction: %w", err)
	}
	defer tx.Rollback()

	// 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(imh.Context, manifestListCreateGCLockTimeout)
	defer cancel()

	mts := datastore.NewGCManifestTaskStore(tx)
	if _, err := mts.FindAndLockNBefore(ctx, r.NamespaceID, 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 https://gitlab.com/gitlab-org/container-registry/-/issues/1132
	mStore := datastore.NewManifestStore(tx)
	if err := mStore.CreateOrFind(imh, ml); err != nil {
		return err
	}

	// Associate manifests to the manifest list.
	for _, m := range mm {
		if err := mStore.AssociateManifest(imh.Context, ml, m); err != nil {
			if errors.Is(err, datastore.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 behavior of the corresponding filesystem validation.
				return distribution.ErrManifestVerification{
					distribution.ErrManifestBlobUnknown{Digest: m.Digest},
				}
			}
			return err
		}
	}

	if err := tx.Commit(); err != nil {
		return fmt.Errorf("failed to commit database transaction: %w", err)
	}

	return nil
}