fix: limit cumulative registry blob uploads
- commit
- 8488ad1a1f2e827a0f5e7922478199572e94bdd9
- author
- brice · · 2026-04-25 08:43 UTC
- parent
- 26d49b5
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
13 files · +335 -51
M internal/adapters/in/http/registry/handler.go +16 -1
| @@ -26,6 +26,8 @@ const ( | |||
| 26 | 26 | // Kept at 95MB to stay within Cloudflare's 100MB per-request limit. | |
| 27 | 27 | // Users who need larger chunks can configure it explicitly via max_blob_chunk_size. | |
| 28 | 28 | DefaultMaxBlobChunkSize = 95 * 1024 * 1024 | |
| 29 | + | // DefaultMaxBlobSize is the default maximum cumulative size for a blob upload. | |
| 30 | + | DefaultMaxBlobSize = 1024 * 1024 * 1024 | |
| 29 | 31 | ) | |
| 30 | 32 | ||
| 31 | 33 | // Handler implements the HTTP handler for Docker Registry API v2. | |
| @@ -33,6 +35,7 @@ type Handler struct { | |||
| 33 | 35 | registrySvc in.RegistryService | |
| 34 | 36 | log zerowrap.Logger | |
| 35 | 37 | maxBlobChunkSize int64 | |
| 38 | + | maxBlobSize int64 | |
| 36 | 39 | } | |
| 37 | 40 | ||
| 38 | 41 | // NewHandler creates a new registry HTTP handler. | |
| @@ -40,15 +43,21 @@ func NewHandler( | |||
| 40 | 43 | registrySvc in.RegistryService, | |
| 41 | 44 | log zerowrap.Logger, | |
| 42 | 45 | maxBlobChunkSize int64, | |
| 46 | + | maxBlobSize ...int64, | |
| 43 | 47 | ) *Handler { | |
| 44 | 48 | if maxBlobChunkSize <= 0 { | |
| 45 | 49 | maxBlobChunkSize = DefaultMaxBlobChunkSize | |
| 46 | 50 | } | |
| 51 | + | blobSizeLimit := int64(DefaultMaxBlobSize) | |
| 52 | + | if len(maxBlobSize) > 0 && maxBlobSize[0] > 0 { | |
| 53 | + | blobSizeLimit = maxBlobSize[0] | |
| 54 | + | } | |
| 47 | 55 | ||
| 48 | 56 | return &Handler{ | |
| 49 | 57 | registrySvc: registrySvc, | |
| 50 | 58 | log: log, | |
| 51 | 59 | maxBlobChunkSize: maxBlobChunkSize, | |
| 60 | + | maxBlobSize: blobSizeLimit, | |
| 52 | 61 | } | |
| 53 | 62 | } | |
| 54 | 63 | ||
| @@ -413,7 +422,7 @@ func (h *Handler) handleBlobUpload(w http.ResponseWriter, r *http.Request) { | |||
| 413 | 422 | ||
| 414 | 423 | // Stream the body directly to storage — memory usage is bounded to a small | |
| 415 | 424 | // copy buffer (~32KB) regardless of chunk size. | |
| 416 | - | length, err := h.registrySvc.AppendBlobChunk(ctx, name, uuid, r.Body) | |
| 425 | + | length, err := h.registrySvc.AppendBlobChunk(ctx, name, uuid, r.Body, r.ContentLength, h.maxBlobSize) | |
| 417 | 426 | if err != nil { | |
| 418 | 427 | var maxBytesErr *http.MaxBytesError | |
| 419 | 428 | if errors.As(err, &maxBytesErr) { | |
| @@ -421,6 +430,12 @@ func (h *Handler) handleBlobUpload(w http.ResponseWriter, r *http.Request) { | |||
| 421 | 430 | h.sendRegistryError(w, http.StatusRequestEntityTooLarge, "SIZE_INVALID", "blob chunk exceeds maximum size") | |
| 422 | 431 | return | |
| 423 | 432 | } | |
| 433 | + | if errors.Is(err, domain.ErrBlobSizeExceeded) { | |
| 434 | + | log.Warn().Int64("max_size", h.maxBlobSize).Msg("blob upload too large") | |
| 435 | + | _ = h.registrySvc.CancelUpload(ctx, uuid) | |
| 436 | + | h.sendRegistryError(w, http.StatusRequestEntityTooLarge, "SIZE_INVALID", "blob exceeds maximum size") | |
| 437 | + | return | |
| 438 | + | } | |
| 424 | 439 | log.Error().Err(err).Msg("failed to append blob chunk") | |
| 425 | 440 | h.sendRegistryError(w, http.StatusInternalServerError, "BLOB_UPLOAD_UNKNOWN", "failed to append blob chunk") | |
| 426 | 441 | return | |
| @@ -26,6 +26,8 @@ const ( | |||||
| 26 | // Kept at 95MB to stay within Cloudflare's 100MB per-request limit. | 26 | // Kept at 95MB to stay within Cloudflare's 100MB per-request limit. | ||
| 27 | // Users who need larger chunks can configure it explicitly via max_blob_chunk_size. | 27 | // Users who need larger chunks can configure it explicitly via max_blob_chunk_size. | ||
| 28 | DefaultMaxBlobChunkSize = 95 * 1024 * 1024 | 28 | DefaultMaxBlobChunkSize = 95 * 1024 * 1024 | ||
| 29 | + | // DefaultMaxBlobSize is the default maximum cumulative size for a blob upload. | |||
| 30 | + | DefaultMaxBlobSize = 1024 * 1024 * 1024 | |||
| 29 | ) | 31 | ) | ||
| 30 | 32 | ||||
| 31 | // Handler implements the HTTP handler for Docker Registry API v2. | 33 | // Handler implements the HTTP handler for Docker Registry API v2. | ||
| @@ -33,6 +35,7 @@ type Handler struct { | |||||
| 33 | registrySvc in.RegistryService | 35 | registrySvc in.RegistryService | ||
| 34 | log zerowrap.Logger | 36 | log zerowrap.Logger | ||
| 35 | maxBlobChunkSize int64 | 37 | maxBlobChunkSize int64 | ||
| 38 | + | maxBlobSize int64 | |||
| 36 | } | 39 | } | ||
| 37 | 40 | ||||
| 38 | // NewHandler creates a new registry HTTP handler. | 41 | // NewHandler creates a new registry HTTP handler. | ||
| @@ -40,15 +43,21 @@ func NewHandler( | |||||
| 40 | registrySvc in.RegistryService, | 43 | registrySvc in.RegistryService, | ||
| 41 | log zerowrap.Logger, | 44 | log zerowrap.Logger, | ||
| 42 | maxBlobChunkSize int64, | 45 | maxBlobChunkSize int64, | ||
| 46 | + | maxBlobSize ...int64, | |||
| 43 | ) *Handler { | 47 | ) *Handler { | ||
| 44 | if maxBlobChunkSize <= 0 { | 48 | if maxBlobChunkSize <= 0 { | ||
| 45 | maxBlobChunkSize = DefaultMaxBlobChunkSize | 49 | maxBlobChunkSize = DefaultMaxBlobChunkSize | ||
| 46 | } | 50 | } | ||
| 51 | + | blobSizeLimit := int64(DefaultMaxBlobSize) | |||
| 52 | + | if len(maxBlobSize) > 0 && maxBlobSize[0] > 0 { | |||
| 53 | + | blobSizeLimit = maxBlobSize[0] | |||
| 54 | + | } | |||
| 47 | 55 | ||||
| 48 | return &Handler{ | 56 | return &Handler{ | ||
| 49 | registrySvc: registrySvc, | 57 | registrySvc: registrySvc, | ||
| 50 | log: log, | 58 | log: log, | ||
| 51 | maxBlobChunkSize: maxBlobChunkSize, | 59 | maxBlobChunkSize: maxBlobChunkSize, | ||
| 60 | + | maxBlobSize: blobSizeLimit, | |||
| 52 | } | 61 | } | ||
| 53 | } | 62 | } | ||
| 54 | 63 | ||||
| @@ -413,7 +422,7 @@ func (h *Handler) handleBlobUpload(w http.ResponseWriter, r *http.Request) { | |||||
| 413 | 422 | ||||
| 414 | // Stream the body directly to storage — memory usage is bounded to a small | 423 | // Stream the body directly to storage — memory usage is bounded to a small | ||
| 415 | // copy buffer (~32KB) regardless of chunk size. | 424 | // copy buffer (~32KB) regardless of chunk size. | ||
| 416 | - | length, err := h.registrySvc.AppendBlobChunk(ctx, name, uuid, r.Body) | 425 | + | length, err := h.registrySvc.AppendBlobChunk(ctx, name, uuid, r.Body, r.ContentLength, h.maxBlobSize) |
| 417 | if err != nil { | 426 | if err != nil { | ||
| 418 | var maxBytesErr *http.MaxBytesError | 427 | var maxBytesErr *http.MaxBytesError | ||
| 419 | if errors.As(err, &maxBytesErr) { | 428 | if errors.As(err, &maxBytesErr) { | ||
| @@ -421,6 +430,12 @@ func (h *Handler) handleBlobUpload(w http.ResponseWriter, r *http.Request) { | |||||
| 421 | h.sendRegistryError(w, http.StatusRequestEntityTooLarge, "SIZE_INVALID", "blob chunk exceeds maximum size") | 430 | h.sendRegistryError(w, http.StatusRequestEntityTooLarge, "SIZE_INVALID", "blob chunk exceeds maximum size") | ||
| 422 | return | 431 | return | ||
| 423 | } | 432 | } | ||
| 433 | + | if errors.Is(err, domain.ErrBlobSizeExceeded) { | |||
| 434 | + | log.Warn().Int64("max_size", h.maxBlobSize).Msg("blob upload too large") | |||
| 435 | + | _ = h.registrySvc.CancelUpload(ctx, uuid) | |||
| 436 | + | h.sendRegistryError(w, http.StatusRequestEntityTooLarge, "SIZE_INVALID", "blob exceeds maximum size") | |||
| 437 | + | return | |||
| 438 | + | } | |||
| 424 | log.Error().Err(err).Msg("failed to append blob chunk") | 439 | log.Error().Err(err).Msg("failed to append blob chunk") | ||
| 425 | h.sendRegistryError(w, http.StatusInternalServerError, "BLOB_UPLOAD_UNKNOWN", "failed to append blob chunk") | 440 | h.sendRegistryError(w, http.StatusInternalServerError, "BLOB_UPLOAD_UNKNOWN", "failed to append blob chunk") | ||
| 426 | return | 441 | return | ||
M internal/adapters/in/http/registry/handler_test.go +80 -6
| @@ -293,7 +293,7 @@ func TestHandler_BlobUpload_PATCH(t *testing.T) { | |||
| 293 | 293 | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) | |
| 294 | 294 | ||
| 295 | 295 | chunkData := []byte("chunk content") | |
| 296 | - | registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", mock.Anything).Return(int64(len(chunkData)), nil) | |
| 296 | + | registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", mock.Anything, mock.Anything, mock.Anything).Return(int64(len(chunkData)), nil) | |
| 297 | 297 | ||
| 298 | 298 | req := httptest.NewRequest("PATCH", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000", bytes.NewReader(chunkData)) | |
| 299 | 299 | rec := httptest.NewRecorder() | |
| @@ -310,7 +310,7 @@ func TestHandler_BlobUpload_PUT_Finalize(t *testing.T) { | |||
| 310 | 310 | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) | |
| 311 | 311 | ||
| 312 | 312 | chunkData := []byte("final chunk") | |
| 313 | - | registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", mock.Anything).Return(int64(len(chunkData)), nil) | |
| 313 | + | registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", mock.Anything, mock.Anything, mock.Anything).Return(int64(len(chunkData)), nil) | |
| 314 | 314 | registrySvc.EXPECT().FinishUpload(mock.Anything, "550e8400-e29b-41d4-a716-446655440000", "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4").Return(nil) | |
| 315 | 315 | ||
| 316 | 316 | req := httptest.NewRequest("PUT", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000?digest=sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", bytes.NewReader(chunkData)) | |
| @@ -432,7 +432,7 @@ func TestHandler_BlobUpload_PATCH_ReturnsDockerUploadUUID(t *testing.T) { | |||
| 432 | 432 | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) | |
| 433 | 433 | ||
| 434 | 434 | chunkData := []byte("chunk content") | |
| 435 | - | registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", mock.Anything).Return(int64(len(chunkData)), nil) | |
| 435 | + | registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", mock.Anything, mock.Anything, mock.Anything).Return(int64(len(chunkData)), nil) | |
| 436 | 436 | ||
| 437 | 437 | req := httptest.NewRequest("PATCH", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000", bytes.NewReader(chunkData)) | |
| 438 | 438 | rec := httptest.NewRecorder() | |
| @@ -453,8 +453,8 @@ func TestHandler_BlobUpload_PATCH_RespectsConfiguredMaxBlobChunkSize(t *testing. | |||
| 453 | 453 | ||
| 454 | 454 | // The handler wraps r.Body in MaxBytesReader and passes it to AppendBlobChunk. | |
| 455 | 455 | // The mock must read the body to trigger MaxBytesError, which the handler detects. | |
| 456 | - | registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", mock.Anything). | |
| 457 | - | Run(func(_ context.Context, _ string, _ string, data io.Reader) { | |
| 456 | + | registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", mock.Anything, mock.Anything, mock.Anything). | |
| 457 | + | Run(func(_ context.Context, _ string, _ string, data io.Reader, _ int64, _ int64) { | |
| 458 | 458 | // Drain the reader to trigger MaxBytesError | |
| 459 | 459 | _, _ = io.ReadAll(data) | |
| 460 | 460 | }). | |
| @@ -473,7 +473,7 @@ func TestHandler_BlobUpload_PATCH_ReturnsRangeHeader(t *testing.T) { | |||
| 473 | 473 | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) | |
| 474 | 474 | ||
| 475 | 475 | chunkData := []byte("chunk content") | |
| 476 | - | registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", mock.Anything).Return(int64(len(chunkData)), nil) | |
| 476 | + | registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", mock.Anything, mock.Anything, mock.Anything).Return(int64(len(chunkData)), nil) | |
| 477 | 477 | ||
| 478 | 478 | req := httptest.NewRequest("PATCH", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000", bytes.NewReader(chunkData)) | |
| 479 | 479 | rec := httptest.NewRecorder() | |
| @@ -484,3 +484,77 @@ func TestHandler_BlobUpload_PATCH_ReturnsRangeHeader(t *testing.T) { | |||
| 484 | 484 | // OCI spec: Range header should reflect total received bytes | |
| 485 | 485 | assert.Equal(t, "0-12", rec.Header().Get("Range")) | |
| 486 | 486 | } | |
| 487 | + | ||
| 488 | + | func TestDefaultMaxBlobSize_Is1GB(t *testing.T) { | |
| 489 | + | assert.Equal(t, int64(1024*1024*1024), int64(DefaultMaxBlobSize)) | |
| 490 | + | } | |
| 491 | + | ||
| 492 | + | func TestHandler_BlobUpload_PATCH_MaxBlobSizeExceededReturnsSizeInvalidAndCancels(t *testing.T) { | |
| 493 | + | registrySvc := inmocks.NewMockRegistryService(t) | |
| 494 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize, 10) | |
| 495 | + | ||
| 496 | + | registrySvc.EXPECT().AppendBlobChunk( | |
| 497 | + | mock.Anything, | |
| 498 | + | "myapp", | |
| 499 | + | "550e8400-e29b-41d4-a716-446655440000", | |
| 500 | + | mock.Anything, | |
| 501 | + | int64(6), | |
| 502 | + | int64(10), | |
| 503 | + | ).Return(int64(0), domain.ErrBlobSizeExceeded) | |
| 504 | + | registrySvc.EXPECT().CancelUpload(mock.Anything, "550e8400-e29b-41d4-a716-446655440000").Return(nil) | |
| 505 | + | ||
| 506 | + | req := httptest.NewRequest("PATCH", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000", bytes.NewReader([]byte("123456"))) | |
| 507 | + | rec := httptest.NewRecorder() | |
| 508 | + | ||
| 509 | + | handler.ServeHTTP(rec, req) | |
| 510 | + | ||
| 511 | + | assert.Equal(t, http.StatusRequestEntityTooLarge, rec.Code) | |
| 512 | + | assert.Contains(t, rec.Body.String(), "SIZE_INVALID") | |
| 513 | + | } | |
| 514 | + | ||
| 515 | + | func TestHandler_BlobUpload_PATCH_ContentLengthTooLargeReturnsSizeInvalidAndCancels(t *testing.T) { | |
| 516 | + | registrySvc := inmocks.NewMockRegistryService(t) | |
| 517 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize, 10) | |
| 518 | + | ||
| 519 | + | registrySvc.EXPECT().AppendBlobChunk( | |
| 520 | + | mock.Anything, | |
| 521 | + | "myapp", | |
| 522 | + | "550e8400-e29b-41d4-a716-446655440000", | |
| 523 | + | mock.Anything, | |
| 524 | + | int64(11), | |
| 525 | + | int64(10), | |
| 526 | + | ).Return(int64(0), domain.ErrBlobSizeExceeded) | |
| 527 | + | registrySvc.EXPECT().CancelUpload(mock.Anything, "550e8400-e29b-41d4-a716-446655440000").Return(nil) | |
| 528 | + | ||
| 529 | + | req := httptest.NewRequest("PATCH", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000", bytes.NewReader([]byte("12345678901"))) | |
| 530 | + | rec := httptest.NewRecorder() | |
| 531 | + | ||
| 532 | + | handler.ServeHTTP(rec, req) | |
| 533 | + | ||
| 534 | + | assert.Equal(t, http.StatusRequestEntityTooLarge, rec.Code) | |
| 535 | + | assert.Contains(t, rec.Body.String(), "SIZE_INVALID") | |
| 536 | + | } | |
| 537 | + | ||
| 538 | + | func TestHandler_BlobUpload_PUT_FinalizeUnderMaxBlobSize(t *testing.T) { | |
| 539 | + | registrySvc := inmocks.NewMockRegistryService(t) | |
| 540 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize, 10) | |
| 541 | + | ||
| 542 | + | chunkData := []byte("1234567890") | |
| 543 | + | registrySvc.EXPECT().AppendBlobChunk( | |
| 544 | + | mock.Anything, | |
| 545 | + | "myapp", | |
| 546 | + | "550e8400-e29b-41d4-a716-446655440000", | |
| 547 | + | mock.Anything, | |
| 548 | + | int64(len(chunkData)), | |
| 549 | + | int64(10), | |
| 550 | + | ).Return(int64(len(chunkData)), nil) | |
| 551 | + | registrySvc.EXPECT().FinishUpload(mock.Anything, "550e8400-e29b-41d4-a716-446655440000", "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4").Return(nil) | |
| 552 | + | ||
| 553 | + | req := httptest.NewRequest("PUT", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000?digest=sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", bytes.NewReader(chunkData)) | |
| 554 | + | rec := httptest.NewRecorder() | |
| 555 | + | ||
| 556 | + | handler.ServeHTTP(rec, req) | |
| 557 | + | ||
| 558 | + | assert.Equal(t, http.StatusCreated, rec.Code) | |
| 559 | + | assert.Equal(t, "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", rec.Header().Get("Docker-Content-Digest")) | |
| 560 | + | } | |
| @@ -293,7 +293,7 @@ func TestHandler_BlobUpload_PATCH(t *testing.T) { | |||||
| 293 | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) | 293 | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) | ||
| 294 | 294 | ||||
| 295 | chunkData := []byte("chunk content") | 295 | chunkData := []byte("chunk content") | ||
| 296 | - | registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", mock.Anything).Return(int64(len(chunkData)), nil) | 296 | + | registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", mock.Anything, mock.Anything, mock.Anything).Return(int64(len(chunkData)), nil) |
| 297 | 297 | ||||
| 298 | req := httptest.NewRequest("PATCH", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000", bytes.NewReader(chunkData)) | 298 | req := httptest.NewRequest("PATCH", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000", bytes.NewReader(chunkData)) | ||
| 299 | rec := httptest.NewRecorder() | 299 | rec := httptest.NewRecorder() | ||
| @@ -310,7 +310,7 @@ func TestHandler_BlobUpload_PUT_Finalize(t *testing.T) { | |||||
| 310 | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) | 310 | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) | ||
| 311 | 311 | ||||
| 312 | chunkData := []byte("final chunk") | 312 | chunkData := []byte("final chunk") | ||
| 313 | - | registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", mock.Anything).Return(int64(len(chunkData)), nil) | 313 | + | registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", mock.Anything, mock.Anything, mock.Anything).Return(int64(len(chunkData)), nil) |
| 314 | registrySvc.EXPECT().FinishUpload(mock.Anything, "550e8400-e29b-41d4-a716-446655440000", "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4").Return(nil) | 314 | registrySvc.EXPECT().FinishUpload(mock.Anything, "550e8400-e29b-41d4-a716-446655440000", "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4").Return(nil) | ||
| 315 | 315 | ||||
| 316 | req := httptest.NewRequest("PUT", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000?digest=sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", bytes.NewReader(chunkData)) | 316 | req := httptest.NewRequest("PUT", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000?digest=sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", bytes.NewReader(chunkData)) | ||
| @@ -432,7 +432,7 @@ func TestHandler_BlobUpload_PATCH_ReturnsDockerUploadUUID(t *testing.T) { | |||||
| 432 | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) | 432 | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) | ||
| 433 | 433 | ||||
| 434 | chunkData := []byte("chunk content") | 434 | chunkData := []byte("chunk content") | ||
| 435 | - | registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", mock.Anything).Return(int64(len(chunkData)), nil) | 435 | + | registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", mock.Anything, mock.Anything, mock.Anything).Return(int64(len(chunkData)), nil) |
| 436 | 436 | ||||
| 437 | req := httptest.NewRequest("PATCH", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000", bytes.NewReader(chunkData)) | 437 | req := httptest.NewRequest("PATCH", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000", bytes.NewReader(chunkData)) | ||
| 438 | rec := httptest.NewRecorder() | 438 | rec := httptest.NewRecorder() | ||
| @@ -453,8 +453,8 @@ func TestHandler_BlobUpload_PATCH_RespectsConfiguredMaxBlobChunkSize(t *testing. | |||||
| 453 | 453 | ||||
| 454 | // The handler wraps r.Body in MaxBytesReader and passes it to AppendBlobChunk. | 454 | // The handler wraps r.Body in MaxBytesReader and passes it to AppendBlobChunk. | ||
| 455 | // The mock must read the body to trigger MaxBytesError, which the handler detects. | 455 | // The mock must read the body to trigger MaxBytesError, which the handler detects. | ||
| 456 | - | registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", mock.Anything). | 456 | + | registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", mock.Anything, mock.Anything, mock.Anything). |
| 457 | - | Run(func(_ context.Context, _ string, _ string, data io.Reader) { | 457 | + | Run(func(_ context.Context, _ string, _ string, data io.Reader, _ int64, _ int64) { |
| 458 | // Drain the reader to trigger MaxBytesError | 458 | // Drain the reader to trigger MaxBytesError | ||
| 459 | _, _ = io.ReadAll(data) | 459 | _, _ = io.ReadAll(data) | ||
| 460 | }). | 460 | }). | ||
| @@ -473,7 +473,7 @@ func TestHandler_BlobUpload_PATCH_ReturnsRangeHeader(t *testing.T) { | |||||
| 473 | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) | 473 | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) | ||
| 474 | 474 | ||||
| 475 | chunkData := []byte("chunk content") | 475 | chunkData := []byte("chunk content") | ||
| 476 | - | registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", mock.Anything).Return(int64(len(chunkData)), nil) | 476 | + | registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", mock.Anything, mock.Anything, mock.Anything).Return(int64(len(chunkData)), nil) |
| 477 | 477 | ||||
| 478 | req := httptest.NewRequest("PATCH", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000", bytes.NewReader(chunkData)) | 478 | req := httptest.NewRequest("PATCH", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000", bytes.NewReader(chunkData)) | ||
| 479 | rec := httptest.NewRecorder() | 479 | rec := httptest.NewRecorder() | ||
| @@ -484,3 +484,77 @@ func TestHandler_BlobUpload_PATCH_ReturnsRangeHeader(t *testing.T) { | |||||
| 484 | // OCI spec: Range header should reflect total received bytes | 484 | // OCI spec: Range header should reflect total received bytes | ||
| 485 | assert.Equal(t, "0-12", rec.Header().Get("Range")) | 485 | assert.Equal(t, "0-12", rec.Header().Get("Range")) | ||
| 486 | } | 486 | } | ||
| 487 | + | ||||
| 488 | + | func TestDefaultMaxBlobSize_Is1GB(t *testing.T) { | |||
| 489 | + | assert.Equal(t, int64(1024*1024*1024), int64(DefaultMaxBlobSize)) | |||
| 490 | + | } | |||
| 491 | + | ||||
| 492 | + | func TestHandler_BlobUpload_PATCH_MaxBlobSizeExceededReturnsSizeInvalidAndCancels(t *testing.T) { | |||
| 493 | + | registrySvc := inmocks.NewMockRegistryService(t) | |||
| 494 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize, 10) | |||
| 495 | + | ||||
| 496 | + | registrySvc.EXPECT().AppendBlobChunk( | |||
| 497 | + | mock.Anything, | |||
| 498 | + | "myapp", | |||
| 499 | + | "550e8400-e29b-41d4-a716-446655440000", | |||
| 500 | + | mock.Anything, | |||
| 501 | + | int64(6), | |||
| 502 | + | int64(10), | |||
| 503 | + | ).Return(int64(0), domain.ErrBlobSizeExceeded) | |||
| 504 | + | registrySvc.EXPECT().CancelUpload(mock.Anything, "550e8400-e29b-41d4-a716-446655440000").Return(nil) | |||
| 505 | + | ||||
| 506 | + | req := httptest.NewRequest("PATCH", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000", bytes.NewReader([]byte("123456"))) | |||
| 507 | + | rec := httptest.NewRecorder() | |||
| 508 | + | ||||
| 509 | + | handler.ServeHTTP(rec, req) | |||
| 510 | + | ||||
| 511 | + | assert.Equal(t, http.StatusRequestEntityTooLarge, rec.Code) | |||
| 512 | + | assert.Contains(t, rec.Body.String(), "SIZE_INVALID") | |||
| 513 | + | } | |||
| 514 | + | ||||
| 515 | + | func TestHandler_BlobUpload_PATCH_ContentLengthTooLargeReturnsSizeInvalidAndCancels(t *testing.T) { | |||
| 516 | + | registrySvc := inmocks.NewMockRegistryService(t) | |||
| 517 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize, 10) | |||
| 518 | + | ||||
| 519 | + | registrySvc.EXPECT().AppendBlobChunk( | |||
| 520 | + | mock.Anything, | |||
| 521 | + | "myapp", | |||
| 522 | + | "550e8400-e29b-41d4-a716-446655440000", | |||
| 523 | + | mock.Anything, | |||
| 524 | + | int64(11), | |||
| 525 | + | int64(10), | |||
| 526 | + | ).Return(int64(0), domain.ErrBlobSizeExceeded) | |||
| 527 | + | registrySvc.EXPECT().CancelUpload(mock.Anything, "550e8400-e29b-41d4-a716-446655440000").Return(nil) | |||
| 528 | + | ||||
| 529 | + | req := httptest.NewRequest("PATCH", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000", bytes.NewReader([]byte("12345678901"))) | |||
| 530 | + | rec := httptest.NewRecorder() | |||
| 531 | + | ||||
| 532 | + | handler.ServeHTTP(rec, req) | |||
| 533 | + | ||||
| 534 | + | assert.Equal(t, http.StatusRequestEntityTooLarge, rec.Code) | |||
| 535 | + | assert.Contains(t, rec.Body.String(), "SIZE_INVALID") | |||
| 536 | + | } | |||
| 537 | + | ||||
| 538 | + | func TestHandler_BlobUpload_PUT_FinalizeUnderMaxBlobSize(t *testing.T) { | |||
| 539 | + | registrySvc := inmocks.NewMockRegistryService(t) | |||
| 540 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize, 10) | |||
| 541 | + | ||||
| 542 | + | chunkData := []byte("1234567890") | |||
| 543 | + | registrySvc.EXPECT().AppendBlobChunk( | |||
| 544 | + | mock.Anything, | |||
| 545 | + | "myapp", | |||
| 546 | + | "550e8400-e29b-41d4-a716-446655440000", | |||
| 547 | + | mock.Anything, | |||
| 548 | + | int64(len(chunkData)), | |||
| 549 | + | int64(10), | |||
| 550 | + | ).Return(int64(len(chunkData)), nil) | |||
| 551 | + | registrySvc.EXPECT().FinishUpload(mock.Anything, "550e8400-e29b-41d4-a716-446655440000", "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4").Return(nil) | |||
| 552 | + | ||||
| 553 | + | req := httptest.NewRequest("PUT", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000?digest=sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", bytes.NewReader(chunkData)) | |||
| 554 | + | rec := httptest.NewRecorder() | |||
| 555 | + | ||||
| 556 | + | handler.ServeHTTP(rec, req) | |||
| 557 | + | ||||
| 558 | + | assert.Equal(t, http.StatusCreated, rec.Code) | |||
| 559 | + | assert.Equal(t, "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", rec.Header().Get("Docker-Content-Digest")) | |||
| 560 | + | } | |||
M internal/adapters/out/filesystem/blob.go +24 -2
| @@ -17,6 +17,7 @@ import ( | |||
| 17 | 17 | "github.com/bnema/zerowrap" | |
| 18 | 18 | "github.com/google/uuid" | |
| 19 | 19 | ||
| 20 | + | "github.com/bnema/gordon/internal/domain" | |
| 20 | 21 | "github.com/bnema/gordon/pkg/validation" | |
| 21 | 22 | ) | |
| 22 | 23 | ||
| @@ -254,7 +255,7 @@ func (s *BlobStorage) StartBlobUpload(name string) (string, error) { | |||
| 254 | 255 | } | |
| 255 | 256 | ||
| 256 | 257 | // AppendBlobChunk appends data to an in-progress upload. | |
| 257 | - | func (s *BlobStorage) AppendBlobChunk(name, uuid string, data io.Reader) (int64, error) { | |
| 258 | + | func (s *BlobStorage) AppendBlobChunk(name, uuid string, data io.Reader, contentLength, maxBlobSize int64) (int64, error) { | |
| 258 | 259 | uploadPath, err := s.getUploadPath(uuid) | |
| 259 | 260 | if err != nil { | |
| 260 | 261 | return 0, fmt.Errorf("invalid upload path: %w", err) | |
| @@ -274,12 +275,33 @@ func (s *BlobStorage) AppendBlobChunk(name, uuid string, data io.Reader) (int64, | |||
| 274 | 275 | } | |
| 275 | 276 | defer file.Close() | |
| 276 | 277 | ||
| 278 | + | fi, err := file.Stat() | |
| 279 | + | if err != nil { | |
| 280 | + | return 0, fmt.Errorf("failed to get file info: %w", err) | |
| 281 | + | } | |
| 282 | + | originalSize := fi.Size() | |
| 283 | + | ||
| 284 | + | if maxBlobSize > 0 { | |
| 285 | + | remaining := maxBlobSize - originalSize | |
| 286 | + | if remaining < 0 || (contentLength >= 0 && contentLength > remaining) { | |
| 287 | + | return 0, domain.ErrBlobSizeExceeded | |
| 288 | + | } | |
| 289 | + | data = io.LimitReader(data, remaining+1) | |
| 290 | + | } | |
| 291 | + | ||
| 277 | 292 | written, err := io.Copy(file, data) | |
| 278 | 293 | if err != nil { | |
| 279 | 294 | return 0, fmt.Errorf("failed to write chunk to upload file: %w", err) | |
| 280 | 295 | } | |
| 281 | 296 | ||
| 282 | - | fi, err := file.Stat() | |
| 297 | + | if maxBlobSize > 0 && originalSize+written > maxBlobSize { | |
| 298 | + | if err := file.Truncate(originalSize); err != nil { | |
| 299 | + | return 0, fmt.Errorf("failed to truncate oversized upload: %w", err) | |
| 300 | + | } | |
| 301 | + | return 0, domain.ErrBlobSizeExceeded | |
| 302 | + | } | |
| 303 | + | ||
| 304 | + | fi, err = file.Stat() | |
| 283 | 305 | if err != nil { | |
| 284 | 306 | return 0, fmt.Errorf("failed to get file info: %w", err) | |
| 285 | 307 | } | |
| @@ -17,6 +17,7 @@ import ( | |||||
| 17 | "github.com/bnema/zerowrap" | 17 | "github.com/bnema/zerowrap" | ||
| 18 | "github.com/google/uuid" | 18 | "github.com/google/uuid" | ||
| 19 | 19 | ||||
| 20 | + | "github.com/bnema/gordon/internal/domain" | |||
| 20 | "github.com/bnema/gordon/pkg/validation" | 21 | "github.com/bnema/gordon/pkg/validation" | ||
| 21 | ) | 22 | ) | ||
| 22 | 23 | ||||
| @@ -254,7 +255,7 @@ func (s *BlobStorage) StartBlobUpload(name string) (string, error) { | |||||
| 254 | } | 255 | } | ||
| 255 | 256 | ||||
| 256 | // AppendBlobChunk appends data to an in-progress upload. | 257 | // AppendBlobChunk appends data to an in-progress upload. | ||
| 257 | - | func (s *BlobStorage) AppendBlobChunk(name, uuid string, data io.Reader) (int64, error) { | 258 | + | func (s *BlobStorage) AppendBlobChunk(name, uuid string, data io.Reader, contentLength, maxBlobSize int64) (int64, error) { |
| 258 | uploadPath, err := s.getUploadPath(uuid) | 259 | uploadPath, err := s.getUploadPath(uuid) | ||
| 259 | if err != nil { | 260 | if err != nil { | ||
| 260 | return 0, fmt.Errorf("invalid upload path: %w", err) | 261 | return 0, fmt.Errorf("invalid upload path: %w", err) | ||
| @@ -274,12 +275,33 @@ func (s *BlobStorage) AppendBlobChunk(name, uuid string, data io.Reader) (int64, | |||||
| 274 | } | 275 | } | ||
| 275 | defer file.Close() | 276 | defer file.Close() | ||
| 276 | 277 | ||||
| 278 | + | fi, err := file.Stat() | |||
| 279 | + | if err != nil { | |||
| 280 | + | return 0, fmt.Errorf("failed to get file info: %w", err) | |||
| 281 | + | } | |||
| 282 | + | originalSize := fi.Size() | |||
| 283 | + | ||||
| 284 | + | if maxBlobSize > 0 { | |||
| 285 | + | remaining := maxBlobSize - originalSize | |||
| 286 | + | if remaining < 0 || (contentLength >= 0 && contentLength > remaining) { | |||
| 287 | + | return 0, domain.ErrBlobSizeExceeded | |||
| 288 | + | } | |||
| 289 | + | data = io.LimitReader(data, remaining+1) | |||
| 290 | + | } | |||
| 291 | + | ||||
| 277 | written, err := io.Copy(file, data) | 292 | written, err := io.Copy(file, data) | ||
| 278 | if err != nil { | 293 | if err != nil { | ||
| 279 | return 0, fmt.Errorf("failed to write chunk to upload file: %w", err) | 294 | return 0, fmt.Errorf("failed to write chunk to upload file: %w", err) | ||
| 280 | } | 295 | } | ||
| 281 | 296 | ||||
| 282 | - | fi, err := file.Stat() | 297 | + | if maxBlobSize > 0 && originalSize+written > maxBlobSize { |
| 298 | + | if err := file.Truncate(originalSize); err != nil { | |||
| 299 | + | return 0, fmt.Errorf("failed to truncate oversized upload: %w", err) | |||
| 300 | + | } | |||
| 301 | + | return 0, domain.ErrBlobSizeExceeded | |||
| 302 | + | } | |||
| 303 | + | ||||
| 304 | + | fi, err = file.Stat() | |||
| 283 | if err != nil { | 305 | if err != nil { | ||
| 284 | return 0, fmt.Errorf("failed to get file info: %w", err) | 306 | return 0, fmt.Errorf("failed to get file info: %w", err) | ||
| 285 | } | 307 | } | ||
M internal/adapters/out/filesystem/blob_test.go +95 -12
| @@ -4,6 +4,7 @@ import ( | |||
| 4 | 4 | "bytes" | |
| 5 | 5 | "crypto/sha256" | |
| 6 | 6 | "encoding/hex" | |
| 7 | + | "errors" | |
| 7 | 8 | "io" | |
| 8 | 9 | "os" | |
| 9 | 10 | "path/filepath" | |
| @@ -13,6 +14,8 @@ import ( | |||
| 13 | 14 | "github.com/bnema/zerowrap" | |
| 14 | 15 | "github.com/stretchr/testify/assert" | |
| 15 | 16 | "github.com/stretchr/testify/require" | |
| 17 | + | ||
| 18 | + | "github.com/bnema/gordon/internal/domain" | |
| 16 | 19 | ) | |
| 17 | 20 | ||
| 18 | 21 | const ( | |
| @@ -26,6 +29,15 @@ func testLogger() zerowrap.Logger { | |||
| 26 | 29 | return zerowrap.Default() | |
| 27 | 30 | } | |
| 28 | 31 | ||
| 32 | + | type readFailer struct { | |
| 33 | + | read bool | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | func (r *readFailer) Read(_ []byte) (int, error) { | |
| 37 | + | r.read = true | |
| 38 | + | return 0, io.ErrUnexpectedEOF | |
| 39 | + | } | |
| 40 | + | ||
| 29 | 41 | func TestNewBlobStorage(t *testing.T) { | |
| 30 | 42 | tmpDir := t.TempDir() | |
| 31 | 43 | log := testLogger() | |
| @@ -280,13 +292,13 @@ func TestBlobStorage_AppendBlobChunk(t *testing.T) { | |||
| 280 | 292 | ||
| 281 | 293 | // Append first chunk | |
| 282 | 294 | chunk1 := []byte("first chunk") | |
| 283 | - | size1, err := storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(chunk1)) | |
| 295 | + | size1, err := storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(chunk1), -1, 0) | |
| 284 | 296 | require.NoError(t, err) | |
| 285 | 297 | assert.Equal(t, int64(len(chunk1)), size1) | |
| 286 | 298 | ||
| 287 | 299 | // Append second chunk | |
| 288 | 300 | chunk2 := []byte(" second chunk") | |
| 289 | - | size2, err := storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(chunk2)) | |
| 301 | + | size2, err := storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(chunk2), -1, 0) | |
| 290 | 302 | require.NoError(t, err) | |
| 291 | 303 | assert.Equal(t, int64(len(chunk1)+len(chunk2)), size2) | |
| 292 | 304 | } | |
| @@ -298,12 +310,83 @@ func TestBlobStorage_AppendBlobChunk_NotFound(t *testing.T) { | |||
| 298 | 310 | storage, err := NewBlobStorage(tmpDir, log) | |
| 299 | 311 | require.NoError(t, err) | |
| 300 | 312 | ||
| 301 | - | _, err = storage.AppendBlobChunk("myapp", "00000000-0000-4000-a000-000000000000", bytes.NewReader([]byte("data"))) | |
| 313 | + | _, err = storage.AppendBlobChunk("myapp", "00000000-0000-4000-a000-000000000000", bytes.NewReader([]byte("data")), -1, 100) | |
| 302 | 314 | ||
| 303 | 315 | assert.Error(t, err) | |
| 304 | 316 | assert.Contains(t, err.Error(), "upload not found") | |
| 305 | 317 | } | |
| 306 | 318 | ||
| 319 | + | func TestBlobStorage_AppendBlobChunk_MaxBlobSize(t *testing.T) { | |
| 320 | + | tmpDir := t.TempDir() | |
| 321 | + | log := testLogger() | |
| 322 | + | ||
| 323 | + | storage, err := NewBlobStorage(tmpDir, log) | |
| 324 | + | require.NoError(t, err) | |
| 325 | + | ||
| 326 | + | uuid, err := storage.StartBlobUpload("myapp") | |
| 327 | + | require.NoError(t, err) | |
| 328 | + | ||
| 329 | + | size, err := storage.AppendBlobChunk("myapp", uuid, bytes.NewReader([]byte("12345")), -1, 10) | |
| 330 | + | require.NoError(t, err) | |
| 331 | + | assert.Equal(t, int64(5), size) | |
| 332 | + | ||
| 333 | + | size, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader([]byte("67890")), -1, 10) | |
| 334 | + | require.NoError(t, err) | |
| 335 | + | assert.Equal(t, int64(10), size) | |
| 336 | + | ||
| 337 | + | _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader([]byte("x")), -1, 10) | |
| 338 | + | require.Error(t, err) | |
| 339 | + | assert.True(t, errors.Is(err, domain.ErrBlobSizeExceeded)) | |
| 340 | + | ||
| 341 | + | uploadPath := filepath.Join(tmpDir, "uploads", uuid) | |
| 342 | + | info, statErr := os.Stat(uploadPath) | |
| 343 | + | require.NoError(t, statErr) | |
| 344 | + | assert.Equal(t, int64(10), info.Size()) | |
| 345 | + | } | |
| 346 | + | ||
| 347 | + | func TestBlobStorage_AppendBlobChunk_MaxBlobSizeContentLengthTooLargeDoesNotRead(t *testing.T) { | |
| 348 | + | tmpDir := t.TempDir() | |
| 349 | + | log := testLogger() | |
| 350 | + | ||
| 351 | + | storage, err := NewBlobStorage(tmpDir, log) | |
| 352 | + | require.NoError(t, err) | |
| 353 | + | ||
| 354 | + | uuid, err := storage.StartBlobUpload("myapp") | |
| 355 | + | require.NoError(t, err) | |
| 356 | + | ||
| 357 | + | _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader([]byte("12345")), 5, 10) | |
| 358 | + | require.NoError(t, err) | |
| 359 | + | ||
| 360 | + | reader := &readFailer{} | |
| 361 | + | _, err = storage.AppendBlobChunk("myapp", uuid, reader, 6, 10) | |
| 362 | + | require.Error(t, err) | |
| 363 | + | assert.ErrorIs(t, err, domain.ErrBlobSizeExceeded) | |
| 364 | + | assert.False(t, reader.read) | |
| 365 | + | } | |
| 366 | + | ||
| 367 | + | func TestBlobStorage_AppendBlobChunk_MaxBlobSizeOverflowMidReadTruncates(t *testing.T) { | |
| 368 | + | tmpDir := t.TempDir() | |
| 369 | + | log := testLogger() | |
| 370 | + | ||
| 371 | + | storage, err := NewBlobStorage(tmpDir, log) | |
| 372 | + | require.NoError(t, err) | |
| 373 | + | ||
| 374 | + | uuid, err := storage.StartBlobUpload("myapp") | |
| 375 | + | require.NoError(t, err) | |
| 376 | + | ||
| 377 | + | _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader([]byte("12345")), -1, 10) | |
| 378 | + | require.NoError(t, err) | |
| 379 | + | ||
| 380 | + | _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader([]byte("6789012345")), -1, 10) | |
| 381 | + | require.Error(t, err) | |
| 382 | + | assert.True(t, errors.Is(err, domain.ErrBlobSizeExceeded)) | |
| 383 | + | ||
| 384 | + | uploadPath := filepath.Join(tmpDir, "uploads", uuid) | |
| 385 | + | info, statErr := os.Stat(uploadPath) | |
| 386 | + | require.NoError(t, statErr) | |
| 387 | + | assert.Equal(t, int64(5), info.Size()) | |
| 388 | + | } | |
| 389 | + | ||
| 307 | 390 | func TestBlobStorage_GetBlobUpload(t *testing.T) { | |
| 308 | 391 | tmpDir := t.TempDir() | |
| 309 | 392 | log := testLogger() | |
| @@ -350,7 +433,7 @@ func TestBlobStorage_FinishBlobUpload(t *testing.T) { | |||
| 350 | 433 | ||
| 351 | 434 | // Append data - content that hashes to testDigest1 | |
| 352 | 435 | blobData := []byte("complete blob content") | |
| 353 | - | _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(blobData)) | |
| 436 | + | _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(blobData), -1, 0) | |
| 354 | 437 | require.NoError(t, err) | |
| 355 | 438 | ||
| 356 | 439 | // Finish upload - use testDigest1 (validation will pass, but hash won't match) | |
| @@ -477,7 +560,7 @@ func TestBlobStorage_CompleteUploadFlow(t *testing.T) { | |||
| 477 | 560 | ||
| 478 | 561 | var totalSize int64 | |
| 479 | 562 | for _, chunk := range chunks { | |
| 480 | - | size, err := storage.AppendBlobChunk(repoName, uuid, bytes.NewReader(chunk)) | |
| 563 | + | size, err := storage.AppendBlobChunk(repoName, uuid, bytes.NewReader(chunk), -1, 0) | |
| 481 | 564 | require.NoError(t, err) | |
| 482 | 565 | totalSize = size | |
| 483 | 566 | } | |
| @@ -501,7 +584,7 @@ func TestBlobStorage_ConcurrentAppendAndFinalize(t *testing.T) { | |||
| 501 | 584 | require.NoError(t, err) | |
| 502 | 585 | ||
| 503 | 586 | initialData := []byte("initial-") | |
| 504 | - | _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(initialData)) | |
| 587 | + | _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(initialData), -1, 0) | |
| 505 | 588 | require.NoError(t, err) | |
| 506 | 589 | ||
| 507 | 590 | appendChunk := []byte("chunk") | |
| @@ -516,7 +599,7 @@ func TestBlobStorage_ConcurrentAppendAndFinalize(t *testing.T) { | |||
| 516 | 599 | go func() { | |
| 517 | 600 | defer wg.Done() | |
| 518 | 601 | <-appendReady | |
| 519 | - | _, appendErr := storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(appendChunk)) | |
| 602 | + | _, appendErr := storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(appendChunk), -1, 0) | |
| 520 | 603 | appendErrs <- appendErr | |
| 521 | 604 | }() | |
| 522 | 605 | } | |
| @@ -570,7 +653,7 @@ func TestBlobStorage_ConcurrentAppendAndFinalize(t *testing.T) { | |||
| 570 | 653 | assert.Equal(t, fullyAppended, blobData) | |
| 571 | 654 | } | |
| 572 | 655 | ||
| 573 | - | _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader([]byte("after"))) | |
| 656 | + | _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader([]byte("after")), -1, 0) | |
| 574 | 657 | assert.Error(t, err) | |
| 575 | 658 | assert.Contains(t, err.Error(), "upload not found") | |
| 576 | 659 | } | |
| @@ -586,13 +669,13 @@ func TestBlobStorage_AppendAfterFinalize(t *testing.T) { | |||
| 586 | 669 | require.NoError(t, err) | |
| 587 | 670 | ||
| 588 | 671 | blobData := []byte("finalized content") | |
| 589 | - | _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(blobData)) | |
| 672 | + | _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(blobData), -1, 0) | |
| 590 | 673 | require.NoError(t, err) | |
| 591 | 674 | ||
| 592 | 675 | err = storage.FinishBlobUpload(uuid, digestForData(blobData)) | |
| 593 | 676 | require.NoError(t, err) | |
| 594 | 677 | ||
| 595 | - | _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader([]byte("more"))) | |
| 678 | + | _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader([]byte("more")), -1, 0) | |
| 596 | 679 | assert.Error(t, err) | |
| 597 | 680 | assert.Contains(t, err.Error(), "upload not found") | |
| 598 | 681 | } | |
| @@ -608,7 +691,7 @@ func TestBlobStorage_FailedFinalizeRetryable(t *testing.T) { | |||
| 608 | 691 | require.NoError(t, err) | |
| 609 | 692 | ||
| 610 | 693 | blobData := []byte("retryable finalize") | |
| 611 | - | _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(blobData)) | |
| 694 | + | _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(blobData), -1, 0) | |
| 612 | 695 | require.NoError(t, err) | |
| 613 | 696 | ||
| 614 | 697 | err = storage.FinishBlobUpload(uuid, testDigest1) | |
| @@ -638,7 +721,7 @@ func TestBlobStorage_SequentialFlow(t *testing.T) { | |||
| 638 | 721 | var blobData []byte | |
| 639 | 722 | for _, chunk := range chunks { | |
| 640 | 723 | blobData = append(blobData, chunk...) | |
| 641 | - | _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(chunk)) | |
| 724 | + | _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(chunk), -1, 0) | |
| 642 | 725 | require.NoError(t, err) | |
| 643 | 726 | } | |
| 644 | 727 | ||
| @@ -4,6 +4,7 @@ import ( | |||||
| 4 | "bytes" | 4 | "bytes" | ||
| 5 | "crypto/sha256" | 5 | "crypto/sha256" | ||
| 6 | "encoding/hex" | 6 | "encoding/hex" | ||
| 7 | + | "errors" | |||
| 7 | "io" | 8 | "io" | ||
| 8 | "os" | 9 | "os" | ||
| 9 | "path/filepath" | 10 | "path/filepath" | ||
| @@ -13,6 +14,8 @@ import ( | |||||
| 13 | "github.com/bnema/zerowrap" | 14 | "github.com/bnema/zerowrap" | ||
| 14 | "github.com/stretchr/testify/assert" | 15 | "github.com/stretchr/testify/assert" | ||
| 15 | "github.com/stretchr/testify/require" | 16 | "github.com/stretchr/testify/require" | ||
| 17 | + | ||||
| 18 | + | "github.com/bnema/gordon/internal/domain" | |||
| 16 | ) | 19 | ) | ||
| 17 | 20 | ||||
| 18 | const ( | 21 | const ( | ||
| @@ -26,6 +29,15 @@ func testLogger() zerowrap.Logger { | |||||
| 26 | return zerowrap.Default() | 29 | return zerowrap.Default() | ||
| 27 | } | 30 | } | ||
| 28 | 31 | ||||
| 32 | + | type readFailer struct { | |||
| 33 | + | read bool | |||
| 34 | + | } | |||
| 35 | + | ||||
| 36 | + | func (r *readFailer) Read(_ []byte) (int, error) { | |||
| 37 | + | r.read = true | |||
| 38 | + | return 0, io.ErrUnexpectedEOF | |||
| 39 | + | } | |||
| 40 | + | ||||
| 29 | func TestNewBlobStorage(t *testing.T) { | 41 | func TestNewBlobStorage(t *testing.T) { | ||
| 30 | tmpDir := t.TempDir() | 42 | tmpDir := t.TempDir() | ||
| 31 | log := testLogger() | 43 | log := testLogger() | ||
| @@ -280,13 +292,13 @@ func TestBlobStorage_AppendBlobChunk(t *testing.T) { | |||||
| 280 | 292 | ||||
| 281 | // Append first chunk | 293 | // Append first chunk | ||
| 282 | chunk1 := []byte("first chunk") | 294 | chunk1 := []byte("first chunk") | ||
| 283 | - | size1, err := storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(chunk1)) | 295 | + | size1, err := storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(chunk1), -1, 0) |
| 284 | require.NoError(t, err) | 296 | require.NoError(t, err) | ||
| 285 | assert.Equal(t, int64(len(chunk1)), size1) | 297 | assert.Equal(t, int64(len(chunk1)), size1) | ||
| 286 | 298 | ||||
| 287 | // Append second chunk | 299 | // Append second chunk | ||
| 288 | chunk2 := []byte(" second chunk") | 300 | chunk2 := []byte(" second chunk") | ||
| 289 | - | size2, err := storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(chunk2)) | 301 | + | size2, err := storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(chunk2), -1, 0) |
| 290 | require.NoError(t, err) | 302 | require.NoError(t, err) | ||
| 291 | assert.Equal(t, int64(len(chunk1)+len(chunk2)), size2) | 303 | assert.Equal(t, int64(len(chunk1)+len(chunk2)), size2) | ||
| 292 | } | 304 | } | ||
| @@ -298,12 +310,83 @@ func TestBlobStorage_AppendBlobChunk_NotFound(t *testing.T) { | |||||
| 298 | storage, err := NewBlobStorage(tmpDir, log) | 310 | storage, err := NewBlobStorage(tmpDir, log) | ||
| 299 | require.NoError(t, err) | 311 | require.NoError(t, err) | ||
| 300 | 312 | ||||
| 301 | - | _, err = storage.AppendBlobChunk("myapp", "00000000-0000-4000-a000-000000000000", bytes.NewReader([]byte("data"))) | 313 | + | _, err = storage.AppendBlobChunk("myapp", "00000000-0000-4000-a000-000000000000", bytes.NewReader([]byte("data")), -1, 100) |
| 302 | 314 | ||||
| 303 | assert.Error(t, err) | 315 | assert.Error(t, err) | ||
| 304 | assert.Contains(t, err.Error(), "upload not found") | 316 | assert.Contains(t, err.Error(), "upload not found") | ||
| 305 | } | 317 | } | ||
| 306 | 318 | ||||
| 319 | + | func TestBlobStorage_AppendBlobChunk_MaxBlobSize(t *testing.T) { | |||
| 320 | + | tmpDir := t.TempDir() | |||
| 321 | + | log := testLogger() | |||
| 322 | + | ||||
| 323 | + | storage, err := NewBlobStorage(tmpDir, log) | |||
| 324 | + | require.NoError(t, err) | |||
| 325 | + | ||||
| 326 | + | uuid, err := storage.StartBlobUpload("myapp") | |||
| 327 | + | require.NoError(t, err) | |||
| 328 | + | ||||
| 329 | + | size, err := storage.AppendBlobChunk("myapp", uuid, bytes.NewReader([]byte("12345")), -1, 10) | |||
| 330 | + | require.NoError(t, err) | |||
| 331 | + | assert.Equal(t, int64(5), size) | |||
| 332 | + | ||||
| 333 | + | size, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader([]byte("67890")), -1, 10) | |||
| 334 | + | require.NoError(t, err) | |||
| 335 | + | assert.Equal(t, int64(10), size) | |||
| 336 | + | ||||
| 337 | + | _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader([]byte("x")), -1, 10) | |||
| 338 | + | require.Error(t, err) | |||
| 339 | + | assert.True(t, errors.Is(err, domain.ErrBlobSizeExceeded)) | |||
| 340 | + | ||||
| 341 | + | uploadPath := filepath.Join(tmpDir, "uploads", uuid) | |||
| 342 | + | info, statErr := os.Stat(uploadPath) | |||
| 343 | + | require.NoError(t, statErr) | |||
| 344 | + | assert.Equal(t, int64(10), info.Size()) | |||
| 345 | + | } | |||
| 346 | + | ||||
| 347 | + | func TestBlobStorage_AppendBlobChunk_MaxBlobSizeContentLengthTooLargeDoesNotRead(t *testing.T) { | |||
| 348 | + | tmpDir := t.TempDir() | |||
| 349 | + | log := testLogger() | |||
| 350 | + | ||||
| 351 | + | storage, err := NewBlobStorage(tmpDir, log) | |||
| 352 | + | require.NoError(t, err) | |||
| 353 | + | ||||
| 354 | + | uuid, err := storage.StartBlobUpload("myapp") | |||
| 355 | + | require.NoError(t, err) | |||
| 356 | + | ||||
| 357 | + | _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader([]byte("12345")), 5, 10) | |||
| 358 | + | require.NoError(t, err) | |||
| 359 | + | ||||
| 360 | + | reader := &readFailer{} | |||
| 361 | + | _, err = storage.AppendBlobChunk("myapp", uuid, reader, 6, 10) | |||
| 362 | + | require.Error(t, err) | |||
| 363 | + | assert.ErrorIs(t, err, domain.ErrBlobSizeExceeded) | |||
| 364 | + | assert.False(t, reader.read) | |||
| 365 | + | } | |||
| 366 | + | ||||
| 367 | + | func TestBlobStorage_AppendBlobChunk_MaxBlobSizeOverflowMidReadTruncates(t *testing.T) { | |||
| 368 | + | tmpDir := t.TempDir() | |||
| 369 | + | log := testLogger() | |||
| 370 | + | ||||
| 371 | + | storage, err := NewBlobStorage(tmpDir, log) | |||
| 372 | + | require.NoError(t, err) | |||
| 373 | + | ||||
| 374 | + | uuid, err := storage.StartBlobUpload("myapp") | |||
| 375 | + | require.NoError(t, err) | |||
| 376 | + | ||||
| 377 | + | _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader([]byte("12345")), -1, 10) | |||
| 378 | + | require.NoError(t, err) | |||
| 379 | + | ||||
| 380 | + | _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader([]byte("6789012345")), -1, 10) | |||
| 381 | + | require.Error(t, err) | |||
| 382 | + | assert.True(t, errors.Is(err, domain.ErrBlobSizeExceeded)) | |||
| 383 | + | ||||
| 384 | + | uploadPath := filepath.Join(tmpDir, "uploads", uuid) | |||
| 385 | + | info, statErr := os.Stat(uploadPath) | |||
| 386 | + | require.NoError(t, statErr) | |||
| 387 | + | assert.Equal(t, int64(5), info.Size()) | |||
| 388 | + | } | |||
| 389 | + | ||||
| 307 | func TestBlobStorage_GetBlobUpload(t *testing.T) { | 390 | func TestBlobStorage_GetBlobUpload(t *testing.T) { | ||
| 308 | tmpDir := t.TempDir() | 391 | tmpDir := t.TempDir() | ||
| 309 | log := testLogger() | 392 | log := testLogger() | ||
| @@ -350,7 +433,7 @@ func TestBlobStorage_FinishBlobUpload(t *testing.T) { | |||||
| 350 | 433 | ||||
| 351 | // Append data - content that hashes to testDigest1 | 434 | // Append data - content that hashes to testDigest1 | ||
| 352 | blobData := []byte("complete blob content") | 435 | blobData := []byte("complete blob content") | ||
| 353 | - | _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(blobData)) | 436 | + | _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(blobData), -1, 0) |
| 354 | require.NoError(t, err) | 437 | require.NoError(t, err) | ||
| 355 | 438 | ||||
| 356 | // Finish upload - use testDigest1 (validation will pass, but hash won't match) | 439 | // Finish upload - use testDigest1 (validation will pass, but hash won't match) | ||
| @@ -477,7 +560,7 @@ func TestBlobStorage_CompleteUploadFlow(t *testing.T) { | |||||
| 477 | 560 | ||||
| 478 | var totalSize int64 | 561 | var totalSize int64 | ||
| 479 | for _, chunk := range chunks { | 562 | for _, chunk := range chunks { | ||
| 480 | - | size, err := storage.AppendBlobChunk(repoName, uuid, bytes.NewReader(chunk)) | 563 | + | size, err := storage.AppendBlobChunk(repoName, uuid, bytes.NewReader(chunk), -1, 0) |
| 481 | require.NoError(t, err) | 564 | require.NoError(t, err) | ||
| 482 | totalSize = size | 565 | totalSize = size | ||
| 483 | } | 566 | } | ||
| @@ -501,7 +584,7 @@ func TestBlobStorage_ConcurrentAppendAndFinalize(t *testing.T) { | |||||
| 501 | require.NoError(t, err) | 584 | require.NoError(t, err) | ||
| 502 | 585 | ||||
| 503 | initialData := []byte("initial-") | 586 | initialData := []byte("initial-") | ||
| 504 | - | _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(initialData)) | 587 | + | _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(initialData), -1, 0) |
| 505 | require.NoError(t, err) | 588 | require.NoError(t, err) | ||
| 506 | 589 | ||||
| 507 | appendChunk := []byte("chunk") | 590 | appendChunk := []byte("chunk") | ||
| @@ -516,7 +599,7 @@ func TestBlobStorage_ConcurrentAppendAndFinalize(t *testing.T) { | |||||
| 516 | go func() { | 599 | go func() { | ||
| 517 | defer wg.Done() | 600 | defer wg.Done() | ||
| 518 | <-appendReady | 601 | <-appendReady | ||
| 519 | - | _, appendErr := storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(appendChunk)) | 602 | + | _, appendErr := storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(appendChunk), -1, 0) |
| 520 | appendErrs <- appendErr | 603 | appendErrs <- appendErr | ||
| 521 | }() | 604 | }() | ||
| 522 | } | 605 | } | ||
| @@ -570,7 +653,7 @@ func TestBlobStorage_ConcurrentAppendAndFinalize(t *testing.T) { | |||||
| 570 | assert.Equal(t, fullyAppended, blobData) | 653 | assert.Equal(t, fullyAppended, blobData) | ||
| 571 | } | 654 | } | ||
| 572 | 655 | ||||
| 573 | - | _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader([]byte("after"))) | 656 | + | _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader([]byte("after")), -1, 0) |
| 574 | assert.Error(t, err) | 657 | assert.Error(t, err) | ||
| 575 | assert.Contains(t, err.Error(), "upload not found") | 658 | assert.Contains(t, err.Error(), "upload not found") | ||
| 576 | } | 659 | } | ||
| @@ -586,13 +669,13 @@ func TestBlobStorage_AppendAfterFinalize(t *testing.T) { | |||||
| 586 | require.NoError(t, err) | 669 | require.NoError(t, err) | ||
| 587 | 670 | ||||
| 588 | blobData := []byte("finalized content") | 671 | blobData := []byte("finalized content") | ||
| 589 | - | _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(blobData)) | 672 | + | _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(blobData), -1, 0) |
| 590 | require.NoError(t, err) | 673 | require.NoError(t, err) | ||
| 591 | 674 | ||||
| 592 | err = storage.FinishBlobUpload(uuid, digestForData(blobData)) | 675 | err = storage.FinishBlobUpload(uuid, digestForData(blobData)) | ||
| 593 | require.NoError(t, err) | 676 | require.NoError(t, err) | ||
| 594 | 677 | ||||
| 595 | - | _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader([]byte("more"))) | 678 | + | _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader([]byte("more")), -1, 0) |
| 596 | assert.Error(t, err) | 679 | assert.Error(t, err) | ||
| 597 | assert.Contains(t, err.Error(), "upload not found") | 680 | assert.Contains(t, err.Error(), "upload not found") | ||
| 598 | } | 681 | } | ||
| @@ -608,7 +691,7 @@ func TestBlobStorage_FailedFinalizeRetryable(t *testing.T) { | |||||
| 608 | require.NoError(t, err) | 691 | require.NoError(t, err) | ||
| 609 | 692 | ||||
| 610 | blobData := []byte("retryable finalize") | 693 | blobData := []byte("retryable finalize") | ||
| 611 | - | _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(blobData)) | 694 | + | _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(blobData), -1, 0) |
| 612 | require.NoError(t, err) | 695 | require.NoError(t, err) | ||
| 613 | 696 | ||||
| 614 | err = storage.FinishBlobUpload(uuid, testDigest1) | 697 | err = storage.FinishBlobUpload(uuid, testDigest1) | ||
| @@ -638,7 +721,7 @@ func TestBlobStorage_SequentialFlow(t *testing.T) { | |||||
| 638 | var blobData []byte | 721 | var blobData []byte | ||
| 639 | for _, chunk := range chunks { | 722 | for _, chunk := range chunks { | ||
| 640 | blobData = append(blobData, chunk...) | 723 | blobData = append(blobData, chunk...) | ||
| 641 | - | _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(chunk)) | 724 | + | _, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(chunk), -1, 0) |
| 642 | require.NoError(t, err) | 725 | require.NoError(t, err) | ||
| 643 | } | 726 | } | ||
| 644 | 727 | ||||
M internal/app/run.go +16 -2
| @@ -96,6 +96,7 @@ type Config struct { | |||
| 96 | 96 | DataDir string `mapstructure:"data_dir"` | |
| 97 | 97 | MaxProxyBodySize string `mapstructure:"max_proxy_body_size"` // e.g., "512MB", "1GB" | |
| 98 | 98 | MaxBlobChunkSize string `mapstructure:"max_blob_chunk_size"` // e.g., "512MB", "1GB" | |
| 99 | + | MaxBlobSize string `mapstructure:"max_blob_size"` // e.g., "1GB", "2GB" | |
| 99 | 100 | MaxProxyResponseSize string `mapstructure:"max_proxy_response_size"` // e.g., "1GB", "0" for no limit | |
| 100 | 101 | MaxConcurrentConns int `mapstructure:"max_concurrent_connections"` | |
| 101 | 102 | RegistryAllowedIPs []string `mapstructure:"registry_allowed_ips"` | |
| @@ -215,6 +216,7 @@ type services struct { | |||
| 215 | 216 | previewService *preview.Service | |
| 216 | 217 | envDir string | |
| 217 | 218 | maxBlobChunkSize int64 | |
| 219 | + | maxBlobSize int64 | |
| 218 | 220 | caAdapter *pkiadapter.CA | |
| 219 | 221 | pkiSvc *pkiusecase.Service | |
| 220 | 222 | reloadCoordinator *reloadCoordinator | |
| @@ -526,6 +528,7 @@ func (si *serviceInit) initRuntimeAndProxy() error { | |||
| 526 | 528 | return err | |
| 527 | 529 | } | |
| 528 | 530 | si.svc.maxBlobChunkSize = proxyCfg.maxBlobChunkSize | |
| 531 | + | si.svc.maxBlobSize = proxyCfg.maxBlobSize | |
| 529 | 532 | si.svc.proxySvc = proxy.NewService(si.svc.runtime, si.svc.containerSvc, si.svc.configSvc, proxyCfg.proxyConfig) | |
| 530 | 533 | ||
| 531 | 534 | // Wire synchronous proxy cache invalidation for zero-downtime deployments. | |
| @@ -1349,6 +1352,7 @@ func loadSecret(ctx context.Context, backend domain.SecretsBackend, path, dataDi | |||
| 1349 | 1352 | type proxyConfigResult struct { | |
| 1350 | 1353 | proxyConfig proxy.Config | |
| 1351 | 1354 | maxBlobChunkSize int64 | |
| 1355 | + | maxBlobSize int64 | |
| 1352 | 1356 | } | |
| 1353 | 1357 | ||
| 1354 | 1358 | type configWatcher interface { | |
| @@ -1477,6 +1481,15 @@ func buildProxyConfig(cfg Config, log zerowrap.Logger) (*proxyConfigResult, erro | |||
| 1477 | 1481 | maxBlobChunkSize = parsedSize | |
| 1478 | 1482 | } | |
| 1479 | 1483 | ||
| 1484 | + | maxBlobSize := int64(registry.DefaultMaxBlobSize) | |
| 1485 | + | if cfg.Server.MaxBlobSize != "" { | |
| 1486 | + | parsedSize, err := bytesize.Parse(cfg.Server.MaxBlobSize) | |
| 1487 | + | if err != nil { | |
| 1488 | + | return nil, log.WrapErrWithFields(err, "invalid server.max_blob_size configuration", map[string]any{"value": cfg.Server.MaxBlobSize}) | |
| 1489 | + | } | |
| 1490 | + | maxBlobSize = parsedSize | |
| 1491 | + | } | |
| 1492 | + | ||
| 1480 | 1493 | maxProxyResponseSize := int64(1 << 30) // 1GB default | |
| 1481 | 1494 | if cfg.Server.MaxProxyResponseSize != "" { | |
| 1482 | 1495 | parsedSize, err := bytesize.Parse(cfg.Server.MaxProxyResponseSize) | |
| @@ -1501,6 +1514,7 @@ func buildProxyConfig(cfg Config, log zerowrap.Logger) (*proxyConfigResult, erro | |||
| 1501 | 1514 | MaxConcurrentConns: maxConcurrentConns, | |
| 1502 | 1515 | }, | |
| 1503 | 1516 | maxBlobChunkSize: maxBlobChunkSize, | |
| 1517 | + | maxBlobSize: maxBlobSize, | |
| 1504 | 1518 | }, nil | |
| 1505 | 1519 | } | |
| 1506 | 1520 | ||
| @@ -1758,8 +1772,8 @@ func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger, accessWr | |||
| 1758 | 1772 | // This ensures consistent IP extraction across logging, rate limiting, and auth. | |
| 1759 | 1773 | trustedNets := httphelper.ParseTrustedProxies(cfg.API.RateLimit.TrustedProxies) | |
| 1760 | 1774 | ||
| 1761 | - | // Registry handler (unchanged) | |
| 1762 | - | registryHandler := registry.NewHandler(svc.registrySvc, log, svc.maxBlobChunkSize) | |
| 1775 | + | // Registry handler | |
| 1776 | + | registryHandler := registry.NewHandler(svc.registrySvc, log, svc.maxBlobChunkSize, svc.maxBlobSize) | |
| 1763 | 1777 | registryWithMiddleware, cidrAllowlistMiddleware, rateLimitMiddleware := buildRegistryHandlerWithMiddleware( | |
| 1764 | 1778 | svc, | |
| 1765 | 1779 | cfg, | |
| @@ -96,6 +96,7 @@ type Config struct { | |||||
| 96 | DataDir string `mapstructure:"data_dir"` | 96 | DataDir string `mapstructure:"data_dir"` | ||
| 97 | MaxProxyBodySize string `mapstructure:"max_proxy_body_size"` // e.g., "512MB", "1GB" | 97 | MaxProxyBodySize string `mapstructure:"max_proxy_body_size"` // e.g., "512MB", "1GB" | ||
| 98 | MaxBlobChunkSize string `mapstructure:"max_blob_chunk_size"` // e.g., "512MB", "1GB" | 98 | MaxBlobChunkSize string `mapstructure:"max_blob_chunk_size"` // e.g., "512MB", "1GB" | ||
| 99 | + | MaxBlobSize string `mapstructure:"max_blob_size"` // e.g., "1GB", "2GB" | |||
| 99 | MaxProxyResponseSize string `mapstructure:"max_proxy_response_size"` // e.g., "1GB", "0" for no limit | 100 | MaxProxyResponseSize string `mapstructure:"max_proxy_response_size"` // e.g., "1GB", "0" for no limit | ||
| 100 | MaxConcurrentConns int `mapstructure:"max_concurrent_connections"` | 101 | MaxConcurrentConns int `mapstructure:"max_concurrent_connections"` | ||
| 101 | RegistryAllowedIPs []string `mapstructure:"registry_allowed_ips"` | 102 | RegistryAllowedIPs []string `mapstructure:"registry_allowed_ips"` | ||
| @@ -215,6 +216,7 @@ type services struct { | |||||
| 215 | previewService *preview.Service | 216 | previewService *preview.Service | ||
| 216 | envDir string | 217 | envDir string | ||
| 217 | maxBlobChunkSize int64 | 218 | maxBlobChunkSize int64 | ||
| 219 | + | maxBlobSize int64 | |||
| 218 | caAdapter *pkiadapter.CA | 220 | caAdapter *pkiadapter.CA | ||
| 219 | pkiSvc *pkiusecase.Service | 221 | pkiSvc *pkiusecase.Service | ||
| 220 | reloadCoordinator *reloadCoordinator | 222 | reloadCoordinator *reloadCoordinator | ||
| @@ -526,6 +528,7 @@ func (si *serviceInit) initRuntimeAndProxy() error { | |||||
| 526 | return err | 528 | return err | ||
| 527 | } | 529 | } | ||
| 528 | si.svc.maxBlobChunkSize = proxyCfg.maxBlobChunkSize | 530 | si.svc.maxBlobChunkSize = proxyCfg.maxBlobChunkSize | ||
| 531 | + | si.svc.maxBlobSize = proxyCfg.maxBlobSize | |||
| 529 | si.svc.proxySvc = proxy.NewService(si.svc.runtime, si.svc.containerSvc, si.svc.configSvc, proxyCfg.proxyConfig) | 532 | si.svc.proxySvc = proxy.NewService(si.svc.runtime, si.svc.containerSvc, si.svc.configSvc, proxyCfg.proxyConfig) | ||
| 530 | 533 | ||||
| 531 | // Wire synchronous proxy cache invalidation for zero-downtime deployments. | 534 | // Wire synchronous proxy cache invalidation for zero-downtime deployments. | ||
| @@ -1349,6 +1352,7 @@ func loadSecret(ctx context.Context, backend domain.SecretsBackend, path, dataDi | |||||
| 1349 | type proxyConfigResult struct { | 1352 | type proxyConfigResult struct { | ||
| 1350 | proxyConfig proxy.Config | 1353 | proxyConfig proxy.Config | ||
| 1351 | maxBlobChunkSize int64 | 1354 | maxBlobChunkSize int64 | ||
| 1355 | + | maxBlobSize int64 | |||
| 1352 | } | 1356 | } | ||
| 1353 | 1357 | ||||
| 1354 | type configWatcher interface { | 1358 | type configWatcher interface { | ||
| @@ -1477,6 +1481,15 @@ func buildProxyConfig(cfg Config, log zerowrap.Logger) (*proxyConfigResult, erro | |||||
| 1477 | maxBlobChunkSize = parsedSize | 1481 | maxBlobChunkSize = parsedSize | ||
| 1478 | } | 1482 | } | ||
| 1479 | 1483 | ||||
| 1484 | + | maxBlobSize := int64(registry.DefaultMaxBlobSize) | |||
| 1485 | + | if cfg.Server.MaxBlobSize != "" { | |||
| 1486 | + | parsedSize, err := bytesize.Parse(cfg.Server.MaxBlobSize) | |||
| 1487 | + | if err != nil { | |||
| 1488 | + | return nil, log.WrapErrWithFields(err, "invalid server.max_blob_size configuration", map[string]any{"value": cfg.Server.MaxBlobSize}) | |||
| 1489 | + | } | |||
| 1490 | + | maxBlobSize = parsedSize | |||
| 1491 | + | } | |||
| 1492 | + | ||||
| 1480 | maxProxyResponseSize := int64(1 << 30) // 1GB default | 1493 | maxProxyResponseSize := int64(1 << 30) // 1GB default | ||
| 1481 | if cfg.Server.MaxProxyResponseSize != "" { | 1494 | if cfg.Server.MaxProxyResponseSize != "" { | ||
| 1482 | parsedSize, err := bytesize.Parse(cfg.Server.MaxProxyResponseSize) | 1495 | parsedSize, err := bytesize.Parse(cfg.Server.MaxProxyResponseSize) | ||
| @@ -1501,6 +1514,7 @@ func buildProxyConfig(cfg Config, log zerowrap.Logger) (*proxyConfigResult, erro | |||||
| 1501 | MaxConcurrentConns: maxConcurrentConns, | 1514 | MaxConcurrentConns: maxConcurrentConns, | ||
| 1502 | }, | 1515 | }, | ||
| 1503 | maxBlobChunkSize: maxBlobChunkSize, | 1516 | maxBlobChunkSize: maxBlobChunkSize, | ||
| 1517 | + | maxBlobSize: maxBlobSize, | |||
| 1504 | }, nil | 1518 | }, nil | ||
| 1505 | } | 1519 | } | ||
| 1506 | 1520 | ||||
| @@ -1758,8 +1772,8 @@ func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger, accessWr | |||||
| 1758 | // This ensures consistent IP extraction across logging, rate limiting, and auth. | 1772 | // This ensures consistent IP extraction across logging, rate limiting, and auth. | ||
| 1759 | trustedNets := httphelper.ParseTrustedProxies(cfg.API.RateLimit.TrustedProxies) | 1773 | trustedNets := httphelper.ParseTrustedProxies(cfg.API.RateLimit.TrustedProxies) | ||
| 1760 | 1774 | ||||
| 1761 | - | // Registry handler (unchanged) | 1775 | + | // Registry handler |
| 1762 | - | registryHandler := registry.NewHandler(svc.registrySvc, log, svc.maxBlobChunkSize) | 1776 | + | registryHandler := registry.NewHandler(svc.registrySvc, log, svc.maxBlobChunkSize, svc.maxBlobSize) |
| 1763 | registryWithMiddleware, cidrAllowlistMiddleware, rateLimitMiddleware := buildRegistryHandlerWithMiddleware( | 1777 | registryWithMiddleware, cidrAllowlistMiddleware, rateLimitMiddleware := buildRegistryHandlerWithMiddleware( | ||
| 1764 | svc, | 1778 | svc, | ||
| 1765 | cfg, | 1779 | cfg, | ||
M internal/app/run_reload_test.go +19 -0
| @@ -12,6 +12,7 @@ import ( | |||
| 12 | 12 | "github.com/stretchr/testify/assert" | |
| 13 | 13 | "github.com/stretchr/testify/require" | |
| 14 | 14 | ||
| 15 | + | "github.com/bnema/gordon/internal/adapters/in/http/registry" | |
| 15 | 16 | "github.com/bnema/gordon/internal/domain" | |
| 16 | 17 | "github.com/bnema/gordon/internal/usecase/proxy" | |
| 17 | 18 | ) | |
| @@ -138,6 +139,24 @@ func TestSetupConfigHotReload_WatchCallbackInvokesCoordinator(t *testing.T) { | |||
| 138 | 139 | require.Equal(t, 0, coordinator.TriggerCalls()) | |
| 139 | 140 | } | |
| 140 | 141 | ||
| 142 | + | func TestBuildProxyConfig_ParsesMaxBlobSizeWithoutChangingChunkDefault(t *testing.T) { | |
| 143 | + | cfg := Config{} | |
| 144 | + | cfg.Server.MaxBlobSize = "2GB" | |
| 145 | + | ||
| 146 | + | result, err := buildProxyConfig(cfg, zerowrap.Default()) | |
| 147 | + | ||
| 148 | + | require.NoError(t, err) | |
| 149 | + | assert.Equal(t, int64(2<<30), result.maxBlobSize) | |
| 150 | + | assert.Equal(t, int64(registry.DefaultMaxBlobChunkSize), result.maxBlobChunkSize) | |
| 151 | + | } | |
| 152 | + | ||
| 153 | + | func TestBuildProxyConfig_DefaultMaxBlobSize(t *testing.T) { | |
| 154 | + | result, err := buildProxyConfig(Config{}, zerowrap.Default()) | |
| 155 | + | ||
| 156 | + | require.NoError(t, err) | |
| 157 | + | assert.Equal(t, int64(registry.DefaultMaxBlobSize), result.maxBlobSize) | |
| 158 | + | } | |
| 159 | + | ||
| 141 | 160 | func TestReloadCoordinator_ApplyLoadedConfig_RebuildsProxyConfigAndPublishesEvent(t *testing.T) { | |
| 142 | 161 | ctx := context.Background() | |
| 143 | 162 | v := viper.New() | |
| @@ -12,6 +12,7 @@ import ( | |||||
| 12 | "github.com/stretchr/testify/assert" | 12 | "github.com/stretchr/testify/assert" | ||
| 13 | "github.com/stretchr/testify/require" | 13 | "github.com/stretchr/testify/require" | ||
| 14 | 14 | ||||
| 15 | + | "github.com/bnema/gordon/internal/adapters/in/http/registry" | |||
| 15 | "github.com/bnema/gordon/internal/domain" | 16 | "github.com/bnema/gordon/internal/domain" | ||
| 16 | "github.com/bnema/gordon/internal/usecase/proxy" | 17 | "github.com/bnema/gordon/internal/usecase/proxy" | ||
| 17 | ) | 18 | ) | ||
| @@ -138,6 +139,24 @@ func TestSetupConfigHotReload_WatchCallbackInvokesCoordinator(t *testing.T) { | |||||
| 138 | require.Equal(t, 0, coordinator.TriggerCalls()) | 139 | require.Equal(t, 0, coordinator.TriggerCalls()) | ||
| 139 | } | 140 | } | ||
| 140 | 141 | ||||
| 142 | + | func TestBuildProxyConfig_ParsesMaxBlobSizeWithoutChangingChunkDefault(t *testing.T) { | |||
| 143 | + | cfg := Config{} | |||
| 144 | + | cfg.Server.MaxBlobSize = "2GB" | |||
| 145 | + | ||||
| 146 | + | result, err := buildProxyConfig(cfg, zerowrap.Default()) | |||
| 147 | + | ||||
| 148 | + | require.NoError(t, err) | |||
| 149 | + | assert.Equal(t, int64(2<<30), result.maxBlobSize) | |||
| 150 | + | assert.Equal(t, int64(registry.DefaultMaxBlobChunkSize), result.maxBlobChunkSize) | |||
| 151 | + | } | |||
| 152 | + | ||||
| 153 | + | func TestBuildProxyConfig_DefaultMaxBlobSize(t *testing.T) { | |||
| 154 | + | result, err := buildProxyConfig(Config{}, zerowrap.Default()) | |||
| 155 | + | ||||
| 156 | + | require.NoError(t, err) | |||
| 157 | + | assert.Equal(t, int64(registry.DefaultMaxBlobSize), result.maxBlobSize) | |||
| 158 | + | } | |||
| 159 | + | ||||
| 141 | func TestReloadCoordinator_ApplyLoadedConfig_RebuildsProxyConfigAndPublishesEvent(t *testing.T) { | 160 | func TestReloadCoordinator_ApplyLoadedConfig_RebuildsProxyConfigAndPublishesEvent(t *testing.T) { | ||
| 142 | ctx := context.Background() | 161 | ctx := context.Background() | ||
| 143 | v := viper.New() | 162 | v := viper.New() | ||
M internal/boundaries/in/mocks/mock_registry_service.go +24 -12
| @@ -40,8 +40,8 @@ func (_m *MockRegistryService) EXPECT() *MockRegistryService_Expecter { | |||
| 40 | 40 | } | |
| 41 | 41 | ||
| 42 | 42 | // AppendBlobChunk provides a mock function for the type MockRegistryService | |
| 43 | - | func (_mock *MockRegistryService) AppendBlobChunk(ctx context.Context, name string, uuid string, data io.Reader) (int64, error) { | |
| 44 | - | ret := _mock.Called(ctx, name, uuid, data) | |
| 43 | + | func (_mock *MockRegistryService) AppendBlobChunk(ctx context.Context, name string, uuid string, data io.Reader, contentLength int64, maxBlobSize int64) (int64, error) { | |
| 44 | + | ret := _mock.Called(ctx, name, uuid, data, contentLength, maxBlobSize) | |
| 45 | 45 | ||
| 46 | 46 | if len(ret) == 0 { | |
| 47 | 47 | panic("no return value specified for AppendBlobChunk") | |
| @@ -49,16 +49,16 @@ func (_mock *MockRegistryService) AppendBlobChunk(ctx context.Context, name stri | |||
| 49 | 49 | ||
| 50 | 50 | var r0 int64 | |
| 51 | 51 | var r1 error | |
| 52 | - | if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, io.Reader) (int64, error)); ok { | |
| 53 | - | return returnFunc(ctx, name, uuid, data) | |
| 52 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, io.Reader, int64, int64) (int64, error)); ok { | |
| 53 | + | return returnFunc(ctx, name, uuid, data, contentLength, maxBlobSize) | |
| 54 | 54 | } | |
| 55 | - | if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, io.Reader) int64); ok { | |
| 56 | - | r0 = returnFunc(ctx, name, uuid, data) | |
| 55 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, io.Reader, int64, int64) int64); ok { | |
| 56 | + | r0 = returnFunc(ctx, name, uuid, data, contentLength, maxBlobSize) | |
| 57 | 57 | } else { | |
| 58 | 58 | r0 = ret.Get(0).(int64) | |
| 59 | 59 | } | |
| 60 | - | if returnFunc, ok := ret.Get(1).(func(context.Context, string, string, io.Reader) error); ok { | |
| 61 | - | r1 = returnFunc(ctx, name, uuid, data) | |
| 60 | + | if returnFunc, ok := ret.Get(1).(func(context.Context, string, string, io.Reader, int64, int64) error); ok { | |
| 61 | + | r1 = returnFunc(ctx, name, uuid, data, contentLength, maxBlobSize) | |
| 62 | 62 | } else { | |
| 63 | 63 | r1 = ret.Error(1) | |
| 64 | 64 | } | |
| @@ -75,11 +75,13 @@ type MockRegistryService_AppendBlobChunk_Call struct { | |||
| 75 | 75 | // - name string | |
| 76 | 76 | // - uuid string | |
| 77 | 77 | // - data io.Reader | |
| 78 | - | func (_e *MockRegistryService_Expecter) AppendBlobChunk(ctx interface{}, name interface{}, uuid interface{}, data interface{}) *MockRegistryService_AppendBlobChunk_Call { | |
| 79 | - | return &MockRegistryService_AppendBlobChunk_Call{Call: _e.mock.On("AppendBlobChunk", ctx, name, uuid, data)} | |
| 78 | + | // - contentLength int64 | |
| 79 | + | // - maxBlobSize int64 | |
| 80 | + | func (_e *MockRegistryService_Expecter) AppendBlobChunk(ctx interface{}, name interface{}, uuid interface{}, data interface{}, contentLength interface{}, maxBlobSize interface{}) *MockRegistryService_AppendBlobChunk_Call { | |
| 81 | + | return &MockRegistryService_AppendBlobChunk_Call{Call: _e.mock.On("AppendBlobChunk", ctx, name, uuid, data, contentLength, maxBlobSize)} | |
| 80 | 82 | } | |
| 81 | 83 | ||
| 82 | - | func (_c *MockRegistryService_AppendBlobChunk_Call) Run(run func(ctx context.Context, name string, uuid string, data io.Reader)) *MockRegistryService_AppendBlobChunk_Call { | |
| 84 | + | func (_c *MockRegistryService_AppendBlobChunk_Call) Run(run func(ctx context.Context, name string, uuid string, data io.Reader, contentLength int64, maxBlobSize int64)) *MockRegistryService_AppendBlobChunk_Call { | |
| 83 | 85 | _c.Call.Run(func(args mock.Arguments) { | |
| 84 | 86 | var arg0 context.Context | |
| 85 | 87 | if args[0] != nil { | |
| @@ -97,11 +99,21 @@ func (_c *MockRegistryService_AppendBlobChunk_Call) Run(run func(ctx context.Con | |||
| 97 | 99 | if args[3] != nil { | |
| 98 | 100 | arg3 = args[3].(io.Reader) | |
| 99 | 101 | } | |
| 102 | + | var arg4 int64 | |
| 103 | + | if args[4] != nil { | |
| 104 | + | arg4 = args[4].(int64) | |
| 105 | + | } | |
| 106 | + | var arg5 int64 | |
| 107 | + | if args[5] != nil { | |
| 108 | + | arg5 = args[5].(int64) | |
| 109 | + | } | |
| 100 | 110 | run( | |
| 101 | 111 | arg0, | |
| 102 | 112 | arg1, | |
| 103 | 113 | arg2, | |
| 104 | 114 | arg3, | |
| 115 | + | arg4, | |
| 116 | + | arg5, | |
| 105 | 117 | ) | |
| 106 | 118 | }) | |
| 107 | 119 | return _c | |
| @@ -112,7 +124,7 @@ func (_c *MockRegistryService_AppendBlobChunk_Call) Return(n int64, err error) * | |||
| 112 | 124 | return _c | |
| 113 | 125 | } | |
| 114 | 126 | ||
| 115 | - | func (_c *MockRegistryService_AppendBlobChunk_Call) RunAndReturn(run func(ctx context.Context, name string, uuid string, data io.Reader) (int64, error)) *MockRegistryService_AppendBlobChunk_Call { | |
| 127 | + | func (_c *MockRegistryService_AppendBlobChunk_Call) RunAndReturn(run func(ctx context.Context, name string, uuid string, data io.Reader, contentLength int64, maxBlobSize int64) (int64, error)) *MockRegistryService_AppendBlobChunk_Call { | |
| 116 | 128 | _c.Call.Return(run) | |
| 117 | 129 | return _c | |
| 118 | 130 | } | |
| @@ -40,8 +40,8 @@ func (_m *MockRegistryService) EXPECT() *MockRegistryService_Expecter { | |||||
| 40 | } | 40 | } | ||
| 41 | 41 | ||||
| 42 | // AppendBlobChunk provides a mock function for the type MockRegistryService | 42 | // AppendBlobChunk provides a mock function for the type MockRegistryService | ||
| 43 | - | func (_mock *MockRegistryService) AppendBlobChunk(ctx context.Context, name string, uuid string, data io.Reader) (int64, error) { | 43 | + | func (_mock *MockRegistryService) AppendBlobChunk(ctx context.Context, name string, uuid string, data io.Reader, contentLength int64, maxBlobSize int64) (int64, error) { |
| 44 | - | ret := _mock.Called(ctx, name, uuid, data) | 44 | + | ret := _mock.Called(ctx, name, uuid, data, contentLength, maxBlobSize) |
| 45 | 45 | ||||
| 46 | if len(ret) == 0 { | 46 | if len(ret) == 0 { | ||
| 47 | panic("no return value specified for AppendBlobChunk") | 47 | panic("no return value specified for AppendBlobChunk") | ||
| @@ -49,16 +49,16 @@ func (_mock *MockRegistryService) AppendBlobChunk(ctx context.Context, name stri | |||||
| 49 | 49 | ||||
| 50 | var r0 int64 | 50 | var r0 int64 | ||
| 51 | var r1 error | 51 | var r1 error | ||
| 52 | - | if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, io.Reader) (int64, error)); ok { | 52 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, io.Reader, int64, int64) (int64, error)); ok { |
| 53 | - | return returnFunc(ctx, name, uuid, data) | 53 | + | return returnFunc(ctx, name, uuid, data, contentLength, maxBlobSize) |
| 54 | } | 54 | } | ||
| 55 | - | if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, io.Reader) int64); ok { | 55 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, io.Reader, int64, int64) int64); ok { |
| 56 | - | r0 = returnFunc(ctx, name, uuid, data) | 56 | + | r0 = returnFunc(ctx, name, uuid, data, contentLength, maxBlobSize) |
| 57 | } else { | 57 | } else { | ||
| 58 | r0 = ret.Get(0).(int64) | 58 | r0 = ret.Get(0).(int64) | ||
| 59 | } | 59 | } | ||
| 60 | - | if returnFunc, ok := ret.Get(1).(func(context.Context, string, string, io.Reader) error); ok { | 60 | + | if returnFunc, ok := ret.Get(1).(func(context.Context, string, string, io.Reader, int64, int64) error); ok { |
| 61 | - | r1 = returnFunc(ctx, name, uuid, data) | 61 | + | r1 = returnFunc(ctx, name, uuid, data, contentLength, maxBlobSize) |
| 62 | } else { | 62 | } else { | ||
| 63 | r1 = ret.Error(1) | 63 | r1 = ret.Error(1) | ||
| 64 | } | 64 | } | ||
| @@ -75,11 +75,13 @@ type MockRegistryService_AppendBlobChunk_Call struct { | |||||
| 75 | // - name string | 75 | // - name string | ||
| 76 | // - uuid string | 76 | // - uuid string | ||
| 77 | // - data io.Reader | 77 | // - data io.Reader | ||
| 78 | - | func (_e *MockRegistryService_Expecter) AppendBlobChunk(ctx interface{}, name interface{}, uuid interface{}, data interface{}) *MockRegistryService_AppendBlobChunk_Call { | 78 | + | // - contentLength int64 |
| 79 | - | return &MockRegistryService_AppendBlobChunk_Call{Call: _e.mock.On("AppendBlobChunk", ctx, name, uuid, data)} | 79 | + | // - maxBlobSize int64 |
| 80 | + | func (_e *MockRegistryService_Expecter) AppendBlobChunk(ctx interface{}, name interface{}, uuid interface{}, data interface{}, contentLength interface{}, maxBlobSize interface{}) *MockRegistryService_AppendBlobChunk_Call { | |||
| 81 | + | return &MockRegistryService_AppendBlobChunk_Call{Call: _e.mock.On("AppendBlobChunk", ctx, name, uuid, data, contentLength, maxBlobSize)} | |||
| 80 | } | 82 | } | ||
| 81 | 83 | ||||
| 82 | - | func (_c *MockRegistryService_AppendBlobChunk_Call) Run(run func(ctx context.Context, name string, uuid string, data io.Reader)) *MockRegistryService_AppendBlobChunk_Call { | 84 | + | func (_c *MockRegistryService_AppendBlobChunk_Call) Run(run func(ctx context.Context, name string, uuid string, data io.Reader, contentLength int64, maxBlobSize int64)) *MockRegistryService_AppendBlobChunk_Call { |
| 83 | _c.Call.Run(func(args mock.Arguments) { | 85 | _c.Call.Run(func(args mock.Arguments) { | ||
| 84 | var arg0 context.Context | 86 | var arg0 context.Context | ||
| 85 | if args[0] != nil { | 87 | if args[0] != nil { | ||
| @@ -97,11 +99,21 @@ func (_c *MockRegistryService_AppendBlobChunk_Call) Run(run func(ctx context.Con | |||||
| 97 | if args[3] != nil { | 99 | if args[3] != nil { | ||
| 98 | arg3 = args[3].(io.Reader) | 100 | arg3 = args[3].(io.Reader) | ||
| 99 | } | 101 | } | ||
| 102 | + | var arg4 int64 | |||
| 103 | + | if args[4] != nil { | |||
| 104 | + | arg4 = args[4].(int64) | |||
| 105 | + | } | |||
| 106 | + | var arg5 int64 | |||
| 107 | + | if args[5] != nil { | |||
| 108 | + | arg5 = args[5].(int64) | |||
| 109 | + | } | |||
| 100 | run( | 110 | run( | ||
| 101 | arg0, | 111 | arg0, | ||
| 102 | arg1, | 112 | arg1, | ||
| 103 | arg2, | 113 | arg2, | ||
| 104 | arg3, | 114 | arg3, | ||
| 115 | + | arg4, | |||
| 116 | + | arg5, | |||
| 105 | ) | 117 | ) | ||
| 106 | }) | 118 | }) | ||
| 107 | return _c | 119 | return _c | ||
| @@ -112,7 +124,7 @@ func (_c *MockRegistryService_AppendBlobChunk_Call) Return(n int64, err error) * | |||||
| 112 | return _c | 124 | return _c | ||
| 113 | } | 125 | } | ||
| 114 | 126 | ||||
| 115 | - | func (_c *MockRegistryService_AppendBlobChunk_Call) RunAndReturn(run func(ctx context.Context, name string, uuid string, data io.Reader) (int64, error)) *MockRegistryService_AppendBlobChunk_Call { | 127 | + | func (_c *MockRegistryService_AppendBlobChunk_Call) RunAndReturn(run func(ctx context.Context, name string, uuid string, data io.Reader, contentLength int64, maxBlobSize int64) (int64, error)) *MockRegistryService_AppendBlobChunk_Call { |
| 116 | _c.Call.Return(run) | 128 | _c.Call.Return(run) | ||
| 117 | return _c | 129 | return _c | ||
| 118 | } | 130 | } | ||
M internal/boundaries/in/registry.go +1 -1
| @@ -22,7 +22,7 @@ type RegistryService interface { | |||
| 22 | 22 | ||
| 23 | 23 | // Upload operations | |
| 24 | 24 | StartUpload(ctx context.Context, name string) (string, error) | |
| 25 | - | AppendBlobChunk(ctx context.Context, name, uuid string, data io.Reader) (int64, error) | |
| 25 | + | AppendBlobChunk(ctx context.Context, name, uuid string, data io.Reader, contentLength, maxBlobSize int64) (int64, error) | |
| 26 | 26 | FinishUpload(ctx context.Context, uuid, digest string) error | |
| 27 | 27 | CancelUpload(ctx context.Context, uuid string) error | |
| 28 | 28 | ||
| @@ -22,7 +22,7 @@ type RegistryService interface { | |||||
| 22 | 22 | ||||
| 23 | // Upload operations | 23 | // Upload operations | ||
| 24 | StartUpload(ctx context.Context, name string) (string, error) | 24 | StartUpload(ctx context.Context, name string) (string, error) | ||
| 25 | - | AppendBlobChunk(ctx context.Context, name, uuid string, data io.Reader) (int64, error) | 25 | + | AppendBlobChunk(ctx context.Context, name, uuid string, data io.Reader, contentLength, maxBlobSize int64) (int64, error) |
| 26 | FinishUpload(ctx context.Context, uuid, digest string) error | 26 | FinishUpload(ctx context.Context, uuid, digest string) error | ||
| 27 | CancelUpload(ctx context.Context, uuid string) error | 27 | CancelUpload(ctx context.Context, uuid string) error | ||
| 28 | 28 | ||||
M internal/boundaries/out/mocks/mock_blob_storage.go +24 -12
| @@ -39,8 +39,8 @@ func (_m *MockBlobStorage) EXPECT() *MockBlobStorage_Expecter { | |||
| 39 | 39 | } | |
| 40 | 40 | ||
| 41 | 41 | // AppendBlobChunk provides a mock function for the type MockBlobStorage | |
| 42 | - | func (_mock *MockBlobStorage) AppendBlobChunk(name string, uuid string, data io.Reader) (int64, error) { | |
| 43 | - | ret := _mock.Called(name, uuid, data) | |
| 42 | + | func (_mock *MockBlobStorage) AppendBlobChunk(name string, uuid string, data io.Reader, contentLength int64, maxBlobSize int64) (int64, error) { | |
| 43 | + | ret := _mock.Called(name, uuid, data, contentLength, maxBlobSize) | |
| 44 | 44 | ||
| 45 | 45 | if len(ret) == 0 { | |
| 46 | 46 | panic("no return value specified for AppendBlobChunk") | |
| @@ -48,16 +48,16 @@ func (_mock *MockBlobStorage) AppendBlobChunk(name string, uuid string, data io. | |||
| 48 | 48 | ||
| 49 | 49 | var r0 int64 | |
| 50 | 50 | var r1 error | |
| 51 | - | if returnFunc, ok := ret.Get(0).(func(string, string, io.Reader) (int64, error)); ok { | |
| 52 | - | return returnFunc(name, uuid, data) | |
| 51 | + | if returnFunc, ok := ret.Get(0).(func(string, string, io.Reader, int64, int64) (int64, error)); ok { | |
| 52 | + | return returnFunc(name, uuid, data, contentLength, maxBlobSize) | |
| 53 | 53 | } | |
| 54 | - | if returnFunc, ok := ret.Get(0).(func(string, string, io.Reader) int64); ok { | |
| 55 | - | r0 = returnFunc(name, uuid, data) | |
| 54 | + | if returnFunc, ok := ret.Get(0).(func(string, string, io.Reader, int64, int64) int64); ok { | |
| 55 | + | r0 = returnFunc(name, uuid, data, contentLength, maxBlobSize) | |
| 56 | 56 | } else { | |
| 57 | 57 | r0 = ret.Get(0).(int64) | |
| 58 | 58 | } | |
| 59 | - | if returnFunc, ok := ret.Get(1).(func(string, string, io.Reader) error); ok { | |
| 60 | - | r1 = returnFunc(name, uuid, data) | |
| 59 | + | if returnFunc, ok := ret.Get(1).(func(string, string, io.Reader, int64, int64) error); ok { | |
| 60 | + | r1 = returnFunc(name, uuid, data, contentLength, maxBlobSize) | |
| 61 | 61 | } else { | |
| 62 | 62 | r1 = ret.Error(1) | |
| 63 | 63 | } | |
| @@ -73,11 +73,13 @@ type MockBlobStorage_AppendBlobChunk_Call struct { | |||
| 73 | 73 | // - name string | |
| 74 | 74 | // - uuid string | |
| 75 | 75 | // - data io.Reader | |
| 76 | - | func (_e *MockBlobStorage_Expecter) AppendBlobChunk(name interface{}, uuid interface{}, data interface{}) *MockBlobStorage_AppendBlobChunk_Call { | |
| 77 | - | return &MockBlobStorage_AppendBlobChunk_Call{Call: _e.mock.On("AppendBlobChunk", name, uuid, data)} | |
| 76 | + | // - contentLength int64 | |
| 77 | + | // - maxBlobSize int64 | |
| 78 | + | func (_e *MockBlobStorage_Expecter) AppendBlobChunk(name interface{}, uuid interface{}, data interface{}, contentLength interface{}, maxBlobSize interface{}) *MockBlobStorage_AppendBlobChunk_Call { | |
| 79 | + | return &MockBlobStorage_AppendBlobChunk_Call{Call: _e.mock.On("AppendBlobChunk", name, uuid, data, contentLength, maxBlobSize)} | |
| 78 | 80 | } | |
| 79 | 81 | ||
| 80 | - | func (_c *MockBlobStorage_AppendBlobChunk_Call) Run(run func(name string, uuid string, data io.Reader)) *MockBlobStorage_AppendBlobChunk_Call { | |
| 82 | + | func (_c *MockBlobStorage_AppendBlobChunk_Call) Run(run func(name string, uuid string, data io.Reader, contentLength int64, maxBlobSize int64)) *MockBlobStorage_AppendBlobChunk_Call { | |
| 81 | 83 | _c.Call.Run(func(args mock.Arguments) { | |
| 82 | 84 | var arg0 string | |
| 83 | 85 | if args[0] != nil { | |
| @@ -91,10 +93,20 @@ func (_c *MockBlobStorage_AppendBlobChunk_Call) Run(run func(name string, uuid s | |||
| 91 | 93 | if args[2] != nil { | |
| 92 | 94 | arg2 = args[2].(io.Reader) | |
| 93 | 95 | } | |
| 96 | + | var arg3 int64 | |
| 97 | + | if args[3] != nil { | |
| 98 | + | arg3 = args[3].(int64) | |
| 99 | + | } | |
| 100 | + | var arg4 int64 | |
| 101 | + | if args[4] != nil { | |
| 102 | + | arg4 = args[4].(int64) | |
| 103 | + | } | |
| 94 | 104 | run( | |
| 95 | 105 | arg0, | |
| 96 | 106 | arg1, | |
| 97 | 107 | arg2, | |
| 108 | + | arg3, | |
| 109 | + | arg4, | |
| 98 | 110 | ) | |
| 99 | 111 | }) | |
| 100 | 112 | return _c | |
| @@ -105,7 +117,7 @@ func (_c *MockBlobStorage_AppendBlobChunk_Call) Return(n int64, err error) *Mock | |||
| 105 | 117 | return _c | |
| 106 | 118 | } | |
| 107 | 119 | ||
| 108 | - | func (_c *MockBlobStorage_AppendBlobChunk_Call) RunAndReturn(run func(name string, uuid string, data io.Reader) (int64, error)) *MockBlobStorage_AppendBlobChunk_Call { | |
| 120 | + | func (_c *MockBlobStorage_AppendBlobChunk_Call) RunAndReturn(run func(name string, uuid string, data io.Reader, contentLength int64, maxBlobSize int64) (int64, error)) *MockBlobStorage_AppendBlobChunk_Call { | |
| 109 | 121 | _c.Call.Return(run) | |
| 110 | 122 | return _c | |
| 111 | 123 | } | |
| @@ -39,8 +39,8 @@ func (_m *MockBlobStorage) EXPECT() *MockBlobStorage_Expecter { | |||||
| 39 | } | 39 | } | ||
| 40 | 40 | ||||
| 41 | // AppendBlobChunk provides a mock function for the type MockBlobStorage | 41 | // AppendBlobChunk provides a mock function for the type MockBlobStorage | ||
| 42 | - | func (_mock *MockBlobStorage) AppendBlobChunk(name string, uuid string, data io.Reader) (int64, error) { | 42 | + | func (_mock *MockBlobStorage) AppendBlobChunk(name string, uuid string, data io.Reader, contentLength int64, maxBlobSize int64) (int64, error) { |
| 43 | - | ret := _mock.Called(name, uuid, data) | 43 | + | ret := _mock.Called(name, uuid, data, contentLength, maxBlobSize) |
| 44 | 44 | ||||
| 45 | if len(ret) == 0 { | 45 | if len(ret) == 0 { | ||
| 46 | panic("no return value specified for AppendBlobChunk") | 46 | panic("no return value specified for AppendBlobChunk") | ||
| @@ -48,16 +48,16 @@ func (_mock *MockBlobStorage) AppendBlobChunk(name string, uuid string, data io. | |||||
| 48 | 48 | ||||
| 49 | var r0 int64 | 49 | var r0 int64 | ||
| 50 | var r1 error | 50 | var r1 error | ||
| 51 | - | if returnFunc, ok := ret.Get(0).(func(string, string, io.Reader) (int64, error)); ok { | 51 | + | if returnFunc, ok := ret.Get(0).(func(string, string, io.Reader, int64, int64) (int64, error)); ok { |
| 52 | - | return returnFunc(name, uuid, data) | 52 | + | return returnFunc(name, uuid, data, contentLength, maxBlobSize) |
| 53 | } | 53 | } | ||
| 54 | - | if returnFunc, ok := ret.Get(0).(func(string, string, io.Reader) int64); ok { | 54 | + | if returnFunc, ok := ret.Get(0).(func(string, string, io.Reader, int64, int64) int64); ok { |
| 55 | - | r0 = returnFunc(name, uuid, data) | 55 | + | r0 = returnFunc(name, uuid, data, contentLength, maxBlobSize) |
| 56 | } else { | 56 | } else { | ||
| 57 | r0 = ret.Get(0).(int64) | 57 | r0 = ret.Get(0).(int64) | ||
| 58 | } | 58 | } | ||
| 59 | - | if returnFunc, ok := ret.Get(1).(func(string, string, io.Reader) error); ok { | 59 | + | if returnFunc, ok := ret.Get(1).(func(string, string, io.Reader, int64, int64) error); ok { |
| 60 | - | r1 = returnFunc(name, uuid, data) | 60 | + | r1 = returnFunc(name, uuid, data, contentLength, maxBlobSize) |
| 61 | } else { | 61 | } else { | ||
| 62 | r1 = ret.Error(1) | 62 | r1 = ret.Error(1) | ||
| 63 | } | 63 | } | ||
| @@ -73,11 +73,13 @@ type MockBlobStorage_AppendBlobChunk_Call struct { | |||||
| 73 | // - name string | 73 | // - name string | ||
| 74 | // - uuid string | 74 | // - uuid string | ||
| 75 | // - data io.Reader | 75 | // - data io.Reader | ||
| 76 | - | func (_e *MockBlobStorage_Expecter) AppendBlobChunk(name interface{}, uuid interface{}, data interface{}) *MockBlobStorage_AppendBlobChunk_Call { | 76 | + | // - contentLength int64 |
| 77 | - | return &MockBlobStorage_AppendBlobChunk_Call{Call: _e.mock.On("AppendBlobChunk", name, uuid, data)} | 77 | + | // - maxBlobSize int64 |
| 78 | + | func (_e *MockBlobStorage_Expecter) AppendBlobChunk(name interface{}, uuid interface{}, data interface{}, contentLength interface{}, maxBlobSize interface{}) *MockBlobStorage_AppendBlobChunk_Call { | |||
| 79 | + | return &MockBlobStorage_AppendBlobChunk_Call{Call: _e.mock.On("AppendBlobChunk", name, uuid, data, contentLength, maxBlobSize)} | |||
| 78 | } | 80 | } | ||
| 79 | 81 | ||||
| 80 | - | func (_c *MockBlobStorage_AppendBlobChunk_Call) Run(run func(name string, uuid string, data io.Reader)) *MockBlobStorage_AppendBlobChunk_Call { | 82 | + | func (_c *MockBlobStorage_AppendBlobChunk_Call) Run(run func(name string, uuid string, data io.Reader, contentLength int64, maxBlobSize int64)) *MockBlobStorage_AppendBlobChunk_Call { |
| 81 | _c.Call.Run(func(args mock.Arguments) { | 83 | _c.Call.Run(func(args mock.Arguments) { | ||
| 82 | var arg0 string | 84 | var arg0 string | ||
| 83 | if args[0] != nil { | 85 | if args[0] != nil { | ||
| @@ -91,10 +93,20 @@ func (_c *MockBlobStorage_AppendBlobChunk_Call) Run(run func(name string, uuid s | |||||
| 91 | if args[2] != nil { | 93 | if args[2] != nil { | ||
| 92 | arg2 = args[2].(io.Reader) | 94 | arg2 = args[2].(io.Reader) | ||
| 93 | } | 95 | } | ||
| 96 | + | var arg3 int64 | |||
| 97 | + | if args[3] != nil { | |||
| 98 | + | arg3 = args[3].(int64) | |||
| 99 | + | } | |||
| 100 | + | var arg4 int64 | |||
| 101 | + | if args[4] != nil { | |||
| 102 | + | arg4 = args[4].(int64) | |||
| 103 | + | } | |||
| 94 | run( | 104 | run( | ||
| 95 | arg0, | 105 | arg0, | ||
| 96 | arg1, | 106 | arg1, | ||
| 97 | arg2, | 107 | arg2, | ||
| 108 | + | arg3, | |||
| 109 | + | arg4, | |||
| 98 | ) | 110 | ) | ||
| 99 | }) | 111 | }) | ||
| 100 | return _c | 112 | return _c | ||
| @@ -105,7 +117,7 @@ func (_c *MockBlobStorage_AppendBlobChunk_Call) Return(n int64, err error) *Mock | |||||
| 105 | return _c | 117 | return _c | ||
| 106 | } | 118 | } | ||
| 107 | 119 | ||||
| 108 | - | func (_c *MockBlobStorage_AppendBlobChunk_Call) RunAndReturn(run func(name string, uuid string, data io.Reader) (int64, error)) *MockBlobStorage_AppendBlobChunk_Call { | 120 | + | func (_c *MockBlobStorage_AppendBlobChunk_Call) RunAndReturn(run func(name string, uuid string, data io.Reader, contentLength int64, maxBlobSize int64) (int64, error)) *MockBlobStorage_AppendBlobChunk_Call { |
| 109 | _c.Call.Return(run) | 121 | _c.Call.Return(run) | ||
| 110 | return _c | 122 | return _c | ||
| 111 | } | 123 | } | ||
M internal/boundaries/out/storage.go +1 -1
| @@ -30,7 +30,7 @@ type BlobStorage interface { | |||
| 30 | 30 | StartBlobUpload(name string) (string, error) | |
| 31 | 31 | ||
| 32 | 32 | // AppendBlobChunk appends data to an in-progress upload. | |
| 33 | - | AppendBlobChunk(name, uuid string, data io.Reader) (int64, error) | |
| 33 | + | AppendBlobChunk(name, uuid string, data io.Reader, contentLength, maxBlobSize int64) (int64, error) | |
| 34 | 34 | ||
| 35 | 35 | // GetBlobUpload returns a writer for the upload. | |
| 36 | 36 | GetBlobUpload(uuid string) (io.WriteCloser, error) | |
| @@ -30,7 +30,7 @@ type BlobStorage interface { | |||||
| 30 | StartBlobUpload(name string) (string, error) | 30 | StartBlobUpload(name string) (string, error) | ||
| 31 | 31 | ||||
| 32 | // AppendBlobChunk appends data to an in-progress upload. | 32 | // AppendBlobChunk appends data to an in-progress upload. | ||
| 33 | - | AppendBlobChunk(name, uuid string, data io.Reader) (int64, error) | 33 | + | AppendBlobChunk(name, uuid string, data io.Reader, contentLength, maxBlobSize int64) (int64, error) |
| 34 | 34 | ||||
| 35 | // GetBlobUpload returns a writer for the upload. | 35 | // GetBlobUpload returns a writer for the upload. | ||
| 36 | GetBlobUpload(uuid string) (io.WriteCloser, error) | 36 | GetBlobUpload(uuid string) (io.WriteCloser, error) | ||
M internal/domain/errors.go +1 -0
| @@ -34,6 +34,7 @@ var ( | |||
| 34 | 34 | ErrInvalidDigest = errors.New("invalid digest") | |
| 35 | 35 | ErrDigestMismatch = errors.New("digest mismatch") | |
| 36 | 36 | ErrUnauthorized = errors.New("unauthorized") | |
| 37 | + | ErrBlobSizeExceeded = errors.New("blob size exceeds maximum") | |
| 37 | 38 | ||
| 38 | 39 | // Network errors | |
| 39 | 40 | ErrNetworkNotFound = errors.New("network not found") | |
| @@ -34,6 +34,7 @@ var ( | |||||
| 34 | ErrInvalidDigest = errors.New("invalid digest") | 34 | ErrInvalidDigest = errors.New("invalid digest") | ||
| 35 | ErrDigestMismatch = errors.New("digest mismatch") | 35 | ErrDigestMismatch = errors.New("digest mismatch") | ||
| 36 | ErrUnauthorized = errors.New("unauthorized") | 36 | ErrUnauthorized = errors.New("unauthorized") | ||
| 37 | + | ErrBlobSizeExceeded = errors.New("blob size exceeds maximum") | |||
| 37 | 38 | ||||
| 38 | // Network errors | 39 | // Network errors | ||
| 39 | ErrNetworkNotFound = errors.New("network not found") | 40 | ErrNetworkNotFound = errors.New("network not found") | ||
M internal/usecase/registry/service.go +2 -2
| @@ -304,7 +304,7 @@ func (s *Service) StartUpload(ctx context.Context, name string) (string, error) | |||
| 304 | 304 | } | |
| 305 | 305 | ||
| 306 | 306 | // AppendBlobChunk appends data to an in-progress blob upload. | |
| 307 | - | func (s *Service) AppendBlobChunk(ctx context.Context, name, uuid string, data io.Reader) (int64, error) { | |
| 307 | + | func (s *Service) AppendBlobChunk(ctx context.Context, name, uuid string, data io.Reader, contentLength, maxBlobSize int64) (int64, error) { | |
| 308 | 308 | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |
| 309 | 309 | zerowrap.FieldLayer: "usecase", | |
| 310 | 310 | zerowrap.FieldUseCase: "AppendBlobChunk", | |
| @@ -313,7 +313,7 @@ func (s *Service) AppendBlobChunk(ctx context.Context, name, uuid string, data i | |||
| 313 | 313 | }) | |
| 314 | 314 | log := zerowrap.FromCtx(ctx) | |
| 315 | 315 | ||
| 316 | - | length, err := s.blobStorage.AppendBlobChunk(name, uuid, data) | |
| 316 | + | length, err := s.blobStorage.AppendBlobChunk(name, uuid, data, contentLength, maxBlobSize) | |
| 317 | 317 | if err != nil { | |
| 318 | 318 | return 0, log.WrapErr(err, "failed to append blob chunk") | |
| 319 | 319 | } | |
| @@ -304,7 +304,7 @@ func (s *Service) StartUpload(ctx context.Context, name string) (string, error) | |||||
| 304 | } | 304 | } | ||
| 305 | 305 | ||||
| 306 | // AppendBlobChunk appends data to an in-progress blob upload. | 306 | // AppendBlobChunk appends data to an in-progress blob upload. | ||
| 307 | - | func (s *Service) AppendBlobChunk(ctx context.Context, name, uuid string, data io.Reader) (int64, error) { | 307 | + | func (s *Service) AppendBlobChunk(ctx context.Context, name, uuid string, data io.Reader, contentLength, maxBlobSize int64) (int64, error) { |
| 308 | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | 308 | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | ||
| 309 | zerowrap.FieldLayer: "usecase", | 309 | zerowrap.FieldLayer: "usecase", | ||
| 310 | zerowrap.FieldUseCase: "AppendBlobChunk", | 310 | zerowrap.FieldUseCase: "AppendBlobChunk", | ||
| @@ -313,7 +313,7 @@ func (s *Service) AppendBlobChunk(ctx context.Context, name, uuid string, data i | |||||
| 313 | }) | 313 | }) | ||
| 314 | log := zerowrap.FromCtx(ctx) | 314 | log := zerowrap.FromCtx(ctx) | ||
| 315 | 315 | ||||
| 316 | - | length, err := s.blobStorage.AppendBlobChunk(name, uuid, data) | 316 | + | length, err := s.blobStorage.AppendBlobChunk(name, uuid, data, contentLength, maxBlobSize) |
| 317 | if err != nil { | 317 | if err != nil { | ||
| 318 | return 0, log.WrapErr(err, "failed to append blob chunk") | 318 | return 0, log.WrapErr(err, "failed to append blob chunk") | ||
| 319 | } | 319 | } | ||
M internal/usecase/registry/service_test.go +32 -0
| @@ -306,6 +306,38 @@ func TestService_StartUpload_Error(t *testing.T) { | |||
| 306 | 306 | assert.Contains(t, err.Error(), "failed to start blob upload") | |
| 307 | 307 | } | |
| 308 | 308 | ||
| 309 | + | func TestService_AppendBlobChunk_PassesMaxBlobSize(t *testing.T) { | |
| 310 | + | blobStorage := mocks.NewMockBlobStorage(t) | |
| 311 | + | manifestStorage := mocks.NewMockManifestStorage(t) | |
| 312 | + | eventBus := mocks.NewMockEventPublisher(t) | |
| 313 | + | ||
| 314 | + | svc := NewService(blobStorage, manifestStorage, eventBus) | |
| 315 | + | ctx := testContext() | |
| 316 | + | ||
| 317 | + | blobStorage.EXPECT().AppendBlobChunk("myapp", "upload-id", mock.Anything, int64(4), int64(10)).Return(int64(4), nil) | |
| 318 | + | ||
| 319 | + | length, err := svc.AppendBlobChunk(ctx, "myapp", "upload-id", strings.NewReader("data"), 4, 10) | |
| 320 | + | ||
| 321 | + | assert.NoError(t, err) | |
| 322 | + | assert.Equal(t, int64(4), length) | |
| 323 | + | } | |
| 324 | + | ||
| 325 | + | func TestService_AppendBlobChunk_MaxBlobSizeExceeded(t *testing.T) { | |
| 326 | + | blobStorage := mocks.NewMockBlobStorage(t) | |
| 327 | + | manifestStorage := mocks.NewMockManifestStorage(t) | |
| 328 | + | eventBus := mocks.NewMockEventPublisher(t) | |
| 329 | + | ||
| 330 | + | svc := NewService(blobStorage, manifestStorage, eventBus) | |
| 331 | + | ctx := testContext() | |
| 332 | + | ||
| 333 | + | blobStorage.EXPECT().AppendBlobChunk("myapp", "upload-id", mock.Anything, int64(11), int64(10)).Return(int64(0), domain.ErrBlobSizeExceeded) | |
| 334 | + | ||
| 335 | + | _, err := svc.AppendBlobChunk(ctx, "myapp", "upload-id", strings.NewReader("too large"), 11, 10) | |
| 336 | + | ||
| 337 | + | require.Error(t, err) | |
| 338 | + | assert.ErrorIs(t, err, domain.ErrBlobSizeExceeded) | |
| 339 | + | } | |
| 340 | + | ||
| 309 | 341 | func TestService_FinishUpload_Success(t *testing.T) { | |
| 310 | 342 | blobStorage := mocks.NewMockBlobStorage(t) | |
| 311 | 343 | manifestStorage := mocks.NewMockManifestStorage(t) | |
| @@ -306,6 +306,38 @@ func TestService_StartUpload_Error(t *testing.T) { | |||||
| 306 | assert.Contains(t, err.Error(), "failed to start blob upload") | 306 | assert.Contains(t, err.Error(), "failed to start blob upload") | ||
| 307 | } | 307 | } | ||
| 308 | 308 | ||||
| 309 | + | func TestService_AppendBlobChunk_PassesMaxBlobSize(t *testing.T) { | |||
| 310 | + | blobStorage := mocks.NewMockBlobStorage(t) | |||
| 311 | + | manifestStorage := mocks.NewMockManifestStorage(t) | |||
| 312 | + | eventBus := mocks.NewMockEventPublisher(t) | |||
| 313 | + | ||||
| 314 | + | svc := NewService(blobStorage, manifestStorage, eventBus) | |||
| 315 | + | ctx := testContext() | |||
| 316 | + | ||||
| 317 | + | blobStorage.EXPECT().AppendBlobChunk("myapp", "upload-id", mock.Anything, int64(4), int64(10)).Return(int64(4), nil) | |||
| 318 | + | ||||
| 319 | + | length, err := svc.AppendBlobChunk(ctx, "myapp", "upload-id", strings.NewReader("data"), 4, 10) | |||
| 320 | + | ||||
| 321 | + | assert.NoError(t, err) | |||
| 322 | + | assert.Equal(t, int64(4), length) | |||
| 323 | + | } | |||
| 324 | + | ||||
| 325 | + | func TestService_AppendBlobChunk_MaxBlobSizeExceeded(t *testing.T) { | |||
| 326 | + | blobStorage := mocks.NewMockBlobStorage(t) | |||
| 327 | + | manifestStorage := mocks.NewMockManifestStorage(t) | |||
| 328 | + | eventBus := mocks.NewMockEventPublisher(t) | |||
| 329 | + | ||||
| 330 | + | svc := NewService(blobStorage, manifestStorage, eventBus) | |||
| 331 | + | ctx := testContext() | |||
| 332 | + | ||||
| 333 | + | blobStorage.EXPECT().AppendBlobChunk("myapp", "upload-id", mock.Anything, int64(11), int64(10)).Return(int64(0), domain.ErrBlobSizeExceeded) | |||
| 334 | + | ||||
| 335 | + | _, err := svc.AppendBlobChunk(ctx, "myapp", "upload-id", strings.NewReader("too large"), 11, 10) | |||
| 336 | + | ||||
| 337 | + | require.Error(t, err) | |||
| 338 | + | assert.ErrorIs(t, err, domain.ErrBlobSizeExceeded) | |||
| 339 | + | } | |||
| 340 | + | ||||
| 309 | func TestService_FinishUpload_Success(t *testing.T) { | 341 | func TestService_FinishUpload_Success(t *testing.T) { | ||
| 310 | blobStorage := mocks.NewMockBlobStorage(t) | 342 | blobStorage := mocks.NewMockBlobStorage(t) | ||
| 311 | manifestStorage := mocks.NewMockManifestStorage(t) | 343 | manifestStorage := mocks.NewMockManifestStorage(t) | ||