mirror of
https://github.com/project-zot/zot.git
synced 2026-06-18 05:28:07 +08:00
6c1f1bdd40
* feat(metrics): add Prometheus GC metrics Track garbage collection activity with three new metrics: - zot_gc_runs_total (counter, label: error) — GC run count - zot_gc_duration_seconds (summary) — GC run duration - zot_gc_deleted_total (counter, label: type) — items deleted by type: blob, manifest, upload MetricServer is added to GarbageCollect and wired through all callers (controller, verify-feature retention, tests). Signed-off-by: Benoit Tigeot <benoit.tigeot@lifen.fr> * fix(test): add missing metrics var in GCS GC tests TestGCSGarbageCollectImageIndex and TestGCSGarbageCollectChainedImageIndexes were missing the metrics variable required by NewGarbageCollect after the MetricServer parameter was added. Signed-off-by: Benoit Tigeot <benoit.tigeot@lifen.fr> * fix(test): add defer metrics.Stop() in GC tests Prevent goroutine/port leaks by stopping MetricsServer in storage_test.go (3 functions) and gcs_test.go (also add missing metrics declaration in TestGCSGarbageCollectImageManifest). Signed-off-by: Benoit Tigeot <benoit.tigeot@lifen.fr> * fix(test): cover `CleanRepo` error path Add test that exercises the error branch in `CleanRepo` where `cleanRepo` fails, covering the metrics calls and log lines flagged by Codecov. Signed-off-by: Benoit Tigeot <benoit.tigeot@lifen.fr> * test: Cover GC error paths for codecov Add three tests in gc_internal_test.go to cover previously untested error branches in `removeBlobUploads` and `removeUnreferencedBlobs`: `ListBlobUploads` failure, `addIndexBlobsToReferences` failure, and `PathNotFoundError` from `GetAllBlobs`. Signed-off-by: Benoit Tigeot <benoit.tigeot@lifen.fr> * test(gc): cover remaining error paths Cover `StatBlobUpload`, `digest.Validate()`, `isBlobOlderThan`, and `CleanupRepo` error branches in `removeBlobUploads` and `removeUnreferencedBlobs`. `removeUnreferencedBlobs` now at 100% coverage, `removeBlobUploads` from 78.3% to 91.3%. Signed-off-by: Benoit Tigeot <benoit.tigeot@lifen.fr> * test: cover `sanityChecks` label name mismatch Try to avoid -0.09% coverage regression on `minimal.go` by exercising the uncovered branch in `sanityChecks` where label names have correct count but wrong values. Signed-off-by: Benoit Tigeot <benoit.tigeot@lifen.fr> * test(gc): exercise real GC path in metrics test TestGCMetrics was calling metric helpers directly instead of running actual garbage collection, so it couldn't catch wiring regressions where `CleanRepo` stops recording metrics. Now uploads an orphaned blob and runs `gc.CleanRepo` end-to-end, verifying metrics appear on the Prometheus endpoint. Suggestion from Copilot: https://github.com/project-zot/zot/pull/3863#discussion_r3129324719 Signed-off-by: Benoit Tigeot <benoit.tigeot@lifen.fr> * fix(gc): skip deletion metrics when DryRun is enabled https://github.com/project-zot/zot/pull/3863#discussion_r3129324684 Signed-off-by: Benoit Tigeot <benoit.tigeot@lifen.fr> * fix(test): stop leaked MetricsServer goroutines in GCS tests https://github.com/project-zot/zot/pull/3863#discussion_r3129324657 Signed-off-by: Benoit Tigeot <benoit.tigeot@lifen.fr> * refactor(test): drop unnecessary zlog import alias Signed-off-by: Benoit Tigeot <benoit.tigeot@lifen.fr> * fix(monitoring): expose metric types outside build tag `MetricsCopy` and related types were only visible under `\!metrics`, causing a typecheck failure when golangci-lint runs with `-tags metrics`. Moving the type definitions to `common.go` makes them unconditionally available. Signed-off-by: Benoit Tigeot <benoit.tigeot@lifen.fr> * fix(monitoring): remove extra blank line for gci Signed-off-by: Benoit Tigeot <benoit.tigeot@lifen.fr> * test(gc): cover both dry-run and real deletion metrics And fix issue with build tag with metrics Signed-off-by: Benoit Tigeot <benoit.tigeot@lifen.fr> * Satisfy testpackage linter for gc metrics test The `testpackage` linter allows `package gc` only in files named `*_internal_test.go`; rename to follow that convention. Signed-off-by: Benoit Tigeot <benoit.tigeot@lifen.fr> --------- Signed-off-by: Benoit Tigeot <benoit.tigeot@lifen.fr>
966 lines
28 KiB
Go
966 lines
28 KiB
Go
package gc
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"os"
|
|
"path"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/distribution/distribution/v3/registry/storage/driver"
|
|
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/api/config"
|
|
zcommon "zotregistry.dev/zot/v2/pkg/common"
|
|
"zotregistry.dev/zot/v2/pkg/extensions/monitoring"
|
|
zlog "zotregistry.dev/zot/v2/pkg/log"
|
|
"zotregistry.dev/zot/v2/pkg/meta/types"
|
|
"zotregistry.dev/zot/v2/pkg/storage"
|
|
"zotregistry.dev/zot/v2/pkg/storage/cache"
|
|
common "zotregistry.dev/zot/v2/pkg/storage/common"
|
|
storageConstants "zotregistry.dev/zot/v2/pkg/storage/constants"
|
|
"zotregistry.dev/zot/v2/pkg/storage/local"
|
|
. "zotregistry.dev/zot/v2/pkg/test/image-utils"
|
|
"zotregistry.dev/zot/v2/pkg/test/mocks"
|
|
)
|
|
|
|
var (
|
|
errGC = errors.New("gc error")
|
|
repoName = "test" //nolint: gochecknoglobals
|
|
)
|
|
|
|
func TestGarbageCollectManifestErrors(t *testing.T) {
|
|
Convey("Make imagestore and upload manifest", t, func(c C) {
|
|
dir := t.TempDir()
|
|
|
|
log := zlog.NewTestLogger()
|
|
audit := zlog.NewAuditLogger("debug", "")
|
|
|
|
metrics := monitoring.NewMetricsServer(false, log)
|
|
defer metrics.Stop() // Clean up metrics server to prevent resource leaks
|
|
|
|
cacheDriver, _ := storage.Create("boltdb", cache.BoltDBDriverParameters{
|
|
RootDir: dir,
|
|
Name: "cache",
|
|
UseRelPaths: true,
|
|
}, log)
|
|
imgStore := local.NewImageStore(dir, true, true, log, metrics, nil, cacheDriver, nil, nil)
|
|
|
|
gc := NewGarbageCollect(imgStore, mocks.MetaDBMock{}, Options{
|
|
Delay: storageConstants.DefaultGCDelay,
|
|
ImageRetention: config.ImageRetention{
|
|
Delay: storageConstants.DefaultGCDelay,
|
|
Policies: []config.RetentionPolicy{
|
|
{
|
|
Repositories: []string{"**"},
|
|
DeleteReferrers: true,
|
|
},
|
|
},
|
|
},
|
|
}, audit, log, metrics)
|
|
|
|
Convey("trigger missing blob in addImageIndexBlobsToReferences()", func() {
|
|
// GC should continue when blobs are missing (not found), not return an error
|
|
err := gc.addIndexBlobsToReferences(repoName, ispec.Index{
|
|
Manifests: []ispec.Descriptor{
|
|
{
|
|
Digest: godigest.FromString("miss"),
|
|
MediaType: ispec.MediaTypeImageIndex,
|
|
},
|
|
},
|
|
}, map[string]bool{})
|
|
So(err, ShouldBeNil)
|
|
})
|
|
|
|
Convey("trigger missing blob in addImageManifestBlobsToReferences()", func() {
|
|
// GC should continue when blobs are missing (not found), not return an error
|
|
err := gc.addIndexBlobsToReferences(repoName, ispec.Index{
|
|
Manifests: []ispec.Descriptor{
|
|
{
|
|
Digest: godigest.FromString("miss"),
|
|
MediaType: ispec.MediaTypeImageManifest,
|
|
},
|
|
},
|
|
}, map[string]bool{})
|
|
So(err, ShouldBeNil)
|
|
})
|
|
|
|
content := []byte("this is a blob")
|
|
digest := godigest.FromBytes(content)
|
|
So(digest, ShouldNotBeNil)
|
|
|
|
_, blen, err := imgStore.FullBlobUpload(repoName, bytes.NewReader(content), digest)
|
|
So(err, ShouldBeNil)
|
|
So(blen, ShouldEqual, len(content))
|
|
|
|
cblob, cdigest := GetRandomImageConfig()
|
|
_, clen, err := imgStore.FullBlobUpload(repoName, bytes.NewReader(cblob), cdigest)
|
|
So(err, ShouldBeNil)
|
|
So(clen, ShouldEqual, len(cblob))
|
|
|
|
manifest := ispec.Manifest{
|
|
Config: ispec.Descriptor{
|
|
MediaType: ispec.MediaTypeImageConfig,
|
|
Digest: cdigest,
|
|
Size: int64(len(cblob)),
|
|
},
|
|
Layers: []ispec.Descriptor{
|
|
{
|
|
MediaType: ispec.MediaTypeImageLayer,
|
|
Digest: digest,
|
|
Size: int64(len(content)),
|
|
},
|
|
},
|
|
}
|
|
|
|
manifest.SchemaVersion = 2
|
|
|
|
body, err := json.Marshal(manifest)
|
|
So(err, ShouldBeNil)
|
|
|
|
manifestDigest := godigest.FromBytes(body)
|
|
|
|
_, _, err = imgStore.PutImageManifest(repoName, "1.0", ispec.MediaTypeImageManifest, body, nil)
|
|
So(err, ShouldBeNil)
|
|
|
|
Convey("trigger GetIndex error in GetReferencedBlobs", func() {
|
|
index, err := common.GetIndex(imgStore, repoName, log)
|
|
So(err, ShouldBeNil)
|
|
|
|
err = os.Chmod(path.Join(imgStore.RootDir(), repoName), 0o000)
|
|
So(err, ShouldBeNil)
|
|
|
|
defer func() {
|
|
err := os.Chmod(path.Join(imgStore.RootDir(), repoName), 0o755)
|
|
So(err, ShouldBeNil)
|
|
}()
|
|
|
|
// Note: Permission denied from Stat() is converted to ErrBlobNotFound in originalBlobInfo,
|
|
// so we can't distinguish it from missing blobs. GC treats missing blobs gracefully,
|
|
// so permission denied from Stat() will also be treated as missing (return nil).
|
|
// Permission denied from ReadFile() will still return an error.
|
|
err = gc.addIndexBlobsToReferences(repoName, index, map[string]bool{})
|
|
So(err, ShouldBeNil)
|
|
})
|
|
|
|
Convey("trigger GetImageManifest error in AddIndexBlobsToReferences", func() {
|
|
index, err := common.GetIndex(imgStore, repoName, log)
|
|
So(err, ShouldBeNil)
|
|
|
|
err = os.Chmod(path.Join(imgStore.RootDir(), repoName, "blobs", "sha256", manifestDigest.Encoded()), 0o000)
|
|
So(err, ShouldBeNil)
|
|
|
|
defer func() {
|
|
err := os.Chmod(path.Join(imgStore.RootDir(), repoName, "blobs", "sha256", manifestDigest.Encoded()), 0o755)
|
|
So(err, ShouldBeNil)
|
|
}()
|
|
|
|
err = gc.addIndexBlobsToReferences(repoName, index, map[string]bool{})
|
|
So(err, ShouldNotBeNil)
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestGarbageCollectIndexErrors(t *testing.T) {
|
|
Convey("Make imagestore and upload manifest", t, func(c C) {
|
|
dir := t.TempDir()
|
|
|
|
log := zlog.NewTestLogger()
|
|
audit := zlog.NewAuditLogger("debug", "")
|
|
|
|
metrics := monitoring.NewMetricsServer(false, log)
|
|
defer metrics.Stop() // Clean up metrics server to prevent resource leaks
|
|
cacheDriver, _ := storage.Create("boltdb", cache.BoltDBDriverParameters{
|
|
RootDir: dir,
|
|
Name: "cache",
|
|
UseRelPaths: true,
|
|
}, log)
|
|
imgStore := local.NewImageStore(dir, true, true, log, metrics, nil, cacheDriver, nil, nil)
|
|
|
|
gc := NewGarbageCollect(imgStore, mocks.MetaDBMock{}, Options{
|
|
Delay: storageConstants.DefaultGCDelay,
|
|
ImageRetention: config.ImageRetention{
|
|
Delay: storageConstants.DefaultGCDelay,
|
|
Policies: []config.RetentionPolicy{
|
|
{
|
|
Repositories: []string{"**"},
|
|
DeleteReferrers: true,
|
|
},
|
|
},
|
|
},
|
|
}, audit, log, metrics)
|
|
|
|
content := []byte("this is a blob")
|
|
bdgst := godigest.FromBytes(content)
|
|
So(bdgst, ShouldNotBeNil)
|
|
|
|
_, bsize, err := imgStore.FullBlobUpload(repoName, bytes.NewReader(content), bdgst)
|
|
So(err, ShouldBeNil)
|
|
So(bsize, ShouldEqual, len(content))
|
|
|
|
var index ispec.Index
|
|
index.SchemaVersion = 2
|
|
index.MediaType = ispec.MediaTypeImageIndex
|
|
|
|
var digest godigest.Digest
|
|
|
|
for i := 0; i < 4; i++ {
|
|
// upload image config blob
|
|
upload, err := imgStore.NewBlobUpload(repoName)
|
|
So(err, ShouldBeNil)
|
|
So(upload, ShouldNotBeEmpty)
|
|
|
|
cblob, cdigest := GetRandomImageConfig()
|
|
buf := bytes.NewBuffer(cblob)
|
|
buflen := buf.Len()
|
|
blob, err := imgStore.PutBlobChunkStreamed(repoName, upload, buf)
|
|
So(err, ShouldBeNil)
|
|
So(blob, ShouldEqual, buflen)
|
|
|
|
err = imgStore.FinishBlobUpload(repoName, upload, buf, cdigest)
|
|
So(err, ShouldBeNil)
|
|
So(blob, ShouldEqual, buflen)
|
|
|
|
// create a manifest
|
|
manifest := ispec.Manifest{
|
|
Config: ispec.Descriptor{
|
|
MediaType: ispec.MediaTypeImageConfig,
|
|
Digest: cdigest,
|
|
Size: int64(len(cblob)),
|
|
},
|
|
Layers: []ispec.Descriptor{
|
|
{
|
|
MediaType: ispec.MediaTypeImageLayer,
|
|
Digest: bdgst,
|
|
Size: bsize,
|
|
},
|
|
},
|
|
}
|
|
manifest.SchemaVersion = 2
|
|
content, err = json.Marshal(manifest)
|
|
So(err, ShouldBeNil)
|
|
|
|
digest = godigest.FromBytes(content)
|
|
So(digest, ShouldNotBeNil)
|
|
_, _, err = imgStore.PutImageManifest(repoName, digest.String(), ispec.MediaTypeImageManifest, content, nil)
|
|
So(err, ShouldBeNil)
|
|
|
|
index.Manifests = append(index.Manifests, ispec.Descriptor{
|
|
Digest: digest,
|
|
MediaType: ispec.MediaTypeImageManifest,
|
|
Size: int64(len(content)),
|
|
})
|
|
}
|
|
|
|
// upload index image
|
|
indexContent, err := json.Marshal(index)
|
|
So(err, ShouldBeNil)
|
|
|
|
indexDigest := godigest.FromBytes(indexContent)
|
|
So(indexDigest, ShouldNotBeNil)
|
|
|
|
_, _, err = imgStore.PutImageManifest(repoName, "1.0", ispec.MediaTypeImageIndex, indexContent, nil)
|
|
So(err, ShouldBeNil)
|
|
|
|
index, err = common.GetIndex(imgStore, repoName, log)
|
|
So(err, ShouldBeNil)
|
|
|
|
err = gc.addIndexBlobsToReferences(repoName, index, map[string]bool{})
|
|
So(err, ShouldBeNil)
|
|
|
|
Convey("trigger GetImageIndex error in GetReferencedBlobsInImageIndex", func() {
|
|
err := os.Chmod(path.Join(imgStore.RootDir(), repoName, "blobs", "sha256", indexDigest.Encoded()), 0o000)
|
|
So(err, ShouldBeNil)
|
|
|
|
defer func() {
|
|
err := os.Chmod(path.Join(imgStore.RootDir(), repoName, "blobs", "sha256", indexDigest.Encoded()), 0o755)
|
|
So(err, ShouldBeNil)
|
|
}()
|
|
|
|
err = gc.addIndexBlobsToReferences(repoName, index, map[string]bool{})
|
|
So(err, ShouldNotBeNil)
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestGarbageCollectWithMockedImageStore(t *testing.T) {
|
|
trueVal := true
|
|
|
|
ctx := context.Background()
|
|
|
|
Convey("Cover gc error paths", t, func(c C) {
|
|
log := zlog.NewTestLogger()
|
|
audit := zlog.NewAuditLogger("debug", "")
|
|
metrics := monitoring.NewMetricsServer(false, log)
|
|
defer metrics.Stop()
|
|
|
|
gcOptions := Options{
|
|
Delay: storageConstants.DefaultGCDelay,
|
|
ImageRetention: config.ImageRetention{
|
|
Delay: storageConstants.DefaultGCDelay,
|
|
Policies: []config.RetentionPolicy{
|
|
{
|
|
Repositories: []string{"**"},
|
|
DeleteReferrers: true,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
Convey("Error on GetIndex in gc.cleanRepo()", func() {
|
|
gc := NewGarbageCollect(mocks.MockedImageStore{}, mocks.MetaDBMock{
|
|
GetRepoMetaFn: func(ctx context.Context, repo string) (types.RepoMeta, error) {
|
|
return types.RepoMeta{}, errGC
|
|
},
|
|
}, gcOptions, audit, log, metrics)
|
|
|
|
err := gc.cleanRepo(ctx, repoName)
|
|
So(err, ShouldNotBeNil)
|
|
})
|
|
|
|
Convey("Error on GetIndex in gc.removeUnreferencedBlobs()", func() {
|
|
gc := NewGarbageCollect(mocks.MockedImageStore{}, mocks.MetaDBMock{
|
|
GetRepoMetaFn: func(ctx context.Context, repo string) (types.RepoMeta, error) {
|
|
return types.RepoMeta{}, errGC
|
|
},
|
|
}, gcOptions, audit, log, metrics)
|
|
|
|
_, err := gc.removeUnreferencedBlobs("repo", time.Hour, log)
|
|
So(err, ShouldNotBeNil)
|
|
})
|
|
|
|
Convey("Error on gc.removeManifest()", func() {
|
|
gc := NewGarbageCollect(mocks.MockedImageStore{}, mocks.MetaDBMock{
|
|
GetRepoMetaFn: func(ctx context.Context, repo string) (types.RepoMeta, error) {
|
|
return types.RepoMeta{}, errGC
|
|
},
|
|
}, gcOptions, audit, log, metrics)
|
|
|
|
_, err := gc.removeManifest("", &ispec.Index{}, ispec.DescriptorEmptyJSON, "tag", "", "")
|
|
So(err, ShouldNotBeNil)
|
|
})
|
|
|
|
Convey("Error on metaDB in gc.cleanRepo()", func() {
|
|
gcOptions := Options{
|
|
Delay: storageConstants.DefaultGCDelay,
|
|
ImageRetention: config.ImageRetention{
|
|
Delay: storageConstants.DefaultGCDelay,
|
|
Policies: []config.RetentionPolicy{
|
|
{
|
|
Repositories: []string{"**"},
|
|
KeepTags: []config.KeepTagsPolicy{
|
|
{
|
|
Patterns: []string{".*"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
gc := NewGarbageCollect(mocks.MockedImageStore{}, mocks.MetaDBMock{
|
|
GetRepoMetaFn: func(ctx context.Context, repo string) (types.RepoMeta, error) {
|
|
return types.RepoMeta{}, errGC
|
|
},
|
|
}, gcOptions, audit, log, metrics)
|
|
|
|
err := gc.removeTagsPerRetentionPolicy(ctx, "name", &ispec.Index{})
|
|
So(err, ShouldNotBeNil)
|
|
})
|
|
|
|
Convey("Error on context done in removeTags...", func() {
|
|
gcOptions := Options{
|
|
Delay: storageConstants.DefaultGCDelay,
|
|
ImageRetention: config.ImageRetention{
|
|
Delay: storageConstants.DefaultGCDelay,
|
|
Policies: []config.RetentionPolicy{
|
|
{
|
|
Repositories: []string{"**"},
|
|
KeepTags: []config.KeepTagsPolicy{
|
|
{
|
|
Patterns: []string{".*"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
gc := NewGarbageCollect(mocks.MockedImageStore{}, mocks.MetaDBMock{}, gcOptions, audit, log, metrics)
|
|
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
cancel()
|
|
|
|
err := gc.removeTagsPerRetentionPolicy(ctx, "name", &ispec.Index{
|
|
Manifests: []ispec.Descriptor{
|
|
{
|
|
MediaType: ispec.MediaTypeImageManifest,
|
|
Digest: godigest.FromBytes([]byte("digest")),
|
|
},
|
|
},
|
|
})
|
|
So(err, ShouldNotBeNil)
|
|
})
|
|
|
|
Convey("Error on PutIndexContent in gc.cleanRepo()", func() {
|
|
returnedIndexJSON := ispec.Index{}
|
|
|
|
returnedIndexJSONBuf, err := json.Marshal(returnedIndexJSON)
|
|
So(err, ShouldBeNil)
|
|
|
|
imgStore := mocks.MockedImageStore{
|
|
PutIndexContentFn: func(repo string, index ispec.Index) error {
|
|
return errGC
|
|
},
|
|
GetIndexContentFn: func(repo string) ([]byte, error) {
|
|
return returnedIndexJSONBuf, nil
|
|
},
|
|
}
|
|
|
|
gc := NewGarbageCollect(imgStore, mocks.MetaDBMock{}, gcOptions, audit, log, metrics)
|
|
|
|
err = gc.cleanRepo(ctx, repoName)
|
|
So(err, ShouldNotBeNil)
|
|
})
|
|
|
|
Convey("Error on gc.cleanBlobs() in gc.cleanRepo()", func() {
|
|
returnedIndexJSON := ispec.Index{}
|
|
|
|
returnedIndexJSONBuf, err := json.Marshal(returnedIndexJSON)
|
|
So(err, ShouldBeNil)
|
|
|
|
imgStore := mocks.MockedImageStore{
|
|
PutIndexContentFn: func(repo string, index ispec.Index) error {
|
|
return nil
|
|
},
|
|
GetIndexContentFn: func(repo string) ([]byte, error) {
|
|
return returnedIndexJSONBuf, nil
|
|
},
|
|
GetAllBlobsFn: func(repo string) ([]godigest.Digest, error) {
|
|
return []godigest.Digest{}, errGC
|
|
},
|
|
}
|
|
|
|
gc := NewGarbageCollect(imgStore, mocks.MetaDBMock{}, gcOptions, audit, log, metrics)
|
|
|
|
err = gc.cleanRepo(ctx, repoName)
|
|
So(err, ShouldNotBeNil)
|
|
})
|
|
|
|
Convey("False on imgStore.DirExists() in gc.cleanRepo()", func() {
|
|
imgStore := mocks.MockedImageStore{
|
|
DirExistsFn: func(d string) bool {
|
|
return false
|
|
},
|
|
}
|
|
|
|
gc := NewGarbageCollect(imgStore, mocks.MetaDBMock{}, gcOptions, audit, log, metrics)
|
|
|
|
err := gc.cleanRepo(ctx, repoName)
|
|
So(err, ShouldNotBeNil)
|
|
})
|
|
|
|
Convey("Error on gc.identifyManifestsReferencedInIndex in gc.cleanManifests() with multiarch image", func() {
|
|
indexImageDigest := godigest.FromBytes([]byte("digest"))
|
|
|
|
returnedIndexImage := ispec.Index{
|
|
Subject: &ispec.DescriptorEmptyJSON,
|
|
Manifests: []ispec.Descriptor{
|
|
{
|
|
MediaType: ispec.MediaTypeImageIndex,
|
|
Digest: godigest.FromBytes([]byte("digest2")),
|
|
},
|
|
},
|
|
}
|
|
|
|
returnedIndexImageBuf, err := json.Marshal(returnedIndexImage)
|
|
So(err, ShouldBeNil)
|
|
|
|
imgStore := mocks.MockedImageStore{
|
|
GetBlobContentFn: func(repo string, digest godigest.Digest) ([]byte, error) {
|
|
if digest == indexImageDigest {
|
|
return returnedIndexImageBuf, nil
|
|
} else {
|
|
return nil, errGC
|
|
}
|
|
},
|
|
}
|
|
|
|
gcOptions.ImageRetention = config.ImageRetention{
|
|
Policies: []config.RetentionPolicy{
|
|
{
|
|
Repositories: []string{"**"},
|
|
DeleteUntagged: &trueVal,
|
|
},
|
|
},
|
|
}
|
|
gc := NewGarbageCollect(imgStore, mocks.MetaDBMock{}, gcOptions, audit, log, metrics)
|
|
|
|
err = gc.removeManifestsPerRepoPolicy(ctx, repoName, &ispec.Index{
|
|
Manifests: []ispec.Descriptor{
|
|
{
|
|
MediaType: ispec.MediaTypeImageIndex,
|
|
Digest: indexImageDigest,
|
|
},
|
|
},
|
|
})
|
|
So(err, ShouldNotBeNil)
|
|
})
|
|
|
|
Convey("Error on gc.identifyManifestsReferencedInIndex in gc.cleanManifests() with image", func() {
|
|
imgStore := mocks.MockedImageStore{
|
|
GetBlobContentFn: func(repo string, digest godigest.Digest) ([]byte, error) {
|
|
return nil, errGC
|
|
},
|
|
}
|
|
|
|
gcOptions.ImageRetention = config.ImageRetention{
|
|
Policies: []config.RetentionPolicy{
|
|
{
|
|
Repositories: []string{"**"},
|
|
DeleteUntagged: &trueVal,
|
|
},
|
|
},
|
|
}
|
|
|
|
gc := NewGarbageCollect(imgStore, mocks.MetaDBMock{}, gcOptions, audit, log, metrics)
|
|
|
|
err := gc.removeManifestsPerRepoPolicy(ctx, repoName, &ispec.Index{
|
|
Manifests: []ispec.Descriptor{
|
|
{
|
|
MediaType: ispec.MediaTypeImageManifest,
|
|
Digest: godigest.FromBytes([]byte("digest")),
|
|
},
|
|
},
|
|
})
|
|
So(err, ShouldNotBeNil)
|
|
})
|
|
|
|
Convey("Error on context done in removeManifests...", func() {
|
|
imgStore := mocks.MockedImageStore{}
|
|
|
|
gcOptions.ImageRetention = config.ImageRetention{
|
|
Policies: []config.RetentionPolicy{
|
|
{
|
|
Repositories: []string{"**"},
|
|
DeleteUntagged: &trueVal,
|
|
},
|
|
},
|
|
}
|
|
|
|
gc := NewGarbageCollect(imgStore, mocks.MetaDBMock{}, gcOptions, audit, log, metrics)
|
|
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
cancel()
|
|
|
|
err := gc.removeManifestsPerRepoPolicy(ctx, repoName, &ispec.Index{
|
|
Manifests: []ispec.Descriptor{
|
|
{
|
|
MediaType: ispec.MediaTypeImageManifest,
|
|
Digest: godigest.FromBytes([]byte("digest")),
|
|
},
|
|
},
|
|
})
|
|
So(err, ShouldNotBeNil)
|
|
})
|
|
|
|
Convey("Error on gc.gcManifest() in gc.cleanManifests() with image", func() {
|
|
returnedImage := ispec.Manifest{
|
|
MediaType: ispec.MediaTypeImageManifest,
|
|
}
|
|
|
|
returnedImageBuf, err := json.Marshal(returnedImage)
|
|
So(err, ShouldBeNil)
|
|
|
|
imgStore := mocks.MockedImageStore{
|
|
GetBlobContentFn: func(repo string, digest godigest.Digest) ([]byte, error) {
|
|
return returnedImageBuf, nil
|
|
},
|
|
}
|
|
|
|
metaDB := mocks.MetaDBMock{
|
|
RemoveRepoReferenceFn: func(repo, reference string, manifestDigest godigest.Digest) error {
|
|
return errGC
|
|
},
|
|
}
|
|
|
|
gcOptions.ImageRetention = config.ImageRetention{
|
|
Policies: []config.RetentionPolicy{
|
|
{
|
|
Repositories: []string{"**"},
|
|
DeleteUntagged: &trueVal,
|
|
},
|
|
},
|
|
}
|
|
gc := NewGarbageCollect(imgStore, metaDB, gcOptions, audit, log, metrics)
|
|
|
|
err = gc.removeManifestsPerRepoPolicy(ctx, repoName, &ispec.Index{
|
|
Manifests: []ispec.Descriptor{
|
|
{
|
|
MediaType: ispec.MediaTypeImageManifest,
|
|
Digest: godigest.FromBytes([]byte("digest")),
|
|
},
|
|
},
|
|
})
|
|
So(err, ShouldNotBeNil)
|
|
})
|
|
Convey("Error on gc.gcManifest() in gc.cleanManifests() with signature", func() {
|
|
returnedImage := ispec.Manifest{
|
|
MediaType: ispec.MediaTypeImageManifest,
|
|
ArtifactType: zcommon.NotationSignature,
|
|
}
|
|
|
|
returnedImageBuf, err := json.Marshal(returnedImage)
|
|
So(err, ShouldBeNil)
|
|
|
|
imgStore := mocks.MockedImageStore{
|
|
GetBlobContentFn: func(repo string, digest godigest.Digest) ([]byte, error) {
|
|
return returnedImageBuf, nil
|
|
},
|
|
}
|
|
|
|
metaDB := mocks.MetaDBMock{
|
|
DeleteSignatureFn: func(repo string, signedManifestDigest godigest.Digest, sm types.SignatureMetadata) error {
|
|
return errGC
|
|
},
|
|
}
|
|
|
|
gcOptions.ImageRetention = config.ImageRetention{}
|
|
gc := NewGarbageCollect(imgStore, metaDB, gcOptions, audit, log, metrics)
|
|
|
|
desc := ispec.Descriptor{
|
|
MediaType: ispec.MediaTypeImageManifest,
|
|
Digest: godigest.FromBytes([]byte("digest")),
|
|
}
|
|
|
|
index := &ispec.Index{
|
|
Manifests: []ispec.Descriptor{desc},
|
|
}
|
|
_, err = gc.removeManifest(repoName, index, desc, desc.Digest.String(), storage.NotationType,
|
|
godigest.FromBytes([]byte("digest2")))
|
|
|
|
So(err, ShouldNotBeNil)
|
|
})
|
|
|
|
Convey("Error on gc.gcReferrer() in gc.cleanManifests() with image index", func() {
|
|
manifestDesc := ispec.Descriptor{
|
|
MediaType: ispec.MediaTypeImageIndex,
|
|
Digest: godigest.FromBytes([]byte("digest")),
|
|
}
|
|
|
|
returnedIndexImage := ispec.Index{
|
|
MediaType: ispec.MediaTypeImageIndex,
|
|
Subject: &ispec.Descriptor{
|
|
Digest: godigest.FromBytes([]byte("digest2")),
|
|
},
|
|
Manifests: []ispec.Descriptor{
|
|
manifestDesc,
|
|
},
|
|
}
|
|
|
|
returnedIndexImageBuf, err := json.Marshal(returnedIndexImage)
|
|
So(err, ShouldBeNil)
|
|
|
|
imgStore := mocks.MockedImageStore{
|
|
GetBlobContentFn: func(repo string, digest godigest.Digest) ([]byte, error) {
|
|
return returnedIndexImageBuf, nil
|
|
},
|
|
StatBlobFn: func(repo string, digest godigest.Digest) (bool, int64, time.Time, error) {
|
|
return false, -1, time.Time{}, errGC
|
|
},
|
|
}
|
|
|
|
gc := NewGarbageCollect(imgStore, mocks.MetaDBMock{}, gcOptions, audit, log, metrics)
|
|
|
|
err = gc.removeManifestsPerRepoPolicy(ctx, repoName, &returnedIndexImage)
|
|
So(err, ShouldNotBeNil)
|
|
})
|
|
|
|
Convey("Error on gc.gcReferrer() in gc.cleanManifests() with image", func() {
|
|
manifestDesc := ispec.Descriptor{
|
|
MediaType: ispec.MediaTypeImageManifest,
|
|
Digest: godigest.FromBytes([]byte("digest")),
|
|
}
|
|
|
|
returnedImage := ispec.Manifest{
|
|
Subject: &ispec.Descriptor{
|
|
Digest: godigest.FromBytes([]byte("digest2")),
|
|
},
|
|
MediaType: ispec.MediaTypeImageManifest,
|
|
}
|
|
|
|
returnedImageBuf, err := json.Marshal(returnedImage)
|
|
So(err, ShouldBeNil)
|
|
|
|
imgStore := mocks.MockedImageStore{
|
|
GetBlobContentFn: func(repo string, digest godigest.Digest) ([]byte, error) {
|
|
return returnedImageBuf, nil
|
|
},
|
|
StatBlobFn: func(repo string, digest godigest.Digest) (bool, int64, time.Time, error) {
|
|
return false, -1, time.Time{}, errGC
|
|
},
|
|
}
|
|
|
|
gc := NewGarbageCollect(imgStore, mocks.MetaDBMock{}, gcOptions, audit, log, metrics)
|
|
|
|
err = gc.removeManifestsPerRepoPolicy(ctx, repoName, &ispec.Index{
|
|
Manifests: []ispec.Descriptor{
|
|
manifestDesc,
|
|
},
|
|
})
|
|
So(err, ShouldNotBeNil)
|
|
})
|
|
|
|
Convey("Missing nested index blob in removeIndexReferrers is skipped gracefully", func() {
|
|
// Create a top-level index that contains a nested index
|
|
// The nested index blob will be missing
|
|
topLevelIndex := ispec.Index{
|
|
MediaType: ispec.MediaTypeImageIndex,
|
|
Manifests: []ispec.Descriptor{
|
|
{
|
|
MediaType: ispec.MediaTypeImageIndex,
|
|
Digest: godigest.FromString("missing-nested-index"),
|
|
Size: 100,
|
|
},
|
|
},
|
|
}
|
|
|
|
imgStore := mocks.MockedImageStore{
|
|
GetBlobContentFn: func(repo string, digest godigest.Digest) ([]byte, error) {
|
|
// Return ErrBlobNotFound for the missing nested index
|
|
return nil, zerr.ErrBlobNotFound
|
|
},
|
|
}
|
|
|
|
gcOptions.ImageRetention = config.ImageRetention{
|
|
Policies: []config.RetentionPolicy{
|
|
{
|
|
Repositories: []string{"**"},
|
|
DeleteReferrers: true,
|
|
},
|
|
},
|
|
}
|
|
|
|
gc := NewGarbageCollect(imgStore, mocks.MetaDBMock{}, gcOptions, audit, log, metrics)
|
|
|
|
// removeIndexReferrers should skip the missing nested index and continue
|
|
gced, err := gc.removeIndexReferrers(repoName, &topLevelIndex, topLevelIndex)
|
|
So(err, ShouldBeNil)
|
|
So(gced, ShouldBeFalse)
|
|
})
|
|
|
|
Convey("Missing nested index blob in identifyManifestsReferencedInIndex is skipped gracefully", func() {
|
|
// Create a top-level index that contains a nested index
|
|
// The nested index blob will be missing
|
|
topLevelIndex := ispec.Index{
|
|
MediaType: ispec.MediaTypeImageIndex,
|
|
Manifests: []ispec.Descriptor{
|
|
{
|
|
MediaType: ispec.MediaTypeImageIndex,
|
|
Digest: godigest.FromString("missing-nested-index"),
|
|
Size: 100,
|
|
},
|
|
},
|
|
}
|
|
|
|
imgStore := mocks.MockedImageStore{
|
|
GetBlobContentFn: func(repo string, digest godigest.Digest) ([]byte, error) {
|
|
// Return ErrBlobNotFound for the missing nested index
|
|
return nil, zerr.ErrBlobNotFound
|
|
},
|
|
}
|
|
|
|
gc := NewGarbageCollect(imgStore, mocks.MetaDBMock{}, gcOptions, audit, log, metrics)
|
|
|
|
// identifyManifestsReferencedInIndex should skip the missing nested index and continue
|
|
referenced := make(map[godigest.Digest]bool)
|
|
err := gc.identifyManifestsReferencedInIndex(topLevelIndex, repoName, referenced)
|
|
So(err, ShouldBeNil)
|
|
// No manifests should be marked as referenced since the nested index is missing
|
|
So(len(referenced), ShouldEqual, 0)
|
|
})
|
|
|
|
Convey("Error on ListBlobUploads in removeBlobUploads", func() {
|
|
imgStore := mocks.MockedImageStore{
|
|
DirExistsFn: func(d string) bool {
|
|
return true
|
|
},
|
|
ListBlobUploadsFn: func(repo string) ([]string, error) {
|
|
return nil, errGC
|
|
},
|
|
}
|
|
|
|
gc := NewGarbageCollect(imgStore, mocks.MetaDBMock{}, gcOptions, audit, log, metrics)
|
|
|
|
deleted, err := gc.removeBlobUploads(repoName, time.Hour)
|
|
So(err, ShouldNotBeNil)
|
|
So(deleted, ShouldEqual, 0)
|
|
})
|
|
|
|
Convey("Error on addIndexBlobsToReferences in removeUnreferencedBlobs", func() {
|
|
returnedIndex := ispec.Index{
|
|
Manifests: []ispec.Descriptor{
|
|
{
|
|
MediaType: ispec.MediaTypeImageManifest,
|
|
Digest: godigest.FromBytes([]byte("manifest-content")),
|
|
},
|
|
},
|
|
}
|
|
returnedIndexBuf, err := json.Marshal(returnedIndex)
|
|
So(err, ShouldBeNil)
|
|
|
|
imgStore := mocks.MockedImageStore{
|
|
GetIndexContentFn: func(repo string) ([]byte, error) {
|
|
return returnedIndexBuf, nil
|
|
},
|
|
GetBlobContentFn: func(repo string, digest godigest.Digest) ([]byte, error) {
|
|
return nil, errGC
|
|
},
|
|
}
|
|
|
|
gc := NewGarbageCollect(imgStore, mocks.MetaDBMock{}, gcOptions, audit, log, metrics)
|
|
|
|
deleted, err := gc.removeUnreferencedBlobs(repoName, time.Hour, log)
|
|
So(err, ShouldNotBeNil)
|
|
So(deleted, ShouldEqual, 0)
|
|
})
|
|
|
|
Convey("PathNotFoundError on GetAllBlobs in removeUnreferencedBlobs", func() {
|
|
returnedIndex := ispec.Index{}
|
|
returnedIndexBuf, err := json.Marshal(returnedIndex)
|
|
So(err, ShouldBeNil)
|
|
|
|
imgStore := mocks.MockedImageStore{
|
|
GetIndexContentFn: func(repo string) ([]byte, error) {
|
|
return returnedIndexBuf, nil
|
|
},
|
|
GetAllBlobsFn: func(repo string) ([]godigest.Digest, error) {
|
|
return nil, driver.PathNotFoundError{Path: "/blobs/sha256", DriverName: "local"}
|
|
},
|
|
}
|
|
|
|
gc := NewGarbageCollect(imgStore, mocks.MetaDBMock{}, gcOptions, audit, log, metrics)
|
|
|
|
deleted, err := gc.removeUnreferencedBlobs(repoName, time.Hour, log)
|
|
So(err, ShouldBeNil)
|
|
So(deleted, ShouldEqual, 0)
|
|
})
|
|
|
|
Convey("StatBlobUpload error in removeBlobUploads", func() {
|
|
imgStore := mocks.MockedImageStore{
|
|
DirExistsFn: func(d string) bool {
|
|
return true
|
|
},
|
|
ListBlobUploadsFn: func(repo string) ([]string, error) {
|
|
return []string{"upload-1"}, nil
|
|
},
|
|
StatBlobUploadFn: func(repo string, uuid string) (bool, int64, time.Time, error) {
|
|
return false, 0, time.Time{}, errGC
|
|
},
|
|
}
|
|
|
|
gc := NewGarbageCollect(imgStore, mocks.MetaDBMock{}, gcOptions, audit, log, metrics)
|
|
|
|
deleted, err := gc.removeBlobUploads(repoName, time.Hour)
|
|
So(err, ShouldNotBeNil)
|
|
So(deleted, ShouldEqual, 0)
|
|
})
|
|
|
|
Convey("Invalid digest from GetAllBlobs in removeUnreferencedBlobs", func() {
|
|
returnedIndex := ispec.Index{}
|
|
returnedIndexBuf, err := json.Marshal(returnedIndex)
|
|
So(err, ShouldBeNil)
|
|
|
|
imgStore := mocks.MockedImageStore{
|
|
GetIndexContentFn: func(repo string) ([]byte, error) {
|
|
return returnedIndexBuf, nil
|
|
},
|
|
GetAllBlobsFn: func(repo string) ([]godigest.Digest, error) {
|
|
return []godigest.Digest{godigest.Digest("invalid")}, nil
|
|
},
|
|
}
|
|
|
|
gc := NewGarbageCollect(imgStore, mocks.MetaDBMock{}, gcOptions, audit, log, metrics)
|
|
|
|
deleted, err := gc.removeUnreferencedBlobs(repoName, time.Hour, log)
|
|
So(err, ShouldNotBeNil)
|
|
So(deleted, ShouldEqual, 0)
|
|
})
|
|
|
|
Convey("StatBlob error in removeUnreferencedBlobs", func() {
|
|
blobDigest := godigest.FromBytes([]byte("blob-content"))
|
|
|
|
returnedIndex := ispec.Index{}
|
|
returnedIndexBuf, err := json.Marshal(returnedIndex)
|
|
So(err, ShouldBeNil)
|
|
|
|
imgStore := mocks.MockedImageStore{
|
|
GetIndexContentFn: func(repo string) ([]byte, error) {
|
|
return returnedIndexBuf, nil
|
|
},
|
|
GetAllBlobsFn: func(repo string) ([]godigest.Digest, error) {
|
|
return []godigest.Digest{blobDigest}, nil
|
|
},
|
|
StatBlobFn: func(repo string, digest godigest.Digest) (bool, int64, time.Time, error) {
|
|
return false, 0, time.Time{}, errGC
|
|
},
|
|
}
|
|
|
|
gc := NewGarbageCollect(imgStore, mocks.MetaDBMock{}, gcOptions, audit, log, metrics)
|
|
|
|
deleted, err := gc.removeUnreferencedBlobs(repoName, time.Hour, log)
|
|
So(err, ShouldNotBeNil)
|
|
So(deleted, ShouldEqual, 0)
|
|
})
|
|
|
|
Convey("CleanupRepo error in removeUnreferencedBlobs", func() {
|
|
blobDigest := godigest.FromBytes([]byte("blob-content"))
|
|
|
|
returnedIndex := ispec.Index{}
|
|
returnedIndexBuf, err := json.Marshal(returnedIndex)
|
|
So(err, ShouldBeNil)
|
|
|
|
imgStore := mocks.MockedImageStore{
|
|
GetIndexContentFn: func(repo string) ([]byte, error) {
|
|
return returnedIndexBuf, nil
|
|
},
|
|
GetAllBlobsFn: func(repo string) ([]godigest.Digest, error) {
|
|
return []godigest.Digest{blobDigest}, nil
|
|
},
|
|
StatBlobFn: func(repo string, digest godigest.Digest) (bool, int64, time.Time, error) {
|
|
return true, 100, time.Now().Add(-2 * time.Hour), nil
|
|
},
|
|
CleanupRepoFn: func(repo string, blobs []godigest.Digest, removeRepo bool) (int, error) {
|
|
return 0, errGC
|
|
},
|
|
}
|
|
|
|
gc := NewGarbageCollect(imgStore, mocks.MetaDBMock{}, gcOptions, audit, log, metrics)
|
|
|
|
deleted, err := gc.removeUnreferencedBlobs(repoName, time.Hour, log)
|
|
So(err, ShouldNotBeNil)
|
|
So(deleted, ShouldEqual, 0)
|
|
})
|
|
|
|
Convey("CleanRepo records error metrics when cleanRepo fails", func() {
|
|
imgStore := mocks.MockedImageStore{
|
|
DirExistsFn: func(d string) bool {
|
|
return false
|
|
},
|
|
}
|
|
|
|
gc := NewGarbageCollect(imgStore, mocks.MetaDBMock{}, gcOptions, audit, log, metrics)
|
|
|
|
err := gc.CleanRepo(ctx, repoName)
|
|
So(err, ShouldNotBeNil)
|
|
So(errors.Is(err, zerr.ErrRepoNotFound), ShouldBeTrue)
|
|
})
|
|
})
|
|
}
|