fix: remove database restart limits and clear stale Traefik state
Drop restart-limit fields and enforcement from database resources, clear cached Traefik version data when proxies change, and show missing service environment variables from disabled deploy actions.
This commit is contained in:
parent
8dc35e2b5f
commit
053b030c42
27 changed files with 326 additions and 97 deletions
|
|
@ -28,7 +28,11 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St
|
|||
if (! $server->isFunctional()) {
|
||||
return 'Server is not functional';
|
||||
}
|
||||
$database->resetRestartLimit();
|
||||
$database->update([
|
||||
'restart_count' => 0,
|
||||
'last_restart_at' => null,
|
||||
'last_restart_type' => null,
|
||||
]);
|
||||
switch ($database->getMorphClass()) {
|
||||
case StandalonePostgresql::class:
|
||||
$activity = StartPostgresql::run($database);
|
||||
|
|
|
|||
|
|
@ -32,7 +32,11 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St
|
|||
// Reset restart tracking when database is manually stopped
|
||||
$database->update(['status' => 'exited']);
|
||||
if ($resetRestartCount) {
|
||||
$database->resetRestartLimit();
|
||||
$database->update([
|
||||
'restart_count' => 0,
|
||||
'last_restart_at' => null,
|
||||
'last_restart_type' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
if ($dockerCleanup) {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
use App\Actions\Application\StopApplication;
|
||||
use App\Actions\Application\StopApplicationPreview;
|
||||
use App\Actions\Database\StartDatabaseProxy;
|
||||
use App\Actions\Database\StopDatabase;
|
||||
use App\Actions\Database\StopDatabaseProxy;
|
||||
use App\Actions\Service\StopServiceApplication;
|
||||
use App\Actions\Shared\ComplexStatusCheck;
|
||||
|
|
@ -249,9 +248,12 @@ public function handle(Server $server, ?Collection $containers = null, ?Collecti
|
|||
|
||||
$database->update($updateData);
|
||||
|
||||
if ($database->trackRestartCount((int) $restartCount)) {
|
||||
StopDatabase::dispatch($database, false, false, false);
|
||||
$database->team()?->notify(new ApplicationRestartLimitReached($database));
|
||||
if ($restartCount > ($database->restart_count ?? 0)) {
|
||||
$database->update([
|
||||
'restart_count' => (int) $restartCount,
|
||||
'last_restart_at' => now(),
|
||||
'last_restart_type' => 'crash',
|
||||
]);
|
||||
}
|
||||
|
||||
if ($isPublic) {
|
||||
|
|
@ -357,7 +359,9 @@ public function handle(Server $server, ?Collection $containers = null, ?Collecti
|
|||
continue;
|
||||
}
|
||||
|
||||
if (! $exitedService->stoppedAfterRestartLimit()) {
|
||||
if ($exitedService instanceof ServiceDatabase) {
|
||||
$exitedService->update(['status' => 'exited']);
|
||||
} elseif (! $exitedService->stoppedAfterRestartLimit()) {
|
||||
$exitedService->update([
|
||||
'status' => 'exited',
|
||||
'restart_count' => 0,
|
||||
|
|
@ -424,9 +428,6 @@ public function handle(Server $server, ?Collection $containers = null, ?Collecti
|
|||
$notRunningDatabases = $databases->pluck('id')->diff($foundDatabases);
|
||||
foreach ($notRunningDatabases as $database) {
|
||||
$database = $databases->where('id', $database)->first();
|
||||
if ($database->stoppedAfterRestartLimit()) {
|
||||
continue;
|
||||
}
|
||||
if (str($database->status)->startsWith('exited')) {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -442,7 +443,6 @@ public function handle(Server $server, ?Collection $containers = null, ?Collecti
|
|||
'restart_count' => 0,
|
||||
'last_restart_at' => null,
|
||||
'last_restart_type' => null,
|
||||
'restart_limit_reached' => false,
|
||||
]);
|
||||
|
||||
// Stop proxy if database was public
|
||||
|
|
@ -582,7 +582,7 @@ private function aggregateServiceContainerStatuses($services)
|
|||
$restartCount = isset($this->serviceContainerRestartCounts)
|
||||
? ($this->serviceContainerRestartCounts->get($key)?->max() ?? 0)
|
||||
: 0;
|
||||
if ($subResource->trackRestartCount($restartCount)) {
|
||||
if (! $subResource instanceof ServiceDatabase && $subResource->trackRestartCount($restartCount)) {
|
||||
StopServiceApplication::dispatch($subResource, false, false);
|
||||
$subResource->team()?->notify(new ApplicationRestartLimitReached($subResource));
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ public function handle(Service $service, bool $pullLatestImages = false, bool $s
|
|||
$service->saveComposeConfigs();
|
||||
$service->isConfigurationChanged(save: true);
|
||||
$service->applications()->get()->each->resetRestartLimit();
|
||||
$service->databases()->get()->each->resetRestartLimit();
|
||||
$workdir = $service->workdir();
|
||||
// $commands[] = "cd {$workdir}";
|
||||
$commands[] = "echo 'Saved configuration files to {$workdir}.'";
|
||||
|
|
|
|||
|
|
@ -55,7 +55,6 @@ public function handle(Service $service, bool $deleteConnectedNetworks = false,
|
|||
});
|
||||
$dbs->each(function ($database): void {
|
||||
$database->update(['status' => 'exited']);
|
||||
$database->resetRestartLimit();
|
||||
});
|
||||
|
||||
if ($deleteConnectedNetworks) {
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ public function handle(ServiceApplication|ServiceDatabase $serviceApplication, b
|
|||
instant_remote_process($commands, $server, throwError: ! $removeContainer);
|
||||
|
||||
$serviceApplication->update(['status' => 'exited']);
|
||||
if ($resetRestartCount) {
|
||||
if ($resetRestartCount && $serviceApplication instanceof ServiceApplication) {
|
||||
$serviceApplication->resetRestartLimit();
|
||||
}
|
||||
ServiceStatusChanged::dispatch($service->environment->project->team->id);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Enums\ProxyStatus;
|
||||
use App\Enums\ProxyTypes;
|
||||
use App\Events\ProxyStatusChangedUI;
|
||||
use App\Models\Server;
|
||||
use App\Notifications\Server\TraefikVersionOutdated;
|
||||
|
|
@ -33,8 +35,13 @@ public function __construct(
|
|||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
$this->server->refresh();
|
||||
$this->clearOutdatedInfo();
|
||||
|
||||
if ($this->server->proxyType() !== ProxyTypes::TRAEFIK->value || $this->server->proxy->get('status') !== ProxyStatus::RUNNING->value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Detect current version (makes SSH call)
|
||||
$currentVersion = getTraefikVersionFromDockerCompose($this->server);
|
||||
|
||||
|
|
@ -116,7 +123,10 @@ public function handle(): void
|
|||
|
||||
private function clearOutdatedInfo(): void
|
||||
{
|
||||
$this->server->update(['traefik_outdated_info' => null]);
|
||||
$this->server->update([
|
||||
'detected_traefik_version' => null,
|
||||
'traefik_outdated_info' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -19,6 +19,20 @@ class CheckTraefikVersionJob implements ShouldBeEncrypted, ShouldQueue
|
|||
|
||||
public function handle(): void
|
||||
{
|
||||
Server::query()
|
||||
->where(function ($query) {
|
||||
$query->whereNull('proxy')
|
||||
->orWhere('proxy->type', '!=', ProxyTypes::TRAEFIK->value);
|
||||
})
|
||||
->where(function ($query) {
|
||||
$query->whereNotNull('detected_traefik_version')
|
||||
->orWhereNotNull('traefik_outdated_info');
|
||||
})
|
||||
->update([
|
||||
'detected_traefik_version' => null,
|
||||
'traefik_outdated_info' => null,
|
||||
]);
|
||||
|
||||
// Load versions from cached data
|
||||
$traefikVersions = get_traefik_versions();
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
use App\Actions\Application\StopApplication;
|
||||
use App\Actions\Application\StopApplicationPreview;
|
||||
use App\Actions\Database\StartDatabaseProxy;
|
||||
use App\Actions\Database\StopDatabase;
|
||||
use App\Actions\Database\StopDatabaseProxy;
|
||||
use App\Actions\Proxy\CheckProxy;
|
||||
use App\Actions\Proxy\StartProxy;
|
||||
|
|
@ -483,7 +482,7 @@ private function loadServices(): Collection
|
|||
])
|
||||
->with([
|
||||
'applications:id,service_id,status,last_online_at,restart_count,max_restart_count,restart_limit_reached,last_restart_at,last_restart_type',
|
||||
'databases:id,service_id,status,last_online_at,is_public,name,restart_count,max_restart_count,restart_limit_reached,last_restart_at,last_restart_type',
|
||||
'databases:id,service_id,status,last_online_at,is_public,name',
|
||||
])
|
||||
->get();
|
||||
}
|
||||
|
|
@ -506,8 +505,6 @@ private function loadDatabases(): Collection
|
|||
'restart_count',
|
||||
'last_restart_at',
|
||||
'last_restart_type',
|
||||
'max_restart_count',
|
||||
'restart_limit_reached',
|
||||
];
|
||||
|
||||
return collect([
|
||||
|
|
@ -675,7 +672,7 @@ private function aggregateServiceContainerStatuses()
|
|||
}
|
||||
|
||||
$restartCount = $this->serviceContainerRestartCounts->get($key)?->max() ?? 0;
|
||||
if ($subResource->trackRestartCount($restartCount)) {
|
||||
if (! $subResource instanceof ServiceDatabase && $subResource->trackRestartCount($restartCount)) {
|
||||
StopServiceApplication::dispatch($subResource, false, false);
|
||||
$subResource->team()?->notify(new ApplicationRestartLimitReached($subResource));
|
||||
|
||||
|
|
@ -821,11 +818,12 @@ private function updateDatabaseStatus(string $databaseUuid, string $containerSta
|
|||
$database->status = $containerStatus;
|
||||
$database->save();
|
||||
}
|
||||
if (is_numeric($restartCount) && $database->trackRestartCount((int) $restartCount)) {
|
||||
StopDatabase::dispatch($database, false, false, false);
|
||||
$database->team()?->notify(new ApplicationRestartLimitReached($database));
|
||||
|
||||
return;
|
||||
if (is_numeric($restartCount) && $restartCount > ($database->restart_count ?? 0)) {
|
||||
$database->update([
|
||||
'restart_count' => (int) $restartCount,
|
||||
'last_restart_at' => now(),
|
||||
'last_restart_type' => 'crash',
|
||||
]);
|
||||
}
|
||||
if (! $this->isCompleteSnapshot()) {
|
||||
return;
|
||||
|
|
@ -883,16 +881,12 @@ private function updateNotFoundDatabaseStatus()
|
|||
$notFoundDatabaseUuids->each(function ($databaseUuid) {
|
||||
$database = $this->databasesByUuid->get($databaseUuid);
|
||||
if ($database) {
|
||||
if ($database->stoppedAfterRestartLimit()) {
|
||||
return;
|
||||
}
|
||||
if (! str($database->status)->startsWith('exited')) {
|
||||
$database->update([
|
||||
'status' => 'exited',
|
||||
'restart_count' => 0,
|
||||
'last_restart_at' => null,
|
||||
'last_restart_type' => null,
|
||||
'restart_limit_reached' => false,
|
||||
]);
|
||||
}
|
||||
if ($database->is_public) {
|
||||
|
|
@ -918,9 +912,8 @@ private function updateNotFoundServiceStatus()
|
|||
// Batch update service databases
|
||||
if ($notFoundServiceDatabaseIds->isNotEmpty()) {
|
||||
ServiceDatabase::whereIn('id', $notFoundServiceDatabaseIds)
|
||||
->where('restart_limit_reached', false)
|
||||
->where('status', '!=', 'exited')
|
||||
->update(['status' => 'exited', 'restart_count' => 0, 'last_restart_at' => null, 'last_restart_type' => null]);
|
||||
->update(['status' => 'exited']);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -106,6 +106,8 @@ public function changeProxy()
|
|||
try {
|
||||
$this->authorize('update', $this->server);
|
||||
$this->server->proxy = null;
|
||||
$this->server->detected_traefik_version = null;
|
||||
$this->server->traefik_outdated_info = null;
|
||||
$this->server->save();
|
||||
|
||||
$this->dispatch('reloadWindow');
|
||||
|
|
|
|||
|
|
@ -1812,6 +1812,8 @@ public function changeProxy(string $proxyType, bool $async = true)
|
|||
$this->proxy->set('last_saved_proxy_configuration', null);
|
||||
$this->proxy->set('last_saved_settings', null);
|
||||
$this->proxy->set('last_applied_settings', null);
|
||||
$this->detected_traefik_version = null;
|
||||
$this->traefik_outdated_info = null;
|
||||
$this->save();
|
||||
if ($this->proxySet()) {
|
||||
if ($async) {
|
||||
|
|
|
|||
|
|
@ -2,13 +2,12 @@
|
|||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\HasRestartLimit;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class ServiceDatabase extends BaseModel
|
||||
{
|
||||
use HasFactory, HasRestartLimit, SoftDeletes;
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'service_id',
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
use App\Traits\ClearsGlobalSearchCache;
|
||||
use App\Traits\HasDatabaseHealthCheck;
|
||||
use App\Traits\HasMetrics;
|
||||
use App\Traits\HasRestartLimit;
|
||||
use App\Traits\HasSafeStringAttribute;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
|
@ -13,7 +12,7 @@
|
|||
|
||||
class StandaloneClickhouse extends BaseModel
|
||||
{
|
||||
use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasRestartLimit, HasSafeStringAttribute, SoftDeletes;
|
||||
use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'uuid',
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
use App\Traits\ClearsGlobalSearchCache;
|
||||
use App\Traits\HasDatabaseHealthCheck;
|
||||
use App\Traits\HasMetrics;
|
||||
use App\Traits\HasRestartLimit;
|
||||
use App\Traits\HasSafeStringAttribute;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
|
@ -13,7 +12,7 @@
|
|||
|
||||
class StandaloneDragonfly extends BaseModel
|
||||
{
|
||||
use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasRestartLimit, HasSafeStringAttribute, SoftDeletes;
|
||||
use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'uuid',
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
use App\Traits\ClearsGlobalSearchCache;
|
||||
use App\Traits\HasDatabaseHealthCheck;
|
||||
use App\Traits\HasMetrics;
|
||||
use App\Traits\HasRestartLimit;
|
||||
use App\Traits\HasSafeStringAttribute;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
|
@ -13,7 +12,7 @@
|
|||
|
||||
class StandaloneKeydb extends BaseModel
|
||||
{
|
||||
use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasRestartLimit, HasSafeStringAttribute, SoftDeletes;
|
||||
use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'uuid',
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
use App\Traits\ClearsGlobalSearchCache;
|
||||
use App\Traits\HasDatabaseHealthCheck;
|
||||
use App\Traits\HasMetrics;
|
||||
use App\Traits\HasRestartLimit;
|
||||
use App\Traits\HasSafeStringAttribute;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
|
@ -14,7 +13,7 @@
|
|||
|
||||
class StandaloneMariadb extends BaseModel
|
||||
{
|
||||
use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasRestartLimit, HasSafeStringAttribute, SoftDeletes;
|
||||
use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'uuid',
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
use App\Traits\ClearsGlobalSearchCache;
|
||||
use App\Traits\HasDatabaseHealthCheck;
|
||||
use App\Traits\HasMetrics;
|
||||
use App\Traits\HasRestartLimit;
|
||||
use App\Traits\HasSafeStringAttribute;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
|
@ -13,7 +12,7 @@
|
|||
|
||||
class StandaloneMongodb extends BaseModel
|
||||
{
|
||||
use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasRestartLimit, HasSafeStringAttribute, SoftDeletes;
|
||||
use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'uuid',
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
use App\Traits\ClearsGlobalSearchCache;
|
||||
use App\Traits\HasDatabaseHealthCheck;
|
||||
use App\Traits\HasMetrics;
|
||||
use App\Traits\HasRestartLimit;
|
||||
use App\Traits\HasSafeStringAttribute;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
|
@ -13,7 +12,7 @@
|
|||
|
||||
class StandaloneMysql extends BaseModel
|
||||
{
|
||||
use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasRestartLimit, HasSafeStringAttribute, SoftDeletes;
|
||||
use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'uuid',
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
use App\Traits\ClearsGlobalSearchCache;
|
||||
use App\Traits\HasDatabaseHealthCheck;
|
||||
use App\Traits\HasMetrics;
|
||||
use App\Traits\HasRestartLimit;
|
||||
use App\Traits\HasSafeStringAttribute;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
|
@ -13,7 +12,7 @@
|
|||
|
||||
class StandalonePostgresql extends BaseModel
|
||||
{
|
||||
use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasRestartLimit, HasSafeStringAttribute, SoftDeletes;
|
||||
use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'uuid',
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
use App\Traits\ClearsGlobalSearchCache;
|
||||
use App\Traits\HasDatabaseHealthCheck;
|
||||
use App\Traits\HasMetrics;
|
||||
use App\Traits\HasRestartLimit;
|
||||
use App\Traits\HasSafeStringAttribute;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
|
@ -13,7 +12,7 @@
|
|||
|
||||
class StandaloneRedis extends BaseModel
|
||||
{
|
||||
use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasRestartLimit, HasSafeStringAttribute, SoftDeletes;
|
||||
use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'uuid',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
private const STANDALONE_DATABASE_TABLES = [
|
||||
'standalone_postgresqls',
|
||||
'standalone_redis',
|
||||
'standalone_mongodbs',
|
||||
'standalone_mysqls',
|
||||
'standalone_mariadbs',
|
||||
'standalone_keydbs',
|
||||
'standalone_dragonflies',
|
||||
'standalone_clickhouses',
|
||||
];
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
foreach (self::STANDALONE_DATABASE_TABLES as $tableName) {
|
||||
Schema::table($tableName, function (Blueprint $table) {
|
||||
$table->dropColumn(['max_restart_count', 'restart_limit_reached']);
|
||||
});
|
||||
}
|
||||
|
||||
Schema::table('service_databases', function (Blueprint $table) {
|
||||
$table->dropColumn([
|
||||
'restart_count',
|
||||
'max_restart_count',
|
||||
'restart_limit_reached',
|
||||
'last_restart_at',
|
||||
'last_restart_type',
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
foreach (self::STANDALONE_DATABASE_TABLES as $tableName) {
|
||||
Schema::table($tableName, function (Blueprint $table) {
|
||||
$table->integer('max_restart_count')->default(10);
|
||||
$table->boolean('restart_limit_reached')->default(false);
|
||||
});
|
||||
}
|
||||
|
||||
Schema::table('service_databases', function (Blueprint $table) {
|
||||
$table->integer('restart_count')->default(0);
|
||||
$table->integer('max_restart_count')->default(10);
|
||||
$table->boolean('restart_limit_reached')->default(false);
|
||||
$table->timestamp('last_restart_at')->nullable();
|
||||
$table->string('last_restart_type', 10)->nullable();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
@props(['application'])
|
||||
|
||||
@if ($application->stoppedAfterRestartLimit())
|
||||
@if (method_exists($application, 'stoppedAfterRestartLimit') && $application->stoppedAfterRestartLimit())
|
||||
@php($restartLimit = method_exists($application, 'restartLimitMaximum') ? $application->restartLimitMaximum() : ($application->max_restart_count ?? 0))
|
||||
@php($displayRestartCount = max($application->restart_count ?? 0, $restartLimit))
|
||||
<x-status-badge
|
||||
|
|
|
|||
|
|
@ -189,15 +189,28 @@ class="listbox-panel top-full! left-0! right-0! mt-1! w-full! min-w-0!" role="me
|
|||
@endcan
|
||||
@else
|
||||
@can('deploy', $service)
|
||||
<button type="button" class="button mb-2 w-full justify-between" disabled>
|
||||
<span>Deploy</span>
|
||||
<span class="text-xs font-normal opacity-70">Fill required variables first</span>
|
||||
</button>
|
||||
<div id="service-mobile-actions" class="relative mb-3"
|
||||
x-data="{ open: false }" @click.outside="open = false"
|
||||
@keydown.escape.window="open = false">
|
||||
<button type="button" class="button w-full justify-between" @click="open = !open"
|
||||
:aria-expanded="open" aria-haspopup="menu">
|
||||
<span>Actions</span>
|
||||
<span class="inline-flex transition-transform" :class="open && 'rotate-180'">
|
||||
<x-reicon name="chevron-down" class="size-3 opacity-55" />
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div x-cloak x-show="open" x-transition.origin.top.left
|
||||
class="listbox-panel top-full! left-0! right-0! mt-1! w-full! min-w-0!" role="menu">
|
||||
<div class="listbox-option cursor-default! justify-start! gap-2.5! text-neutral-400! dark:text-fg-faint!"
|
||||
role="menuitem" aria-disabled="true">
|
||||
<x-reicon name="play-circle" class="size-3.5 opacity-70" />
|
||||
<span>Deploy (<a href="{{ $environmentVariablesUrl }}" {{ wireNavigate() }}
|
||||
class="cursor-pointer underline underline-offset-2">missing required env vars</a>)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endcan
|
||||
<a href="{{ $environmentVariablesUrl }}" {{ wireNavigate() }}
|
||||
class="mb-3 inline-flex" aria-label="Open required environment variables">
|
||||
<x-status-badge status="Required variables missing" type="error" />
|
||||
</a>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
|
|
@ -287,10 +300,26 @@ class="listbox-panel top-full! right-0! left-auto! mt-1! w-64! min-w-0!" role="m
|
|||
</div>
|
||||
@endcan
|
||||
@else
|
||||
<a href="{{ $environmentVariablesUrl }}" {{ wireNavigate() }}
|
||||
aria-label="Open required environment variables">
|
||||
<x-status-badge status="Required variables missing" type="error" />
|
||||
</a>
|
||||
@can('deploy', $service)
|
||||
<div id="service-desktop-actions" class="relative" x-data="{ open: false }"
|
||||
x-effect="$dispatch('resource-actions-toggled', { open })"
|
||||
@click.outside="open = false" @keydown.escape.window="open = false">
|
||||
<button type="button" class="button button-highlighted" @click="open = !open"
|
||||
:aria-expanded="open" aria-haspopup="menu">
|
||||
Actions
|
||||
<x-reicon name="chevron-down" class="size-3 opacity-55" />
|
||||
</button>
|
||||
<div x-cloak x-show="open" x-transition.origin.top.right
|
||||
class="listbox-panel top-full! right-0! left-auto! mt-1! w-64! min-w-0!" role="menu">
|
||||
<div class="listbox-option cursor-default! justify-start! gap-2.5! text-neutral-400! dark:text-fg-faint!"
|
||||
role="menuitem" aria-disabled="true">
|
||||
<x-reicon name="play-circle" class="size-3.5 opacity-70" />
|
||||
<span>Deploy (<a href="{{ $environmentVariablesUrl }}" {{ wireNavigate() }}
|
||||
class="cursor-pointer underline underline-offset-2">missing required env vars</a>)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endcan
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
<?php
|
||||
|
||||
use App\Actions\Docker\GetContainersStatus;
|
||||
use App\Actions\Service\StopServiceApplication;
|
||||
use App\Jobs\PushServerUpdateJob;
|
||||
use App\Models\ApplicationPreview;
|
||||
use App\Models\ServiceApplication;
|
||||
use App\Models\ServiceDatabase;
|
||||
|
|
@ -14,9 +17,10 @@
|
|||
use App\Traits\HasRestartLimit;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
it('gives every independently runnable non-application resource restart limit state', function (string $modelClass) {
|
||||
it('gives independently runnable application resources restart limit state', function (string $modelClass) {
|
||||
expect(class_uses_recursive($modelClass))->toContain(HasRestartLimit::class);
|
||||
|
||||
$resource = new $modelClass;
|
||||
|
|
@ -34,17 +38,8 @@
|
|||
'last_restart_at' => 'datetime',
|
||||
]);
|
||||
})->with([
|
||||
ApplicationPreview::class,
|
||||
ServiceApplication::class,
|
||||
ServiceDatabase::class,
|
||||
StandaloneClickhouse::class,
|
||||
StandaloneDragonfly::class,
|
||||
StandaloneKeydb::class,
|
||||
StandaloneMariadb::class,
|
||||
StandaloneMongodb::class,
|
||||
StandaloneMysql::class,
|
||||
StandalonePostgresql::class,
|
||||
StandaloneRedis::class,
|
||||
[ApplicationPreview::class],
|
||||
[ServiceApplication::class],
|
||||
]);
|
||||
|
||||
it('collects restart counts for preview and service containers from both status sources', function () {
|
||||
|
|
@ -157,7 +152,7 @@
|
|||
->toContain('Application::query()');
|
||||
});
|
||||
|
||||
it('adds restart limit columns to previews services and standalone databases', function () {
|
||||
it('limits restarts only for applications', function () {
|
||||
$migrations = collect(glob(database_path('migrations/*.php')))
|
||||
->map(fn (string $path): string => file_get_contents($path))
|
||||
->implode("\n");
|
||||
|
|
@ -165,19 +160,62 @@
|
|||
expect($migrations)
|
||||
->toContain("'application_previews'")
|
||||
->toContain("'service_applications'")
|
||||
->toContain("'service_databases'")
|
||||
->toContain("'max_restart_count'")
|
||||
->toContain("'restart_limit_reached'");
|
||||
->toContain("'restart_limit_reached'")
|
||||
->toContain("dropColumn(['max_restart_count', 'restart_limit_reached'])");
|
||||
|
||||
$restartLimitMigrations = collect(glob(database_path('migrations/*_add_restart_limit_to_*.php')));
|
||||
$databaseModels = [
|
||||
ServiceDatabase::class,
|
||||
StandalonePostgresql::class,
|
||||
StandaloneRedis::class,
|
||||
StandaloneMongodb::class,
|
||||
StandaloneMysql::class,
|
||||
StandaloneMariadb::class,
|
||||
StandaloneKeydb::class,
|
||||
StandaloneDragonfly::class,
|
||||
StandaloneClickhouse::class,
|
||||
];
|
||||
|
||||
expect($restartLimitMigrations)->toHaveCount(11);
|
||||
expect($restartLimitMigrations->map(
|
||||
fn (string $path): string => substr(basename($path), 0, 17)
|
||||
)->unique())->toHaveCount(11);
|
||||
$restartLimitMigrations->each(function (string $path): void {
|
||||
expect(file_get_contents($path))->not->toContain('foreach (');
|
||||
});
|
||||
foreach ($databaseModels as $databaseModel) {
|
||||
expect(class_uses_recursive($databaseModel))->not->toContain(HasRestartLimit::class);
|
||||
}
|
||||
|
||||
foreach ([
|
||||
'service_databases',
|
||||
'standalone_postgresqls',
|
||||
'standalone_redis',
|
||||
'standalone_mongodbs',
|
||||
'standalone_mysqls',
|
||||
'standalone_mariadbs',
|
||||
'standalone_keydbs',
|
||||
'standalone_dragonflies',
|
||||
'standalone_clickhouses',
|
||||
] as $databaseTable) {
|
||||
expect(Schema::hasColumn($databaseTable, 'max_restart_count'))->toBeFalse()
|
||||
->and(Schema::hasColumn($databaseTable, 'restart_limit_reached'))->toBeFalse();
|
||||
}
|
||||
|
||||
foreach ([GetContainersStatus::class, PushServerUpdateJob::class] as $statusUpdater) {
|
||||
$source = file_get_contents((new ReflectionClass($statusUpdater))->getFileName());
|
||||
|
||||
expect($source)
|
||||
->not->toContain('$database->trackRestartCount')
|
||||
->not->toContain('$database->stoppedAfterRestartLimit()');
|
||||
}
|
||||
|
||||
$stopServiceResource = file_get_contents((new ReflectionClass(StopServiceApplication::class))->getFileName());
|
||||
|
||||
expect($stopServiceResource)
|
||||
->toContain('$resetRestartCount && $serviceApplication instanceof ServiceApplication');
|
||||
});
|
||||
|
||||
it('does not render restart limit warnings for service databases', function () {
|
||||
$html = Blade::render(
|
||||
'<x-application.restart-limit-warning :application="$database" />',
|
||||
['database' => new ServiceDatabase],
|
||||
);
|
||||
|
||||
expect(trim($html))->toBeEmpty();
|
||||
});
|
||||
|
||||
it('atomically claims a resource restart limit once and can reset it', function () {
|
||||
|
|
|
|||
|
|
@ -82,21 +82,31 @@
|
|||
}
|
||||
});
|
||||
|
||||
it('links the service header missing variables warning to environment variables', function () {
|
||||
it('shows disabled deploy actions when service variables are missing', function () {
|
||||
$heading = file_get_contents(resource_path('views/livewire/project/service/heading.blade.php'));
|
||||
$mobileActions = str($heading)
|
||||
->after('<div class="w-full xl:hidden">')
|
||||
->before("@teleport('#resource-action-hud-slot')")
|
||||
->toString();
|
||||
$desktopActions = str($heading)
|
||||
->after("@teleport('#resource-action-hud-slot')")
|
||||
->before('@endteleport')
|
||||
->toString();
|
||||
|
||||
expect($heading)
|
||||
->toContain("route('project.service.environment-variables'")
|
||||
->toContain('Required variables missing')
|
||||
expect($mobileActions)
|
||||
->toContain('id="service-mobile-actions"')
|
||||
->toContain('aria-disabled="true"')
|
||||
->toContain('Deploy')
|
||||
->toContain('missing required env vars')
|
||||
->toContain('href="{{ $environmentVariablesUrl }}"')
|
||||
->and($mobileActions)
|
||||
->toContain('Fill required variables first')
|
||||
->toContain('disabled')
|
||||
->toContain('Deploy');
|
||||
->toContain('underline')
|
||||
->and($desktopActions)
|
||||
->toContain('id="service-desktop-actions"')
|
||||
->toContain('aria-disabled="true"')
|
||||
->toContain('Deploy')
|
||||
->toContain('missing required env vars')
|
||||
->toContain('href="{{ $environmentVariablesUrl }}"')
|
||||
->toContain('underline');
|
||||
});
|
||||
|
||||
it('places the account menu beside the desktop sidebar toggle while retaining it on mobile', function () {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\ProxyTypes;
|
||||
use App\Events\ProxyStatusChangedUI;
|
||||
use App\Jobs\CheckTraefikVersionForServerJob;
|
||||
use App\Jobs\CheckTraefikVersionJob;
|
||||
use App\Livewire\Server\Proxy;
|
||||
use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
|
|
@ -185,7 +189,7 @@
|
|||
expect($server->hasCurrentTraefikOutdatedInfo())->toBeTrue();
|
||||
});
|
||||
|
||||
it('clears stale outdated information before detecting the current version', function () {
|
||||
it('clears stale Traefik version state before detecting the current version', function () {
|
||||
$team = Team::factory()->create();
|
||||
$server = Server::factory()->create([
|
||||
'team_id' => $team->id,
|
||||
|
|
@ -204,6 +208,80 @@
|
|||
|
||||
$server->refresh();
|
||||
|
||||
expect($server->detected_traefik_version)->toBe('3.6.23')
|
||||
expect($server->detected_traefik_version)->toBeNull()
|
||||
->and($server->traefik_outdated_info)->toBeNull();
|
||||
});
|
||||
|
||||
it('clears Traefik version state when the proxy changes', function () {
|
||||
$team = Team::factory()->create();
|
||||
$server = Server::factory()->create([
|
||||
'team_id' => $team->id,
|
||||
'proxy' => [
|
||||
'type' => ProxyTypes::TRAEFIK->value,
|
||||
'status' => 'running',
|
||||
],
|
||||
'detected_traefik_version' => '3.6.23',
|
||||
'traefik_outdated_info' => [
|
||||
'current' => '3.6.23',
|
||||
'latest' => '3.7.8',
|
||||
'type' => 'minor_upgrade',
|
||||
],
|
||||
]);
|
||||
|
||||
$server->changeProxy(ProxyTypes::NONE->value);
|
||||
|
||||
expect($server->refresh()->detected_traefik_version)->toBeNull()
|
||||
->and($server->traefik_outdated_info)->toBeNull();
|
||||
});
|
||||
|
||||
it('cleans stale Traefik version state while selecting servers to check', function () {
|
||||
Bus::fake();
|
||||
Cache::put('coolify:versions:all', [
|
||||
'traefik' => ['v3.7' => '3.7.8'],
|
||||
]);
|
||||
|
||||
$team = Team::factory()->create();
|
||||
$server = Server::factory()->create([
|
||||
'team_id' => $team->id,
|
||||
'proxy' => [
|
||||
'type' => ProxyTypes::NONE->value,
|
||||
'status' => 'exited',
|
||||
],
|
||||
'detected_traefik_version' => '3.6.23',
|
||||
'traefik_outdated_info' => [
|
||||
'current' => '3.6.23',
|
||||
'latest' => '3.7.8',
|
||||
'type' => 'minor_upgrade',
|
||||
],
|
||||
]);
|
||||
|
||||
(new CheckTraefikVersionJob)->handle();
|
||||
|
||||
expect($server->refresh()->detected_traefik_version)->toBeNull()
|
||||
->and($server->traefik_outdated_info)->toBeNull();
|
||||
});
|
||||
|
||||
it('does not inspect a server after its Traefik proxy has been disabled', function () {
|
||||
$team = Team::factory()->create();
|
||||
$server = Server::factory()->create([
|
||||
'team_id' => $team->id,
|
||||
'proxy' => [
|
||||
'type' => ProxyTypes::NONE->value,
|
||||
'status' => 'exited',
|
||||
],
|
||||
'detected_traefik_version' => '3.6.23',
|
||||
'traefik_outdated_info' => [
|
||||
'current' => '3.6.23',
|
||||
'latest' => '3.7.8',
|
||||
'type' => 'minor_upgrade',
|
||||
],
|
||||
]);
|
||||
Event::fake();
|
||||
|
||||
(new CheckTraefikVersionForServerJob($server, ['v3.7' => '3.7.8']))->handle();
|
||||
|
||||
expect($server->refresh()->detected_traefik_version)->toBeNull()
|
||||
->and($server->traefik_outdated_info)->toBeNull();
|
||||
|
||||
Event::assertNotDispatched(ProxyStatusChangedUI::class);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
->toContain("\$application->update(['status' => 'exited']);")
|
||||
->toContain('$application->resetRestartLimit();')
|
||||
->toContain("\$database->update(['status' => 'exited']);")
|
||||
->toContain('$database->resetRestartLimit();');
|
||||
->not->toContain('$database->resetRestartLimit();');
|
||||
});
|
||||
|
||||
it('persists exited status when stopping an individual service resource', function () {
|
||||
|
|
|
|||
Loading…
Reference in a new issue