mirror of
https://github.com/tiennm99/coolify.git
synced 2026-08-19 08:23:28 +00:00
feat(backups): add configurable CPU usage for volume compression
Add server-level compression CPU presets for volume backups and close the private key modal after saving.
This commit is contained in:
@@ -77,13 +77,14 @@ class VolumeBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$source = $this->backup->sourcePath();
|
||||
$containerName = 'volume-backup-'.$this->execution->uuid;
|
||||
$image = coolifyHelperImage().':'.getHelperVersion();
|
||||
$this->logCompressorInDevelopment($image, $server);
|
||||
$compressionCpuPercentage = $this->compressionCpuPercentage($server);
|
||||
$this->logCompressorInDevelopment($image, $server, $compressionCpuPercentage);
|
||||
$verifySourceCommand = $target instanceof LocalPersistentVolume && blank($target->host_path)
|
||||
? 'docker volume inspect '.escapeshellarg($source).' >/dev/null'
|
||||
: 'test -d '.escapeshellarg($source);
|
||||
|
||||
$archiveScript = "compressor='gzip -3'; "
|
||||
.'if command -v pigz >/dev/null 2>&1; then compressor="pigz -3 -p $(( ($(nproc) + 1) / 2 ))"; fi; '
|
||||
."if command -v pigz >/dev/null 2>&1; then compressor=\"pigz -3 -p \$(( (\$(nproc) * {$compressionCpuPercentage} + 99) / 100 ))\"; fi; "
|
||||
.'tar -I "$compressor" -cf - -C /volume .';
|
||||
$archiveCommand = 'docker run --rm --name '.escapeshellarg($containerName)
|
||||
.' -v '.escapeshellarg($source.':/volume:ro')
|
||||
@@ -337,13 +338,13 @@ class VolumeBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
}
|
||||
}
|
||||
|
||||
private function logCompressorInDevelopment(string $image, Server $server): void
|
||||
private function logCompressorInDevelopment(string $image, Server $server, int $compressionCpuPercentage): void
|
||||
{
|
||||
if (! isDev()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$script = "if command -v pigz >/dev/null 2>&1; then printf 'pigz -3 -p %s' \"$(( ($(nproc) + 1) / 2 ))\"; else printf 'gzip -3'; fi";
|
||||
$script = "if command -v pigz >/dev/null 2>&1; then printf 'pigz -3 -p %s' \"\$(( (\$(nproc) * {$compressionCpuPercentage} + 99) / 100 ))\"; else printf 'gzip -3'; fi";
|
||||
$compressor = instant_remote_process(
|
||||
['docker run --rm '.escapeshellarg($image).' sh -c '.escapeshellarg($script)],
|
||||
$server,
|
||||
@@ -356,9 +357,17 @@ class VolumeBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
'execution_id' => $this->execution?->id,
|
||||
'compressor' => $compressor,
|
||||
'helper_image' => $image,
|
||||
'cpu_percentage' => $compressionCpuPercentage,
|
||||
]);
|
||||
}
|
||||
|
||||
private function compressionCpuPercentage(Server $server): int
|
||||
{
|
||||
$percentage = (int) ($server->settings->backup_compression_cpu_percentage ?? 25);
|
||||
|
||||
return in_array($percentage, [25, 50, 75, 100], true) ? $percentage : 25;
|
||||
}
|
||||
|
||||
private function removeExpiredBackups(Server $server): void
|
||||
{
|
||||
if ($this->hasRetentionLimits(
|
||||
|
||||
@@ -151,6 +151,9 @@ class Show extends Component
|
||||
refresh_server_connection($this->private_key);
|
||||
$this->dispatch('success', 'Private key updated.');
|
||||
$this->dispatch('securityResourceChanged');
|
||||
if ($this->modalMode) {
|
||||
$this->dispatch('close-modal');
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,9 @@ class Advanced extends Component
|
||||
#[Validate(['required', 'integer', 'min:1'])]
|
||||
public int|string $deploymentQueueLimit = 25;
|
||||
|
||||
#[Validate(['required', 'integer', 'in:25,50,75,100'])]
|
||||
public int|string $backupCompressionCpuPercentage = 25;
|
||||
|
||||
public function mount(string $server_uuid)
|
||||
{
|
||||
try {
|
||||
@@ -47,6 +50,7 @@ class Advanced extends Component
|
||||
$this->server->settings->concurrent_builds = $this->concurrentBuilds;
|
||||
$this->server->settings->dynamic_timeout = $this->dynamicTimeout;
|
||||
$this->server->settings->deployment_queue_limit = $this->deploymentQueueLimit;
|
||||
$this->server->settings->backup_compression_cpu_percentage = $this->backupCompressionCpuPercentage;
|
||||
$this->server->settings->server_disk_usage_notification_threshold = $this->serverDiskUsageNotificationThreshold;
|
||||
$this->server->settings->server_disk_usage_check_frequency = $this->serverDiskUsageCheckFrequency;
|
||||
$this->server->settings->save();
|
||||
@@ -54,6 +58,7 @@ class Advanced extends Component
|
||||
$this->concurrentBuilds = $this->server->settings->concurrent_builds;
|
||||
$this->dynamicTimeout = $this->server->settings->dynamic_timeout;
|
||||
$this->deploymentQueueLimit = $this->server->settings->deployment_queue_limit;
|
||||
$this->backupCompressionCpuPercentage = $this->server->settings->backup_compression_cpu_percentage;
|
||||
$this->serverDiskUsageNotificationThreshold = $this->server->settings->server_disk_usage_notification_threshold;
|
||||
$this->serverDiskUsageCheckFrequency = $this->server->settings->server_disk_usage_check_frequency;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ use OpenApi\Attributes as OA;
|
||||
'id' => ['type' => 'integer'],
|
||||
'concurrent_builds' => ['type' => 'integer'],
|
||||
'deployment_queue_limit' => ['type' => 'integer'],
|
||||
'backup_compression_cpu_percentage' => ['type' => 'integer'],
|
||||
'dynamic_timeout' => ['type' => 'integer'],
|
||||
'force_disabled' => ['type' => 'boolean'],
|
||||
'force_server_cleanup' => ['type' => 'boolean'],
|
||||
@@ -102,6 +103,7 @@ class ServerSetting extends Model
|
||||
'server_disk_usage_check_frequency',
|
||||
'is_terminal_enabled',
|
||||
'deployment_queue_limit',
|
||||
'backup_compression_cpu_percentage',
|
||||
'disable_application_image_retention',
|
||||
'connection_timeout',
|
||||
'docker_version',
|
||||
@@ -123,6 +125,7 @@ class ServerSetting extends Model
|
||||
'connection_timeout' => 'integer',
|
||||
'docker_version_checked_at' => 'datetime',
|
||||
'compose_version_checked_at' => 'datetime',
|
||||
'backup_compression_cpu_percentage' => 'integer',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('server_settings', function (Blueprint $table) {
|
||||
$table->unsignedTinyInteger('backup_compression_cpu_percentage')->default(25);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('server_settings', function (Blueprint $table) {
|
||||
$table->dropColumn('backup_compression_cpu_percentage');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -25,6 +25,18 @@
|
||||
</div>
|
||||
</x-application.settings-section>
|
||||
|
||||
<x-application.settings-section id="server-backups-section" title="Backups"
|
||||
helper="Limit how much CPU volume backup compression may use on this server.">
|
||||
<x-forms.listbox canGate="update" :canResource="$server" id="backupCompressionCpuPercentage"
|
||||
label="Backup compression CPU" onChange="instantSave"
|
||||
helper="Sets how many CPU threads can be used to compress volume backups, based on this server's available CPUs." :options="[
|
||||
['value' => 25, 'label' => 'Low (25%)'],
|
||||
['value' => 50, 'label' => 'Balanced (50%)'],
|
||||
['value' => 75, 'label' => 'High (75%)'],
|
||||
['value' => 100, 'label' => 'Maximum (100%)'],
|
||||
]" />
|
||||
</x-application.settings-section>
|
||||
|
||||
<x-application.settings-section id="server-builds-section" title="Builds"
|
||||
helper="Set deployment concurrency, execution timeouts, and queue capacity.">
|
||||
<div class="grid gap-4 lg:grid-cols-3">
|
||||
|
||||
@@ -60,6 +60,22 @@ test('generating a private key from the index stores it without redirecting to a
|
||||
->assertNoRedirect();
|
||||
});
|
||||
|
||||
test('saving a private key from its modal editor closes the modal', function () {
|
||||
$privateKey = PrivateKey::factory()->create([
|
||||
'team_id' => $this->team->id,
|
||||
]);
|
||||
|
||||
Livewire::test(Show::class, [
|
||||
'private_key_uuid' => $privateKey->uuid,
|
||||
'modalMode' => true,
|
||||
])->set('name', 'Updated SSH key')
|
||||
->call('changePrivateKey')
|
||||
->assertDispatched('securityResourceChanged')
|
||||
->assertDispatched('close-modal');
|
||||
|
||||
expect($privateKey->fresh()->name)->toBe('Updated SSH key');
|
||||
});
|
||||
|
||||
test('manual private key form does not expose key generation controls', function () {
|
||||
Livewire::test(Create::class)
|
||||
->assertDontSee('Generate new ED25519 SSH Key')
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Server\Advanced;
|
||||
use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('defaults and saves the server backup compression CPU preset', function () {
|
||||
$team = Team::factory()->create();
|
||||
$user = User::factory()->create();
|
||||
$team->members()->attach($user, ['role' => 'owner']);
|
||||
$server = Server::factory()->create(['team_id' => $team->id]);
|
||||
|
||||
$this->actingAs($user);
|
||||
session(['currentTeam' => $team]);
|
||||
|
||||
expect($server->settings->backup_compression_cpu_percentage)->toBe(25);
|
||||
|
||||
Livewire::test(Advanced::class, ['server_uuid' => $server->uuid])
|
||||
->assertSet('backupCompressionCpuPercentage', 25)
|
||||
->assertSee('Backup compression CPU')
|
||||
->assertSee('Sets how many CPU threads can be used to compress volume backups')
|
||||
->assertDontSee('gzip fallback')
|
||||
->assertSee('Low (25%)')
|
||||
->set('backupCompressionCpuPercentage', 75)
|
||||
->call('instantSave')
|
||||
->assertHasNoErrors()
|
||||
->assertDispatched('success');
|
||||
|
||||
expect($server->settings->fresh()->backup_compression_cpu_percentage)->toBe(75);
|
||||
});
|
||||
|
||||
it('rejects unsupported server backup compression CPU percentages', function () {
|
||||
$team = Team::factory()->create();
|
||||
$user = User::factory()->create();
|
||||
$team->members()->attach($user, ['role' => 'owner']);
|
||||
$server = Server::factory()->create(['team_id' => $team->id]);
|
||||
|
||||
$this->actingAs($user);
|
||||
session(['currentTeam' => $team]);
|
||||
|
||||
Livewire::test(Advanced::class, ['server_uuid' => $server->uuid])
|
||||
->set('backupCompressionCpuPercentage', 30)
|
||||
->call('instantSave')
|
||||
->assertHasErrors(['backupCompressionCpuPercentage']);
|
||||
|
||||
expect($server->settings->fresh()->backup_compression_cpu_percentage)->toBe(25);
|
||||
});
|
||||
@@ -1570,11 +1570,12 @@ it('marks a running execution failed even when the job instance lost its executi
|
||||
&& str_contains($process->command, 'timed-out.tar.gz'));
|
||||
});
|
||||
|
||||
it('archives a named volume on its server', function () {
|
||||
it('archives a named volume using the server compression CPU percentage', function (int $compressionCpuPercentage) {
|
||||
config(['broadcasting.default' => 'null']);
|
||||
InstanceSettings::unguarded(fn () => InstanceSettings::create(['id' => 0]));
|
||||
$team = Team::factory()->create();
|
||||
[$application, $volume] = createVolumeBackupApplication($team);
|
||||
[$application, $volume, $server] = createVolumeBackupApplication($team);
|
||||
$server->settings->update(['backup_compression_cpu_percentage' => $compressionCpuPercentage]);
|
||||
$backup = $volume->scheduledBackups()->create([
|
||||
'team_id' => $team->id,
|
||||
'frequency' => 'daily',
|
||||
@@ -1608,13 +1609,16 @@ it('archives a named volume on its server', function () {
|
||||
&& str_contains($process->command, 'app-data:/volume:ro')
|
||||
&& str_contains($process->command, 'command -v pigz')
|
||||
&& str_contains($process->command, 'pigz -3 -p')
|
||||
&& str_contains($process->command, '$(nproc) + 1')
|
||||
&& str_contains($process->command, "\$(nproc) * {$compressionCpuPercentage} + 99")
|
||||
&& str_contains($process->command, 'gzip -3')
|
||||
&& str_contains($process->command, 'tar -I "$compressor" -cf -')
|
||||
&& str_contains($process->command, '> ')
|
||||
&& str_contains($process->command, '.tar.gz')
|
||||
&& ! str_contains($process->command, ':/backup'));
|
||||
});
|
||||
})->with([
|
||||
'low' => 25,
|
||||
'high' => 75,
|
||||
]);
|
||||
|
||||
it('logs the selected volume backup compressor in development', function (string $detectedCompressor) {
|
||||
config(['app.env' => 'local', 'broadcasting.default' => 'null']);
|
||||
@@ -1644,7 +1648,8 @@ it('logs the selected volume backup compressor in development', function (string
|
||||
Log::shouldHaveReceived('info')->once()->with(
|
||||
'Volume backup compressor selected',
|
||||
Mockery::on(fn (array $context): bool => $context['compressor'] === $detectedCompressor
|
||||
&& $context['backup_id'] === $backup->id),
|
||||
&& $context['backup_id'] === $backup->id
|
||||
&& $context['cpu_percentage'] === 25),
|
||||
);
|
||||
})->with([
|
||||
'pigz' => 'pigz -3 -p 4',
|
||||
|
||||
Reference in New Issue
Block a user