From 66bf00a4164d8de792db55e8c47a135f0dfaf07f Mon Sep 17 00:00:00 2001
From: "James E. Blair" <jim@acmegating.com>
Date: Wed, 6 Nov 2024 14:49:09 -0800
Subject: [PATCH] Add dry-run option

This will help us validate and debug the prune command.

Change-Id: I54ba30e1963593762e1e1435bc7e67e7eb637d3e
---
 zuul_registry/main.py    | 11 +++++++----
 zuul_registry/storage.py | 16 +++++++++-------
 2 files changed, 16 insertions(+), 11 deletions(-)

diff --git a/zuul_registry/main.py b/zuul_registry/main.py
index 50d91dd..5f2dff7 100644
--- a/zuul_registry/main.py
+++ b/zuul_registry/main.py
@@ -1,5 +1,5 @@
 # Copyright 2019 Red Hat, Inc.
-# Copyright 2021 Acme Gating, LLC
+# Copyright 2021, 2024 Acme Gating, LLC
 #
 # This module is free software: you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -553,8 +553,8 @@ class RegistryServer:
         # same host/port settings.
         cherrypy.server.httpserver = None
 
-    def prune(self):
-        self.store.prune()
+    def prune(self, dry_run):
+        self.store.prune(dry_run)
 
 
 def main():
@@ -566,6 +566,9 @@ def main():
     parser.add_argument('-d', dest='debug',
                         help='Debug log level',
                         action='store_true')
+    parser.add_argument('--dry-run', dest='dry_run',
+                        help='Do not actually delete anything when pruning',
+                        action='store_true')
     parser.add_argument('command',
                         nargs='?',
                         help='Command: serve, prune',
@@ -591,7 +594,7 @@ def main():
         s.start()
         cherrypy.engine.block()
     elif args.command == 'prune':
-        s.prune()
+        s.prune(args.dry_run)
     else:
         print("Unknown command: %s", args.command)
         sys.exit(1)
diff --git a/zuul_registry/storage.py b/zuul_registry/storage.py
index 2563701..fde2bdf 100644
--- a/zuul_registry/storage.py
+++ b/zuul_registry/storage.py
@@ -258,7 +258,7 @@ class Storage:
         path = os.path.join(namespace, 'repos', repo, 'manifests')
         return self.backend.list_objects(path)
 
-    def prune(self):
+    def prune(self, dry_run):
         """Prune the registry
 
         Prune all namespaces in the registry according to configured
@@ -273,13 +273,14 @@ class Storage:
         for namespace in self.backend.list_objects(''):
             uploadpath = os.path.join(namespace.path, 'uploads/')
             for upload in self.backend.list_objects(uploadpath):
-                self._prune(upload, upload_target)
+                self._prune(upload, upload_target, dry_run)
             if not manifest_target:
                 continue
             repopath = os.path.join(namespace.path, 'repos/')
             kept_manifests = []
             for repo in self.backend.list_objects(repopath):
-                kept_manifests.extend(self._prune(repo, manifest_target))
+                kept_manifests.extend(
+                    self._prune(repo, manifest_target, dry_run))
             # mark/sweep manifest blobs
             layers = set()
             for manifest in kept_manifests:
@@ -290,7 +291,7 @@ class Storage:
             blobpath = os.path.join(namespace.path, 'blobs/')
             for blob in self.backend.list_objects(blobpath):
                 if blob.name not in layers:
-                    self._prune(blob, upload_target)
+                    self._prune(blob, upload_target, dry_run)
 
     def _get_layers_from_manifest(self, namespace, path):
         self.log.debug('Get layers %s', path)
@@ -310,14 +311,15 @@ class Storage:
             layers.append(layer['digest'])
         return layers
 
-    def _prune(self, root_obj, target):
+    def _prune(self, root_obj, target, dry_run):
         kept = []
         if root_obj.isdir:
             for obj in self.backend.list_objects(root_obj.path):
-                kept.extend(self._prune(obj, target))
+                kept.extend(self._prune(obj, target, dry_run))
         if not kept and root_obj.ctime < target:
             self.log.debug('Prune %s', root_obj.path)
-            self.backend.delete_object(root_obj.path)
+            if not dry_run:
+                self.backend.delete_object(root_obj.path)
         else:
             self.log.debug('Keep %s', root_obj.path)
             kept.append(root_obj)