refactor: resolve lint findings in scheduler and image prune
- commit
- 2dfdfc9296030362fb110f82dcc85159d9837c47
- author
- brice · · 2026-02-08 09:53 UTC
- parent
- 9316975
- signature
- unsigned
3 files · +132 -64
M internal/adapters/out/docker/runtime.go +6 -1
| @@ -624,9 +624,14 @@ func (r *Runtime) PruneImages(ctx context.Context, danglingOnly bool) (runtimepk | |||
| 624 | 624 | spaceReclaimed = uint64(math.MaxInt64) | |
| 625 | 625 | } | |
| 626 | 626 | ||
| 627 | + | spaceReclaimedInt, err := strconv.ParseInt(strconv.FormatUint(spaceReclaimed, 10), 10, 64) | |
| 628 | + | if err != nil { | |
| 629 | + | return runtimepkg.PruneReport{}, log.WrapErr(err, "failed to map space reclaimed to int64") | |
| 630 | + | } | |
| 631 | + | ||
| 627 | 632 | return runtimepkg.PruneReport{ | |
| 628 | 633 | DeletedIDs: deletedIDs, | |
| 629 | - | SpaceReclaimed: int64(spaceReclaimed), | |
| 634 | + | SpaceReclaimed: spaceReclaimedInt, | |
| 630 | 635 | }, nil | |
| 631 | 636 | } | |
| 632 | 637 | ||
| @@ -624,9 +624,14 @@ func (r *Runtime) PruneImages(ctx context.Context, danglingOnly bool) (runtimepk | |||||
| 624 | spaceReclaimed = uint64(math.MaxInt64) | 624 | spaceReclaimed = uint64(math.MaxInt64) | ||
| 625 | } | 625 | } | ||
| 626 | 626 | ||||
| 627 | + | spaceReclaimedInt, err := strconv.ParseInt(strconv.FormatUint(spaceReclaimed, 10), 10, 64) | |||
| 628 | + | if err != nil { | |||
| 629 | + | return runtimepkg.PruneReport{}, log.WrapErr(err, "failed to map space reclaimed to int64") | |||
| 630 | + | } | |||
| 631 | + | ||||
| 627 | return runtimepkg.PruneReport{ | 632 | return runtimepkg.PruneReport{ | ||
| 628 | DeletedIDs: deletedIDs, | 633 | DeletedIDs: deletedIDs, | ||
| 629 | - | SpaceReclaimed: int64(spaceReclaimed), | 634 | + | SpaceReclaimed: spaceReclaimedInt, |
| 630 | }, nil | 635 | }, nil | ||
| 631 | } | 636 | } | ||
| 632 | 637 | ||||
M internal/app/run.go +44 -19
| @@ -1419,22 +1419,16 @@ func runServers(ctx context.Context, v *viper.Viper, cfg Config, registryHandler | |||
| 1419 | 1419 | ) | |
| 1420 | 1420 | } | |
| 1421 | 1421 | ||
| 1422 | - | // Wait for both servers to bind their ports before auto-starting containers. | |
| 1422 | + | // Wait for all enabled servers to bind their ports before auto-starting containers. | |
| 1423 | 1423 | // This prevents the race where auto-start pulls from the registry before it's listening. | |
| 1424 | - | select { | |
| 1425 | - | case <-registryReady: | |
| 1426 | - | case err := <-errChan: | |
| 1424 | + | if err := waitForServerReady(registryReady, errChan); err != nil { | |
| 1427 | 1425 | return err | |
| 1428 | 1426 | } | |
| 1429 | - | select { | |
| 1430 | - | case <-proxyReady: | |
| 1431 | - | case err := <-errChan: | |
| 1427 | + | if err := waitForServerReady(proxyReady, errChan); err != nil { | |
| 1432 | 1428 | return err | |
| 1433 | 1429 | } | |
| 1434 | 1430 | if cfg.Server.TLSEnabled { | |
| 1435 | - | select { | |
| 1436 | - | case <-tlsReady: | |
| 1437 | - | case err := <-errChan: | |
| 1431 | + | if err := waitForServerReady(tlsReady, errChan); err != nil { | |
| 1438 | 1432 | return err | |
| 1439 | 1433 | } | |
| 1440 | 1434 | } | |
| @@ -1447,30 +1441,61 @@ func runServers(ctx context.Context, v *viper.Viper, cfg Config, registryHandler | |||
| 1447 | 1441 | } | |
| 1448 | 1442 | logEvent.Msg("Gordon is running") | |
| 1449 | 1443 | ||
| 1450 | - | backupScheduler, err := startBackupScheduler(ctx, cfg, svc, log) | |
| 1444 | + | schedulerCleanup, err := startOptionalSchedulers(ctx, cfg, svc, log, v) | |
| 1451 | 1445 | if err != nil { | |
| 1452 | 1446 | return err | |
| 1453 | 1447 | } | |
| 1448 | + | if schedulerCleanup != nil { | |
| 1449 | + | defer schedulerCleanup() | |
| 1450 | + | } | |
| 1451 | + | ||
| 1452 | + | // Auto-start after servers are listening (registry port is now bound). | |
| 1453 | + | syncAndAutoStart(ctx, svc, log) | |
| 1454 | + | ||
| 1455 | + | waitForShutdown(ctx, errChan, reloadChan, deployChan, eventBus, log) | |
| 1456 | + | gracefulShutdown(registrySrv, proxySrv, containerSvc, log) | |
| 1457 | + | return nil | |
| 1458 | + | } | |
| 1459 | + | ||
| 1460 | + | func waitForServerReady(ready <-chan struct{}, errChan <-chan error) error { | |
| 1461 | + | select { | |
| 1462 | + | case <-ready: | |
| 1463 | + | return nil | |
| 1464 | + | case err := <-errChan: | |
| 1465 | + | return err | |
| 1466 | + | } | |
| 1467 | + | } | |
| 1468 | + | ||
| 1469 | + | func startOptionalSchedulers(ctx context.Context, cfg Config, svc *services, log zerowrap.Logger, v *viper.Viper) (func(), error) { | |
| 1470 | + | schedulers := make([]*cronSvc.Scheduler, 0, 2) | |
| 1471 | + | ||
| 1472 | + | backupScheduler, err := startBackupScheduler(ctx, cfg, svc, log) | |
| 1473 | + | if err != nil { | |
| 1474 | + | return nil, err | |
| 1475 | + | } | |
| 1454 | 1476 | if backupScheduler != nil { | |
| 1455 | - | defer backupScheduler.Stop() | |
| 1477 | + | schedulers = append(schedulers, backupScheduler) | |
| 1456 | 1478 | } | |
| 1457 | 1479 | ||
| 1458 | 1480 | imageScheduler, err := startImagePruneScheduler(ctx, cfg, svc, log, func() int { | |
| 1459 | 1481 | return v.GetInt("images.prune.keep_last") | |
| 1460 | 1482 | }) | |
| 1461 | 1483 | if err != nil { | |
| 1462 | - | return err | |
| 1484 | + | return nil, err | |
| 1463 | 1485 | } | |
| 1464 | 1486 | if imageScheduler != nil { | |
| 1465 | - | defer imageScheduler.Stop() | |
| 1487 | + | schedulers = append(schedulers, imageScheduler) | |
| 1466 | 1488 | } | |
| 1467 | 1489 | ||
| 1468 | - | // Auto-start after servers are listening (registry port is now bound). | |
| 1469 | - | syncAndAutoStart(ctx, svc, log) | |
| 1490 | + | if len(schedulers) == 0 { | |
| 1491 | + | return nil, nil | |
| 1492 | + | } | |
| 1470 | 1493 | ||
| 1471 | - | waitForShutdown(ctx, errChan, reloadChan, deployChan, eventBus, log) | |
| 1472 | - | gracefulShutdown(registrySrv, proxySrv, containerSvc, log) | |
| 1473 | - | return nil | |
| 1494 | + | return func() { | |
| 1495 | + | for i := len(schedulers) - 1; i >= 0; i-- { | |
| 1496 | + | schedulers[i].Stop() | |
| 1497 | + | } | |
| 1498 | + | }, nil | |
| 1474 | 1499 | } | |
| 1475 | 1500 | ||
| 1476 | 1501 | func startBackupScheduler(ctx context.Context, cfg Config, svc *services, log zerowrap.Logger) (*cronSvc.Scheduler, error) { | |
| @@ -1419,22 +1419,16 @@ func runServers(ctx context.Context, v *viper.Viper, cfg Config, registryHandler | |||||
| 1419 | ) | 1419 | ) | ||
| 1420 | } | 1420 | } | ||
| 1421 | 1421 | ||||
| 1422 | - | // Wait for both servers to bind their ports before auto-starting containers. | 1422 | + | // Wait for all enabled servers to bind their ports before auto-starting containers. |
| 1423 | // This prevents the race where auto-start pulls from the registry before it's listening. | 1423 | // This prevents the race where auto-start pulls from the registry before it's listening. | ||
| 1424 | - | select { | 1424 | + | if err := waitForServerReady(registryReady, errChan); err != nil { |
| 1425 | - | case <-registryReady: | |||
| 1426 | - | case err := <-errChan: | |||
| 1427 | return err | 1425 | return err | ||
| 1428 | } | 1426 | } | ||
| 1429 | - | select { | 1427 | + | if err := waitForServerReady(proxyReady, errChan); err != nil { |
| 1430 | - | case <-proxyReady: | |||
| 1431 | - | case err := <-errChan: | |||
| 1432 | return err | 1428 | return err | ||
| 1433 | } | 1429 | } | ||
| 1434 | if cfg.Server.TLSEnabled { | 1430 | if cfg.Server.TLSEnabled { | ||
| 1435 | - | select { | 1431 | + | if err := waitForServerReady(tlsReady, errChan); err != nil { |
| 1436 | - | case <-tlsReady: | |||
| 1437 | - | case err := <-errChan: | |||
| 1438 | return err | 1432 | return err | ||
| 1439 | } | 1433 | } | ||
| 1440 | } | 1434 | } | ||
| @@ -1447,30 +1441,61 @@ func runServers(ctx context.Context, v *viper.Viper, cfg Config, registryHandler | |||||
| 1447 | } | 1441 | } | ||
| 1448 | logEvent.Msg("Gordon is running") | 1442 | logEvent.Msg("Gordon is running") | ||
| 1449 | 1443 | ||||
| 1450 | - | backupScheduler, err := startBackupScheduler(ctx, cfg, svc, log) | 1444 | + | schedulerCleanup, err := startOptionalSchedulers(ctx, cfg, svc, log, v) |
| 1451 | if err != nil { | 1445 | if err != nil { | ||
| 1452 | return err | 1446 | return err | ||
| 1453 | } | 1447 | } | ||
| 1448 | + | if schedulerCleanup != nil { | |||
| 1449 | + | defer schedulerCleanup() | |||
| 1450 | + | } | |||
| 1451 | + | ||||
| 1452 | + | // Auto-start after servers are listening (registry port is now bound). | |||
| 1453 | + | syncAndAutoStart(ctx, svc, log) | |||
| 1454 | + | ||||
| 1455 | + | waitForShutdown(ctx, errChan, reloadChan, deployChan, eventBus, log) | |||
| 1456 | + | gracefulShutdown(registrySrv, proxySrv, containerSvc, log) | |||
| 1457 | + | return nil | |||
| 1458 | + | } | |||
| 1459 | + | ||||
| 1460 | + | func waitForServerReady(ready <-chan struct{}, errChan <-chan error) error { | |||
| 1461 | + | select { | |||
| 1462 | + | case <-ready: | |||
| 1463 | + | return nil | |||
| 1464 | + | case err := <-errChan: | |||
| 1465 | + | return err | |||
| 1466 | + | } | |||
| 1467 | + | } | |||
| 1468 | + | ||||
| 1469 | + | func startOptionalSchedulers(ctx context.Context, cfg Config, svc *services, log zerowrap.Logger, v *viper.Viper) (func(), error) { | |||
| 1470 | + | schedulers := make([]*cronSvc.Scheduler, 0, 2) | |||
| 1471 | + | ||||
| 1472 | + | backupScheduler, err := startBackupScheduler(ctx, cfg, svc, log) | |||
| 1473 | + | if err != nil { | |||
| 1474 | + | return nil, err | |||
| 1475 | + | } | |||
| 1454 | if backupScheduler != nil { | 1476 | if backupScheduler != nil { | ||
| 1455 | - | defer backupScheduler.Stop() | 1477 | + | schedulers = append(schedulers, backupScheduler) |
| 1456 | } | 1478 | } | ||
| 1457 | 1479 | ||||
| 1458 | imageScheduler, err := startImagePruneScheduler(ctx, cfg, svc, log, func() int { | 1480 | imageScheduler, err := startImagePruneScheduler(ctx, cfg, svc, log, func() int { | ||
| 1459 | return v.GetInt("images.prune.keep_last") | 1481 | return v.GetInt("images.prune.keep_last") | ||
| 1460 | }) | 1482 | }) | ||
| 1461 | if err != nil { | 1483 | if err != nil { | ||
| 1462 | - | return err | 1484 | + | return nil, err |
| 1463 | } | 1485 | } | ||
| 1464 | if imageScheduler != nil { | 1486 | if imageScheduler != nil { | ||
| 1465 | - | defer imageScheduler.Stop() | 1487 | + | schedulers = append(schedulers, imageScheduler) |
| 1466 | } | 1488 | } | ||
| 1467 | 1489 | ||||
| 1468 | - | // Auto-start after servers are listening (registry port is now bound). | 1490 | + | if len(schedulers) == 0 { |
| 1469 | - | syncAndAutoStart(ctx, svc, log) | 1491 | + | return nil, nil |
| 1492 | + | } | |||
| 1470 | 1493 | ||||
| 1471 | - | waitForShutdown(ctx, errChan, reloadChan, deployChan, eventBus, log) | 1494 | + | return func() { |
| 1472 | - | gracefulShutdown(registrySrv, proxySrv, containerSvc, log) | 1495 | + | for i := len(schedulers) - 1; i >= 0; i-- { |
| 1473 | - | return nil | 1496 | + | schedulers[i].Stop() |
| 1497 | + | } | |||
| 1498 | + | }, nil | |||
| 1474 | } | 1499 | } | ||
| 1475 | 1500 | ||||
| 1476 | func startBackupScheduler(ctx context.Context, cfg Config, svc *services, log zerowrap.Logger) (*cronSvc.Scheduler, error) { | 1501 | func startBackupScheduler(ctx context.Context, cfg Config, svc *services, log zerowrap.Logger) (*cronSvc.Scheduler, error) { | ||
M internal/usecase/images/service.go +82 -44
| @@ -134,57 +134,23 @@ func (s *Service) PruneRegistry(ctx context.Context, keepLast int) (domain.Image | |||
| 134 | 134 | report := domain.ImagePruneReport{} | |
| 135 | 135 | ||
| 136 | 136 | for _, repository := range repositories { | |
| 137 | - | tags, err := s.manifestStorage.ListTags(repository) | |
| 137 | + | tagInfos, err := s.loadRepositoryTagInfos(repository) | |
| 138 | 138 | if err != nil { | |
| 139 | - | return domain.ImagePruneReport{}, log.WrapErr(err, "failed to list repository tags") | |
| 139 | + | return domain.ImagePruneReport{}, log.WrapErr(err, "failed to load repository tags") | |
| 140 | 140 | } | |
| 141 | - | ||
| 142 | - | if len(tags) == 0 { | |
| 141 | + | if len(tagInfos) == 0 { | |
| 143 | 142 | continue | |
| 144 | 143 | } | |
| 145 | 144 | ||
| 146 | - | tagInfos := make([]registryTag, 0, len(tags)) | |
| 147 | - | for _, tag := range tags { | |
| 148 | - | modTime, err := s.manifestStorage.GetManifestModTime(repository, tag) | |
| 149 | - | if err != nil { | |
| 150 | - | return domain.ImagePruneReport{}, log.WrapErr(err, "failed to read manifest modification time") | |
| 151 | - | } | |
| 152 | - | tagInfos = append(tagInfos, registryTag{name: tag, modTime: modTime}) | |
| 153 | - | } | |
| 154 | - | ||
| 155 | - | sort.Slice(tagInfos, func(i, j int) bool { | |
| 156 | - | if tagInfos[i].modTime.Equal(tagInfos[j].modTime) { | |
| 157 | - | return tagInfos[i].name > tagInfos[j].name | |
| 158 | - | } | |
| 159 | - | return tagInfos[i].modTime.After(tagInfos[j].modTime) | |
| 160 | - | }) | |
| 161 | - | ||
| 162 | - | keptTags := make(map[string]struct{}) | |
| 163 | - | if containsTag(tagInfos, "latest") { | |
| 164 | - | keptTags["latest"] = struct{}{} | |
| 165 | - | } | |
| 166 | - | for i := 0; i < len(tagInfos) && i < keepLast; i++ { | |
| 167 | - | keptTags[tagInfos[i].name] = struct{}{} | |
| 168 | - | } | |
| 169 | - | ||
| 170 | - | for _, tag := range tagInfos { | |
| 171 | - | if _, kept := keptTags[tag.name]; kept { | |
| 172 | - | continue | |
| 173 | - | } | |
| 174 | - | if err := s.manifestStorage.DeleteManifest(repository, tag.name); err != nil { | |
| 175 | - | return domain.ImagePruneReport{}, log.WrapErr(err, "failed to delete manifest") | |
| 176 | - | } | |
| 177 | - | report.Registry.TagsRemoved++ | |
| 145 | + | keptTags := buildKeptTagSet(tagInfos, keepLast) | |
| 146 | + | removed, err := s.deleteUnkeptManifests(repository, tagInfos, keptTags) | |
| 147 | + | if err != nil { | |
| 148 | + | return domain.ImagePruneReport{}, log.WrapErr(err, "failed to delete manifest") | |
| 178 | 149 | } | |
| 150 | + | report.Registry.TagsRemoved += removed | |
| 179 | 151 | ||
| 180 | - | for _, tag := range tagInfos { | |
| 181 | - | if _, kept := keptTags[tag.name]; !kept { | |
| 182 | - | continue | |
| 183 | - | } | |
| 184 | - | ||
| 185 | - | if err := s.collectReferencedDigests(log, repository, tag.name, referencedDigests, make(map[string]struct{})); err != nil { | |
| 186 | - | return domain.ImagePruneReport{}, err | |
| 187 | - | } | |
| 152 | + | if err := s.collectKeptTagDigests(log, repository, tagInfos, keptTags, referencedDigests); err != nil { | |
| 153 | + | return domain.ImagePruneReport{}, err | |
| 188 | 154 | } | |
| 189 | 155 | } | |
| 190 | 156 | ||
| @@ -206,6 +172,78 @@ func (s *Service) PruneRegistry(ctx context.Context, keepLast int) (domain.Image | |||
| 206 | 172 | return report, nil | |
| 207 | 173 | } | |
| 208 | 174 | ||
| 175 | + | func (s *Service) loadRepositoryTagInfos(repository string) ([]registryTag, error) { | |
| 176 | + | tags, err := s.manifestStorage.ListTags(repository) | |
| 177 | + | if err != nil { | |
| 178 | + | return nil, err | |
| 179 | + | } | |
| 180 | + | ||
| 181 | + | tagInfos := make([]registryTag, 0, len(tags)) | |
| 182 | + | for _, tag := range tags { | |
| 183 | + | modTime, err := s.manifestStorage.GetManifestModTime(repository, tag) | |
| 184 | + | if err != nil { | |
| 185 | + | return nil, err | |
| 186 | + | } | |
| 187 | + | tagInfos = append(tagInfos, registryTag{name: tag, modTime: modTime}) | |
| 188 | + | } | |
| 189 | + | ||
| 190 | + | sort.Slice(tagInfos, func(i, j int) bool { | |
| 191 | + | if tagInfos[i].modTime.Equal(tagInfos[j].modTime) { | |
| 192 | + | return tagInfos[i].name > tagInfos[j].name | |
| 193 | + | } | |
| 194 | + | return tagInfos[i].modTime.After(tagInfos[j].modTime) | |
| 195 | + | }) | |
| 196 | + | ||
| 197 | + | return tagInfos, nil | |
| 198 | + | } | |
| 199 | + | ||
| 200 | + | func buildKeptTagSet(tagInfos []registryTag, keepLast int) map[string]struct{} { | |
| 201 | + | keptTags := make(map[string]struct{}) | |
| 202 | + | if containsTag(tagInfos, "latest") { | |
| 203 | + | keptTags["latest"] = struct{}{} | |
| 204 | + | } | |
| 205 | + | ||
| 206 | + | for i := 0; i < len(tagInfos) && i < keepLast; i++ { | |
| 207 | + | keptTags[tagInfos[i].name] = struct{}{} | |
| 208 | + | } | |
| 209 | + | ||
| 210 | + | return keptTags | |
| 211 | + | } | |
| 212 | + | ||
| 213 | + | func (s *Service) deleteUnkeptManifests(repository string, tagInfos []registryTag, keptTags map[string]struct{}) (int, error) { | |
| 214 | + | removed := 0 | |
| 215 | + | for _, tag := range tagInfos { | |
| 216 | + | if _, kept := keptTags[tag.name]; kept { | |
| 217 | + | continue | |
| 218 | + | } | |
| 219 | + | if err := s.manifestStorage.DeleteManifest(repository, tag.name); err != nil { | |
| 220 | + | return 0, err | |
| 221 | + | } | |
| 222 | + | removed++ | |
| 223 | + | } | |
| 224 | + | ||
| 225 | + | return removed, nil | |
| 226 | + | } | |
| 227 | + | ||
| 228 | + | func (s *Service) collectKeptTagDigests( | |
| 229 | + | log zerowrap.Logger, | |
| 230 | + | repository string, | |
| 231 | + | tagInfos []registryTag, | |
| 232 | + | keptTags map[string]struct{}, | |
| 233 | + | referencedDigests map[string]struct{}, | |
| 234 | + | ) error { | |
| 235 | + | for _, tag := range tagInfos { | |
| 236 | + | if _, kept := keptTags[tag.name]; !kept { | |
| 237 | + | continue | |
| 238 | + | } | |
| 239 | + | if err := s.collectReferencedDigests(log, repository, tag.name, referencedDigests, make(map[string]struct{})); err != nil { | |
| 240 | + | return err | |
| 241 | + | } | |
| 242 | + | } | |
| 243 | + | ||
| 244 | + | return nil | |
| 245 | + | } | |
| 246 | + | ||
| 209 | 247 | // Prune runs runtime prune and registry prune and aggregates reports. | |
| 210 | 248 | func (s *Service) Prune(ctx context.Context, keepLast int) (domain.ImagePruneReport, error) { | |
| 211 | 249 | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |
| @@ -134,57 +134,23 @@ func (s *Service) PruneRegistry(ctx context.Context, keepLast int) (domain.Image | |||||
| 134 | report := domain.ImagePruneReport{} | 134 | report := domain.ImagePruneReport{} | ||
| 135 | 135 | ||||
| 136 | for _, repository := range repositories { | 136 | for _, repository := range repositories { | ||
| 137 | - | tags, err := s.manifestStorage.ListTags(repository) | 137 | + | tagInfos, err := s.loadRepositoryTagInfos(repository) |
| 138 | if err != nil { | 138 | if err != nil { | ||
| 139 | - | return domain.ImagePruneReport{}, log.WrapErr(err, "failed to list repository tags") | 139 | + | return domain.ImagePruneReport{}, log.WrapErr(err, "failed to load repository tags") |
| 140 | } | 140 | } | ||
| 141 | - | 141 | + | if len(tagInfos) == 0 { | |
| 142 | - | if len(tags) == 0 { | |||
| 143 | continue | 142 | continue | ||
| 144 | } | 143 | } | ||
| 145 | 144 | ||||
| 146 | - | tagInfos := make([]registryTag, 0, len(tags)) | 145 | + | keptTags := buildKeptTagSet(tagInfos, keepLast) |
| 147 | - | for _, tag := range tags { | 146 | + | removed, err := s.deleteUnkeptManifests(repository, tagInfos, keptTags) |
| 148 | - | modTime, err := s.manifestStorage.GetManifestModTime(repository, tag) | 147 | + | if err != nil { |
| 149 | - | if err != nil { | 148 | + | return domain.ImagePruneReport{}, log.WrapErr(err, "failed to delete manifest") |
| 150 | - | return domain.ImagePruneReport{}, log.WrapErr(err, "failed to read manifest modification time") | |||
| 151 | - | } | |||
| 152 | - | tagInfos = append(tagInfos, registryTag{name: tag, modTime: modTime}) | |||
| 153 | - | } | |||
| 154 | - | ||||
| 155 | - | sort.Slice(tagInfos, func(i, j int) bool { | |||
| 156 | - | if tagInfos[i].modTime.Equal(tagInfos[j].modTime) { | |||
| 157 | - | return tagInfos[i].name > tagInfos[j].name | |||
| 158 | - | } | |||
| 159 | - | return tagInfos[i].modTime.After(tagInfos[j].modTime) | |||
| 160 | - | }) | |||
| 161 | - | ||||
| 162 | - | keptTags := make(map[string]struct{}) | |||
| 163 | - | if containsTag(tagInfos, "latest") { | |||
| 164 | - | keptTags["latest"] = struct{}{} | |||
| 165 | - | } | |||
| 166 | - | for i := 0; i < len(tagInfos) && i < keepLast; i++ { | |||
| 167 | - | keptTags[tagInfos[i].name] = struct{}{} | |||
| 168 | - | } | |||
| 169 | - | ||||
| 170 | - | for _, tag := range tagInfos { | |||
| 171 | - | if _, kept := keptTags[tag.name]; kept { | |||
| 172 | - | continue | |||
| 173 | - | } | |||
| 174 | - | if err := s.manifestStorage.DeleteManifest(repository, tag.name); err != nil { | |||
| 175 | - | return domain.ImagePruneReport{}, log.WrapErr(err, "failed to delete manifest") | |||
| 176 | - | } | |||
| 177 | - | report.Registry.TagsRemoved++ | |||
| 178 | } | 149 | } | ||
| 150 | + | report.Registry.TagsRemoved += removed | |||
| 179 | 151 | ||||
| 180 | - | for _, tag := range tagInfos { | 152 | + | if err := s.collectKeptTagDigests(log, repository, tagInfos, keptTags, referencedDigests); err != nil { |
| 181 | - | if _, kept := keptTags[tag.name]; !kept { | 153 | + | return domain.ImagePruneReport{}, err |
| 182 | - | continue | |||
| 183 | - | } | |||
| 184 | - | ||||
| 185 | - | if err := s.collectReferencedDigests(log, repository, tag.name, referencedDigests, make(map[string]struct{})); err != nil { | |||
| 186 | - | return domain.ImagePruneReport{}, err | |||
| 187 | - | } | |||
| 188 | } | 154 | } | ||
| 189 | } | 155 | } | ||
| 190 | 156 | ||||
| @@ -206,6 +172,78 @@ func (s *Service) PruneRegistry(ctx context.Context, keepLast int) (domain.Image | |||||
| 206 | return report, nil | 172 | return report, nil | ||
| 207 | } | 173 | } | ||
| 208 | 174 | ||||
| 175 | + | func (s *Service) loadRepositoryTagInfos(repository string) ([]registryTag, error) { | |||
| 176 | + | tags, err := s.manifestStorage.ListTags(repository) | |||
| 177 | + | if err != nil { | |||
| 178 | + | return nil, err | |||
| 179 | + | } | |||
| 180 | + | ||||
| 181 | + | tagInfos := make([]registryTag, 0, len(tags)) | |||
| 182 | + | for _, tag := range tags { | |||
| 183 | + | modTime, err := s.manifestStorage.GetManifestModTime(repository, tag) | |||
| 184 | + | if err != nil { | |||
| 185 | + | return nil, err | |||
| 186 | + | } | |||
| 187 | + | tagInfos = append(tagInfos, registryTag{name: tag, modTime: modTime}) | |||
| 188 | + | } | |||
| 189 | + | ||||
| 190 | + | sort.Slice(tagInfos, func(i, j int) bool { | |||
| 191 | + | if tagInfos[i].modTime.Equal(tagInfos[j].modTime) { | |||
| 192 | + | return tagInfos[i].name > tagInfos[j].name | |||
| 193 | + | } | |||
| 194 | + | return tagInfos[i].modTime.After(tagInfos[j].modTime) | |||
| 195 | + | }) | |||
| 196 | + | ||||
| 197 | + | return tagInfos, nil | |||
| 198 | + | } | |||
| 199 | + | ||||
| 200 | + | func buildKeptTagSet(tagInfos []registryTag, keepLast int) map[string]struct{} { | |||
| 201 | + | keptTags := make(map[string]struct{}) | |||
| 202 | + | if containsTag(tagInfos, "latest") { | |||
| 203 | + | keptTags["latest"] = struct{}{} | |||
| 204 | + | } | |||
| 205 | + | ||||
| 206 | + | for i := 0; i < len(tagInfos) && i < keepLast; i++ { | |||
| 207 | + | keptTags[tagInfos[i].name] = struct{}{} | |||
| 208 | + | } | |||
| 209 | + | ||||
| 210 | + | return keptTags | |||
| 211 | + | } | |||
| 212 | + | ||||
| 213 | + | func (s *Service) deleteUnkeptManifests(repository string, tagInfos []registryTag, keptTags map[string]struct{}) (int, error) { | |||
| 214 | + | removed := 0 | |||
| 215 | + | for _, tag := range tagInfos { | |||
| 216 | + | if _, kept := keptTags[tag.name]; kept { | |||
| 217 | + | continue | |||
| 218 | + | } | |||
| 219 | + | if err := s.manifestStorage.DeleteManifest(repository, tag.name); err != nil { | |||
| 220 | + | return 0, err | |||
| 221 | + | } | |||
| 222 | + | removed++ | |||
| 223 | + | } | |||
| 224 | + | ||||
| 225 | + | return removed, nil | |||
| 226 | + | } | |||
| 227 | + | ||||
| 228 | + | func (s *Service) collectKeptTagDigests( | |||
| 229 | + | log zerowrap.Logger, | |||
| 230 | + | repository string, | |||
| 231 | + | tagInfos []registryTag, | |||
| 232 | + | keptTags map[string]struct{}, | |||
| 233 | + | referencedDigests map[string]struct{}, | |||
| 234 | + | ) error { | |||
| 235 | + | for _, tag := range tagInfos { | |||
| 236 | + | if _, kept := keptTags[tag.name]; !kept { | |||
| 237 | + | continue | |||
| 238 | + | } | |||
| 239 | + | if err := s.collectReferencedDigests(log, repository, tag.name, referencedDigests, make(map[string]struct{})); err != nil { | |||
| 240 | + | return err | |||
| 241 | + | } | |||
| 242 | + | } | |||
| 243 | + | ||||
| 244 | + | return nil | |||
| 245 | + | } | |||
| 246 | + | ||||
| 209 | // Prune runs runtime prune and registry prune and aggregates reports. | 247 | // Prune runs runtime prune and registry prune and aggregates reports. | ||
| 210 | func (s *Service) Prune(ctx context.Context, keepLast int) (domain.ImagePruneReport, error) { | 248 | func (s *Service) Prune(ctx context.Context, keepLast int) (domain.ImagePruneReport, error) { | ||
| 211 | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | 249 | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | ||