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
- 72148df02b700b3613c4502d0f5011f27ede0fca
- author
- brice · · 2026-02-03 06:16 UTC
- parent
- a0d4c75
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
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 | |||
| 304 | 304 | // Tags management helpers | |
| 305 | 305 | ||
| 306 | 306 | func (s *ManifestStorage) updateTagsList(name, reference string) error { | |
| 307 | - | if validation.ValidateDigest(reference) == nil { | |
| 307 | + | if validation.IsDigest(reference) { | |
| 308 | 308 | return nil | |
| 309 | 309 | } | |
| 310 | 310 | ||
| @@ -313,6 +313,15 @@ func (s *ManifestStorage) updateTagsList(name, reference string) error { | |||
| 313 | 313 | 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 | 325 | // Add tag if not already present | |
| 317 | 326 | for _, tag := range tags { | |
| 318 | 327 | if tag == reference { | |
| @@ -304,7 +304,7 @@ func (s *ManifestStorage) deleteManifestContentType(name, reference string) erro | |||||
| 304 | // Tags management helpers | 304 | // 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 nil | 308 | return nil | ||
| 309 | } | 309 | } | ||
| 310 | 310 | ||||
| @@ -313,6 +313,15 @@ func (s *ManifestStorage) updateTagsList(name, reference string) error { | |||||
| 313 | return err | 313 | 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 present | 325 | // 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) { | |||
| 457 | 457 | require.NoError(t, err) | |
| 458 | 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 | + | } | |
| @@ -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) { | |||
| 253 | 253 | ||
| 254 | 254 | filtered := make([]string, 0, len(tags)) | |
| 255 | 255 | for _, tag := range tags { | |
| 256 | - | if validation.ValidateDigest(tag) == nil { | |
| 256 | + | if validation.IsDigest(tag) { | |
| 257 | 257 | continue | |
| 258 | 258 | } | |
| 259 | 259 | 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 | continue | 257 | 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 { | |||
| 102 | 102 | 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 | 111 | // ValidateUUID validates a blob upload UUID. | |
| 106 | 112 | // UUIDs are server-generated but still validated for safety. | |
| 107 | 113 | func ValidateUUID(uuid string) error { | |
| @@ -102,6 +102,12 @@ func ValidateDigest(digest string) error { | |||||
| 102 | return nil | 102 | 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 { | ||