feat: support pushing multiple tags for a single manifest (#3885)

* 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>
This commit is contained in:
Andrei Aaron
2026-03-29 21:13:24 +03:00
committed by GitHub
parent 705939aed3
commit a5cc8ab810
34 changed files with 2225 additions and 365 deletions
+166 -1
View File
@@ -2,10 +2,12 @@ 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"
@@ -13,6 +15,132 @@ import (
"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.
@@ -43,6 +171,43 @@ func OnUpdateManifest(ctx context.Context, repo, reference, mediaType string, di
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.
@@ -82,7 +247,7 @@ func OnDeleteManifest(repo, reference, mediaType string, digest godigest.Digest,
log.Info().Str("component", "metadb").Msg("restoring image store")
// restore image store
_, _, err := imgStore.PutImageManifest(repo, reference, mediaType, manifestBlob)
_, _, 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")
+262
View File
@@ -0,0 +1,262 @@
package meta
import (
"context"
"encoding/json"
"errors"
"testing"
godigest "github.com/opencontainers/go-digest"
ispec "github.com/opencontainers/image-spec/specs-go/v1"
. "github.com/smartystreets/goconvey/convey"
zerr "zotregistry.dev/zot/v2/errors"
"zotregistry.dev/zot/v2/pkg/log"
mTypes "zotregistry.dev/zot/v2/pkg/meta/types"
"zotregistry.dev/zot/v2/pkg/storage"
testimage "zotregistry.dev/zot/v2/pkg/test/image-utils"
"zotregistry.dev/zot/v2/pkg/test/mocks"
)
var errHookInternal = errors.New("hook internal test error")
func TestPriorTagManifestsFromMetaDB(t *testing.T) {
Convey("priorTagManifestsFromMetaDB", t, func() {
ctx := context.Background()
Convey("empty tags", func() {
out, err := priorTagManifestsFromMetaDB(ctx, mocks.MetaDBMock{}, "repo", nil)
So(err, ShouldBeNil)
So(len(out), ShouldEqual, 0)
})
Convey("repo meta not found yields empty map", func() {
db := mocks.MetaDBMock{
GetRepoMetaFn: func(context.Context, string) (mTypes.RepoMeta, error) {
return mTypes.RepoMeta{}, zerr.ErrRepoMetaNotFound
},
}
out, err := priorTagManifestsFromMetaDB(ctx, db, "repo", []string{"t"})
So(err, ShouldBeNil)
So(len(out), ShouldEqual, 0)
})
Convey("get repo meta error propagates", func() {
db := mocks.MetaDBMock{
GetRepoMetaFn: func(context.Context, string) (mTypes.RepoMeta, error) {
return mTypes.RepoMeta{}, errHookInternal
},
}
_, err := priorTagManifestsFromMetaDB(ctx, db, "repo", []string{"t"})
So(errors.Is(err, errHookInternal), ShouldBeTrue)
})
Convey("empty tag map in repo meta", func() {
db := mocks.MetaDBMock{
GetRepoMetaFn: func(context.Context, string) (mTypes.RepoMeta, error) {
return mTypes.RepoMeta{Tags: map[mTypes.Tag]mTypes.Descriptor{}}, nil
},
}
out, err := priorTagManifestsFromMetaDB(ctx, db, "repo", []string{"t"})
So(err, ShouldBeNil)
So(len(out), ShouldEqual, 0)
})
Convey("skips unknown tag empty digest invalid digest", func() {
good := "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
db := mocks.MetaDBMock{
GetRepoMetaFn: func(context.Context, string) (mTypes.RepoMeta, error) {
return mTypes.RepoMeta{
Tags: map[mTypes.Tag]mTypes.Descriptor{
"only-good": {Digest: good, MediaType: ispec.MediaTypeImageManifest},
"empty-dig": {Digest: "", MediaType: ispec.MediaTypeImageManifest},
"bad-dig": {Digest: "not-a-digest", MediaType: ispec.MediaTypeImageManifest},
},
}, nil
},
}
tags := []string{"missing", "only-good", "empty-dig", "bad-dig"}
out, err := priorTagManifestsFromMetaDB(ctx, db, "repo", tags)
So(err, ShouldBeNil)
So(len(out), ShouldEqual, 1)
pm, ok := out["only-good"]
So(ok, ShouldBeTrue)
So(pm.digest.String(), ShouldEqual, good)
So(pm.mediaType, ShouldEqual, ispec.MediaTypeImageManifest)
})
Convey("default media type when descriptor empty", func() {
good := "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
db := mocks.MetaDBMock{
GetRepoMetaFn: func(context.Context, string) (mTypes.RepoMeta, error) {
return mTypes.RepoMeta{
Tags: map[mTypes.Tag]mTypes.Descriptor{
"t": {Digest: good, MediaType: ""},
},
}, nil
},
}
out, err := priorTagManifestsFromMetaDB(ctx, db, "repo", []string{"t"})
So(err, ShouldBeNil)
So(out["t"].mediaType, ShouldEqual, ispec.MediaTypeImageManifest)
})
})
}
func TestRollbackDigestManifestTags(t *testing.T) {
Convey("rollbackDigestManifestTags", t, func() {
ctx := context.Background()
testLog := log.NewTestLogger()
img := testimage.CreateDefaultImage()
mediaType := img.ManifestDescriptor.MediaType
if mediaType == "" {
mediaType = ispec.MediaTypeImageManifest
}
body := img.ManifestDescriptor.Data
dgst := img.Digest()
Convey("delete manifest error is logged path", func() {
var deleteCalls int
is := mocks.MockedImageStore{
DeleteImageManifestFn: func(repo, reference string, detectCollision bool) error {
deleteCalls++
return errors.New("delete failed")
},
}
sc := storage.StoreController{DefaultStore: &is}
rollbackDigestManifestTags(ctx, "repo", []string{"a"}, nil, mediaType, dgst, body, sc,
mocks.MetaDBMock{}, testLog, nil)
So(deleteCalls, ShouldEqual, 1)
})
Convey("delete manifest not found is ignored", func() {
is := mocks.MockedImageStore{
DeleteImageManifestFn: func(repo, reference string, detectCollision bool) error {
return zerr.ErrManifestNotFound
},
}
sc := storage.StoreController{DefaultStore: &is}
rollbackDigestManifestTags(ctx, "repo", []string{"a"}, nil, mediaType, dgst, body, sc,
mocks.MetaDBMock{}, testLog, nil)
})
Convey("on delete manifest failure is logged", func() {
is := mocks.MockedImageStore{}
metaDB := mocks.MetaDBMock{
RemoveRepoReferenceFn: func(string, string, godigest.Digest) error {
return errors.New("remove failed")
},
}
sc := storage.StoreController{DefaultStore: &is}
rollbackDigestManifestTags(ctx, "repo", []string{"t"}, []string{"t"}, mediaType, dgst, body, sc,
metaDB, testLog, nil)
})
Convey("prior restore get blob fails", func() {
other := testimage.CreateRandomImage()
priorD := (&other).Digest()
prior := map[string]priorTagManifest{
"t": {digest: priorD, mediaType: mediaType},
}
is := mocks.MockedImageStore{
DeleteImageManifestFn: func(string, string, bool) error { return nil },
GetBlobContentFn: func(string, godigest.Digest) ([]byte, error) {
return nil, errors.New("blob missing")
},
}
sc := storage.StoreController{DefaultStore: &is}
rollbackDigestManifestTags(ctx, "repo", []string{"t"}, []string{"t"}, mediaType, dgst, body, sc,
mocks.MetaDBMock{}, testLog, prior)
})
Convey("prior restore put manifest fails", func() {
priorBody := body
priorD := dgst
prior := map[string]priorTagManifest{
"t": {digest: priorD, mediaType: mediaType},
}
is := mocks.MockedImageStore{
DeleteImageManifestFn: func(string, string, bool) error { return nil },
GetBlobContentFn: func(_ string, blobDigest godigest.Digest) ([]byte, error) {
So(blobDigest, ShouldResemble, priorD)
return priorBody, nil
},
PutImageManifestFn: func(string, string, string, []byte, []string) (godigest.Digest, godigest.Digest, error) {
return "", "", errors.New("put failed")
},
}
sc := storage.StoreController{DefaultStore: &is}
rollbackDigestManifestTags(ctx, "repo", []string{"t"}, []string{"t"}, mediaType, dgst, body, sc,
mocks.MetaDBMock{}, testLog, prior)
})
Convey("prior restore metadb update fails", func() {
priorBody := body
priorD := dgst
prior := map[string]priorTagManifest{
"t": {digest: priorD, mediaType: mediaType},
}
var manifest ispec.Manifest
err := json.Unmarshal(priorBody, &manifest)
So(err, ShouldBeNil)
configBytes, err := json.Marshal(img.Config)
So(err, ShouldBeNil)
metaDB := mocks.MetaDBMock{
SetRepoReferenceFn: func(context.Context, string, string, mTypes.ImageMeta) error {
return errors.New("set ref failed")
},
}
is := mocks.MockedImageStore{
DeleteImageManifestFn: func(string, string, bool) error { return nil },
GetBlobContentFn: func(_ string, blobDigest godigest.Digest) ([]byte, error) {
switch {
case blobDigest == priorD:
return priorBody, nil
case blobDigest == manifest.Config.Digest:
return configBytes, nil
default:
So(blobDigest.String(), ShouldBeIn,
[]string{priorD.String(), manifest.Config.Digest.String()})
}
return nil, nil
},
PutImageManifestFn: func(_, _, _ string, blob []byte, _ []string) (godigest.Digest, godigest.Digest, error) {
d := godigest.FromBytes(blob)
return d, d, nil
},
}
sc := storage.StoreController{DefaultStore: &is}
rollbackDigestManifestTags(ctx, "repo", []string{"t"}, []string{"t"}, mediaType, dgst, body, sc,
metaDB, testLog, prior)
})
})
}
+393 -1
View File
@@ -5,20 +5,412 @@ import (
"errors"
"testing"
godigest "github.com/opencontainers/go-digest"
ispec "github.com/opencontainers/image-spec/specs-go/v1"
. "github.com/smartystreets/goconvey/convey"
zerr "zotregistry.dev/zot/v2/errors"
"zotregistry.dev/zot/v2/pkg/extensions/monitoring"
"zotregistry.dev/zot/v2/pkg/log"
"zotregistry.dev/zot/v2/pkg/meta"
"zotregistry.dev/zot/v2/pkg/meta/boltdb"
mTypes "zotregistry.dev/zot/v2/pkg/meta/types"
"zotregistry.dev/zot/v2/pkg/storage"
"zotregistry.dev/zot/v2/pkg/storage/local"
stypes "zotregistry.dev/zot/v2/pkg/storage/types"
. "zotregistry.dev/zot/v2/pkg/test/image-utils"
"zotregistry.dev/zot/v2/pkg/test/mocks"
)
var ErrTestError = errors.New("test error")
var (
errDeleteAfterMetaHookTest = errors.New("delete manifest after meta failure hook test")
errDigestTagsSetRepoReferenceFail = errors.New("injected SetRepoReference failure for digest-tags rollback tests")
errGetRepoMetaForDigestTags = errors.New("get repo meta failed for digest tags test")
errSetRepoRefForHookTest = errors.New("set repo reference failed for hook test")
)
// setRepoRefFailMetaDB delegates to an inner MetaDB but fails SetRepoReference for one tag (used to
// exercise multi-tag digest rollback after a partial OnUpdateManifest success).
type setRepoRefFailMetaDB struct {
mTypes.MetaDB
failRef string
}
func (w *setRepoRefFailMetaDB) SetRepoReference(
ctx context.Context, repo, ref string, imageMeta mTypes.ImageMeta,
) error {
if ref == w.failRef {
return errDigestTagsSetRepoReferenceFail
}
return w.MetaDB.SetRepoReference(ctx, repo, ref, imageMeta)
}
// failDeleteImageStore delegates to an inner ImageStore but forces DeleteImageManifest to return deleteErr.
type failDeleteImageStore struct {
stypes.ImageStore
deleteErr error
}
func (f *failDeleteImageStore) DeleteImageManifest(repo, reference string, detectCollision bool) error {
return f.deleteErr
}
func TestOnUpdateManifestDigestTags_emptyTags(t *testing.T) {
Convey("OnUpdateManifestDigestTags with no tags is a no-op (nil MetaDB: no GetRepoMeta/SetRepoReference path)",
t, func() {
log := log.NewTestLogger()
err := meta.OnUpdateManifestDigestTags(context.Background(), "repo", nil, ispec.MediaTypeImageManifest,
godigest.Digest(""), nil, storage.StoreController{}, nil, log)
So(err, ShouldBeNil)
})
}
func TestOnUpdateManifestDigestTags_success(t *testing.T) {
Convey("OnUpdateManifestDigestTags updates metadb for each digest query tag", t, func() {
rootDir := t.TempDir()
storeController := storage.StoreController{}
log := log.NewTestLogger()
metrics := monitoring.NewMetricsServer(false, log)
defer metrics.Stop()
storeController.DefaultStore = local.NewImageStore(rootDir, true, true, log, metrics, nil, nil, nil, nil)
params := boltdb.DBParameters{RootDir: rootDir}
boltDriver, err := boltdb.GetBoltDriver(params)
So(err, ShouldBeNil)
metaDB, err := boltdb.New(boltDriver, log)
So(err, ShouldBeNil)
image := CreateDefaultImage()
mediaType := image.ManifestDescriptor.MediaType
if mediaType == "" {
mediaType = ispec.MediaTypeImageManifest
}
manifestBody := image.ManifestDescriptor.Data
manifestDigest := image.Digest()
err = WriteImageToFileSystem(image, "repo", "seed", storeController)
So(err, ShouldBeNil)
err = meta.OnUpdateManifest(context.Background(), "repo", "seed", mediaType, manifestDigest, manifestBody,
storeController, metaDB, log)
So(err, ShouldBeNil)
imgStore := storeController.GetImageStore("repo")
_, _, err = imgStore.PutImageManifest("repo", manifestDigest.String(), mediaType, manifestBody,
[]string{"ta", "tb"})
So(err, ShouldBeNil)
err = meta.OnUpdateManifestDigestTags(context.Background(), "repo", []string{"ta", "tb"}, mediaType,
manifestDigest, manifestBody, storeController, metaDB, log)
So(err, ShouldBeNil)
wantDigest := manifestDigest.String()
repoMeta, err := metaDB.GetRepoMeta(context.Background(), "repo")
So(err, ShouldBeNil)
So(repoMeta.Tags, ShouldContainKey, "ta")
So(repoMeta.Tags, ShouldContainKey, "tb")
So(repoMeta.Tags, ShouldContainKey, "seed")
So(repoMeta.Tags["ta"].Digest, ShouldEqual, wantDigest)
So(repoMeta.Tags["tb"].Digest, ShouldEqual, wantDigest)
So(repoMeta.Tags["seed"].Digest, ShouldEqual, wantDigest)
})
}
func TestOnUpdateManifestDigestTags_rollbackPartialMeta(t *testing.T) {
Convey("OnUpdateManifestDigestTags rollback deletes all new index tags; meta rollback only for applied tags",
t, func() {
rootDir := t.TempDir()
storeController := storage.StoreController{}
log := log.NewTestLogger()
metrics := monitoring.NewMetricsServer(false, log)
defer metrics.Stop()
storeController.DefaultStore = local.NewImageStore(rootDir, true, true, log, metrics, nil, nil, nil, nil)
params := boltdb.DBParameters{RootDir: rootDir}
boltDriver, err := boltdb.GetBoltDriver(params)
So(err, ShouldBeNil)
metaDB, err := boltdb.New(boltDriver, log)
So(err, ShouldBeNil)
image := CreateDefaultImage()
mediaType := image.ManifestDescriptor.MediaType
if mediaType == "" {
mediaType = ispec.MediaTypeImageManifest
}
manifestBody := image.ManifestDescriptor.Data
manifestDigest := image.Digest()
err = WriteImageToFileSystem(image, "repo", "seed", storeController)
So(err, ShouldBeNil)
err = meta.OnUpdateManifest(context.Background(), "repo", "seed", mediaType, manifestDigest, manifestBody,
storeController, metaDB, log)
So(err, ShouldBeNil)
imgStore := storeController.GetImageStore("repo")
_, _, err = imgStore.PutImageManifest("repo", manifestDigest.String(), mediaType, manifestBody,
[]string{"ta", "tb"})
So(err, ShouldBeNil)
repoMetaBefore, err := metaDB.GetRepoMeta(context.Background(), "repo")
So(err, ShouldBeNil)
seedDigestBefore := repoMetaBefore.Tags["seed"].Digest
So(seedDigestBefore, ShouldEqual, manifestDigest.String())
wrapped := &setRepoRefFailMetaDB{MetaDB: metaDB, failRef: "tb"}
err = meta.OnUpdateManifestDigestTags(context.Background(), "repo", []string{"ta", "tb"}, mediaType,
manifestDigest, manifestBody, storeController, wrapped, log)
So(err, ShouldEqual, errDigestTagsSetRepoReferenceFail)
_, _, _, err = imgStore.GetImageManifest("repo", "ta")
So(errors.Is(err, zerr.ErrManifestNotFound), ShouldBeTrue)
_, _, _, err = imgStore.GetImageManifest("repo", "tb")
So(errors.Is(err, zerr.ErrManifestNotFound), ShouldBeTrue)
seedBody, _, _, err := imgStore.GetImageManifest("repo", "seed")
So(err, ShouldBeNil)
So(godigest.FromBytes(seedBody).String(), ShouldEqual, manifestDigest.String())
repoMeta, err := metaDB.GetRepoMeta(context.Background(), "repo")
So(err, ShouldBeNil)
So(repoMeta.Tags, ShouldNotContainKey, "ta")
So(repoMeta.Tags, ShouldNotContainKey, "tb")
So(repoMeta.Tags, ShouldContainKey, "seed")
So(repoMeta.Tags["seed"].Digest, ShouldEqual, seedDigestBefore)
})
}
func TestOnUpdateManifestDigestTags_rollbackRestoresMovedTag(t *testing.T) {
Convey("rollback restores a tag moved from digest A to digest B back to digest A when MetaDB fails later",
t, func() {
rootDir := t.TempDir()
storeController := storage.StoreController{}
log := log.NewTestLogger()
metrics := monitoring.NewMetricsServer(false, log)
defer metrics.Stop()
storeController.DefaultStore = local.NewImageStore(rootDir, true, true, log, metrics, nil, nil, nil, nil)
params := boltdb.DBParameters{RootDir: rootDir}
boltDriver, err := boltdb.GetBoltDriver(params)
So(err, ShouldBeNil)
metaDB, err := boltdb.New(boltDriver, log)
So(err, ShouldBeNil)
imageA := CreateDefaultImage()
imageB := CreateRandomImage()
So(imageA.Digest(), ShouldNotEqual, imageB.Digest())
mediaTypeA := imageA.ManifestDescriptor.MediaType
if mediaTypeA == "" {
mediaTypeA = ispec.MediaTypeImageManifest
}
mediaTypeB := imageB.ManifestDescriptor.MediaType
if mediaTypeB == "" {
mediaTypeB = ispec.MediaTypeImageManifest
}
bodyA := imageA.ManifestDescriptor.Data
digestA := imageA.Digest()
bodyB := imageB.ManifestDescriptor.Data
digestB := imageB.Digest()
err = WriteImageToFileSystem(imageA, "repo", "movable", storeController)
So(err, ShouldBeNil)
err = meta.OnUpdateManifest(context.Background(), "repo", "movable", mediaTypeA, digestA, bodyA,
storeController, metaDB, log)
So(err, ShouldBeNil)
err = WriteImageToFileSystem(imageB, "repo", "yardB", storeController)
So(err, ShouldBeNil)
err = meta.OnUpdateManifest(context.Background(), "repo", "yardB", mediaTypeB, digestB, bodyB,
storeController, metaDB, log)
So(err, ShouldBeNil)
imgStore := storeController.GetImageStore("repo")
_, _, err = imgStore.PutImageManifest("repo", digestB.String(), mediaTypeB, bodyB,
[]string{"movable", "onlyB"})
So(err, ShouldBeNil)
wrapped := &setRepoRefFailMetaDB{MetaDB: metaDB, failRef: "onlyB"}
err = meta.OnUpdateManifestDigestTags(context.Background(), "repo", []string{"movable", "onlyB"}, mediaTypeB,
digestB, bodyB, storeController, wrapped, log)
So(err, ShouldEqual, errDigestTagsSetRepoReferenceFail)
movableBody, movableD, _, err := imgStore.GetImageManifest("repo", "movable")
So(err, ShouldBeNil)
So(movableD.String(), ShouldEqual, digestA.String())
So(godigest.FromBytes(movableBody).String(), ShouldEqual, digestA.String())
_, _, _, err = imgStore.GetImageManifest("repo", "onlyB")
So(errors.Is(err, zerr.ErrManifestNotFound), ShouldBeTrue)
repoMeta, err := metaDB.GetRepoMeta(context.Background(), "repo")
So(err, ShouldBeNil)
So(repoMeta.Tags["movable"].Digest, ShouldEqual, digestA.String())
So(repoMeta.Tags["yardB"].Digest, ShouldEqual, digestB.String())
So(repoMeta.Tags, ShouldNotContainKey, "onlyB")
})
}
func TestOnUpdateManifestDigestTags_getRepoMetaError(t *testing.T) {
Convey("OnUpdateManifestDigestTags returns when GetRepoMeta fails with a non-ErrRepoMetaNotFound error", t, func() {
log := log.NewTestLogger()
metaDB := mocks.MetaDBMock{
GetRepoMetaFn: func(context.Context, string) (mTypes.RepoMeta, error) {
return mTypes.RepoMeta{}, errGetRepoMetaForDigestTags
},
}
d := godigest.FromString("sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
err := meta.OnUpdateManifestDigestTags(context.Background(), "repo", []string{"a"}, ispec.MediaTypeImageManifest,
d, []byte("{}"), storage.StoreController{}, metaDB, log)
So(errors.Is(err, errGetRepoMetaForDigestTags), ShouldBeTrue)
})
}
func TestOnUpdateManifestDigestTags_whenRepoMetaMissing(t *testing.T) {
Convey("ErrRepoMetaNotFound during snapshot still allows digest query tag meta updates", t, func() {
rootDir := t.TempDir()
storeController := storage.StoreController{}
log := log.NewTestLogger()
metrics := monitoring.NewMetricsServer(false, log)
defer metrics.Stop()
storeController.DefaultStore = local.NewImageStore(rootDir, true, true, log, metrics, nil, nil, nil, nil)
params := boltdb.DBParameters{RootDir: rootDir}
boltDriver, err := boltdb.GetBoltDriver(params)
So(err, ShouldBeNil)
metaDB, err := boltdb.New(boltDriver, log)
So(err, ShouldBeNil)
image := CreateDefaultImage()
mediaType := image.ManifestDescriptor.MediaType
if mediaType == "" {
mediaType = ispec.MediaTypeImageManifest
}
manifestBody := image.ManifestDescriptor.Data
manifestDigest := image.Digest()
err = WriteImageToFileSystem(image, "repo", "seed", storeController)
So(err, ShouldBeNil)
_, err = metaDB.GetRepoMeta(context.Background(), "repo")
So(errors.Is(err, zerr.ErrRepoMetaNotFound), ShouldBeTrue)
imgStore := storeController.GetImageStore("repo")
_, _, err = imgStore.PutImageManifest("repo", manifestDigest.String(), mediaType, manifestBody,
[]string{"ta", "tb"})
So(err, ShouldBeNil)
err = meta.OnUpdateManifestDigestTags(context.Background(), "repo", []string{"ta", "tb"}, mediaType,
manifestDigest, manifestBody, storeController, metaDB, log)
So(err, ShouldBeNil)
wantDigest := manifestDigest.String()
repoMeta, err := metaDB.GetRepoMeta(context.Background(), "repo")
So(err, ShouldBeNil)
So(repoMeta.Tags, ShouldContainKey, "ta")
So(repoMeta.Tags, ShouldContainKey, "tb")
So(repoMeta.Tags, ShouldNotContainKey, "seed")
So(repoMeta.Tags["ta"].Digest, ShouldEqual, wantDigest)
So(repoMeta.Tags["tb"].Digest, ShouldEqual, wantDigest)
})
}
func TestOnUpdateManifest_setRepoReferenceFailsRemovesManifest(t *testing.T) {
Convey("OnUpdateManifest deletes the manifest from the store when SetRepoReference fails", t, func() {
rootDir := t.TempDir()
storeController := storage.StoreController{}
log := log.NewTestLogger()
metrics := monitoring.NewMetricsServer(false, log)
defer metrics.Stop()
storeController.DefaultStore = local.NewImageStore(rootDir, true, true, log, metrics, nil, nil, nil, nil)
metaDB := mocks.MetaDBMock{
SetRepoReferenceFn: func(context.Context, string, string, mTypes.ImageMeta) error {
return errSetRepoRefForHookTest
},
}
image := CreateDefaultImage()
mediaType := image.ManifestDescriptor.MediaType
if mediaType == "" {
mediaType = ispec.MediaTypeImageManifest
}
err := WriteImageToFileSystem(image, "repo", "tag1", storeController)
So(err, ShouldBeNil)
imgStore := storeController.GetImageStore("repo")
err = meta.OnUpdateManifest(context.Background(), "repo", "tag1", mediaType, image.Digest(),
image.ManifestDescriptor.Data, storeController, metaDB, log)
So(errors.Is(err, errSetRepoRefForHookTest), ShouldBeTrue)
_, _, _, err = imgStore.GetImageManifest("repo", "tag1")
So(errors.Is(err, zerr.ErrManifestNotFound), ShouldBeTrue)
})
}
func TestOnUpdateManifest_whenDeleteAfterMetaFailureFails(t *testing.T) {
Convey("OnUpdateManifest returns the delete error when meta fails and store cleanup fails", t, func() {
rootDir := t.TempDir()
storeController := storage.StoreController{}
log := log.NewTestLogger()
metrics := monitoring.NewMetricsServer(false, log)
defer metrics.Stop()
baseStore := local.NewImageStore(rootDir, true, true, log, metrics, nil, nil, nil, nil)
storeController.DefaultStore = &failDeleteImageStore{
ImageStore: baseStore,
deleteErr: errDeleteAfterMetaHookTest,
}
metaDB := mocks.MetaDBMock{
SetRepoReferenceFn: func(context.Context, string, string, mTypes.ImageMeta) error {
return errSetRepoRefForHookTest
},
}
image := CreateDefaultImage()
mediaType := image.ManifestDescriptor.MediaType
if mediaType == "" {
mediaType = ispec.MediaTypeImageManifest
}
err := WriteImageToFileSystem(image, "repo", "tag1", storeController)
So(err, ShouldBeNil)
err = meta.OnUpdateManifest(context.Background(), "repo", "tag1", mediaType, image.Digest(),
image.ManifestDescriptor.Data, storeController, metaDB, log)
So(errors.Is(err, errDeleteAfterMetaHookTest), ShouldBeTrue)
})
}
func TestOnUpdateManifest(t *testing.T) {
Convey("On UpdateManifest", t, func() {
+17 -14
View File
@@ -40,13 +40,16 @@ import (
const repo = "repo"
// errMetaTestInjected is returned from mocks in this file to assert error paths in ParseStorage and related tests.
var errMetaTestInjected = errors.New("injected error for parse_test mocks")
func TestParseStorageErrors(t *testing.T) {
ctx := context.Background()
Convey("ParseStorage", t, func() {
imageStore := mocks.MockedImageStore{
GetIndexContentFn: func(repo string) ([]byte, error) {
return nil, ErrTestError
return nil, errMetaTestInjected
},
GetRepositoriesFn: func() ([]string, error) {
return []string{"repo1", "repo2"}, nil
@@ -67,7 +70,7 @@ func TestParseStorageErrors(t *testing.T) {
}
imageStore2 := mocks.MockedImageStore{
GetRepositoriesFn: func() ([]string, error) {
return nil, ErrTestError
return nil, errMetaTestInjected
},
}
storeController := storage.StoreController{
@@ -82,7 +85,7 @@ func TestParseStorageErrors(t *testing.T) {
})
Convey("metaDB.GetAllRepoNames errors", func() {
metaDB.GetAllRepoNamesFn = func() ([]string, error) { return nil, ErrTestError }
metaDB.GetAllRepoNamesFn = func() ([]string, error) { return nil, errMetaTestInjected }
err := meta.ParseStorage(metaDB, storeController, log.NewTestLogger())
So(err, ShouldNotBeNil)
@@ -95,7 +98,7 @@ func TestParseStorageErrors(t *testing.T) {
storeController := storage.StoreController{DefaultStore: imageStore1}
metaDB.GetAllRepoNamesFn = func() ([]string, error) { return []string{"deleted"}, nil }
metaDB.DeleteRepoMetaFn = func(repo string) error { return ErrTestError }
metaDB.DeleteRepoMetaFn = func(repo string) error { return errMetaTestInjected }
err := meta.ParseStorage(metaDB, storeController, log.NewTestLogger())
So(err, ShouldNotBeNil)
@@ -106,7 +109,7 @@ func TestParseStorageErrors(t *testing.T) {
GetRepositoriesFn: func() ([]string, error) { return []string{"repo1", "repo2"}, nil },
}
imageStore1.StatIndexFn = func(repo string) (bool, int64, time.Time, error) {
return false, 0, time.Time{}, ErrTestError
return false, 0, time.Time{}, errMetaTestInjected
}
storeController := storage.StoreController{DefaultStore: imageStore1}
@@ -124,7 +127,7 @@ func TestParseStorageErrors(t *testing.T) {
Convey("imageStore.GetIndexContent errors", func() {
imageStore.GetIndexContentFn = func(repo string) ([]byte, error) {
return nil, ErrTestError
return nil, errMetaTestInjected
}
err := meta.ParseRepo("repo", metaDB, storeController, log)
@@ -144,7 +147,7 @@ func TestParseStorageErrors(t *testing.T) {
imageStore.GetIndexContentFn = func(repo string) ([]byte, error) {
return []byte("{}"), nil
}
metaDB.ResetRepoReferencesFn = func(repo string, tagsToKeep map[string]bool) error { return ErrTestError }
metaDB.ResetRepoReferencesFn = func(repo string, tagsToKeep map[string]bool) error { return errMetaTestInjected }
err := meta.ParseRepo("repo", metaDB, storeController, log)
So(err, ShouldNotBeNil)
})
@@ -184,11 +187,11 @@ func TestParseStorageErrors(t *testing.T) {
}
imageStore.GetBlobContentFn = func(repo string, digest godigest.Digest) ([]byte, error) {
// Return a non-missing error (not ErrBlobNotFound or PathNotFoundError)
return nil, ErrTestError
return nil, errMetaTestInjected
}
err := meta.ParseRepo("repo", metaDB, storeController, log)
So(err, ShouldNotBeNil)
So(err, ShouldEqual, ErrTestError)
So(err, ShouldEqual, errMetaTestInjected)
})
Convey("imageStore.GetImageManifest missing blob - graceful handling", func() {
@@ -274,7 +277,7 @@ func TestParseStorageErrors(t *testing.T) {
Convey("metaDB.SetRepoReference", func() {
metaDB.SetRepoReferenceFn = func(ctx context.Context, repo, reference string, imageMeta mTypes.ImageMeta) error {
return ErrTestError
return errMetaTestInjected
}
err = meta.ParseRepo("repo", metaDB, storeController, log)
@@ -293,7 +296,7 @@ func TestParseStorageErrors(t *testing.T) {
Convey("Image Manifest errors", func() {
Convey("Get Config blob error", func() {
mockImageStore.GetBlobContentFn = func(repo string, digest godigest.Digest) ([]byte, error) {
return []byte{}, ErrTestError
return []byte{}, errMetaTestInjected
}
err := meta.SetImageMetaFromInput(ctx, "repo", "tag", ispec.MediaTypeImageManifest, image.Digest(),
@@ -326,7 +329,7 @@ func TestParseStorageErrors(t *testing.T) {
mockedMetaDB.UpdateSignaturesValidityFn = func(ctx context.Context, repo string,
manifestDigest godigest.Digest,
) error {
return ErrTestError
return errMetaTestInjected
}
err := meta.SetImageMetaFromInput(ctx, "repo", "tag", mediaType, goodNotationSignature.Digest(),
goodNotationSignature.ManifestDescriptor.Data, mockImageStore, mockedMetaDB, log)
@@ -907,7 +910,7 @@ func TestGetSignatureLayersInfo(t *testing.T) {
Convey("GetBlobContent errors", t, func() {
mockImageStore := mocks.MockedImageStore{}
mockImageStore.GetBlobContentFn = func(repo string, digest godigest.Digest) ([]byte, error) {
return nil, ErrTestError
return nil, errMetaTestInjected
}
image := CreateRandomImage()
@@ -930,7 +933,7 @@ func TestGetSignatureLayersInfo(t *testing.T) {
Convey("notation GetBlobContent errors", t, func() {
mockImageStore := mocks.MockedImageStore{}
mockImageStore.GetBlobContentFn = func(repo string, digest godigest.Digest) ([]byte, error) {
return nil, ErrTestError
return nil, errMetaTestInjected
}
image := CreateImageWith().RandomLayers(1, 10).RandomConfig().Build()