mirror of
https://github.com/coollabsio/coolify.git
synced 2026-09-24 07:25:13 -05:00
fix(storages): remove host path from storage configuration
This commit is contained in:
@@ -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(),
|
||||
]);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.',
|
||||
]));
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
+5
-37
@@ -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;
|
||||
|
||||
@@ -116,25 +116,9 @@
|
||||
<p class="text-[13px] leading-5 text-neutral-500 dark:text-fg-dim">
|
||||
Mount a Docker volume inside the container.
|
||||
</p>
|
||||
@if ($isSwarm)
|
||||
<div class="text-warning">Swarm Mode detected: You need to set a shared
|
||||
volume
|
||||
(EFS/NFS/etc) on all the worker nodes if you would like to use a
|
||||
persistent
|
||||
volumes.</div>
|
||||
@endif
|
||||
<div class="flex flex-col gap-4">
|
||||
<x-forms.input canGate="update" :canResource="$resource" placeholder="pv-name"
|
||||
id="name" label="Name" required helper="Volume name." />
|
||||
@if ($isSwarm)
|
||||
<x-forms.input canGate="update" :canResource="$resource"
|
||||
placeholder="/root" id="host_path" label="Source Path" required
|
||||
helper="Directory on the host system." />
|
||||
@else
|
||||
<x-forms.input canGate="update" :canResource="$resource"
|
||||
placeholder="/root" id="host_path" label="Source Path"
|
||||
helper="Directory on the host system." />
|
||||
@endif
|
||||
<x-forms.input canGate="update" :canResource="$resource"
|
||||
placeholder="/tmp/root" id="mount_path" label="Destination Path"
|
||||
required helper="Directory inside the container." />
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
<div class="data-table w-full">
|
||||
<div class="data-table-header {{ $gridClass }}">
|
||||
<span>Volume Name</span>
|
||||
<span class="volumes-col-source">Source Path</span>
|
||||
<span>Destination Path</span>
|
||||
@if ($supportsPreviewSuffix)
|
||||
<div class="volumes-col-pr flex items-center gap-1.5">
|
||||
@@ -44,7 +43,6 @@
|
||||
$hasS3Backup = $backupMeta['s3'];
|
||||
$backupUrl = $backupMeta['url'];
|
||||
$inputsReadonly = $form['isReadOnly'];
|
||||
$displayHostPath = filled($form['hostPath']) ? $form['hostPath'] : '—';
|
||||
@endphp
|
||||
|
||||
@if ($inputsReadonly)
|
||||
@@ -59,12 +57,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="volumes-col-source min-w-0">
|
||||
<span class="volumes-mobile-label volumes-field-label">Source Path</span>
|
||||
<span class="block min-w-0 truncate text-[13px]"
|
||||
title="{{ $form['hostPath'] }}">{{ $displayHostPath }}</span>
|
||||
</div>
|
||||
|
||||
<div class="volumes-cell-dest min-w-0">
|
||||
<span class="volumes-mobile-label volumes-field-label">Destination Path</span>
|
||||
<span
|
||||
@@ -161,11 +153,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="volumes-col-source min-w-0">
|
||||
<span class="volumes-mobile-label volumes-field-label">Source Path</span>
|
||||
<x-forms.input id="forms.{{ $id }}.hostPath" placeholder="Host path (optional)" />
|
||||
</div>
|
||||
|
||||
<div class="volumes-cell-dest min-w-0">
|
||||
<span class="volumes-mobile-label volumes-field-label">Destination Path</span>
|
||||
<x-forms.input id="forms.{{ $id }}.mountPath" required
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
};
|
||||
$canUpdate = auth()->user()?->can('update', $resource) ?? false;
|
||||
$inputsReadonly = $isReadOnly || ! $canUpdate;
|
||||
$displayHostPath = filled($hostPath) ? $hostPath : '—';
|
||||
@endphp
|
||||
|
||||
@if ($inputsReadonly)
|
||||
@@ -35,13 +34,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="volumes-col-source min-w-0">
|
||||
<span class="volumes-mobile-label volumes-field-label">Source Path</span>
|
||||
<span class="block min-w-0 truncate text-[13px]" title="{{ $hostPath }}">
|
||||
{{ $displayHostPath }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="volumes-cell-dest min-w-0">
|
||||
<span class="volumes-mobile-label volumes-field-label">Destination Path</span>
|
||||
<span class="block min-w-0 truncate text-[13px] text-neutral-950 dark:text-fg"
|
||||
@@ -103,11 +95,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="volumes-col-source min-w-0">
|
||||
<span class="volumes-mobile-label volumes-field-label">Source Path</span>
|
||||
<x-forms.input id="hostPath" placeholder="Host path (optional)" />
|
||||
</div>
|
||||
|
||||
<div class="volumes-cell-dest min-w-0">
|
||||
<span class="volumes-mobile-label volumes-field-label">Destination Path</span>
|
||||
<x-forms.input id="mountPath" required placeholder="/path/in/container" />
|
||||
|
||||
@@ -12,6 +12,7 @@ use Illuminate\Support\Once;
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
config()->set('app.maintenance.store', 'array');
|
||||
InstanceSettings::query()->where('id', 0)->delete();
|
||||
InstanceSettings::query()->delete();
|
||||
$settings = new InstanceSettings(['is_mcp_server_enabled' => true]);
|
||||
@@ -123,6 +124,7 @@ test('MCP endpoint lists tools for an authenticated token', function () {
|
||||
$response->assertOk();
|
||||
|
||||
$toolNames = collect($response->json('result.tools'))->pluck('name')->all();
|
||||
expect(json_encode($response->json('result.tools')))->not->toContain('host_path');
|
||||
expect($toolNames)->toContain(
|
||||
'get_infrastructure_overview',
|
||||
'list_servers',
|
||||
@@ -139,7 +141,11 @@ test('MCP endpoint lists tools for an authenticated token', function () {
|
||||
'get_logs',
|
||||
'list_env_keys',
|
||||
);
|
||||
expect($toolNames)->not->toContain('get_resource_status');
|
||||
expect($toolNames)->not->toContain(
|
||||
'get_resource_status',
|
||||
'create_storage',
|
||||
'update_storage',
|
||||
);
|
||||
expect($toolNames)->toContain('coolify_help', 'control', 'deploy');
|
||||
});
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ it('keeps nested storage component keys stable when mounts are added or deleted'
|
||||
->not->toContain('wire:key="svc-volumes-{{ $resource->id }}-{{ $this->volumeCount }}"');
|
||||
});
|
||||
|
||||
use App\Livewire\Project\Service\Storage;
|
||||
use App\Livewire\Project\Service\VolumeBackup\Create as CreateServiceVolumeBackup;
|
||||
use App\Livewire\Project\Shared\Storages\All;
|
||||
use App\Models\Application;
|
||||
@@ -147,7 +148,7 @@ it('renders volumes as a data table with shared column headers', function () {
|
||||
->toContain('volumes-table-grid')
|
||||
->toContain('volumes-table-grid-readonly')
|
||||
->toContain('Volume Name')
|
||||
->toContain('Source Path')
|
||||
->not->toContain('Source Path')
|
||||
->toContain('Destination Path')
|
||||
->toContain('volumes-col-backup')
|
||||
->toContain('supportsPreviewSuffix')
|
||||
@@ -222,6 +223,43 @@ it('renders volumes as a data table with shared column headers', function () {
|
||||
->toMatch('/\.application-settings-form label\s*\{[^}]*font-size:\s*13px/s');
|
||||
});
|
||||
|
||||
it('keeps source paths out of the Docker volume creation form', function () {
|
||||
$view = file_get_contents(resource_path('views/livewire/project/service/storage.blade.php'));
|
||||
|
||||
expect($view)
|
||||
->not->toContain('id="host_path"')
|
||||
->not->toContain('label="Source Path"')
|
||||
->not->toContain('Swarm Mode detected');
|
||||
});
|
||||
|
||||
it('creates named Docker volumes without a source path in swarm mode', function () {
|
||||
[$application] = createApplicationWithVolume();
|
||||
$application->persistentStorages()->delete();
|
||||
|
||||
Livewire::test(Storage::class, ['resource' => $application])
|
||||
->set('isSwarm', true)
|
||||
->set('name', 'storage-app-data')
|
||||
->set('mount_path', '/data')
|
||||
->call('submitPersistentVolume')
|
||||
->assertHasNoErrors();
|
||||
|
||||
expect($application->persistentStorages()->first()->host_path)->toBeNull();
|
||||
});
|
||||
|
||||
it('removes the source path column from Docker volume views', function () {
|
||||
$allView = file_get_contents(resource_path('views/livewire/project/shared/storages/all.blade.php'));
|
||||
$showView = file_get_contents(resource_path('views/livewire/project/shared/storages/show.blade.php'));
|
||||
|
||||
expect($allView)
|
||||
->not->toContain('Source Path')
|
||||
->not->toContain('volumes-col-source')
|
||||
->not->toContain('forms.{{ $id }}.hostPath')
|
||||
->and($showView)
|
||||
->not->toContain('Source Path')
|
||||
->not->toContain('volumes-col-source')
|
||||
->not->toContain('id="hostPath"');
|
||||
});
|
||||
|
||||
it('renders volume actions and PR suffix controls as valid markup', function () {
|
||||
[$application] = createApplicationWithVolume();
|
||||
LocalPersistentVolume::create([
|
||||
|
||||
@@ -621,3 +621,70 @@ describe('DELETE /api/v1/databases/{uuid}/storages/{storage_uuid}', function ()
|
||||
expect(LocalPersistentVolume::find($vol->id))->toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
test('rejects host paths when creating persistent storage through the API', function (string $resourceType) {
|
||||
if ($resourceType === 'application') {
|
||||
$resource = createTestApplication($this);
|
||||
$url = "/api/v1/applications/{$resource->uuid}/storages";
|
||||
$payload = [];
|
||||
} elseif ($resourceType === 'database') {
|
||||
$resource = createTestDatabase($this);
|
||||
$url = "/api/v1/databases/{$resource->uuid}/storages";
|
||||
$payload = [];
|
||||
} else {
|
||||
[$service, $resource] = createTestServiceApplication($this);
|
||||
$url = "/api/v1/services/{$service->uuid}/storages";
|
||||
$payload = ['resource_uuid' => $resource->uuid];
|
||||
}
|
||||
|
||||
$storageCountBefore = $resource->persistentStorages()->count();
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$this->bearerToken,
|
||||
'Content-Type' => 'application/json',
|
||||
])->postJson($url, array_merge($payload, [
|
||||
'type' => 'persistent',
|
||||
'name' => 'blocked-bind-mount',
|
||||
'mount_path' => '/data',
|
||||
'host_path' => '/srv/data',
|
||||
]));
|
||||
|
||||
$response->assertUnprocessable()
|
||||
->assertJsonPath('errors.host_path.0', 'This field is not allowed.');
|
||||
|
||||
expect($resource->persistentStorages()->count())->toBe($storageCountBefore);
|
||||
})->with(['application', 'database', 'service']);
|
||||
|
||||
test('rejects host paths when updating persistent storage through the API', function (string $resourceType) {
|
||||
if ($resourceType === 'application') {
|
||||
$resource = createTestApplication($this);
|
||||
$url = "/api/v1/applications/{$resource->uuid}/storages";
|
||||
} elseif ($resourceType === 'database') {
|
||||
$resource = createTestDatabase($this);
|
||||
$url = "/api/v1/databases/{$resource->uuid}/storages";
|
||||
} else {
|
||||
[$service, $resource] = createTestServiceApplication($this);
|
||||
$url = "/api/v1/services/{$service->uuid}/storages";
|
||||
}
|
||||
|
||||
$storage = LocalPersistentVolume::create([
|
||||
'name' => $resource->uuid.'-data',
|
||||
'mount_path' => '/data',
|
||||
'resource_id' => $resource->id,
|
||||
'resource_type' => $resource->getMorphClass(),
|
||||
]);
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$this->bearerToken,
|
||||
'Content-Type' => 'application/json',
|
||||
])->patchJson($url, [
|
||||
'uuid' => $storage->uuid,
|
||||
'type' => 'persistent',
|
||||
'host_path' => '/srv/data',
|
||||
]);
|
||||
|
||||
$response->assertUnprocessable()
|
||||
->assertJsonPath('errors.host_path.0', 'This field is not allowed.');
|
||||
|
||||
expect($storage->refresh()->host_path)->toBeNull();
|
||||
})->with(['application', 'database', 'service']);
|
||||
|
||||
Reference in New Issue
Block a user