mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
- Security updates: apt marks a package from a "-security" suite (Debian 12 now reports oldstable-security). dnf reads `dnf updateinfo list --security` (dnf5 keeps updateinfo as an alias of advisory). zypper uses `zypper -x patch --dry-run --category security`, because `zypper -x list-patches` ignores --category and has no package names. pacman and apk cannot tell, so they show no security count. https://dnf5.readthedocs.io/en/latest/commands/advisory.8.html https://github.com/openSUSE/zypper/blob/master/doc/zypper.8.txt - Reboot required: /var/run/reboot-required, `needrestart -b` (KSTA 2 or 3), `dnf needs-restarting -r` (matched by text, because a missing plugin also exits 1), and `zypper needs-rebooting` (exit 102). https://github.com/liske/needrestart/blob/master/README.batch.md https://dnf-plugins-core.readthedocs.io/en/latest/needs_restarting.html - An explicit risk list per package manager (Docker engine, kernels, libc/glibc/musl, systemd, dbus, openssh) replaces the "name contains docker/kernel" guess on the page and in every notification channel. - The page shows security and reboot badges, a security count, and the risk of each package. "Reboot server" (with confirmation, update policy on the loaded server) schedules the reboot with systemd-run and checks the server again five minutes later. Archived (transferred) servers are never touched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
116 lines
3.9 KiB
PHP
116 lines
3.9 KiB
PHP
<?php
|
|
|
|
namespace App\Jobs;
|
|
|
|
use App\Actions\Server\CheckUpdates;
|
|
use App\Events\ServerPatchCheckFinished;
|
|
use App\Models\Server;
|
|
use App\Notifications\Server\ServerPatchCheck;
|
|
use Illuminate\Bus\Queueable;
|
|
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
use Illuminate\Foundation\Bus\Dispatchable;
|
|
use Illuminate\Queue\InteractsWithQueue;
|
|
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
|
use Illuminate\Queue\SerializesModels;
|
|
use Illuminate\Support\Arr;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class ServerPatchCheckJob implements ShouldBeEncrypted, ShouldQueue
|
|
{
|
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
|
|
|
public $timeout = 600;
|
|
|
|
public $tries = 1;
|
|
|
|
/**
|
|
* @param bool $manual A check that a user started from the patches page. It saves the result also for a
|
|
* server that is not functional, and it sends no "updates available" notification.
|
|
*/
|
|
public function __construct(public Server $server, public bool $manual = false)
|
|
{
|
|
$this->onQueue(maintenance_queue());
|
|
}
|
|
|
|
public static function runningCacheKey(Server $server): string
|
|
{
|
|
return "server-patch-check-running:{$server->uuid}";
|
|
}
|
|
|
|
/**
|
|
* Queues a manual check and marks it as running, so the patches page shows the progress after a reload.
|
|
*/
|
|
public static function dispatchManual(Server $server): void
|
|
{
|
|
Cache::put(self::runningCacheKey($server), true, now()->addMinutes(15));
|
|
self::dispatch($server, true);
|
|
}
|
|
|
|
public function middleware(): array
|
|
{
|
|
return [(new WithoutOverlapping('server-patch-check-'.$this->server->uuid))->expireAfter(600)->dontRelease()];
|
|
}
|
|
|
|
public function handle(): void
|
|
{
|
|
try {
|
|
$team = data_get($this->server, 'team');
|
|
if (! $team || $this->server->isTransferredAway()) {
|
|
return;
|
|
}
|
|
|
|
if ($this->server->isFunctional() === false) {
|
|
if ($this->manual) {
|
|
$this->saveResult(['error' => 'Server is not reachable or not ready.']);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
$previousError = $this->server->settings->patch_check_error;
|
|
$patchData = CheckUpdates::run($this->server);
|
|
$this->saveResult($patchData);
|
|
|
|
if (isset($patchData['error'])) {
|
|
// Notify only when the state changes from ok to error, not on every check of a broken server.
|
|
if (blank($previousError)) {
|
|
$team->notify(new ServerPatchCheck($this->server, $patchData));
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (! $this->manual && ($patchData['total_updates'] ?? 0) > 0) {
|
|
$team->notify(new ServerPatchCheck($this->server, $patchData));
|
|
}
|
|
} catch (\Throwable $e) {
|
|
Log::error('ServerPatchCheckJob failed: '.$e->getMessage(), [
|
|
'server_id' => $this->server->id,
|
|
'server_name' => $this->server->name,
|
|
'error' => $e->getMessage(),
|
|
'trace' => $e->getTraceAsString(),
|
|
]);
|
|
$this->saveResult(['error' => $e->getMessage()]);
|
|
} finally {
|
|
Cache::forget(self::runningCacheKey($this->server));
|
|
ServerPatchCheckFinished::dispatch($this->server->team_id, $this->server->uuid);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $patchData
|
|
*/
|
|
private function saveResult(array $patchData): void
|
|
{
|
|
$error = $patchData['error'] ?? null;
|
|
|
|
$this->server->settings->forceFill([
|
|
'patch_check_result' => $error === null ? $patchData : Arr::only($patchData, ['osId', 'package_manager']),
|
|
'patch_check_error' => $error === null ? null : (string) $error,
|
|
'patch_checked_at' => now(),
|
|
])->saveQuietly();
|
|
}
|
|
}
|