mirror of
https://github.com/coollabsio/coolify.git
synced 2026-08-24 02:24:11 -05:00
refactor(storage): separate volumes from directory mounts
This commit is contained in:
@@ -77,6 +77,7 @@ class Storage extends Component
|
||||
$this->activeTab = $this->resolveDefaultTab();
|
||||
$this->fileStorage = collect();
|
||||
$this->loadFileStorageForActiveTab();
|
||||
$this->name = $this->generateDefaultVolumeName();
|
||||
}
|
||||
|
||||
public function refreshStoragesFromEvent()
|
||||
@@ -201,9 +202,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.',
|
||||
]));
|
||||
@@ -340,7 +339,7 @@ class Storage extends Component
|
||||
|
||||
public function clearForm()
|
||||
{
|
||||
$this->name = '';
|
||||
$this->name = $this->generateDefaultVolumeName();
|
||||
$this->mount_path = '';
|
||||
$this->host_path = null;
|
||||
$this->file_storage_path = '';
|
||||
@@ -373,6 +372,14 @@ class Storage extends Component
|
||||
throw new \Exception('No valid resource type for file mount storage type!');
|
||||
}
|
||||
|
||||
private function generateDefaultVolumeName(): string
|
||||
{
|
||||
return str($this->resource->name ?? 'volume')
|
||||
->slug()
|
||||
->append('-data')
|
||||
->value();
|
||||
}
|
||||
|
||||
public function fileStoragePreviewPath(): string
|
||||
{
|
||||
$path = str($this->file_storage_path)->trim();
|
||||
|
||||
@@ -107,6 +107,25 @@ class All extends Component
|
||||
$this->submit($storageId);
|
||||
}
|
||||
|
||||
public function clearHostPath(int $storageId): void
|
||||
{
|
||||
$this->authorize('update', $this->resource);
|
||||
|
||||
$storage = $this->findStorageOrFail($storageId);
|
||||
if ($storage->shouldBeReadOnlyInUI()) {
|
||||
$this->dispatch('error', 'This volume is read-only.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$storage->host_path = null;
|
||||
$storage->save();
|
||||
$this->forms[$storageId]['hostPath'] = null;
|
||||
|
||||
$this->dispatch('configurationChanged');
|
||||
$this->dispatch('success', 'Source path removed. Use a directory mount for host directory bindings.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Livewire listbox onChange cannot pass args; PR suffix fields call this via updatedForms.
|
||||
*/
|
||||
|
||||
@@ -197,13 +197,4 @@ class Show extends Component
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function clearHostPath()
|
||||
{
|
||||
$this->authorize('update', $this->resource);
|
||||
$this->hostPath = null;
|
||||
$this->storage->host_path = null;
|
||||
$this->storage->save();
|
||||
$this->dispatch('success', 'Source path removed. Use Directory Mount for host directory bindings.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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." />
|
||||
|
||||
@@ -154,7 +154,21 @@
|
||||
|
||||
<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)" />
|
||||
@if (filled($form['hostPath']))
|
||||
<div class="flex items-center gap-1.5">
|
||||
<div class="min-w-0 flex-1">
|
||||
<x-forms.input id="forms.{{ $id }}.hostPath" />
|
||||
</div>
|
||||
<x-modal-confirmation title="Remove Source Path?" isErrorButton
|
||||
buttonTitle="Remove" submitAction="clearHostPath({{ $id }})"
|
||||
:actions="[
|
||||
'Are you sure you want to remove the source path?',
|
||||
'Use a Directory Mount when you need to mount a host directory.',
|
||||
]" />
|
||||
</div>
|
||||
@else
|
||||
<span class="data-table-cell-dash">-</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="volumes-cell-dest min-w-0">
|
||||
|
||||
@@ -33,6 +33,7 @@ it('keeps storage backup schedule tables horizontally scrollable on mobile', fun
|
||||
->and($css)->toMatch('/\.backup-table-grid\s*\{[^}]*min-width:\s*50rem;/');
|
||||
});
|
||||
|
||||
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;
|
||||
@@ -206,6 +207,50 @@ 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 bind mount source paths out of the add volume form', function () {
|
||||
$storageView = file_get_contents(resource_path('views/livewire/project/service/storage.blade.php'));
|
||||
|
||||
expect($storageView)
|
||||
->not->toContain('id="host_path"')
|
||||
->not->toContain('Swarm Mode detected');
|
||||
});
|
||||
|
||||
it('creates named volumes without a host 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())
|
||||
->name->toBe($application->uuid.'-storage-app-data')
|
||||
->host_path->toBeNull();
|
||||
});
|
||||
|
||||
it('uses a resource based default name for new volumes', function () {
|
||||
[$application] = createApplicationWithVolume(['name' => 'Storage App']);
|
||||
|
||||
Livewire::test(Storage::class, ['resource' => $application])
|
||||
->assertSet('name', 'storage-app-data');
|
||||
});
|
||||
|
||||
it('removes existing bind mount source paths from the volume table', function () {
|
||||
[$application, $volume] = createApplicationWithVolume(volumeAttributes: [
|
||||
'host_path' => '/srv/storage',
|
||||
]);
|
||||
|
||||
Livewire::test(All::class, ['resource' => $application])
|
||||
->assertSet("forms.{$volume->id}.hostPath", '/srv/storage')
|
||||
->call('clearHostPath', $volume->id)
|
||||
->assertHasNoErrors();
|
||||
|
||||
expect($volume->refresh()->host_path)->toBeNull();
|
||||
});
|
||||
|
||||
it('creates and exposes volume backups for service storage', function () {
|
||||
$service = Service::factory()->create([
|
||||
'environment_id' => $this->environment->id,
|
||||
|
||||
Reference in New Issue
Block a user