mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-04 10:42:23 +00:00
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:
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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user