fix(backups): back up all databases by default for Odoo services

Odoo keeps its data in databases that the user creates in Odoo, so the
POSTGRES_DB=postgres database that the backup used was empty. Also show the
sections of the edit backup schedule modal without nested cards.

Closes #7555

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andras BacsaiandClaude Opus 5.5 committed 2026-11-03 22:49:38 +01:00
1 parent da7779d061
commit 232e8b8616
6 files changed
+60 -14

No files matched your search

@@ -82,6 +82,12 @@ class CreateScheduledBackup extends Component
$payload['databases_to_backup'] = $database->clickhouse_db;
}
// Odoo keeps its data in databases that the user creates in Odoo, so the database
// that the container env names (POSTGRES_DB=postgres) stays empty.
if ($database instanceof ServiceDatabase && $database->service?->service_type === 'odoo') {
$payload['dump_all'] = true;
}
$databaseBackup = ScheduledDatabaseBackup::create($payload);
auditLog('ui.database.backup_schedule_created', [
'team_id' => $database->team()?->id,
+20
View File
@@ -2100,6 +2100,26 @@ html[data-theme="custom"]:not(.dark) :is(.application-settings-section, .applica
padding: 1rem;
}
/* Sections inside a modal well: the modal is already the card, so show each
section flat (title row and fields) with a divider between sections. */
.modal-flat-sections .application-settings-section,
.modal-flat-sections .application-settings-section-body {
border: 0;
border-radius: 0;
background: none;
box-shadow: none;
padding: 0;
}
.modal-flat-sections .application-settings-section > :is(header, .application-settings-section-header) {
padding-inline: 0;
}
.modal-flat-sections .application-settings-section ~ .application-settings-section {
border-top: 1px solid var(--well-border);
padding-top: 1.5rem;
}
/* The dark well is darker than the canvas, so controls inside it use a darker,
translucent fill and border (like haruvo) instead of the global opaque ones. */
.dark .application-settings-section-body {
@@ -2,7 +2,7 @@
description="The first reached limit removes the oldest backup. Use 0 for unlimited retention.">
<div class="space-y-6">
<div>
<h3 class="mb-3 text-sm font-semibold text-black dark:text-fg">Local backups</h3>
<h4 class="mb-2 text-xs! font-medium! text-neutral-500! dark:text-fg-dim!">Local backups</h4>
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
<x-forms.input label="Backups to keep" id="databaseBackupRetentionAmountLocally"
type="number" min="0" helper="Maximum number of recent local backups." required />
@@ -14,8 +14,8 @@
</div>
</div>
<div class="border-t border-neutral-200 pt-6 dark:border-white/[0.06]">
<h3 class="mb-3 text-sm font-semibold text-black dark:text-fg">S3 backups</h3>
<div>
<h4 class="mb-2 text-xs! font-medium! text-neutral-500! dark:text-fg-dim!">S3 backups</h4>
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
<x-forms.input label="Backups to keep" id="databaseBackupRetentionAmountS3"
type="number" min="0" helper="Maximum number of recent S3 backups." required />
@@ -79,20 +79,15 @@
@php
$selectedSchedule = $selectedDatabaseBackup ?: $selectedVolumeBackup;
@endphp
<x-modal-input :title="'Edit backup schedule'" wireOpen="scheduleModalOpen" :wireIgnore="false" isLarge fixedHeight
<x-modal-input :title="'Edit backup schedule'"
:subtitle="($selectedDatabaseBackup
? ($selectedDatabaseBackup->database->human_name ?: $selectedDatabaseBackup->database->name)
: $selectedVolumeBackup->targetName()).' · '.$selectedSchedule->frequency.' schedule'"
wireOpen="scheduleModalOpen" :wireIgnore="false" isLarge fixedHeight
canGate="update" :canResource="$service">
<x-slot:content><span></span></x-slot:content>
<div x-data="{ activeSection: 'general' }" class="flex min-w-0 flex-col gap-6">
<div>
<h2 class="text-base font-semibold text-neutral-950 dark:text-fg">
{{ $selectedDatabaseBackup
? ($selectedDatabaseBackup->database->human_name ?: $selectedDatabaseBackup->database->name)
: $selectedVolumeBackup->targetName() }}
</h2>
<p class="mt-1 text-xs text-neutral-500 dark:text-fg-dim">{{ $selectedSchedule->frequency }} schedule</p>
</div>
<div x-data="{ activeSection: 'general' }" class="modal-flat-sections flex min-w-0 flex-col gap-6">
<x-backup-tabs context="service-schedule" :parameters="$parameters" section="general" />
@foreach (['general', 'danger'] as $modalSection)
+1
View File
@@ -359,6 +359,7 @@ it('backs up a service database without a database env variable from the backup
'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'],
'postgres with the maintenance database, all databases' => ['postgres:16-alpine', "POSTGRES_USER=odoo\nPOSTGRES_DB=postgres", ['dump_all' => true], 'all'],
]);
it('backs up the databases from the backup settings instead of the container env database', function (string $image, string $env) {
@@ -99,6 +99,30 @@ it('creates a service database backup without S3 and opens its configuration', f
->and($backup->s3_storage_id)->toBeNull();
});
it('backs up all databases by default for an Odoo service database', function (string $serviceType, bool $expectedDumpAll) {
$service = Service::factory()->create([
'server_id' => $this->server->id,
'destination_id' => $this->destination->id,
'destination_type' => $this->destination->getMorphClass(),
'environment_id' => $this->environment->id,
'service_type' => $serviceType,
]);
$database = ServiceDatabase::create([
'service_id' => $service->id,
'name' => 'postgresql',
'image' => 'postgres:16-alpine',
]);
Livewire::test(CreateScheduledBackup::class, ['database' => $database])
->set('frequency', 'daily')
->call('submit');
expect((bool) ScheduledDatabaseBackup::query()->sole()->dump_all)->toBe($expectedDumpAll);
})->with([
'odoo' => ['odoo', true],
'other service' => ['drupal-with-postgresql', false],
]);
it('rejects an unsupported service database name during backup', function () {
$service = Service::factory()->create([
'server_id' => $this->server->id,