Skip to content

Commit c4f83a8

Browse files
committed
feat: enable backup support for Git-based Docker Compose databases
1 parent e1aac50 commit c4f83a8

20 files changed

Lines changed: 493 additions & 63 deletions

app/Actions/Database/StartDatabaseProxy.php

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,22 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St
3131

3232
if ($database->getMorphClass() === \App\Models\ServiceDatabase::class) {
3333
$databaseType = $database->databaseType();
34-
$network = $database->service->uuid;
35-
$server = data_get($database, 'service.destination.server');
36-
$containerName = "{$database->name}-{$database->service->uuid}";
34+
if ($database->service_id) {
35+
$network = $database->service->uuid;
36+
$server = data_get($database, 'service.destination.server');
37+
$containerName = "{$database->name}-{$database->service->uuid}";
38+
} else {
39+
if ($database->application_id) {
40+
$network = $database->application->destination->network;
41+
$server = data_get($database, 'application.destination.server');
42+
$containerName = "{$database->name}-" . generateApplicationContainerName($database->application);
43+
} else {
44+
$preview = $database->application_preview;
45+
$network = $preview->application->destination->network;
46+
$server = data_get($database, 'application_preview.application.destination.server');
47+
$containerName = "{$database->name}-" . generateApplicationContainerName($preview->application, $preview->pull_request_id);
48+
}
49+
}
3750
}
3851
$internalPort = match ($databaseType) {
3952
'standalone-mariadb', 'standalone-mysql' => 3306,

app/Jobs/DatabaseBackupJob.php

Lines changed: 68 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
3737

3838
public ?Team $team = null;
3939

40-
public Server $server;
40+
public ?Server $server = null;
4141

4242
public StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|ServiceDatabase $database;
4343

@@ -47,7 +47,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
4747

4848
public ?ScheduledDatabaseBackupExecution $backup_log = null;
4949

50-
public string $backup_status = 'failed';
50+
public string $backup_status = ScheduledDatabaseBackupExecution::STATUS_FAILED;
5151

5252
public ?string $backup_location = null;
5353

@@ -101,7 +101,13 @@ public function handle(): void
101101
}
102102
if (data_get($this->backup, 'database_type') === ServiceDatabase::class) {
103103
$this->database = data_get($this->backup, 'database');
104-
$this->server = $this->database->service->server;
104+
if ($this->database->service_id) {
105+
$this->server = $this->database->service->server;
106+
} elseif ($this->database->application_id) {
107+
$this->server = data_get($this->database, 'application.destination.server');
108+
} elseif ($this->database->application_preview_id) {
109+
$this->server = data_get($this->database, 'application_preview.application.destination.server');
110+
}
105111
$this->s3 = $this->backup->s3;
106112
} else {
107113
$this->database = data_get($this->backup, 'database');
@@ -115,6 +121,28 @@ public function handle(): void
115121
throw new \Exception('Database not found?!');
116122
}
117123

124+
if ($this->database instanceof ServiceDatabase) {
125+
$applicationId = $this->database->application_id;
126+
$pullRequestId = 0;
127+
if ($this->database->application_preview_id) {
128+
$applicationId = data_get($this->database->application_preview, 'application_id');
129+
$pullRequestId = data_get($this->database->application_preview, 'pull_request_id');
130+
}
131+
132+
if ($applicationId) {
133+
$deploymentInProgress = \App\Models\ApplicationDeploymentQueue::where('application_id', $applicationId)
134+
->where('pull_request_id', $pullRequestId)
135+
->where('status', \App\Enums\ApplicationDeploymentStatus::IN_PROGRESS->value)
136+
->exists();
137+
138+
if ($deploymentInProgress) {
139+
$this->release(60);
140+
141+
return;
142+
}
143+
}
144+
}
145+
118146
$this->markStaleExecutionsAsFailed();
119147

120148
BackupCreated::dispatch($this->team->id);
@@ -131,8 +159,19 @@ public function handle(): void
131159
}
132160
if (data_get($this->backup, 'database_type') === ServiceDatabase::class) {
133161
$databaseType = $this->database->databaseType();
134-
$serviceUuid = $this->database->service->uuid;
135-
$serviceName = str($this->database->service->name)->slug();
162+
if ($this->database->service_id) {
163+
$serviceUuid = $this->database->service->uuid;
164+
$serviceName = str($this->database->service->name)->slug();
165+
} else {
166+
if ($this->database->application_id) {
167+
$serviceUuid = generateApplicationContainerName($this->database->application);
168+
$serviceName = str($this->database->application->name)->slug();
169+
} else {
170+
$preview = $this->database->application_preview;
171+
$serviceUuid = generateApplicationContainerName($preview->application, $preview->pull_request_id);
172+
$serviceName = str($preview->application->name)->slug();
173+
}
174+
}
136175
if (str($databaseType)->contains('postgres')) {
137176
$this->container_name = "{$this->database->name}-$serviceUuid";
138177
$this->directory_name = $serviceName.'-'.$this->container_name;
@@ -489,6 +528,18 @@ public function handle(): void
489528
]);
490529
}
491530
}
531+
532+
if ($this->database instanceof ServiceDatabase) {
533+
if ($this->database->application_id || $this->database->application_preview_id) {
534+
$application = $this->database->application_id
535+
? $this->database->application
536+
: $this->database->application_preview?->application;
537+
538+
if ($application) {
539+
queue_next_deployment($application);
540+
}
541+
}
542+
}
492543
}
493544

494545
private function backup_standalone_mongodb(string $databaseWithCollections): void
@@ -684,7 +735,13 @@ private function upload_to_s3(): void
684735
$endpoint = $this->s3->endpoint;
685736
$this->s3->testConnection(shouldSave: true);
686737
if (data_get($this->backup, 'database_type') === ServiceDatabase::class) {
687-
$network = $this->database->service->destination->network;
738+
if ($this->database->service_id) {
739+
$network = $this->database->service->destination->network;
740+
} elseif ($this->database->application_id) {
741+
$network = $this->database->application->destination->network;
742+
} elseif ($this->database->application_preview_id) {
743+
$network = $this->database->application_preview->application->destination->network;
744+
}
688745
} else {
689746
$network = $this->database->destination->network;
690747
}
@@ -743,13 +800,13 @@ private function markStaleExecutionsAsFailed(): void
743800
$timeoutSeconds = ($this->backup->timeout ?? 3600) * 2;
744801

745802
$staleExecutions = $this->backup->executions()
746-
->where('status', 'running')
803+
->where('status', ScheduledDatabaseBackupExecution::STATUS_RUNNING)
747804
->where('created_at', '<', now()->subSeconds($timeoutSeconds))
748805
->get();
749806

750807
foreach ($staleExecutions as $execution) {
751808
$execution->update([
752-
'status' => 'failed',
809+
'status' => ScheduledDatabaseBackupExecution::STATUS_FAILED,
753810
'message' => 'Marked as failed - backup execution exceeded maximum allowed time',
754811
'finished_at' => now(),
755812
]);
@@ -781,9 +838,9 @@ public function failed(?Throwable $exception): void
781838
// Don't overwrite a successful backup status — a post-backup error
782839
// (e.g. notification failure) should not retroactively mark the backup
783840
// as failed (see GitHub issue #9088)
784-
if ($log->status !== 'success') {
841+
if ($log->status !== ScheduledDatabaseBackupExecution::STATUS_SUCCESS) {
785842
$log->update([
786-
'status' => 'failed',
843+
'status' => ScheduledDatabaseBackupExecution::STATUS_FAILED,
787844
'message' => 'Job permanently failed after '.$this->attempts().' attempts: '.($exception?->getMessage() ?? 'Unknown error'),
788845
'size' => 0,
789846
'filename' => null,
@@ -793,7 +850,7 @@ public function failed(?Throwable $exception): void
793850
}
794851

795852
// Notify team about permanent failure (only if backup didn't already succeed)
796-
if ($this->team && $log?->status !== 'success') {
853+
if ($this->team && $log?->status !== ScheduledDatabaseBackupExecution::STATUS_SUCCESS) {
797854
$databaseName = $log?->database_name ?? 'unknown';
798855
$output = $this->backup_output ?? $exception?->getMessage() ?? 'Unknown error';
799856
try {
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
namespace App\Livewire\Project\Application;
4+
5+
use App\Models\Application;
6+
use Livewire\Component;
7+
8+
use Illuminate\Support\Collection;
9+
use Illuminate\Contracts\View\View;
10+
11+
use Livewire\Attributes\Locked;
12+
use Illuminate\Support\Facades\Gate;
13+
14+
class Backups extends Component
15+
{
16+
#[Locked]
17+
public Application $application;
18+
public Collection $databases;
19+
20+
public function mount(): void
21+
{
22+
abort_if(Gate::denies('view', $this->application), 403);
23+
$this->databases = $this->application->databases()->get();
24+
}
25+
26+
public function render(): View
27+
{
28+
return view('livewire.project.application.backups');
29+
}
30+
}

app/Livewire/Project/Application/Configuration.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ public function mount()
5656
if ($this->application->build_pack === 'dockercompose' && $this->currentRoute === 'project.application.healthcheck') {
5757
return redirect()->route('project.application.configuration', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]);
5858
}
59+
60+
if ($this->application->build_pack !== 'dockercompose' && $this->currentRoute === 'project.application.backups') {
61+
return redirect()->route('project.application.configuration', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]);
62+
}
5963
}
6064

6165
public function render()

app/Livewire/Project/CloneMe.php

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -379,18 +379,20 @@ public function clone(string $type)
379379
$newPersistentVolume->save();
380380

381381
if ($this->cloneVolumeData) {
382-
try {
383-
StopService::dispatch($database->service);
384-
$sourceVolume = $volume->name;
385-
$targetVolume = $newPersistentVolume->name;
386-
$sourceServer = $database->service->destination->server;
387-
$targetServer = $newService->destination->server;
388-
389-
VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume);
390-
391-
StartService::dispatch($database->service);
392-
} catch (\Exception $e) {
393-
\Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage());
382+
if ($database->service_id) {
383+
try {
384+
StopService::dispatch($database->service);
385+
$sourceVolume = $volume->name;
386+
$targetVolume = $newPersistentVolume->name;
387+
$sourceServer = $database->service->destination->server;
388+
$targetServer = $newService->destination->server;
389+
390+
VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume);
391+
392+
StartService::dispatch($database->service);
393+
} catch (\Exception $e) {
394+
\Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage());
395+
}
394396
}
395397
}
396398
}

app/Livewire/Project/Shared/ResourceOperations.php

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -331,18 +331,20 @@ public function cloneTo($destination_id)
331331
$newPersistentVolume->save();
332332

333333
if ($this->cloneVolumeData) {
334-
try {
335-
StopService::dispatch($database->service);
336-
$sourceVolume = $volume->name;
337-
$targetVolume = $newPersistentVolume->name;
338-
$sourceServer = $database->service->destination->server;
339-
$targetServer = $new_resource->destination->server;
340-
341-
VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume);
342-
343-
StartService::dispatch($database->service);
344-
} catch (\Exception $e) {
345-
\Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage());
334+
if ($database->service_id) {
335+
try {
336+
StopService::dispatch($database->service);
337+
$sourceVolume = $volume->name;
338+
$targetVolume = $newPersistentVolume->name;
339+
$sourceServer = $database->service->destination->server;
340+
$targetServer = $new_resource->destination->server;
341+
342+
VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume);
343+
344+
StartService::dispatch($database->service);
345+
} catch (\Exception $e) {
346+
\Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage());
347+
}
346348
}
347349
}
348350
}

app/Models/Application.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,7 @@ protected static function booted()
342342
}
343343
});
344344
static::forceDeleting(function ($application) {
345+
$application->databases()->get()->each->delete();
345346
$application->update(['fqdn' => null]);
346347
$application->settings()->delete();
347348
$application->persistentStorages()->delete();
@@ -2270,4 +2271,9 @@ public function setConfig($config)
22702271
throw new \Exception('Failed to update application settings');
22712272
}
22722273
}
2274+
2275+
public function databases(): HasMany
2276+
{
2277+
return $this->hasMany(ServiceDatabase::class);
2278+
}
22732279
}

app/Models/ApplicationPreview.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace App\Models;
44

55
use App\Support\ValidationPatterns;
6+
use Illuminate\Database\Eloquent\Relations\HasMany;
67
use Illuminate\Database\Eloquent\SoftDeletes;
78
use Spatie\Url\Url;
89
use Visus\Cuid2\Cuid2;
@@ -32,6 +33,7 @@ class ApplicationPreview extends BaseModel
3233
protected static function booted()
3334
{
3435
static::forceDeleting(function ($preview) {
36+
$preview->databases()->get()->each->delete();
3537
$server = $preview->application->destination->server;
3638
$application = $preview->application;
3739

@@ -202,4 +204,9 @@ public function generate_preview_fqdn_compose()
202204

203205
$this->save();
204206
}
207+
208+
public function databases(): HasMany
209+
{
210+
return $this->hasMany(ServiceDatabase::class);
211+
}
205212
}

app/Models/ScheduledDatabaseBackup.php

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@
88

99
class ScheduledDatabaseBackup extends BaseModel
1010
{
11+
protected static function booted(): void
12+
{
13+
static::deleting(function (ScheduledDatabaseBackup $backup): void {
14+
$backup->executions()->delete();
15+
});
16+
}
17+
1118
protected function casts(): array
1219
{
1320
return [
@@ -91,12 +98,21 @@ public function executionsPaginated(int $skip = 0, int $take = 10)
9198
];
9299
}
93100

94-
public function server()
101+
public function server(): ?Server
95102
{
96103
if ($this->database) {
104+
$server = null;
97105
if ($this->database instanceof ServiceDatabase) {
98-
$destination = data_get($this->database->service, 'destination');
99-
$server = data_get($destination, 'server');
106+
if ($this->database->service_id) {
107+
$destination = data_get($this->database->service, 'destination');
108+
$server = data_get($destination, 'server');
109+
} elseif ($this->database->application_id) {
110+
$destination = data_get($this->database->application, 'destination');
111+
$server = data_get($destination, 'server');
112+
} elseif ($this->database->application_preview_id) {
113+
$destination = data_get($this->database->application_preview?->application, 'destination');
114+
$server = data_get($destination, 'server');
115+
}
100116
} else {
101117
$destination = data_get($this->database, 'destination');
102118
$server = data_get($destination, 'server');

app/Models/ScheduledDatabaseBackupExecution.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66

77
class ScheduledDatabaseBackupExecution extends BaseModel
88
{
9+
const STATUS_RUNNING = 'running';
10+
const STATUS_SUCCESS = 'success';
11+
const STATUS_FAILED = 'failed';
912
protected $fillable = [
1013
'uuid',
1114
'scheduled_database_backup_id',

0 commit comments

Comments
 (0)