detached 446fba0

refactor(validation): add IsDigest helper and clean legacy digest entries

- Add IsDigest() helper function for clearer digest validation checks
- Use IsDigest() in service tag filtering for improved readability
- Clean up legacy digest entries from tags.json on update to prevent stale data
- Add test coverage for legacy digest cleanup functionality
commit
446fba026846cd8ea4efd9dccdfb1a77e427f00d
author
brice · · 2026-02-03 06:16 UTC
parent
1cca2cb
signature
unsigned
4 files · +59 -2
M internal/adapters/out/filesystem/manifest.go +10 -1
@@ -304,7 +304,7 @@ func (s *ManifestStorage) deleteManifestContentType(name, reference string) erro
304304 // Tags management helpers
305305
306306 func (s *ManifestStorage) updateTagsList(name, reference string) error {
307- if validation.ValidateDigest(reference) == nil {
307+ if validation.IsDigest(reference) {
308308 return nil
309309 }
310310
@@ -313,6 +313,15 @@ func (s *ManifestStorage) updateTagsList(name, reference string) error {
313313 return err
314314 }
315315
316+ // Drop any legacy digest entries before persisting
317+ filtered := make([]string, 0, len(tags))
318+ for _, tag := range tags {
319+ if !validation.IsDigest(tag) {
320+ filtered = append(filtered, tag)
321+ }
322+ }
323+ tags = filtered
324+
316325 // Add tag if not already present
317326 for _, tag := range tags {
318327 if tag == reference {
@@ -304,7 +304,7 @@ func (s *ManifestStorage) deleteManifestContentType(name, reference string) erro
304 // Tags management helpers304 // Tags management helpers
305 305
306 func (s *ManifestStorage) updateTagsList(name, reference string) error {306 func (s *ManifestStorage) updateTagsList(name, reference string) error {
307- if validation.ValidateDigest(reference) == nil {307+ if validation.IsDigest(reference) {
308 return nil308 return nil
309 }309 }
310 310
@@ -313,6 +313,15 @@ func (s *ManifestStorage) updateTagsList(name, reference string) error {
313 return err313 return err
314 }314 }
315 315
316+ // Drop any legacy digest entries before persisting
317+ filtered := make([]string, 0, len(tags))
318+ for _, tag := range tags {
319+ if !validation.IsDigest(tag) {
320+ filtered = append(filtered, tag)
321+ }
322+ }
323+ tags = filtered
324+
316 // Add tag if not already present325 // Add tag if not already present
317 for _, tag := range tags {326 for _, tag := range tags {
318 if tag == reference {327 if tag == reference {
M internal/adapters/out/filesystem/manifest_test.go +42 -0
@@ -457,3 +457,45 @@ func TestManifestStorage_CompleteWorkflow(t *testing.T) {
457457 require.NoError(t, err)
458458 assert.Contains(t, repos, "myorg/myapp")
459459 }
460+
461+func TestManifestStorage_UpdateTagsList_CleansLegacyDigestEntries(t *testing.T) {
462+ tmpDir := t.TempDir()
463+ log := testLogger()
464+
465+ storage, err := NewManifestStorage(tmpDir, log)
466+ require.NoError(t, err)
467+
468+ manifestData := []byte(`{"schemaVersion": 2}`)
469+ contentType := "application/vnd.docker.distribution.manifest.v2+json"
470+
471+ // Create initial tags
472+ err = storage.PutManifest("myapp", "v1.0", contentType, manifestData)
473+ require.NoError(t, err)
474+ err = storage.PutManifest("myapp", "v2.0", contentType, manifestData)
475+ require.NoError(t, err)
476+
477+ // Manually inject digest entries into tags.json to simulate legacy data
478+ tagsPath := filepath.Join(tmpDir, "repositories", "myapp", "tags.json")
479+
480+ // Write the contaminated tags list directly
481+ err = os.WriteFile(tagsPath, []byte(`["v1.0","v2.0","sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","sha256:a6b1c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"]`), 0600)
482+ require.NoError(t, err)
483+
484+ // Verify tags.json contains digest entries
485+ tagsBefore, _ := storage.ListTags("myapp")
486+ require.Len(t, tagsBefore, 4, "Should have 4 entries (2 tags + 2 digests)")
487+
488+ // Add a new tag - this should trigger cleanup of legacy digest entries
489+ err = storage.PutManifest("myapp", "v3.0", contentType, manifestData)
490+ require.NoError(t, err)
491+
492+ // Verify that digest entries were cleaned up
493+ tagsAfter, err := storage.ListTags("myapp")
494+ require.NoError(t, err)
495+ assert.Len(t, tagsAfter, 3, "Should have 3 entries (only tags, digests removed)")
496+ assert.Contains(t, tagsAfter, "v1.0")
497+ assert.Contains(t, tagsAfter, "v2.0")
498+ assert.Contains(t, tagsAfter, "v3.0")
499+ assert.NotContains(t, tagsAfter, "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
500+ assert.NotContains(t, tagsAfter, "sha256:a6b1c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
501+}
@@ -457,3 +457,45 @@ func TestManifestStorage_CompleteWorkflow(t *testing.T) {
457 require.NoError(t, err)457 require.NoError(t, err)
458 assert.Contains(t, repos, "myorg/myapp")458 assert.Contains(t, repos, "myorg/myapp")
459 }459 }
460+
461+func TestManifestStorage_UpdateTagsList_CleansLegacyDigestEntries(t *testing.T) {
462+ tmpDir := t.TempDir()
463+ log := testLogger()
464+
465+ storage, err := NewManifestStorage(tmpDir, log)
466+ require.NoError(t, err)
467+
468+ manifestData := []byte(`{"schemaVersion": 2}`)
469+ contentType := "application/vnd.docker.distribution.manifest.v2+json"
470+
471+ // Create initial tags
472+ err = storage.PutManifest("myapp", "v1.0", contentType, manifestData)
473+ require.NoError(t, err)
474+ err = storage.PutManifest("myapp", "v2.0", contentType, manifestData)
475+ require.NoError(t, err)
476+
477+ // Manually inject digest entries into tags.json to simulate legacy data
478+ tagsPath := filepath.Join(tmpDir, "repositories", "myapp", "tags.json")
479+
480+ // Write the contaminated tags list directly
481+ err = os.WriteFile(tagsPath, []byte(`["v1.0","v2.0","sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","sha256:a6b1c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"]`), 0600)
482+ require.NoError(t, err)
483+
484+ // Verify tags.json contains digest entries
485+ tagsBefore, _ := storage.ListTags("myapp")
486+ require.Len(t, tagsBefore, 4, "Should have 4 entries (2 tags + 2 digests)")
487+
488+ // Add a new tag - this should trigger cleanup of legacy digest entries
489+ err = storage.PutManifest("myapp", "v3.0", contentType, manifestData)
490+ require.NoError(t, err)
491+
492+ // Verify that digest entries were cleaned up
493+ tagsAfter, err := storage.ListTags("myapp")
494+ require.NoError(t, err)
495+ assert.Len(t, tagsAfter, 3, "Should have 3 entries (only tags, digests removed)")
496+ assert.Contains(t, tagsAfter, "v1.0")
497+ assert.Contains(t, tagsAfter, "v2.0")
498+ assert.Contains(t, tagsAfter, "v3.0")
499+ assert.NotContains(t, tagsAfter, "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
500+ assert.NotContains(t, tagsAfter, "sha256:a6b1c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
501+}
M internal/usecase/registry/service.go +1 -1
@@ -253,7 +253,7 @@ func (s *Service) ListTags(ctx context.Context, name string) ([]string, error) {
253253
254254 filtered := make([]string, 0, len(tags))
255255 for _, tag := range tags {
256- if validation.ValidateDigest(tag) == nil {
256+ if validation.IsDigest(tag) {
257257 continue
258258 }
259259 filtered = append(filtered, tag)
@@ -253,7 +253,7 @@ func (s *Service) ListTags(ctx context.Context, name string) ([]string, error) {
253 253
254 filtered := make([]string, 0, len(tags))254 filtered := make([]string, 0, len(tags))
255 for _, tag := range tags {255 for _, tag := range tags {
256- if validation.ValidateDigest(tag) == nil {256+ if validation.IsDigest(tag) {
257 continue257 continue
258 }258 }
259 filtered = append(filtered, tag)259 filtered = append(filtered, tag)
M pkg/validation/path.go +6 -0
@@ -102,6 +102,12 @@ func ValidateDigest(digest string) error {
102102 return nil
103103 }
104104
105+// IsDigest checks if a string is a valid Docker content digest.
106+// Returns true if the string is a valid digest format.
107+func IsDigest(digest string) bool {
108+ return ValidateDigest(digest) == nil
109+}
110+
105111 // ValidateUUID validates a blob upload UUID.
106112 // UUIDs are server-generated but still validated for safety.
107113 func ValidateUUID(uuid string) error {
@@ -102,6 +102,12 @@ func ValidateDigest(digest string) error {
102 return nil102 return nil
103 }103 }
104 104
105+// IsDigest checks if a string is a valid Docker content digest.
106+// Returns true if the string is a valid digest format.
107+func IsDigest(digest string) bool {
108+ return ValidateDigest(digest) == nil
109+}
110+
105 // ValidateUUID validates a blob upload UUID.111 // ValidateUUID validates a blob upload UUID.
106 // UUIDs are server-generated but still validated for safety.112 // UUIDs are server-generated but still validated for safety.
107 func ValidateUUID(uuid string) error {113 func ValidateUUID(uuid string) error {

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