feat(sentinel): make sentinel mandatory on regular servers

Remove the enable/disable toggle from the server UI, logs page, and
Sentinel API so is_sentinel_enabled is derived and read-only. Enable
existing regular servers via migration, start Sentinel after validate-
and-install, and drop the daily ServerManagerJob restart.
This commit is contained in:
Andras Bacsai
2026-09-09 06:30:32 +02:00
parent d71a72a45d
commit 424dbd36ff
23 changed files with 213 additions and 292 deletions
@@ -9,6 +9,7 @@ use App\Models\Server;
use App\Models\Team;
use Illuminate\Console\Command;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
class ScheduledJobDiagnostics extends Command
@@ -203,7 +204,6 @@ class ScheduledJobDiagnostics extends Command
}
$dedupKeys = [
"sentinel-restart:{$server->id}" => '0 0 * * *',
"server-patch-check:{$server->id}" => '0 0 * * 0',
"server-check:{$server->id}" => isCloud() ? '*/5 * * * *' : '* * * * *',
"server-storage-check:{$server->id}" => data_get($server->settings, 'server_disk_usage_check_frequency', '0 23 * * *'),
@@ -235,7 +235,7 @@ class ScheduledJobDiagnostics extends Command
$this->newLine();
}
private function getServers(?string $serverFilter): \Illuminate\Support\Collection
private function getServers(?string $serverFilter): Collection
{
$query = Server::with('settings')->where('ip', '!=', '1.2.3.4');
@@ -12,7 +12,6 @@ use OpenApi\Attributes as OA;
class ServerSentinelController extends Controller
{
private const ALLOWED_FIELDS = [
'is_sentinel_enabled',
'is_metrics_enabled',
'is_sentinel_debug_enabled',
'sentinel_token',
@@ -36,7 +35,7 @@ class ServerSentinelController extends Controller
{
$settings = $server->settings;
$payload = [
'is_sentinel_enabled' => (bool) $settings->is_sentinel_enabled,
'is_sentinel_enabled' => $server->isSentinelEnabled(),
'is_metrics_enabled' => (bool) $settings->is_metrics_enabled,
'is_sentinel_debug_enabled' => (bool) $settings->is_sentinel_debug_enabled,
'sentinel_metrics_refresh_rate_seconds' => (int) $settings->sentinel_metrics_refresh_rate_seconds,
@@ -69,7 +68,7 @@ class ServerSentinelController extends Controller
description: 'Sentinel settings.',
content: new OA\JsonContent(
properties: [
new OA\Property(property: 'is_sentinel_enabled', type: 'boolean'),
new OA\Property(property: 'is_sentinel_enabled', type: 'boolean', readOnly: true, description: 'Sentinel is mandatory on regular managed servers.'),
new OA\Property(property: 'is_metrics_enabled', type: 'boolean'),
new OA\Property(property: 'is_sentinel_debug_enabled', type: 'boolean'),
new OA\Property(property: 'sentinel_token', type: 'string', description: 'Only present with read:sensitive.'),
@@ -118,7 +117,6 @@ class ServerSentinelController extends Controller
required: true,
content: new OA\JsonContent(
properties: [
new OA\Property(property: 'is_sentinel_enabled', type: 'boolean'),
new OA\Property(property: 'is_metrics_enabled', type: 'boolean'),
new OA\Property(property: 'is_sentinel_debug_enabled', type: 'boolean'),
new OA\Property(property: 'sentinel_token', type: 'string'),
@@ -158,7 +156,6 @@ class ServerSentinelController extends Controller
$this->authorize('update', $server);
$validator = customApiValidator($request->all(), [
'is_sentinel_enabled' => 'boolean',
'is_metrics_enabled' => 'boolean',
'is_sentinel_debug_enabled' => 'boolean',
'sentinel_token' => ['string', 'max:500', 'regex:/\A[a-zA-Z0-9._\-+=\/]+\z/'],
@@ -189,29 +186,12 @@ class ServerSentinelController extends Controller
}
$settings = $server->settings;
$enablingSentinel = $request->has('is_sentinel_enabled')
&& $request->boolean('is_sentinel_enabled')
&& ! $settings->is_sentinel_enabled;
if ($enablingSentinel && $server->isBuildServer()) {
return response()->json([
'message' => 'Validation failed.',
'errors' => ['is_sentinel_enabled' => ['Sentinel cannot be enabled on build servers.']],
], 422);
}
foreach (self::ALLOWED_FIELDS as $field) {
if ($request->has($field)) {
$settings->{$field} = $request->input($field);
}
}
// Disabling Sentinel also clears related toggles (matches Livewire toggleSentinel).
if ($request->has('is_sentinel_enabled') && ! $request->boolean('is_sentinel_enabled')) {
$settings->is_metrics_enabled = false;
$settings->is_sentinel_debug_enabled = false;
}
$settings->save();
auditLog('api.server.sentinel.updated', [
-9
View File
@@ -166,14 +166,6 @@ class ServerManagerJob implements ShouldBeEncrypted, ShouldQueue
}
}
$isSentinelEnabled = $server->isSentinelEnabled();
$shouldRestartSentinel = $isSentinelEnabled && shouldRunCronNow('0 0 * * *', $serverTimezone, "sentinel-restart:{$server->id}", $this->executionTime);
// Dispatch Sentinel restart if due (daily for Sentinel-enabled servers)
if ($shouldRestartSentinel) {
CheckAndStartSentinelJob::dispatch($server);
}
// Dispatch ServerStorageCheckJob if due (only when Sentinel is out of sync or disabled)
// When Sentinel is active, PushServerUpdateJob handles storage checks with real-time data
if ($sentinelOutOfSync) {
@@ -195,7 +187,6 @@ class ServerManagerJob implements ShouldBeEncrypted, ShouldQueue
ServerPatchCheckJob::dispatch($server);
}
// Note: CheckAndStartSentinelJob is only dispatched daily (line above) for version updates.
// Crash recovery is handled by sentinelOutOfSync → ServerCheckJob → CheckAndStartSentinelJob.
}
+3
View File
@@ -202,6 +202,9 @@ class ValidateAndInstallServerJob implements ShouldBeEncrypted, ShouldQueue
// Broadcast events to update UI
ServerValidated::dispatch($this->server->team_id, $this->server->uuid);
ServerReachabilityChanged::dispatch($this->server);
if ($this->server->isSentinelEnabled()) {
CheckAndStartSentinelJob::dispatch($this->server);
}
} catch (\Throwable $e) {
Log::error('ValidateAndInstallServer: Exception occurred', [
-33
View File
@@ -2,8 +2,6 @@
namespace App\Livewire\Server;
use App\Actions\Server\StartSentinel;
use App\Actions\Server\StopSentinel;
use App\Models\Server;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Attributes\Validate;
@@ -34,8 +32,6 @@ class Sentinel extends Component
#[Validate(['nullable', 'url'])]
public ?string $sentinelCustomUrl = null;
public bool $isSentinelEnabled;
public bool $isSentinelDebugEnabled;
public ?string $sentinelCustomDockerImage = null;
@@ -64,7 +60,6 @@ class Sentinel extends Component
$this->server->settings->sentinel_metrics_history_days = $this->sentinelMetricsHistoryDays;
$this->server->settings->sentinel_push_interval_seconds = $this->sentinelPushIntervalSeconds;
$this->server->settings->sentinel_custom_url = $this->sentinelCustomUrl;
$this->server->settings->is_sentinel_enabled = $this->isSentinelEnabled;
$this->server->settings->is_sentinel_debug_enabled = $this->isSentinelDebugEnabled;
$this->server->settings->save();
} else {
@@ -74,7 +69,6 @@ class Sentinel extends Component
$this->sentinelMetricsHistoryDays = $this->server->settings->sentinel_metrics_history_days;
$this->sentinelPushIntervalSeconds = $this->server->settings->sentinel_push_interval_seconds;
$this->sentinelCustomUrl = $this->server->settings->sentinel_custom_url;
$this->isSentinelEnabled = $this->server->settings->is_sentinel_enabled;
$this->isSentinelDebugEnabled = $this->server->settings->is_sentinel_debug_enabled;
$this->sentinelUpdatedAt = $this->server->sentinel_updated_at;
}
@@ -103,33 +97,6 @@ class Sentinel extends Component
}
}
public function toggleSentinel(): void
{
try {
$this->authorize('manageSentinel', $this->server);
if (! $this->isSentinelEnabled) {
if ($this->server->isBuildServer()) {
$this->dispatch('error', 'Sentinel cannot be enabled on build servers.');
return;
}
$customImage = isDev() ? $this->sentinelCustomDockerImage : null;
StartSentinel::run($this->server, true, null, $customImage);
$this->sentinelCustomUrl = $this->server->settings->sentinel_custom_url;
$this->isSentinelEnabled = true;
} else {
$this->isSentinelEnabled = false;
$this->isMetricsEnabled = false;
$this->isSentinelDebugEnabled = false;
StopSentinel::dispatch($this->server);
}
$this->submit();
$this->dispatch('refreshServerShow');
} catch (\Throwable $e) {
handleError($e, $this);
}
}
public function regenerateSentinelToken()
{
try {
-30
View File
@@ -2,7 +2,6 @@
namespace App\Livewire\Server\Sentinel;
use App\Actions\Server\StartSentinel;
use App\Models\Server;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\View\View;
@@ -30,35 +29,6 @@ class Logs extends Component
$this->authorize('viewSentinel', $this->server);
}
public function enableSentinel(): void
{
$this->authorize('manageSentinel', $this->server);
try {
$this->server->refresh();
if ($this->server->isBuildServer()) {
$this->dispatch('error', 'Sentinel cannot be enabled on build servers.');
return;
}
if ($this->server->isSwarm()) {
$this->dispatch('error', 'Sentinel cannot be enabled on Swarm servers.');
return;
}
if ($this->server->isSentinelEnabled()) {
return;
}
StartSentinel::run($this->server, true);
$this->server->refresh();
$this->dispatch('refreshServerShow');
$this->dispatch('success', 'Sentinel has been enabled.');
} catch (\Throwable $e) {
handleError($e, $this);
}
}
public function render(): View
{
return view('livewire.server.sentinel.logs');
+2 -32
View File
@@ -2,7 +2,6 @@
namespace App\Livewire\Server;
use App\Actions\Server\StartSentinel;
use App\Actions\Server\StopSentinel;
use App\Events\ServerReachabilityChanged;
use App\Models\CloudProviderToken;
@@ -67,8 +66,6 @@ class Show extends Component
public ?string $sentinelCustomUrl = null;
public bool $isSentinelEnabled;
public bool $isSentinelDebugEnabled;
public ?string $sentinelCustomDockerImage = null;
@@ -161,7 +158,6 @@ class Show extends Component
'sentinelMetricsHistoryDays' => 'required|integer|min:1',
'sentinelPushIntervalSeconds' => 'required|integer|min:10',
'sentinelCustomUrl' => 'nullable|url',
'isSentinelEnabled' => 'required',
'isSentinelDebugEnabled' => 'required',
'serverTimezone' => 'required',
];
@@ -265,7 +261,6 @@ class Show extends Component
$this->server->settings->sentinel_metrics_history_days = $this->sentinelMetricsHistoryDays;
$this->server->settings->sentinel_push_interval_seconds = $this->sentinelPushIntervalSeconds;
$this->server->settings->sentinel_custom_url = $this->sentinelCustomUrl;
$this->server->settings->is_sentinel_enabled = $this->isSentinelEnabled;
$this->server->settings->is_sentinel_debug_enabled = $this->isSentinelDebugEnabled;
if (! validate_timezone($this->serverTimezone)) {
@@ -296,7 +291,6 @@ class Show extends Component
$this->sentinelMetricsHistoryDays = $this->server->settings->sentinel_metrics_history_days;
$this->sentinelPushIntervalSeconds = $this->server->settings->sentinel_push_interval_seconds;
$this->sentinelCustomUrl = $this->server->settings->sentinel_custom_url;
$this->isSentinelEnabled = $this->server->settings->is_sentinel_enabled;
$this->isSentinelDebugEnabled = $this->server->settings->is_sentinel_debug_enabled;
$this->sentinelUpdatedAt = $this->server->sentinel_updated_at;
$this->serverTimezone = $this->server->settings->server_timezone;
@@ -425,10 +419,10 @@ class Show extends Component
return;
}
if ($value === true && $this->isSentinelEnabled) {
$this->isSentinelEnabled = false;
if ($value === true && $this->server->isSentinelEnabled()) {
$this->isMetricsEnabled = false;
$this->isSentinelDebugEnabled = false;
$this->server->settings->is_sentinel_enabled = false;
StopSentinel::dispatch($this->server);
$this->dispatch('info', 'Sentinel has been disabled as build servers cannot run Sentinel.');
}
@@ -440,30 +434,6 @@ class Show extends Component
}
}
public function updatedIsSentinelEnabled($value)
{
try {
$this->authorize('manageSentinel', $this->server);
if ($value === true) {
if ($this->isBuildServer) {
$this->isSentinelEnabled = false;
$this->dispatch('error', 'Sentinel cannot be enabled on build servers.');
return;
}
$customImage = isDev() ? $this->sentinelCustomDockerImage : null;
StartSentinel::run($this->server, true, null, $customImage);
} else {
$this->isMetricsEnabled = false;
$this->isSentinelDebugEnabled = false;
StopSentinel::dispatch($this->server);
}
$this->submit();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function regenerateSentinelToken()
{
try {
@@ -5,6 +5,7 @@ namespace App\Livewire\Server;
use App\Actions\Proxy\CheckProxy;
use App\Actions\Proxy\StartProxy;
use App\Events\ServerValidated;
use App\Jobs\CheckAndStartSentinelJob;
use App\Models\Server;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
@@ -275,6 +276,9 @@ class ValidateAndInstall extends Component
$this->dispatch('refreshServerShow');
$this->dispatch('refreshBoardingIndex');
ServerValidated::dispatch($this->server->team_id, $this->server->uuid);
if ($this->server->isSentinelEnabled()) {
CheckAndStartSentinelJob::dispatch($this->server);
}
$this->dispatch('success', 'Server validated, proxy is starting in a moment.');
$proxyShouldRun = CheckProxy::run($this->server, true);
if (! $proxyShouldRun) {
+7 -4
View File
@@ -972,17 +972,20 @@ $siteAddress {
return Carbon::parse($this->sentinel_updated_at)->isAfter(now()->subSeconds($this->waitBeforeDoingSshCheck()));
}
public function isSentinelEnabled()
public function isSentinelEnabled(): bool
{
return ($this->isMetricsEnabled() || $this->isServerApiEnabled()) && ! $this->isBuildServer();
return ! $this->isBuildServer()
&& ! $this->isSwarm()
&& ! $this->isForceDisabled()
&& ! $this->isTransferredAway();
}
public function isMetricsEnabled()
public function isMetricsEnabled(): bool
{
return $this->settings->is_metrics_enabled;
}
public function isServerApiEnabled()
public function isServerApiEnabled(): bool
{
return $this->settings->is_sentinel_enabled;
}
@@ -54,7 +54,7 @@ class ServerTransferClaimer
if ($rebindSentinel && $server->settings) {
$server->settings->sentinel_custom_url = $instanceUrl;
$server->settings->ensureValidSentinelToken();
// Leave sentinel disabled until operator enables metrics; endpoint is ready.
$server->settings->is_sentinel_enabled = true;
$server->settings->save();
$sentinelRebound = true;
}