fix(registry): make blob chunk upload limit configurable
- commit
- c00c10c5df8a53b25afe9b9f0264fb9eb4cf195a
- author
- brice · · 2026-02-06 14:54 UTC
- committer
- brice · · 2026-02-06 19:34 UTC
- parent
- dca6537
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +116 -58
M docs/config/reference.md +2 -0
| @@ -13,6 +13,7 @@ port = 80 # HTTP proxy port | |||
| 13 | 13 | registry_port = 5000 # Container registry port | |
| 14 | 14 | gordon_domain = "" # Required: Gordon domain (registry + API) | |
| 15 | 15 | data_dir = "~/.gordon" # Data directory (varies by install type) | |
| 16 | + | max_blob_chunk_size = "512MB" # Max size per registry blob upload chunk | |
| 16 | 17 | ||
| 17 | 18 | # ============================================================================= | |
| 18 | 19 | # AUTHENTICATION (required - Gordon won't start without credentials configured) | |
| @@ -125,6 +126,7 @@ preserve = true # Keep volumes when containers are | |||
| 125 | 126 | | `server.registry_port` | `5000` | Container registry port | | |
| 126 | 127 | | `server.gordon_domain` | `""` | **Required** - Gordon domain | | |
| 127 | 128 | | `server.data_dir` | `~/.gordon` | Data directory | | |
| 129 | + | | `server.max_blob_chunk_size` | `"512MB"` | Max size per registry blob upload chunk | | |
| 128 | 130 | | `auth.enabled` | `true` | Enable authentication | | |
| 129 | 131 | | `auth.secrets_backend` | `"unsafe"` | Secrets storage | | |
| 130 | 132 | | `auth.token_expiry` | `"720h"` | 30 days | | |
| @@ -13,6 +13,7 @@ port = 80 # HTTP proxy port | |||||
| 13 | registry_port = 5000 # Container registry port | 13 | registry_port = 5000 # Container registry port | ||
| 14 | gordon_domain = "" # Required: Gordon domain (registry + API) | 14 | gordon_domain = "" # Required: Gordon domain (registry + API) | ||
| 15 | data_dir = "~/.gordon" # Data directory (varies by install type) | 15 | data_dir = "~/.gordon" # Data directory (varies by install type) | ||
| 16 | + | max_blob_chunk_size = "512MB" # Max size per registry blob upload chunk | |||
| 16 | 17 | ||||
| 17 | # ============================================================================= | 18 | # ============================================================================= | ||
| 18 | # AUTHENTICATION (required - Gordon won't start without credentials configured) | 19 | # AUTHENTICATION (required - Gordon won't start without credentials configured) | ||
| @@ -125,6 +126,7 @@ preserve = true # Keep volumes when containers are | |||||
| 125 | | `server.registry_port` | `5000` | Container registry port | | 126 | | `server.registry_port` | `5000` | Container registry port | | ||
| 126 | | `server.gordon_domain` | `""` | **Required** - Gordon domain | | 127 | | `server.gordon_domain` | `""` | **Required** - Gordon domain | | ||
| 127 | | `server.data_dir` | `~/.gordon` | Data directory | | 128 | | `server.data_dir` | `~/.gordon` | Data directory | | ||
| 129 | + | | `server.max_blob_chunk_size` | `"512MB"` | Max size per registry blob upload chunk | | |||
| 128 | | `auth.enabled` | `true` | Enable authentication | | 130 | | `auth.enabled` | `true` | Enable authentication | | ||
| 129 | | `auth.secrets_backend` | `"unsafe"` | Secrets storage | | 131 | | `auth.secrets_backend` | `"unsafe"` | Secrets storage | | ||
| 130 | | `auth.token_expiry` | `"720h"` | 30 days | | 132 | | `auth.token_expiry` | `"720h"` | 30 days | | ||
M docs/config/server.md +26 -1
| @@ -22,6 +22,7 @@ gordon_domain = "gordon.mydomain.com" # Gordon domain (required) | |||
| 22 | 22 | | `registry_domain` | string | - | Deprecated alias for `gordon_domain` | | |
| 23 | 23 | | `data_dir` | string | `~/.gordon` | Directory for registry data, logs, and env files | | |
| 24 | 24 | | `max_proxy_body_size` | string | `"512MB"` | Maximum request body size for proxied requests | | |
| 25 | + | | `max_blob_chunk_size` | string | `"512MB"` | Maximum request body size for a single registry blob upload chunk | | |
| 25 | 26 | ||
| 26 | 27 | ## Port Configuration | |
| 27 | 28 | ||
| @@ -140,7 +141,31 @@ data_dir = "~/.gordon" # Default for user installations | |||
| 140 | 141 | ||
| 141 | 142 | > **Warning:** Setting this to `0` (no limit) allows unlimited upload sizes, which can lead to disk exhaustion and DoS vulnerabilities. Always set a reasonable limit for production deployments. | |
| 142 | 143 | ||
| 143 | - | ## Examples | |
| 144 | + | ## Maximum Registry Blob Chunk Size | |
| 145 | + | ||
| 146 | + | The `max_blob_chunk_size` setting limits the size of a single blob upload request to the registry (`PATCH`/`PUT` under `/v2/*/blobs/uploads/*`): | |
| 147 | + | ||
| 148 | + | ```toml | |
| 149 | + | [server] | |
| 150 | + | max_blob_chunk_size = "512MB" # Default | |
| 151 | + | ``` | |
| 152 | + | ||
| 153 | + | **Purpose:** Prevents excessive memory usage during blob uploads while allowing large real-world image layers. | |
| 154 | + | ||
| 155 | + | **Supported units:** B, KB, MB, GB, TB (case-insensitive, decimal/1024-based) | |
| 156 | + | ||
| 157 | + | **Examples:** | |
| 158 | + | ```toml | |
| 159 | + | max_blob_chunk_size = "256MB" # Stricter limit | |
| 160 | + | max_blob_chunk_size = "1GB" # Allow larger layers | |
| 161 | + | ``` | |
| 162 | + | ||
| 163 | + | **Behavior:** | |
| 164 | + | - When a blob chunk exceeds the limit, Gordon returns `413 Request Entity Too Large` | |
| 165 | + | - Docker/Podman may send a full layer in one request, so set this high enough for your base images | |
| 166 | + | - Default (`512MB`) supports common large base layers (for example, `postgres`) | |
| 167 | + | ||
| 168 | + | ## Examples | |
| 144 | 169 | ||
| 145 | 170 | ### Development Configuration | |
| 146 | 171 | ||
| @@ -22,6 +22,7 @@ gordon_domain = "gordon.mydomain.com" # Gordon domain (required) | |||||
| 22 | | `registry_domain` | string | - | Deprecated alias for `gordon_domain` | | 22 | | `registry_domain` | string | - | Deprecated alias for `gordon_domain` | | ||
| 23 | | `data_dir` | string | `~/.gordon` | Directory for registry data, logs, and env files | | 23 | | `data_dir` | string | `~/.gordon` | Directory for registry data, logs, and env files | | ||
| 24 | | `max_proxy_body_size` | string | `"512MB"` | Maximum request body size for proxied requests | | 24 | | `max_proxy_body_size` | string | `"512MB"` | Maximum request body size for proxied requests | | ||
| 25 | + | | `max_blob_chunk_size` | string | `"512MB"` | Maximum request body size for a single registry blob upload chunk | | |||
| 25 | 26 | ||||
| 26 | ## Port Configuration | 27 | ## Port Configuration | ||
| 27 | 28 | ||||
| @@ -140,7 +141,31 @@ data_dir = "~/.gordon" # Default for user installations | |||||
| 140 | 141 | ||||
| 141 | > **Warning:** Setting this to `0` (no limit) allows unlimited upload sizes, which can lead to disk exhaustion and DoS vulnerabilities. Always set a reasonable limit for production deployments. | 142 | > **Warning:** Setting this to `0` (no limit) allows unlimited upload sizes, which can lead to disk exhaustion and DoS vulnerabilities. Always set a reasonable limit for production deployments. | ||
| 142 | 143 | ||||
| 143 | - | ## Examples | 144 | + | ## Maximum Registry Blob Chunk Size |
| 145 | + | ||||
| 146 | + | The `max_blob_chunk_size` setting limits the size of a single blob upload request to the registry (`PATCH`/`PUT` under `/v2/*/blobs/uploads/*`): | |||
| 147 | + | ||||
| 148 | + | ```toml | |||
| 149 | + | [server] | |||
| 150 | + | max_blob_chunk_size = "512MB" # Default | |||
| 151 | + | ``` | |||
| 152 | + | ||||
| 153 | + | **Purpose:** Prevents excessive memory usage during blob uploads while allowing large real-world image layers. | |||
| 154 | + | ||||
| 155 | + | **Supported units:** B, KB, MB, GB, TB (case-insensitive, decimal/1024-based) | |||
| 156 | + | ||||
| 157 | + | **Examples:** | |||
| 158 | + | ```toml | |||
| 159 | + | max_blob_chunk_size = "256MB" # Stricter limit | |||
| 160 | + | max_blob_chunk_size = "1GB" # Allow larger layers | |||
| 161 | + | ``` | |||
| 162 | + | ||||
| 163 | + | **Behavior:** | |||
| 164 | + | - When a blob chunk exceeds the limit, Gordon returns `413 Request Entity Too Large` | |||
| 165 | + | - Docker/Podman may send a full layer in one request, so set this high enough for your base images | |||
| 166 | + | - Default (`512MB`) supports common large base layers (for example, `postgres`) | |||
| 167 | + | ||||
| 168 | + | ## Examples | |||
| 144 | 169 | ||||
| 145 | ### Development Configuration | 170 | ### Development Configuration | ||
| 146 | 171 | ||||
M internal/adapters/in/http/registry/handler.go +17 -10
| @@ -22,26 +22,33 @@ import ( | |||
| 22 | 22 | const ( | |
| 23 | 23 | // MaxManifestSize limits manifest uploads to 10MB. | |
| 24 | 24 | MaxManifestSize = 10 * 1024 * 1024 | |
| 25 | - | // MaxBlobChunkSize limits individual blob chunks to 95MB. | |
| 26 | - | // Kept below 100MB to leave headroom for HTTP overhead when behind | |
| 27 | - | // proxies like Cloudflare that enforce a 100MB per-request limit. | |
| 28 | - | MaxBlobChunkSize = 95 * 1024 * 1024 | |
| 25 | + | // DefaultMaxBlobChunkSize is the default maximum size for a single blob upload chunk. | |
| 26 | + | // Docker/Podman typically send a full layer as one request body, so this default | |
| 27 | + | // must handle common base image layer sizes. | |
| 28 | + | DefaultMaxBlobChunkSize = 512 * 1024 * 1024 | |
| 29 | 29 | ) | |
| 30 | 30 | ||
| 31 | 31 | // Handler implements the HTTP handler for Docker Registry API v2. | |
| 32 | 32 | type Handler struct { | |
| 33 | - | registrySvc in.RegistryService | |
| 34 | - | log zerowrap.Logger | |
| 33 | + | registrySvc in.RegistryService | |
| 34 | + | log zerowrap.Logger | |
| 35 | + | maxBlobChunkSize int64 | |
| 35 | 36 | } | |
| 36 | 37 | ||
| 37 | 38 | // NewHandler creates a new registry HTTP handler. | |
| 38 | 39 | func NewHandler( | |
| 39 | 40 | registrySvc in.RegistryService, | |
| 40 | 41 | log zerowrap.Logger, | |
| 42 | + | maxBlobChunkSize int64, | |
| 41 | 43 | ) *Handler { | |
| 44 | + | if maxBlobChunkSize <= 0 { | |
| 45 | + | maxBlobChunkSize = DefaultMaxBlobChunkSize | |
| 46 | + | } | |
| 47 | + | ||
| 42 | 48 | return &Handler{ | |
| 43 | - | registrySvc: registrySvc, | |
| 44 | - | log: log, | |
| 49 | + | registrySvc: registrySvc, | |
| 50 | + | log: log, | |
| 51 | + | maxBlobChunkSize: maxBlobChunkSize, | |
| 45 | 52 | } | |
| 46 | 53 | } | |
| 47 | 54 | ||
| @@ -401,14 +408,14 @@ func (h *Handler) handleBlobUpload(w http.ResponseWriter, r *http.Request) { | |||
| 401 | 408 | } | |
| 402 | 409 | ||
| 403 | 410 | // Limit blob chunk size to prevent memory exhaustion | |
| 404 | - | r.Body = http.MaxBytesReader(w, r.Body, MaxBlobChunkSize) | |
| 411 | + | r.Body = http.MaxBytesReader(w, r.Body, h.maxBlobChunkSize) | |
| 405 | 412 | ||
| 406 | 413 | // Read the chunk from the request body | |
| 407 | 414 | chunk, err := io.ReadAll(r.Body) | |
| 408 | 415 | if err != nil { | |
| 409 | 416 | var maxBytesErr *http.MaxBytesError | |
| 410 | 417 | if errors.As(err, &maxBytesErr) { | |
| 411 | - | log.Warn().Int64("max_size", MaxBlobChunkSize).Msg("blob chunk too large") | |
| 418 | + | log.Warn().Int64("max_size", h.maxBlobChunkSize).Msg("blob chunk too large") | |
| 412 | 419 | h.sendRegistryError(w, http.StatusRequestEntityTooLarge, "SIZE_INVALID", "blob chunk exceeds maximum size") | |
| 413 | 420 | return | |
| 414 | 421 | } | |
| @@ -22,26 +22,33 @@ import ( | |||||
| 22 | const ( | 22 | const ( | ||
| 23 | // MaxManifestSize limits manifest uploads to 10MB. | 23 | // MaxManifestSize limits manifest uploads to 10MB. | ||
| 24 | MaxManifestSize = 10 * 1024 * 1024 | 24 | MaxManifestSize = 10 * 1024 * 1024 | ||
| 25 | - | // MaxBlobChunkSize limits individual blob chunks to 95MB. | 25 | + | // DefaultMaxBlobChunkSize is the default maximum size for a single blob upload chunk. |
| 26 | - | // Kept below 100MB to leave headroom for HTTP overhead when behind | 26 | + | // Docker/Podman typically send a full layer as one request body, so this default |
| 27 | - | // proxies like Cloudflare that enforce a 100MB per-request limit. | 27 | + | // must handle common base image layer sizes. |
| 28 | - | MaxBlobChunkSize = 95 * 1024 * 1024 | 28 | + | DefaultMaxBlobChunkSize = 512 * 1024 * 1024 |
| 29 | ) | 29 | ) | ||
| 30 | 30 | ||||
| 31 | // Handler implements the HTTP handler for Docker Registry API v2. | 31 | // Handler implements the HTTP handler for Docker Registry API v2. | ||
| 32 | type Handler struct { | 32 | type Handler struct { | ||
| 33 | - | registrySvc in.RegistryService | 33 | + | registrySvc in.RegistryService |
| 34 | - | log zerowrap.Logger | 34 | + | log zerowrap.Logger |
| 35 | + | maxBlobChunkSize int64 | |||
| 35 | } | 36 | } | ||
| 36 | 37 | ||||
| 37 | // NewHandler creates a new registry HTTP handler. | 38 | // NewHandler creates a new registry HTTP handler. | ||
| 38 | func NewHandler( | 39 | func NewHandler( | ||
| 39 | registrySvc in.RegistryService, | 40 | registrySvc in.RegistryService, | ||
| 40 | log zerowrap.Logger, | 41 | log zerowrap.Logger, | ||
| 42 | + | maxBlobChunkSize int64, | |||
| 41 | ) *Handler { | 43 | ) *Handler { | ||
| 44 | + | if maxBlobChunkSize <= 0 { | |||
| 45 | + | maxBlobChunkSize = DefaultMaxBlobChunkSize | |||
| 46 | + | } | |||
| 47 | + | ||||
| 42 | return &Handler{ | 48 | return &Handler{ | ||
| 43 | - | registrySvc: registrySvc, | 49 | + | registrySvc: registrySvc, |
| 44 | - | log: log, | 50 | + | log: log, |
| 51 | + | maxBlobChunkSize: maxBlobChunkSize, | |||
| 45 | } | 52 | } | ||
| 46 | } | 53 | } | ||
| 47 | 54 | ||||
| @@ -401,14 +408,14 @@ func (h *Handler) handleBlobUpload(w http.ResponseWriter, r *http.Request) { | |||||
| 401 | } | 408 | } | ||
| 402 | 409 | ||||
| 403 | // Limit blob chunk size to prevent memory exhaustion | 410 | // Limit blob chunk size to prevent memory exhaustion | ||
| 404 | - | r.Body = http.MaxBytesReader(w, r.Body, MaxBlobChunkSize) | 411 | + | r.Body = http.MaxBytesReader(w, r.Body, h.maxBlobChunkSize) |
| 405 | 412 | ||||
| 406 | // Read the chunk from the request body | 413 | // Read the chunk from the request body | ||
| 407 | chunk, err := io.ReadAll(r.Body) | 414 | chunk, err := io.ReadAll(r.Body) | ||
| 408 | if err != nil { | 415 | if err != nil { | ||
| 409 | var maxBytesErr *http.MaxBytesError | 416 | var maxBytesErr *http.MaxBytesError | ||
| 410 | if errors.As(err, &maxBytesErr) { | 417 | if errors.As(err, &maxBytesErr) { | ||
| 411 | - | log.Warn().Int64("max_size", MaxBlobChunkSize).Msg("blob chunk too large") | 418 | + | log.Warn().Int64("max_size", h.maxBlobChunkSize).Msg("blob chunk too large") |
| 412 | h.sendRegistryError(w, http.StatusRequestEntityTooLarge, "SIZE_INVALID", "blob chunk exceeds maximum size") | 419 | h.sendRegistryError(w, http.StatusRequestEntityTooLarge, "SIZE_INVALID", "blob chunk exceeds maximum size") | ||
| 413 | return | 420 | return | ||
| 414 | } | 421 | } | ||
M internal/adapters/in/http/registry/handler_test.go +39 -29
| @@ -23,7 +23,7 @@ func testLogger() zerowrap.Logger { | |||
| 23 | 23 | func TestHandler_Base(t *testing.T) { | |
| 24 | 24 | registrySvc := inmocks.NewMockRegistryService(t) | |
| 25 | 25 | ||
| 26 | - | handler := NewHandler(registrySvc, testLogger()) | |
| 26 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) | |
| 27 | 27 | ||
| 28 | 28 | req := httptest.NewRequest("GET", "/v2/", nil) | |
| 29 | 29 | rec := httptest.NewRecorder() | |
| @@ -37,7 +37,7 @@ func TestHandler_Base(t *testing.T) { | |||
| 37 | 37 | func TestHandler_NotFound(t *testing.T) { | |
| 38 | 38 | registrySvc := inmocks.NewMockRegistryService(t) | |
| 39 | 39 | ||
| 40 | - | handler := NewHandler(registrySvc, testLogger()) | |
| 40 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) | |
| 41 | 41 | ||
| 42 | 42 | req := httptest.NewRequest("GET", "/v2/unknown/path", nil) | |
| 43 | 43 | rec := httptest.NewRecorder() | |
| @@ -50,7 +50,7 @@ func TestHandler_NotFound(t *testing.T) { | |||
| 50 | 50 | func TestHandler_GetManifest_Success(t *testing.T) { | |
| 51 | 51 | registrySvc := inmocks.NewMockRegistryService(t) | |
| 52 | 52 | ||
| 53 | - | handler := NewHandler(registrySvc, testLogger()) | |
| 53 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) | |
| 54 | 54 | ||
| 55 | 55 | manifestData := []byte(`{"schemaVersion": 2}`) | |
| 56 | 56 | registrySvc.EXPECT().GetManifest(mock.Anything, "myapp", "latest").Return(&domain.Manifest{ | |
| @@ -73,7 +73,7 @@ func TestHandler_GetManifest_Success(t *testing.T) { | |||
| 73 | 73 | func TestHandler_GetManifest_HEAD(t *testing.T) { | |
| 74 | 74 | registrySvc := inmocks.NewMockRegistryService(t) | |
| 75 | 75 | ||
| 76 | - | handler := NewHandler(registrySvc, testLogger()) | |
| 76 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) | |
| 77 | 77 | ||
| 78 | 78 | manifestData := []byte(`{"schemaVersion": 2}`) | |
| 79 | 79 | registrySvc.EXPECT().GetManifest(mock.Anything, "myapp", "latest").Return(&domain.Manifest{ | |
| @@ -97,7 +97,7 @@ func TestHandler_GetManifest_HEAD(t *testing.T) { | |||
| 97 | 97 | func TestHandler_GetManifest_NotFound(t *testing.T) { | |
| 98 | 98 | registrySvc := inmocks.NewMockRegistryService(t) | |
| 99 | 99 | ||
| 100 | - | handler := NewHandler(registrySvc, testLogger()) | |
| 100 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) | |
| 101 | 101 | ||
| 102 | 102 | registrySvc.EXPECT().GetManifest(mock.Anything, "myapp", "notexists").Return(nil, assert.AnError) | |
| 103 | 103 | ||
| @@ -113,7 +113,7 @@ func TestHandler_GetManifest_NotFound(t *testing.T) { | |||
| 113 | 113 | func TestHandler_GetManifest_NestedName(t *testing.T) { | |
| 114 | 114 | registrySvc := inmocks.NewMockRegistryService(t) | |
| 115 | 115 | ||
| 116 | - | handler := NewHandler(registrySvc, testLogger()) | |
| 116 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) | |
| 117 | 117 | ||
| 118 | 118 | manifestData := []byte(`{"schemaVersion": 2}`) | |
| 119 | 119 | registrySvc.EXPECT().GetManifest(mock.Anything, "org/project/app", "v1.0").Return(&domain.Manifest{ | |
| @@ -134,7 +134,7 @@ func TestHandler_GetManifest_NestedName(t *testing.T) { | |||
| 134 | 134 | func TestHandler_PutManifest_Success(t *testing.T) { | |
| 135 | 135 | registrySvc := inmocks.NewMockRegistryService(t) | |
| 136 | 136 | ||
| 137 | - | handler := NewHandler(registrySvc, testLogger()) | |
| 137 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) | |
| 138 | 138 | ||
| 139 | 139 | manifestData := []byte(`{"schemaVersion": 2}`) | |
| 140 | 140 | registrySvc.EXPECT().PutManifest(mock.Anything, mock.MatchedBy(func(m *domain.Manifest) bool { | |
| @@ -155,7 +155,7 @@ func TestHandler_PutManifest_Success(t *testing.T) { | |||
| 155 | 155 | func TestHandler_PutManifest_MissingContentType(t *testing.T) { | |
| 156 | 156 | registrySvc := inmocks.NewMockRegistryService(t) | |
| 157 | 157 | ||
| 158 | - | handler := NewHandler(registrySvc, testLogger()) | |
| 158 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) | |
| 159 | 159 | ||
| 160 | 160 | manifestData := []byte(`{"schemaVersion": 2}`) | |
| 161 | 161 | ||
| @@ -171,7 +171,7 @@ func TestHandler_PutManifest_MissingContentType(t *testing.T) { | |||
| 171 | 171 | func TestHandler_PutManifest_StorageError(t *testing.T) { | |
| 172 | 172 | registrySvc := inmocks.NewMockRegistryService(t) | |
| 173 | 173 | ||
| 174 | - | handler := NewHandler(registrySvc, testLogger()) | |
| 174 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) | |
| 175 | 175 | ||
| 176 | 176 | manifestData := []byte(`{"schemaVersion": 2}`) | |
| 177 | 177 | registrySvc.EXPECT().PutManifest(mock.Anything, mock.Anything).Return("", assert.AnError) | |
| @@ -188,7 +188,7 @@ func TestHandler_PutManifest_StorageError(t *testing.T) { | |||
| 188 | 188 | func TestHandler_ManifestRoutes_MethodNotAllowed(t *testing.T) { | |
| 189 | 189 | registrySvc := inmocks.NewMockRegistryService(t) | |
| 190 | 190 | ||
| 191 | - | handler := NewHandler(registrySvc, testLogger()) | |
| 191 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) | |
| 192 | 192 | ||
| 193 | 193 | req := httptest.NewRequest("DELETE", "/v2/myapp/manifests/latest", nil) | |
| 194 | 194 | rec := httptest.NewRecorder() | |
| @@ -201,7 +201,7 @@ func TestHandler_ManifestRoutes_MethodNotAllowed(t *testing.T) { | |||
| 201 | 201 | func TestHandler_GetBlob_Success(t *testing.T) { | |
| 202 | 202 | registrySvc := inmocks.NewMockRegistryService(t) | |
| 203 | 203 | ||
| 204 | - | handler := NewHandler(registrySvc, testLogger()) | |
| 204 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) | |
| 205 | 205 | ||
| 206 | 206 | // Create a temp file for http.ServeFile | |
| 207 | 207 | tmpFile, err := os.CreateTemp("", "blob-*") | |
| @@ -226,7 +226,7 @@ func TestHandler_GetBlob_Success(t *testing.T) { | |||
| 226 | 226 | func TestHandler_GetBlob_NotFound(t *testing.T) { | |
| 227 | 227 | registrySvc := inmocks.NewMockRegistryService(t) | |
| 228 | 228 | ||
| 229 | - | handler := NewHandler(registrySvc, testLogger()) | |
| 229 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) | |
| 230 | 230 | ||
| 231 | 231 | registrySvc.EXPECT().GetBlobPath(mock.Anything, "sha256:0000000000000000000000000000000000000000000000000000000000000000").Return("", assert.AnError) | |
| 232 | 232 | ||
| @@ -241,7 +241,7 @@ func TestHandler_GetBlob_NotFound(t *testing.T) { | |||
| 241 | 241 | func TestHandler_BlobRoutes_MethodNotAllowed(t *testing.T) { | |
| 242 | 242 | registrySvc := inmocks.NewMockRegistryService(t) | |
| 243 | 243 | ||
| 244 | - | handler := NewHandler(registrySvc, testLogger()) | |
| 244 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) | |
| 245 | 245 | ||
| 246 | 246 | req := httptest.NewRequest("POST", "/v2/myapp/blobs/sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", nil) | |
| 247 | 247 | rec := httptest.NewRecorder() | |
| @@ -255,7 +255,7 @@ func TestHandler_BlobRoutes_MethodNotAllowed(t *testing.T) { | |||
| 255 | 255 | func TestHandler_StartBlobUpload_Success(t *testing.T) { | |
| 256 | 256 | registrySvc := inmocks.NewMockRegistryService(t) | |
| 257 | 257 | ||
| 258 | - | handler := NewHandler(registrySvc, testLogger()) | |
| 258 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) | |
| 259 | 259 | ||
| 260 | 260 | registrySvc.EXPECT().StartUpload(mock.Anything, "myapp").Return("550e8400-e29b-41d4-a716-446655440000", nil) | |
| 261 | 261 | ||
| @@ -272,7 +272,7 @@ func TestHandler_StartBlobUpload_Success(t *testing.T) { | |||
| 272 | 272 | func TestHandler_StartBlobUpload_Error(t *testing.T) { | |
| 273 | 273 | registrySvc := inmocks.NewMockRegistryService(t) | |
| 274 | 274 | ||
| 275 | - | handler := NewHandler(registrySvc, testLogger()) | |
| 275 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) | |
| 276 | 276 | ||
| 277 | 277 | registrySvc.EXPECT().StartUpload(mock.Anything, "myapp").Return("", assert.AnError) | |
| 278 | 278 | ||
| @@ -288,7 +288,7 @@ func TestHandler_StartBlobUpload_Error(t *testing.T) { | |||
| 288 | 288 | func TestHandler_BlobUpload_PATCH(t *testing.T) { | |
| 289 | 289 | registrySvc := inmocks.NewMockRegistryService(t) | |
| 290 | 290 | ||
| 291 | - | handler := NewHandler(registrySvc, testLogger()) | |
| 291 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) | |
| 292 | 292 | ||
| 293 | 293 | chunkData := []byte("chunk content") | |
| 294 | 294 | registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", chunkData).Return(int64(len(chunkData)), nil) | |
| @@ -305,7 +305,7 @@ func TestHandler_BlobUpload_PATCH(t *testing.T) { | |||
| 305 | 305 | func TestHandler_BlobUpload_PUT_Finalize(t *testing.T) { | |
| 306 | 306 | registrySvc := inmocks.NewMockRegistryService(t) | |
| 307 | 307 | ||
| 308 | - | handler := NewHandler(registrySvc, testLogger()) | |
| 308 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) | |
| 309 | 309 | ||
| 310 | 310 | chunkData := []byte("final chunk") | |
| 311 | 311 | registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", chunkData).Return(int64(len(chunkData)), nil) | |
| @@ -324,7 +324,7 @@ func TestHandler_BlobUpload_PUT_Finalize(t *testing.T) { | |||
| 324 | 324 | func TestHandler_BlobUpload_PUT_DigestMismatch(t *testing.T) { | |
| 325 | 325 | registrySvc := inmocks.NewMockRegistryService(t) | |
| 326 | 326 | ||
| 327 | - | handler := NewHandler(registrySvc, testLogger()) | |
| 327 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) | |
| 328 | 328 | ||
| 329 | 329 | chunkData := []byte("final chunk") | |
| 330 | 330 | // Note: Invalid digest format is rejected by validation before reaching storage layer | |
| @@ -342,7 +342,7 @@ func TestHandler_BlobUpload_PUT_DigestMismatch(t *testing.T) { | |||
| 342 | 342 | func TestHandler_ListTags_Success(t *testing.T) { | |
| 343 | 343 | registrySvc := inmocks.NewMockRegistryService(t) | |
| 344 | 344 | ||
| 345 | - | handler := NewHandler(registrySvc, testLogger()) | |
| 345 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) | |
| 346 | 346 | ||
| 347 | 347 | registrySvc.EXPECT().ListTags(mock.Anything, "myapp").Return([]string{"latest", "v1.0", "v2.0"}, nil) | |
| 348 | 348 | ||
| @@ -366,7 +366,7 @@ func TestHandler_ListTags_Success(t *testing.T) { | |||
| 366 | 366 | func TestHandler_ListTags_NotFound(t *testing.T) { | |
| 367 | 367 | registrySvc := inmocks.NewMockRegistryService(t) | |
| 368 | 368 | ||
| 369 | - | handler := NewHandler(registrySvc, testLogger()) | |
| 369 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) | |
| 370 | 370 | ||
| 371 | 371 | registrySvc.EXPECT().ListTags(mock.Anything, "notexists").Return(nil, assert.AnError) | |
| 372 | 372 | ||
| @@ -382,7 +382,7 @@ func TestHandler_ListTags_NotFound(t *testing.T) { | |||
| 382 | 382 | func TestHandler_ListTags_MethodNotAllowed(t *testing.T) { | |
| 383 | 383 | registrySvc := inmocks.NewMockRegistryService(t) | |
| 384 | 384 | ||
| 385 | - | handler := NewHandler(registrySvc, testLogger()) | |
| 385 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) | |
| 386 | 386 | ||
| 387 | 387 | req := httptest.NewRequest("POST", "/v2/myapp/tags/list", nil) | |
| 388 | 388 | rec := httptest.NewRecorder() | |
| @@ -396,7 +396,7 @@ func TestHandler_ListTags_MethodNotAllowed(t *testing.T) { | |||
| 396 | 396 | func TestHandler_RegisterRoutes(t *testing.T) { | |
| 397 | 397 | registrySvc := inmocks.NewMockRegistryService(t) | |
| 398 | 398 | ||
| 399 | - | handler := NewHandler(registrySvc, testLogger()) | |
| 399 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) | |
| 400 | 400 | ||
| 401 | 401 | mux := http.NewServeMux() | |
| 402 | 402 | handler.RegisterRoutes(mux) | |
| @@ -412,7 +412,7 @@ func TestHandler_RegisterRoutes(t *testing.T) { | |||
| 412 | 412 | ||
| 413 | 413 | func TestHandler_StartBlobUpload_ReturnsDockerUploadUUID(t *testing.T) { | |
| 414 | 414 | registrySvc := inmocks.NewMockRegistryService(t) | |
| 415 | - | handler := NewHandler(registrySvc, testLogger()) | |
| 415 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) | |
| 416 | 416 | ||
| 417 | 417 | registrySvc.EXPECT().StartUpload(mock.Anything, "myapp").Return("550e8400-e29b-41d4-a716-446655440000", nil) | |
| 418 | 418 | ||
| @@ -427,7 +427,7 @@ func TestHandler_StartBlobUpload_ReturnsDockerUploadUUID(t *testing.T) { | |||
| 427 | 427 | ||
| 428 | 428 | func TestHandler_BlobUpload_PATCH_ReturnsDockerUploadUUID(t *testing.T) { | |
| 429 | 429 | registrySvc := inmocks.NewMockRegistryService(t) | |
| 430 | - | handler := NewHandler(registrySvc, testLogger()) | |
| 430 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) | |
| 431 | 431 | ||
| 432 | 432 | chunkData := []byte("chunk content") | |
| 433 | 433 | registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", chunkData).Return(int64(len(chunkData)), nil) | |
| @@ -441,15 +441,25 @@ func TestHandler_BlobUpload_PATCH_ReturnsDockerUploadUUID(t *testing.T) { | |||
| 441 | 441 | assert.Equal(t, "550e8400-e29b-41d4-a716-446655440000", rec.Header().Get("Docker-Upload-UUID")) | |
| 442 | 442 | } | |
| 443 | 443 | ||
| 444 | - | func TestMaxBlobChunkSize_Under100MB(t *testing.T) { | |
| 445 | - | // Verify the constant leaves headroom below Cloudflare's 100MB limit | |
| 446 | - | assert.Less(t, int64(MaxBlobChunkSize), int64(100*1024*1024)) | |
| 447 | - | assert.Greater(t, int64(MaxBlobChunkSize), int64(90*1024*1024)) | |
| 444 | + | func TestDefaultMaxBlobChunkSize_Is512MB(t *testing.T) { | |
| 445 | + | assert.Equal(t, int64(512*1024*1024), int64(DefaultMaxBlobChunkSize)) | |
| 446 | + | } | |
| 447 | + | ||
| 448 | + | func TestHandler_BlobUpload_PATCH_RespectsConfiguredMaxBlobChunkSize(t *testing.T) { | |
| 449 | + | registrySvc := inmocks.NewMockRegistryService(t) | |
| 450 | + | handler := NewHandler(registrySvc, testLogger(), 5) | |
| 451 | + | ||
| 452 | + | req := httptest.NewRequest("PATCH", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000", bytes.NewReader([]byte("chunk content"))) | |
| 453 | + | rec := httptest.NewRecorder() | |
| 454 | + | ||
| 455 | + | handler.ServeHTTP(rec, req) | |
| 456 | + | ||
| 457 | + | assert.Equal(t, http.StatusRequestEntityTooLarge, rec.Code) | |
| 448 | 458 | } | |
| 449 | 459 | ||
| 450 | 460 | func TestHandler_BlobUpload_PATCH_ReturnsRangeHeader(t *testing.T) { | |
| 451 | 461 | registrySvc := inmocks.NewMockRegistryService(t) | |
| 452 | - | handler := NewHandler(registrySvc, testLogger()) | |
| 462 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) | |
| 453 | 463 | ||
| 454 | 464 | chunkData := []byte("chunk content") | |
| 455 | 465 | registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", chunkData).Return(int64(len(chunkData)), nil) | |
| @@ -23,7 +23,7 @@ func testLogger() zerowrap.Logger { | |||||
| 23 | func TestHandler_Base(t *testing.T) { | 23 | func TestHandler_Base(t *testing.T) { | ||
| 24 | registrySvc := inmocks.NewMockRegistryService(t) | 24 | registrySvc := inmocks.NewMockRegistryService(t) | ||
| 25 | 25 | ||||
| 26 | - | handler := NewHandler(registrySvc, testLogger()) | 26 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) |
| 27 | 27 | ||||
| 28 | req := httptest.NewRequest("GET", "/v2/", nil) | 28 | req := httptest.NewRequest("GET", "/v2/", nil) | ||
| 29 | rec := httptest.NewRecorder() | 29 | rec := httptest.NewRecorder() | ||
| @@ -37,7 +37,7 @@ func TestHandler_Base(t *testing.T) { | |||||
| 37 | func TestHandler_NotFound(t *testing.T) { | 37 | func TestHandler_NotFound(t *testing.T) { | ||
| 38 | registrySvc := inmocks.NewMockRegistryService(t) | 38 | registrySvc := inmocks.NewMockRegistryService(t) | ||
| 39 | 39 | ||||
| 40 | - | handler := NewHandler(registrySvc, testLogger()) | 40 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) |
| 41 | 41 | ||||
| 42 | req := httptest.NewRequest("GET", "/v2/unknown/path", nil) | 42 | req := httptest.NewRequest("GET", "/v2/unknown/path", nil) | ||
| 43 | rec := httptest.NewRecorder() | 43 | rec := httptest.NewRecorder() | ||
| @@ -50,7 +50,7 @@ func TestHandler_NotFound(t *testing.T) { | |||||
| 50 | func TestHandler_GetManifest_Success(t *testing.T) { | 50 | func TestHandler_GetManifest_Success(t *testing.T) { | ||
| 51 | registrySvc := inmocks.NewMockRegistryService(t) | 51 | registrySvc := inmocks.NewMockRegistryService(t) | ||
| 52 | 52 | ||||
| 53 | - | handler := NewHandler(registrySvc, testLogger()) | 53 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) |
| 54 | 54 | ||||
| 55 | manifestData := []byte(`{"schemaVersion": 2}`) | 55 | manifestData := []byte(`{"schemaVersion": 2}`) | ||
| 56 | registrySvc.EXPECT().GetManifest(mock.Anything, "myapp", "latest").Return(&domain.Manifest{ | 56 | registrySvc.EXPECT().GetManifest(mock.Anything, "myapp", "latest").Return(&domain.Manifest{ | ||
| @@ -73,7 +73,7 @@ func TestHandler_GetManifest_Success(t *testing.T) { | |||||
| 73 | func TestHandler_GetManifest_HEAD(t *testing.T) { | 73 | func TestHandler_GetManifest_HEAD(t *testing.T) { | ||
| 74 | registrySvc := inmocks.NewMockRegistryService(t) | 74 | registrySvc := inmocks.NewMockRegistryService(t) | ||
| 75 | 75 | ||||
| 76 | - | handler := NewHandler(registrySvc, testLogger()) | 76 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) |
| 77 | 77 | ||||
| 78 | manifestData := []byte(`{"schemaVersion": 2}`) | 78 | manifestData := []byte(`{"schemaVersion": 2}`) | ||
| 79 | registrySvc.EXPECT().GetManifest(mock.Anything, "myapp", "latest").Return(&domain.Manifest{ | 79 | registrySvc.EXPECT().GetManifest(mock.Anything, "myapp", "latest").Return(&domain.Manifest{ | ||
| @@ -97,7 +97,7 @@ func TestHandler_GetManifest_HEAD(t *testing.T) { | |||||
| 97 | func TestHandler_GetManifest_NotFound(t *testing.T) { | 97 | func TestHandler_GetManifest_NotFound(t *testing.T) { | ||
| 98 | registrySvc := inmocks.NewMockRegistryService(t) | 98 | registrySvc := inmocks.NewMockRegistryService(t) | ||
| 99 | 99 | ||||
| 100 | - | handler := NewHandler(registrySvc, testLogger()) | 100 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) |
| 101 | 101 | ||||
| 102 | registrySvc.EXPECT().GetManifest(mock.Anything, "myapp", "notexists").Return(nil, assert.AnError) | 102 | registrySvc.EXPECT().GetManifest(mock.Anything, "myapp", "notexists").Return(nil, assert.AnError) | ||
| 103 | 103 | ||||
| @@ -113,7 +113,7 @@ func TestHandler_GetManifest_NotFound(t *testing.T) { | |||||
| 113 | func TestHandler_GetManifest_NestedName(t *testing.T) { | 113 | func TestHandler_GetManifest_NestedName(t *testing.T) { | ||
| 114 | registrySvc := inmocks.NewMockRegistryService(t) | 114 | registrySvc := inmocks.NewMockRegistryService(t) | ||
| 115 | 115 | ||||
| 116 | - | handler := NewHandler(registrySvc, testLogger()) | 116 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) |
| 117 | 117 | ||||
| 118 | manifestData := []byte(`{"schemaVersion": 2}`) | 118 | manifestData := []byte(`{"schemaVersion": 2}`) | ||
| 119 | registrySvc.EXPECT().GetManifest(mock.Anything, "org/project/app", "v1.0").Return(&domain.Manifest{ | 119 | registrySvc.EXPECT().GetManifest(mock.Anything, "org/project/app", "v1.0").Return(&domain.Manifest{ | ||
| @@ -134,7 +134,7 @@ func TestHandler_GetManifest_NestedName(t *testing.T) { | |||||
| 134 | func TestHandler_PutManifest_Success(t *testing.T) { | 134 | func TestHandler_PutManifest_Success(t *testing.T) { | ||
| 135 | registrySvc := inmocks.NewMockRegistryService(t) | 135 | registrySvc := inmocks.NewMockRegistryService(t) | ||
| 136 | 136 | ||||
| 137 | - | handler := NewHandler(registrySvc, testLogger()) | 137 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) |
| 138 | 138 | ||||
| 139 | manifestData := []byte(`{"schemaVersion": 2}`) | 139 | manifestData := []byte(`{"schemaVersion": 2}`) | ||
| 140 | registrySvc.EXPECT().PutManifest(mock.Anything, mock.MatchedBy(func(m *domain.Manifest) bool { | 140 | registrySvc.EXPECT().PutManifest(mock.Anything, mock.MatchedBy(func(m *domain.Manifest) bool { | ||
| @@ -155,7 +155,7 @@ func TestHandler_PutManifest_Success(t *testing.T) { | |||||
| 155 | func TestHandler_PutManifest_MissingContentType(t *testing.T) { | 155 | func TestHandler_PutManifest_MissingContentType(t *testing.T) { | ||
| 156 | registrySvc := inmocks.NewMockRegistryService(t) | 156 | registrySvc := inmocks.NewMockRegistryService(t) | ||
| 157 | 157 | ||||
| 158 | - | handler := NewHandler(registrySvc, testLogger()) | 158 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) |
| 159 | 159 | ||||
| 160 | manifestData := []byte(`{"schemaVersion": 2}`) | 160 | manifestData := []byte(`{"schemaVersion": 2}`) | ||
| 161 | 161 | ||||
| @@ -171,7 +171,7 @@ func TestHandler_PutManifest_MissingContentType(t *testing.T) { | |||||
| 171 | func TestHandler_PutManifest_StorageError(t *testing.T) { | 171 | func TestHandler_PutManifest_StorageError(t *testing.T) { | ||
| 172 | registrySvc := inmocks.NewMockRegistryService(t) | 172 | registrySvc := inmocks.NewMockRegistryService(t) | ||
| 173 | 173 | ||||
| 174 | - | handler := NewHandler(registrySvc, testLogger()) | 174 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) |
| 175 | 175 | ||||
| 176 | manifestData := []byte(`{"schemaVersion": 2}`) | 176 | manifestData := []byte(`{"schemaVersion": 2}`) | ||
| 177 | registrySvc.EXPECT().PutManifest(mock.Anything, mock.Anything).Return("", assert.AnError) | 177 | registrySvc.EXPECT().PutManifest(mock.Anything, mock.Anything).Return("", assert.AnError) | ||
| @@ -188,7 +188,7 @@ func TestHandler_PutManifest_StorageError(t *testing.T) { | |||||
| 188 | func TestHandler_ManifestRoutes_MethodNotAllowed(t *testing.T) { | 188 | func TestHandler_ManifestRoutes_MethodNotAllowed(t *testing.T) { | ||
| 189 | registrySvc := inmocks.NewMockRegistryService(t) | 189 | registrySvc := inmocks.NewMockRegistryService(t) | ||
| 190 | 190 | ||||
| 191 | - | handler := NewHandler(registrySvc, testLogger()) | 191 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) |
| 192 | 192 | ||||
| 193 | req := httptest.NewRequest("DELETE", "/v2/myapp/manifests/latest", nil) | 193 | req := httptest.NewRequest("DELETE", "/v2/myapp/manifests/latest", nil) | ||
| 194 | rec := httptest.NewRecorder() | 194 | rec := httptest.NewRecorder() | ||
| @@ -201,7 +201,7 @@ func TestHandler_ManifestRoutes_MethodNotAllowed(t *testing.T) { | |||||
| 201 | func TestHandler_GetBlob_Success(t *testing.T) { | 201 | func TestHandler_GetBlob_Success(t *testing.T) { | ||
| 202 | registrySvc := inmocks.NewMockRegistryService(t) | 202 | registrySvc := inmocks.NewMockRegistryService(t) | ||
| 203 | 203 | ||||
| 204 | - | handler := NewHandler(registrySvc, testLogger()) | 204 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) |
| 205 | 205 | ||||
| 206 | // Create a temp file for http.ServeFile | 206 | // Create a temp file for http.ServeFile | ||
| 207 | tmpFile, err := os.CreateTemp("", "blob-*") | 207 | tmpFile, err := os.CreateTemp("", "blob-*") | ||
| @@ -226,7 +226,7 @@ func TestHandler_GetBlob_Success(t *testing.T) { | |||||
| 226 | func TestHandler_GetBlob_NotFound(t *testing.T) { | 226 | func TestHandler_GetBlob_NotFound(t *testing.T) { | ||
| 227 | registrySvc := inmocks.NewMockRegistryService(t) | 227 | registrySvc := inmocks.NewMockRegistryService(t) | ||
| 228 | 228 | ||||
| 229 | - | handler := NewHandler(registrySvc, testLogger()) | 229 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) |
| 230 | 230 | ||||
| 231 | registrySvc.EXPECT().GetBlobPath(mock.Anything, "sha256:0000000000000000000000000000000000000000000000000000000000000000").Return("", assert.AnError) | 231 | registrySvc.EXPECT().GetBlobPath(mock.Anything, "sha256:0000000000000000000000000000000000000000000000000000000000000000").Return("", assert.AnError) | ||
| 232 | 232 | ||||
| @@ -241,7 +241,7 @@ func TestHandler_GetBlob_NotFound(t *testing.T) { | |||||
| 241 | func TestHandler_BlobRoutes_MethodNotAllowed(t *testing.T) { | 241 | func TestHandler_BlobRoutes_MethodNotAllowed(t *testing.T) { | ||
| 242 | registrySvc := inmocks.NewMockRegistryService(t) | 242 | registrySvc := inmocks.NewMockRegistryService(t) | ||
| 243 | 243 | ||||
| 244 | - | handler := NewHandler(registrySvc, testLogger()) | 244 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) |
| 245 | 245 | ||||
| 246 | req := httptest.NewRequest("POST", "/v2/myapp/blobs/sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", nil) | 246 | req := httptest.NewRequest("POST", "/v2/myapp/blobs/sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", nil) | ||
| 247 | rec := httptest.NewRecorder() | 247 | rec := httptest.NewRecorder() | ||
| @@ -255,7 +255,7 @@ func TestHandler_BlobRoutes_MethodNotAllowed(t *testing.T) { | |||||
| 255 | func TestHandler_StartBlobUpload_Success(t *testing.T) { | 255 | func TestHandler_StartBlobUpload_Success(t *testing.T) { | ||
| 256 | registrySvc := inmocks.NewMockRegistryService(t) | 256 | registrySvc := inmocks.NewMockRegistryService(t) | ||
| 257 | 257 | ||||
| 258 | - | handler := NewHandler(registrySvc, testLogger()) | 258 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) |
| 259 | 259 | ||||
| 260 | registrySvc.EXPECT().StartUpload(mock.Anything, "myapp").Return("550e8400-e29b-41d4-a716-446655440000", nil) | 260 | registrySvc.EXPECT().StartUpload(mock.Anything, "myapp").Return("550e8400-e29b-41d4-a716-446655440000", nil) | ||
| 261 | 261 | ||||
| @@ -272,7 +272,7 @@ func TestHandler_StartBlobUpload_Success(t *testing.T) { | |||||
| 272 | func TestHandler_StartBlobUpload_Error(t *testing.T) { | 272 | func TestHandler_StartBlobUpload_Error(t *testing.T) { | ||
| 273 | registrySvc := inmocks.NewMockRegistryService(t) | 273 | registrySvc := inmocks.NewMockRegistryService(t) | ||
| 274 | 274 | ||||
| 275 | - | handler := NewHandler(registrySvc, testLogger()) | 275 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) |
| 276 | 276 | ||||
| 277 | registrySvc.EXPECT().StartUpload(mock.Anything, "myapp").Return("", assert.AnError) | 277 | registrySvc.EXPECT().StartUpload(mock.Anything, "myapp").Return("", assert.AnError) | ||
| 278 | 278 | ||||
| @@ -288,7 +288,7 @@ func TestHandler_StartBlobUpload_Error(t *testing.T) { | |||||
| 288 | func TestHandler_BlobUpload_PATCH(t *testing.T) { | 288 | func TestHandler_BlobUpload_PATCH(t *testing.T) { | ||
| 289 | registrySvc := inmocks.NewMockRegistryService(t) | 289 | registrySvc := inmocks.NewMockRegistryService(t) | ||
| 290 | 290 | ||||
| 291 | - | handler := NewHandler(registrySvc, testLogger()) | 291 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) |
| 292 | 292 | ||||
| 293 | chunkData := []byte("chunk content") | 293 | chunkData := []byte("chunk content") | ||
| 294 | registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", chunkData).Return(int64(len(chunkData)), nil) | 294 | registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", chunkData).Return(int64(len(chunkData)), nil) | ||
| @@ -305,7 +305,7 @@ func TestHandler_BlobUpload_PATCH(t *testing.T) { | |||||
| 305 | func TestHandler_BlobUpload_PUT_Finalize(t *testing.T) { | 305 | func TestHandler_BlobUpload_PUT_Finalize(t *testing.T) { | ||
| 306 | registrySvc := inmocks.NewMockRegistryService(t) | 306 | registrySvc := inmocks.NewMockRegistryService(t) | ||
| 307 | 307 | ||||
| 308 | - | handler := NewHandler(registrySvc, testLogger()) | 308 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) |
| 309 | 309 | ||||
| 310 | chunkData := []byte("final chunk") | 310 | chunkData := []byte("final chunk") | ||
| 311 | registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", chunkData).Return(int64(len(chunkData)), nil) | 311 | registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", chunkData).Return(int64(len(chunkData)), nil) | ||
| @@ -324,7 +324,7 @@ func TestHandler_BlobUpload_PUT_Finalize(t *testing.T) { | |||||
| 324 | func TestHandler_BlobUpload_PUT_DigestMismatch(t *testing.T) { | 324 | func TestHandler_BlobUpload_PUT_DigestMismatch(t *testing.T) { | ||
| 325 | registrySvc := inmocks.NewMockRegistryService(t) | 325 | registrySvc := inmocks.NewMockRegistryService(t) | ||
| 326 | 326 | ||||
| 327 | - | handler := NewHandler(registrySvc, testLogger()) | 327 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) |
| 328 | 328 | ||||
| 329 | chunkData := []byte("final chunk") | 329 | chunkData := []byte("final chunk") | ||
| 330 | // Note: Invalid digest format is rejected by validation before reaching storage layer | 330 | // Note: Invalid digest format is rejected by validation before reaching storage layer | ||
| @@ -342,7 +342,7 @@ func TestHandler_BlobUpload_PUT_DigestMismatch(t *testing.T) { | |||||
| 342 | func TestHandler_ListTags_Success(t *testing.T) { | 342 | func TestHandler_ListTags_Success(t *testing.T) { | ||
| 343 | registrySvc := inmocks.NewMockRegistryService(t) | 343 | registrySvc := inmocks.NewMockRegistryService(t) | ||
| 344 | 344 | ||||
| 345 | - | handler := NewHandler(registrySvc, testLogger()) | 345 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) |
| 346 | 346 | ||||
| 347 | registrySvc.EXPECT().ListTags(mock.Anything, "myapp").Return([]string{"latest", "v1.0", "v2.0"}, nil) | 347 | registrySvc.EXPECT().ListTags(mock.Anything, "myapp").Return([]string{"latest", "v1.0", "v2.0"}, nil) | ||
| 348 | 348 | ||||
| @@ -366,7 +366,7 @@ func TestHandler_ListTags_Success(t *testing.T) { | |||||
| 366 | func TestHandler_ListTags_NotFound(t *testing.T) { | 366 | func TestHandler_ListTags_NotFound(t *testing.T) { | ||
| 367 | registrySvc := inmocks.NewMockRegistryService(t) | 367 | registrySvc := inmocks.NewMockRegistryService(t) | ||
| 368 | 368 | ||||
| 369 | - | handler := NewHandler(registrySvc, testLogger()) | 369 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) |
| 370 | 370 | ||||
| 371 | registrySvc.EXPECT().ListTags(mock.Anything, "notexists").Return(nil, assert.AnError) | 371 | registrySvc.EXPECT().ListTags(mock.Anything, "notexists").Return(nil, assert.AnError) | ||
| 372 | 372 | ||||
| @@ -382,7 +382,7 @@ func TestHandler_ListTags_NotFound(t *testing.T) { | |||||
| 382 | func TestHandler_ListTags_MethodNotAllowed(t *testing.T) { | 382 | func TestHandler_ListTags_MethodNotAllowed(t *testing.T) { | ||
| 383 | registrySvc := inmocks.NewMockRegistryService(t) | 383 | registrySvc := inmocks.NewMockRegistryService(t) | ||
| 384 | 384 | ||||
| 385 | - | handler := NewHandler(registrySvc, testLogger()) | 385 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) |
| 386 | 386 | ||||
| 387 | req := httptest.NewRequest("POST", "/v2/myapp/tags/list", nil) | 387 | req := httptest.NewRequest("POST", "/v2/myapp/tags/list", nil) | ||
| 388 | rec := httptest.NewRecorder() | 388 | rec := httptest.NewRecorder() | ||
| @@ -396,7 +396,7 @@ func TestHandler_ListTags_MethodNotAllowed(t *testing.T) { | |||||
| 396 | func TestHandler_RegisterRoutes(t *testing.T) { | 396 | func TestHandler_RegisterRoutes(t *testing.T) { | ||
| 397 | registrySvc := inmocks.NewMockRegistryService(t) | 397 | registrySvc := inmocks.NewMockRegistryService(t) | ||
| 398 | 398 | ||||
| 399 | - | handler := NewHandler(registrySvc, testLogger()) | 399 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) |
| 400 | 400 | ||||
| 401 | mux := http.NewServeMux() | 401 | mux := http.NewServeMux() | ||
| 402 | handler.RegisterRoutes(mux) | 402 | handler.RegisterRoutes(mux) | ||
| @@ -412,7 +412,7 @@ func TestHandler_RegisterRoutes(t *testing.T) { | |||||
| 412 | 412 | ||||
| 413 | func TestHandler_StartBlobUpload_ReturnsDockerUploadUUID(t *testing.T) { | 413 | func TestHandler_StartBlobUpload_ReturnsDockerUploadUUID(t *testing.T) { | ||
| 414 | registrySvc := inmocks.NewMockRegistryService(t) | 414 | registrySvc := inmocks.NewMockRegistryService(t) | ||
| 415 | - | handler := NewHandler(registrySvc, testLogger()) | 415 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) |
| 416 | 416 | ||||
| 417 | registrySvc.EXPECT().StartUpload(mock.Anything, "myapp").Return("550e8400-e29b-41d4-a716-446655440000", nil) | 417 | registrySvc.EXPECT().StartUpload(mock.Anything, "myapp").Return("550e8400-e29b-41d4-a716-446655440000", nil) | ||
| 418 | 418 | ||||
| @@ -427,7 +427,7 @@ func TestHandler_StartBlobUpload_ReturnsDockerUploadUUID(t *testing.T) { | |||||
| 427 | 427 | ||||
| 428 | func TestHandler_BlobUpload_PATCH_ReturnsDockerUploadUUID(t *testing.T) { | 428 | func TestHandler_BlobUpload_PATCH_ReturnsDockerUploadUUID(t *testing.T) { | ||
| 429 | registrySvc := inmocks.NewMockRegistryService(t) | 429 | registrySvc := inmocks.NewMockRegistryService(t) | ||
| 430 | - | handler := NewHandler(registrySvc, testLogger()) | 430 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) |
| 431 | 431 | ||||
| 432 | chunkData := []byte("chunk content") | 432 | chunkData := []byte("chunk content") | ||
| 433 | registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", chunkData).Return(int64(len(chunkData)), nil) | 433 | registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", chunkData).Return(int64(len(chunkData)), nil) | ||
| @@ -441,15 +441,25 @@ func TestHandler_BlobUpload_PATCH_ReturnsDockerUploadUUID(t *testing.T) { | |||||
| 441 | assert.Equal(t, "550e8400-e29b-41d4-a716-446655440000", rec.Header().Get("Docker-Upload-UUID")) | 441 | assert.Equal(t, "550e8400-e29b-41d4-a716-446655440000", rec.Header().Get("Docker-Upload-UUID")) | ||
| 442 | } | 442 | } | ||
| 443 | 443 | ||||
| 444 | - | func TestMaxBlobChunkSize_Under100MB(t *testing.T) { | 444 | + | func TestDefaultMaxBlobChunkSize_Is512MB(t *testing.T) { |
| 445 | - | // Verify the constant leaves headroom below Cloudflare's 100MB limit | 445 | + | assert.Equal(t, int64(512*1024*1024), int64(DefaultMaxBlobChunkSize)) |
| 446 | - | assert.Less(t, int64(MaxBlobChunkSize), int64(100*1024*1024)) | 446 | + | } |
| 447 | - | assert.Greater(t, int64(MaxBlobChunkSize), int64(90*1024*1024)) | 447 | + | |
| 448 | + | func TestHandler_BlobUpload_PATCH_RespectsConfiguredMaxBlobChunkSize(t *testing.T) { | |||
| 449 | + | registrySvc := inmocks.NewMockRegistryService(t) | |||
| 450 | + | handler := NewHandler(registrySvc, testLogger(), 5) | |||
| 451 | + | ||||
| 452 | + | req := httptest.NewRequest("PATCH", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000", bytes.NewReader([]byte("chunk content"))) | |||
| 453 | + | rec := httptest.NewRecorder() | |||
| 454 | + | ||||
| 455 | + | handler.ServeHTTP(rec, req) | |||
| 456 | + | ||||
| 457 | + | assert.Equal(t, http.StatusRequestEntityTooLarge, rec.Code) | |||
| 448 | } | 458 | } | ||
| 449 | 459 | ||||
| 450 | func TestHandler_BlobUpload_PATCH_ReturnsRangeHeader(t *testing.T) { | 460 | func TestHandler_BlobUpload_PATCH_ReturnsRangeHeader(t *testing.T) { | ||
| 451 | registrySvc := inmocks.NewMockRegistryService(t) | 461 | registrySvc := inmocks.NewMockRegistryService(t) | ||
| 452 | - | handler := NewHandler(registrySvc, testLogger()) | 462 | + | handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize) |
| 453 | 463 | ||||
| 454 | chunkData := []byte("chunk content") | 464 | chunkData := []byte("chunk content") | ||
| 455 | registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", chunkData).Return(int64(len(chunkData)), nil) | 465 | registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", chunkData).Return(int64(len(chunkData)), nil) | ||
M internal/app/run.go +32 -18
| @@ -68,6 +68,7 @@ type Config struct { | |||
| 68 | 68 | RegistryDomain string `mapstructure:"registry_domain"` // Deprecated: use gordon_domain | |
| 69 | 69 | DataDir string `mapstructure:"data_dir"` | |
| 70 | 70 | MaxProxyBodySize string `mapstructure:"max_proxy_body_size"` // e.g., "512MB", "1GB" | |
| 71 | + | MaxBlobChunkSize string `mapstructure:"max_blob_chunk_size"` // e.g., "512MB", "1GB" | |
| 71 | 72 | } `mapstructure:"server"` | |
| 72 | 73 | ||
| 73 | 74 | Logging struct { | |
| @@ -117,23 +118,24 @@ type Config struct { | |||
| 117 | 118 | ||
| 118 | 119 | // services holds all the services used by the application. | |
| 119 | 120 | type services struct { | |
| 120 | - | runtime *docker.Runtime | |
| 121 | - | eventBus *eventbus.InMemory | |
| 122 | - | blobStorage *filesystem.BlobStorage | |
| 123 | - | manifestStorage *filesystem.ManifestStorage | |
| 124 | - | envLoader out.EnvLoader | |
| 125 | - | logWriter *logwriter.LogWriter | |
| 126 | - | tokenStore out.TokenStore | |
| 127 | - | configSvc *config.Service | |
| 128 | - | containerSvc *container.Service | |
| 129 | - | registrySvc *registrySvc.Service | |
| 130 | - | proxySvc *proxy.Service | |
| 131 | - | authSvc *auth.Service | |
| 132 | - | authHandler *authhandler.Handler | |
| 133 | - | adminHandler *admin.Handler | |
| 134 | - | internalRegUser string | |
| 135 | - | internalRegPass string | |
| 136 | - | envDir string | |
| 121 | + | runtime *docker.Runtime | |
| 122 | + | eventBus *eventbus.InMemory | |
| 123 | + | blobStorage *filesystem.BlobStorage | |
| 124 | + | manifestStorage *filesystem.ManifestStorage | |
| 125 | + | envLoader out.EnvLoader | |
| 126 | + | logWriter *logwriter.LogWriter | |
| 127 | + | tokenStore out.TokenStore | |
| 128 | + | configSvc *config.Service | |
| 129 | + | containerSvc *container.Service | |
| 130 | + | registrySvc *registrySvc.Service | |
| 131 | + | proxySvc *proxy.Service | |
| 132 | + | authSvc *auth.Service | |
| 133 | + | authHandler *authhandler.Handler | |
| 134 | + | adminHandler *admin.Handler | |
| 135 | + | internalRegUser string | |
| 136 | + | internalRegPass string | |
| 137 | + | envDir string | |
| 138 | + | maxBlobChunkSize int64 | |
| 137 | 139 | } | |
| 138 | 140 | ||
| 139 | 141 | // Run initializes and starts the Gordon application. | |
| @@ -306,6 +308,18 @@ func createServices(ctx context.Context, v *viper.Viper, cfg Config, log zerowra | |||
| 306 | 308 | maxProxyBodySize = parsedSize | |
| 307 | 309 | } | |
| 308 | 310 | ||
| 311 | + | // Parse max_blob_chunk_size config (default: 512MB) | |
| 312 | + | maxBlobChunkSize := int64(registry.DefaultMaxBlobChunkSize) | |
| 313 | + | if cfg.Server.MaxBlobChunkSize != "" { | |
| 314 | + | parsedSize, err := bytesize.Parse(cfg.Server.MaxBlobChunkSize) | |
| 315 | + | if err != nil { | |
| 316 | + | return nil, log.WrapErrWithFields(err, "invalid server.max_blob_chunk_size configuration", map[string]any{"value": cfg.Server.MaxBlobChunkSize}) | |
| 317 | + | } | |
| 318 | + | maxBlobChunkSize = parsedSize | |
| 319 | + | } | |
| 320 | + | ||
| 321 | + | svc.maxBlobChunkSize = maxBlobChunkSize | |
| 322 | + | ||
| 309 | 323 | svc.proxySvc = proxy.NewService(svc.runtime, svc.containerSvc, svc.configSvc, proxy.Config{ | |
| 310 | 324 | RegistryDomain: cfg.Server.RegistryDomain, | |
| 311 | 325 | RegistryPort: cfg.Server.RegistryPort, | |
| @@ -971,7 +985,7 @@ func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger) (http.Ha | |||
| 971 | 985 | // This ensures consistent IP extraction across logging, rate limiting, and auth. | |
| 972 | 986 | trustedNets := middleware.ParseTrustedProxies(cfg.API.RateLimit.TrustedProxies) | |
| 973 | 987 | ||
| 974 | - | registryHandler := registry.NewHandler(svc.registrySvc, log) | |
| 988 | + | registryHandler := registry.NewHandler(svc.registrySvc, log, svc.maxBlobChunkSize) | |
| 975 | 989 | ||
| 976 | 990 | registryMiddlewares := []func(http.Handler) http.Handler{ | |
| 977 | 991 | middleware.PanicRecovery(log), | |
| @@ -68,6 +68,7 @@ type Config struct { | |||||
| 68 | RegistryDomain string `mapstructure:"registry_domain"` // Deprecated: use gordon_domain | 68 | RegistryDomain string `mapstructure:"registry_domain"` // Deprecated: use gordon_domain | ||
| 69 | DataDir string `mapstructure:"data_dir"` | 69 | DataDir string `mapstructure:"data_dir"` | ||
| 70 | MaxProxyBodySize string `mapstructure:"max_proxy_body_size"` // e.g., "512MB", "1GB" | 70 | MaxProxyBodySize string `mapstructure:"max_proxy_body_size"` // e.g., "512MB", "1GB" | ||
| 71 | + | MaxBlobChunkSize string `mapstructure:"max_blob_chunk_size"` // e.g., "512MB", "1GB" | |||
| 71 | } `mapstructure:"server"` | 72 | } `mapstructure:"server"` | ||
| 72 | 73 | ||||
| 73 | Logging struct { | 74 | Logging struct { | ||
| @@ -117,23 +118,24 @@ type Config struct { | |||||
| 117 | 118 | ||||
| 118 | // services holds all the services used by the application. | 119 | // services holds all the services used by the application. | ||
| 119 | type services struct { | 120 | type services struct { | ||
| 120 | - | runtime *docker.Runtime | 121 | + | runtime *docker.Runtime |
| 121 | - | eventBus *eventbus.InMemory | 122 | + | eventBus *eventbus.InMemory |
| 122 | - | blobStorage *filesystem.BlobStorage | 123 | + | blobStorage *filesystem.BlobStorage |
| 123 | - | manifestStorage *filesystem.ManifestStorage | 124 | + | manifestStorage *filesystem.ManifestStorage |
| 124 | - | envLoader out.EnvLoader | 125 | + | envLoader out.EnvLoader |
| 125 | - | logWriter *logwriter.LogWriter | 126 | + | logWriter *logwriter.LogWriter |
| 126 | - | tokenStore out.TokenStore | 127 | + | tokenStore out.TokenStore |
| 127 | - | configSvc *config.Service | 128 | + | configSvc *config.Service |
| 128 | - | containerSvc *container.Service | 129 | + | containerSvc *container.Service |
| 129 | - | registrySvc *registrySvc.Service | 130 | + | registrySvc *registrySvc.Service |
| 130 | - | proxySvc *proxy.Service | 131 | + | proxySvc *proxy.Service |
| 131 | - | authSvc *auth.Service | 132 | + | authSvc *auth.Service |
| 132 | - | authHandler *authhandler.Handler | 133 | + | authHandler *authhandler.Handler |
| 133 | - | adminHandler *admin.Handler | 134 | + | adminHandler *admin.Handler |
| 134 | - | internalRegUser string | 135 | + | internalRegUser string |
| 135 | - | internalRegPass string | 136 | + | internalRegPass string |
| 136 | - | envDir string | 137 | + | envDir string |
| 138 | + | maxBlobChunkSize int64 | |||
| 137 | } | 139 | } | ||
| 138 | 140 | ||||
| 139 | // Run initializes and starts the Gordon application. | 141 | // Run initializes and starts the Gordon application. | ||
| @@ -306,6 +308,18 @@ func createServices(ctx context.Context, v *viper.Viper, cfg Config, log zerowra | |||||
| 306 | maxProxyBodySize = parsedSize | 308 | maxProxyBodySize = parsedSize | ||
| 307 | } | 309 | } | ||
| 308 | 310 | ||||
| 311 | + | // Parse max_blob_chunk_size config (default: 512MB) | |||
| 312 | + | maxBlobChunkSize := int64(registry.DefaultMaxBlobChunkSize) | |||
| 313 | + | if cfg.Server.MaxBlobChunkSize != "" { | |||
| 314 | + | parsedSize, err := bytesize.Parse(cfg.Server.MaxBlobChunkSize) | |||
| 315 | + | if err != nil { | |||
| 316 | + | return nil, log.WrapErrWithFields(err, "invalid server.max_blob_chunk_size configuration", map[string]any{"value": cfg.Server.MaxBlobChunkSize}) | |||
| 317 | + | } | |||
| 318 | + | maxBlobChunkSize = parsedSize | |||
| 319 | + | } | |||
| 320 | + | ||||
| 321 | + | svc.maxBlobChunkSize = maxBlobChunkSize | |||
| 322 | + | ||||
| 309 | svc.proxySvc = proxy.NewService(svc.runtime, svc.containerSvc, svc.configSvc, proxy.Config{ | 323 | svc.proxySvc = proxy.NewService(svc.runtime, svc.containerSvc, svc.configSvc, proxy.Config{ | ||
| 310 | RegistryDomain: cfg.Server.RegistryDomain, | 324 | RegistryDomain: cfg.Server.RegistryDomain, | ||
| 311 | RegistryPort: cfg.Server.RegistryPort, | 325 | RegistryPort: cfg.Server.RegistryPort, | ||
| @@ -971,7 +985,7 @@ func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger) (http.Ha | |||||
| 971 | // This ensures consistent IP extraction across logging, rate limiting, and auth. | 985 | // This ensures consistent IP extraction across logging, rate limiting, and auth. | ||
| 972 | trustedNets := middleware.ParseTrustedProxies(cfg.API.RateLimit.TrustedProxies) | 986 | trustedNets := middleware.ParseTrustedProxies(cfg.API.RateLimit.TrustedProxies) | ||
| 973 | 987 | ||||
| 974 | - | registryHandler := registry.NewHandler(svc.registrySvc, log) | 988 | + | registryHandler := registry.NewHandler(svc.registrySvc, log, svc.maxBlobChunkSize) |
| 975 | 989 | ||||
| 976 | registryMiddlewares := []func(http.Handler) http.Handler{ | 990 | registryMiddlewares := []func(http.Handler) http.Handler{ | ||
| 977 | middleware.PanicRecovery(log), | 991 | middleware.PanicRecovery(log), | ||