detached 3fa0d2d

feat(images): add domain prune models and filesystem retention primitives

commit
3fa0d2daa392ade49e1afabf7e4b5af69bea5cd0
author
brice · · 2026-02-08 06:11 UTC
committer
brice · · 2026-02-08 09:40 UTC
parent
e607146
signature
unsigned
8 files · +309 -0
M internal/adapters/out/filesystem/blob.go +46 -0
@@ -165,6 +165,52 @@ func (s *BlobStorage) BlobExists(digest string) bool {
165165 return err == nil
166166 }
167167
168+// ListBlobs returns all blob digests.
169+func (s *BlobStorage) ListBlobs() ([]string, error) {
170+ blobsDir := filepath.Join(s.rootDir, "blobs")
171+
172+ if _, err := os.Stat(blobsDir); err != nil {
173+ if os.IsNotExist(err) {
174+ return []string{}, nil
175+ }
176+ return nil, fmt.Errorf("failed to access blobs directory: %w", err)
177+ }
178+
179+ digests := make([]string, 0)
180+ err := filepath.Walk(blobsDir, func(path string, info os.FileInfo, err error) error {
181+ if err != nil {
182+ return err
183+ }
184+
185+ if info.IsDir() {
186+ return nil
187+ }
188+
189+ relPath, err := filepath.Rel(blobsDir, path)
190+ if err != nil {
191+ return err
192+ }
193+
194+ parts := strings.Split(relPath, string(filepath.Separator))
195+ if len(parts) < 2 {
196+ return nil
197+ }
198+
199+ digest := parts[0] + ":" + parts[len(parts)-1]
200+ if err := validation.ValidateDigest(digest); err != nil {
201+ return nil
202+ }
203+
204+ digests = append(digests, digest)
205+ return nil
206+ })
207+ if err != nil {
208+ return nil, fmt.Errorf("failed to list blobs: %w", err)
209+ }
210+
211+ return digests, nil
212+}
213+
168214 // StartBlobUpload starts a new blob upload and returns the upload UUID.
169215 func (s *BlobStorage) StartBlobUpload(name string) (string, error) {
170216 uploadID := uuid.New().String()
@@ -165,6 +165,52 @@ func (s *BlobStorage) BlobExists(digest string) bool {
165 return err == nil165 return err == nil
166 }166 }
167 167
168+// ListBlobs returns all blob digests.
169+func (s *BlobStorage) ListBlobs() ([]string, error) {
170+ blobsDir := filepath.Join(s.rootDir, "blobs")
171+
172+ if _, err := os.Stat(blobsDir); err != nil {
173+ if os.IsNotExist(err) {
174+ return []string{}, nil
175+ }
176+ return nil, fmt.Errorf("failed to access blobs directory: %w", err)
177+ }
178+
179+ digests := make([]string, 0)
180+ err := filepath.Walk(blobsDir, func(path string, info os.FileInfo, err error) error {
181+ if err != nil {
182+ return err
183+ }
184+
185+ if info.IsDir() {
186+ return nil
187+ }
188+
189+ relPath, err := filepath.Rel(blobsDir, path)
190+ if err != nil {
191+ return err
192+ }
193+
194+ parts := strings.Split(relPath, string(filepath.Separator))
195+ if len(parts) < 2 {
196+ return nil
197+ }
198+
199+ digest := parts[0] + ":" + parts[len(parts)-1]
200+ if err := validation.ValidateDigest(digest); err != nil {
201+ return nil
202+ }
203+
204+ digests = append(digests, digest)
205+ return nil
206+ })
207+ if err != nil {
208+ return nil, fmt.Errorf("failed to list blobs: %w", err)
209+ }
210+
211+ return digests, nil
212+}
213+
168 // StartBlobUpload starts a new blob upload and returns the upload UUID.214 // StartBlobUpload starts a new blob upload and returns the upload UUID.
169 func (s *BlobStorage) StartBlobUpload(name string) (string, error) {215 func (s *BlobStorage) StartBlobUpload(name string) (string, error) {
170 uploadID := uuid.New().String()216 uploadID := uuid.New().String()
M internal/adapters/out/filesystem/blob_test.go +33 -0
@@ -211,6 +211,39 @@ func TestBlobStorage_BlobExists(t *testing.T) {
211211 assert.True(t, storage.BlobExists(testDigest2))
212212 }
213213
214+func TestBlobStorage_ListBlobs(t *testing.T) {
215+ tmpDir := t.TempDir()
216+ log := testLogger()
217+
218+ storage, err := NewBlobStorage(tmpDir, log)
219+ require.NoError(t, err)
220+
221+ blobData := []byte("test blob content")
222+
223+ err = storage.PutBlob(testDigest1, bytes.NewReader(blobData), int64(len(blobData)))
224+ require.NoError(t, err)
225+
226+ err = storage.PutBlob(testDigest2, bytes.NewReader(blobData), int64(len(blobData)))
227+ require.NoError(t, err)
228+
229+ digests, err := storage.ListBlobs()
230+ require.NoError(t, err)
231+
232+ assert.ElementsMatch(t, []string{testDigest1, testDigest2}, digests)
233+}
234+
235+func TestBlobStorage_ListBlobs_Empty(t *testing.T) {
236+ tmpDir := t.TempDir()
237+ log := testLogger()
238+
239+ storage, err := NewBlobStorage(tmpDir, log)
240+ require.NoError(t, err)
241+
242+ digests, err := storage.ListBlobs()
243+ require.NoError(t, err)
244+ assert.Empty(t, digests)
245+}
246+
214247 func TestBlobStorage_StartBlobUpload(t *testing.T) {
215248 tmpDir := t.TempDir()
216249 log := testLogger()
@@ -211,6 +211,39 @@ func TestBlobStorage_BlobExists(t *testing.T) {
211 assert.True(t, storage.BlobExists(testDigest2))211 assert.True(t, storage.BlobExists(testDigest2))
212 }212 }
213 213
214+func TestBlobStorage_ListBlobs(t *testing.T) {
215+ tmpDir := t.TempDir()
216+ log := testLogger()
217+
218+ storage, err := NewBlobStorage(tmpDir, log)
219+ require.NoError(t, err)
220+
221+ blobData := []byte("test blob content")
222+
223+ err = storage.PutBlob(testDigest1, bytes.NewReader(blobData), int64(len(blobData)))
224+ require.NoError(t, err)
225+
226+ err = storage.PutBlob(testDigest2, bytes.NewReader(blobData), int64(len(blobData)))
227+ require.NoError(t, err)
228+
229+ digests, err := storage.ListBlobs()
230+ require.NoError(t, err)
231+
232+ assert.ElementsMatch(t, []string{testDigest1, testDigest2}, digests)
233+}
234+
235+func TestBlobStorage_ListBlobs_Empty(t *testing.T) {
236+ tmpDir := t.TempDir()
237+ log := testLogger()
238+
239+ storage, err := NewBlobStorage(tmpDir, log)
240+ require.NoError(t, err)
241+
242+ digests, err := storage.ListBlobs()
243+ require.NoError(t, err)
244+ assert.Empty(t, digests)
245+}
246+
214 func TestBlobStorage_StartBlobUpload(t *testing.T) {247 func TestBlobStorage_StartBlobUpload(t *testing.T) {
215 tmpDir := t.TempDir()248 tmpDir := t.TempDir()
216 log := testLogger()249 log := testLogger()
M internal/adapters/out/filesystem/manifest.go +19 -0
@@ -5,6 +5,7 @@ import (
55 "fmt"
66 "os"
77 "path/filepath"
8+ "time"
89
910 "github.com/bnema/zerowrap"
1011
@@ -222,6 +223,24 @@ func (s *ManifestStorage) ListRepositories() ([]string, error) {
222223 return repositories, nil
223224 }
224225
226+// GetManifestModTime returns the manifest modification time.
227+func (s *ManifestStorage) GetManifestModTime(name, reference string) (time.Time, error) {
228+ manifestPath, err := s.getManifestPath(name, reference)
229+ if err != nil {
230+ return time.Time{}, fmt.Errorf("invalid path: %w", err)
231+ }
232+
233+ info, err := os.Stat(manifestPath)
234+ if err != nil {
235+ if os.IsNotExist(err) {
236+ return time.Time{}, fmt.Errorf("manifest not found: %s/%s", name, reference)
237+ }
238+ return time.Time{}, fmt.Errorf("failed to stat manifest: %w", err)
239+ }
240+
241+ return info.ModTime(), nil
242+}
243+
225244 // Helper methods for path generation with security validation
226245
227246 func (s *ManifestStorage) getManifestPath(name, reference string) (string, error) {
@@ -5,6 +5,7 @@ import (
5 "fmt"5 "fmt"
6 "os"6 "os"
7 "path/filepath"7 "path/filepath"
8+ "time"
8 9
9 "github.com/bnema/zerowrap"10 "github.com/bnema/zerowrap"
10 11
@@ -222,6 +223,24 @@ func (s *ManifestStorage) ListRepositories() ([]string, error) {
222 return repositories, nil223 return repositories, nil
223 }224 }
224 225
226+// GetManifestModTime returns the manifest modification time.
227+func (s *ManifestStorage) GetManifestModTime(name, reference string) (time.Time, error) {
228+ manifestPath, err := s.getManifestPath(name, reference)
229+ if err != nil {
230+ return time.Time{}, fmt.Errorf("invalid path: %w", err)
231+ }
232+
233+ info, err := os.Stat(manifestPath)
234+ if err != nil {
235+ if os.IsNotExist(err) {
236+ return time.Time{}, fmt.Errorf("manifest not found: %s/%s", name, reference)
237+ }
238+ return time.Time{}, fmt.Errorf("failed to stat manifest: %w", err)
239+ }
240+
241+ return info.ModTime(), nil
242+}
243+
225 // Helper methods for path generation with security validation244 // Helper methods for path generation with security validation
226 245
227 func (s *ManifestStorage) getManifestPath(name, reference string) (string, error) {246 func (s *ManifestStorage) getManifestPath(name, reference string) (string, error) {
M internal/adapters/out/filesystem/manifest_test.go +37 -0
@@ -4,6 +4,7 @@ import (
44 "os"
55 "path/filepath"
66 "testing"
7+ "time"
78
89 "github.com/stretchr/testify/assert"
910 "github.com/stretchr/testify/require"
@@ -67,6 +68,42 @@ func TestManifestStorage_GetManifest_NotFound(t *testing.T) {
6768 assert.Contains(t, err.Error(), "manifest not found")
6869 }
6970
71+func TestManifestStorage_GetManifestModTime(t *testing.T) {
72+ tmpDir := t.TempDir()
73+ log := testLogger()
74+
75+ storage, err := NewManifestStorage(tmpDir, log)
76+ require.NoError(t, err)
77+
78+ manifestData := []byte(`{"schemaVersion": 2}`)
79+ contentType := "application/vnd.docker.distribution.manifest.v2+json"
80+
81+ err = storage.PutManifest("myapp", "latest", contentType, manifestData)
82+ require.NoError(t, err)
83+
84+ modTime, err := storage.GetManifestModTime("myapp", "latest")
85+ require.NoError(t, err)
86+
87+ manifestPath := filepath.Join(tmpDir, "repositories", "myapp", "manifests", "latest")
88+ info, err := os.Stat(manifestPath)
89+ require.NoError(t, err)
90+
91+ assert.WithinDuration(t, info.ModTime(), modTime, time.Second)
92+}
93+
94+func TestManifestStorage_GetManifestModTime_NotFound(t *testing.T) {
95+ tmpDir := t.TempDir()
96+ log := testLogger()
97+
98+ storage, err := NewManifestStorage(tmpDir, log)
99+ require.NoError(t, err)
100+
101+ _, err = storage.GetManifestModTime("notexists", "latest")
102+
103+ assert.Error(t, err)
104+ assert.Contains(t, err.Error(), "manifest not found")
105+}
106+
70107 func TestManifestStorage_GetManifest_FallbackContentType(t *testing.T) {
71108 tmpDir := t.TempDir()
72109 log := testLogger()
@@ -4,6 +4,7 @@ import (
4 "os"4 "os"
5 "path/filepath"5 "path/filepath"
6 "testing"6 "testing"
7+ "time"
7 8
8 "github.com/stretchr/testify/assert"9 "github.com/stretchr/testify/assert"
9 "github.com/stretchr/testify/require"10 "github.com/stretchr/testify/require"
@@ -67,6 +68,42 @@ func TestManifestStorage_GetManifest_NotFound(t *testing.T) {
67 assert.Contains(t, err.Error(), "manifest not found")68 assert.Contains(t, err.Error(), "manifest not found")
68 }69 }
69 70
71+func TestManifestStorage_GetManifestModTime(t *testing.T) {
72+ tmpDir := t.TempDir()
73+ log := testLogger()
74+
75+ storage, err := NewManifestStorage(tmpDir, log)
76+ require.NoError(t, err)
77+
78+ manifestData := []byte(`{"schemaVersion": 2}`)
79+ contentType := "application/vnd.docker.distribution.manifest.v2+json"
80+
81+ err = storage.PutManifest("myapp", "latest", contentType, manifestData)
82+ require.NoError(t, err)
83+
84+ modTime, err := storage.GetManifestModTime("myapp", "latest")
85+ require.NoError(t, err)
86+
87+ manifestPath := filepath.Join(tmpDir, "repositories", "myapp", "manifests", "latest")
88+ info, err := os.Stat(manifestPath)
89+ require.NoError(t, err)
90+
91+ assert.WithinDuration(t, info.ModTime(), modTime, time.Second)
92+}
93+
94+func TestManifestStorage_GetManifestModTime_NotFound(t *testing.T) {
95+ tmpDir := t.TempDir()
96+ log := testLogger()
97+
98+ storage, err := NewManifestStorage(tmpDir, log)
99+ require.NoError(t, err)
100+
101+ _, err = storage.GetManifestModTime("notexists", "latest")
102+
103+ assert.Error(t, err)
104+ assert.Contains(t, err.Error(), "manifest not found")
105+}
106+
70 func TestManifestStorage_GetManifest_FallbackContentType(t *testing.T) {107 func TestManifestStorage_GetManifest_FallbackContentType(t *testing.T) {
71 tmpDir := t.TempDir()108 tmpDir := t.TempDir()
72 log := testLogger()109 log := testLogger()
M internal/boundaries/out/mocks/mock_blob_storage.go +55 -0
@@ -503,6 +503,61 @@ func (_c *MockBlobStorage_GetBlobUpload_Call) RunAndReturn(run func(uuid string)
503503 return _c
504504 }
505505
506+// ListBlobs provides a mock function for the type MockBlobStorage
507+func (_mock *MockBlobStorage) ListBlobs() ([]string, error) {
508+ ret := _mock.Called()
509+
510+ if len(ret) == 0 {
511+ panic("no return value specified for ListBlobs")
512+ }
513+
514+ var r0 []string
515+ var r1 error
516+ if returnFunc, ok := ret.Get(0).(func() ([]string, error)); ok {
517+ return returnFunc()
518+ }
519+ if returnFunc, ok := ret.Get(0).(func() []string); ok {
520+ r0 = returnFunc()
521+ } else {
522+ if ret.Get(0) != nil {
523+ r0 = ret.Get(0).([]string)
524+ }
525+ }
526+ if returnFunc, ok := ret.Get(1).(func() error); ok {
527+ r1 = returnFunc()
528+ } else {
529+ r1 = ret.Error(1)
530+ }
531+ return r0, r1
532+}
533+
534+// MockBlobStorage_ListBlobs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListBlobs'
535+type MockBlobStorage_ListBlobs_Call struct {
536+ *mock.Call
537+}
538+
539+// ListBlobs is a helper method to define mock.On call
540+func (_e *MockBlobStorage_Expecter) ListBlobs() *MockBlobStorage_ListBlobs_Call {
541+ return &MockBlobStorage_ListBlobs_Call{Call: _e.mock.On("ListBlobs")}
542+}
543+
544+func (_c *MockBlobStorage_ListBlobs_Call) Run(run func()) *MockBlobStorage_ListBlobs_Call {
545+ _c.Call.Run(func(args mock.Arguments) {
546+ run()
547+ })
548+ return _c
549+}
550+
551+func (_c *MockBlobStorage_ListBlobs_Call) Return(strings []string, err error) *MockBlobStorage_ListBlobs_Call {
552+ _c.Call.Return(strings, err)
553+ return _c
554+}
555+
556+func (_c *MockBlobStorage_ListBlobs_Call) RunAndReturn(run func() ([]string, error)) *MockBlobStorage_ListBlobs_Call {
557+ _c.Call.Return(run)
558+ return _c
559+}
560+
506561 // PutBlob provides a mock function for the type MockBlobStorage
507562 func (_mock *MockBlobStorage) PutBlob(digest string, data io.Reader, size int64) error {
508563 ret := _mock.Called(digest, data, size)
@@ -503,6 +503,61 @@ func (_c *MockBlobStorage_GetBlobUpload_Call) RunAndReturn(run func(uuid string)
503 return _c503 return _c
504 }504 }
505 505
506+// ListBlobs provides a mock function for the type MockBlobStorage
507+func (_mock *MockBlobStorage) ListBlobs() ([]string, error) {
508+ ret := _mock.Called()
509+
510+ if len(ret) == 0 {
511+ panic("no return value specified for ListBlobs")
512+ }
513+
514+ var r0 []string
515+ var r1 error
516+ if returnFunc, ok := ret.Get(0).(func() ([]string, error)); ok {
517+ return returnFunc()
518+ }
519+ if returnFunc, ok := ret.Get(0).(func() []string); ok {
520+ r0 = returnFunc()
521+ } else {
522+ if ret.Get(0) != nil {
523+ r0 = ret.Get(0).([]string)
524+ }
525+ }
526+ if returnFunc, ok := ret.Get(1).(func() error); ok {
527+ r1 = returnFunc()
528+ } else {
529+ r1 = ret.Error(1)
530+ }
531+ return r0, r1
532+}
533+
534+// MockBlobStorage_ListBlobs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListBlobs'
535+type MockBlobStorage_ListBlobs_Call struct {
536+ *mock.Call
537+}
538+
539+// ListBlobs is a helper method to define mock.On call
540+func (_e *MockBlobStorage_Expecter) ListBlobs() *MockBlobStorage_ListBlobs_Call {
541+ return &MockBlobStorage_ListBlobs_Call{Call: _e.mock.On("ListBlobs")}
542+}
543+
544+func (_c *MockBlobStorage_ListBlobs_Call) Run(run func()) *MockBlobStorage_ListBlobs_Call {
545+ _c.Call.Run(func(args mock.Arguments) {
546+ run()
547+ })
548+ return _c
549+}
550+
551+func (_c *MockBlobStorage_ListBlobs_Call) Return(strings []string, err error) *MockBlobStorage_ListBlobs_Call {
552+ _c.Call.Return(strings, err)
553+ return _c
554+}
555+
556+func (_c *MockBlobStorage_ListBlobs_Call) RunAndReturn(run func() ([]string, error)) *MockBlobStorage_ListBlobs_Call {
557+ _c.Call.Return(run)
558+ return _c
559+}
560+
506 // PutBlob provides a mock function for the type MockBlobStorage561 // PutBlob provides a mock function for the type MockBlobStorage
507 func (_mock *MockBlobStorage) PutBlob(digest string, data io.Reader, size int64) error {562 func (_mock *MockBlobStorage) PutBlob(digest string, data io.Reader, size int64) error {
508 ret := _mock.Called(digest, data, size)563 ret := _mock.Called(digest, data, size)
M internal/boundaries/out/mocks/mock_manifest_storage.go +68 -0
@@ -5,6 +5,8 @@
55 package mocks
66
77 import (
8+ "time"
9+
810 mock "github.com/stretchr/testify/mock"
911 )
1012
@@ -166,6 +168,72 @@ func (_c *MockManifestStorage_GetManifest_Call) RunAndReturn(run func(name strin
166168 return _c
167169 }
168170
171+// GetManifestModTime provides a mock function for the type MockManifestStorage
172+func (_mock *MockManifestStorage) GetManifestModTime(name string, reference string) (time.Time, error) {
173+ ret := _mock.Called(name, reference)
174+
175+ if len(ret) == 0 {
176+ panic("no return value specified for GetManifestModTime")
177+ }
178+
179+ var r0 time.Time
180+ var r1 error
181+ if returnFunc, ok := ret.Get(0).(func(string, string) (time.Time, error)); ok {
182+ return returnFunc(name, reference)
183+ }
184+ if returnFunc, ok := ret.Get(0).(func(string, string) time.Time); ok {
185+ r0 = returnFunc(name, reference)
186+ } else {
187+ r0 = ret.Get(0).(time.Time)
188+ }
189+ if returnFunc, ok := ret.Get(1).(func(string, string) error); ok {
190+ r1 = returnFunc(name, reference)
191+ } else {
192+ r1 = ret.Error(1)
193+ }
194+ return r0, r1
195+}
196+
197+// MockManifestStorage_GetManifestModTime_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetManifestModTime'
198+type MockManifestStorage_GetManifestModTime_Call struct {
199+ *mock.Call
200+}
201+
202+// GetManifestModTime is a helper method to define mock.On call
203+// - name string
204+// - reference string
205+func (_e *MockManifestStorage_Expecter) GetManifestModTime(name interface{}, reference interface{}) *MockManifestStorage_GetManifestModTime_Call {
206+ return &MockManifestStorage_GetManifestModTime_Call{Call: _e.mock.On("GetManifestModTime", name, reference)}
207+}
208+
209+func (_c *MockManifestStorage_GetManifestModTime_Call) Run(run func(name string, reference string)) *MockManifestStorage_GetManifestModTime_Call {
210+ _c.Call.Run(func(args mock.Arguments) {
211+ var arg0 string
212+ if args[0] != nil {
213+ arg0 = args[0].(string)
214+ }
215+ var arg1 string
216+ if args[1] != nil {
217+ arg1 = args[1].(string)
218+ }
219+ run(
220+ arg0,
221+ arg1,
222+ )
223+ })
224+ return _c
225+}
226+
227+func (_c *MockManifestStorage_GetManifestModTime_Call) Return(time1 time.Time, err error) *MockManifestStorage_GetManifestModTime_Call {
228+ _c.Call.Return(time1, err)
229+ return _c
230+}
231+
232+func (_c *MockManifestStorage_GetManifestModTime_Call) RunAndReturn(run func(name string, reference string) (time.Time, error)) *MockManifestStorage_GetManifestModTime_Call {
233+ _c.Call.Return(run)
234+ return _c
235+}
236+
169237 // ListRepositories provides a mock function for the type MockManifestStorage
170238 func (_mock *MockManifestStorage) ListRepositories() ([]string, error) {
171239 ret := _mock.Called()
@@ -5,6 +5,8 @@
5 package mocks5 package mocks
6 6
7 import (7 import (
8+ "time"
9+
8 mock "github.com/stretchr/testify/mock"10 mock "github.com/stretchr/testify/mock"
9 )11 )
10 12
@@ -166,6 +168,72 @@ func (_c *MockManifestStorage_GetManifest_Call) RunAndReturn(run func(name strin
166 return _c168 return _c
167 }169 }
168 170
171+// GetManifestModTime provides a mock function for the type MockManifestStorage
172+func (_mock *MockManifestStorage) GetManifestModTime(name string, reference string) (time.Time, error) {
173+ ret := _mock.Called(name, reference)
174+
175+ if len(ret) == 0 {
176+ panic("no return value specified for GetManifestModTime")
177+ }
178+
179+ var r0 time.Time
180+ var r1 error
181+ if returnFunc, ok := ret.Get(0).(func(string, string) (time.Time, error)); ok {
182+ return returnFunc(name, reference)
183+ }
184+ if returnFunc, ok := ret.Get(0).(func(string, string) time.Time); ok {
185+ r0 = returnFunc(name, reference)
186+ } else {
187+ r0 = ret.Get(0).(time.Time)
188+ }
189+ if returnFunc, ok := ret.Get(1).(func(string, string) error); ok {
190+ r1 = returnFunc(name, reference)
191+ } else {
192+ r1 = ret.Error(1)
193+ }
194+ return r0, r1
195+}
196+
197+// MockManifestStorage_GetManifestModTime_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetManifestModTime'
198+type MockManifestStorage_GetManifestModTime_Call struct {
199+ *mock.Call
200+}
201+
202+// GetManifestModTime is a helper method to define mock.On call
203+// - name string
204+// - reference string
205+func (_e *MockManifestStorage_Expecter) GetManifestModTime(name interface{}, reference interface{}) *MockManifestStorage_GetManifestModTime_Call {
206+ return &MockManifestStorage_GetManifestModTime_Call{Call: _e.mock.On("GetManifestModTime", name, reference)}
207+}
208+
209+func (_c *MockManifestStorage_GetManifestModTime_Call) Run(run func(name string, reference string)) *MockManifestStorage_GetManifestModTime_Call {
210+ _c.Call.Run(func(args mock.Arguments) {
211+ var arg0 string
212+ if args[0] != nil {
213+ arg0 = args[0].(string)
214+ }
215+ var arg1 string
216+ if args[1] != nil {
217+ arg1 = args[1].(string)
218+ }
219+ run(
220+ arg0,
221+ arg1,
222+ )
223+ })
224+ return _c
225+}
226+
227+func (_c *MockManifestStorage_GetManifestModTime_Call) Return(time1 time.Time, err error) *MockManifestStorage_GetManifestModTime_Call {
228+ _c.Call.Return(time1, err)
229+ return _c
230+}
231+
232+func (_c *MockManifestStorage_GetManifestModTime_Call) RunAndReturn(run func(name string, reference string) (time.Time, error)) *MockManifestStorage_GetManifestModTime_Call {
233+ _c.Call.Return(run)
234+ return _c
235+}
236+
169 // ListRepositories provides a mock function for the type MockManifestStorage237 // ListRepositories provides a mock function for the type MockManifestStorage
170 func (_mock *MockManifestStorage) ListRepositories() ([]string, error) {238 func (_mock *MockManifestStorage) ListRepositories() ([]string, error) {
171 ret := _mock.Called()239 ret := _mock.Called()
M internal/boundaries/out/storage.go +7 -0
@@ -2,6 +2,7 @@ package out
22
33 import (
44 "io"
5+ "time"
56 )
67
78 // BlobStorage defines the contract for blob storage operations.
@@ -21,6 +22,9 @@ type BlobStorage interface {
2122 // BlobExists checks if a blob exists.
2223 BlobExists(digest string) bool
2324
25+ // ListBlobs returns all blob digests.
26+ ListBlobs() ([]string, error)
27+
2428 // StartBlobUpload starts a new blob upload and returns the upload UUID.
2529 StartBlobUpload(name string) (string, error)
2630
@@ -54,4 +58,7 @@ type ManifestStorage interface {
5458
5559 // ListRepositories returns all repository names.
5660 ListRepositories() ([]string, error)
61+
62+ // GetManifestModTime returns the manifest modification time.
63+ GetManifestModTime(name, reference string) (time.Time, error)
5764 }
@@ -2,6 +2,7 @@ package out
2 2
3 import (3 import (
4 "io"4 "io"
5+ "time"
5 )6 )
6 7
7 // BlobStorage defines the contract for blob storage operations.8 // BlobStorage defines the contract for blob storage operations.
@@ -21,6 +22,9 @@ type BlobStorage interface {
21 // BlobExists checks if a blob exists.22 // BlobExists checks if a blob exists.
22 BlobExists(digest string) bool23 BlobExists(digest string) bool
23 24
25+ // ListBlobs returns all blob digests.
26+ ListBlobs() ([]string, error)
27+
24 // StartBlobUpload starts a new blob upload and returns the upload UUID.28 // StartBlobUpload starts a new blob upload and returns the upload UUID.
25 StartBlobUpload(name string) (string, error)29 StartBlobUpload(name string) (string, error)
26 30
@@ -54,4 +58,7 @@ type ManifestStorage interface {
54 58
55 // ListRepositories returns all repository names.59 // ListRepositories returns all repository names.
56 ListRepositories() ([]string, error)60 ListRepositories() ([]string, error)
61+
62+ // GetManifestModTime returns the manifest modification time.
63+ GetManifestModTime(name, reference string) (time.Time, error)
57 }64 }
A internal/domain/images.go +44 -0
@@ -0,0 +1,44 @@
1+package domain
2+
3+import "time"
4+
5+// ImagePruneConfig defines scheduled image cleanup behavior.
6+type ImagePruneConfig struct {
7+ Enabled bool
8+ Schedule BackupSchedule
9+ // KeepLast is the number of most-recent tags to retain per repository.
10+ KeepLast int
11+}
12+
13+// RuntimePruneResult reports runtime image cleanup results.
14+type RuntimePruneResult struct {
15+ DeletedCount int
16+ // SpaceReclaimed is reported in bytes.
17+ SpaceReclaimed int64
18+}
19+
20+// RegistryPruneResult reports registry cleanup results.
21+type RegistryPruneResult struct {
22+ TagsRemoved int
23+ BlobsRemoved int
24+ // SpaceReclaimed is reported in bytes.
25+ SpaceReclaimed int64
26+}
27+
28+// ImagePruneReport aggregates runtime and registry cleanup results.
29+type ImagePruneReport struct {
30+ Runtime RuntimePruneResult
31+ Registry RegistryPruneResult
32+}
33+
34+// ImageInfo describes a runtime image.
35+type ImageInfo struct {
36+ Repository string
37+ Tag string
38+ // Size is reported in bytes.
39+ Size int64
40+ Created time.Time
41+ // ID is the runtime image identifier (for example, image ID/digest).
42+ ID string
43+ Dangling bool
44+}
@@ -0,0 +1,44 @@
1+package domain
2+
3+import "time"
4+
5+// ImagePruneConfig defines scheduled image cleanup behavior.
6+type ImagePruneConfig struct {
7+ Enabled bool
8+ Schedule BackupSchedule
9+ // KeepLast is the number of most-recent tags to retain per repository.
10+ KeepLast int
11+}
12+
13+// RuntimePruneResult reports runtime image cleanup results.
14+type RuntimePruneResult struct {
15+ DeletedCount int
16+ // SpaceReclaimed is reported in bytes.
17+ SpaceReclaimed int64
18+}
19+
20+// RegistryPruneResult reports registry cleanup results.
21+type RegistryPruneResult struct {
22+ TagsRemoved int
23+ BlobsRemoved int
24+ // SpaceReclaimed is reported in bytes.
25+ SpaceReclaimed int64
26+}
27+
28+// ImagePruneReport aggregates runtime and registry cleanup results.
29+type ImagePruneReport struct {
30+ Runtime RuntimePruneResult
31+ Registry RegistryPruneResult
32+}
33+
34+// ImageInfo describes a runtime image.
35+type ImageInfo struct {
36+ Repository string
37+ Tag string
38+ // Size is reported in bytes.
39+ Size int64
40+ Created time.Time
41+ // ID is the runtime image identifier (for example, image ID/digest).
42+ ID string
43+ Dangling bool
44+}

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