fix: backup database one-by-one.

This commit is contained in:
Andras Bacsai 2023-10-13 15:45:24 +02:00
parent 49c56524e1
commit d635e5dbae
7 changed files with 104 additions and 45 deletions

View File

@ -17,6 +17,7 @@ class BackupEdit extends Component
'backup.number_of_backups_locally' => 'required|integer|min:1', 'backup.number_of_backups_locally' => 'required|integer|min:1',
'backup.save_s3' => 'required|boolean', 'backup.save_s3' => 'required|boolean',
'backup.s3_storage_id' => 'nullable|integer', 'backup.s3_storage_id' => 'nullable|integer',
'backup.databases_to_backup' => 'nullable',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
'backup.enabled' => 'Enabled', 'backup.enabled' => 'Enabled',
@ -24,6 +25,7 @@ class BackupEdit extends Component
'backup.number_of_backups_locally' => 'Number of Backups Locally', 'backup.number_of_backups_locally' => 'Number of Backups Locally',
'backup.save_s3' => 'Save to S3', 'backup.save_s3' => 'Save to S3',
'backup.s3_storage_id' => 'S3 Storage', 'backup.s3_storage_id' => 'S3 Storage',
'backup.databases_to_backup' => 'Databases to Backup',
]; ];
protected $messages = [ protected $messages = [
'backup.s3_storage_id' => 'Select a S3 Storage', 'backup.s3_storage_id' => 'Select a S3 Storage',
@ -37,7 +39,6 @@ public function mount()
} }
} }
public function delete() public function delete()
{ {
// TODO: Delete backup from server and add a confirmation modal // TODO: Delete backup from server and add a confirmation modal
@ -49,6 +50,7 @@ public function instantSave()
{ {
try { try {
$this->custom_validate(); $this->custom_validate();
$this->backup->save(); $this->backup->save();
$this->backup->refresh(); $this->backup->refresh();
$this->emit('success', 'Backup updated successfully'); $this->emit('success', 'Backup updated successfully');
@ -71,9 +73,11 @@ private function custom_validate()
public function submit() public function submit()
{ {
ray($this->backup->s3_storage_id);
try { try {
$this->custom_validate(); $this->custom_validate();
if ($this->backup->databases_to_backup == '' || $this->backup->databases_to_backup === null) {
$this->backup->databases_to_backup = null;
}
$this->backup->save(); $this->backup->save();
$this->backup->refresh(); $this->backup->refresh();
$this->emit('success', 'Backup updated successfully'); $this->emit('success', 'Backup updated successfully');

View File

@ -13,6 +13,6 @@ public function backup_now()
dispatch(new DatabaseBackupJob( dispatch(new DatabaseBackupJob(
backup: $this->backup backup: $this->backup
)); ));
$this->emit('success', 'Backup queued. It will be available in a few minutes'); $this->emit('success', 'Backup queued. It will be available in a few minutes.');
} }
} }

View File

@ -78,10 +78,10 @@ public function backup_now()
dispatch(new DatabaseBackupJob( dispatch(new DatabaseBackupJob(
backup: $this->backup backup: $this->backup
)); ));
$this->emit('success', 'Backup queued. It will be available in a few minutes'); $this->emit('success', 'Backup queued. It will be available in a few minutes.');
} }
public function submit() public function submit()
{ {
$this->emit('success', 'Backup updated successfully'); $this->emit('success', 'Backup updated successfully.');
} }
} }

View File

@ -66,50 +66,77 @@ public function handle(): void
ray('database not running'); ray('database not running');
return; return;
} }
$databaseType = $this->database->type();
$databasesToBackup = data_get($this->backup, 'databases_to_backup');
if (is_null($databasesToBackup)) {
if ($databaseType === 'standalone-postgresql') {
$databasesToBackup = [$this->database->postgres_db];
} else {
return;
}
} else {
$databasesToBackup = explode(',', $databasesToBackup);
$databasesToBackup = array_map('trim', $databasesToBackup);
}
$this->container_name = $this->database->uuid; $this->container_name = $this->database->uuid;
$this->backup_dir = backup_dir() . "/databases/" . Str::of($this->team->name)->slug() . '-' . $this->team->id . '/' . $this->container_name; $this->backup_dir = backup_dir() . "/databases/" . Str::of($this->team->name)->slug() . '-' . $this->team->id . '/' . $this->container_name;
if ($this->database->name === 'coolify-db') { if ($this->database->name === 'coolify-db') {
$databasesToBackup = ['coolify'];
$this->container_name = "coolify-db"; $this->container_name = "coolify-db";
$ip = Str::slug($this->server->ip); $ip = Str::slug($this->server->ip);
$this->backup_dir = backup_dir() . "/coolify" . "/coolify-db-$ip"; $this->backup_dir = backup_dir() . "/coolify" . "/coolify-db-$ip";
} }
$this->backup_file = "/pg-backup-customformat-" . Carbon::now()->timestamp . ".backup"; foreach ($databasesToBackup as $database) {
$this->backup_location = $this->backup_dir . $this->backup_file; $size = 0;
ray('Backing up ' . $database);
$this->backup_log = ScheduledDatabaseBackupExecution::create([ try {
'filename' => $this->backup_location, $this->backup_file = "/pg-dump-$database-" . Carbon::now()->timestamp . ".dmp";
'scheduled_database_backup_id' => $this->backup->id, $this->backup_location = $this->backup_dir . $this->backup_file;
]); $this->backup_log = ScheduledDatabaseBackupExecution::create([
if ($this->database->type() === 'standalone-postgresql') { 'database_name' => $database,
$this->backup_standalone_postgresql(); 'filename' => $this->backup_location,
'scheduled_database_backup_id' => $this->backup->id,
]);
if ($databaseType === 'standalone-postgresql') {
$this->backup_standalone_postgresql($database);
}
$size = $this->calculate_size();
$this->remove_old_backups();
if ($this->backup->save_s3) {
$this->upload_to_s3();
}
$this->team->notify(new BackupSuccess($this->backup, $this->database));
$this->backup_log->update([
'status' => 'success',
'message' => $this->backup_output,
'size' => $size,
]);
} catch (\Throwable $e) {
$this->backup_log->update([
'status' => 'failed',
'message' => $this->backup_output,
'size' => $size,
'filename' => null
]);
send_internal_notification('DatabaseBackupJob failed with: ' . $e->getMessage());
$this->team->notify(new BackupFailed($this->backup, $this->database, $this->backup_output));
throw $e;
}
} }
$this->calculate_size();
$this->remove_old_backups();
if ($this->backup->save_s3) {
$this->upload_to_s3();
}
$this->save_backup_logs();
$this->team->notify(new BackupSuccess($this->backup, $this->database));
$this->backup_status = 'success';
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->backup_status = 'failed';
send_internal_notification('DatabaseBackupJob failed with: ' . $e->getMessage()); send_internal_notification('DatabaseBackupJob failed with: ' . $e->getMessage());
$this->team->notify(new BackupFailed($this->backup, $this->database, $this->backup_output));
throw $e; throw $e;
} finally {
$this->backup_log->update([
'status' => $this->backup_status,
]);
} }
} }
private function backup_standalone_postgresql(): void private function backup_standalone_postgresql(string $database): void
{ {
try { try {
ray($this->backup_dir); ray($this->backup_dir);
$commands[] = "mkdir -p " . $this->backup_dir; $commands[] = "mkdir -p " . $this->backup_dir;
$commands[] = "docker exec $this->container_name pg_dump -Fc -U {$this->database->postgres_user} > $this->backup_location"; $commands[] = "docker exec $this->container_name pg_dump --format=custom --no-acl --no-owner --username {$this->database->postgres_user} $database > $this->backup_location";
$this->backup_output = instant_remote_process($commands, $this->server); $this->backup_output = instant_remote_process($commands, $this->server);
$this->backup_output = trim($this->backup_output); $this->backup_output = trim($this->backup_output);
if ($this->backup_output === '') { if ($this->backup_output === '') {
@ -119,6 +146,7 @@ private function backup_standalone_postgresql(): void
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->add_to_backup_output($e->getMessage()); $this->add_to_backup_output($e->getMessage());
ray('Backup failed for ' . $this->container_name . ' at ' . $this->server->name . ':' . $this->backup_location . '\n\nError:' . $e->getMessage()); ray('Backup failed for ' . $this->container_name . ' at ' . $this->server->name . ':' . $this->backup_location . '\n\nError:' . $e->getMessage());
throw $e;
} }
} }
@ -131,9 +159,9 @@ private function add_to_backup_output($output): void
} }
} }
private function calculate_size(): void private function calculate_size()
{ {
$this->size = instant_remote_process(["du -b $this->backup_location | cut -f1"], $this->server); return instant_remote_process(["du -b $this->backup_location | cut -f1"], $this->server, false);
} }
private function remove_old_backups(): void private function remove_old_backups(): void
@ -180,13 +208,4 @@ private function upload_to_s3(): void
instant_remote_process([$command], $this->server); instant_remote_process([$command], $this->server);
} }
} }
private function save_backup_logs(): void
{
$this->backup_log->update([
'status' => $this->backup_status,
'message' => $this->backup_output,
'size' => $this->size,
]);
}
} }

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('scheduled_database_backups', function (Blueprint $table) {
$table->text('databases_to_backup')->nullable();
});
Schema::table('scheduled_database_backup_executions', function (Blueprint $table) {
$table->string('database_name')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('scheduled_database_backups', function (Blueprint $table) {
$table->dropColumn('databases_to_backup');
});
Schema::table('scheduled_database_backup_executions', function (Blueprint $table) {
$table->dropColumn('database_name');
});
}
};

View File

@ -26,6 +26,7 @@
</div> </div>
@endif @endif
<div class="flex gap-2"> <div class="flex gap-2">
<x-forms.input label="Databases To Backup" helper="Comma separated list of databases to backup. Empty will include the default one." id="backup.databases_to_backup" />
<x-forms.input label="Frequency" id="backup.frequency" /> <x-forms.input label="Frequency" id="backup.frequency" />
<x-forms.input label="Number of backups to keep (locally)" id="backup.number_of_backups_locally" /> <x-forms.input label="Number of backups to keep (locally)" id="backup.number_of_backups_locally" />
</div> </div>

View File

@ -1,18 +1,19 @@
<div class="flex flex-col flex-col-reverse gap-2"> <div class="flex flex-col-reverse gap-2">
@forelse($executions as $execution) @forelse($executions as $execution)
<form class="flex flex-col p-2 border-dotted border-1 bg-coolgray-300" @class([ <form class="flex flex-col p-2 border-dotted border-1 bg-coolgray-300" @class([
'border-green-500' => data_get($execution, 'status') === 'success', 'border-green-500' => data_get($execution, 'status') === 'success',
'border-red-500' => data_get($execution, 'status') === 'failed', 'border-red-500' => data_get($execution, 'status') === 'failed',
])> ])>
<div>Started At: {{ data_get($execution, 'created_at') }}</div> <div>Database: {{ data_get($execution, 'database_name', 'N/A') }}</div>
<div>Status: {{ data_get($execution, 'status') }}</div> <div>Status: {{ data_get($execution, 'status') }}</div>
<div>Started At: {{ data_get($execution, 'created_at') }}</div>
@if (data_get($execution, 'message')) @if (data_get($execution, 'message'))
<div>Message: {{ data_get($execution, 'message') }}</div> <div>Message: {{ data_get($execution, 'message') }}</div>
@endif @endif
<div>Size: {{ data_get($execution, 'size') }} B / {{ round((int) data_get($execution, 'size') / 1024, 2) }} <div>Size: {{ data_get($execution, 'size') }} B / {{ round((int) data_get($execution, 'size') / 1024, 2) }}
kB / {{ round((int) data_get($execution, 'size') / 1024 / 1024, 2) }} MB kB / {{ round((int) data_get($execution, 'size') / 1024 / 1024, 3) }} MB
</div> </div>
<div>Location: {{ data_get($execution, 'filename') }}</div> <div>Location: {{ data_get($execution, 'filename', 'N/A') }}</div>
<livewire:project.database.backup-execution :execution="$execution" :wire:key="$execution->id" /> <livewire:project.database.backup-execution :execution="$execution" :wire:key="$execution->id" />
</form> </form>
@empty @empty