diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index 4df187c19..5265fbb37 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -863,7 +863,7 @@ class ApplicationsController extends Controller $application->settings->save(); } $application->refresh(); - if (! $application->settings->is_container_label_readonly_enabled) { + if ($application->settings->is_container_label_readonly_enabled) { $application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n"); $application->save(); } @@ -964,7 +964,7 @@ class ApplicationsController extends Controller $application->settings->is_build_server_enabled = $useBuildServer; $application->settings->save(); } - if (! $application->settings->is_container_label_readonly_enabled) { + if ($application->settings->is_container_label_readonly_enabled) { $application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n"); $application->save(); } @@ -1061,7 +1061,7 @@ class ApplicationsController extends Controller $application->settings->is_build_server_enabled = $useBuildServer; $application->settings->save(); } - if (! $application->settings->is_container_label_readonly_enabled) { + if ($application->settings->is_container_label_readonly_enabled) { $application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n"); $application->save(); } @@ -1150,7 +1150,7 @@ class ApplicationsController extends Controller $application->settings->is_build_server_enabled = $useBuildServer; $application->settings->save(); } - if (! $application->settings->is_container_label_readonly_enabled) { + if ($application->settings->is_container_label_readonly_enabled) { $application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n"); $application->save(); } @@ -1214,7 +1214,7 @@ class ApplicationsController extends Controller $application->settings->is_build_server_enabled = $useBuildServer; $application->settings->save(); } - if (! $application->settings->is_container_label_readonly_enabled) { + if ($application->settings->is_container_label_readonly_enabled) { $application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n"); $application->save(); } diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 49660f4a8..21b2b6d18 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -1682,7 +1682,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $this->application->custom_labels = base64_encode($labels->implode("\n")); $this->application->save(); } else { - if (! $this->application->settings->is_container_label_readonly_enabled) { + if ($this->application->settings->is_container_label_readonly_enabled) { $labels = collect(generateLabelsApplication($this->application, $this->preview)); } } diff --git a/app/Jobs/VolumeCloneJob.php b/app/Jobs/VolumeCloneJob.php new file mode 100644 index 000000000..f37a9704e --- /dev/null +++ b/app/Jobs/VolumeCloneJob.php @@ -0,0 +1,104 @@ +onQueue('high'); + } + + public function handle() + { + try { + if (! $this->targetServer || $this->targetServer->id === $this->sourceServer->id) { + $this->cloneLocalVolume(); + } else { + $this->cloneRemoteVolume(); + } + } catch (\Exception $e) { + \Log::error("Failed to copy volume data for {$this->sourceVolume}: ".$e->getMessage()); + throw $e; + } + } + + protected function cloneLocalVolume() + { + instant_remote_process([ + "docker volume create $this->targetVolume", + "docker run --rm -v $this->sourceVolume:/source -v $this->targetVolume:/target alpine sh -c 'cp -a /source/. /target/ && chown -R 1000:1000 /target'", + ], $this->sourceServer); + } + + protected function cloneRemoteVolume() + { + $sourceCloneDir = "{$this->cloneDir}/{$this->sourceVolume}"; + $targetCloneDir = "{$this->cloneDir}/{$this->targetVolume}"; + + try { + instant_remote_process([ + "mkdir -p $sourceCloneDir", + "chmod 777 $sourceCloneDir", + "docker run --rm -v $this->sourceVolume:/source -v $sourceCloneDir:/clone alpine sh -c 'cd /source && tar czf /clone/volume-data.tar.gz .'", + ], $this->sourceServer); + + instant_remote_process([ + "mkdir -p $targetCloneDir", + "chmod 777 $targetCloneDir", + ], $this->targetServer); + + instant_scp( + "$sourceCloneDir/volume-data.tar.gz", + "$targetCloneDir/volume-data.tar.gz", + $this->sourceServer, + $this->targetServer + ); + + instant_remote_process([ + "docker volume create $this->targetVolume", + "docker run --rm -v $this->targetVolume:/target -v $targetCloneDir:/clone alpine sh -c 'cd /target && tar xzf /clone/volume-data.tar.gz && chown -R 1000:1000 /target'", + ], $this->targetServer); + + } catch (\Exception $e) { + \Log::error("Failed to clone volume {$this->sourceVolume} to {$this->targetVolume}: ".$e->getMessage()); + throw $e; + } finally { + try { + instant_remote_process([ + "rm -rf $sourceCloneDir", + ], $this->sourceServer, false); + } catch (\Exception $e) { + \Log::warning('Failed to clean up source server clone directory: '.$e->getMessage()); + } + + try { + if ($this->targetServer) { + instant_remote_process([ + "rm -rf $targetCloneDir", + ], $this->targetServer, false); + } + } catch (\Exception $e) { + \Log::warning('Failed to clean up target server clone directory: '.$e->getMessage()); + } + } + } +} diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index d1cc3476c..576f87801 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -153,7 +153,7 @@ class General extends Component $this->is_preserve_repository_enabled = $this->application->settings->is_preserve_repository_enabled; $this->is_container_label_escape_enabled = $this->application->settings->is_container_label_escape_enabled; $this->customLabels = $this->application->parseContainerLabels(); - if (! $this->customLabels && $this->application->destination->server->proxyType() !== 'NONE' && ! $this->application->settings->is_container_label_readonly_enabled) { + if (! $this->customLabels && $this->application->destination->server->proxyType() !== 'NONE' && $this->application->settings->is_container_label_readonly_enabled === true) { $this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n"); $this->application->custom_labels = base64_encode($this->customLabels); $this->application->save(); diff --git a/app/Livewire/Project/CloneMe.php b/app/Livewire/Project/CloneMe.php index 593355c44..c71f6db64 100644 --- a/app/Livewire/Project/CloneMe.php +++ b/app/Livewire/Project/CloneMe.php @@ -2,6 +2,12 @@ namespace App\Livewire\Project; +use App\Actions\Application\StopApplication; +use App\Actions\Database\StartDatabase; +use App\Actions\Database\StopDatabase; +use App\Actions\Service\StartService; +use App\Actions\Service\StopService; +use App\Jobs\VolumeCloneJob; use App\Models\Environment; use App\Models\Project; use App\Models\Server; @@ -34,6 +40,8 @@ class CloneMe extends Component public string $newName = ''; + public bool $cloneVolumeData = false; + protected $messages = [ 'selectedServer' => 'Please select a server.', 'selectedDestination' => 'Please select a server & destination.', @@ -50,6 +58,11 @@ class CloneMe extends Component $this->newName = str($this->project->name.'-clone-'.(string) new Cuid2)->slug(); } + public function toggleVolumeCloning(bool $value) + { + $this->cloneVolumeData = $value; + } + public function render() { return view('livewire.project.clone-me'); @@ -108,35 +121,153 @@ class CloneMe extends Component $databases = $this->environment->databases(); $services = $this->environment->services; foreach ($applications as $application) { + $applicationSettings = $application->settings; + $uuid = (string) new Cuid2; - $newApplication = $application->replicate()->fill([ + $url = $application->fqdn; + if ($this->server->proxyType() !== 'NONE' && $applicationSettings->is_container_label_readonly_enabled === true) { + $url = generateFqdn($this->server, $uuid); + } + + $newApplication = $application->replicate([ + 'id', + 'created_at', + 'updated_at', + 'additional_servers_count', + 'additional_networks_count', + ])->fill([ 'uuid' => $uuid, - 'fqdn' => generateFqdn($this->server, $uuid), + 'fqdn' => $url, 'status' => 'exited', 'environment_id' => $environment->id, - // This is not correct, but we need to set it to something 'destination_id' => $this->selectedDestination, ]); $newApplication->save(); + + if ($newApplication->destination->server->proxyType() !== 'NONE' && $applicationSettings->is_container_label_readonly_enabled === true) { + $customLabels = str(implode('|coolify|', generateLabelsApplication($newApplication)))->replace('|coolify|', "\n"); + $newApplication->custom_labels = base64_encode($customLabels); + $newApplication->save(); + } + + $newApplication->settings()->delete(); + if ($applicationSettings) { + $newApplicationSettings = $applicationSettings->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill([ + 'application_id' => $newApplication->id, + ]); + $newApplicationSettings->save(); + } + + $tags = $application->tags; + foreach ($tags as $tag) { + $newApplication->tags()->attach($tag->id); + } + + $scheduledTasks = $application->scheduled_tasks()->get(); + foreach ($scheduledTasks as $task) { + $newTask = $task->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill([ + 'uuid' => (string) new Cuid2, + 'application_id' => $newApplication->id, + 'team_id' => currentTeam()->id, + ]); + $newTask->save(); + } + + $applicationPreviews = $application->previews()->get(); + foreach ($applicationPreviews as $preview) { + $newPreview = $preview->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill([ + 'application_id' => $newApplication->id, + 'status' => 'exited', + ]); + $newPreview->save(); + } + + $persistentVolumes = $application->persistentStorages()->get(); + foreach ($persistentVolumes as $volume) { + $newName = ''; + if (str_starts_with($volume->name, $application->uuid)) { + $newName = str($volume->name)->replace($application->uuid, $newApplication->uuid); + } else { + $newName = $newApplication->uuid.'-'.$volume->name; + } + + $newPersistentVolume = $volume->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill([ + 'name' => $newName, + 'resource_id' => $newApplication->id, + ]); + $newPersistentVolume->save(); + + if ($this->cloneVolumeData) { + try { + StopApplication::dispatch($application, false, false); + $sourceVolume = $volume->name; + $targetVolume = $newPersistentVolume->name; + $sourceServer = $application->destination->server; + $targetServer = $newApplication->destination->server; + + VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume); + + queue_application_deployment( + deployment_uuid: (string) new Cuid2, + application: $application, + server: $sourceServer, + destination: $application->destination, + no_questions_asked: true + ); + } catch (\Exception $e) { + \Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage()); + } + } + } + + $fileStorages = $application->fileStorages()->get(); + foreach ($fileStorages as $storage) { + $newStorage = $storage->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill([ + 'resource_id' => $newApplication->id, + ]); + $newStorage->save(); + } + $environmentVaribles = $application->environment_variables()->get(); foreach ($environmentVaribles as $environmentVarible) { - $newEnvironmentVariable = $environmentVarible->replicate()->fill([ + $newEnvironmentVariable = $environmentVarible->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill([ 'resourceable_id' => $newApplication->id, ]); $newEnvironmentVariable->save(); } - $persistentVolumes = $application->persistentStorages()->get(); - foreach ($persistentVolumes as $volume) { - $newPersistentVolume = $volume->replicate()->fill([ - 'name' => $newApplication->uuid.'-'.str($volume->name)->afterLast('-'), - 'resource_id' => $newApplication->id, - ]); - $newPersistentVolume->save(); - } } + foreach ($databases as $database) { $uuid = (string) new Cuid2; - $newDatabase = $database->replicate()->fill([ + $newDatabase = $database->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill([ 'uuid' => $uuid, 'status' => 'exited', 'started_at' => null, @@ -144,42 +275,294 @@ class CloneMe extends Component 'destination_id' => $this->selectedDestination, ]); $newDatabase->save(); + + $tags = $database->tags; + foreach ($tags as $tag) { + $newDatabase->tags()->attach($tag->id); + } + + $newDatabase->persistentStorages()->delete(); + $persistentVolumes = $database->persistentStorages()->get(); + foreach ($persistentVolumes as $volume) { + $originalName = $volume->name; + $newName = ''; + + if (str_starts_with($originalName, 'postgres-data-')) { + $newName = 'postgres-data-'.$newDatabase->uuid; + } elseif (str_starts_with($originalName, 'mysql-data-')) { + $newName = 'mysql-data-'.$newDatabase->uuid; + } elseif (str_starts_with($originalName, 'redis-data-')) { + $newName = 'redis-data-'.$newDatabase->uuid; + } elseif (str_starts_with($originalName, 'clickhouse-data-')) { + $newName = 'clickhouse-data-'.$newDatabase->uuid; + } elseif (str_starts_with($originalName, 'mariadb-data-')) { + $newName = 'mariadb-data-'.$newDatabase->uuid; + } elseif (str_starts_with($originalName, 'mongodb-data-')) { + $newName = 'mongodb-data-'.$newDatabase->uuid; + } elseif (str_starts_with($originalName, 'keydb-data-')) { + $newName = 'keydb-data-'.$newDatabase->uuid; + } elseif (str_starts_with($originalName, 'dragonfly-data-')) { + $newName = 'dragonfly-data-'.$newDatabase->uuid; + } else { + if (str_starts_with($volume->name, $database->uuid)) { + $newName = str($volume->name)->replace($database->uuid, $newDatabase->uuid); + } else { + $newName = $newDatabase->uuid.'-'.$volume->name; + } + } + + $newPersistentVolume = $volume->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill([ + 'name' => $newName, + 'resource_id' => $newDatabase->id, + ]); + $newPersistentVolume->save(); + + if ($this->cloneVolumeData) { + try { + StopDatabase::dispatch($database); + $sourceVolume = $volume->name; + $targetVolume = $newPersistentVolume->name; + $sourceServer = $database->destination->server; + $targetServer = $newDatabase->destination->server; + + VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume); + + StartDatabase::dispatch($database); + } catch (\Exception $e) { + \Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage()); + } + } + } + + $fileStorages = $database->fileStorages()->get(); + foreach ($fileStorages as $storage) { + $newStorage = $storage->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill([ + 'resource_id' => $newDatabase->id, + ]); + $newStorage->save(); + } + + $scheduledBackups = $database->scheduledBackups()->get(); + foreach ($scheduledBackups as $backup) { + $uuid = (string) new Cuid2; + $newBackup = $backup->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill([ + 'uuid' => $uuid, + 'database_id' => $newDatabase->id, + 'database_type' => $newDatabase->getMorphClass(), + 'team_id' => currentTeam()->id, + ]); + $newBackup->save(); + } + $environmentVaribles = $database->environment_variables()->get(); foreach ($environmentVaribles as $environmentVarible) { $payload = []; $payload['resourceable_id'] = $newDatabase->id; $payload['resourceable_type'] = $newDatabase->getMorphClass(); - $newEnvironmentVariable = $environmentVarible->replicate()->fill($payload); + $newEnvironmentVariable = $environmentVarible->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill($payload); $newEnvironmentVariable->save(); } } + foreach ($services as $service) { $uuid = (string) new Cuid2; - $newService = $service->replicate()->fill([ + $newService = $service->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill([ 'uuid' => $uuid, 'environment_id' => $environment->id, 'destination_id' => $this->selectedDestination, ]); $newService->save(); + + $tags = $service->tags; + foreach ($tags as $tag) { + $newService->tags()->attach($tag->id); + } + + $scheduledTasks = $service->scheduled_tasks()->get(); + foreach ($scheduledTasks as $task) { + $newTask = $task->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill([ + 'uuid' => (string) new Cuid2, + 'service_id' => $newService->id, + 'team_id' => currentTeam()->id, + ]); + $newTask->save(); + } + + $environmentVariables = $service->environment_variables()->get(); + foreach ($environmentVariables as $environmentVariable) { + $newEnvironmentVariable = $environmentVariable->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill([ + 'resourceable_id' => $newService->id, + 'resourceable_type' => $newService->getMorphClass(), + ]); + $newEnvironmentVariable->save(); + } + foreach ($newService->applications() as $application) { $application->update([ 'status' => 'exited', ]); + + $persistentVolumes = $application->persistentStorages()->get(); + foreach ($persistentVolumes as $volume) { + $newName = ''; + if (str_starts_with($volume->name, $application->uuid)) { + $newName = str($volume->name)->replace($application->uuid, $application->uuid); + } else { + $newName = $application->uuid.'-'.$volume->name; + } + + $newPersistentVolume = $volume->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill([ + 'name' => $newName, + 'resource_id' => $application->id, + ]); + $newPersistentVolume->save(); + + if ($this->cloneVolumeData) { + try { + StopService::dispatch($application, false, false); + $sourceVolume = $volume->name; + $targetVolume = $newPersistentVolume->name; + $sourceServer = $application->service->destination->server; + $targetServer = $newService->destination->server; + + VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume); + + StartService::dispatch($application); + } catch (\Exception $e) { + \Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage()); + } + } + } + + $fileStorages = $application->fileStorages()->get(); + foreach ($fileStorages as $storage) { + $newStorage = $storage->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill([ + 'resource_id' => $application->id, + ]); + $newStorage->save(); + } } + foreach ($newService->databases() as $database) { $database->update([ 'status' => 'exited', ]); + + $persistentVolumes = $database->persistentStorages()->get(); + foreach ($persistentVolumes as $volume) { + $newName = ''; + if (str_starts_with($volume->name, $database->uuid)) { + $newName = str($volume->name)->replace($database->uuid, $database->uuid); + } else { + $newName = $database->uuid.'-'.$volume->name; + } + + $newPersistentVolume = $volume->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill([ + 'name' => $newName, + 'resource_id' => $database->id, + ]); + $newPersistentVolume->save(); + + if ($this->cloneVolumeData) { + try { + StopService::dispatch($database->service, false, false); + $sourceVolume = $volume->name; + $targetVolume = $newPersistentVolume->name; + $sourceServer = $database->service->destination->server; + $targetServer = $newService->destination->server; + + VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume); + + StartService::dispatch($database->service); + } catch (\Exception $e) { + \Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage()); + } + } + } + + $fileStorages = $database->fileStorages()->get(); + foreach ($fileStorages as $storage) { + $newStorage = $storage->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill([ + 'resource_id' => $database->id, + ]); + $newStorage->save(); + } + + $scheduledBackups = $database->scheduledBackups()->get(); + foreach ($scheduledBackups as $backup) { + $uuid = (string) new Cuid2; + $newBackup = $backup->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill([ + 'uuid' => $uuid, + 'database_id' => $database->id, + 'database_type' => $database->getMorphClass(), + 'team_id' => currentTeam()->id, + ]); + $newBackup->save(); + } } + $newService->parse(); } - return redirect()->route('project.resource.index', [ - 'project_uuid' => $project->uuid, - 'environment_uuid' => $environment->uuid, - ]); } catch (\Exception $e) { - return handleError($e, $this); + handleError($e, $this); + + return; + } finally { + if (! isset($e)) { + return redirect()->route('project.resource.index', [ + 'project_uuid' => $project->uuid, + 'environment_uuid' => $environment->uuid, + ]); + } } } } diff --git a/app/Livewire/Project/Shared/ResourceOperations.php b/app/Livewire/Project/Shared/ResourceOperations.php index c6a32f0c7..e19f1272d 100644 --- a/app/Livewire/Project/Shared/ResourceOperations.php +++ b/app/Livewire/Project/Shared/ResourceOperations.php @@ -2,6 +2,12 @@ namespace App\Livewire\Project\Shared; +use App\Actions\Application\StopApplication; +use App\Actions\Database\StartDatabase; +use App\Actions\Database\StopDatabase; +use App\Actions\Service\StartService; +use App\Actions\Service\StopService; +use App\Jobs\VolumeCloneJob; use App\Models\Environment; use App\Models\Project; use App\Models\StandaloneDocker; @@ -21,6 +27,8 @@ class ResourceOperations extends Component public $servers; + public bool $cloneVolumeData = false; + public function mount() { $parameters = get_route_parameters(); @@ -30,6 +38,11 @@ class ResourceOperations extends Component $this->servers = currentTeam()->servers; } + public function toggleVolumeCloning(bool $value) + { + $this->cloneVolumeData = $value; + } + public function cloneTo($destination_id) { $new_destination = StandaloneDocker::find($destination_id); @@ -41,42 +54,148 @@ class ResourceOperations extends Component } $uuid = (string) new Cuid2; $server = $new_destination->server; + if ($this->resource->getMorphClass() === \App\Models\Application::class) { $name = 'clone-of-'.str($this->resource->name)->limit(20).'-'.$uuid; + $applicationSettings = $this->resource->settings; + $url = $this->resource->fqdn; - $new_resource = $this->resource->replicate()->fill([ + if ($server->proxyType() !== 'NONE' && $applicationSettings->is_container_label_readonly_enabled === true) { + $url = generateFqdn($server, $uuid); + } + + $new_resource = $this->resource->replicate([ + 'id', + 'created_at', + 'updated_at', + 'additional_servers_count', + 'additional_networks_count', + ])->fill([ 'uuid' => $uuid, 'name' => $name, - 'fqdn' => generateFqdn($server, $uuid), + 'fqdn' => $url, 'status' => 'exited', 'destination_id' => $new_destination->id, ]); $new_resource->save(); - if ($new_resource->destination->server->proxyType() !== 'NONE') { + + if ($new_resource->destination->server->proxyType() !== 'NONE' && $applicationSettings->is_container_label_readonly_enabled === true) { $customLabels = str(implode('|coolify|', generateLabelsApplication($new_resource)))->replace('|coolify|', "\n"); $new_resource->custom_labels = base64_encode($customLabels); $new_resource->save(); } + + $new_resource->settings()->delete(); + if ($applicationSettings) { + $newApplicationSettings = $applicationSettings->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill([ + 'application_id' => $new_resource->id, + ]); + $newApplicationSettings->save(); + } + + $tags = $this->resource->tags; + foreach ($tags as $tag) { + $new_resource->tags()->attach($tag->id); + } + + $scheduledTasks = $this->resource->scheduled_tasks()->get(); + foreach ($scheduledTasks as $task) { + $newTask = $task->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill([ + 'uuid' => (string) new Cuid2, + 'application_id' => $new_resource->id, + 'team_id' => currentTeam()->id, + ]); + $newTask->save(); + } + + $applicationPreviews = $this->resource->previews()->get(); + foreach ($applicationPreviews as $preview) { + $newPreview = $preview->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill([ + 'application_id' => $new_resource->id, + 'status' => 'exited', + ]); + $newPreview->save(); + } + + $persistentVolumes = $this->resource->persistentStorages()->get(); + foreach ($persistentVolumes as $volume) { + $newName = ''; + if (str_starts_with($volume->name, $this->resource->uuid)) { + $newName = str($volume->name)->replace($this->resource->uuid, $new_resource->uuid); + } else { + $newName = $new_resource->uuid.'-'.str($volume->name)->afterLast('-'); + } + + $newPersistentVolume = $volume->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill([ + 'name' => $newName, + 'resource_id' => $new_resource->id, + ]); + $newPersistentVolume->save(); + + if ($this->cloneVolumeData) { + try { + StopApplication::dispatch($this->resource, false, false); + $sourceVolume = $volume->name; + $targetVolume = $newPersistentVolume->name; + $sourceServer = $this->resource->destination->server; + $targetServer = $new_resource->destination->server; + + VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume); + + queue_application_deployment( + deployment_uuid: (string) new Cuid2, + application: $this->resource, + server: $sourceServer, + destination: $this->resource->destination, + no_questions_asked: true + ); + } catch (\Exception $e) { + \Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage()); + } + } + } + + $fileStorages = $this->resource->fileStorages()->get(); + foreach ($fileStorages as $storage) { + $newStorage = $storage->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill([ + 'resource_id' => $new_resource->id, + ]); + $newStorage->save(); + } + $environmentVaribles = $this->resource->environment_variables()->get(); foreach ($environmentVaribles as $environmentVarible) { - $newEnvironmentVariable = $environmentVarible->replicate()->fill([ + $newEnvironmentVariable = $environmentVarible->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill([ 'resourceable_id' => $new_resource->id, 'resourceable_type' => $new_resource->getMorphClass(), ]); $newEnvironmentVariable->save(); } - $persistentVolumes = $this->resource->persistentStorages()->get(); - foreach ($persistentVolumes as $volume) { - $volumeName = str($volume->name)->replace($this->resource->uuid, $new_resource->uuid)->value(); - if ($volumeName === $volume->name) { - $volumeName = $new_resource->uuid.'-'.str($volume->name)->afterLast('-'); - } - $newPersistentVolume = $volume->replicate()->fill([ - 'name' => $volumeName, - 'resource_id' => $new_resource->id, - ]); - $newPersistentVolume->save(); - } + $route = route('project.application.configuration', [ 'project_uuid' => $this->projectUuid, 'environment_uuid' => $this->environmentUuid, @@ -95,7 +214,11 @@ class ResourceOperations extends Component $this->resource->getMorphClass() === \App\Models\StandaloneClickhouse::class ) { $uuid = (string) new Cuid2; - $new_resource = $this->resource->replicate()->fill([ + $new_resource = $this->resource->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill([ 'uuid' => $uuid, 'name' => $this->resource->name.'-clone-'.$uuid, 'status' => 'exited', @@ -103,23 +226,111 @@ class ResourceOperations extends Component 'destination_id' => $new_destination->id, ]); $new_resource->save(); + + $tags = $this->resource->tags; + foreach ($tags as $tag) { + $new_resource->tags()->attach($tag->id); + } + + $new_resource->persistentStorages()->delete(); + $persistentVolumes = $this->resource->persistentStorages()->get(); + foreach ($persistentVolumes as $volume) { + $originalName = $volume->name; + $newName = ''; + + if (str_starts_with($originalName, 'postgres-data-')) { + $newName = 'postgres-data-'.$new_resource->uuid; + } elseif (str_starts_with($originalName, 'mysql-data-')) { + $newName = 'mysql-data-'.$new_resource->uuid; + } elseif (str_starts_with($originalName, 'redis-data-')) { + $newName = 'redis-data-'.$new_resource->uuid; + } elseif (str_starts_with($originalName, 'clickhouse-data-')) { + $newName = 'clickhouse-data-'.$new_resource->uuid; + } elseif (str_starts_with($originalName, 'mariadb-data-')) { + $newName = 'mariadb-data-'.$new_resource->uuid; + } elseif (str_starts_with($originalName, 'mongodb-data-')) { + $newName = 'mongodb-data-'.$new_resource->uuid; + } elseif (str_starts_with($originalName, 'keydb-data-')) { + $newName = 'keydb-data-'.$new_resource->uuid; + } elseif (str_starts_with($originalName, 'dragonfly-data-')) { + $newName = 'dragonfly-data-'.$new_resource->uuid; + } else { + if (str_starts_with($volume->name, $this->resource->uuid)) { + $newName = str($volume->name)->replace($this->resource->uuid, $new_resource->uuid); + } else { + $newName = $new_resource->uuid.'-'.$volume->name; + } + } + + $newPersistentVolume = $volume->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill([ + 'name' => $newName, + 'resource_id' => $new_resource->id, + ]); + $newPersistentVolume->save(); + + if ($this->cloneVolumeData) { + try { + StopDatabase::dispatch($this->resource); + $sourceVolume = $volume->name; + $targetVolume = $newPersistentVolume->name; + $sourceServer = $this->resource->destination->server; + $targetServer = $new_resource->destination->server; + + VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume); + + StartDatabase::dispatch($this->resource); + } catch (\Exception $e) { + \Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage()); + } + } + } + + $fileStorages = $this->resource->fileStorages()->get(); + foreach ($fileStorages as $storage) { + $newStorage = $storage->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill([ + 'resource_id' => $new_resource->id, + ]); + $newStorage->save(); + } + + $scheduledBackups = $this->resource->scheduledBackups()->get(); + foreach ($scheduledBackups as $backup) { + $uuid = (string) new Cuid2; + $newBackup = $backup->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill([ + 'uuid' => $uuid, + 'database_id' => $new_resource->id, + 'database_type' => $new_resource->getMorphClass(), + 'team_id' => currentTeam()->id, + ]); + $newBackup->save(); + } + $environmentVaribles = $this->resource->environment_variables()->get(); foreach ($environmentVaribles as $environmentVarible) { - $payload = []; - if ($this->resource->type() === 'standalone-postgresql') { - $payload['standalone_postgresql_id'] = $new_resource->id; - } elseif ($this->resource->type() === 'standalone-redis') { - $payload['standalone_redis_id'] = $new_resource->id; - } elseif ($this->resource->type() === 'standalone-mongodb') { - $payload['standalone_mongodb_id'] = $new_resource->id; - } elseif ($this->resource->type() === 'standalone-mysql') { - $payload['standalone_mysql_id'] = $new_resource->id; - } elseif ($this->resource->type() === 'standalone-mariadb') { - $payload['standalone_mariadb_id'] = $new_resource->id; - } - $newEnvironmentVariable = $environmentVarible->replicate()->fill($payload); + $payload = [ + 'resourceable_id' => $new_resource->id, + 'resourceable_type' => $new_resource->getMorphClass(), + ]; + $newEnvironmentVariable = $environmentVarible->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill($payload); $newEnvironmentVariable->save(); } + $route = route('project.database.configuration', [ 'project_uuid' => $this->projectUuid, 'environment_uuid' => $this->environmentUuid, @@ -129,23 +340,138 @@ class ResourceOperations extends Component return redirect()->to($route); } elseif ($this->resource->type() === 'service') { $uuid = (string) new Cuid2; - $new_resource = $this->resource->replicate()->fill([ + $new_resource = $this->resource->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill([ 'uuid' => $uuid, 'name' => $this->resource->name.'-clone-'.$uuid, 'destination_id' => $new_destination->id, + 'destination_type' => $new_destination->getMorphClass(), + 'server_id' => $new_destination->server_id, // server_id is probably not needed anymore because of the new polymorphic relationships (here it is needed for clone to a different server to work - but maybe we can drop the column) ]); + $new_resource->save(); + + $tags = $this->resource->tags; + foreach ($tags as $tag) { + $new_resource->tags()->attach($tag->id); + } + + $scheduledTasks = $this->resource->scheduled_tasks()->get(); + foreach ($scheduledTasks as $task) { + $newTask = $task->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill([ + 'uuid' => (string) new Cuid2, + 'service_id' => $new_resource->id, + 'team_id' => currentTeam()->id, + ]); + $newTask->save(); + } + + $environmentVariables = $this->resource->environment_variables()->get(); + foreach ($environmentVariables as $environmentVariable) { + $newEnvironmentVariable = $environmentVariable->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill([ + 'resourceable_id' => $new_resource->id, + 'resourceable_type' => $new_resource->getMorphClass(), + ]); + $newEnvironmentVariable->save(); + } + foreach ($new_resource->applications() as $application) { $application->update([ 'status' => 'exited', ]); + + $persistentVolumes = $application->persistentStorages()->get(); + foreach ($persistentVolumes as $volume) { + $newName = ''; + if (str_starts_with($volume->name, $volume->resource->uuid)) { + $newName = str($volume->name)->replace($volume->resource->uuid, $application->uuid); + } else { + $newName = $application->uuid.'-'.str($volume->name)->afterLast('-'); + } + + $newPersistentVolume = $volume->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill([ + 'name' => $newName, + 'resource_id' => $application->id, + ]); + $newPersistentVolume->save(); + + if ($this->cloneVolumeData) { + try { + StopService::dispatch($application, false, false); + $sourceVolume = $volume->name; + $targetVolume = $newPersistentVolume->name; + $sourceServer = $application->service->destination->server; + $targetServer = $new_resource->destination->server; + + VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume); + + StartService::dispatch($application); + } catch (\Exception $e) { + \Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage()); + } + } + } } + foreach ($new_resource->databases() as $database) { $database->update([ 'status' => 'exited', ]); + + $persistentVolumes = $database->persistentStorages()->get(); + foreach ($persistentVolumes as $volume) { + $newName = ''; + if (str_starts_with($volume->name, $volume->resource->uuid)) { + $newName = str($volume->name)->replace($volume->resource->uuid, $database->uuid); + } else { + $newName = $database->uuid.'-'.str($volume->name)->afterLast('-'); + } + + $newPersistentVolume = $volume->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill([ + 'name' => $newName, + 'resource_id' => $database->id, + ]); + $newPersistentVolume->save(); + + if ($this->cloneVolumeData) { + try { + StopService::dispatch($database->service, false, false); + $sourceVolume = $volume->name; + $targetVolume = $newPersistentVolume->name; + $sourceServer = $database->service->destination->server; + $targetServer = $new_resource->destination->server; + + VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume); + + StartService::dispatch($database->service); + } catch (\Exception $e) { + \Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage()); + } + } + } } + $new_resource->parse(); + $route = route('project.service.configuration', [ 'project_uuid' => $this->projectUuid, 'environment_uuid' => $this->environmentUuid, diff --git a/app/Livewire/Project/Shared/Storages/Add.php b/app/Livewire/Project/Shared/Storages/Add.php index 6e250bd90..dc015386c 100644 --- a/app/Livewire/Project/Shared/Storages/Add.php +++ b/app/Livewire/Project/Shared/Storages/Add.php @@ -81,11 +81,18 @@ class Add extends Component 'file_storage_path' => 'string', 'file_storage_content' => 'nullable|string', ]); + $this->file_storage_path = trim($this->file_storage_path); $this->file_storage_path = str($this->file_storage_path)->start('/')->value(); + if ($this->resource->getMorphClass() === \App\Models\Application::class) { $fs_path = application_configuration_dir().'/'.$this->resource->uuid.$this->file_storage_path; + } elseif (str($this->resource->getMorphClass())->contains('Standalone')) { + $fs_path = database_configuration_dir().'/'.$this->resource->uuid.$this->file_storage_path; + } else { + throw new \Exception('No valid resource type for file mount storage type!'); } + LocalFileVolume::create( [ 'fs_path' => $fs_path, @@ -109,10 +116,12 @@ class Add extends Component 'file_storage_directory_source' => 'string', 'file_storage_directory_destination' => 'string', ]); + $this->file_storage_directory_source = trim($this->file_storage_directory_source); $this->file_storage_directory_source = str($this->file_storage_directory_source)->start('/')->value(); $this->file_storage_directory_destination = trim($this->file_storage_directory_destination); $this->file_storage_directory_destination = str($this->file_storage_directory_destination)->start('/')->value(); + LocalFileVolume::create( [ 'fs_path' => $this->file_storage_directory_source, diff --git a/database/migrations/2025_01_08_154008_switch_up_readonly_labels.php b/database/migrations/2025_01_08_154008_switch_up_readonly_labels.php new file mode 100644 index 000000000..aae089d9e --- /dev/null +++ b/database/migrations/2025_01_08_154008_switch_up_readonly_labels.php @@ -0,0 +1,39 @@ +update([ + 'is_container_label_readonly_enabled' => DB::raw('NOT is_container_label_readonly_enabled'), + ]); + + Schema::table('application_settings', function (Blueprint $table) { + $table->boolean('is_container_label_readonly_enabled')->default(true)->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::table('application_settings') + ->update([ + 'is_container_label_readonly_enabled' => DB::raw('NOT is_container_label_readonly_enabled'), + ]); + + Schema::table('application_settings', function (Blueprint $table) { + $table->boolean('is_container_label_readonly_enabled')->default(false)->change(); + }); + } +}; diff --git a/resources/views/components/forms/monaco-editor.blade.php b/resources/views/components/forms/monaco-editor.blade.php index c25080cdd..690e654d4 100644 --- a/resources/views/components/forms/monaco-editor.blade.php +++ b/resources/views/components/forms/monaco-editor.blade.php @@ -54,7 +54,10 @@ fontSize: monacoFontSize, lineNumbersMinChars: 3, automaticLayout: true, - language: '{{ $language }}' + language: '{{ $language }}', + domReadOnly: '{{ $readonly ?? false }}', + contextmenu: '!{{ $readonly ?? false }}', + renderLineHighlight: '{{ $readonly ?? false }} ? none : all' }); const observer = new MutationObserver((mutations) => { @@ -95,7 +98,7 @@ }, 5);" :id="monacoId">