From d8be5d48d5372427c2750f6318df5690bdb82998 Mon Sep 17 00:00:00 2001 From: adolfo-ab Date: Wed, 15 Jul 2026 15:24:49 +0200 Subject: [PATCH] CLID-687: Add integration test for delete with --force-cache-delete --- tests/integration/delete_test.go | 123 ++++++++++++++++++++++++++++++- tests/integration/go.mod | 2 +- 2 files changed, 122 insertions(+), 3 deletions(-) diff --git a/tests/integration/delete_test.go b/tests/integration/delete_test.go index d1dabc865..01c20384f 100644 --- a/tests/integration/delete_test.go +++ b/tests/integration/delete_test.go @@ -1,9 +1,12 @@ package integration_test import ( + "os" "path/filepath" . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/util/sets" ) var _ = Describe("delete", func() { @@ -25,8 +28,15 @@ var _ = Describe("delete", func() { It("should create delete yaml files with the delete-id in their names", func() { deleteYaml := filepath.Join(workDir, "working-dir", "delete", "delete-images-"+deleteId+".yaml") - By("running mirrorToMirror") - result, err := runner.MirrorToMirror(ctx, filepath.Join(iscDir, iscHappyPath), workDir, testRegistry.Endpoint(), + By("running mirrorToDisk") + result, err := runner.MirrorToDisk(ctx, filepath.Join(iscDir, iscHappyPath), workDir, "--remove-signatures=true") + expectOcMirrorCommandSuccess(result, err) + + By("verifying images are cached locally") + expectSuccessfulMirrorInLocalCache(filepath.Join(iscDir, iscHappyPath), cacheDir) + + By("running diskToMirror") + result, err = runner.DiskToMirror(ctx, filepath.Join(iscDir, iscHappyPath), workDir, testRegistry.Endpoint(), "--remove-signatures=true", "--dest-tls-verify=false") expectOcMirrorCommandSuccess(result, err) @@ -44,6 +54,115 @@ var _ = Describe("delete", func() { By("verifying local registry is empty after delete") expectEmptyRegistry(*testRegistry) + + By("verifying local cache is not deleted") + expectSuccessfulMirrorInLocalCache(filepath.Join(iscDir, iscHappyPath), cacheDir) + }) + + // CLID-253 / OCP-77693 / OCP-86931 + It("should also delete images from the local cache when --force-cache-delete=true is set", func() { + deleteYaml := filepath.Join(workDir, "working-dir", "delete", "delete-images.yaml") + cfg := parseImageSetConfig(filepath.Join(iscDir, iscHappyPath)) + nonCatalogRepos := collectExpectedNonCatalogRepos(cfg) + + By("running mirrorToDisk") + result, err := runner.MirrorToDisk(ctx, filepath.Join(iscDir, iscHappyPath), workDir, "--remove-signatures=true") + expectOcMirrorCommandSuccess(result, err) + + By("verifying images are cached locally") + expectSuccessfulMirrorInLocalCache(filepath.Join(iscDir, iscHappyPath), cacheDir) + + By("running diskToMirror") + result, err = runner.DiskToMirror(ctx, filepath.Join(iscDir, iscHappyPath), workDir, testRegistry.Endpoint(), + "--remove-signatures=true", "--dest-tls-verify=false") + expectOcMirrorCommandSuccess(result, err) + + By("generating the delete image list") + result, err = runner.DeletePhaseOne(ctx, filepath.Join(iscDir, discHappyPath), workDir, "", testRegistry.Endpoint()) + expectOcMirrorCommandSuccess(result, err) + + By("verifying delete images files are correct") + expectValidDeleteImagesFiles(workDir, "") + + By("executing the delete with --force-cache-delete=true") + result, err = runner.DeletePhaseTwo(ctx, deleteYaml, testRegistry.Endpoint(), + "--dest-tls-verify=false", "--force-cache-delete=true") + expectOcMirrorCommandSuccess(result, err) + + By("verifying local registry is empty after delete") + expectEmptyRegistry(*testRegistry) + + By("verifying non-catalog images were also removed from the local cache") + expectRepositoriesAbsentFromLocalCache(cacheDir, nonCatalogRepos) }) }) }) + +// collectExpectedNonCatalogRepos returns the expected repos from an ImageSetConfig, excluding +// operator catalog repos. Catalog images are intentionally left behind by delete, both with and +// without --force-cache-delete, as already reflected in expectEmptyRegistry's catalog exception. +func collectExpectedNonCatalogRepos(cfg ImageSetConfiguration) []string { + catalogs := sets.New[string]() + for _, op := range cfg.Mirror.Operators { + catalogs.Insert(extractRepositoryName(op.Catalog)) + } + + var nonCatalog []string + for _, repo := range collectExpectedRepos(cfg) { + if !catalogs.Has(repo) { + nonCatalog = append(nonCatalog, repo) + } + } + return nonCatalog +} + +// expectRepositoriesAbsentFromLocalCache verifies that none of the given repositories have any +// remaining tags in the oc-mirror local cache, e.g. after running delete with +// --force-cache-delete=true. Deletion untags manifests but does not immediately garbage-collect +// the underlying blobs/manifests from disk (the same way registry deletion works, see +// expectEmptyRegistry), so this checks for tags rather than mere directory presence. +func expectRepositoriesAbsentFromLocalCache(cacheDir string, expected []string) { + repos, err := listLocalCacheTaggedRepositories(cacheDir) + Expect(err).NotTo(HaveOccurred()) + + for _, exp := range expected { + for _, repo := range repos { + Expect(repo).NotTo(ContainSubstring(exp), + "repository %q unexpectedly still has tags in local cache (%s)", exp, repo) + } + } +} + +// listLocalCacheTaggedRepositories walks /docker/registry/v2/repositories/ and +// returns the repository paths that still have at least one tag under _manifests/tags/. Unlike +// listLocalCacheRepositories, this reflects whether a repository is still reachable by tag, +// regardless of whether its blobs/manifests have been garbage-collected from disk yet. +func listLocalCacheTaggedRepositories(cacheDir string) ([]string, error) { + reposRoot := filepath.Join(cacheDir, cacheRepositoriesSubdir) + + var repos []string + err := filepath.Walk(reposRoot, func(path string, info os.FileInfo, err error) error { + if err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + if !info.IsDir() || info.Name() != "tags" || filepath.Base(filepath.Dir(path)) != "_manifests" { + return nil + } + entries, err := os.ReadDir(path) + if err != nil { + return err + } + if len(entries) > 0 { + repoPath, relErr := filepath.Rel(reposRoot, filepath.Dir(filepath.Dir(path))) + if relErr != nil { + return relErr + } + repos = append(repos, repoPath) + } + return nil + }) + return repos, err +} diff --git a/tests/integration/go.mod b/tests/integration/go.mod index 5e216d2ad..53518505c 100644 --- a/tests/integration/go.mod +++ b/tests/integration/go.mod @@ -13,6 +13,7 @@ require ( github.com/operator-framework/operator-registry v1.50.0 github.com/sirupsen/logrus v1.9.4 gopkg.in/yaml.v3 v3.0.1 + k8s.io/apimachinery v0.32.0 ) require ( @@ -126,7 +127,6 @@ require ( gopkg.in/yaml.v2 v2.4.0 // indirect gotest.tools/v3 v3.5.1 // indirect k8s.io/api v0.32.0 // indirect - k8s.io/apimachinery v0.32.0 // indirect k8s.io/client-go v0.32.0 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect