mirror of
https://github.com/project-zot/zot.git
synced 2026-06-17 21:17:58 +08:00
a5cc8ab810
* feat: support pushing multiple tags for a single manifest See https://github.com/opencontainers/distribution-spec/pull/600 Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com> * fix: constants not replaced in swagger output Also godot mandates comments ending in dots, which produces bad results in the swagger generated files, see the extra ". which is now fixed below: ``` diff --git a/swagger/docs.go b/swagger/docs.go index 84b08277..fb2c45c3 100644 --- a/swagger/docs.go +++ b/swagger/docs.go @@ -114,7 +114,7 @@ const docTemplate = `{ } }, "400": { - "description": "bad request\".", + "description": "bad request", "schema": { "type": "string" } @@ -200,7 +200,7 @@ const docTemplate = `{ } }, "400": { - "description": "bad request\".", + "description": "bad request", "schema": { "type": "string" } diff --git a/swagger/swagger.json b/swagger/swagger.json index cfeb3900..247f95fa 100644 --- a/swagger/swagger.json +++ b/swagger/swagger.json @@ -106,7 +106,7 @@ } }, "400": { - "description": "bad request\".", + "description": "bad request", "schema": { "type": "string" } @@ -192,7 +192,7 @@ } }, "400": { - "description": "bad request\".", + "description": "bad request", "schema": { "type": "string" } diff --git a/swagger/swagger.yaml b/swagger/swagger.yaml index 57641c2f..09b30dcc 100644 --- a/swagger/swagger.yaml +++ b/swagger/swagger.yaml @@ -310,7 +310,7 @@ paths: schema: type: string "400": - description: bad request". + description: bad request schema: type: string "500": @@ -366,7 +366,7 @@ paths: schema: type: string "400": - description: bad request". + description: bad request schema: type: string "500": ``` Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com> --------- Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com>
301 lines
9.8 KiB
Go
301 lines
9.8 KiB
Go
package meta
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
|
|
godigest "github.com/opencontainers/go-digest"
|
|
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
|
|
|
zerr "zotregistry.dev/zot/v2/errors"
|
|
zcommon "zotregistry.dev/zot/v2/pkg/common"
|
|
"zotregistry.dev/zot/v2/pkg/compat"
|
|
"zotregistry.dev/zot/v2/pkg/log"
|
|
mTypes "zotregistry.dev/zot/v2/pkg/meta/types"
|
|
"zotregistry.dev/zot/v2/pkg/storage"
|
|
)
|
|
|
|
// priorTagManifest records where MetaDB believed each tag pointed before a digest PUT with tag=
|
|
// parameters could move it. Rollback loads manifest bytes from the blob store (GetBlobContent).
|
|
type priorTagManifest struct {
|
|
digest godigest.Digest
|
|
mediaType string
|
|
}
|
|
|
|
// priorTagManifestsFromMetaDB returns digest and media type from RepoMeta for tags that already
|
|
// exist in metadb. Omitted tags are new or unknown to metadb. ErrRepoMetaNotFound yields an empty
|
|
// map. Rollback reads manifest blobs from storage via GetBlobContent(prior.digest).
|
|
// If a tag exists only in the image store and not in metadb, rollback cannot restore a moved tag
|
|
// (metadb and storage should stay in sync during normal operation).
|
|
func priorTagManifestsFromMetaDB(ctx context.Context, metaDB mTypes.MetaDB, repo string, tags []string,
|
|
) (map[string]priorTagManifest, error) {
|
|
empty := map[string]priorTagManifest{}
|
|
|
|
if len(tags) == 0 {
|
|
return empty, nil
|
|
}
|
|
|
|
repoMeta, err := metaDB.GetRepoMeta(ctx, repo)
|
|
if err != nil {
|
|
if errors.Is(err, zerr.ErrRepoMetaNotFound) {
|
|
return empty, nil
|
|
}
|
|
|
|
return nil, err
|
|
}
|
|
|
|
if len(repoMeta.Tags) == 0 {
|
|
return empty, nil
|
|
}
|
|
|
|
out := make(map[string]priorTagManifest, len(tags))
|
|
|
|
for _, tag := range tags {
|
|
desc, ok := repoMeta.Tags[tag]
|
|
if !ok || desc.Digest == "" {
|
|
continue
|
|
}
|
|
|
|
dgst, parseErr := godigest.Parse(desc.Digest)
|
|
if parseErr != nil {
|
|
continue
|
|
}
|
|
|
|
descMediaType := desc.MediaType
|
|
if descMediaType == "" {
|
|
descMediaType = v1.MediaTypeImageManifest
|
|
}
|
|
|
|
out[tag] = priorTagManifest{
|
|
digest: dgst,
|
|
mediaType: descMediaType,
|
|
}
|
|
}
|
|
|
|
return out, nil
|
|
}
|
|
|
|
// rollbackDigestManifestTags deletes every tag in tags from the image store (this PUT added them to the
|
|
// index). It runs OnDeleteManifest only for tags in appliedMetaTags: those had a successful meta update for
|
|
// digest and must be reverted. Calling OnDeleteManifest for other tags is unsafe—RemoveRepoReference can
|
|
// drop a tag entry even when metadb still maps that tag to a different digest (e.g. meta not updated yet).
|
|
// When priorTagManifests has an entry for a tag, it re-applies that manifest so moved tags point at their
|
|
// original digest again.
|
|
func rollbackDigestManifestTags(ctx context.Context, repo string, tags, appliedMetaTags []string, mediaType string,
|
|
digest godigest.Digest,
|
|
body []byte, storeController storage.StoreController, metaDB mTypes.MetaDB, log log.Logger,
|
|
priorTagManifests map[string]priorTagManifest,
|
|
) {
|
|
imgStore := storeController.GetImageStore(repo)
|
|
|
|
for i := len(tags) - 1; i >= 0; i-- {
|
|
refTag := tags[i]
|
|
if delErr := imgStore.DeleteImageManifest(repo, refTag, false); delErr != nil &&
|
|
!errors.Is(delErr, zerr.ErrManifestNotFound) {
|
|
log.Error().Err(delErr).Str("repository", repo).Str("tag", refTag).
|
|
Msg("multi-tag digest push: rollback DeleteImageManifest failed")
|
|
}
|
|
}
|
|
|
|
for i := len(appliedMetaTags) - 1; i >= 0; i-- {
|
|
refTag := appliedMetaTags[i]
|
|
|
|
metaDelErr := OnDeleteManifest(repo, refTag, mediaType, digest, body, storeController, metaDB, log)
|
|
if metaDelErr != nil {
|
|
log.Error().Err(metaDelErr).Str("repository", repo).Str("tag", refTag).
|
|
Msg("multi-tag digest push: rollback OnDeleteManifest failed")
|
|
}
|
|
}
|
|
|
|
if len(priorTagManifests) == 0 {
|
|
return
|
|
}
|
|
|
|
for _, refTag := range tags {
|
|
prior, ok := priorTagManifests[refTag]
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
restoreBody, blobErr := imgStore.GetBlobContent(repo, prior.digest)
|
|
if blobErr != nil {
|
|
log.Error().Err(blobErr).Str("repository", repo).Str("tag", refTag).
|
|
Msg("multi-tag digest push: rollback load prior manifest blob failed")
|
|
|
|
continue
|
|
}
|
|
|
|
if _, _, putErr := imgStore.PutImageManifest(repo, prior.digest.String(), prior.mediaType, restoreBody,
|
|
[]string{refTag}); putErr != nil {
|
|
log.Error().Err(putErr).Str("repository", repo).Str("tag", refTag).
|
|
Msg("multi-tag digest push: rollback restore prior manifest in store failed")
|
|
|
|
continue
|
|
}
|
|
|
|
if metaErr := OnUpdateManifest(ctx, repo, refTag, prior.mediaType, prior.digest, restoreBody,
|
|
storeController, metaDB, log); metaErr != nil {
|
|
log.Error().Err(metaErr).Str("repository", repo).Str("tag", refTag).
|
|
Msg("multi-tag digest push: rollback restore prior metadb failed")
|
|
}
|
|
}
|
|
}
|
|
|
|
// OnUpdateManifest is called when a new manifest is added. It updates metadb according to the type
|
|
// of image pushed(normal images, signatues, etc.). In care of any errors, it makes sure to keep
|
|
// consistency between metadb and the image store.
|
|
func OnUpdateManifest(ctx context.Context, repo, reference, mediaType string, digest godigest.Digest, body []byte,
|
|
storeController storage.StoreController, metaDB mTypes.MetaDB, log log.Logger,
|
|
) error {
|
|
if zcommon.IsReferrersTag(reference) {
|
|
return nil
|
|
}
|
|
|
|
imgStore := storeController.GetImageStore(repo)
|
|
|
|
err := SetImageMetaFromInput(ctx, repo, reference, mediaType, digest, body,
|
|
imgStore, metaDB, log)
|
|
if err != nil {
|
|
log.Info().Str("tag", reference).Str("repository", repo).Msg("uploading image meta was unsuccessful for tag in repo")
|
|
|
|
if err := imgStore.DeleteImageManifest(repo, reference, false); err != nil {
|
|
log.Error().Err(err).Str("reference", reference).Str("repository", repo).
|
|
Msg("failed to remove image manifest in repo")
|
|
|
|
return err
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// OnUpdateManifestDigestTags updates metadb for each tag from a digest-addressed manifest push that used
|
|
// repeated `tag=` query parameters. It snapshots each tag's prior digest and media type from MetaDB
|
|
// (GetRepoMeta) before updates, then calls OnUpdateManifest per tag; on the first failure it removes
|
|
// every tag in tags from the image store, reverts MetaDB only for tags that had already completed
|
|
// OnUpdateManifest successfully, and restores moved tags using the snapshot (see rollbackDigestManifestTags).
|
|
func OnUpdateManifestDigestTags(ctx context.Context, repo string, tags []string, mediaType string,
|
|
digest godigest.Digest, body []byte,
|
|
storeController storage.StoreController, metaDB mTypes.MetaDB, log log.Logger,
|
|
) error {
|
|
if len(tags) == 0 {
|
|
return nil
|
|
}
|
|
|
|
priorTagManifests, err := priorTagManifestsFromMetaDB(ctx, metaDB, repo, tags)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
applied := make([]string, 0, len(tags))
|
|
|
|
for _, tag := range tags {
|
|
if err := OnUpdateManifest(ctx, repo, tag, mediaType, digest, body, storeController, metaDB, log); err != nil {
|
|
log.Error().Err(err).Str("repository", repo).Str("tag", tag).
|
|
Msg("multi-tag digest push: meta update failed; rolling back tag query additions")
|
|
|
|
rollbackDigestManifestTags(ctx, repo, tags, applied, mediaType, digest, body, storeController, metaDB, log,
|
|
priorTagManifests)
|
|
|
|
return err
|
|
}
|
|
|
|
applied = append(applied, tag)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// OnDeleteManifest is called when a manifest is deleted. It updates metadb according to the type
|
|
// of image pushed(normal images, signatues, etc.). In care of any errors, it makes sure to keep
|
|
// consistency between metadb and the image store.
|
|
func OnDeleteManifest(repo, reference, mediaType string, digest godigest.Digest, manifestBlob []byte,
|
|
storeController storage.StoreController, metaDB mTypes.MetaDB, log log.Logger,
|
|
) error {
|
|
if zcommon.IsReferrersTag(reference) {
|
|
return nil
|
|
}
|
|
|
|
imgStore := storeController.GetImageStore(repo)
|
|
|
|
isSignature, signatureType, signedManifestDigest, err := storage.CheckIsImageSignature(repo, manifestBlob,
|
|
reference)
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("failed to check if image is a signature or not")
|
|
|
|
return err
|
|
}
|
|
|
|
manageRepoMetaSuccessfully := true
|
|
|
|
if isSignature {
|
|
err = metaDB.DeleteSignature(repo, signedManifestDigest, mTypes.SignatureMetadata{
|
|
SignatureDigest: digest.String(),
|
|
SignatureType: signatureType,
|
|
})
|
|
if err != nil {
|
|
log.Error().Err(err).Str("component", "metadb").
|
|
Msg("failed to check if image is a signature or not")
|
|
|
|
manageRepoMetaSuccessfully = false
|
|
}
|
|
} else {
|
|
err = metaDB.RemoveRepoReference(repo, reference, digest)
|
|
if err != nil {
|
|
log.Info().Str("component", "metadb").Msg("restoring image store")
|
|
|
|
// restore image store
|
|
_, _, err := imgStore.PutImageManifest(repo, reference, mediaType, manifestBlob, nil)
|
|
if err != nil {
|
|
log.Error().Err(err).Str("component", "metadb").
|
|
Msg("failed to restore manifest to image store, database is not consistent")
|
|
}
|
|
|
|
manageRepoMetaSuccessfully = false
|
|
}
|
|
}
|
|
|
|
if !manageRepoMetaSuccessfully {
|
|
log.Info().Str("tag", reference).Str("repository", repo).Str("component", "metadb").
|
|
Msg("failed to delete image meta was unsuccessful for tag in repo")
|
|
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// OnGetManifest is called when a manifest is downloaded. It increments the download couter on that manifest.
|
|
func OnGetManifest(name, reference, mediaType string, body []byte,
|
|
storeController storage.StoreController, metaDB mTypes.MetaDB, log log.Logger,
|
|
) error {
|
|
// check if image is a signature
|
|
isSignature, _, _, err := storage.CheckIsImageSignature(name, body, reference)
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("failed to check if manifest is a signature or not")
|
|
|
|
return err
|
|
}
|
|
|
|
if isSignature || zcommon.IsReferrersTag(reference) {
|
|
return nil
|
|
}
|
|
|
|
if !(mediaType == v1.MediaTypeImageManifest || mediaType == v1.MediaTypeImageIndex ||
|
|
compat.IsCompatibleManifestMediaType(mediaType) || compat.IsCompatibleManifestListMediaType(mediaType)) {
|
|
return nil
|
|
}
|
|
|
|
err = metaDB.UpdateStatsOnDownload(name, reference)
|
|
if err != nil {
|
|
log.Error().Err(err).Str("repository", name).Str("reference", reference).
|
|
Msg("failed to update stats on download image")
|
|
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|