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