diff --git a/app/Http/Livewire/Project/CloneProject.php b/app/Http/Livewire/Project/CloneProject.php index 735bbc0da..215f494a7 100644 --- a/app/Http/Livewire/Project/CloneProject.php +++ b/app/Http/Livewire/Project/CloneProject.php @@ -104,6 +104,7 @@ public function clone() $uuid = (string)new Cuid2(7); $newDatabase = $database->replicate()->fill([ 'uuid' => $uuid, + 'status' => 'exited', 'environment_id' => $newEnvironment->id, 'destination_id' => $this->selectedServer, ]); @@ -111,15 +112,15 @@ public function clone() $environmentVaribles = $database->environment_variables()->get(); foreach ($environmentVaribles as $environmentVarible) { $payload = []; - if ($database->type() === 'standalone-postgres') { + if ($database->type() === 'standalone-postgresql') { $payload['standalone_postgresql_id'] = $newDatabase->id; - } else if ($database->type() === 'standalone_redis') { + } else if ($database->type() === 'standalone-redis') { $payload['standalone_redis_id'] = $newDatabase->id; - } else if ($database->type() === 'standalone_mongodb') { + } else if ($database->type() === 'standalone-mongodb') { $payload['standalone_mongodb_id'] = $newDatabase->id; - } else if ($database->type() === 'standalone_mysql') { + } else if ($database->type() === 'standalone-mysql') { $payload['standalone_mysql_id'] = $newDatabase->id; - }else if ($database->type() === 'standalone_mariadb') { + } else if ($database->type() === 'standalone-mariadb') { $payload['standalone_mariadb_id'] = $newDatabase->id; } $newEnvironmentVariable = $environmentVarible->replicate()->fill($payload); @@ -134,6 +135,16 @@ public function clone() 'destination_id' => $this->selectedServer, ]); $newService->save(); + foreach ($newService->applications() as $application) { + $application->update([ + 'status' => 'exited', + ]); + } + foreach ($newService->databases() as $database) { + $database->update([ + 'status' => 'exited', + ]); + } $newService->parse(); } return redirect()->route('project.resources', [ diff --git a/app/Http/Livewire/Project/Database/BackupEdit.php b/app/Http/Livewire/Project/Database/BackupEdit.php index 951f95468..813016dba 100644 --- a/app/Http/Livewire/Project/Database/BackupEdit.php +++ b/app/Http/Livewire/Project/Database/BackupEdit.php @@ -3,6 +3,7 @@ namespace App\Http\Livewire\Project\Database; use Livewire\Component; +use Spatie\Url\Url; class BackupEdit extends Component { @@ -43,14 +44,23 @@ public function delete() { // TODO: Delete backup from server and add a confirmation modal $this->backup->delete(); - redirect()->route('project.database.backups.all', $this->parameters); + if ($this->backup->database->getMorphClass() === 'App\Models\ServiceDatabase') { + $previousUrl = url()->previous(); + $url = Url::fromString($previousUrl); + $url = $url->withoutQueryParameter('selectedBackupId'); + $url = $url->withFragment('backups'); + $url = $url->getPath() . "#{$url->getFragment()}"; + return redirect()->to($url); + } else { + redirect()->route('project.database.backups.all', $this->parameters); + } + } public function instantSave() { try { $this->custom_validate(); - $this->backup->save(); $this->backup->refresh(); $this->emit('success', 'Backup updated successfully'); diff --git a/app/Http/Livewire/Project/Database/BackupExecutions.php b/app/Http/Livewire/Project/Database/BackupExecutions.php index f8ec4efbe..41a1cfbd6 100644 --- a/app/Http/Livewire/Project/Database/BackupExecutions.php +++ b/app/Http/Livewire/Project/Database/BackupExecutions.php @@ -19,7 +19,11 @@ public function deleteBackup($exeuctionId) $this->emit('error', 'Backup execution not found.'); return; } - delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->destination->server); + if ($execution->scheduledDatabaseBackup->database->getMorphClass() === 'App\Models\ServiceDatabase') { + delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->service->destination->server); + } else { + delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->destination->server); + } $execution->delete(); $this->emit('success', 'Backup deleted successfully.'); $this->emit('refreshBackupExecutions'); @@ -33,7 +37,11 @@ public function download($exeuctionId) return; } $filename = data_get($execution, 'filename'); - $server = $execution->scheduledDatabaseBackup->database->destination->server; + if ($execution->scheduledDatabaseBackup->database->getMorphClass() === 'App\Models\ServiceDatabase') { + $server = $execution->scheduledDatabaseBackup->database->service->destination->server; + } else { + $server = $execution->scheduledDatabaseBackup->database->destination->server; + } $privateKeyLocation = savePrivateKeyToFs($server); $disk = Storage::build([ 'driver' => 'sftp', diff --git a/app/Http/Livewire/Project/Database/CreateScheduledBackup.php b/app/Http/Livewire/Project/Database/CreateScheduledBackup.php index f804c389d..a36266a6c 100644 --- a/app/Http/Livewire/Project/Database/CreateScheduledBackup.php +++ b/app/Http/Livewire/Project/Database/CreateScheduledBackup.php @@ -22,7 +22,8 @@ class CreateScheduledBackup extends Component 'frequency' => 'Backup Frequency', 'save_s3' => 'Save to S3', ]; - public function mount() { + public function mount() + { if ($this->s3s->count() > 0) { $this->s3_storage_id = $this->s3s->first()->id; } @@ -50,11 +51,16 @@ public function submit(): void $payload['databases_to_backup'] = $this->database->postgres_db; } else if ($this->database->type() === 'standalone-mysql') { $payload['databases_to_backup'] = $this->database->mysql_database; - }else if ($this->database->type() === 'standalone-mariadb') { + } else if ($this->database->type() === 'standalone-mariadb') { $payload['databases_to_backup'] = $this->database->mariadb_database; } - ScheduledDatabaseBackup::create($payload); - $this->emit('refreshScheduledBackups'); + + $databaseBackup = ScheduledDatabaseBackup::create($payload); + if ($this->database->getMorphClass() === 'App\Models\ServiceDatabase') { + $this->emit('refreshScheduledBackups', $databaseBackup->id); + } else { + $this->emit('refreshScheduledBackups'); + } } catch (\Throwable $e) { handleError($e, $this); } finally { diff --git a/app/Http/Livewire/Project/Database/ScheduledBackups.php b/app/Http/Livewire/Project/Database/ScheduledBackups.php index f1abbb86d..baac1bb99 100644 --- a/app/Http/Livewire/Project/Database/ScheduledBackups.php +++ b/app/Http/Livewire/Project/Database/ScheduledBackups.php @@ -8,13 +8,31 @@ class ScheduledBackups extends Component { public $database; public $parameters; + public $type; + public $selectedBackup; + public $selectedBackupId; protected $listeners = ['refreshScheduledBackups']; + protected $queryString = ['selectedBackupId']; public function mount(): void { + if ($this->selectedBackupId) { + $this->setSelectedBackup($this->selectedBackupId); + } $this->parameters = get_route_parameters(); + if ($this->database->getMorphClass() === 'App\Models\ServiceDatabase') { + $this->type = 'service-database'; + } else { + $this->type = 'database'; + } + } + public function setSelectedBackup($backupId) { + $this->selectedBackupId = $backupId; + $this->selectedBackup = $this->database->scheduledBackups->find($this->selectedBackupId); + if (is_null($this->selectedBackup)) { + $this->selectedBackupId = null; + } } - public function delete($scheduled_backup_id): void { $this->database->scheduledBackups->find($scheduled_backup_id)->delete(); @@ -22,9 +40,11 @@ public function delete($scheduled_backup_id): void $this->refreshScheduledBackups(); } - public function refreshScheduledBackups(): void + public function refreshScheduledBackups(?int $id = null): void { - ray('refreshScheduledBackups'); $this->database->refresh(); + if ($id) { + $this->setSelectedBackup($id); + } } } diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index 804233de9..1fc1a02d6 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -7,6 +7,7 @@ use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledDatabaseBackupExecution; use App\Models\Server; +use App\Models\ServiceDatabase; use App\Models\StandaloneMariadb; use App\Models\StandaloneMongodb; use App\Models\StandaloneMysql; @@ -32,7 +33,7 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted public ?Team $team = null; public Server $server; public ScheduledDatabaseBackup $backup; - public StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $database; + public StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|ServiceDatabase $database; public ?string $container_name = null; public ?ScheduledDatabaseBackupExecution $backup_log = null; @@ -48,9 +49,15 @@ public function __construct($backup) { $this->backup = $backup; $this->team = Team::find($backup->team_id); - $this->database = data_get($this->backup, 'database'); - $this->server = $this->database->destination->server; - $this->s3 = $this->backup->s3; + if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') { + $this->database = data_get($this->backup, 'database'); + $this->server = $this->database->service->server; + $this->s3 = $this->backup->s3; + } else { + $this->database = data_get($this->backup, 'database'); + $this->server = $this->database->destination->server; + $this->s3 = $this->backup->s3; + } } public function middleware(): array @@ -73,14 +80,38 @@ public function handle(): void $this->database->delete(); return; } - $status = Str::of(data_get($this->database, 'status')); if (!$status->startsWith('running') && $this->database->id !== 0) { ray('database not running'); return; } - $databaseType = $this->database->type(); - $databasesToBackup = data_get($this->backup, 'databases_to_backup'); + if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') { + $databaseType = $this->database->databaseType(); + $serviceUuid = $this->database->service->uuid; + if ($databaseType === 'standalone-postgresql') { + $this->container_name = "postgresql-$serviceUuid"; + $commands[] = "docker exec $this->container_name env | grep POSTGRES_"; + $envs = instant_remote_process($commands, $this->server); + $databasesToBackup = Str::of($envs)->after('POSTGRES_DB=')->before("\n")->value(); + $this->database->postgres_user = Str::of($envs)->after('POSTGRES_USER=')->before("\n")->value(); + } else if ($databaseType === 'standalone-mysql') { + $this->container_name = "mysql-$serviceUuid"; + $commands[] = "docker exec $this->container_name env | grep MYSQL_"; + $envs = instant_remote_process($commands, $this->server); + $databasesToBackup = Str::of($envs)->after('MYSQL_DATABASE=')->before("\n")->value(); + $this->database->mysql_root_password = Str::of($envs)->after('MYSQL_ROOT_PASSWORD=')->before("\n")->value(); + } else if ($databaseType === 'standalone-mariadb') { + $this->container_name = "mariadb-$serviceUuid"; + $commands[] = "docker exec $this->container_name env | grep MARIADB_"; + $envs = instant_remote_process($commands, $this->server); + $databasesToBackup = Str::of($envs)->after('MARIADB_DATABASE=')->before("\n")->value(); + $this->database->mysql_root_password = Str::of($envs)->after('MARIADB_ROOT_PASSWORD=')->before("\n")->value(); + } + } else { + $this->container_name = $this->database->uuid; + $databaseType = $this->database->type(); + $databasesToBackup = data_get($this->backup, 'databases_to_backup'); + } if (is_null($databasesToBackup)) { if ($databaseType === 'standalone-postgresql') { @@ -116,7 +147,6 @@ public function handle(): void return; } } - $this->container_name = $this->database->uuid; $this->backup_dir = backup_dir() . "/databases/" . Str::of($this->team->name)->slug() . '-' . $this->team->id . '/' . $this->container_name; if ($this->database->name === 'coolify-db') { @@ -314,7 +344,7 @@ private function remove_old_backups(): void if ($this->backup->number_of_backups_locally === 0) { $deletable = $this->backup->executions()->where('status', 'success'); } else { - $deletable = $this->backup->executions()->where('status', 'success')->orderByDesc('created_at')->skip($this->backup->number_of_backups_locally); + $deletable = $this->backup->executions()->where('status', 'success')->orderByDesc('created_at')->skip($this->backup->number_of_backups_locally - 1); } foreach ($deletable->get() as $execution) { delete_backup_locally($execution->filename, $this->server); diff --git a/app/Models/ServiceDatabase.php b/app/Models/ServiceDatabase.php index 900cdc3b4..d5ecbd2a6 100644 --- a/app/Models/ServiceDatabase.php +++ b/app/Models/ServiceDatabase.php @@ -20,6 +20,14 @@ public function type() { return 'service'; } + public function databaseType() + { + $image = str($this->image)->before(':'); + if ($image->value() === 'postgres') { + $image = 'postgresql'; + } + return "standalone-$image"; + } public function service() { return $this->belongsTo(Service::class); @@ -36,4 +44,8 @@ public function getFilesFromServer(bool $isInit = false) { getFilesystemVolumesFromServer($this, $isInit); } + public function scheduledBackups() + { + return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); + } } diff --git a/resources/views/livewire/project/database/scheduled-backups.blade.php b/resources/views/livewire/project/database/scheduled-backups.blade.php index ee1e9ecd0..b2be851a6 100644 --- a/resources/views/livewire/project/database/scheduled-backups.blade.php +++ b/resources/views/livewire/project/database/scheduled-backups.blade.php @@ -1,12 +1,36 @@ -
- @forelse($database->scheduledBackups as $backup) - -
Frequency: {{ $backup->frequency }}
-
Last backup: {{ data_get($backup->latest_log, 'status', 'No backup yet') }}
-
Number of backups to keep (locally): {{ $backup->number_of_backups_locally }}
-
- @empty -
No scheduled backups configured.
- @endforelse +
+
+ @forelse($database->scheduledBackups as $backup) + @if ($type === 'database') + +
Frequency: {{ $backup->frequency }}
+
Last backup: {{ data_get($backup->latest_log, 'status', 'No backup yet') }}
+
Number of backups to keep (locally): {{ $backup->number_of_backups_locally }}
+
+ @else +
+ data_get($backup, 'id') === data_get($selectedBackup, 'id'), + 'flex flex-col box border-l-2 border-transparent', + ]) + wire:click="setSelectedBackup('{{ data_get($backup, 'id') }}')"> +
Frequency: {{ $backup->frequency }}
+
Last backup: {{ data_get($backup->latest_log, 'status', 'No backup yet') }}
+
Number of backups to keep (locally): {{ $backup->number_of_backups_locally }}
+
+ @endif + @empty +
No scheduled backups configured.
+ @endforelse +
+ @if ($type === 'service-database' && $selectedBackup) +
+ +

Executions

+ +
+ @endif
diff --git a/resources/views/livewire/project/service/index.blade.php b/resources/views/livewire/project/service/index.blade.php index cd4548066..ba2bd8877 100644 --- a/resources/views/livewire/project/service/index.blade.php +++ b/resources/views/livewire/project/service/index.blade.php @@ -1,5 +1,5 @@
- +
diff --git a/resources/views/livewire/project/service/show.blade.php b/resources/views/livewire/project/service/show.blade.php index 0ac516660..bd87b7203 100644 --- a/resources/views/livewire/project/service/show.blade.php +++ b/resources/views/livewire/project/service/show.blade.php @@ -7,10 +7,14 @@ General + @click.prevent="activeTab = 'general'; window.location.hash = 'general'; if(window.location.search) window.location.search = ''" + href="#">General Storages + @click.prevent="activeTab = 'storages'; window.location.hash = 'storages'; if(window.location.search) window.location.search = ''" + href="#">Storages + Backups @if (data_get($parameters, 'service_name')) @@ -43,8 +47,15 @@
Persistent storage to preserve data between deployments.
Please modify storage layout in your Docker Compose file. - + +
+
+
+

Scheduled Backups

+ + Add +
+ +
@endisset