Merge pull request #51 from bnema/fix/rollback-tag-list
fix(registry): exclude digest references from tag list
5 files · +146 -2
M internal/adapters/out/filesystem/manifest.go +19 -0
| @@ -303,12 +303,29 @@ func (s *ManifestStorage) deleteManifestContentType(name, reference string) erro | |||
| 303 | 303 | ||
| 304 | 304 | // Tags management helpers | |
| 305 | 305 | ||
| 306 | + | // filterOutDigests removes digest entries from a tag list. | |
| 307 | + | func filterOutDigests(tags []string) []string { | |
| 308 | + | filtered := make([]string, 0, len(tags)) | |
| 309 | + | for _, tag := range tags { | |
| 310 | + | if !validation.IsDigest(tag) { | |
| 311 | + | filtered = append(filtered, tag) | |
| 312 | + | } | |
| 313 | + | } | |
| 314 | + | return filtered | |
| 315 | + | } | |
| 316 | + | ||
| 306 | 317 | func (s *ManifestStorage) updateTagsList(name, reference string) error { | |
| 318 | + | if validation.IsDigest(reference) { | |
| 319 | + | return nil | |
| 320 | + | } | |
| 321 | + | ||
| 307 | 322 | tags, err := s.ListTags(name) | |
| 308 | 323 | if err != nil { | |
| 309 | 324 | return err | |
| 310 | 325 | } | |
| 311 | 326 | ||
| 327 | + | tags = filterOutDigests(tags) | |
| 328 | + | ||
| 312 | 329 | // Add tag if not already present | |
| 313 | 330 | for _, tag := range tags { | |
| 314 | 331 | if tag == reference { | |
| @@ -326,6 +343,8 @@ func (s *ManifestStorage) removeFromTagsList(name, reference string) error { | |||
| 326 | 343 | return err | |
| 327 | 344 | } | |
| 328 | 345 | ||
| 346 | + | tags = filterOutDigests(tags) | |
| 347 | + | ||
| 329 | 348 | // Remove tag if present | |
| 330 | 349 | var newTags []string | |
| 331 | 350 | for _, tag := range tags { | |
| @@ -303,12 +303,29 @@ func (s *ManifestStorage) deleteManifestContentType(name, reference string) erro | |||||
| 303 | 303 | ||||
| 304 | // Tags management helpers | 304 | // Tags management helpers | ||
| 305 | 305 | ||||
| 306 | + | // filterOutDigests removes digest entries from a tag list. | |||
| 307 | + | func filterOutDigests(tags []string) []string { | |||
| 308 | + | filtered := make([]string, 0, len(tags)) | |||
| 309 | + | for _, tag := range tags { | |||
| 310 | + | if !validation.IsDigest(tag) { | |||
| 311 | + | filtered = append(filtered, tag) | |||
| 312 | + | } | |||
| 313 | + | } | |||
| 314 | + | return filtered | |||
| 315 | + | } | |||
| 316 | + | ||||
| 306 | func (s *ManifestStorage) updateTagsList(name, reference string) error { | 317 | func (s *ManifestStorage) updateTagsList(name, reference string) error { | ||
| 318 | + | if validation.IsDigest(reference) { | |||
| 319 | + | return nil | |||
| 320 | + | } | |||
| 321 | + | ||||
| 307 | tags, err := s.ListTags(name) | 322 | tags, err := s.ListTags(name) | ||
| 308 | if err != nil { | 323 | if err != nil { | ||
| 309 | return err | 324 | return err | ||
| 310 | } | 325 | } | ||
| 311 | 326 | ||||
| 327 | + | tags = filterOutDigests(tags) | |||
| 328 | + | ||||
| 312 | // Add tag if not already present | 329 | // Add tag if not already present | ||
| 313 | for _, tag := range tags { | 330 | for _, tag := range tags { | ||
| 314 | if tag == reference { | 331 | if tag == reference { | ||
| @@ -326,6 +343,8 @@ func (s *ManifestStorage) removeFromTagsList(name, reference string) error { | |||||
| 326 | return err | 343 | return err | ||
| 327 | } | 344 | } | ||
| 328 | 345 | ||||
| 346 | + | tags = filterOutDigests(tags) | |||
| 347 | + | ||||
| 329 | // Remove tag if present | 348 | // Remove tag if present | ||
| 330 | var newTags []string | 349 | var newTags []string | ||
| 331 | for _, tag := range tags { | 350 | for _, tag := range tags { | ||
M internal/adapters/out/filesystem/manifest_test.go +105 -0
| @@ -173,6 +173,25 @@ func TestManifestStorage_ListTags_Empty(t *testing.T) { | |||
| 173 | 173 | assert.Empty(t, tags) | |
| 174 | 174 | } | |
| 175 | 175 | ||
| 176 | + | func TestManifestStorage_ListTags_SkipsDigestReference(t *testing.T) { | |
| 177 | + | tmpDir := t.TempDir() | |
| 178 | + | log := testLogger() | |
| 179 | + | ||
| 180 | + | storage, err := NewManifestStorage(tmpDir, log) | |
| 181 | + | require.NoError(t, err) | |
| 182 | + | ||
| 183 | + | manifestData := []byte(`{"schemaVersion": 2}`) | |
| 184 | + | contentType := "application/vnd.docker.distribution.manifest.v2+json" | |
| 185 | + | ||
| 186 | + | err = storage.PutManifest("myapp", "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", contentType, manifestData) | |
| 187 | + | require.NoError(t, err) | |
| 188 | + | ||
| 189 | + | tags, err := storage.ListTags("myapp") | |
| 190 | + | require.NoError(t, err) | |
| 191 | + | ||
| 192 | + | assert.Empty(t, tags) | |
| 193 | + | } | |
| 194 | + | ||
| 176 | 195 | func TestManifestStorage_ListTags_AfterDelete(t *testing.T) { | |
| 177 | 196 | tmpDir := t.TempDir() | |
| 178 | 197 | log := testLogger() | |
| @@ -438,3 +457,89 @@ func TestManifestStorage_CompleteWorkflow(t *testing.T) { | |||
| 438 | 457 | require.NoError(t, err) | |
| 439 | 458 | assert.Contains(t, repos, "myorg/myapp") | |
| 440 | 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, err := storage.ListTags("myapp") | |
| 486 | + | require.NoError(t, err) | |
| 487 | + | require.Len(t, tagsBefore, 4, "Should have 4 entries (2 tags + 2 digests)") | |
| 488 | + | ||
| 489 | + | // Add a new tag - this should trigger cleanup of legacy digest entries | |
| 490 | + | err = storage.PutManifest("myapp", "v3.0", contentType, manifestData) | |
| 491 | + | require.NoError(t, err) | |
| 492 | + | ||
| 493 | + | // Verify that digest entries were cleaned up | |
| 494 | + | tagsAfter, err := storage.ListTags("myapp") | |
| 495 | + | require.NoError(t, err) | |
| 496 | + | assert.Len(t, tagsAfter, 3, "Should have 3 entries (only tags, digests removed)") | |
| 497 | + | assert.Contains(t, tagsAfter, "v1.0") | |
| 498 | + | assert.Contains(t, tagsAfter, "v2.0") | |
| 499 | + | assert.Contains(t, tagsAfter, "v3.0") | |
| 500 | + | assert.NotContains(t, tagsAfter, "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") | |
| 501 | + | assert.NotContains(t, tagsAfter, "sha256:a6b1c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") | |
| 502 | + | } | |
| 503 | + | ||
| 504 | + | func TestManifestStorage_RemoveFromTagsList_CleansLegacyDigestEntries(t *testing.T) { | |
| 505 | + | tmpDir := t.TempDir() | |
| 506 | + | log := testLogger() | |
| 507 | + | ||
| 508 | + | storage, err := NewManifestStorage(tmpDir, log) | |
| 509 | + | require.NoError(t, err) | |
| 510 | + | ||
| 511 | + | manifestData := []byte(`{"schemaVersion": 2}`) | |
| 512 | + | contentType := "application/vnd.docker.distribution.manifest.v2+json" | |
| 513 | + | ||
| 514 | + | // Create initial tags | |
| 515 | + | err = storage.PutManifest("myapp", "v1.0", contentType, manifestData) | |
| 516 | + | require.NoError(t, err) | |
| 517 | + | err = storage.PutManifest("myapp", "v2.0", contentType, manifestData) | |
| 518 | + | require.NoError(t, err) | |
| 519 | + | err = storage.PutManifest("myapp", "v3.0", contentType, manifestData) | |
| 520 | + | require.NoError(t, err) | |
| 521 | + | ||
| 522 | + | // Manually inject digest entries into tags.json to simulate legacy data | |
| 523 | + | tagsPath := filepath.Join(tmpDir, "repositories", "myapp", "tags.json") | |
| 524 | + | err = os.WriteFile(tagsPath, []byte(`["v1.0","v2.0","v3.0","sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","sha256:a6b1c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"]`), 0600) | |
| 525 | + | require.NoError(t, err) | |
| 526 | + | ||
| 527 | + | // Verify tags.json contains digest entries | |
| 528 | + | tagsBefore, err := storage.ListTags("myapp") | |
| 529 | + | require.NoError(t, err) | |
| 530 | + | require.Len(t, tagsBefore, 5, "Should have 5 entries (3 tags + 2 digests)") | |
| 531 | + | ||
| 532 | + | // Delete a tag - this should trigger cleanup of legacy digest entries | |
| 533 | + | err = storage.DeleteManifest("myapp", "v2.0") | |
| 534 | + | require.NoError(t, err) | |
| 535 | + | ||
| 536 | + | // Verify that digest entries were cleaned up and v2.0 was removed | |
| 537 | + | tagsAfter, err := storage.ListTags("myapp") | |
| 538 | + | require.NoError(t, err) | |
| 539 | + | assert.Len(t, tagsAfter, 2, "Should have 2 entries (only remaining tags, digests removed)") | |
| 540 | + | assert.Contains(t, tagsAfter, "v1.0") | |
| 541 | + | assert.Contains(t, tagsAfter, "v3.0") | |
| 542 | + | assert.NotContains(t, tagsAfter, "v2.0") | |
| 543 | + | assert.NotContains(t, tagsAfter, "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") | |
| 544 | + | assert.NotContains(t, tagsAfter, "sha256:a6b1c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") | |
| 545 | + | } | |
| @@ -173,6 +173,25 @@ func TestManifestStorage_ListTags_Empty(t *testing.T) { | |||||
| 173 | assert.Empty(t, tags) | 173 | assert.Empty(t, tags) | ||
| 174 | } | 174 | } | ||
| 175 | 175 | ||||
| 176 | + | func TestManifestStorage_ListTags_SkipsDigestReference(t *testing.T) { | |||
| 177 | + | tmpDir := t.TempDir() | |||
| 178 | + | log := testLogger() | |||
| 179 | + | ||||
| 180 | + | storage, err := NewManifestStorage(tmpDir, log) | |||
| 181 | + | require.NoError(t, err) | |||
| 182 | + | ||||
| 183 | + | manifestData := []byte(`{"schemaVersion": 2}`) | |||
| 184 | + | contentType := "application/vnd.docker.distribution.manifest.v2+json" | |||
| 185 | + | ||||
| 186 | + | err = storage.PutManifest("myapp", "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", contentType, manifestData) | |||
| 187 | + | require.NoError(t, err) | |||
| 188 | + | ||||
| 189 | + | tags, err := storage.ListTags("myapp") | |||
| 190 | + | require.NoError(t, err) | |||
| 191 | + | ||||
| 192 | + | assert.Empty(t, tags) | |||
| 193 | + | } | |||
| 194 | + | ||||
| 176 | func TestManifestStorage_ListTags_AfterDelete(t *testing.T) { | 195 | func TestManifestStorage_ListTags_AfterDelete(t *testing.T) { | ||
| 177 | tmpDir := t.TempDir() | 196 | tmpDir := t.TempDir() | ||
| 178 | log := testLogger() | 197 | log := testLogger() | ||
| @@ -438,3 +457,89 @@ func TestManifestStorage_CompleteWorkflow(t *testing.T) { | |||||
| 438 | require.NoError(t, err) | 457 | require.NoError(t, err) | ||
| 439 | assert.Contains(t, repos, "myorg/myapp") | 458 | assert.Contains(t, repos, "myorg/myapp") | ||
| 440 | } | 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, err := storage.ListTags("myapp") | |||
| 486 | + | require.NoError(t, err) | |||
| 487 | + | require.Len(t, tagsBefore, 4, "Should have 4 entries (2 tags + 2 digests)") | |||
| 488 | + | ||||
| 489 | + | // Add a new tag - this should trigger cleanup of legacy digest entries | |||
| 490 | + | err = storage.PutManifest("myapp", "v3.0", contentType, manifestData) | |||
| 491 | + | require.NoError(t, err) | |||
| 492 | + | ||||
| 493 | + | // Verify that digest entries were cleaned up | |||
| 494 | + | tagsAfter, err := storage.ListTags("myapp") | |||
| 495 | + | require.NoError(t, err) | |||
| 496 | + | assert.Len(t, tagsAfter, 3, "Should have 3 entries (only tags, digests removed)") | |||
| 497 | + | assert.Contains(t, tagsAfter, "v1.0") | |||
| 498 | + | assert.Contains(t, tagsAfter, "v2.0") | |||
| 499 | + | assert.Contains(t, tagsAfter, "v3.0") | |||
| 500 | + | assert.NotContains(t, tagsAfter, "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") | |||
| 501 | + | assert.NotContains(t, tagsAfter, "sha256:a6b1c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") | |||
| 502 | + | } | |||
| 503 | + | ||||
| 504 | + | func TestManifestStorage_RemoveFromTagsList_CleansLegacyDigestEntries(t *testing.T) { | |||
| 505 | + | tmpDir := t.TempDir() | |||
| 506 | + | log := testLogger() | |||
| 507 | + | ||||
| 508 | + | storage, err := NewManifestStorage(tmpDir, log) | |||
| 509 | + | require.NoError(t, err) | |||
| 510 | + | ||||
| 511 | + | manifestData := []byte(`{"schemaVersion": 2}`) | |||
| 512 | + | contentType := "application/vnd.docker.distribution.manifest.v2+json" | |||
| 513 | + | ||||
| 514 | + | // Create initial tags | |||
| 515 | + | err = storage.PutManifest("myapp", "v1.0", contentType, manifestData) | |||
| 516 | + | require.NoError(t, err) | |||
| 517 | + | err = storage.PutManifest("myapp", "v2.0", contentType, manifestData) | |||
| 518 | + | require.NoError(t, err) | |||
| 519 | + | err = storage.PutManifest("myapp", "v3.0", contentType, manifestData) | |||
| 520 | + | require.NoError(t, err) | |||
| 521 | + | ||||
| 522 | + | // Manually inject digest entries into tags.json to simulate legacy data | |||
| 523 | + | tagsPath := filepath.Join(tmpDir, "repositories", "myapp", "tags.json") | |||
| 524 | + | err = os.WriteFile(tagsPath, []byte(`["v1.0","v2.0","v3.0","sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","sha256:a6b1c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"]`), 0600) | |||
| 525 | + | require.NoError(t, err) | |||
| 526 | + | ||||
| 527 | + | // Verify tags.json contains digest entries | |||
| 528 | + | tagsBefore, err := storage.ListTags("myapp") | |||
| 529 | + | require.NoError(t, err) | |||
| 530 | + | require.Len(t, tagsBefore, 5, "Should have 5 entries (3 tags + 2 digests)") | |||
| 531 | + | ||||
| 532 | + | // Delete a tag - this should trigger cleanup of legacy digest entries | |||
| 533 | + | err = storage.DeleteManifest("myapp", "v2.0") | |||
| 534 | + | require.NoError(t, err) | |||
| 535 | + | ||||
| 536 | + | // Verify that digest entries were cleaned up and v2.0 was removed | |||
| 537 | + | tagsAfter, err := storage.ListTags("myapp") | |||
| 538 | + | require.NoError(t, err) | |||
| 539 | + | assert.Len(t, tagsAfter, 2, "Should have 2 entries (only remaining tags, digests removed)") | |||
| 540 | + | assert.Contains(t, tagsAfter, "v1.0") | |||
| 541 | + | assert.Contains(t, tagsAfter, "v3.0") | |||
| 542 | + | assert.NotContains(t, tagsAfter, "v2.0") | |||
| 543 | + | assert.NotContains(t, tagsAfter, "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") | |||
| 544 | + | assert.NotContains(t, tagsAfter, "sha256:a6b1c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") | |||
| 545 | + | } | |||
M internal/usecase/registry/service.go +10 -1
| @@ -11,6 +11,7 @@ import ( | |||
| 11 | 11 | ||
| 12 | 12 | "github.com/bnema/gordon/internal/boundaries/out" | |
| 13 | 13 | "github.com/bnema/gordon/internal/domain" | |
| 14 | + | "github.com/bnema/gordon/pkg/validation" | |
| 14 | 15 | ) | |
| 15 | 16 | ||
| 16 | 17 | // Service implements the RegistryService interface. | |
| @@ -250,7 +251,15 @@ func (s *Service) ListTags(ctx context.Context, name string) ([]string, error) { | |||
| 250 | 251 | return nil, log.WrapErr(err, "failed to list tags") | |
| 251 | 252 | } | |
| 252 | 253 | ||
| 253 | - | return tags, nil | |
| 254 | + | filtered := make([]string, 0, len(tags)) | |
| 255 | + | for _, tag := range tags { | |
| 256 | + | if validation.IsDigest(tag) { | |
| 257 | + | continue | |
| 258 | + | } | |
| 259 | + | filtered = append(filtered, tag) | |
| 260 | + | } | |
| 261 | + | ||
| 262 | + | return filtered, nil | |
| 254 | 263 | } | |
| 255 | 264 | ||
| 256 | 265 | // ListRepositories returns all repository names. | |
| @@ -11,6 +11,7 @@ import ( | |||||
| 11 | 11 | ||||
| 12 | "github.com/bnema/gordon/internal/boundaries/out" | 12 | "github.com/bnema/gordon/internal/boundaries/out" | ||
| 13 | "github.com/bnema/gordon/internal/domain" | 13 | "github.com/bnema/gordon/internal/domain" | ||
| 14 | + | "github.com/bnema/gordon/pkg/validation" | |||
| 14 | ) | 15 | ) | ||
| 15 | 16 | ||||
| 16 | // Service implements the RegistryService interface. | 17 | // Service implements the RegistryService interface. | ||
| @@ -250,7 +251,15 @@ func (s *Service) ListTags(ctx context.Context, name string) ([]string, error) { | |||||
| 250 | return nil, log.WrapErr(err, "failed to list tags") | 251 | return nil, log.WrapErr(err, "failed to list tags") | ||
| 251 | } | 252 | } | ||
| 252 | 253 | ||||
| 253 | - | return tags, nil | 254 | + | filtered := make([]string, 0, len(tags)) |
| 255 | + | for _, tag := range tags { | |||
| 256 | + | if validation.IsDigest(tag) { | |||
| 257 | + | continue | |||
| 258 | + | } | |||
| 259 | + | filtered = append(filtered, tag) | |||
| 260 | + | } | |||
| 261 | + | ||||
| 262 | + | return filtered, nil | |||
| 254 | } | 263 | } | ||
| 255 | 264 | ||||
| 256 | // ListRepositories returns all repository names. | 265 | // ListRepositories returns all repository names. | ||
M internal/usecase/registry/service_test.go +6 -1
| @@ -375,7 +375,12 @@ func TestService_ListTags_Success(t *testing.T) { | |||
| 375 | 375 | svc := NewService(blobStorage, manifestStorage, eventBus) | |
| 376 | 376 | ctx := testContext() | |
| 377 | 377 | ||
| 378 | - | manifestStorage.EXPECT().ListTags("myapp").Return([]string{"latest", "v1.0", "v2.0"}, nil) | |
| 378 | + | manifestStorage.EXPECT().ListTags("myapp").Return([]string{ | |
| 379 | + | "latest", | |
| 380 | + | "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", | |
| 381 | + | "v1.0", | |
| 382 | + | "v2.0", | |
| 383 | + | }, nil) | |
| 379 | 384 | ||
| 380 | 385 | tags, err := svc.ListTags(ctx, "myapp") | |
| 381 | 386 | ||
| @@ -375,7 +375,12 @@ func TestService_ListTags_Success(t *testing.T) { | |||||
| 375 | svc := NewService(blobStorage, manifestStorage, eventBus) | 375 | svc := NewService(blobStorage, manifestStorage, eventBus) | ||
| 376 | ctx := testContext() | 376 | ctx := testContext() | ||
| 377 | 377 | ||||
| 378 | - | manifestStorage.EXPECT().ListTags("myapp").Return([]string{"latest", "v1.0", "v2.0"}, nil) | 378 | + | manifestStorage.EXPECT().ListTags("myapp").Return([]string{ |
| 379 | + | "latest", | |||
| 380 | + | "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", | |||
| 381 | + | "v1.0", | |||
| 382 | + | "v2.0", | |||
| 383 | + | }, nil) | |||
| 379 | 384 | ||||
| 380 | tags, err := svc.ListTags(ctx, "myapp") | 385 | tags, err := svc.ListTags(ctx, "myapp") | ||
| 381 | 386 | ||||
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 { | ||