detached 806527c

refactor: resolve lint findings in scheduler and image prune

commit
806527c1bc87648e098bb940747b107569af3efb
author
brice · · 2026-02-08 09:53 UTC
parent
f6bf3a9
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
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
624624 spaceReclaimed = uint64(math.MaxInt64)
625625 }
626626
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+
627632 return runtimepkg.PruneReport{
628633 DeletedIDs: deletedIDs,
629- SpaceReclaimed: int64(spaceReclaimed),
634+ SpaceReclaimed: spaceReclaimedInt,
630635 }, nil
631636 }
632637
@@ -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 }, nil635 }, 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
14191419 )
14201420 }
14211421
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.
14231423 // 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 {
14271425 return err
14281426 }
1429- select {
1430- case <-proxyReady:
1431- case err := <-errChan:
1427+ if err := waitForServerReady(proxyReady, errChan); err != nil {
14321428 return err
14331429 }
14341430 if cfg.Server.TLSEnabled {
1435- select {
1436- case <-tlsReady:
1437- case err := <-errChan:
1431+ if err := waitForServerReady(tlsReady, errChan); err != nil {
14381432 return err
14391433 }
14401434 }
@@ -1447,30 +1441,61 @@ func runServers(ctx context.Context, v *viper.Viper, cfg Config, registryHandler
14471441 }
14481442 logEvent.Msg("Gordon is running")
14491443
1450- backupScheduler, err := startBackupScheduler(ctx, cfg, svc, log)
1444+ schedulerCleanup, err := startOptionalSchedulers(ctx, cfg, svc, log, v)
14511445 if err != nil {
14521446 return err
14531447 }
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+ }
14541476 if backupScheduler != nil {
1455- defer backupScheduler.Stop()
1477+ schedulers = append(schedulers, backupScheduler)
14561478 }
14571479
14581480 imageScheduler, err := startImagePruneScheduler(ctx, cfg, svc, log, func() int {
14591481 return v.GetInt("images.prune.keep_last")
14601482 })
14611483 if err != nil {
1462- return err
1484+ return nil, err
14631485 }
14641486 if imageScheduler != nil {
1465- defer imageScheduler.Stop()
1487+ schedulers = append(schedulers, imageScheduler)
14661488 }
14671489
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+ }
14701493
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
14741499 }
14751500
14761501 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 err1425 return err
1428 }1426 }
1429- select {1427+ if err := waitForServerReady(proxyReady, errChan); err != nil {
1430- case <-proxyReady:
1431- case err := <-errChan:
1432 return err1428 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 err1432 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 err1446 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 err1484+ 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 nil1496+ 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
134134 report := domain.ImagePruneReport{}
135135
136136 for _, repository := range repositories {
137- tags, err := s.manifestStorage.ListTags(repository)
137+ tagInfos, err := s.loadRepositoryTagInfos(repository)
138138 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")
140140 }
141-
142- if len(tags) == 0 {
141+ if len(tagInfos) == 0 {
143142 continue
144143 }
145144
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")
178149 }
150+ report.Registry.TagsRemoved += removed
179151
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
188154 }
189155 }
190156
@@ -206,6 +172,78 @@ func (s *Service) PruneRegistry(ctx context.Context, keepLast int) (domain.Image
206172 return report, nil
207173 }
208174
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+
209247 // Prune runs runtime prune and registry prune and aggregates reports.
210248 func (s *Service) Prune(ctx context.Context, keepLast int) (domain.ImagePruneReport, error) {
211249 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 continue142 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, nil172 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{

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close