diff --git a/app/Livewire/Project/Service/FileStorage.php b/app/Livewire/Project/Service/FileStorage.php index 2933a8cca..7f0caaba3 100644 --- a/app/Livewire/Project/Service/FileStorage.php +++ b/app/Livewire/Project/Service/FileStorage.php @@ -34,6 +34,8 @@ class FileStorage extends Component public bool $permanently_delete = true; + public bool $isReadOnly = false; + protected $rules = [ 'fileStorage.is_directory' => 'required', 'fileStorage.fs_path' => 'required', @@ -52,6 +54,8 @@ public function mount() $this->workdir = null; $this->fs_path = $this->fileStorage->fs_path; } + + $this->isReadOnly = $this->fileStorage->isReadOnlyVolume(); } public function convertToDirectory() diff --git a/app/Livewire/Project/Shared/Storages/Show.php b/app/Livewire/Project/Shared/Storages/Show.php index 3928ee1d4..4f57cbfa6 100644 --- a/app/Livewire/Project/Shared/Storages/Show.php +++ b/app/Livewire/Project/Shared/Storages/Show.php @@ -37,6 +37,11 @@ class Show extends Component 'host_path' => 'host', ]; + public function mount() + { + $this->isReadOnly = $this->storage->isReadOnlyVolume(); + } + public function submit() { $this->authorize('update', $this->resource); diff --git a/app/Models/LocalFileVolume.php b/app/Models/LocalFileVolume.php index b3e71d75d..376ea9c5e 100644 --- a/app/Models/LocalFileVolume.php +++ b/app/Models/LocalFileVolume.php @@ -5,6 +5,7 @@ use App\Events\FileStorageChanged; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Symfony\Component\Yaml\Yaml; class LocalFileVolume extends BaseModel { @@ -192,4 +193,61 @@ public function scopeWherePlainMountPath($query, $path) { return $query->get()->where('plain_mount_path', $path); } + + // Check if this volume is read-only by parsing the docker-compose content + public function isReadOnlyVolume(): bool + { + try { + // Only check for services + $service = $this->service; + if (! $service || ! method_exists($service, 'service')) { + return false; + } + + $actualService = $service->service; + if (! $actualService || ! $actualService->docker_compose_raw) { + return false; + } + + // Parse the docker-compose content + $compose = Yaml::parse($actualService->docker_compose_raw); + if (! isset($compose['services'])) { + return false; + } + + // Find the service that this volume belongs to + $serviceName = $service->name; + if (! isset($compose['services'][$serviceName]['volumes'])) { + return false; + } + + $volumes = $compose['services'][$serviceName]['volumes']; + + // Check each volume to find a match + foreach ($volumes as $volume) { + // Volume can be string like "host:container:ro" or "host:container" + if (is_string($volume)) { + $parts = explode(':', $volume); + + // Check if this volume matches our fs_path and mount_path + if (count($parts) >= 2) { + $hostPath = $parts[0]; + $containerPath = $parts[1]; + $options = $parts[2] ?? null; + + // Match based on fs_path and mount_path + if ($hostPath === $this->fs_path && $containerPath === $this->mount_path) { + return $options === 'ro'; + } + } + } + } + + return false; + } catch (\Throwable $e) { + ray($e->getMessage(), 'Error checking read-only volume'); + + return false; + } + } } diff --git a/app/Models/LocalPersistentVolume.php b/app/Models/LocalPersistentVolume.php index 00dc15fea..e7862478b 100644 --- a/app/Models/LocalPersistentVolume.php +++ b/app/Models/LocalPersistentVolume.php @@ -4,6 +4,7 @@ use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; +use Symfony\Component\Yaml\Yaml; class LocalPersistentVolume extends Model { @@ -48,4 +49,69 @@ protected function hostPath(): Attribute } ); } + + // Check if this volume is read-only by parsing the docker-compose content + public function isReadOnlyVolume(): bool + { + try { + // Get the resource (can be application, service, or database) + $resource = $this->resource; + if (! $resource) { + return false; + } + + // Only check for services + if (! method_exists($resource, 'service')) { + return false; + } + + $actualService = $resource->service; + if (! $actualService || ! $actualService->docker_compose_raw) { + return false; + } + + // Parse the docker-compose content + $compose = Yaml::parse($actualService->docker_compose_raw); + if (! isset($compose['services'])) { + return false; + } + + // Find the service that this volume belongs to + $serviceName = $resource->name; + if (! isset($compose['services'][$serviceName]['volumes'])) { + return false; + } + + $volumes = $compose['services'][$serviceName]['volumes']; + + // Check each volume to find a match + foreach ($volumes as $volume) { + // Volume can be string like "host:container:ro" or "host:container" + if (is_string($volume)) { + $parts = explode(':', $volume); + + // Check if this volume matches our mount_path + if (count($parts) >= 2) { + $containerPath = $parts[1]; + $options = $parts[2] ?? null; + + // Match based on mount_path + // Remove leading slash from mount_path if present for comparison + $mountPath = str($this->mount_path)->ltrim('/')->toString(); + $containerPathClean = str($containerPath)->ltrim('/')->toString(); + + if ($mountPath === $containerPathClean || $this->mount_path === $containerPath) { + return $options === 'ro'; + } + } + } + } + + return false; + } catch (\Throwable $e) { + ray($e->getMessage(), 'Error checking read-only persistent volume'); + + return false; + } + } } diff --git a/resources/views/livewire/project/service/file-storage.blade.php b/resources/views/livewire/project/service/file-storage.blade.php index 3aa24b087..dc8f949fa 100644 --- a/resources/views/livewire/project/service/file-storage.blade.php +++ b/resources/views/livewire/project/service/file-storage.blade.php @@ -1,5 +1,14 @@
+ @if ($isReadOnly) +
+ @if ($fileStorage->is_directory) + This directory is mounted as read-only and cannot be modified from the UI. + @else + This file is mounted as read-only and cannot be modified from the UI. + @endif +
+ @endif
@@ -7,58 +16,75 @@
- @can('update', $resource) -
- @if ($fileStorage->is_directory) - - - @else - @if (!$fileStorage->is_binary) - + @if ($fileStorage->is_directory) + + shortConfirmationLabel="Filepath" :confirmWithPassword="false" step2ButtonText="Convert to file" /> + + @else + @if (!$fileStorage->is_binary) + + @endif + Load from + server + @endif - Load from server - - @endif -
- @endcan - @if (!$fileStorage->is_directory) - @can('update', $resource) - @if (data_get($resource, 'settings.is_preserve_repository_enabled')) -
- -
- @endif - - @if (!$fileStorage->is_based_on_git && !$fileStorage->is_binary) - Save - @endif - @else +
+ @endcan + @if (!$fileStorage->is_directory) + @can('update', $resource) + @if (data_get($resource, 'settings.is_preserve_repository_enabled')) +
+ +
+ @endif + + @if (!$fileStorage->is_based_on_git && !$fileStorage->is_binary) + Save + @endif + @else + @if (data_get($resource, 'settings.is_preserve_repository_enabled')) +
+ +
+ @endif + + @endcan + @endif + @else + {{-- Read-only view --}} + @if (!$fileStorage->is_directory) @if (data_get($resource, 'settings.is_preserve_repository_enabled'))
- @endcan + @endif @endif
diff --git a/resources/views/livewire/project/service/storage.blade.php b/resources/views/livewire/project/service/storage.blade.php index 47ee99114..d55bd801a 100644 --- a/resources/views/livewire/project/service/storage.blade.php +++ b/resources/views/livewire/project/service/storage.blade.php @@ -23,7 +23,8 @@ volumeModalOpen: false, fileModalOpen: false, directoryModalOpen: false - }" @close-storage-modal.window=" + }" + @close-storage-modal.window=" if ($event.detail === 'volume') volumeModalOpen = false; if ($event.detail === 'file') fileModalOpen = false; if ($event.detail === 'directory') directoryModalOpen = false; @@ -45,8 +46,7 @@
- + -
+ x-init="$watch('volumeModalOpen', value => { + if (value) { + $nextTick(() => { + const input = $el.querySelector('input'); + input?.focus(); + }) + } + })"> +
Docker Volumes mounted to the container.
@if ($isSwarm) -
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 +
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.
@endif
@if ($isSwarm) - @else - @endif - + Add @@ -169,15 +179,24 @@ class="absolute top-0 right-0 flex items-center justify-center w-8 h-8 mt-5 mr-5
-
+ x-init="$watch('fileModalOpen', value => { + if (value) { + $nextTick(() => { + const input = $el.querySelector('input'); + input?.focus(); + }) + } + })"> +
Actual file mounted from the host system to the container.
+ label="Destination Path" required + helper="File location inside the container" /> @@ -219,18 +238,27 @@ class="absolute top-0 right-0 flex items-center justify-center w-8 h-8 mt-5 mr-5
- + x-init="$watch('directoryModalOpen', value => { + if (value) { + $nextTick(() => { + const input = $el.querySelector('input'); + input?.focus(); + }) + } + })"> +
Directory mounted from the host system to the container.
- + Add @@ -270,19 +298,22 @@ class="absolute top-0 right-0 flex items-center justify-center w-8 h-8 mt-5 mr-5 {{-- Tabs Navigation --}}
@endif @else - @if ($resource->persistentStorages()->get()->count() > 0) -

{{ Str::headline($resource->name) }}

- @endif - @if ($resource->persistentStorages()->get()->count() > 0) - - @endif - @if ($fileStorage->count() > 0) -
- @foreach ($fileStorage->sort() as $fileStorage) - - @endforeach +
+
+
+

{{ Str::headline($resource->name) }}

+
- @endif + + @if ($resource->persistentStorages()->get()->count() === 0 && $fileStorage->count() == 0) +
No storage found.
+ @endif + + @php + $hasVolumes = $this->volumeCount > 0; + $hasFiles = $this->fileCount > 0; + $hasDirectories = $this->directoryCount > 0; + $defaultTab = $hasVolumes ? 'volumes' : ($hasFiles ? 'files' : 'directories'); + @endphp + + @if ($hasVolumes || $hasFiles || $hasDirectories) +
+ {{-- Tabs Navigation --}} +
+ + + +
+ + {{-- Tab Content --}} +
+ {{-- Volumes Tab --}} +
+ @if ($hasVolumes) + + @else +
+ No volumes configured. +
+ @endif +
+ + {{-- Files Tab --}} +
+ @if ($hasFiles) + @foreach ($this->files as $fs) + + @endforeach + @else +
+ No file mounts configured. +
+ @endif +
+ + {{-- Directories Tab --}} +
+ @if ($hasDirectories) + @foreach ($this->directories as $fs) + + @endforeach + @else +
+ No directory mounts configured. +
+ @endif +
+
+
+ @endif +
@endif
diff --git a/resources/views/livewire/project/shared/storages/show.blade.php b/resources/views/livewire/project/shared/storages/show.blade.php index 8c0ba0c06..798a97d94 100644 --- a/resources/views/livewire/project/shared/storages/show.blade.php +++ b/resources/views/livewire/project/shared/storages/show.blade.php @@ -1,6 +1,9 @@
@if ($isReadOnly) +
+ This volume is mounted as read-only and cannot be modified from the UI. +
@if ($isFirst)
@if (