2023-08-10 13:52:54 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
namespace App\Jobs;
|
|
|
|
|
2023-08-10 16:20:12 +00:00
|
|
|
use App\Models\S3Storage;
|
2023-08-10 13:52:54 +00:00
|
|
|
use App\Models\ScheduledDatabaseBackup;
|
|
|
|
use App\Models\ScheduledDatabaseBackupExecution;
|
|
|
|
use App\Models\Server;
|
|
|
|
use App\Models\StandalonePostgresql;
|
|
|
|
use App\Models\Team;
|
2023-08-10 19:00:02 +00:00
|
|
|
use App\Notifications\Database\BackupFailed;
|
|
|
|
use App\Notifications\Database\BackupSuccess;
|
2023-08-10 13:52:54 +00:00
|
|
|
use Carbon\Carbon;
|
|
|
|
use Illuminate\Bus\Queueable;
|
|
|
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
|
|
use Illuminate\Foundation\Bus\Dispatchable;
|
|
|
|
use Illuminate\Queue\InteractsWithQueue;
|
|
|
|
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
|
|
|
use Illuminate\Queue\SerializesModels;
|
|
|
|
use Throwable;
|
2023-08-15 13:39:15 +00:00
|
|
|
use Illuminate\Support\Str;
|
2023-08-10 13:52:54 +00:00
|
|
|
|
|
|
|
class DatabaseBackupJob implements ShouldQueue
|
|
|
|
{
|
|
|
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
|
|
|
|
2023-09-08 07:37:58 +00:00
|
|
|
public ?Team $team = null;
|
2023-08-10 13:52:54 +00:00
|
|
|
public Server $server;
|
2023-09-09 11:18:49 +00:00
|
|
|
public ScheduledDatabaseBackup $backup;
|
|
|
|
public StandalonePostgresql $database;
|
2023-08-10 13:52:54 +00:00
|
|
|
|
2023-09-08 07:37:58 +00:00
|
|
|
public ?string $container_name = null;
|
|
|
|
public ?ScheduledDatabaseBackupExecution $backup_log = null;
|
2023-08-10 13:52:54 +00:00
|
|
|
public string $backup_status;
|
2023-09-08 07:37:58 +00:00
|
|
|
public ?string $backup_location = null;
|
2023-08-11 14:13:53 +00:00
|
|
|
public string $backup_dir;
|
|
|
|
public string $backup_file;
|
2023-08-10 13:52:54 +00:00
|
|
|
public int $size = 0;
|
2023-09-08 07:37:58 +00:00
|
|
|
public ?string $backup_output = null;
|
|
|
|
public ?S3Storage $s3 = null;
|
2023-08-10 13:52:54 +00:00
|
|
|
|
|
|
|
public function __construct($backup)
|
|
|
|
{
|
|
|
|
$this->backup = $backup;
|
|
|
|
$this->team = Team::find($backup->team_id);
|
2023-09-09 11:18:49 +00:00
|
|
|
$this->database = data_get($this->backup, 'database');
|
2023-08-10 13:52:54 +00:00
|
|
|
$this->server = $this->database->destination->server;
|
2023-08-11 14:13:53 +00:00
|
|
|
$this->s3 = $this->backup->s3;
|
2023-08-10 13:52:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public function middleware(): array
|
|
|
|
{
|
|
|
|
return [new WithoutOverlapping($this->backup->id)];
|
|
|
|
}
|
|
|
|
|
|
|
|
public function uniqueId(): int
|
|
|
|
{
|
|
|
|
return $this->backup->id;
|
|
|
|
}
|
|
|
|
|
2023-08-11 14:13:53 +00:00
|
|
|
public function handle(): void
|
2023-08-10 13:52:54 +00:00
|
|
|
{
|
2023-08-24 14:14:09 +00:00
|
|
|
try {
|
2023-09-09 11:18:49 +00:00
|
|
|
if (data_get($this->database, 'status') !== 'running') {
|
2023-08-24 14:14:09 +00:00
|
|
|
ray('database not running');
|
|
|
|
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;
|
2023-08-15 13:39:15 +00:00
|
|
|
|
2023-08-24 14:14:09 +00:00
|
|
|
if ($this->database->name === 'coolify-db') {
|
|
|
|
$this->container_name = "coolify-db";
|
|
|
|
$ip = Str::slug($this->server->ip);
|
|
|
|
$this->backup_dir = backup_dir() . "/coolify" . "/coolify-db-$ip";
|
|
|
|
}
|
2023-09-05 14:03:24 +00:00
|
|
|
$this->backup_file = "/pg_dump-" . Carbon::now()->timestamp . ".dump";
|
2023-08-24 14:14:09 +00:00
|
|
|
$this->backup_location = $this->backup_dir . $this->backup_file;
|
|
|
|
|
|
|
|
$this->backup_log = ScheduledDatabaseBackupExecution::create([
|
|
|
|
'filename' => $this->backup_location,
|
|
|
|
'scheduled_database_backup_id' => $this->backup->id,
|
|
|
|
]);
|
2023-09-09 11:18:49 +00:00
|
|
|
if ($this->database->type() === 'standalone-postgresql') {
|
2023-08-24 14:14:09 +00:00
|
|
|
$this->backup_standalone_postgresql();
|
|
|
|
}
|
|
|
|
$this->calculate_size();
|
|
|
|
$this->remove_old_backups();
|
|
|
|
if ($this->backup->save_s3) {
|
|
|
|
$this->upload_to_s3();
|
|
|
|
}
|
|
|
|
$this->save_backup_logs();
|
|
|
|
// TODO: Notify user
|
2023-09-11 15:36:30 +00:00
|
|
|
} catch (\Throwable $e) {
|
|
|
|
ray($e->getMessage());
|
|
|
|
send_internal_notification('DatabaseBackupJob failed with: ' . $e->getMessage());
|
|
|
|
throw $e;
|
2023-08-10 16:20:12 +00:00
|
|
|
}
|
2023-08-10 13:52:54 +00:00
|
|
|
}
|
|
|
|
|
2023-08-11 14:13:53 +00:00
|
|
|
private function backup_standalone_postgresql(): void
|
2023-08-10 13:52:54 +00:00
|
|
|
{
|
|
|
|
try {
|
2023-08-15 13:39:15 +00:00
|
|
|
ray($this->backup_dir);
|
2023-08-11 14:13:53 +00:00
|
|
|
$commands[] = "mkdir -p " . $this->backup_dir;
|
2023-09-05 13:43:56 +00:00
|
|
|
$commands[] = "docker exec $this->container_name pg_dump -Fc -U {$this->database->postgres_user} > $this->backup_location";
|
2023-08-10 13:52:54 +00:00
|
|
|
|
|
|
|
$this->backup_output = instant_remote_process($commands, $this->server);
|
|
|
|
|
|
|
|
$this->backup_output = trim($this->backup_output);
|
|
|
|
|
|
|
|
if ($this->backup_output === '') {
|
|
|
|
$this->backup_output = null;
|
|
|
|
}
|
|
|
|
|
2023-08-11 14:13:53 +00:00
|
|
|
ray('Backup done for ' . $this->container_name . ' at ' . $this->server->name . ':' . $this->backup_location);
|
2023-08-10 13:52:54 +00:00
|
|
|
|
|
|
|
$this->backup_status = 'success';
|
2023-08-10 19:00:02 +00:00
|
|
|
$this->team->notify(new BackupSuccess($this->backup, $this->database));
|
2023-09-11 15:36:30 +00:00
|
|
|
} catch (Throwable $e) {
|
2023-08-10 13:52:54 +00:00
|
|
|
$this->backup_status = 'failed';
|
2023-09-11 15:36:30 +00:00
|
|
|
$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());
|
2023-08-11 08:42:57 +00:00
|
|
|
$this->team->notify(new BackupFailed($this->backup, $this->database, $this->backup_output));
|
2023-08-10 13:52:54 +00:00
|
|
|
} finally {
|
|
|
|
$this->backup_log->update([
|
|
|
|
'status' => $this->backup_status,
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-11 14:13:53 +00:00
|
|
|
private function add_to_backup_output($output): void
|
2023-08-10 13:52:54 +00:00
|
|
|
{
|
|
|
|
if ($this->backup_output) {
|
|
|
|
$this->backup_output = $this->backup_output . "\n" . $output;
|
|
|
|
} else {
|
|
|
|
$this->backup_output = $output;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-11 14:13:53 +00:00
|
|
|
private function calculate_size(): void
|
2023-08-10 13:52:54 +00:00
|
|
|
{
|
2023-08-11 14:13:53 +00:00
|
|
|
$this->size = instant_remote_process(["du -b $this->backup_location | cut -f1"], $this->server);
|
2023-08-10 13:52:54 +00:00
|
|
|
}
|
|
|
|
|
2023-08-11 14:13:53 +00:00
|
|
|
private function remove_old_backups(): void
|
2023-08-10 13:52:54 +00:00
|
|
|
{
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
foreach ($deletable->get() as $execution) {
|
|
|
|
delete_backup_locally($execution->filename, $this->server);
|
|
|
|
$execution->delete();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-11 14:13:53 +00:00
|
|
|
private function upload_to_s3(): void
|
2023-08-10 16:20:12 +00:00
|
|
|
{
|
|
|
|
try {
|
|
|
|
if (is_null($this->s3)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
$key = $this->s3->key;
|
|
|
|
$secret = $this->s3->secret;
|
2023-08-11 18:48:52 +00:00
|
|
|
// $region = $this->s3->region;
|
2023-08-10 16:20:12 +00:00
|
|
|
$bucket = $this->s3->bucket;
|
|
|
|
$endpoint = $this->s3->endpoint;
|
2023-08-10 19:00:02 +00:00
|
|
|
|
2023-08-11 14:13:53 +00:00
|
|
|
$commands[] = "docker run --pull=always -d --network {$this->database->destination->network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro ghcr.io/coollabsio/coolify-helper";
|
|
|
|
$commands[] = "docker exec backup-of-{$this->backup->uuid} mc config host add temporary {$endpoint} $key $secret";
|
|
|
|
$commands[] = "docker exec backup-of-{$this->backup->uuid} mc cp $this->backup_location temporary/$bucket{$this->backup_dir}/";
|
|
|
|
instant_remote_process($commands, $this->server);
|
2023-08-10 16:20:12 +00:00
|
|
|
$this->add_to_backup_output('Uploaded to S3.');
|
2023-08-11 14:13:53 +00:00
|
|
|
ray('Uploaded to S3. ' . $this->backup_location . ' to s3://' . $bucket . $this->backup_dir);
|
2023-09-11 15:36:30 +00:00
|
|
|
} catch (\Throwable $e) {
|
|
|
|
$this->add_to_backup_output($e->getMessage());
|
|
|
|
ray($e->getMessage());
|
2023-08-11 14:13:53 +00:00
|
|
|
} finally {
|
|
|
|
$command = "docker rm -f backup-of-{$this->backup->uuid}";
|
|
|
|
instant_remote_process([$command], $this->server);
|
2023-08-10 16:20:12 +00:00
|
|
|
}
|
|
|
|
}
|
2023-08-11 14:13:53 +00:00
|
|
|
|
|
|
|
private function save_backup_logs(): void
|
|
|
|
{
|
|
|
|
$this->backup_log->update([
|
|
|
|
'status' => $this->backup_status,
|
|
|
|
'message' => $this->backup_output,
|
|
|
|
'size' => $this->size,
|
|
|
|
]);
|
|
|
|
}
|
2023-08-10 13:52:54 +00:00
|
|
|
}
|