From 128939b06247c9e088df41ff6bc5feea58885390 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 16 Sep 2026 14:24:52 +0200 Subject: [PATCH] fix(storages): remove host path from storage configuration --- .../Api/ApplicationsController.php | 14 +--- .../Controllers/Api/DatabasesController.php | 14 +--- .../Controllers/Api/ServicesController.php | 14 +--- app/Livewire/Project/Service/Storage.php | 4 +- openapi.json | 30 --------- openapi.yaml | 24 ------- resources/css/app.css | 42 ++---------- .../project/service/storage.blade.php | 16 ----- .../project/shared/storages/all.blade.php | 13 ---- .../project/shared/storages/show.blade.php | 13 ---- tests/Feature/Mcp/McpEndpointTest.php | 8 ++- .../PersistentStorageVolumesLayoutTest.php | 40 ++++++++++- tests/Feature/StorageApiTest.php | 67 +++++++++++++++++++ 13 files changed, 128 insertions(+), 171 deletions(-) diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index bf68713e94..8cf61ef69b 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -4872,7 +4872,6 @@ class ApplicationsController extends Controller 'is_preview_suffix_enabled' => ['type' => 'boolean', 'description' => 'Whether to add -pr-N suffix for preview deployments.'], 'name' => ['type' => 'string', 'description' => 'The volume name (persistent only, not allowed for read-only storages).'], 'mount_path' => ['type' => 'string', 'description' => 'The container mount path (not allowed for read-only storages).'], - 'host_path' => ['type' => 'string', 'nullable' => true, 'description' => 'The host path (persistent only, not allowed for read-only storages).'], 'content' => ['type' => 'string', 'nullable' => true, 'description' => 'The file content (file only, not allowed for read-only storages).'], ], additionalProperties: false, @@ -4934,11 +4933,10 @@ class ApplicationsController extends Controller 'is_preview_suffix_enabled' => 'boolean', 'name' => ['string', 'regex:'.ValidationPatterns::VOLUME_NAME_PATTERN], 'mount_path' => 'string', - 'host_path' => ['string', 'nullable', 'regex:'.ValidationPatterns::DIRECTORY_PATH_PATTERN], 'content' => 'string|nullable', ]); - $allAllowedFields = ['uuid', 'id', 'type', 'is_preview_suffix_enabled', 'name', 'mount_path', 'host_path', 'content']; + $allAllowedFields = ['uuid', 'id', 'type', 'is_preview_suffix_enabled', 'name', 'mount_path', 'content']; $extraFields = array_diff(array_keys($request->all()), $allAllowedFields); if ($validator->fails() || ! empty($extraFields)) { $errors = $validator->errors(); @@ -4980,7 +4978,7 @@ class ApplicationsController extends Controller } $isReadOnly = $storage->shouldBeReadOnlyInUI(); - $editableOnlyFields = ['name', 'mount_path', 'host_path', 'content']; + $editableOnlyFields = ['name', 'mount_path', 'content']; $requestedEditableFields = array_intersect($editableOnlyFields, array_keys($request->all())); if ($isReadOnly && ! empty($requestedEditableFields)) { @@ -5019,9 +5017,6 @@ class ApplicationsController extends Controller if ($request->has('mount_path')) { $storage->mount_path = $request->mount_path; } - if ($request->has('host_path')) { - $storage->host_path = $request->host_path; - } } else { if ($request->has('mount_path')) { $storage->mount_path = $request->mount_path; @@ -5076,7 +5071,6 @@ class ApplicationsController extends Controller 'type' => ['type' => 'string', 'enum' => ['persistent', 'file'], 'description' => 'The type of storage.'], 'name' => ['type' => 'string', 'description' => 'Volume name (persistent only, required for persistent).'], 'mount_path' => ['type' => 'string', 'description' => 'The container mount path.'], - 'host_path' => ['type' => 'string', 'nullable' => true, 'description' => 'The host path (persistent only, optional).'], 'content' => ['type' => 'string', 'nullable' => true, 'description' => 'File content (file only, optional).'], 'is_directory' => ['type' => 'boolean', 'description' => 'Whether this is a directory mount (file only, default false).'], 'fs_path' => ['type' => 'string', 'description' => 'Host directory path (required when is_directory is true).'], @@ -5121,14 +5115,13 @@ class ApplicationsController extends Controller 'type' => 'required|string|in:persistent,file', 'name' => ['string', 'regex:'.ValidationPatterns::VOLUME_NAME_PATTERN], 'mount_path' => 'required|string', - 'host_path' => ['string', 'nullable', 'regex:'.ValidationPatterns::DIRECTORY_PATH_PATTERN], 'content' => 'string|nullable', 'is_directory' => 'boolean', 'is_host_file' => 'boolean', 'fs_path' => 'string', ]); - $allAllowedFields = ['type', 'name', 'mount_path', 'host_path', 'content', 'is_directory', 'is_host_file', 'fs_path']; + $allAllowedFields = ['type', 'name', 'mount_path', 'content', 'is_directory', 'is_host_file', 'fs_path']; $extraFields = array_diff(array_keys($request->all()), $allAllowedFields); if ($validator->fails() || ! empty($extraFields)) { $errors = $validator->errors(); @@ -5164,7 +5157,6 @@ class ApplicationsController extends Controller $storage = LocalPersistentVolume::create([ 'name' => $application->uuid.'-'.$request->name, 'mount_path' => $request->mount_path, - 'host_path' => $request->host_path, 'resource_id' => $application->id, 'resource_type' => $application->getMorphClass(), ]); diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index c9b28e5a2f..f101e3ec8e 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -4064,7 +4064,6 @@ class DatabasesController extends Controller 'type' => ['type' => 'string', 'enum' => ['persistent', 'file'], 'description' => 'The type of storage.'], 'name' => ['type' => 'string', 'description' => 'Volume name (persistent only, required for persistent).'], 'mount_path' => ['type' => 'string', 'description' => 'The container mount path.'], - 'host_path' => ['type' => 'string', 'nullable' => true, 'description' => 'The host path (persistent only, optional).'], 'content' => ['type' => 'string', 'nullable' => true, 'description' => 'File content (file only, optional).'], 'is_directory' => ['type' => 'boolean', 'description' => 'Whether this is a directory mount (file only, default false).'], 'fs_path' => ['type' => 'string', 'description' => 'Host directory path (required when is_directory is true).'], @@ -4109,14 +4108,13 @@ class DatabasesController extends Controller 'type' => 'required|string|in:persistent,file', 'name' => ['string', 'regex:'.ValidationPatterns::VOLUME_NAME_PATTERN], 'mount_path' => 'required|string', - 'host_path' => ['string', 'nullable', 'regex:'.ValidationPatterns::DIRECTORY_PATH_PATTERN], 'content' => 'string|nullable', 'is_directory' => 'boolean', 'is_host_file' => 'boolean', 'fs_path' => 'string', ]); - $allAllowedFields = ['type', 'name', 'mount_path', 'host_path', 'content', 'is_directory', 'is_host_file', 'fs_path']; + $allAllowedFields = ['type', 'name', 'mount_path', 'content', 'is_directory', 'is_host_file', 'fs_path']; $extraFields = array_diff(array_keys($request->all()), $allAllowedFields); if ($validator->fails() || ! empty($extraFields)) { $errors = $validator->errors(); @@ -4152,7 +4150,6 @@ class DatabasesController extends Controller $storage = LocalPersistentVolume::create([ 'name' => $database->uuid.'-'.$request->name, 'mount_path' => $request->mount_path, - 'host_path' => $request->host_path, 'resource_id' => $database->id, 'resource_type' => $database->getMorphClass(), ]); @@ -4304,7 +4301,6 @@ class DatabasesController extends Controller 'is_preview_suffix_enabled' => ['type' => 'boolean', 'description' => 'Whether to add -pr-N suffix for preview deployments.'], 'name' => ['type' => 'string', 'description' => 'The volume name (persistent only, not allowed for read-only storages).'], 'mount_path' => ['type' => 'string', 'description' => 'The container mount path (not allowed for read-only storages).'], - 'host_path' => ['type' => 'string', 'nullable' => true, 'description' => 'The host path (persistent only, not allowed for read-only storages).'], 'content' => ['type' => 'string', 'nullable' => true, 'description' => 'The file content (file only, not allowed for read-only storages).'], ], additionalProperties: false, @@ -4363,11 +4359,10 @@ class DatabasesController extends Controller 'is_preview_suffix_enabled' => 'boolean', 'name' => ['string', 'regex:'.ValidationPatterns::VOLUME_NAME_PATTERN], 'mount_path' => 'string', - 'host_path' => ['string', 'nullable', 'regex:'.ValidationPatterns::DIRECTORY_PATH_PATTERN], 'content' => 'string|nullable', ]); - $allAllowedFields = ['uuid', 'id', 'type', 'is_preview_suffix_enabled', 'name', 'mount_path', 'host_path', 'content']; + $allAllowedFields = ['uuid', 'id', 'type', 'is_preview_suffix_enabled', 'name', 'mount_path', 'content']; $extraFields = array_diff(array_keys($request->all()), $allAllowedFields); if ($validator->fails() || ! empty($extraFields)) { $errors = $validator->errors(); @@ -4409,7 +4404,7 @@ class DatabasesController extends Controller } $isReadOnly = $storage->shouldBeReadOnlyInUI(); - $editableOnlyFields = ['name', 'mount_path', 'host_path', 'content']; + $editableOnlyFields = ['name', 'mount_path', 'content']; $requestedEditableFields = array_intersect($editableOnlyFields, array_keys($request->all())); if ($isReadOnly && ! empty($requestedEditableFields)) { @@ -4448,9 +4443,6 @@ class DatabasesController extends Controller if ($request->has('mount_path')) { $storage->mount_path = $request->mount_path; } - if ($request->has('host_path')) { - $storage->host_path = $request->host_path; - } } else { if ($request->has('mount_path')) { $storage->mount_path = $request->mount_path; diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php index 5763f3c276..099c126624 100644 --- a/app/Http/Controllers/Api/ServicesController.php +++ b/app/Http/Controllers/Api/ServicesController.php @@ -2421,7 +2421,6 @@ class ServicesController extends Controller 'resource_uuid' => ['type' => 'string', 'description' => 'UUID of the service application or database sub-resource.'], 'name' => ['type' => 'string', 'description' => 'Volume name (persistent only, required for persistent).'], 'mount_path' => ['type' => 'string', 'description' => 'The container mount path.'], - 'host_path' => ['type' => 'string', 'nullable' => true, 'description' => 'The host path (persistent only, optional).'], 'content' => ['type' => 'string', 'nullable' => true, 'description' => 'File content (file only, optional).'], 'is_directory' => ['type' => 'boolean', 'description' => 'Whether this is a directory mount (file only, default false).'], 'fs_path' => ['type' => 'string', 'description' => 'Host directory path (required when is_directory is true).'], @@ -2467,14 +2466,13 @@ class ServicesController extends Controller 'resource_uuid' => 'required|string', 'name' => ['string', 'regex:'.ValidationPatterns::VOLUME_NAME_PATTERN], 'mount_path' => 'required|string', - 'host_path' => ['string', 'nullable', 'regex:'.ValidationPatterns::DIRECTORY_PATH_PATTERN], 'content' => 'string|nullable', 'is_directory' => 'boolean', 'is_host_file' => 'boolean', 'fs_path' => 'string', ]); - $allAllowedFields = ['type', 'resource_uuid', 'name', 'mount_path', 'host_path', 'content', 'is_directory', 'is_host_file', 'fs_path']; + $allAllowedFields = ['type', 'resource_uuid', 'name', 'mount_path', 'content', 'is_directory', 'is_host_file', 'fs_path']; $extraFields = array_diff(array_keys($request->all()), $allAllowedFields); if ($validator->fails() || ! empty($extraFields)) { $errors = $validator->errors(); @@ -2518,7 +2516,6 @@ class ServicesController extends Controller $storage = LocalPersistentVolume::create([ 'name' => $subResource->uuid.'-'.$request->name, 'mount_path' => $request->mount_path, - 'host_path' => $request->host_path, 'resource_id' => $subResource->id, 'resource_type' => $subResource->getMorphClass(), ]); @@ -2670,7 +2667,6 @@ class ServicesController extends Controller 'is_preview_suffix_enabled' => ['type' => 'boolean', 'description' => 'Whether to add -pr-N suffix for preview deployments.'], 'name' => ['type' => 'string', 'description' => 'The volume name (persistent only, not allowed for read-only storages).'], 'mount_path' => ['type' => 'string', 'description' => 'The container mount path (not allowed for read-only storages).'], - 'host_path' => ['type' => 'string', 'nullable' => true, 'description' => 'The host path (persistent only, not allowed for read-only storages).'], 'content' => ['type' => 'string', 'nullable' => true, 'description' => 'The file content (file only, not allowed for read-only storages).'], ], additionalProperties: false, @@ -2732,11 +2728,10 @@ class ServicesController extends Controller 'is_preview_suffix_enabled' => 'boolean', 'name' => ['string', 'regex:'.ValidationPatterns::VOLUME_NAME_PATTERN], 'mount_path' => 'string', - 'host_path' => ['string', 'nullable', 'regex:'.ValidationPatterns::DIRECTORY_PATH_PATTERN], 'content' => 'string|nullable', ]); - $allAllowedFields = ['uuid', 'id', 'type', 'is_preview_suffix_enabled', 'name', 'mount_path', 'host_path', 'content']; + $allAllowedFields = ['uuid', 'id', 'type', 'is_preview_suffix_enabled', 'name', 'mount_path', 'content']; $extraFields = array_diff(array_keys($request->all()), $allAllowedFields); if ($validator->fails() || ! empty($extraFields)) { $errors = $validator->errors(); @@ -2805,7 +2800,7 @@ class ServicesController extends Controller } $isReadOnly = $storage->shouldBeReadOnlyInUI(); - $editableOnlyFields = ['name', 'mount_path', 'host_path', 'content']; + $editableOnlyFields = ['name', 'mount_path', 'content']; $requestedEditableFields = array_intersect($editableOnlyFields, array_keys($request->all())); if ($isReadOnly && ! empty($requestedEditableFields)) { @@ -2844,9 +2839,6 @@ class ServicesController extends Controller if ($request->has('mount_path')) { $storage->mount_path = $request->mount_path; } - if ($request->has('host_path')) { - $storage->host_path = $request->host_path; - } } else { if ($request->has('mount_path')) { $storage->mount_path = $request->mount_path; diff --git a/app/Livewire/Project/Service/Storage.php b/app/Livewire/Project/Service/Storage.php index adb19a3135..b004e3c16a 100644 --- a/app/Livewire/Project/Service/Storage.php +++ b/app/Livewire/Project/Service/Storage.php @@ -208,9 +208,7 @@ class Storage extends Component $this->validate([ 'name' => ValidationPatterns::volumeNameRules(), 'mount_path' => 'required|string', - 'host_path' => $this->isSwarm - ? ['required', 'string', 'regex:'.ValidationPatterns::DIRECTORY_PATH_PATTERN] - : ['nullable', 'string', 'regex:'.ValidationPatterns::DIRECTORY_PATH_PATTERN], + 'host_path' => ['nullable', 'string', 'regex:'.ValidationPatterns::DIRECTORY_PATH_PATTERN], ], array_merge(ValidationPatterns::volumeNameMessages(), [ 'host_path.regex' => 'Host path must start with / and only contain safe path characters.', ])); diff --git a/openapi.json b/openapi.json index 718191d3cf..ce08cb3d9b 100644 --- a/openapi.json +++ b/openapi.json @@ -4345,11 +4345,6 @@ "type": "string", "description": "The container mount path." }, - "host_path": { - "type": "string", - "nullable": true, - "description": "The host path (persistent only, optional)." - }, "content": { "type": "string", "nullable": true, @@ -4456,11 +4451,6 @@ "type": "string", "description": "The container mount path (not allowed for read-only storages)." }, - "host_path": { - "type": "string", - "nullable": true, - "description": "The host path (persistent only, not allowed for read-only storages)." - }, "content": { "type": "string", "nullable": true, @@ -8769,11 +8759,6 @@ "type": "string", "description": "The container mount path." }, - "host_path": { - "type": "string", - "nullable": true, - "description": "The host path (persistent only, optional)." - }, "content": { "type": "string", "nullable": true, @@ -8880,11 +8865,6 @@ "type": "string", "description": "The container mount path (not allowed for read-only storages)." }, - "host_path": { - "type": "string", - "nullable": true, - "description": "The host path (persistent only, not allowed for read-only storages)." - }, "content": { "type": "string", "nullable": true, @@ -19156,11 +19136,6 @@ "type": "string", "description": "The container mount path." }, - "host_path": { - "type": "string", - "nullable": true, - "description": "The host path (persistent only, optional)." - }, "content": { "type": "string", "nullable": true, @@ -19267,11 +19242,6 @@ "type": "string", "description": "The container mount path (not allowed for read-only storages)." }, - "host_path": { - "type": "string", - "nullable": true, - "description": "The host path (persistent only, not allowed for read-only storages)." - }, "content": { "type": "string", "nullable": true, diff --git a/openapi.yaml b/openapi.yaml index a53ab1439a..62888a0b0f 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2828,10 +2828,6 @@ paths: mount_path: type: string description: 'The container mount path.' - host_path: - type: string - nullable: true - description: 'The host path (persistent only, optional).' content: type: string nullable: true @@ -2904,10 +2900,6 @@ paths: mount_path: type: string description: 'The container mount path (not allowed for read-only storages).' - host_path: - type: string - nullable: true - description: 'The host path (persistent only, not allowed for read-only storages).' content: type: string nullable: true @@ -5745,10 +5737,6 @@ paths: mount_path: type: string description: 'The container mount path.' - host_path: - type: string - nullable: true - description: 'The host path (persistent only, optional).' content: type: string nullable: true @@ -5821,10 +5809,6 @@ paths: mount_path: type: string description: 'The container mount path (not allowed for read-only storages).' - host_path: - type: string - nullable: true - description: 'The host path (persistent only, not allowed for read-only storages).' content: type: string nullable: true @@ -12103,10 +12087,6 @@ paths: mount_path: type: string description: 'The container mount path.' - host_path: - type: string - nullable: true - description: 'The host path (persistent only, optional).' content: type: string nullable: true @@ -12179,10 +12159,6 @@ paths: mount_path: type: string description: 'The container mount path (not allowed for read-only storages).' - host_path: - type: string - nullable: true - description: 'The host path (persistent only, not allowed for read-only storages).' content: type: string nullable: true diff --git a/resources/css/app.css b/resources/css/app.css index da912284cf..1d0cd200ae 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -2942,17 +2942,17 @@ input[type="search"]::-webkit-search-results-decoration { background: color-mix(in srgb, var(--coollabs-base) 98%, white); } -/* Persistent storage volumes: Name | Source | Destination | [PR suffix] | Backup | [Actions] */ +/* Persistent storage volumes: Name | Destination | [PR suffix] | Backup | [Actions] */ .volumes-table-grid-readonly { - grid-template-columns: minmax(12rem, 1.5fr) minmax(8rem, 1fr) minmax(8rem, 1fr) 5rem; + grid-template-columns: minmax(10rem, 1.4fr) minmax(8rem, 1fr) 5rem; } .volumes-table-grid { - grid-template-columns: minmax(10rem, 1.4fr) minmax(6rem, 1fr) minmax(6rem, 1fr) 5rem 15rem; + grid-template-columns: minmax(9rem, 1.2fr) minmax(6rem, 1fr) 5rem 15rem; } .volumes-table-grid-with-pr { - grid-template-columns: minmax(9rem, 1.2fr) minmax(5.5rem, 0.85fr) minmax(5.5rem, 0.85fr) 9.25rem 5rem 15rem; + grid-template-columns: minmax(9rem, 1.1fr) minmax(6rem, 1fr) 8.5rem 5rem 15rem; } .volumes-mobile-label { @@ -2986,35 +2986,6 @@ input[type="search"]::-webkit-search-results-decoration { padding-inline: 0.5rem; } -@media (max-width: 1100px) { - .volumes-table-grid { - grid-template-columns: minmax(9rem, 1.2fr) minmax(6rem, 1fr) 5rem 15rem; - } - - .volumes-table-grid > .volumes-col-source, - .data-table-header.volumes-table-grid > .volumes-col-source { - display: none; - } - - .volumes-table-grid-with-pr { - grid-template-columns: minmax(9rem, 1.1fr) minmax(6rem, 1fr) 8.5rem 5rem 15rem; - } - - .volumes-table-grid-with-pr > .volumes-col-source, - .data-table-header.volumes-table-grid-with-pr > .volumes-col-source { - display: none; - } - - .volumes-table-grid-readonly { - grid-template-columns: minmax(10rem, 1.4fr) minmax(8rem, 1fr) 5rem; - } - - .volumes-table-grid-readonly > .volumes-col-source, - .data-table-header.volumes-table-grid-readonly > .volumes-col-source { - display: none; - } -} - /* Phone: stacked card rows with per-field labels (table headers hidden) */ @media (max-width: 768px) { .data-table-header.volumes-table-grid, @@ -3034,10 +3005,7 @@ input[type="search"]::-webkit-search-results-decoration { min-height: 0; } - .data-table-row.volumes-table-grid > .volumes-col-source, - .data-table-row.volumes-table-grid-with-pr > .volumes-col-source, - .data-table-row.volumes-table-grid-with-pr > .volumes-col-pr, - .data-table-row.volumes-table-grid-readonly > .volumes-col-source { + .data-table-row.volumes-table-grid-with-pr > .volumes-col-pr { display: flex; flex-direction: column; gap: 0.25rem; diff --git a/resources/views/livewire/project/service/storage.blade.php b/resources/views/livewire/project/service/storage.blade.php index e2749aad06..43608188d1 100644 --- a/resources/views/livewire/project/service/storage.blade.php +++ b/resources/views/livewire/project/service/storage.blade.php @@ -116,25 +116,9 @@
Mount a Docker volume inside the container.
- @if ($isSwarm) -