fix(storages): remove host path from storage configuration

This commit is contained in:
Andras Bacsai
2026-09-16 14:24:52 +02:00
parent 011d8bdb2a
commit 128939b062
13 changed files with 128 additions and 171 deletions
@@ -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;
+1 -3
View File
@@ -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.',
]));
-30
View File
@@ -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,
-24
View File
@@ -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
View File
@@ -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" />
+7 -1
View File
@@ -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([
+67
View File
@@ -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']);