Files
coolify/app/Actions/Server/UpdatePackage.php
T
Andras BacsaiandClaude Opus 5.5 f32ecca44c feat(patches): record package updates, reboots, and auto-update changes in the audit log
- ui.server.packages_update_requested: user, server, packages (or all),
  package manager, and the activity of the update.
- ui.server.packages_update_finished: written when the remote process
  finishes (also when it fails), with the result and the exit code of
  the activity. The update gets a fresh type_uuid (an indexed activity
  property), so the listener finds exactly this activity. The actor
  comes from the request, because the listener runs in the queue.
- ui.server.reboot_requested with result started or failed.
- ui.server.auto_security_updates_updated (enabled, time, package
  manager, result) and ui.server.patch_settings_updated for a changed
  check frequency.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 10:44:03 +02:00

103 lines
4.2 KiB
PHP

<?php
namespace App\Actions\Server;
use App\Models\Server;
use Illuminate\Support\Str;
use Lorisleiva\Actions\Concerns\AsAction;
use Spatie\Activitylog\Contracts\Activity;
class UpdatePackage
{
use AsAction;
public string $jobQueue = 'high';
public function handle(Server $server, string $osId, ?string $package = null, ?string $packageManager = null, bool $all = false): Activity|array
{
try {
if ($server->serverStatus() === false) {
return [
'error' => 'Server is not reachable or not ready.',
];
}
// Validate that package name is provided when not updating all packages
if (! $all && ($package === null || $package === '')) {
return [
'error' => "Package name required when 'all' is false.",
];
}
// Sanitize package name to prevent command injection
// Only allow alphanumeric characters, hyphens, underscores, periods, plus signs, and colons
// These are valid characters in package names across most package managers
$sanitizedPackage = '';
if ($package !== null && ! $all) {
if (! preg_match('/^[a-zA-Z0-9._+:-]+$/', $package)) {
return [
'error' => 'Invalid package name. Package names can only contain alphanumeric characters, hyphens, underscores, periods, plus signs, and colons.',
];
}
$sanitizedPackage = escapeshellarg($package);
}
if (! $all && in_array($packageManager, CheckUpdates::FULL_UPGRADE_ONLY, true)) {
return [
'error' => 'This operating system supports only a full system upgrade. Use "Update all packages".',
];
}
$commands = self::commands($packageManager, $all ? null : $sanitizedPackage);
if ($commands === null) {
return [
'error' => 'OS not supported',
];
}
$updateId = (string) Str::uuid();
$packages = $all ? [] : [$package];
$user = auth()->user();
return remote_process($commands, $server, type_uuid: $updateId, callEventOnFinish: 'ServerPackagesUpdateFinished', callEventData: [
'update_id' => $updateId,
'server_uuid' => $server->uuid,
'packages' => $packages,
'package_manager' => $packageManager,
'actor_id' => $user?->id,
'actor_name' => $user?->name,
'actor_email' => $user?->email,
]);
} catch (\Exception $e) {
return [
'error' => $e->getMessage(),
];
}
}
/**
* Update commands that never wait for input. apt keeps changed configuration files (confold) and uses the
* package default for unchanged ones (confdef). A null package updates all packages.
*
* @return array<int, string>|null
*/
public static function commands(?string $packageManager, ?string $escapedPackage = null): ?array
{
$aptOptions = '-y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold';
return match ($packageManager) {
'apt' => [
'env DEBIAN_FRONTEND=noninteractive apt-get update -qq',
$escapedPackage === null
? "env DEBIAN_FRONTEND=noninteractive apt-get {$aptOptions} upgrade"
: "env DEBIAN_FRONTEND=noninteractive apt-get {$aptOptions} install --only-upgrade {$escapedPackage}",
],
'dnf' => [$escapedPackage === null ? 'dnf upgrade -y' : "dnf upgrade -y {$escapedPackage}"],
'zypper' => [$escapedPackage === null ? 'zypper --non-interactive update' : "zypper --non-interactive update {$escapedPackage}"],
'pacman' => $escapedPackage === null ? ['pacman -Syu --noconfirm'] : null,
'apk' => ['apk update', $escapedPackage === null ? 'apk upgrade' : "apk upgrade {$escapedPackage}"],
default => null,
};
}
}