fix(backups): use configured databases when container env is missing (#12212)

This commit is contained in:
Andras Bacsai
2026-10-19 21:21:53 +02:00
committed by GitHub
parent 54315b10e5
commit c089f8a328
2 changed files with 96 additions and 2 deletions
+44 -2
View File
@@ -217,7 +217,12 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
if ($db) {
$databasesToBackup = str($db)->after('MYSQL_DATABASE=')->value();
} else {
throw new \Exception('MYSQL_DATABASE not found');
$databasesToBackup = $this->configuredDatabasesToBackup();
if (blank($databasesToBackup)) {
$this->recordUndeterminedDatabases('MYSQL_DATABASE');
return;
}
}
} elseif (str($databaseType)->contains('mariadb')) {
$commands[] = "docker exec {$escapedContainerName} env";
@@ -251,7 +256,12 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
if ($db) {
$databasesToBackup = str($db)->after('MYSQL_DATABASE=')->value();
} else {
throw new \Exception('MARIADB_DATABASE or MYSQL_DATABASE not found');
$databasesToBackup = $this->configuredDatabasesToBackup();
if (blank($databasesToBackup)) {
$this->recordUndeterminedDatabases('MARIADB_DATABASE or MYSQL_DATABASE');
return;
}
}
}
} elseif (str($databaseType)->contains('mongo')) {
@@ -1017,6 +1027,38 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
]);
}
/**
* Databases picked on the backup, for service databases whose container does not name one.
*
* @return string|array<int, string>|null
*/
private function configuredDatabasesToBackup(): string|array|null
{
if ($this->backup->dump_all) {
return ['all'];
}
return $this->backup->databases_to_backup;
}
/**
* Show a run that cannot start because no database is known in the execution list, and notify the team.
*/
private function recordUndeterminedDatabases(string $envNames): void
{
$message = "No database to back up: the container does not set {$envNames}. Select \"All databases\" or enter the databases to back up in the backup settings.";
$this->recordSkippedBackup($message);
$this->logSkippedRun('no_databases_determined');
try {
$this->team->notify(new BackupFailed($this->backup, $this->database, $message, 'unknown'));
} catch (Throwable $e) {
Log::channel('scheduled-errors')->warning('Failed to send backup failure notification', [
'backup_id' => $this->backup->uuid,
'error' => $e->getMessage(),
]);
}
}
private function markStaleExecutionsAsFailed(): void
{
try {
+52
View File
@@ -277,6 +277,58 @@ it('stores service database backups in the folder of earlier releases', function
->toContain("/rallly-rallly_db-{$service->uuid}/pg-dump-rallly-");
});
function backupRunSafetyServiceDatabase(Server $server, StandaloneDocker $destination, Environment $environment, string $image): ServiceDatabase
{
$service = Service::factory()->create([
'server_id' => $server->id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),
'environment_id' => $environment->id,
]);
return ServiceDatabase::create([
'service_id' => $service->id,
'name' => 'db',
'image' => $image,
'status' => 'running:healthy',
]);
}
it('backs up a service database without a database env variable from the backup settings', function (string $image, string $env, array $settings, string $databaseName) {
$database = backupRunSafetyServiceDatabase($this->server, $this->destination, $this->environment, $image);
$backup = backupRunSafetySchedule($database, $this->team, $settings);
backupRunSafetyFakeRemoteCommands(["docker exec '{$database->name}-{$database->service->uuid}' env" => $env]);
(new DatabaseBackupJob($backup))->handle();
expect(ScheduledDatabaseBackupExecution::query()->sole())
->status->toBe('success')
->database_name->toBe($databaseName);
})->with([
'mariadb, all databases' => ['mariadb:11', 'MARIADB_ROOT_PASSWORD=secret', ['dump_all' => true], 'all'],
'mariadb, selected database' => ['mariadb:11', 'MARIADB_ROOT_PASSWORD=secret', ['databases_to_backup' => 'shop'], 'shop'],
'mysql, all databases' => ['mysql:8', 'MYSQL_ROOT_PASSWORD=secret', ['dump_all' => true], 'all'],
'mysql, selected database' => ['mysql:8', 'MYSQL_ROOT_PASSWORD=secret', ['databases_to_backup' => 'shop'], 'shop'],
]);
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);
$this->team->emailNotificationSettings->update(['smtp_enabled' => true, 'backup_failure_email_notifications' => true]);
$commands = backupRunSafetyFakeRemoteCommands(["docker exec '{$database->name}-{$database->service->uuid}' env" => $env]);
(new DatabaseBackupJob($backup))->handle();
expect(ScheduledDatabaseBackupExecution::query()->sole())
->status->toBe('failed')
->message->toContain('No database to back up');
expect($commands->contains(fn (string $command) => str_contains($command, 'dump')))->toBeFalse();
Notification::assertSentTo($this->team, BackupFailed::class);
})->with([
'mariadb' => ['mariadb:11', 'MARIADB_ROOT_PASSWORD=secret'],
'mysql' => ['mysql:8', 'MYSQL_ROOT_PASSWORD=secret'],
]);
// --- Worker timeout (M16) ---
it('stops the backup command at the backup timeout, before the worker timeout', function () {