mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-04 10:42:23 +00:00
fix(backups): let service databases choose the databases to back up
Closes #12432 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
33a6682791
commit
dbd211486b
4 files changed
+95
-28
No files matched your search
@@ -169,6 +169,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
// The container name is validated above, so it is a safe folder name. Keep the folder of earlier releases.
|
||||
$this->directory_name = $serviceName.'-'.$this->container_name;
|
||||
$escapedContainerName = escapeshellarg($this->container_name);
|
||||
$configuredDatabases = $this->configuredDatabasesToBackup();
|
||||
if (str($databaseType)->contains('postgres')) {
|
||||
$commands[] = "docker exec {$escapedContainerName} env | grep -E '^(POSTGRES|PGUSER=)'";
|
||||
$envs = instant_remote_process($commands, $this->server, true, false, null, disableMultiplexing: true);
|
||||
@@ -180,7 +181,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
return str($env)->startsWith('POSTGRES_DB=');
|
||||
})->first();
|
||||
|
||||
if ($db) {
|
||||
if (filled($configuredDatabases)) {
|
||||
$databasesToBackup = $configuredDatabases;
|
||||
} elseif ($db) {
|
||||
$databasesToBackup = str($db)->after('POSTGRES_DB=')->value();
|
||||
} else {
|
||||
$databasesToBackup = $this->database->postgres_user;
|
||||
@@ -207,15 +210,14 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
return str($env)->startsWith('MYSQL_DATABASE=');
|
||||
})->first();
|
||||
|
||||
if ($db) {
|
||||
if (filled($configuredDatabases)) {
|
||||
$databasesToBackup = $configuredDatabases;
|
||||
} elseif ($db) {
|
||||
$databasesToBackup = str($db)->after('MYSQL_DATABASE=')->value();
|
||||
} else {
|
||||
$databasesToBackup = $this->configuredDatabasesToBackup();
|
||||
if (blank($databasesToBackup)) {
|
||||
$this->recordUndeterminedDatabases('MYSQL_DATABASE');
|
||||
$this->recordUndeterminedDatabases('MYSQL_DATABASE');
|
||||
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
} elseif (str($databaseType)->contains('mariadb')) {
|
||||
$commands[] = "docker exec {$escapedContainerName} env";
|
||||
@@ -235,27 +237,17 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
}
|
||||
}
|
||||
|
||||
$db = $envs->filter(function ($env) {
|
||||
return str($env)->startsWith('MARIADB_DATABASE=');
|
||||
})->first();
|
||||
$db = $envs->first(fn ($env) => str($env)->startsWith('MARIADB_DATABASE='))
|
||||
?? $envs->first(fn ($env) => str($env)->startsWith('MYSQL_DATABASE='));
|
||||
|
||||
if ($db) {
|
||||
$databasesToBackup = str($db)->after('MARIADB_DATABASE=')->value();
|
||||
if (filled($configuredDatabases)) {
|
||||
$databasesToBackup = $configuredDatabases;
|
||||
} elseif ($db) {
|
||||
$databasesToBackup = str($db)->after('=')->value();
|
||||
} else {
|
||||
$db = $envs->filter(function ($env) {
|
||||
return str($env)->startsWith('MYSQL_DATABASE=');
|
||||
})->first();
|
||||
$this->recordUndeterminedDatabases('MARIADB_DATABASE or MYSQL_DATABASE');
|
||||
|
||||
if ($db) {
|
||||
$databasesToBackup = str($db)->after('MYSQL_DATABASE=')->value();
|
||||
} else {
|
||||
$databasesToBackup = $this->configuredDatabasesToBackup();
|
||||
if (blank($databasesToBackup)) {
|
||||
$this->recordUndeterminedDatabases('MARIADB_DATABASE or MYSQL_DATABASE');
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
} elseif (str($databaseType)->contains('mongo')) {
|
||||
$databasesToBackup = ['*'];
|
||||
@@ -1104,7 +1096,8 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
}
|
||||
|
||||
/**
|
||||
* Databases picked on the backup, for service databases whose container does not name one.
|
||||
* Databases picked in the backup settings of a service database. They take priority over the
|
||||
* database that the container env names, as for standalone databases.
|
||||
*
|
||||
* @return string|array<int, string>|null
|
||||
*/
|
||||
|
||||
@@ -24,9 +24,14 @@
|
||||
</x-slot:actions>
|
||||
|
||||
<div class="space-y-5">
|
||||
@php
|
||||
$isServiceSqlDatabase = $backup->database instanceof \App\Models\ServiceDatabase
|
||||
&& str($backup->database->databaseType())->contains(['postgres', 'mysql', 'mariadb']);
|
||||
@endphp
|
||||
@if ($backup->database_type === 'App\Models\StandalonePostgresql' && $backup->database_id !== 0
|
||||
|| $backup->database_type === 'App\Models\StandaloneMysql'
|
||||
|| $backup->database_type === 'App\Models\StandaloneMariadb')
|
||||
|| $backup->database_type === 'App\Models\StandaloneMariadb'
|
||||
|| $isServiceSqlDatabase)
|
||||
<div class="grid w-full gap-4">
|
||||
<x-forms.listbox id="dumpAll" label="Database selection" onChange="instantSave" :options="[
|
||||
['value' => true, 'label' => 'All databases'],
|
||||
@@ -67,7 +72,11 @@
|
||||
placeholder="Type a database and press Enter" />
|
||||
</div>
|
||||
<p class="mt-1.5 text-xs text-neutral-500 dark:text-fg-dim">
|
||||
Add one or more database names. Leave empty to include the default database.
|
||||
@if ($isServiceSqlDatabase)
|
||||
Add one or more database names. Leave empty to back up the database that the container env names.
|
||||
@else
|
||||
Add one or more database names. Leave empty to include the default database.
|
||||
@endif
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@@ -678,3 +678,39 @@ it('toggles the schedule from the form and notifies the page header', function (
|
||||
|
||||
expect((bool) $backup->fresh()->enabled)->toBeFalse();
|
||||
});
|
||||
|
||||
it('lets a service database backup choose the databases to back up', function (string $image, bool $showsSelection) {
|
||||
$server = Server::factory()->create(['team_id' => $this->team->id]);
|
||||
$destination = StandaloneDocker::where('server_id', $server->id)->firstOrFail();
|
||||
$project = Project::factory()->create(['team_id' => $this->team->id]);
|
||||
$environment = Environment::factory()->create(['project_id' => $project->id]);
|
||||
$service = Service::factory()->create([
|
||||
'server_id' => $server->id,
|
||||
'environment_id' => $environment->id,
|
||||
'destination_id' => $destination->id,
|
||||
'destination_type' => $destination->getMorphClass(),
|
||||
]);
|
||||
$database = ServiceDatabase::create(['service_id' => $service->id, 'name' => 'db', 'image' => $image]);
|
||||
$backup = ScheduledDatabaseBackup::create([
|
||||
'frequency' => '0 0 * * *',
|
||||
'database_type' => $database->getMorphClass(),
|
||||
'database_id' => $database->id,
|
||||
'team_id' => $this->team->id,
|
||||
]);
|
||||
|
||||
$component = Livewire::test(BackupEdit::class, ['backup' => $backup->fresh(), 'availableS3Storages' => collect()]);
|
||||
|
||||
$showsSelection
|
||||
? $component->assertSee('Database selection')->assertSee('Databases to back up')
|
||||
: $component->assertDontSee('Database selection');
|
||||
|
||||
if ($showsSelection) {
|
||||
$component->set('databasesToBackup', 'shop')->call('submit')->assertHasNoErrors();
|
||||
expect($backup->fresh()->databases_to_backup)->toBe('shop');
|
||||
}
|
||||
})->with([
|
||||
'mariadb' => ['mariadb:11', true],
|
||||
'mysql' => ['mysql:8', true],
|
||||
'postgres' => ['postgres:16-alpine', true],
|
||||
'mongodb' => ['mongo:7', false],
|
||||
]);
|
||||
@@ -361,6 +361,35 @@ it('backs up a service database without a database env variable from the backup
|
||||
'mysql, selected database' => ['mysql:8', 'MYSQL_ROOT_PASSWORD=secret', ['databases_to_backup' => 'shop'], 'shop'],
|
||||
]);
|
||||
|
||||
it('backs up the databases from the backup settings instead of the container env database', function (string $image, string $env) {
|
||||
$database = backupRunSafetyServiceDatabase($this->server, $this->destination, $this->environment, $image);
|
||||
$backup = backupRunSafetySchedule($database, $this->team, ['databases_to_backup' => 'shop,blog']);
|
||||
backupRunSafetyFakeRemoteCommands(["docker exec '{$database->name}-{$database->service->uuid}' env" => $env]);
|
||||
|
||||
(new DatabaseBackupJob($backup))->handle();
|
||||
|
||||
expect(ScheduledDatabaseBackupExecution::query()->orderBy('id')->pluck('database_name')->all())->toBe(['shop', 'blog']);
|
||||
})->with([
|
||||
'mariadb' => ['mariadb:11', "MARIADB_ROOT_PASSWORD=secret\nMARIADB_DATABASE=app"],
|
||||
'mysql' => ['mysql:8', "MYSQL_ROOT_PASSWORD=secret\nMYSQL_DATABASE=app"],
|
||||
'postgres' => ['postgres:16', "POSTGRES_PASSWORD=secret\nPOSTGRES_DB=app"],
|
||||
]);
|
||||
|
||||
it('backs up the container env database when the backup settings name none', function (string $image, string $env) {
|
||||
$database = backupRunSafetyServiceDatabase($this->server, $this->destination, $this->environment, $image);
|
||||
$backup = backupRunSafetySchedule($database, $this->team);
|
||||
backupRunSafetyFakeRemoteCommands(["docker exec '{$database->name}-{$database->service->uuid}' env" => $env]);
|
||||
|
||||
(new DatabaseBackupJob($backup))->handle();
|
||||
|
||||
expect(ScheduledDatabaseBackupExecution::query()->sole()->database_name)->toBe('app');
|
||||
})->with([
|
||||
'mariadb' => ['mariadb:11', "MARIADB_ROOT_PASSWORD=secret\nMARIADB_DATABASE=app"],
|
||||
'mariadb with mysql env' => ['mariadb:11', "MARIADB_ROOT_PASSWORD=secret\nMYSQL_DATABASE=app"],
|
||||
'mysql' => ['mysql:8', "MYSQL_ROOT_PASSWORD=secret\nMYSQL_DATABASE=app"],
|
||||
'postgres' => ['postgres:16', "POSTGRES_PASSWORD=secret\nPOSTGRES_DB=app"],
|
||||
]);
|
||||
|
||||
it('records a failed execution and notifies when no service database to back up is known', function (string $image, string $env) {
|
||||
$database = backupRunSafetyServiceDatabase($this->server, $this->destination, $this->environment, $image);
|
||||
$backup = backupRunSafetySchedule($database, $this->team);
|
||||
|
||||
Reference in new issue
Block a user