feat: can edit file/dir volumes from ui in compose based apps

This commit is contained in:
Andras Bacsai 2024-04-15 19:47:17 +02:00
parent c99bb4cfd7
commit 85b33a60b3
13 changed files with 171 additions and 48 deletions

View File

@ -2,6 +2,7 @@
namespace App\Jobs; namespace App\Jobs;
use App\Models\Application;
use App\Models\ServiceApplication; use App\Models\ServiceApplication;
use App\Models\ServiceDatabase; use App\Models\ServiceDatabase;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
@ -16,11 +17,11 @@ class ServerFilesFromServerJob implements ShouldQueue, ShouldBeEncrypted
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public ServiceApplication|ServiceDatabase $service) public function __construct(public ServiceApplication|ServiceDatabase|Application $resource)
{ {
} }
public function handle() public function handle()
{ {
$this->service->getFilesFromServer(isInit: true); $this->resource->getFilesFromServer(isInit: true);
} }
} }

View File

@ -3,6 +3,7 @@
namespace App\Livewire\Project\Application; namespace App\Livewire\Project\Application;
use App\Models\Application; use App\Models\Application;
use App\Models\LocalFileVolume;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Livewire\Component; use Livewire\Component;
@ -124,7 +125,7 @@ public function mount()
} }
$this->parsedServiceDomains = $this->application->docker_compose_domains ? json_decode($this->application->docker_compose_domains, true) : []; $this->parsedServiceDomains = $this->application->docker_compose_domains ? json_decode($this->application->docker_compose_domains, true) : [];
$this->ports_exposes = $this->application->ports_exposes; $this->ports_exposes = $this->application->ports_exposes;
$this->customLabels = $this->application->parseContainerLabels(); $this->customLabels = $this->application->parseContainerLabels();
if (!$this->customLabels && $this->application->destination->server->proxyType() !== 'NONE') { if (!$this->customLabels && $this->application->destination->server->proxyType() !== 'NONE') {
$this->customLabels = str(implode("|", generateLabelsApplication($this->application)))->replace("|", "\n"); $this->customLabels = str(implode("|", generateLabelsApplication($this->application)))->replace("|", "\n");
$this->application->custom_labels = base64_encode($this->customLabels); $this->application->custom_labels = base64_encode($this->customLabels);
@ -156,8 +157,36 @@ public function loadComposeFile($isInit = false)
return; return;
} }
['parsedServices' => $this->parsedServices, 'initialDockerComposeLocation' => $this->initialDockerComposeLocation, 'initialDockerComposePrLocation' => $this->initialDockerComposePrLocation] = $this->application->loadComposeFile($isInit); ['parsedServices' => $this->parsedServices, 'initialDockerComposeLocation' => $this->initialDockerComposeLocation, 'initialDockerComposePrLocation' => $this->initialDockerComposePrLocation] = $this->application->loadComposeFile($isInit);
$compose = $this->application->parseCompose();
$services = data_get($compose, 'services');
if ($services) {
$volumes = collect($services)->map(function ($service) {
return data_get($service, 'volumes');
})->flatten()->filter(function ($volume) {
return str($volume)->startsWith('/data/coolify');
})->unique()->values();
foreach ($volumes as $volume) {
$source = Str::of($volume)->before(':');
$target = Str::of($volume)->after(':')->beforeLast(':');
LocalFileVolume::updateOrCreate(
[
'mount_path' => $target,
'resource_id' => $this->application->id,
'resource_type' => get_class($this->application)
],
[
'fs_path' => $source,
'mount_path' => $target,
'resource_id' => $this->application->id,
'resource_type' => get_class($this->application)
]
);
}
}
$this->dispatch('success', 'Docker compose file loaded.'); $this->dispatch('success', 'Docker compose file loaded.');
$this->dispatch('compose_loaded'); $this->dispatch('compose_loaded');
$this->dispatch('refreshStorages');
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->application->docker_compose_location = $this->initialDockerComposeLocation; $this->application->docker_compose_location = $this->initialDockerComposeLocation;
$this->application->docker_compose_pr_location = $this->initialDockerComposePrLocation; $this->application->docker_compose_pr_location = $this->initialDockerComposePrLocation;
@ -183,7 +212,8 @@ public function updatedApplicationBaseDirectory()
$this->loadComposeFile(); $this->loadComposeFile();
} }
} }
public function updatedApplicationFqdn() { public function updatedApplicationFqdn()
{
$this->resetDefaultLabels(); $this->resetDefaultLabels();
} }
public function updatedApplicationBuildPack() public function updatedApplicationBuildPack()

View File

@ -2,6 +2,7 @@
namespace App\Livewire\Project\Service; namespace App\Livewire\Project\Service;
use App\Models\Application;
use App\Models\LocalFileVolume; use App\Models\LocalFileVolume;
use App\Models\ServiceApplication; use App\Models\ServiceApplication;
use App\Models\ServiceDatabase; use App\Models\ServiceDatabase;
@ -12,7 +13,7 @@
class FileStorage extends Component class FileStorage extends Component
{ {
public LocalFileVolume $fileStorage; public LocalFileVolume $fileStorage;
public ServiceApplication|ServiceDatabase|StandaloneClickhouse $resource; public ServiceApplication|ServiceDatabase|StandaloneClickhouse|Application $resource;
public string $fs_path; public string $fs_path;
public ?string $workdir = null; public ?string $workdir = null;
@ -33,6 +34,43 @@ public function mount()
$this->fs_path = $this->fileStorage->fs_path; $this->fs_path = $this->fileStorage->fs_path;
} }
} }
public function convertToDirectory() {
try {
$this->fileStorage->deleteStorageOnServer();
$this->fileStorage->is_directory = true;
$this->fileStorage->content = null;
$this->fileStorage->save();
$this->fileStorage->saveStorageOnServer();
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
$this->dispatch('storagesChanged');
}
}
public function convertToFile() {
try {
$this->fileStorage->deleteStorageOnServer();
$this->fileStorage->is_directory = false;
$this->fileStorage->content = null;
$this->fileStorage->save();
$this->fileStorage->saveStorageOnServer();
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
$this->dispatch('storagesChanged');
}
}
public function delete() {
try {
$this->fileStorage->deleteStorageOnServer();
$this->fileStorage->delete();
$this->dispatch('success', 'File deleted.');
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
$this->dispatch('storagesChanged');
}
}
public function submit() public function submit()
{ {
$original = $this->fileStorage->getOriginal(); $original = $this->fileStorage->getOriginal();

View File

@ -7,12 +7,13 @@
class Storage extends Component class Storage extends Component
{ {
protected $listeners = ['addNewVolume'];
public $resource; public $resource;
public function getListeners()
public function render()
{ {
return view('livewire.project.service.storage'); return [
'addNewVolume',
'storagesChanged'=> '$refresh'
];
} }
public function addNewVolume($data) public function addNewVolume($data)
{ {
@ -32,4 +33,8 @@ public function addNewVolume($data)
return handleError($e, $this); return handleError($e, $this);
} }
} }
public function render()
{
return view('livewire.project.service.storage');
}
} }

View File

@ -7,7 +7,7 @@
class All extends Component class All extends Component
{ {
public $resource; public $resource;
protected $listeners = ['refreshStorages']; protected $listeners = ['refreshStorages', 'storagesChanged' => '$refresh'];
public function refreshStorages() public function refreshStorages()
{ {

View File

@ -954,4 +954,9 @@ public function isWatchPathsTriggered(Collection $modified_files): bool
}); });
return $matches->count() > 0; return $matches->count() > 0;
} }
public function getFilesFromServer(bool $isInit = false)
{
getFilesystemVolumesFromServer($this, $isInit);
}
} }

View File

@ -20,6 +20,26 @@ public function service()
{ {
return $this->morphTo('resource'); return $this->morphTo('resource');
} }
public function deleteStorageOnServer()
{
$isService = data_get($this->resource, 'service');
if ($isService) {
$workdir = $this->resource->service->workdir();
$server = $this->resource->service->server;
} else {
$workdir = $this->resource->workdir();
$server = $this->resource->destination->server;
}
$commands = collect([
"cd $workdir"
]);
$fs_path = data_get($this, 'fs_path');
if ($fs_path && $fs_path != '/' && $fs_path != '.' && $fs_path != '..') {
$commands->push("rm -rf $fs_path");
}
ray($commands);
return instant_remote_process($commands, $server);
}
public function saveStorageOnServer() public function saveStorageOnServer()
{ {
$isService = data_get($this->resource, 'service'); $isService = data_get($this->resource, 'service');
@ -71,7 +91,6 @@ public function saveStorageOnServer()
if ($chmod) { if ($chmod) {
$commands->push("chmod $chmod $path"); $commands->push("chmod $chmod $path");
} }
} }
} else if ($isDir == 'NOK' && $fileVolume->is_directory) { } else if ($isDir == 'NOK' && $fileVolume->is_directory) {
$commands->push("mkdir -p $path > /dev/null 2>&1 || true"); $commands->push("mkdir -p $path > /dev/null 2>&1 || true");

View File

@ -20,9 +20,9 @@ class EventServiceProvider extends ServiceProvider
MaintenanceModeDisabledNotification::class, MaintenanceModeDisabledNotification::class,
], ],
\SocialiteProviders\Manager\SocialiteWasCalled::class => [ \SocialiteProviders\Manager\SocialiteWasCalled::class => [
\SocialiteProviders\Azure\AzureExtendSocialite::class.'@handle', \SocialiteProviders\Azure\AzureExtendSocialite::class . '@handle',
], ],
ProxyStarted::class => [ ProxyStarted::class => [
ProxyStartedNotification::class, ProxyStartedNotification::class,
], ],
]; ];

View File

@ -1,7 +1,7 @@
<?php <?php
use App\Models\Application;
use App\Models\EnvironmentVariable; use App\Models\EnvironmentVariable;
use App\Models\Service;
use App\Models\ServiceApplication; use App\Models\ServiceApplication;
use App\Models\ServiceDatabase; use App\Models\ServiceDatabase;
use Illuminate\Support\Str; use Illuminate\Support\Str;
@ -21,12 +21,16 @@ function replaceVariables($variable)
return $variable->replaceFirst('$', '')->replaceFirst('{', '')->replaceLast('}', ''); return $variable->replaceFirst('$', '')->replaceFirst('{', '')->replaceLast('}', '');
} }
function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase $oneService, bool $isInit = false) function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase|Application $oneService, bool $isInit = false)
{ {
// TODO: make this async
try { try {
$workdir = $oneService->service->workdir(); if ($oneService->getMorphClass() === 'App\Models\Application') {
$server = $oneService->service->server; $workdir = $oneService->workdir();
$server = $oneService->destination->server;
} else{
$workdir = $oneService->service->workdir();
$server = $oneService->service->server;
}
$fileVolumes = $oneService->fileStorages()->get(); $fileVolumes = $oneService->fileStorages()->get();
$commands = collect([ $commands = collect([
"mkdir -p $workdir > /dev/null 2>&1 || true", "mkdir -p $workdir > /dev/null 2>&1 || true",

View File

@ -19,7 +19,7 @@
href="#">Environment href="#">Environment
Variables</a> Variables</a>
@endif @endif
@if ($application->build_pack !== 'static' && $application->build_pack !== 'dockercompose') @if ($application->build_pack !== 'static')
<a class="menu-item" :class="activeTab === 'storages' && 'menu-item-active'" <a class="menu-item" :class="activeTab === 'storages' && 'menu-item-active'"
@click.prevent="activeTab = 'storages'; window.location.hash = 'storages'" href="#">Storages @click.prevent="activeTab = 'storages'; window.location.hash = 'storages'" href="#">Storages
</a> </a>

View File

@ -1,17 +1,32 @@
<div class="p-4 transition border rounded cursor-pointer border-coolgray-200"> <div class="p-4 transition border rounded dark:border-coolgray-200">
<div class="flex flex-col justify-center pb-4 text-sm select-text"> <div class="flex flex-col justify-center pb-4 text-sm select-text">
<h2>{{ data_get($resource, 'name', 'unknown') }}</h2> <h2>{{ data_get($resource, 'name', 'unknown') }}</h2>
<div>{{ $workdir }}{{ $fs_path }} -> {{ $fileStorage->mount_path }}</div> <div>{{ $workdir }}{{ $fs_path }} -> {{ $fileStorage->mount_path }}</div>
@if ($fileStorage->is_directory)
<div class="text-xs">Directory</div>
@else
<div class="text-xs">File</div>
@endif
</div> </div>
<div> <form wire:submit='submit' class="flex flex-col gap-2">
<form wire:submit='submit' class="flex flex-col gap-2"> <div class="flex gap-2">
<div class="w-64"> @if ($fileStorage->is_directory)
<x-forms.checkbox instantSave label="Is directory?" id="fileStorage.is_directory"></x-forms.checkbox> <x-modal-confirmation action="convertToFile" buttonTitle="Convert to file">
</div> This will delete all files in this directory. It is not reversible. <br>Please think again.
@if (!$fileStorage->is_directory) </x-modal-confirmation>
<x-forms.textarea label="Content" rows="20" id="fileStorage.content"></x-forms.textarea> @else
<x-forms.button type="submit">Save</x-forms.button> <x-modal-confirmation action="convertToDirectory" buttonTitle="Convert to directory">
This will convert this to a directory. If it was a file, it will be deleted. It is not reversible. <br>Please think again.
</x-modal-confirmation>
@endif @endif
</form> <x-modal-confirmation isErrorButton buttonTitle="Delete">
</div> This file / directory will be deleted. It is not reversible. <br>Please think again.
</x-modal-confirmation>
</div>
@if (!$fileStorage->is_directory)
<x-forms.textarea label="Content" rows="20" id="fileStorage.content"></x-forms.textarea>
<x-forms.button class="w-full" type="submit">Save</x-forms.button>
@endif
</form>
</div> </div>

View File

@ -14,25 +14,30 @@
helper="For Preview Deployments, storage has a <span class='text-helper'>-pr-#PRNumber</span> in their helper="For Preview Deployments, storage has a <span class='text-helper'>-pr-#PRNumber</span> in their
volume volume
name, example: <span class='text-helper'>-pr-1</span>" /> name, example: <span class='text-helper'>-pr-1</span>" />
<x-modal-input buttonTitle="+ Add" title="New Persistent Storage"> @if ($resource?->build_pack !== 'dockercompose')
<livewire:project.shared.storages.add :uuid="$resource->uuid" /> <x-modal-input buttonTitle="+ Add" title="New Persistent Storage">
</x-modal-input> <livewire:project.shared.storages.add :uuid="$resource->uuid" />
</x-modal-input>
@endif
</div> </div>
<div class="pb-4">Persistent storage to preserve data between deployments.</div> <div class="pb-4">Persistent storage to preserve data between deployments.</div>
@if ($resource?->build_pack === 'dockercompose')
<span class="dark:text-warning text-coollabs">Please modify storage layout in your Docker Compose
file or reload the compose file to reread the storage layout.</span>
@endif
@if ($resource->persistentStorages()->get()->count() === 0 && $resource->fileStorages()->get()->count() == 0) @if ($resource->persistentStorages()->get()->count() === 0 && $resource->fileStorages()->get()->count() == 0)
<div>No storage found.</div> <div class="pt-4">No storage found.</div>
@else @endif
@if ($resource->persistentStorages()->get()->count() > 0) @if ($resource->persistentStorages()->get()->count() > 0)
<livewire:project.shared.storages.all :resource="$resource" /> <livewire:project.shared.storages.all :resource="$resource" />
@endif @endif
@if ($resource->fileStorages()->get()->count() > 0) @if ($resource->fileStorages()->get()->count() > 0)
<div class="flex flex-col gap-4 pt-4"> <div class="flex flex-col gap-4 pt-4">
@foreach ($resource->fileStorages()->get()->sort() as $fileStorage) @foreach ($resource->fileStorages()->get()->sort() as $fileStorage)
<livewire:project.service.file-storage :fileStorage="$fileStorage" <livewire:project.service.file-storage :fileStorage="$fileStorage"
wire:key="resource-{{ $resource->uuid }}" /> wire:key="resource-{{ $fileStorage->uuid }}" />
@endforeach @endforeach
</div> </div>
@endif
@endif @endif
@else @else
@if ($resource->persistentStorages()->get()->count() > 0) @if ($resource->persistentStorages()->get()->count() > 0)
@ -44,7 +49,8 @@
@if ($resource->fileStorages()->get()->count() > 0) @if ($resource->fileStorages()->get()->count() > 0)
<div class="flex flex-col gap-4 pt-4"> <div class="flex flex-col gap-4 pt-4">
@foreach ($resource->fileStorages()->get()->sort() as $fileStorage) @foreach ($resource->fileStorages()->get()->sort() as $fileStorage)
<livewire:project.service.file-storage :fileStorage="$fileStorage" wire:key="resource-{{ $resource->uuid }}" /> <livewire:project.service.file-storage :fileStorage="$fileStorage"
wire:key="resource-{{ $fileStorage->uuid }}" />
@endforeach @endforeach
</div> </div>
@endif @endif

View File

@ -12,7 +12,7 @@
</div> </div>
<div>Environment variables (secrets) for this resource.</div> <div>Environment variables (secrets) for this resource.</div>
@if ($resource->type() === 'service' || $resource?->build_pack === 'dockercompose') @if ($resource->type() === 'service' || $resource?->build_pack === 'dockercompose')
<div>Hardcoded variables are not shown here.</div> <div class="pt-4 dark:text-warning text-coollabs">Hardcoded variables are not shown here.</div>
@endif @endif
</div> </div>
@if ($view === 'normal') @if ($view === 'normal')