feat(server): implement server patch check notifications
- Added a new job, ServerPatchCheckJob, to handle server patch checks and notifications. - Introduced a new notification class, ServerPatchCheck, for sending updates via email, Discord, Slack, Pushover, and Telegram. - Updated notification settings models to include server patch notification options for email, Discord, Slack, Pushover, and Telegram. - Created a migration to add server patch notification fields to the respective settings tables. - Enhanced the UI to allow users to enable/disable server patch notifications across different channels.
This commit is contained in:
@@ -83,8 +83,6 @@ class CheckUpdates
|
||||
$out = $this->parseDnfOutput($output);
|
||||
$out['osId'] = $osId;
|
||||
$out['package_manager'] = $packageManager;
|
||||
$rebootRequired = instant_remote_process(['LANG=C dnf needs-restarting -r'], $server);
|
||||
$out['reboot_required'] = $rebootRequired !== '0';
|
||||
|
||||
return $out;
|
||||
case 'apt':
|
||||
@@ -94,8 +92,6 @@ class CheckUpdates
|
||||
$out = $this->parseAptOutput($output);
|
||||
$out['osId'] = $osId;
|
||||
$out['package_manager'] = $packageManager;
|
||||
$rebootRequired = instant_remote_process(['LANG=C test -f /var/run/reboot-required && echo "YES" || echo "NO"'], $server);
|
||||
$out['reboot_required'] = $rebootRequired === 'YES' ? true : false;
|
||||
|
||||
return $out;
|
||||
default:
|
||||
|
@@ -12,6 +12,7 @@ use App\Jobs\PullTemplatesFromCDN;
|
||||
use App\Jobs\RegenerateSslCertJob;
|
||||
use App\Jobs\ScheduledTaskJob;
|
||||
use App\Jobs\ServerCheckJob;
|
||||
use App\Jobs\ServerPatchCheckJob;
|
||||
use App\Jobs\ServerStorageCheckJob;
|
||||
use App\Jobs\UpdateCoolifyJob;
|
||||
use App\Models\InstanceSettings;
|
||||
@@ -175,6 +176,9 @@ class Kernel extends ConsoleKernel
|
||||
}
|
||||
$this->scheduleInstance->job(new DockerCleanupJob($server))->cron($dockerCleanupFrequency)->timezone($serverTimezone)->onOneServer();
|
||||
|
||||
// Server patch check - weekly
|
||||
$this->scheduleInstance->job(new ServerPatchCheckJob($server))->weekly()->timezone($serverTimezone)->onOneServer();
|
||||
|
||||
// Cleanup multiplexed connections every hour
|
||||
// $this->scheduleInstance->job(new ServerCleanupMux($server))->hourly()->onOneServer();
|
||||
|
||||
|
66
app/Jobs/ServerPatchCheckJob.php
Normal file
66
app/Jobs/ServerPatchCheckJob.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Actions\Server\CheckUpdates;
|
||||
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;
|
||||
|
||||
class ServerPatchCheckJob implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public $tries = 3;
|
||||
|
||||
public $timeout = 600; // 10 minutes timeout
|
||||
|
||||
public function middleware(): array
|
||||
{
|
||||
return [(new WithoutOverlapping($this->server->uuid))->dontRelease()];
|
||||
}
|
||||
|
||||
public function __construct(public Server $server) {}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
if ($this->server->isFunctional() === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
$team = data_get($this->server, 'team');
|
||||
if (! $team) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for updates
|
||||
$patchData = CheckUpdates::run($this->server);
|
||||
|
||||
if (isset($patchData['error'])) {
|
||||
return; // Skip if there's an error checking for updates
|
||||
}
|
||||
|
||||
$totalUpdates = $patchData['total_updates'] ?? 0;
|
||||
|
||||
// Only send notification if there are updates available
|
||||
if ($totalUpdates > 0) {
|
||||
$team->notify(new ServerPatchCheck($this->server, $patchData));
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Log error but don't fail the job
|
||||
\Illuminate\Support\Facades\Log::error('ServerPatchCheckJob failed: '.$e->getMessage(), [
|
||||
'server_id' => $this->server->id,
|
||||
'server_name' => $this->server->name,
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
@@ -56,6 +56,9 @@ class Discord extends Component
|
||||
#[Validate(['boolean'])]
|
||||
public bool $serverUnreachableDiscordNotifications = true;
|
||||
|
||||
#[Validate(['boolean'])]
|
||||
public bool $serverPatchDiscordNotifications = false;
|
||||
|
||||
#[Validate(['boolean'])]
|
||||
public bool $discordPingEnabled = true;
|
||||
|
||||
@@ -89,6 +92,7 @@ class Discord extends Component
|
||||
$this->settings->server_disk_usage_discord_notifications = $this->serverDiskUsageDiscordNotifications;
|
||||
$this->settings->server_reachable_discord_notifications = $this->serverReachableDiscordNotifications;
|
||||
$this->settings->server_unreachable_discord_notifications = $this->serverUnreachableDiscordNotifications;
|
||||
$this->settings->server_patch_discord_notifications = $this->serverPatchDiscordNotifications;
|
||||
|
||||
$this->settings->discord_ping_enabled = $this->discordPingEnabled;
|
||||
|
||||
@@ -110,6 +114,7 @@ class Discord extends Component
|
||||
$this->serverDiskUsageDiscordNotifications = $this->settings->server_disk_usage_discord_notifications;
|
||||
$this->serverReachableDiscordNotifications = $this->settings->server_reachable_discord_notifications;
|
||||
$this->serverUnreachableDiscordNotifications = $this->settings->server_unreachable_discord_notifications;
|
||||
$this->serverPatchDiscordNotifications = $this->settings->server_patch_discord_notifications;
|
||||
|
||||
$this->discordPingEnabled = $this->settings->discord_ping_enabled;
|
||||
}
|
||||
|
@@ -98,6 +98,9 @@ class Email extends Component
|
||||
#[Validate(['boolean'])]
|
||||
public bool $serverUnreachableEmailNotifications = true;
|
||||
|
||||
#[Validate(['boolean'])]
|
||||
public bool $serverPatchEmailNotifications = false;
|
||||
|
||||
#[Validate(['nullable', 'email'])]
|
||||
public ?string $testEmailAddress = null;
|
||||
|
||||
@@ -146,6 +149,7 @@ class Email extends Component
|
||||
$this->settings->server_disk_usage_email_notifications = $this->serverDiskUsageEmailNotifications;
|
||||
$this->settings->server_reachable_email_notifications = $this->serverReachableEmailNotifications;
|
||||
$this->settings->server_unreachable_email_notifications = $this->serverUnreachableEmailNotifications;
|
||||
$this->settings->server_patch_email_notifications = $this->serverPatchEmailNotifications;
|
||||
$this->settings->save();
|
||||
|
||||
} else {
|
||||
@@ -177,6 +181,7 @@ class Email extends Component
|
||||
$this->serverDiskUsageEmailNotifications = $this->settings->server_disk_usage_email_notifications;
|
||||
$this->serverReachableEmailNotifications = $this->settings->server_reachable_email_notifications;
|
||||
$this->serverUnreachableEmailNotifications = $this->settings->server_unreachable_email_notifications;
|
||||
$this->serverPatchEmailNotifications = $this->settings->server_patch_email_notifications;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -64,6 +64,9 @@ class Pushover extends Component
|
||||
#[Validate(['boolean'])]
|
||||
public bool $serverUnreachablePushoverNotifications = true;
|
||||
|
||||
#[Validate(['boolean'])]
|
||||
public bool $serverPatchPushoverNotifications = false;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
try {
|
||||
@@ -95,6 +98,7 @@ class Pushover extends Component
|
||||
$this->settings->server_disk_usage_pushover_notifications = $this->serverDiskUsagePushoverNotifications;
|
||||
$this->settings->server_reachable_pushover_notifications = $this->serverReachablePushoverNotifications;
|
||||
$this->settings->server_unreachable_pushover_notifications = $this->serverUnreachablePushoverNotifications;
|
||||
$this->settings->server_patch_pushover_notifications = $this->serverPatchPushoverNotifications;
|
||||
|
||||
$this->settings->save();
|
||||
refreshSession();
|
||||
@@ -115,6 +119,7 @@ class Pushover extends Component
|
||||
$this->serverDiskUsagePushoverNotifications = $this->settings->server_disk_usage_pushover_notifications;
|
||||
$this->serverReachablePushoverNotifications = $this->settings->server_reachable_pushover_notifications;
|
||||
$this->serverUnreachablePushoverNotifications = $this->settings->server_unreachable_pushover_notifications;
|
||||
$this->serverPatchPushoverNotifications = $this->settings->server_patch_pushover_notifications;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -61,6 +61,9 @@ class Slack extends Component
|
||||
#[Validate(['boolean'])]
|
||||
public bool $serverUnreachableSlackNotifications = true;
|
||||
|
||||
#[Validate(['boolean'])]
|
||||
public bool $serverPatchSlackNotifications = false;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
try {
|
||||
@@ -91,6 +94,7 @@ class Slack extends Component
|
||||
$this->settings->server_disk_usage_slack_notifications = $this->serverDiskUsageSlackNotifications;
|
||||
$this->settings->server_reachable_slack_notifications = $this->serverReachableSlackNotifications;
|
||||
$this->settings->server_unreachable_slack_notifications = $this->serverUnreachableSlackNotifications;
|
||||
$this->settings->server_patch_slack_notifications = $this->serverPatchSlackNotifications;
|
||||
|
||||
$this->settings->save();
|
||||
refreshSession();
|
||||
@@ -110,6 +114,7 @@ class Slack extends Component
|
||||
$this->serverDiskUsageSlackNotifications = $this->settings->server_disk_usage_slack_notifications;
|
||||
$this->serverReachableSlackNotifications = $this->settings->server_reachable_slack_notifications;
|
||||
$this->serverUnreachableSlackNotifications = $this->settings->server_unreachable_slack_notifications;
|
||||
$this->serverPatchSlackNotifications = $this->settings->server_patch_slack_notifications;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -64,6 +64,9 @@ class Telegram extends Component
|
||||
#[Validate(['boolean'])]
|
||||
public bool $serverUnreachableTelegramNotifications = true;
|
||||
|
||||
#[Validate(['boolean'])]
|
||||
public bool $serverPatchTelegramNotifications = false;
|
||||
|
||||
#[Validate(['nullable', 'string'])]
|
||||
public ?string $telegramNotificationsDeploymentSuccessThreadId = null;
|
||||
|
||||
@@ -100,6 +103,9 @@ class Telegram extends Component
|
||||
#[Validate(['nullable', 'string'])]
|
||||
public ?string $telegramNotificationsServerUnreachableThreadId = null;
|
||||
|
||||
#[Validate(['nullable', 'string'])]
|
||||
public ?string $telegramNotificationsServerPatchThreadId = null;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
try {
|
||||
@@ -131,6 +137,7 @@ class Telegram extends Component
|
||||
$this->settings->server_disk_usage_telegram_notifications = $this->serverDiskUsageTelegramNotifications;
|
||||
$this->settings->server_reachable_telegram_notifications = $this->serverReachableTelegramNotifications;
|
||||
$this->settings->server_unreachable_telegram_notifications = $this->serverUnreachableTelegramNotifications;
|
||||
$this->settings->server_patch_telegram_notifications = $this->serverPatchTelegramNotifications;
|
||||
|
||||
$this->settings->telegram_notifications_deployment_success_thread_id = $this->telegramNotificationsDeploymentSuccessThreadId;
|
||||
$this->settings->telegram_notifications_deployment_failure_thread_id = $this->telegramNotificationsDeploymentFailureThreadId;
|
||||
@@ -144,6 +151,7 @@ class Telegram extends Component
|
||||
$this->settings->telegram_notifications_server_disk_usage_thread_id = $this->telegramNotificationsServerDiskUsageThreadId;
|
||||
$this->settings->telegram_notifications_server_reachable_thread_id = $this->telegramNotificationsServerReachableThreadId;
|
||||
$this->settings->telegram_notifications_server_unreachable_thread_id = $this->telegramNotificationsServerUnreachableThreadId;
|
||||
$this->settings->telegram_notifications_server_patch_thread_id = $this->telegramNotificationsServerPatchThreadId;
|
||||
|
||||
$this->settings->save();
|
||||
} else {
|
||||
@@ -163,6 +171,7 @@ class Telegram extends Component
|
||||
$this->serverDiskUsageTelegramNotifications = $this->settings->server_disk_usage_telegram_notifications;
|
||||
$this->serverReachableTelegramNotifications = $this->settings->server_reachable_telegram_notifications;
|
||||
$this->serverUnreachableTelegramNotifications = $this->settings->server_unreachable_telegram_notifications;
|
||||
$this->serverPatchTelegramNotifications = $this->settings->server_patch_telegram_notifications;
|
||||
|
||||
$this->telegramNotificationsDeploymentSuccessThreadId = $this->settings->telegram_notifications_deployment_success_thread_id;
|
||||
$this->telegramNotificationsDeploymentFailureThreadId = $this->settings->telegram_notifications_deployment_failure_thread_id;
|
||||
@@ -176,6 +185,7 @@ class Telegram extends Component
|
||||
$this->telegramNotificationsServerDiskUsageThreadId = $this->settings->telegram_notifications_server_disk_usage_thread_id;
|
||||
$this->telegramNotificationsServerReachableThreadId = $this->settings->telegram_notifications_server_reachable_thread_id;
|
||||
$this->telegramNotificationsServerUnreachableThreadId = $this->settings->telegram_notifications_server_unreachable_thread_id;
|
||||
$this->telegramNotificationsServerPatchThreadId = $this->settings->telegram_notifications_server_patch_thread_id;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -28,6 +28,7 @@ class DiscordNotificationSettings extends Model
|
||||
'server_disk_usage_discord_notifications',
|
||||
'server_reachable_discord_notifications',
|
||||
'server_unreachable_discord_notifications',
|
||||
'server_patch_discord_notifications',
|
||||
'discord_ping_enabled',
|
||||
];
|
||||
|
||||
@@ -46,6 +47,7 @@ class DiscordNotificationSettings extends Model
|
||||
'server_disk_usage_discord_notifications' => 'boolean',
|
||||
'server_reachable_discord_notifications' => 'boolean',
|
||||
'server_unreachable_discord_notifications' => 'boolean',
|
||||
'server_patch_discord_notifications' => 'boolean',
|
||||
'discord_ping_enabled' => 'boolean',
|
||||
];
|
||||
|
||||
|
@@ -35,6 +35,7 @@ class EmailNotificationSettings extends Model
|
||||
'scheduled_task_success_email_notifications',
|
||||
'scheduled_task_failure_email_notifications',
|
||||
'server_disk_usage_email_notifications',
|
||||
'server_patch_email_notifications',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
@@ -61,6 +62,7 @@ class EmailNotificationSettings extends Model
|
||||
'scheduled_task_success_email_notifications' => 'boolean',
|
||||
'scheduled_task_failure_email_notifications' => 'boolean',
|
||||
'server_disk_usage_email_notifications' => 'boolean',
|
||||
'server_patch_email_notifications' => 'boolean',
|
||||
];
|
||||
|
||||
public function team()
|
||||
|
@@ -29,6 +29,7 @@ class PushoverNotificationSettings extends Model
|
||||
'server_disk_usage_pushover_notifications',
|
||||
'server_reachable_pushover_notifications',
|
||||
'server_unreachable_pushover_notifications',
|
||||
'server_patch_pushover_notifications',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
@@ -47,6 +48,7 @@ class PushoverNotificationSettings extends Model
|
||||
'server_disk_usage_pushover_notifications' => 'boolean',
|
||||
'server_reachable_pushover_notifications' => 'boolean',
|
||||
'server_unreachable_pushover_notifications' => 'boolean',
|
||||
'server_patch_pushover_notifications' => 'boolean',
|
||||
];
|
||||
|
||||
public function team()
|
||||
|
@@ -28,6 +28,7 @@ class SlackNotificationSettings extends Model
|
||||
'server_disk_usage_slack_notifications',
|
||||
'server_reachable_slack_notifications',
|
||||
'server_unreachable_slack_notifications',
|
||||
'server_patch_slack_notifications',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
@@ -45,6 +46,7 @@ class SlackNotificationSettings extends Model
|
||||
'server_disk_usage_slack_notifications' => 'boolean',
|
||||
'server_reachable_slack_notifications' => 'boolean',
|
||||
'server_unreachable_slack_notifications' => 'boolean',
|
||||
'server_patch_slack_notifications' => 'boolean',
|
||||
];
|
||||
|
||||
public function team()
|
||||
|
@@ -29,6 +29,7 @@ class TelegramNotificationSettings extends Model
|
||||
'server_disk_usage_telegram_notifications',
|
||||
'server_reachable_telegram_notifications',
|
||||
'server_unreachable_telegram_notifications',
|
||||
'server_patch_telegram_notifications',
|
||||
|
||||
'telegram_notifications_deployment_success_thread_id',
|
||||
'telegram_notifications_deployment_failure_thread_id',
|
||||
@@ -41,6 +42,7 @@ class TelegramNotificationSettings extends Model
|
||||
'telegram_notifications_server_disk_usage_thread_id',
|
||||
'telegram_notifications_server_reachable_thread_id',
|
||||
'telegram_notifications_server_unreachable_thread_id',
|
||||
'telegram_notifications_server_patch_thread_id',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
@@ -59,6 +61,7 @@ class TelegramNotificationSettings extends Model
|
||||
'server_disk_usage_telegram_notifications' => 'boolean',
|
||||
'server_reachable_telegram_notifications' => 'boolean',
|
||||
'server_unreachable_telegram_notifications' => 'boolean',
|
||||
'server_patch_telegram_notifications' => 'boolean',
|
||||
|
||||
'telegram_notifications_deployment_success_thread_id' => 'encrypted',
|
||||
'telegram_notifications_deployment_failure_thread_id' => 'encrypted',
|
||||
@@ -71,6 +74,7 @@ class TelegramNotificationSettings extends Model
|
||||
'telegram_notifications_server_disk_usage_thread_id' => 'encrypted',
|
||||
'telegram_notifications_server_reachable_thread_id' => 'encrypted',
|
||||
'telegram_notifications_server_unreachable_thread_id' => 'encrypted',
|
||||
'telegram_notifications_server_patch_thread_id' => 'encrypted',
|
||||
];
|
||||
|
||||
public function team()
|
||||
|
@@ -34,6 +34,7 @@ class TelegramChannel
|
||||
\App\Notifications\Server\HighDiskUsage::class => $settings->telegram_notifications_server_disk_usage_thread_id,
|
||||
\App\Notifications\Server\Unreachable::class => $settings->telegram_notifications_server_unreachable_thread_id,
|
||||
\App\Notifications\Server\Reachable::class => $settings->telegram_notifications_server_reachable_thread_id,
|
||||
\App\Notifications\Server\ServerPatchCheck::class => $settings->telegram_notifications_server_patch_thread_id,
|
||||
|
||||
default => null,
|
||||
};
|
||||
|
245
app/Notifications/Server/ServerPatchCheck.php
Normal file
245
app/Notifications/Server/ServerPatchCheck.php
Normal file
@@ -0,0 +1,245 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications\Server;
|
||||
|
||||
use App\Models\Server;
|
||||
use App\Notifications\CustomEmailNotification;
|
||||
use App\Notifications\Dto\DiscordMessage;
|
||||
use App\Notifications\Dto\PushoverMessage;
|
||||
use App\Notifications\Dto\SlackMessage;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
|
||||
class ServerPatchCheck extends CustomEmailNotification
|
||||
{
|
||||
public string $serverUrl;
|
||||
|
||||
public function __construct(public Server $server, public array $patchData)
|
||||
{
|
||||
$this->onQueue('high');
|
||||
$this->serverUrl = route('server.security.patches', ['server_uuid' => $this->server->uuid]);
|
||||
if (isDev()) {
|
||||
$this->serverUrl = 'https://staging-but-dev.coolify.io/server/'.$this->server->uuid.'/security/patches';
|
||||
}
|
||||
}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
return $notifiable->getEnabledChannels('server_patch');
|
||||
}
|
||||
|
||||
public function toMail($notifiable = null): MailMessage
|
||||
{
|
||||
$mail = new MailMessage;
|
||||
$totalUpdates = $this->patchData['total_updates'] ?? 0;
|
||||
$mail->subject("Coolify: [ACTION REQUIRED] {$totalUpdates} server patches available on {$this->server->name}");
|
||||
$mail->view('emails.server-patches', [
|
||||
'name' => $this->server->name,
|
||||
'total_updates' => $totalUpdates,
|
||||
'updates' => $this->patchData['updates'] ?? [],
|
||||
'osId' => $this->patchData['osId'] ?? 'unknown',
|
||||
'package_manager' => $this->patchData['package_manager'] ?? 'unknown',
|
||||
'server_url' => $this->serverUrl,
|
||||
]);
|
||||
|
||||
return $mail;
|
||||
}
|
||||
|
||||
public function toDiscord(): DiscordMessage
|
||||
{
|
||||
$totalUpdates = $this->patchData['total_updates'] ?? 0;
|
||||
$updates = $this->patchData['updates'] ?? [];
|
||||
$osId = $this->patchData['osId'] ?? 'unknown';
|
||||
$packageManager = $this->patchData['package_manager'] ?? 'unknown';
|
||||
|
||||
$description = "**{$totalUpdates} package updates** available for server {$this->server->name}\n\n";
|
||||
$description .= "**Summary:**\n";
|
||||
$description .= '• OS: '.ucfirst($osId)."\n";
|
||||
$description .= "• Package Manager: {$packageManager}\n";
|
||||
$description .= "• Total Updates: {$totalUpdates}\n\n";
|
||||
|
||||
// Show first few packages
|
||||
if (count($updates) > 0) {
|
||||
$description .= "**Sample Updates:**\n";
|
||||
$sampleUpdates = array_slice($updates, 0, 5);
|
||||
foreach ($sampleUpdates as $update) {
|
||||
$description .= "• {$update['package']}: {$update['current_version']} → {$update['new_version']}\n";
|
||||
}
|
||||
if (count($updates) > 5) {
|
||||
$description .= '• ... and '.(count($updates) - 5)." more packages\n";
|
||||
}
|
||||
|
||||
// Check for critical packages
|
||||
$criticalPackages = collect($updates)->filter(function ($update) {
|
||||
return str_contains(strtolower($update['package']), 'docker') ||
|
||||
str_contains(strtolower($update['package']), 'kernel') ||
|
||||
str_contains(strtolower($update['package']), 'openssh') ||
|
||||
str_contains(strtolower($update['package']), 'ssl');
|
||||
});
|
||||
|
||||
if ($criticalPackages->count() > 0) {
|
||||
$description .= "\n **Critical packages detected** ({$criticalPackages->count()} packages may require restarts)";
|
||||
}
|
||||
$description .= "\n [Manage Server Patches]($this->serverUrl)";
|
||||
}
|
||||
|
||||
return new DiscordMessage(
|
||||
title: ':warning: Coolify: [ACTION REQUIRED] Server patches available on '.$this->server->name,
|
||||
description: $description,
|
||||
color: DiscordMessage::errorColor(),
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
public function toTelegram(): array
|
||||
{
|
||||
$totalUpdates = $this->patchData['total_updates'] ?? 0;
|
||||
$updates = $this->patchData['updates'] ?? [];
|
||||
$osId = $this->patchData['osId'] ?? 'unknown';
|
||||
$packageManager = $this->patchData['package_manager'] ?? 'unknown';
|
||||
|
||||
$message = "🔧 Coolify: [ACTION REQUIRED] {$totalUpdates} server patches available on {$this->server->name}!\n\n";
|
||||
$message .= "📊 Summary:\n";
|
||||
$message .= '• OS: '.ucfirst($osId)."\n";
|
||||
$message .= "• Package Manager: {$packageManager}\n";
|
||||
$message .= "• Total Updates: {$totalUpdates}\n\n";
|
||||
|
||||
if (count($updates) > 0) {
|
||||
$message .= "📦 Sample Updates:\n";
|
||||
$sampleUpdates = array_slice($updates, 0, 5);
|
||||
foreach ($sampleUpdates as $update) {
|
||||
$message .= "• {$update['package']}: {$update['current_version']} → {$update['new_version']}\n";
|
||||
}
|
||||
if (count($updates) > 5) {
|
||||
$message .= '• ... and '.(count($updates) - 5)." more packages\n";
|
||||
}
|
||||
|
||||
// Check for critical packages
|
||||
$criticalPackages = collect($updates)->filter(function ($update) {
|
||||
return str_contains(strtolower($update['package']), 'docker') ||
|
||||
str_contains(strtolower($update['package']), 'kernel') ||
|
||||
str_contains(strtolower($update['package']), 'openssh') ||
|
||||
str_contains(strtolower($update['package']), 'ssl');
|
||||
});
|
||||
|
||||
if ($criticalPackages->count() > 0) {
|
||||
$message .= "\n⚠️ Critical packages detected: {$criticalPackages->count()} packages may require restarts\n";
|
||||
foreach ($criticalPackages->take(3) as $package) {
|
||||
$message .= "• {$package['package']}: {$package['current_version']} → {$package['new_version']}\n";
|
||||
}
|
||||
if ($criticalPackages->count() > 3) {
|
||||
$message .= '• ... and '.($criticalPackages->count() - 3)." more critical packages\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'message' => $message,
|
||||
'buttons' => [
|
||||
[
|
||||
'text' => 'Manage Server Patches',
|
||||
'url' => $this->serverUrl,
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function toPushover(): PushoverMessage
|
||||
{
|
||||
$totalUpdates = $this->patchData['total_updates'] ?? 0;
|
||||
$updates = $this->patchData['updates'] ?? [];
|
||||
$osId = $this->patchData['osId'] ?? 'unknown';
|
||||
$packageManager = $this->patchData['package_manager'] ?? 'unknown';
|
||||
|
||||
$message = "[ACTION REQUIRED] {$totalUpdates} server patches available on {$this->server->name}!\n\n";
|
||||
$message .= "Summary:\n";
|
||||
$message .= '• OS: '.ucfirst($osId)."\n";
|
||||
$message .= "• Package Manager: {$packageManager}\n";
|
||||
$message .= "• Total Updates: {$totalUpdates}\n\n";
|
||||
|
||||
if (count($updates) > 0) {
|
||||
$message .= "Sample Updates:\n";
|
||||
$sampleUpdates = array_slice($updates, 0, 3);
|
||||
foreach ($sampleUpdates as $update) {
|
||||
$message .= "• {$update['package']}: {$update['current_version']} → {$update['new_version']}\n";
|
||||
}
|
||||
if (count($updates) > 3) {
|
||||
$message .= '• ... and '.(count($updates) - 3)." more packages\n";
|
||||
}
|
||||
|
||||
// Check for critical packages
|
||||
$criticalPackages = collect($updates)->filter(function ($update) {
|
||||
return str_contains(strtolower($update['package']), 'docker') ||
|
||||
str_contains(strtolower($update['package']), 'kernel') ||
|
||||
str_contains(strtolower($update['package']), 'openssh') ||
|
||||
str_contains(strtolower($update['package']), 'ssl');
|
||||
});
|
||||
|
||||
if ($criticalPackages->count() > 0) {
|
||||
$message .= "\nCritical packages detected: {$criticalPackages->count()} may require restarts";
|
||||
}
|
||||
}
|
||||
|
||||
return new PushoverMessage(
|
||||
title: 'Server patches available',
|
||||
level: 'error',
|
||||
message: $message,
|
||||
buttons: [
|
||||
[
|
||||
'text' => 'Manage Server Patches',
|
||||
'url' => $this->serverUrl,
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
public function toSlack(): SlackMessage
|
||||
{
|
||||
$totalUpdates = $this->patchData['total_updates'] ?? 0;
|
||||
$updates = $this->patchData['updates'] ?? [];
|
||||
$osId = $this->patchData['osId'] ?? 'unknown';
|
||||
$packageManager = $this->patchData['package_manager'] ?? 'unknown';
|
||||
|
||||
$description = "{$totalUpdates} server patches available on '{$this->server->name}'!\n\n";
|
||||
$description .= "*Summary:*\n";
|
||||
$description .= '• OS: '.ucfirst($osId)."\n";
|
||||
$description .= "• Package Manager: {$packageManager}\n";
|
||||
$description .= "• Total Updates: {$totalUpdates}\n\n";
|
||||
|
||||
if (count($updates) > 0) {
|
||||
$description .= "*Sample Updates:*\n";
|
||||
$sampleUpdates = array_slice($updates, 0, 5);
|
||||
foreach ($sampleUpdates as $update) {
|
||||
$description .= "• `{$update['package']}`: {$update['current_version']} → {$update['new_version']}\n";
|
||||
}
|
||||
if (count($updates) > 5) {
|
||||
$description .= '• ... and '.(count($updates) - 5)." more packages\n";
|
||||
}
|
||||
|
||||
// Check for critical packages
|
||||
$criticalPackages = collect($updates)->filter(function ($update) {
|
||||
return str_contains(strtolower($update['package']), 'docker') ||
|
||||
str_contains(strtolower($update['package']), 'kernel') ||
|
||||
str_contains(strtolower($update['package']), 'openssh') ||
|
||||
str_contains(strtolower($update['package']), 'ssl');
|
||||
});
|
||||
|
||||
if ($criticalPackages->count() > 0) {
|
||||
$description .= "\n:warning: *Critical packages detected:* {$criticalPackages->count()} packages may require restarts\n";
|
||||
foreach ($criticalPackages->take(3) as $package) {
|
||||
$description .= "• `{$package['package']}`: {$package['current_version']} → {$package['new_version']}\n";
|
||||
}
|
||||
if ($criticalPackages->count() > 3) {
|
||||
$description .= '• ... and '.($criticalPackages->count() - 3)." more critical packages\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$description .= "\n:link: <{$this->serverUrl}|Manage Server Patches>";
|
||||
|
||||
return new SlackMessage(
|
||||
title: 'Coolify: [ACTION REQUIRED] Server patches available',
|
||||
description: $description,
|
||||
color: SlackMessage::errorColor()
|
||||
);
|
||||
}
|
||||
}
|
@@ -0,0 +1,74 @@
|
||||
<?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
|
||||
{
|
||||
// Add server patch notification fields to email notification settings
|
||||
Schema::table('email_notification_settings', function (Blueprint $table) {
|
||||
$table->boolean('server_patch_email_notifications')->default(true);
|
||||
});
|
||||
|
||||
// Add server patch notification fields to discord notification settings
|
||||
Schema::table('discord_notification_settings', function (Blueprint $table) {
|
||||
$table->boolean('server_patch_discord_notifications')->default(true);
|
||||
});
|
||||
|
||||
// Add server patch notification fields to telegram notification settings
|
||||
Schema::table('telegram_notification_settings', function (Blueprint $table) {
|
||||
$table->boolean('server_patch_telegram_notifications')->default(true);
|
||||
$table->string('telegram_notifications_server_patch_thread_id')->nullable();
|
||||
});
|
||||
|
||||
// Add server patch notification fields to slack notification settings
|
||||
Schema::table('slack_notification_settings', function (Blueprint $table) {
|
||||
$table->boolean('server_patch_slack_notifications')->default(true);
|
||||
});
|
||||
|
||||
// Add server patch notification fields to pushover notification settings
|
||||
Schema::table('pushover_notification_settings', function (Blueprint $table) {
|
||||
$table->boolean('server_patch_pushover_notifications')->default(true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
// Remove server patch notification fields from email notification settings
|
||||
Schema::table('email_notification_settings', function (Blueprint $table) {
|
||||
$table->dropColumn('server_patch_email_notifications');
|
||||
});
|
||||
|
||||
// Remove server patch notification fields from discord notification settings
|
||||
Schema::table('discord_notification_settings', function (Blueprint $table) {
|
||||
$table->dropColumn('server_patch_discord_notifications');
|
||||
});
|
||||
|
||||
// Remove server patch notification fields from telegram notification settings
|
||||
Schema::table('telegram_notification_settings', function (Blueprint $table) {
|
||||
$table->dropColumn([
|
||||
'server_patch_telegram_notifications',
|
||||
'telegram_notifications_server_patch_thread_id',
|
||||
]);
|
||||
});
|
||||
|
||||
// Remove server patch notification fields from slack notification settings
|
||||
Schema::table('slack_notification_settings', function (Blueprint $table) {
|
||||
$table->dropColumn('server_patch_slack_notifications');
|
||||
});
|
||||
|
||||
// Remove server patch notification fields from pushover notification settings
|
||||
Schema::table('pushover_notification_settings', function (Blueprint $table) {
|
||||
$table->dropColumn('server_patch_pushover_notifications');
|
||||
});
|
||||
}
|
||||
};
|
53
resources/views/emails/server-patches.blade.php
Normal file
53
resources/views/emails/server-patches.blade.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<x-emails.layout>
|
||||
{{ $total_updates }} package updates are available for your server {{ $name }}.
|
||||
|
||||
## Summary
|
||||
|
||||
- Operating System: {{ ucfirst($osId) }}
|
||||
- Package Manager: {{ $package_manager }}
|
||||
- Total Updates: {{ $total_updates }}
|
||||
|
||||
## Available Updates
|
||||
|
||||
@if ($total_updates > 0)
|
||||
@foreach ($updates as $update)
|
||||
|
||||
Package: {{ $update['package'] }} ({{ $update['architecture'] }}), from version {{ $update['current_version'] }} to {{ $update['new_version'] }} at repository {{ $update['repository'] ?? 'Unknown' }}
|
||||
@endforeach
|
||||
|
||||
## Security Considerations
|
||||
|
||||
Some of these updates may include important security patches. We recommend reviewing and applying these updates promptly.
|
||||
|
||||
### Critical packages that may require container/server/service restarts:
|
||||
@php
|
||||
$criticalPackages = collect($updates)->filter(function ($update) {
|
||||
return str_contains(strtolower($update['package']), 'docker') ||
|
||||
str_contains(strtolower($update['package']), 'kernel') ||
|
||||
str_contains(strtolower($update['package']), 'openssh') ||
|
||||
str_contains(strtolower($update['package']), 'ssl');
|
||||
});
|
||||
@endphp
|
||||
|
||||
@if ($criticalPackages->count() > 0)
|
||||
@foreach ($criticalPackages as $package)
|
||||
- {{ $package['package'] }}: {{ $package['current_version'] }} → {{ $package['new_version'] }}
|
||||
@endforeach
|
||||
@else
|
||||
No critical packages requiring container restarts detected.
|
||||
@endif
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Review the available updates
|
||||
2. Plan maintenance window if critical packages are involved
|
||||
3. Apply updates through the Coolify dashboard
|
||||
4. Monitor services after updates are applied
|
||||
@else
|
||||
Your server is up to date! No packages require updating at this time.
|
||||
@endif
|
||||
|
||||
---
|
||||
|
||||
You can manage server patches in your [Coolify Dashboard]({{ $server_url }}).
|
||||
</x-emails.layout>
|
@@ -78,6 +78,8 @@
|
||||
label="Server Reachable" />
|
||||
<x-forms.checkbox instantSave="saveModel" id="serverUnreachableDiscordNotifications"
|
||||
label="Server Unreachable" />
|
||||
<x-forms.checkbox instantSave="saveModel" id="serverPatchDiscordNotifications"
|
||||
label="Server Patching" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -155,6 +155,8 @@
|
||||
label="Server Reachable" />
|
||||
<x-forms.checkbox instantSave="saveModel" id="serverUnreachableEmailNotifications"
|
||||
label="Server Unreachable" />
|
||||
<x-forms.checkbox instantSave="saveModel" id="serverPatchEmailNotifications"
|
||||
label="Server Patching" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -80,6 +80,8 @@
|
||||
label="Server Reachable" />
|
||||
<x-forms.checkbox instantSave="saveModel" id="serverUnreachablePushoverNotifications"
|
||||
label="Server Unreachable" />
|
||||
<x-forms.checkbox instantSave="saveModel" id="serverPatchPushoverNotifications"
|
||||
label="Server Patching" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -24,8 +24,8 @@
|
||||
<x-forms.checkbox instantSave="instantSaveSlackEnabled" id="slackEnabled" label="Enabled" />
|
||||
</div>
|
||||
<x-forms.input type="password"
|
||||
helper="Create a Slack APP and generate a Incoming Webhook URL. <br><a class='inline-block underline dark:text-white' href='https://api.slack.com/apps' target='_blank'>Create Slack APP</a>" required
|
||||
id="slackWebhookUrl" label="Webhook" />
|
||||
helper="Create a Slack APP and generate a Incoming Webhook URL. <br><a class='inline-block underline dark:text-white' href='https://api.slack.com/apps' target='_blank'>Create Slack APP</a>"
|
||||
required id="slackWebhookUrl" label="Webhook" />
|
||||
</form>
|
||||
<h2 class="mt-4">Notification Settings</h2>
|
||||
<p class="mb-4">
|
||||
@@ -73,6 +73,7 @@
|
||||
label="Server Reachable" />
|
||||
<x-forms.checkbox instantSave="saveModel" id="serverUnreachableSlackNotifications"
|
||||
label="Server Unreachable" />
|
||||
<x-forms.checkbox instantSave="saveModel" id="serverPatchSlackNotifications" label="Server Patching" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -27,8 +27,9 @@
|
||||
<x-forms.input type="password" autocomplete="new-password"
|
||||
helper="Get it from the <a class='inline-block underline dark:text-white' href='https://t.me/botfather' target='_blank'>BotFather Bot</a> on Telegram."
|
||||
required id="telegramToken" label="Bot API Token" />
|
||||
<x-forms.input type="password" autocomplete="new-password" helper="Add your bot to a group chat and add its Chat ID here." required
|
||||
id="telegramChatId" label="Chat ID" />
|
||||
<x-forms.input type="password" autocomplete="new-password"
|
||||
helper="Add your bot to a group chat and add its Chat ID here." required id="telegramChatId"
|
||||
label="Chat ID" />
|
||||
</div>
|
||||
</form>
|
||||
<h2 class="mt-4">Notification Settings</h2>
|
||||
@@ -151,7 +152,6 @@
|
||||
id="telegramNotificationsServerReachableThreadId" />
|
||||
</div>
|
||||
|
||||
|
||||
<div class="pl-1 flex gap-2">
|
||||
<div class="w-96">
|
||||
<x-forms.checkbox instantSave="saveModel" id="serverUnreachableTelegramNotifications"
|
||||
@@ -160,6 +160,15 @@
|
||||
<x-forms.input type="password" placeholder="Custom Telegram Thread ID"
|
||||
id="telegramNotificationsServerUnreachableThreadId" />
|
||||
</div>
|
||||
|
||||
<div class="pl-1 flex gap-2">
|
||||
<div class="w-96">
|
||||
<x-forms.checkbox instantSave="saveModel" id="serverPatchTelegramNotifications"
|
||||
label="Server Patching" />
|
||||
</div>
|
||||
<x-forms.input type="password" placeholder="Custom Telegram Thread ID"
|
||||
id="telegramNotificationsServerPatchThreadId" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -23,7 +23,7 @@
|
||||
<x-forms.button type="button" wire:click="$dispatch('checkForUpdatesDispatch')">
|
||||
Check Now</x-forms.button>
|
||||
</div>
|
||||
<div>Update your servers automatically.</div>
|
||||
<div>Update your servers semi-automatically.</div>
|
||||
<div>
|
||||
<div class="flex flex-col gap-6 pt-4">
|
||||
<div class="flex flex-col">
|
||||
@@ -38,6 +38,7 @@
|
||||
<div class="text-green-500">Your server is up to date.</div>
|
||||
@endif
|
||||
@if (isset($updates) && count($updates) > 0)
|
||||
<div class="pb-2">
|
||||
<x-modal-confirmation title="Confirm package update?"
|
||||
buttonTitle="Update All
|
||||
Packages"
|
||||
@@ -50,6 +51,7 @@
|
||||
shortConfirmationLabel="Name" :confirmWithPassword=false
|
||||
step2ButtonText="Update All
|
||||
Packages" />
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -65,7 +67,7 @@
|
||||
@foreach ($updates as $update)
|
||||
<tr>
|
||||
<td class="inline-flex gap-2 justify-center items-center">
|
||||
@if (data_get_str($update, 'package')->contains('docker'))
|
||||
@if (data_get_str($update, 'package')->contains('docker') || data_get_str($update, 'package')->contains('kernel'))
|
||||
<x-helper :helper="'This package will restart your currently running containers'">
|
||||
<x-slot:icon>
|
||||
<svg class="w-4 h-4 text-red-500 block"
|
||||
|
Reference in New Issue
Block a user