fix(applications): persist restart limit state for stopped containers (#11566)
This commit is contained in:
parent
e514a2d61f
commit
67b33c27a9
97 changed files with 2181 additions and 397 deletions
|
|
@ -13,8 +13,9 @@ class StopApplication
|
||||||
|
|
||||||
public string $jobQueue = 'high';
|
public string $jobQueue = 'high';
|
||||||
|
|
||||||
public function handle(Application $application, bool $previewDeployments = false, bool $dockerCleanup = true, bool $resetRestartCount = true)
|
public function handle(Application $application, bool $previewDeployments = false, bool $dockerCleanup = true, bool $resetRestartCount = true, bool $removeContainers = true): ?string
|
||||||
{
|
{
|
||||||
|
$containerPresent = ! $removeContainers;
|
||||||
$servers = collect([$application->destination->server]);
|
$servers = collect([$application->destination->server]);
|
||||||
if ($application?->additional_servers?->count() > 0) {
|
if ($application?->additional_servers?->count() > 0) {
|
||||||
$servers = $servers->merge($application->additional_servers);
|
$servers = $servers->merge($application->additional_servers);
|
||||||
|
|
@ -26,6 +27,7 @@ public function handle(Application $application, bool $previewDeployments = fals
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($server->isSwarm()) {
|
if ($server->isSwarm()) {
|
||||||
|
$containerPresent = false;
|
||||||
instant_remote_process(["docker stack rm {$application->uuid}"], $server);
|
instant_remote_process(["docker stack rm {$application->uuid}"], $server);
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -39,13 +41,17 @@ public function handle(Application $application, bool $previewDeployments = fals
|
||||||
$timeout = $application->settings->stopGracePeriodSeconds();
|
$timeout = $application->settings->stopGracePeriodSeconds();
|
||||||
|
|
||||||
foreach ($containersToStop as $containerName) {
|
foreach ($containersToStop as $containerName) {
|
||||||
instant_remote_process(command: [
|
$commands = [dockerStopCommand($timeout, $containerName, $server)];
|
||||||
dockerStopCommand($timeout, $containerName, $server),
|
if ($removeContainers) {
|
||||||
"docker rm -f $containerName",
|
$commands[] = "docker rm -f $containerName";
|
||||||
], server: $server, throwError: false);
|
} else {
|
||||||
|
array_unshift($commands, "docker update --restart=no $containerName");
|
||||||
|
}
|
||||||
|
|
||||||
|
instant_remote_process(command: $commands, server: $server, throwError: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($application->build_pack === 'dockercompose') {
|
if ($removeContainers && $application->build_pack === 'dockercompose') {
|
||||||
$application->deleteConnectedNetworks();
|
$application->deleteConnectedNetworks();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,12 +63,16 @@ public function handle(Application $application, bool $previewDeployments = fals
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$status = ['status' => 'exited'];
|
$status = [
|
||||||
|
'status' => 'exited',
|
||||||
|
'container_present' => $containerPresent,
|
||||||
|
];
|
||||||
if ($resetRestartCount) {
|
if ($resetRestartCount) {
|
||||||
$status = array_merge($status, [
|
$status = array_merge($status, [
|
||||||
'restart_count' => 0,
|
'restart_count' => 0,
|
||||||
'last_restart_at' => null,
|
'last_restart_at' => null,
|
||||||
'last_restart_type' => null,
|
'last_restart_type' => null,
|
||||||
|
'restart_limit_reached' => false,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
$application->update($status);
|
$application->update($status);
|
||||||
|
|
|
||||||
36
app/Actions/Application/StopApplicationPreview.php
Normal file
36
app/Actions/Application/StopApplicationPreview.php
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Actions\Application;
|
||||||
|
|
||||||
|
use App\Events\ServiceStatusChanged;
|
||||||
|
use App\Models\ApplicationPreview;
|
||||||
|
use Lorisleiva\Actions\Concerns\AsAction;
|
||||||
|
|
||||||
|
class StopApplicationPreview
|
||||||
|
{
|
||||||
|
use AsAction;
|
||||||
|
|
||||||
|
public function handle(ApplicationPreview $preview, bool $resetRestartCount = true, bool $removeContainer = true): void
|
||||||
|
{
|
||||||
|
$application = $preview->application;
|
||||||
|
$server = $application->destination->server;
|
||||||
|
$containers = getCurrentApplicationContainerStatus($server, $application->id, $preview->pull_request_id);
|
||||||
|
|
||||||
|
foreach ($containers->pluck('Names') as $containerName) {
|
||||||
|
$commands = [dockerStopCommand($application->settings->stopGracePeriodSeconds(), $containerName, $server)];
|
||||||
|
if ($removeContainer) {
|
||||||
|
$commands[] = "docker rm -f $containerName";
|
||||||
|
} else {
|
||||||
|
array_unshift($commands, "docker update --restart=no $containerName");
|
||||||
|
}
|
||||||
|
instant_remote_process($commands, $server, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
$preview->update(['status' => 'exited']);
|
||||||
|
if ($resetRestartCount) {
|
||||||
|
$preview->resetRestartLimit();
|
||||||
|
}
|
||||||
|
|
||||||
|
ServiceStatusChanged::dispatch($application->environment->project->team->id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -28,6 +28,7 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St
|
||||||
if (! $server->isFunctional()) {
|
if (! $server->isFunctional()) {
|
||||||
return 'Server is not functional';
|
return 'Server is not functional';
|
||||||
}
|
}
|
||||||
|
$database->resetRestartLimit();
|
||||||
switch ($database->getMorphClass()) {
|
switch ($database->getMorphClass()) {
|
||||||
case StandalonePostgresql::class:
|
case StandalonePostgresql::class:
|
||||||
$activity = StartPostgresql::run($database);
|
$activity = StartPostgresql::run($database);
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
use App\Actions\Server\CleanupDocker;
|
use App\Actions\Server\CleanupDocker;
|
||||||
use App\Events\ServiceStatusChanged;
|
use App\Events\ServiceStatusChanged;
|
||||||
|
use App\Models\BaseModel;
|
||||||
use App\Models\StandaloneClickhouse;
|
use App\Models\StandaloneClickhouse;
|
||||||
use App\Models\StandaloneDragonfly;
|
use App\Models\StandaloneDragonfly;
|
||||||
use App\Models\StandaloneKeydb;
|
use App\Models\StandaloneKeydb;
|
||||||
|
|
@ -18,7 +19,7 @@ class StopDatabase
|
||||||
{
|
{
|
||||||
use AsAction;
|
use AsAction;
|
||||||
|
|
||||||
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database, bool $dockerCleanup = true)
|
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database, bool $dockerCleanup = true, bool $resetRestartCount = true, bool $removeContainer = true): string
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$server = $database->destination->server;
|
$server = $database->destination->server;
|
||||||
|
|
@ -26,15 +27,13 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St
|
||||||
return 'Server is not functional';
|
return 'Server is not functional';
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->stopContainer($database, $database->uuid, 30);
|
$this->stopContainer($database, $database->uuid, 30, $removeContainer);
|
||||||
|
|
||||||
// Reset restart tracking when database is manually stopped
|
// Reset restart tracking when database is manually stopped
|
||||||
$database->update([
|
$database->update(['status' => 'exited']);
|
||||||
'status' => 'exited',
|
if ($resetRestartCount) {
|
||||||
'restart_count' => 0,
|
$database->resetRestartLimit();
|
||||||
'last_restart_at' => null,
|
}
|
||||||
'last_restart_type' => null,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if ($dockerCleanup) {
|
if ($dockerCleanup) {
|
||||||
CleanupDocker::dispatch($server, false, false);
|
CleanupDocker::dispatch($server, false, false);
|
||||||
|
|
@ -53,12 +52,15 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function stopContainer($database, string $containerName, int $timeout = 30): void
|
private function stopContainer(BaseModel $database, string $containerName, int $timeout = 30, bool $removeContainer = true): void
|
||||||
{
|
{
|
||||||
$server = $database->destination->server;
|
$server = $database->destination->server;
|
||||||
instant_remote_process(command: [
|
$commands = [dockerStopCommand($timeout, $containerName, $server)];
|
||||||
dockerStopCommand($timeout, $containerName, $server),
|
if ($removeContainer) {
|
||||||
"docker rm -f $containerName",
|
$commands[] = "docker rm -f $containerName";
|
||||||
], server: $server, throwError: false);
|
} else {
|
||||||
|
array_unshift($commands, "docker update --restart=no $containerName");
|
||||||
|
}
|
||||||
|
instant_remote_process(command: $commands, server: $server, throwError: false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,20 @@
|
||||||
namespace App\Actions\Docker;
|
namespace App\Actions\Docker;
|
||||||
|
|
||||||
use App\Actions\Application\StopApplication;
|
use App\Actions\Application\StopApplication;
|
||||||
|
use App\Actions\Application\StopApplicationPreview;
|
||||||
use App\Actions\Database\StartDatabaseProxy;
|
use App\Actions\Database\StartDatabaseProxy;
|
||||||
|
use App\Actions\Database\StopDatabase;
|
||||||
use App\Actions\Database\StopDatabaseProxy;
|
use App\Actions\Database\StopDatabaseProxy;
|
||||||
|
use App\Actions\Service\StopServiceApplication;
|
||||||
use App\Actions\Shared\ComplexStatusCheck;
|
use App\Actions\Shared\ComplexStatusCheck;
|
||||||
use App\Events\ServiceChecked;
|
use App\Events\ServiceChecked;
|
||||||
|
use App\Models\Application;
|
||||||
use App\Models\ApplicationPreview;
|
use App\Models\ApplicationPreview;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
use App\Models\ServiceDatabase;
|
use App\Models\ServiceDatabase;
|
||||||
use App\Notifications\Application\RestartLimitReached as ApplicationRestartLimitReached;
|
use App\Notifications\Application\RestartLimitReached as ApplicationRestartLimitReached;
|
||||||
use App\Services\ContainerStatusAggregator;
|
use App\Services\ContainerStatusAggregator;
|
||||||
|
use App\Services\RestartCountTracker;
|
||||||
use App\Traits\CalculatesExcludedStatus;
|
use App\Traits\CalculatesExcludedStatus;
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
|
@ -37,8 +42,12 @@ class GetContainersStatus
|
||||||
|
|
||||||
protected ?Collection $applicationContainerRestartCounts;
|
protected ?Collection $applicationContainerRestartCounts;
|
||||||
|
|
||||||
|
protected ?Collection $previewContainerRestartCounts;
|
||||||
|
|
||||||
protected ?Collection $serviceContainerStatuses;
|
protected ?Collection $serviceContainerStatuses;
|
||||||
|
|
||||||
|
protected ?Collection $serviceContainerRestartCounts;
|
||||||
|
|
||||||
public function handle(Server $server, ?Collection $containers = null, ?Collection $containerReplicates = null)
|
public function handle(Server $server, ?Collection $containers = null, ?Collection $containerReplicates = null)
|
||||||
{
|
{
|
||||||
$this->containers = $containers;
|
$this->containers = $containers;
|
||||||
|
|
@ -117,6 +126,9 @@ public function handle(Server $server, ?Collection $containers = null, ?Collecti
|
||||||
$containerStatus = "$containerStatus:$healthSuffix";
|
$containerStatus = "$containerStatus:$healthSuffix";
|
||||||
}
|
}
|
||||||
$labels = Arr::undot(format_docker_labels_to_json($labels));
|
$labels = Arr::undot(format_docker_labels_to_json($labels));
|
||||||
|
if (filter_var(data_get($labels, 'com.docker.compose.oneoff'), FILTER_VALIDATE_BOOLEAN)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
$applicationId = data_get($labels, 'coolify.applicationId');
|
$applicationId = data_get($labels, 'coolify.applicationId');
|
||||||
if ($applicationId) {
|
if ($applicationId) {
|
||||||
$pullRequestId = data_get($labels, 'coolify.pullRequestId');
|
$pullRequestId = data_get($labels, 'coolify.pullRequestId');
|
||||||
|
|
@ -133,6 +145,12 @@ public function handle(Server $server, ?Collection $containers = null, ?Collecti
|
||||||
} else {
|
} else {
|
||||||
$preview->update(['last_online_at' => now()]);
|
$preview->update(['last_online_at' => now()]);
|
||||||
}
|
}
|
||||||
|
$key = $applicationId.':'.$pullRequestId;
|
||||||
|
$this->previewContainerRestartCounts ??= collect();
|
||||||
|
$this->previewContainerRestartCounts->push([
|
||||||
|
'key' => $key,
|
||||||
|
'count' => (int) data_get($container, 'RestartCount', 0),
|
||||||
|
]);
|
||||||
} else {
|
} else {
|
||||||
// Notify user that this container should not be there.
|
// Notify user that this container should not be there.
|
||||||
}
|
}
|
||||||
|
|
@ -140,6 +158,9 @@ public function handle(Server $server, ?Collection $containers = null, ?Collecti
|
||||||
$application = $this->applications->where('id', $applicationId)->first();
|
$application = $this->applications->where('id', $applicationId)->first();
|
||||||
if ($application) {
|
if ($application) {
|
||||||
$foundApplications[] = $application->id;
|
$foundApplications[] = $application->id;
|
||||||
|
if ($application->container_present !== true) {
|
||||||
|
$application->update(['container_present' => true]);
|
||||||
|
}
|
||||||
// Store container status for aggregation
|
// Store container status for aggregation
|
||||||
if (! isset($this->applicationContainerStatuses)) {
|
if (! isset($this->applicationContainerStatuses)) {
|
||||||
$this->applicationContainerStatuses = collect();
|
$this->applicationContainerStatuses = collect();
|
||||||
|
|
@ -220,23 +241,19 @@ public function handle(Server $server, ?Collection $containers = null, ?Collecti
|
||||||
|
|
||||||
// Track restart count for databases (single-container)
|
// Track restart count for databases (single-container)
|
||||||
$restartCount = data_get($container, 'RestartCount', 0);
|
$restartCount = data_get($container, 'RestartCount', 0);
|
||||||
$previousRestartCount = $database->restart_count ?? 0;
|
|
||||||
|
|
||||||
if ($statusFromDb !== $containerStatus) {
|
if ($statusFromDb !== $containerStatus) {
|
||||||
$updateData = ['status' => $containerStatus];
|
$updateData = ['status' => $containerStatus];
|
||||||
} else {
|
} else {
|
||||||
$updateData = ['last_online_at' => now()];
|
$updateData = ['last_online_at' => now()];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update restart tracking if restart count increased
|
|
||||||
if ($restartCount > $previousRestartCount) {
|
|
||||||
$updateData['restart_count'] = $restartCount;
|
|
||||||
$updateData['last_restart_at'] = now();
|
|
||||||
$updateData['last_restart_type'] = 'crash';
|
|
||||||
}
|
|
||||||
|
|
||||||
$database->update($updateData);
|
$database->update($updateData);
|
||||||
|
|
||||||
|
if ($database->trackRestartCount((int) $restartCount)) {
|
||||||
|
StopDatabase::dispatch($database, false, false, false);
|
||||||
|
$database->team()?->notify(new ApplicationRestartLimitReached($database));
|
||||||
|
}
|
||||||
|
|
||||||
if ($isPublic) {
|
if ($isPublic) {
|
||||||
$foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) {
|
$foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) {
|
||||||
if ($this->server->isSwarm()) {
|
if ($this->server->isSwarm()) {
|
||||||
|
|
@ -292,6 +309,11 @@ public function handle(Server $server, ?Collection $containers = null, ?Collecti
|
||||||
$containerName = data_get($labels, 'com.docker.compose.service');
|
$containerName = data_get($labels, 'com.docker.compose.service');
|
||||||
if ($containerName) {
|
if ($containerName) {
|
||||||
$this->serviceContainerStatuses->get($key)->put($containerName, $containerStatus);
|
$this->serviceContainerStatuses->get($key)->put($containerName, $containerStatus);
|
||||||
|
$this->serviceContainerRestartCounts ??= collect();
|
||||||
|
if (! $this->serviceContainerRestartCounts->has($key)) {
|
||||||
|
$this->serviceContainerRestartCounts->put($key, collect());
|
||||||
|
}
|
||||||
|
$this->serviceContainerRestartCounts->get($key)->put($containerName, (int) data_get($container, 'RestartCount', 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark service as found
|
// Mark service as found
|
||||||
|
|
@ -335,46 +357,35 @@ public function handle(Server $server, ?Collection $containers = null, ?Collecti
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$name = data_get($exitedService, 'name');
|
if (! $exitedService->stoppedAfterRestartLimit()) {
|
||||||
$fqdn = data_get($exitedService, 'fqdn');
|
$exitedService->update([
|
||||||
if ($name) {
|
'status' => 'exited',
|
||||||
if ($fqdn) {
|
'restart_count' => 0,
|
||||||
$containerName = "$name, available at $fqdn";
|
'restart_limit_reached' => false,
|
||||||
} else {
|
'last_restart_at' => null,
|
||||||
$containerName = $name;
|
'last_restart_type' => null,
|
||||||
}
|
]);
|
||||||
} else {
|
|
||||||
if ($fqdn) {
|
|
||||||
$containerName = $fqdn;
|
|
||||||
} else {
|
|
||||||
$containerName = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
$projectUuid = data_get($service, 'environment.project.uuid');
|
|
||||||
$serviceUuid = data_get($service, 'uuid');
|
|
||||||
$environmentName = data_get($service, 'environment.name');
|
|
||||||
|
|
||||||
if ($projectUuid && $serviceUuid && $environmentName) {
|
|
||||||
$url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/service/'.$serviceUuid;
|
|
||||||
} else {
|
|
||||||
$url = null;
|
|
||||||
}
|
|
||||||
// $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
|
|
||||||
$exitedService->update(['status' => 'exited']);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$notRunningApplications = $this->applications->pluck('id')->diff($foundApplications);
|
$notRunningApplications = $this->applications->pluck('id')->diff($foundApplications);
|
||||||
foreach ($notRunningApplications as $applicationId) {
|
foreach ($notRunningApplications as $applicationId) {
|
||||||
$application = $this->applications->where('id', $applicationId)->first();
|
$application = $this->applications->where('id', $applicationId)->first();
|
||||||
if (str($application->status)->startsWith('exited')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only protection: If no containers at all, Docker query might have failed
|
// Only protection: If no containers at all, Docker query might have failed
|
||||||
if ($this->containers->isEmpty()) {
|
if ($this->containers->isEmpty()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (str($application->status)->startsWith('exited')) {
|
||||||
|
$application->update([
|
||||||
|
'container_present' => false,
|
||||||
|
'restart_limit_reached' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// If container was recently restarting (crash loop), keep it as degraded for a grace period
|
// If container was recently restarting (crash loop), keep it as degraded for a grace period
|
||||||
// This prevents false "exited" status during the brief moment between container removal and recreation
|
// This prevents false "exited" status during the brief moment between container removal and recreation
|
||||||
$recentlyRestarted = $application->restart_count > 0 &&
|
$recentlyRestarted = $application->restart_count > 0 &&
|
||||||
|
|
@ -388,9 +399,11 @@ public function handle(Server $server, ?Collection $containers = null, ?Collecti
|
||||||
// Reset restart count when application exits completely
|
// Reset restart count when application exits completely
|
||||||
$application->update([
|
$application->update([
|
||||||
'status' => 'exited',
|
'status' => 'exited',
|
||||||
|
'container_present' => false,
|
||||||
'restart_count' => 0,
|
'restart_count' => 0,
|
||||||
'last_restart_at' => null,
|
'last_restart_at' => null,
|
||||||
'last_restart_type' => null,
|
'last_restart_type' => null,
|
||||||
|
'restart_limit_reached' => false,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -411,6 +424,9 @@ public function handle(Server $server, ?Collection $containers = null, ?Collecti
|
||||||
$notRunningDatabases = $databases->pluck('id')->diff($foundDatabases);
|
$notRunningDatabases = $databases->pluck('id')->diff($foundDatabases);
|
||||||
foreach ($notRunningDatabases as $database) {
|
foreach ($notRunningDatabases as $database) {
|
||||||
$database = $databases->where('id', $database)->first();
|
$database = $databases->where('id', $database)->first();
|
||||||
|
if ($database->stoppedAfterRestartLimit()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (str($database->status)->startsWith('exited')) {
|
if (str($database->status)->startsWith('exited')) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -426,6 +442,7 @@ public function handle(Server $server, ?Collection $containers = null, ?Collecti
|
||||||
'restart_count' => 0,
|
'restart_count' => 0,
|
||||||
'last_restart_at' => null,
|
'last_restart_at' => null,
|
||||||
'last_restart_type' => null,
|
'last_restart_type' => null,
|
||||||
|
'restart_limit_reached' => false,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Stop proxy if database was public
|
// Stop proxy if database was public
|
||||||
|
|
@ -433,23 +450,10 @@ public function handle(Server $server, ?Collection $containers = null, ?Collecti
|
||||||
StopDatabaseProxy::run($database);
|
StopDatabaseProxy::run($database);
|
||||||
}
|
}
|
||||||
|
|
||||||
$name = data_get($database, 'name');
|
|
||||||
$fqdn = data_get($database, 'fqdn');
|
|
||||||
|
|
||||||
$containerName = $name;
|
|
||||||
|
|
||||||
$projectUuid = data_get($database, 'environment.project.uuid');
|
|
||||||
$environmentName = data_get($database, 'environment.name');
|
|
||||||
$databaseUuid = data_get($database, 'uuid');
|
|
||||||
|
|
||||||
if ($projectUuid && $databaseUuid && $environmentName) {
|
|
||||||
$url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/database/'.$databaseUuid;
|
|
||||||
} else {
|
|
||||||
$url = null;
|
|
||||||
}
|
|
||||||
// $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->trackPreviewRestartCounts($previews);
|
||||||
|
|
||||||
// Aggregate multi-container application statuses
|
// Aggregate multi-container application statuses
|
||||||
if (isset($this->applicationContainerStatuses) && $this->applicationContainerStatuses->isNotEmpty()) {
|
if (isset($this->applicationContainerStatuses) && $this->applicationContainerStatuses->isNotEmpty()) {
|
||||||
foreach ($this->applicationContainerStatuses as $applicationId => $containerStatuses) {
|
foreach ($this->applicationContainerStatuses as $applicationId => $containerStatuses) {
|
||||||
|
|
@ -470,21 +474,21 @@ public function handle(Server $server, ?Collection $containers = null, ?Collecti
|
||||||
|
|
||||||
DB::transaction(function () use ($application, $maxRestartCount, $containerStatuses, &$restartLimitReached) {
|
DB::transaction(function () use ($application, $maxRestartCount, $containerStatuses, &$restartLimitReached) {
|
||||||
$previousRestartCount = $application->restart_count ?? 0;
|
$previousRestartCount = $application->restart_count ?? 0;
|
||||||
|
$restartState = (new RestartCountTracker)->evaluate(
|
||||||
|
previousRestartCount: $previousRestartCount,
|
||||||
|
observedRestartCount: $maxRestartCount,
|
||||||
|
maxRestartCount: $application->max_restart_count ?? 0,
|
||||||
|
);
|
||||||
|
|
||||||
if ($maxRestartCount > $previousRestartCount) {
|
if ($restartState['restart_count_changed']) {
|
||||||
// Restart count increased - this is a crash restart
|
$hasCrashRestarts = $restartState['restart_count'] > 0;
|
||||||
$application->update([
|
$application->update([
|
||||||
'restart_count' => $maxRestartCount,
|
'restart_count' => $restartState['restart_count'],
|
||||||
'last_restart_at' => now(),
|
'last_restart_at' => $hasCrashRestarts ? now() : null,
|
||||||
'last_restart_type' => 'crash',
|
'last_restart_type' => $hasCrashRestarts ? 'crash' : null,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Check if restart limit has been reached
|
|
||||||
$maxAllowedRestarts = $application->max_restart_count ?? 0;
|
|
||||||
if ($maxAllowedRestarts > 0 && $maxRestartCount >= $maxAllowedRestarts && $previousRestartCount < $maxAllowedRestarts) {
|
|
||||||
$restartLimitReached = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
$restartLimitReached = $restartState['restart_limit_reached'];
|
||||||
|
|
||||||
// Aggregate status after tracking restart counts
|
// Aggregate status after tracking restart counts
|
||||||
$aggregatedStatus = $this->aggregateApplicationStatus($application, $containerStatuses, $maxRestartCount);
|
$aggregatedStatus = $this->aggregateApplicationStatus($application, $containerStatuses, $maxRestartCount);
|
||||||
|
|
@ -499,9 +503,22 @@ public function handle(Server $server, ?Collection $containers = null, ?Collecti
|
||||||
});
|
});
|
||||||
|
|
||||||
if ($restartLimitReached) {
|
if ($restartLimitReached) {
|
||||||
$application->refresh();
|
$restartLimitClaimed = Application::query()
|
||||||
StopApplication::dispatch($application, false, true, false);
|
->whereKey($application->getKey())
|
||||||
$application->environment->project->team?->notify(new ApplicationRestartLimitReached($application));
|
->where('restart_limit_reached', false)
|
||||||
|
->update(['restart_limit_reached' => true]) === 1;
|
||||||
|
|
||||||
|
if ($restartLimitClaimed) {
|
||||||
|
$application->refresh();
|
||||||
|
StopApplication::dispatch(
|
||||||
|
application: $application,
|
||||||
|
previewDeployments: false,
|
||||||
|
dockerCleanup: false,
|
||||||
|
resetRestartCount: false,
|
||||||
|
removeContainers: false,
|
||||||
|
);
|
||||||
|
$application->environment->project->team?->notify(new ApplicationRestartLimitReached($application));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -562,6 +579,16 @@ private function aggregateServiceContainerStatuses($services)
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$restartCount = isset($this->serviceContainerRestartCounts)
|
||||||
|
? ($this->serviceContainerRestartCounts->get($key)?->max() ?? 0)
|
||||||
|
: 0;
|
||||||
|
if ($subResource->trackRestartCount($restartCount)) {
|
||||||
|
StopServiceApplication::dispatch($subResource, false, false);
|
||||||
|
$subResource->team()?->notify(new ApplicationRestartLimitReached($subResource));
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Parse docker compose from service to check for excluded containers
|
// Parse docker compose from service to check for excluded containers
|
||||||
$dockerComposeRaw = data_get($service, 'docker_compose_raw');
|
$dockerComposeRaw = data_get($service, 'docker_compose_raw');
|
||||||
$excludedContainers = $this->getExcludedContainersFromDockerCompose($dockerComposeRaw);
|
$excludedContainers = $this->getExcludedContainersFromDockerCompose($dockerComposeRaw);
|
||||||
|
|
@ -602,4 +629,24 @@ private function aggregateServiceContainerStatuses($services)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function trackPreviewRestartCounts(Collection $previews): void
|
||||||
|
{
|
||||||
|
if (! isset($this->previewContainerRestartCounts)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->previewContainerRestartCounts
|
||||||
|
->groupBy('key')
|
||||||
|
->each(function (Collection $counts, string $key) use ($previews): void {
|
||||||
|
[$applicationId, $pullRequestId] = explode(':', $key);
|
||||||
|
$preview = $previews->first(fn (ApplicationPreview $preview): bool => (string) $preview->application_id === $applicationId
|
||||||
|
&& (string) $preview->pull_request_id === $pullRequestId
|
||||||
|
);
|
||||||
|
if ($preview?->trackRestartCount((int) $counts->max('count'))) {
|
||||||
|
StopApplicationPreview::dispatch($preview, false, false);
|
||||||
|
$preview->application->environment->project->team?->notify(new ApplicationRestartLimitReached($preview));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ public function handle(Service $service, bool $pullLatestImages = false, bool $s
|
||||||
}
|
}
|
||||||
$service->saveComposeConfigs();
|
$service->saveComposeConfigs();
|
||||||
$service->isConfigurationChanged(save: true);
|
$service->isConfigurationChanged(save: true);
|
||||||
|
$service->applications()->get()->each->resetRestartLimit();
|
||||||
|
$service->databases()->get()->each->resetRestartLimit();
|
||||||
$workdir = $service->workdir();
|
$workdir = $service->workdir();
|
||||||
// $commands[] = "cd {$workdir}";
|
// $commands[] = "cd {$workdir}";
|
||||||
$commands[] = "echo 'Saved configuration files to {$workdir}.'";
|
$commands[] = "echo 'Saved configuration files to {$workdir}.'";
|
||||||
|
|
|
||||||
|
|
@ -49,8 +49,14 @@ public function handle(Service $service, bool $deleteConnectedNetworks = false,
|
||||||
$this->stopContainersInParallel($containersToStop, $server);
|
$this->stopContainersInParallel($containersToStop, $server);
|
||||||
}
|
}
|
||||||
|
|
||||||
$applications->each->update(['status' => 'exited']);
|
$applications->each(function ($application): void {
|
||||||
$dbs->each->update(['status' => 'exited']);
|
$application->update(['status' => 'exited']);
|
||||||
|
$application->resetRestartLimit();
|
||||||
|
});
|
||||||
|
$dbs->each(function ($database): void {
|
||||||
|
$database->update(['status' => 'exited']);
|
||||||
|
$database->resetRestartLimit();
|
||||||
|
});
|
||||||
|
|
||||||
if ($deleteConnectedNetworks) {
|
if ($deleteConnectedNetworks) {
|
||||||
$service->deleteConnectedNetworks();
|
$service->deleteConnectedNetworks();
|
||||||
|
|
|
||||||
|
|
@ -13,17 +13,26 @@ class StopServiceApplication
|
||||||
|
|
||||||
public string $jobQueue = 'high';
|
public string $jobQueue = 'high';
|
||||||
|
|
||||||
public function handle(ServiceApplication|ServiceDatabase $serviceApplication): void
|
public function handle(ServiceApplication|ServiceDatabase $serviceApplication, bool $resetRestartCount = true, bool $removeContainer = false): void
|
||||||
{
|
{
|
||||||
$service = $serviceApplication->service;
|
$service = $serviceApplication->service;
|
||||||
$server = $service->destination->server;
|
$server = $service->destination->server;
|
||||||
$containerName = escapeshellarg($serviceApplication->name.'-'.$service->uuid);
|
$containerName = escapeshellarg($serviceApplication->name.'-'.$service->uuid);
|
||||||
|
|
||||||
instant_remote_process([
|
if ($removeContainer) {
|
||||||
"docker stop {$containerName}",
|
$commands = ["docker rm -f {$containerName}"];
|
||||||
], $server);
|
} else {
|
||||||
|
$commands = [
|
||||||
|
"docker update --restart=no {$containerName}",
|
||||||
|
"docker stop {$containerName}",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
instant_remote_process($commands, $server, throwError: ! $removeContainer);
|
||||||
|
|
||||||
$serviceApplication->update(['status' => 'exited']);
|
$serviceApplication->update(['status' => 'exited']);
|
||||||
|
if ($resetRestartCount) {
|
||||||
|
$serviceApplication->resetRestartLimit();
|
||||||
|
}
|
||||||
ServiceStatusChanged::dispatch($service->environment->project->team->id);
|
ServiceStatusChanged::dispatch($service->environment->project->team->id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
use OpenApi\Attributes as OA;
|
use OpenApi\Attributes as OA;
|
||||||
|
|
||||||
class NotificationsController extends Controller
|
class NotificationsController extends Controller
|
||||||
|
|
@ -45,6 +46,7 @@ private function channelConfig(string $channel): array
|
||||||
'deployment_success_email_notifications' => 'sometimes|boolean',
|
'deployment_success_email_notifications' => 'sometimes|boolean',
|
||||||
'deployment_failure_email_notifications' => 'sometimes|boolean',
|
'deployment_failure_email_notifications' => 'sometimes|boolean',
|
||||||
'status_change_email_notifications' => 'sometimes|boolean',
|
'status_change_email_notifications' => 'sometimes|boolean',
|
||||||
|
'restart_limit_reached_email_notifications' => 'sometimes|boolean',
|
||||||
'backup_success_email_notifications' => 'sometimes|boolean',
|
'backup_success_email_notifications' => 'sometimes|boolean',
|
||||||
'backup_failure_email_notifications' => 'sometimes|boolean',
|
'backup_failure_email_notifications' => 'sometimes|boolean',
|
||||||
'scheduled_task_success_email_notifications' => 'sometimes|boolean',
|
'scheduled_task_success_email_notifications' => 'sometimes|boolean',
|
||||||
|
|
@ -66,6 +68,7 @@ private function channelConfig(string $channel): array
|
||||||
'deployment_success_discord_notifications' => 'sometimes|boolean',
|
'deployment_success_discord_notifications' => 'sometimes|boolean',
|
||||||
'deployment_failure_discord_notifications' => 'sometimes|boolean',
|
'deployment_failure_discord_notifications' => 'sometimes|boolean',
|
||||||
'status_change_discord_notifications' => 'sometimes|boolean',
|
'status_change_discord_notifications' => 'sometimes|boolean',
|
||||||
|
'restart_limit_reached_discord_notifications' => 'sometimes|boolean',
|
||||||
'backup_success_discord_notifications' => 'sometimes|boolean',
|
'backup_success_discord_notifications' => 'sometimes|boolean',
|
||||||
'backup_failure_discord_notifications' => 'sometimes|boolean',
|
'backup_failure_discord_notifications' => 'sometimes|boolean',
|
||||||
'scheduled_task_success_discord_notifications' => 'sometimes|boolean',
|
'scheduled_task_success_discord_notifications' => 'sometimes|boolean',
|
||||||
|
|
@ -88,6 +91,7 @@ private function channelConfig(string $channel): array
|
||||||
'deployment_success_slack_notifications' => 'sometimes|boolean',
|
'deployment_success_slack_notifications' => 'sometimes|boolean',
|
||||||
'deployment_failure_slack_notifications' => 'sometimes|boolean',
|
'deployment_failure_slack_notifications' => 'sometimes|boolean',
|
||||||
'status_change_slack_notifications' => 'sometimes|boolean',
|
'status_change_slack_notifications' => 'sometimes|boolean',
|
||||||
|
'restart_limit_reached_slack_notifications' => 'sometimes|boolean',
|
||||||
'backup_success_slack_notifications' => 'sometimes|boolean',
|
'backup_success_slack_notifications' => 'sometimes|boolean',
|
||||||
'backup_failure_slack_notifications' => 'sometimes|boolean',
|
'backup_failure_slack_notifications' => 'sometimes|boolean',
|
||||||
'scheduled_task_success_slack_notifications' => 'sometimes|boolean',
|
'scheduled_task_success_slack_notifications' => 'sometimes|boolean',
|
||||||
|
|
@ -110,6 +114,7 @@ private function channelConfig(string $channel): array
|
||||||
'deployment_success_telegram_notifications' => 'sometimes|boolean',
|
'deployment_success_telegram_notifications' => 'sometimes|boolean',
|
||||||
'deployment_failure_telegram_notifications' => 'sometimes|boolean',
|
'deployment_failure_telegram_notifications' => 'sometimes|boolean',
|
||||||
'status_change_telegram_notifications' => 'sometimes|boolean',
|
'status_change_telegram_notifications' => 'sometimes|boolean',
|
||||||
|
'restart_limit_reached_telegram_notifications' => 'sometimes|boolean',
|
||||||
'backup_success_telegram_notifications' => 'sometimes|boolean',
|
'backup_success_telegram_notifications' => 'sometimes|boolean',
|
||||||
'backup_failure_telegram_notifications' => 'sometimes|boolean',
|
'backup_failure_telegram_notifications' => 'sometimes|boolean',
|
||||||
'scheduled_task_success_telegram_notifications' => 'sometimes|boolean',
|
'scheduled_task_success_telegram_notifications' => 'sometimes|boolean',
|
||||||
|
|
@ -124,6 +129,7 @@ private function channelConfig(string $channel): array
|
||||||
'telegram_notifications_deployment_success_thread_id' => 'sometimes|nullable|string|max:255',
|
'telegram_notifications_deployment_success_thread_id' => 'sometimes|nullable|string|max:255',
|
||||||
'telegram_notifications_deployment_failure_thread_id' => 'sometimes|nullable|string|max:255',
|
'telegram_notifications_deployment_failure_thread_id' => 'sometimes|nullable|string|max:255',
|
||||||
'telegram_notifications_status_change_thread_id' => 'sometimes|nullable|string|max:255',
|
'telegram_notifications_status_change_thread_id' => 'sometimes|nullable|string|max:255',
|
||||||
|
'telegram_notifications_restart_limit_reached_thread_id' => 'sometimes|nullable|string|max:255',
|
||||||
'telegram_notifications_backup_success_thread_id' => 'sometimes|nullable|string|max:255',
|
'telegram_notifications_backup_success_thread_id' => 'sometimes|nullable|string|max:255',
|
||||||
'telegram_notifications_backup_failure_thread_id' => 'sometimes|nullable|string|max:255',
|
'telegram_notifications_backup_failure_thread_id' => 'sometimes|nullable|string|max:255',
|
||||||
'telegram_notifications_scheduled_task_success_thread_id' => 'sometimes|nullable|string|max:255',
|
'telegram_notifications_scheduled_task_success_thread_id' => 'sometimes|nullable|string|max:255',
|
||||||
|
|
@ -146,6 +152,7 @@ private function channelConfig(string $channel): array
|
||||||
'deployment_success_pushover_notifications' => 'sometimes|boolean',
|
'deployment_success_pushover_notifications' => 'sometimes|boolean',
|
||||||
'deployment_failure_pushover_notifications' => 'sometimes|boolean',
|
'deployment_failure_pushover_notifications' => 'sometimes|boolean',
|
||||||
'status_change_pushover_notifications' => 'sometimes|boolean',
|
'status_change_pushover_notifications' => 'sometimes|boolean',
|
||||||
|
'restart_limit_reached_pushover_notifications' => 'sometimes|boolean',
|
||||||
'backup_success_pushover_notifications' => 'sometimes|boolean',
|
'backup_success_pushover_notifications' => 'sometimes|boolean',
|
||||||
'backup_failure_pushover_notifications' => 'sometimes|boolean',
|
'backup_failure_pushover_notifications' => 'sometimes|boolean',
|
||||||
'scheduled_task_success_pushover_notifications' => 'sometimes|boolean',
|
'scheduled_task_success_pushover_notifications' => 'sometimes|boolean',
|
||||||
|
|
@ -167,6 +174,7 @@ private function channelConfig(string $channel): array
|
||||||
'deployment_success_webhook_notifications' => 'sometimes|boolean',
|
'deployment_success_webhook_notifications' => 'sometimes|boolean',
|
||||||
'deployment_failure_webhook_notifications' => 'sometimes|boolean',
|
'deployment_failure_webhook_notifications' => 'sometimes|boolean',
|
||||||
'status_change_webhook_notifications' => 'sometimes|boolean',
|
'status_change_webhook_notifications' => 'sometimes|boolean',
|
||||||
|
'restart_limit_reached_webhook_notifications' => 'sometimes|boolean',
|
||||||
'backup_success_webhook_notifications' => 'sometimes|boolean',
|
'backup_success_webhook_notifications' => 'sometimes|boolean',
|
||||||
'backup_failure_webhook_notifications' => 'sometimes|boolean',
|
'backup_failure_webhook_notifications' => 'sometimes|boolean',
|
||||||
'scheduled_task_success_webhook_notifications' => 'sometimes|boolean',
|
'scheduled_task_success_webhook_notifications' => 'sometimes|boolean',
|
||||||
|
|
@ -249,7 +257,7 @@ private function updateChannel(Request $request, string $channel): JsonResponse
|
||||||
$body = $request->json()->all();
|
$body = $request->json()->all();
|
||||||
$config = $this->channelConfig($channel);
|
$config = $this->channelConfig($channel);
|
||||||
|
|
||||||
$validator = customApiValidator($body, $config['rules']);
|
$validator = Validator::make($body, $config['rules']);
|
||||||
|
|
||||||
$extraFields = array_diff(array_keys($body), $allowedFields);
|
$extraFields = array_diff(array_keys($body), $allowedFields);
|
||||||
if ($validator->fails() || ! empty($extraFields)) {
|
if ($validator->fails() || ! empty($extraFields)) {
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,7 @@ private function shouldDispatchUpdate(Server $server, array $data): bool
|
||||||
/**
|
/**
|
||||||
* Build a stable hash of container state.
|
* Build a stable hash of container state.
|
||||||
*
|
*
|
||||||
* Covers [name, state] only — metrics, filesystem_usage_root, and
|
* Covers [name, state, restart_count] only — metrics, filesystem_usage_root, and
|
||||||
* health_status are excluded on purpose. Disk % churns constantly, and
|
* health_status are excluded on purpose. Disk % churns constantly, and
|
||||||
* health checks can flap between starting/healthy/unhealthy while the
|
* health checks can flap between starting/healthy/unhealthy while the
|
||||||
* container lifecycle state remains unchanged. Both would otherwise defeat
|
* container lifecycle state remains unchanged. Both would otherwise defeat
|
||||||
|
|
@ -153,6 +153,7 @@ private function containerStateHash(array $data): string
|
||||||
->map(fn ($c) => [
|
->map(fn ($c) => [
|
||||||
'name' => data_get($c, 'name'),
|
'name' => data_get($c, 'name'),
|
||||||
'state' => data_get($c, 'state'),
|
'state' => data_get($c, 'state'),
|
||||||
|
'restart_count' => data_get($c, 'restart_count'),
|
||||||
])
|
])
|
||||||
->sortBy('name')
|
->sortBy('name')
|
||||||
->values()
|
->values()
|
||||||
|
|
|
||||||
|
|
@ -4972,11 +4972,21 @@ private function handleSuccessfulDeployment(): void
|
||||||
// Reset restart count after successful deployment
|
// Reset restart count after successful deployment
|
||||||
// This is done here (not in Livewire) to avoid race conditions
|
// This is done here (not in Livewire) to avoid race conditions
|
||||||
// with GetContainersStatus reading old container restart counts
|
// with GetContainersStatus reading old container restart counts
|
||||||
$this->application->update([
|
$restartState = [
|
||||||
'restart_count' => 0,
|
'restart_count' => 0,
|
||||||
'last_restart_at' => null,
|
'last_restart_at' => null,
|
||||||
'last_restart_type' => null,
|
'last_restart_type' => null,
|
||||||
]);
|
];
|
||||||
|
|
||||||
|
if ($this->pull_request_id === 0) {
|
||||||
|
$restartState['restart_limit_reached'] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->pull_request_id === 0) {
|
||||||
|
$this->application->update($restartState);
|
||||||
|
} else {
|
||||||
|
$this->preview?->resetRestartLimit();
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->application->markDeploymentConfigurationApplied($this->application_deployment_queue);
|
$this->application->markDeploymentConfigurationApplied($this->application_deployment_queue);
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,15 @@
|
||||||
|
|
||||||
namespace App\Jobs;
|
namespace App\Jobs;
|
||||||
|
|
||||||
|
use App\Actions\Application\StopApplication;
|
||||||
|
use App\Actions\Application\StopApplicationPreview;
|
||||||
use App\Actions\Database\StartDatabaseProxy;
|
use App\Actions\Database\StartDatabaseProxy;
|
||||||
|
use App\Actions\Database\StopDatabase;
|
||||||
use App\Actions\Database\StopDatabaseProxy;
|
use App\Actions\Database\StopDatabaseProxy;
|
||||||
use App\Actions\Proxy\CheckProxy;
|
use App\Actions\Proxy\CheckProxy;
|
||||||
use App\Actions\Proxy\StartProxy;
|
use App\Actions\Proxy\StartProxy;
|
||||||
use App\Actions\Server\StartLogDrain;
|
use App\Actions\Server\StartLogDrain;
|
||||||
|
use App\Actions\Service\StopServiceApplication;
|
||||||
use App\Actions\Shared\ComplexStatusCheck;
|
use App\Actions\Shared\ComplexStatusCheck;
|
||||||
use App\Models\Application;
|
use App\Models\Application;
|
||||||
use App\Models\ApplicationPreview;
|
use App\Models\ApplicationPreview;
|
||||||
|
|
@ -23,8 +27,10 @@
|
||||||
use App\Models\StandalonePostgresql;
|
use App\Models\StandalonePostgresql;
|
||||||
use App\Models\StandaloneRedis;
|
use App\Models\StandaloneRedis;
|
||||||
use App\Models\SwarmDocker;
|
use App\Models\SwarmDocker;
|
||||||
|
use App\Notifications\Application\RestartLimitReached as ApplicationRestartLimitReached;
|
||||||
use App\Notifications\Container\ContainerRestarted;
|
use App\Notifications\Container\ContainerRestarted;
|
||||||
use App\Services\ContainerStatusAggregator;
|
use App\Services\ContainerStatusAggregator;
|
||||||
|
use App\Services\RestartCountTracker;
|
||||||
use App\Traits\CalculatesExcludedStatus;
|
use App\Traits\CalculatesExcludedStatus;
|
||||||
use Illuminate\Bus\Queueable;
|
use Illuminate\Bus\Queueable;
|
||||||
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||||
|
|
@ -95,8 +101,14 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue, Silenced
|
||||||
|
|
||||||
public Collection $applicationContainerStatuses;
|
public Collection $applicationContainerStatuses;
|
||||||
|
|
||||||
|
public Collection $applicationContainerRestartCounts;
|
||||||
|
|
||||||
public Collection $serviceContainerStatuses;
|
public Collection $serviceContainerStatuses;
|
||||||
|
|
||||||
|
public Collection $previewContainerRestartCounts;
|
||||||
|
|
||||||
|
public Collection $serviceContainerRestartCounts;
|
||||||
|
|
||||||
public bool $foundProxy = false;
|
public bool $foundProxy = false;
|
||||||
|
|
||||||
public bool $foundLogDrainContainer = false;
|
public bool $foundLogDrainContainer = false;
|
||||||
|
|
@ -122,7 +134,10 @@ public function __construct(public Server $server, public $data)
|
||||||
$this->foundApplicationPreviewsIds = collect();
|
$this->foundApplicationPreviewsIds = collect();
|
||||||
$this->foundServiceDatabaseIds = collect();
|
$this->foundServiceDatabaseIds = collect();
|
||||||
$this->applicationContainerStatuses = collect();
|
$this->applicationContainerStatuses = collect();
|
||||||
|
$this->applicationContainerRestartCounts = collect();
|
||||||
$this->serviceContainerStatuses = collect();
|
$this->serviceContainerStatuses = collect();
|
||||||
|
$this->previewContainerRestartCounts = collect();
|
||||||
|
$this->serviceContainerRestartCounts = collect();
|
||||||
$this->allApplicationIds = collect();
|
$this->allApplicationIds = collect();
|
||||||
$this->allDatabaseUuids = collect();
|
$this->allDatabaseUuids = collect();
|
||||||
$this->allTcpProxyUuids = collect();
|
$this->allTcpProxyUuids = collect();
|
||||||
|
|
@ -140,7 +155,10 @@ public function handle()
|
||||||
{
|
{
|
||||||
// Defensive initialization for Collection properties to handle queue deserialization edge cases
|
// Defensive initialization for Collection properties to handle queue deserialization edge cases
|
||||||
$this->serviceContainerStatuses ??= collect();
|
$this->serviceContainerStatuses ??= collect();
|
||||||
|
$this->previewContainerRestartCounts ??= collect();
|
||||||
|
$this->serviceContainerRestartCounts ??= collect();
|
||||||
$this->applicationContainerStatuses ??= collect();
|
$this->applicationContainerStatuses ??= collect();
|
||||||
|
$this->applicationContainerRestartCounts ??= collect();
|
||||||
$this->foundApplicationIds ??= collect();
|
$this->foundApplicationIds ??= collect();
|
||||||
$this->foundDatabaseUuids ??= collect();
|
$this->foundDatabaseUuids ??= collect();
|
||||||
$this->foundServiceApplicationIds ??= collect();
|
$this->foundServiceApplicationIds ??= collect();
|
||||||
|
|
@ -231,6 +249,9 @@ public function handle()
|
||||||
if (! $coolify_managed) {
|
if (! $coolify_managed) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (filter_var($labels->get('com.docker.compose.oneoff'), FILTER_VALIDATE_BOOLEAN)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$name = data_get($container, 'name');
|
$name = data_get($container, 'name');
|
||||||
if ($name === 'coolify-log-drain' && $this->isRunning($containerStatus)) {
|
if ($name === 'coolify-log-drain' && $this->isRunning($containerStatus)) {
|
||||||
|
|
@ -241,6 +262,10 @@ public function handle()
|
||||||
$pullRequestId = $labels->get('coolify.pullRequestId', '0');
|
$pullRequestId = $labels->get('coolify.pullRequestId', '0');
|
||||||
try {
|
try {
|
||||||
if ($pullRequestId === '0') {
|
if ($pullRequestId === '0') {
|
||||||
|
$application = $this->applicationsById->get((string) $applicationId);
|
||||||
|
if ($application && $application->container_present !== true) {
|
||||||
|
$application->update(['container_present' => true]);
|
||||||
|
}
|
||||||
if ($this->allApplicationIds->contains($applicationId)) {
|
if ($this->allApplicationIds->contains($applicationId)) {
|
||||||
$this->foundApplicationIds->push($applicationId);
|
$this->foundApplicationIds->push($applicationId);
|
||||||
}
|
}
|
||||||
|
|
@ -251,6 +276,13 @@ public function handle()
|
||||||
$containerName = $labels->get('com.docker.compose.service');
|
$containerName = $labels->get('com.docker.compose.service');
|
||||||
if ($containerName) {
|
if ($containerName) {
|
||||||
$this->applicationContainerStatuses->get($applicationId)->put($containerName, $containerStatus);
|
$this->applicationContainerStatuses->get($applicationId)->put($containerName, $containerStatus);
|
||||||
|
$restartCount = data_get($container, 'restart_count');
|
||||||
|
if (is_numeric($restartCount)) {
|
||||||
|
if (! $this->applicationContainerRestartCounts->has($applicationId)) {
|
||||||
|
$this->applicationContainerRestartCounts->put($applicationId, collect());
|
||||||
|
}
|
||||||
|
$this->applicationContainerRestartCounts->get($applicationId)->put($containerName, (int) $restartCount);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$previewKey = $applicationId.':'.$pullRequestId;
|
$previewKey = $applicationId.':'.$pullRequestId;
|
||||||
|
|
@ -258,6 +290,13 @@ public function handle()
|
||||||
$this->foundApplicationPreviewsIds->push($previewKey);
|
$this->foundApplicationPreviewsIds->push($previewKey);
|
||||||
}
|
}
|
||||||
$this->updateApplicationPreviewStatus($applicationId, $pullRequestId, $containerStatus);
|
$this->updateApplicationPreviewStatus($applicationId, $pullRequestId, $containerStatus);
|
||||||
|
$restartCount = data_get($container, 'restart_count');
|
||||||
|
if (is_numeric($restartCount)) {
|
||||||
|
$this->previewContainerRestartCounts->push([
|
||||||
|
'key' => $previewKey,
|
||||||
|
'count' => (int) $restartCount,
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
}
|
}
|
||||||
|
|
@ -278,6 +317,7 @@ public function handle()
|
||||||
$containerName = $labels->get('com.docker.compose.service');
|
$containerName = $labels->get('com.docker.compose.service');
|
||||||
if ($containerName) {
|
if ($containerName) {
|
||||||
$this->serviceContainerStatuses->get($key)->put($containerName, $containerStatus);
|
$this->serviceContainerStatuses->get($key)->put($containerName, $containerStatus);
|
||||||
|
$this->storeServiceRestartCount($key, $containerName, data_get($container, 'restart_count'));
|
||||||
}
|
}
|
||||||
} elseif ($subType === 'database') {
|
} elseif ($subType === 'database') {
|
||||||
$this->foundServiceDatabaseIds->push($subId);
|
$this->foundServiceDatabaseIds->push($subId);
|
||||||
|
|
@ -289,6 +329,7 @@ public function handle()
|
||||||
$containerName = $labels->get('com.docker.compose.service');
|
$containerName = $labels->get('com.docker.compose.service');
|
||||||
if ($containerName) {
|
if ($containerName) {
|
||||||
$this->serviceContainerStatuses->get($key)->put($containerName, $containerStatus);
|
$this->serviceContainerStatuses->get($key)->put($containerName, $containerStatus);
|
||||||
|
$this->storeServiceRestartCount($key, $containerName, data_get($container, 'restart_count'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -302,9 +343,9 @@ public function handle()
|
||||||
$this->foundDatabaseUuids->push($uuid);
|
$this->foundDatabaseUuids->push($uuid);
|
||||||
// TCP proxy should only be started/managed when database is actually running
|
// TCP proxy should only be started/managed when database is actually running
|
||||||
if ($this->allTcpProxyUuids->contains($uuid) && $this->isRunning($containerStatus)) {
|
if ($this->allTcpProxyUuids->contains($uuid) && $this->isRunning($containerStatus)) {
|
||||||
$this->updateDatabaseStatus($uuid, $containerStatus, tcpProxy: true);
|
$this->updateDatabaseStatus($uuid, $containerStatus, data_get($container, 'restart_count'), tcpProxy: true);
|
||||||
} else {
|
} else {
|
||||||
$this->updateDatabaseStatus($uuid, $containerStatus, tcpProxy: false);
|
$this->updateDatabaseStatus($uuid, $containerStatus, data_get($container, 'restart_count'), tcpProxy: false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -317,6 +358,9 @@ public function handle()
|
||||||
|
|
||||||
$this->updateProxyStatus();
|
$this->updateProxyStatus();
|
||||||
|
|
||||||
|
Application::whereIn('id', $this->foundApplicationIds->unique())
|
||||||
|
->update(['container_present' => true]);
|
||||||
|
|
||||||
$this->updateNotFoundApplicationStatus();
|
$this->updateNotFoundApplicationStatus();
|
||||||
$this->updateNotFoundApplicationPreviewStatus();
|
$this->updateNotFoundApplicationPreviewStatus();
|
||||||
$this->updateNotFoundDatabaseStatus();
|
$this->updateNotFoundDatabaseStatus();
|
||||||
|
|
@ -324,6 +368,8 @@ public function handle()
|
||||||
|
|
||||||
$this->updateAdditionalServersStatus();
|
$this->updateAdditionalServersStatus();
|
||||||
|
|
||||||
|
$this->trackPreviewRestartCounts();
|
||||||
|
|
||||||
// Aggregate multi-container application statuses
|
// Aggregate multi-container application statuses
|
||||||
$this->aggregateMultiContainerStatuses();
|
$this->aggregateMultiContainerStatuses();
|
||||||
|
|
||||||
|
|
@ -349,11 +395,18 @@ private function loadApplications(): Collection
|
||||||
'uuid',
|
'uuid',
|
||||||
'name',
|
'name',
|
||||||
'status',
|
'status',
|
||||||
|
'container_present',
|
||||||
'build_pack',
|
'build_pack',
|
||||||
'docker_compose_raw',
|
'docker_compose_raw',
|
||||||
|
'environment_id',
|
||||||
'destination_id',
|
'destination_id',
|
||||||
'destination_type',
|
'destination_type',
|
||||||
'last_online_at',
|
'last_online_at',
|
||||||
|
'restart_count',
|
||||||
|
'max_restart_count',
|
||||||
|
'restart_limit_reached',
|
||||||
|
'last_restart_at',
|
||||||
|
'last_restart_type',
|
||||||
])
|
])
|
||||||
->withCount('additional_servers')
|
->withCount('additional_servers')
|
||||||
->where(fn ($query) => $this->scopeDestination($query, $standaloneDockerIds, $swarmDockerIds))
|
->where(fn ($query) => $this->scopeDestination($query, $standaloneDockerIds, $swarmDockerIds))
|
||||||
|
|
@ -372,11 +425,18 @@ private function loadApplications(): Collection
|
||||||
'uuid',
|
'uuid',
|
||||||
'name',
|
'name',
|
||||||
'status',
|
'status',
|
||||||
|
'container_present',
|
||||||
'build_pack',
|
'build_pack',
|
||||||
'docker_compose_raw',
|
'docker_compose_raw',
|
||||||
|
'environment_id',
|
||||||
'destination_id',
|
'destination_id',
|
||||||
'destination_type',
|
'destination_type',
|
||||||
'last_online_at',
|
'last_online_at',
|
||||||
|
'restart_count',
|
||||||
|
'max_restart_count',
|
||||||
|
'restart_limit_reached',
|
||||||
|
'last_restart_at',
|
||||||
|
'last_restart_type',
|
||||||
])
|
])
|
||||||
->withCount('additional_servers')
|
->withCount('additional_servers')
|
||||||
->whereIn('id', $additionalApplicationIds)
|
->whereIn('id', $additionalApplicationIds)
|
||||||
|
|
@ -402,6 +462,11 @@ private function loadPreviews(): Collection
|
||||||
'pull_request_id',
|
'pull_request_id',
|
||||||
'status',
|
'status',
|
||||||
'last_online_at',
|
'last_online_at',
|
||||||
|
'restart_count',
|
||||||
|
'max_restart_count',
|
||||||
|
'restart_limit_reached',
|
||||||
|
'last_restart_at',
|
||||||
|
'last_restart_type',
|
||||||
])
|
])
|
||||||
->whereIn('application_id', $applicationIds)
|
->whereIn('application_id', $applicationIds)
|
||||||
->get();
|
->get();
|
||||||
|
|
@ -417,8 +482,8 @@ private function loadServices(): Collection
|
||||||
'docker_compose_raw',
|
'docker_compose_raw',
|
||||||
])
|
])
|
||||||
->with([
|
->with([
|
||||||
'applications:id,service_id,status,last_online_at',
|
'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',
|
'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',
|
||||||
])
|
])
|
||||||
->get();
|
->get();
|
||||||
}
|
}
|
||||||
|
|
@ -441,6 +506,8 @@ private function loadDatabases(): Collection
|
||||||
'restart_count',
|
'restart_count',
|
||||||
'last_restart_at',
|
'last_restart_at',
|
||||||
'last_restart_type',
|
'last_restart_type',
|
||||||
|
'max_restart_count',
|
||||||
|
'restart_limit_reached',
|
||||||
];
|
];
|
||||||
|
|
||||||
return collect([
|
return collect([
|
||||||
|
|
@ -495,6 +562,53 @@ private function aggregateMultiContainerStatuses()
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$maxRestartCount = 0;
|
||||||
|
$restartCountsAvailable = $this->applicationContainerRestartCounts->has($applicationId);
|
||||||
|
if ($restartCountsAvailable) {
|
||||||
|
$maxRestartCount = $this->applicationContainerRestartCounts->get($applicationId)->max() ?? 0;
|
||||||
|
$restartState = (new RestartCountTracker)->evaluate(
|
||||||
|
previousRestartCount: $application->restart_count ?? 0,
|
||||||
|
observedRestartCount: $maxRestartCount,
|
||||||
|
maxRestartCount: $application->max_restart_count ?? 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($restartState['restart_count_changed']) {
|
||||||
|
$hasCrashRestarts = $restartState['restart_count'] > 0;
|
||||||
|
$application->update([
|
||||||
|
'restart_count' => $restartState['restart_count'],
|
||||||
|
'last_restart_at' => $hasCrashRestarts ? now() : null,
|
||||||
|
'last_restart_type' => $hasCrashRestarts ? 'crash' : null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($restartState['restart_limit_reached']) {
|
||||||
|
$restartLimitClaimed = Application::query()
|
||||||
|
->whereKey($application->getKey())
|
||||||
|
->where('restart_limit_reached', false)
|
||||||
|
->update(['restart_limit_reached' => true]) === 1;
|
||||||
|
|
||||||
|
if ($restartLimitClaimed) {
|
||||||
|
$application->refresh();
|
||||||
|
StopApplication::dispatch(
|
||||||
|
application: $application,
|
||||||
|
previewDeployments: false,
|
||||||
|
dockerCleanup: false,
|
||||||
|
resetRestartCount: false,
|
||||||
|
removeContainers: false,
|
||||||
|
);
|
||||||
|
$application->environment->project->team?->notify(new ApplicationRestartLimitReached($application));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($application->stoppedAfterRestartLimit() && $containerStatuses->every(
|
||||||
|
fn (string $status): bool => str($status)->contains('exited')
|
||||||
|
)) {
|
||||||
|
$application->update(['status' => 'exited']);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Parse docker compose to check for excluded containers
|
// Parse docker compose to check for excluded containers
|
||||||
$dockerComposeRaw = data_get($application, 'docker_compose_raw');
|
$dockerComposeRaw = data_get($application, 'docker_compose_raw');
|
||||||
$excludedContainers = $this->getExcludedContainersFromDockerCompose($dockerComposeRaw);
|
$excludedContainers = $this->getExcludedContainersFromDockerCompose($dockerComposeRaw);
|
||||||
|
|
@ -519,7 +633,7 @@ private function aggregateMultiContainerStatuses()
|
||||||
// Use ContainerStatusAggregator service for state machine logic
|
// Use ContainerStatusAggregator service for state machine logic
|
||||||
// Use preserveRestarting: true so applications show "Restarting" instead of "Degraded"
|
// Use preserveRestarting: true so applications show "Restarting" instead of "Degraded"
|
||||||
$aggregator = new ContainerStatusAggregator;
|
$aggregator = new ContainerStatusAggregator;
|
||||||
$aggregatedStatus = $aggregator->aggregateFromStrings($relevantStatuses, 0, preserveRestarting: true);
|
$aggregatedStatus = $aggregator->aggregateFromStrings($relevantStatuses, $maxRestartCount, preserveRestarting: true);
|
||||||
|
|
||||||
// Update application status with aggregated result
|
// Update application status with aggregated result
|
||||||
if ($aggregatedStatus && $application->status !== $aggregatedStatus) {
|
if ($aggregatedStatus && $application->status !== $aggregatedStatus) {
|
||||||
|
|
@ -560,6 +674,14 @@ private function aggregateServiceContainerStatuses()
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$restartCount = $this->serviceContainerRestartCounts->get($key)?->max() ?? 0;
|
||||||
|
if ($subResource->trackRestartCount($restartCount)) {
|
||||||
|
StopServiceApplication::dispatch($subResource, false, false);
|
||||||
|
$subResource->team()?->notify(new ApplicationRestartLimitReached($subResource));
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Parse docker compose from service to check for excluded containers
|
// Parse docker compose from service to check for excluded containers
|
||||||
$dockerComposeRaw = data_get($service, 'docker_compose_raw');
|
$dockerComposeRaw = data_get($service, 'docker_compose_raw');
|
||||||
$excludedContainers = $this->getExcludedContainersFromDockerCompose($dockerComposeRaw);
|
$excludedContainers = $this->getExcludedContainersFromDockerCompose($dockerComposeRaw);
|
||||||
|
|
@ -581,10 +703,9 @@ private function aggregateServiceContainerStatuses()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use ContainerStatusAggregator service for state machine logic
|
// Use ContainerStatusAggregator service for state machine logic
|
||||||
// NOTE: Sentinel does NOT provide restart count data, so maxRestartCount is always 0
|
|
||||||
// Use preserveRestarting: true so individual sub-resources show "Restarting" instead of "Degraded"
|
// Use preserveRestarting: true so individual sub-resources show "Restarting" instead of "Degraded"
|
||||||
$aggregator = new ContainerStatusAggregator;
|
$aggregator = new ContainerStatusAggregator;
|
||||||
$aggregatedStatus = $aggregator->aggregateFromStrings($relevantStatuses, 0, preserveRestarting: true);
|
$aggregatedStatus = $aggregator->aggregateFromStrings($relevantStatuses, $restartCount, preserveRestarting: true);
|
||||||
|
|
||||||
// Update service sub-resource status with aggregated result
|
// Update service sub-resource status with aggregated result
|
||||||
if ($aggregatedStatus && $subResource->status !== $aggregatedStatus) {
|
if ($aggregatedStatus && $subResource->status !== $aggregatedStatus) {
|
||||||
|
|
@ -627,8 +748,11 @@ private function updateNotFoundApplicationStatus()
|
||||||
|
|
||||||
// Batch update: mark all not-found applications as exited (excluding already exited ones)
|
// Batch update: mark all not-found applications as exited (excluding already exited ones)
|
||||||
Application::whereIn('id', $notFoundApplicationIds)
|
Application::whereIn('id', $notFoundApplicationIds)
|
||||||
->where('status', 'not like', 'exited%')
|
->update([
|
||||||
->update(['status' => 'exited']);
|
'status' => 'exited',
|
||||||
|
'container_present' => false,
|
||||||
|
'restart_limit_reached' => false,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function updateNotFoundApplicationPreviewStatus()
|
private function updateNotFoundApplicationPreviewStatus()
|
||||||
|
|
@ -687,7 +811,7 @@ private function updateProxyStatus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function updateDatabaseStatus(string $databaseUuid, string $containerStatus, bool $tcpProxy = false)
|
private function updateDatabaseStatus(string $databaseUuid, string $containerStatus, mixed $restartCount = null, bool $tcpProxy = false): void
|
||||||
{
|
{
|
||||||
$database = $this->databasesByUuid->get($databaseUuid);
|
$database = $this->databasesByUuid->get($databaseUuid);
|
||||||
if (! $database) {
|
if (! $database) {
|
||||||
|
|
@ -697,6 +821,12 @@ private function updateDatabaseStatus(string $databaseUuid, string $containerSta
|
||||||
$database->status = $containerStatus;
|
$database->status = $containerStatus;
|
||||||
$database->save();
|
$database->save();
|
||||||
}
|
}
|
||||||
|
if (is_numeric($restartCount) && $database->trackRestartCount((int) $restartCount)) {
|
||||||
|
StopDatabase::dispatch($database, false, false, false);
|
||||||
|
$database->team()?->notify(new ApplicationRestartLimitReached($database));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (! $this->isCompleteSnapshot()) {
|
if (! $this->isCompleteSnapshot()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -719,6 +849,30 @@ private function updateDatabaseStatus(string $databaseUuid, string $containerSta
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function storeServiceRestartCount(string $key, string $containerName, mixed $restartCount): void
|
||||||
|
{
|
||||||
|
if (! is_numeric($restartCount)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (! $this->serviceContainerRestartCounts->has($key)) {
|
||||||
|
$this->serviceContainerRestartCounts->put($key, collect());
|
||||||
|
}
|
||||||
|
$this->serviceContainerRestartCounts->get($key)->put($containerName, (int) $restartCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function trackPreviewRestartCounts(): void
|
||||||
|
{
|
||||||
|
$this->previewContainerRestartCounts
|
||||||
|
->groupBy('key')
|
||||||
|
->each(function (Collection $counts, string $key): void {
|
||||||
|
$preview = $this->previewsByKey->get($key);
|
||||||
|
if ($preview?->trackRestartCount((int) $counts->max('count'))) {
|
||||||
|
StopApplicationPreview::dispatch($preview, false, false);
|
||||||
|
$preview->application->environment->project->team?->notify(new ApplicationRestartLimitReached($preview));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private function updateNotFoundDatabaseStatus()
|
private function updateNotFoundDatabaseStatus()
|
||||||
{
|
{
|
||||||
$notFoundDatabaseUuids = $this->allDatabaseUuids->diff($this->foundDatabaseUuids);
|
$notFoundDatabaseUuids = $this->allDatabaseUuids->diff($this->foundDatabaseUuids);
|
||||||
|
|
@ -729,12 +883,16 @@ private function updateNotFoundDatabaseStatus()
|
||||||
$notFoundDatabaseUuids->each(function ($databaseUuid) {
|
$notFoundDatabaseUuids->each(function ($databaseUuid) {
|
||||||
$database = $this->databasesByUuid->get($databaseUuid);
|
$database = $this->databasesByUuid->get($databaseUuid);
|
||||||
if ($database) {
|
if ($database) {
|
||||||
|
if ($database->stoppedAfterRestartLimit()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (! str($database->status)->startsWith('exited')) {
|
if (! str($database->status)->startsWith('exited')) {
|
||||||
$database->update([
|
$database->update([
|
||||||
'status' => 'exited',
|
'status' => 'exited',
|
||||||
'restart_count' => 0,
|
'restart_count' => 0,
|
||||||
'last_restart_at' => null,
|
'last_restart_at' => null,
|
||||||
'last_restart_type' => null,
|
'last_restart_type' => null,
|
||||||
|
'restart_limit_reached' => false,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
if ($database->is_public) {
|
if ($database->is_public) {
|
||||||
|
|
@ -752,15 +910,17 @@ private function updateNotFoundServiceStatus()
|
||||||
// Batch update service applications
|
// Batch update service applications
|
||||||
if ($notFoundServiceApplicationIds->isNotEmpty()) {
|
if ($notFoundServiceApplicationIds->isNotEmpty()) {
|
||||||
ServiceApplication::whereIn('id', $notFoundServiceApplicationIds)
|
ServiceApplication::whereIn('id', $notFoundServiceApplicationIds)
|
||||||
|
->where('restart_limit_reached', false)
|
||||||
->where('status', '!=', 'exited')
|
->where('status', '!=', 'exited')
|
||||||
->update(['status' => 'exited']);
|
->update(['status' => 'exited', 'restart_count' => 0, 'last_restart_at' => null, 'last_restart_type' => null]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Batch update service databases
|
// Batch update service databases
|
||||||
if ($notFoundServiceDatabaseIds->isNotEmpty()) {
|
if ($notFoundServiceDatabaseIds->isNotEmpty()) {
|
||||||
ServiceDatabase::whereIn('id', $notFoundServiceDatabaseIds)
|
ServiceDatabase::whereIn('id', $notFoundServiceDatabaseIds)
|
||||||
|
->where('restart_limit_reached', false)
|
||||||
->where('status', '!=', 'exited')
|
->where('status', '!=', 'exited')
|
||||||
->update(['status' => 'exited']);
|
->update(['status' => 'exited', 'restart_count' => 0, 'last_restart_at' => null, 'last_restart_type' => null]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,9 @@ class Discord extends Component
|
||||||
#[Validate(['boolean'])]
|
#[Validate(['boolean'])]
|
||||||
public bool $statusChangeDiscordNotifications = false;
|
public bool $statusChangeDiscordNotifications = false;
|
||||||
|
|
||||||
|
#[Validate(['boolean'])]
|
||||||
|
public bool $restartLimitReachedDiscordNotifications = true;
|
||||||
|
|
||||||
#[Validate(['boolean'])]
|
#[Validate(['boolean'])]
|
||||||
public bool $backupSuccessDiscordNotifications = false;
|
public bool $backupSuccessDiscordNotifications = false;
|
||||||
|
|
||||||
|
|
@ -93,6 +96,7 @@ public function syncData(bool $toModel = false)
|
||||||
$this->settings->deployment_success_discord_notifications = $this->deploymentSuccessDiscordNotifications;
|
$this->settings->deployment_success_discord_notifications = $this->deploymentSuccessDiscordNotifications;
|
||||||
$this->settings->deployment_failure_discord_notifications = $this->deploymentFailureDiscordNotifications;
|
$this->settings->deployment_failure_discord_notifications = $this->deploymentFailureDiscordNotifications;
|
||||||
$this->settings->status_change_discord_notifications = $this->statusChangeDiscordNotifications;
|
$this->settings->status_change_discord_notifications = $this->statusChangeDiscordNotifications;
|
||||||
|
$this->settings->restart_limit_reached_discord_notifications = $this->restartLimitReachedDiscordNotifications;
|
||||||
$this->settings->backup_success_discord_notifications = $this->backupSuccessDiscordNotifications;
|
$this->settings->backup_success_discord_notifications = $this->backupSuccessDiscordNotifications;
|
||||||
$this->settings->backup_failure_discord_notifications = $this->backupFailureDiscordNotifications;
|
$this->settings->backup_failure_discord_notifications = $this->backupFailureDiscordNotifications;
|
||||||
$this->settings->scheduled_task_success_discord_notifications = $this->scheduledTaskSuccessDiscordNotifications;
|
$this->settings->scheduled_task_success_discord_notifications = $this->scheduledTaskSuccessDiscordNotifications;
|
||||||
|
|
@ -118,6 +122,7 @@ public function syncData(bool $toModel = false)
|
||||||
$this->deploymentSuccessDiscordNotifications = $this->settings->deployment_success_discord_notifications;
|
$this->deploymentSuccessDiscordNotifications = $this->settings->deployment_success_discord_notifications;
|
||||||
$this->deploymentFailureDiscordNotifications = $this->settings->deployment_failure_discord_notifications;
|
$this->deploymentFailureDiscordNotifications = $this->settings->deployment_failure_discord_notifications;
|
||||||
$this->statusChangeDiscordNotifications = $this->settings->status_change_discord_notifications;
|
$this->statusChangeDiscordNotifications = $this->settings->status_change_discord_notifications;
|
||||||
|
$this->restartLimitReachedDiscordNotifications = $this->settings->restart_limit_reached_discord_notifications;
|
||||||
$this->backupSuccessDiscordNotifications = $this->settings->backup_success_discord_notifications;
|
$this->backupSuccessDiscordNotifications = $this->settings->backup_success_discord_notifications;
|
||||||
$this->backupFailureDiscordNotifications = $this->settings->backup_failure_discord_notifications;
|
$this->backupFailureDiscordNotifications = $this->settings->backup_failure_discord_notifications;
|
||||||
$this->scheduledTaskSuccessDiscordNotifications = $this->settings->scheduled_task_success_discord_notifications;
|
$this->scheduledTaskSuccessDiscordNotifications = $this->settings->scheduled_task_success_discord_notifications;
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,9 @@ class Email extends Component
|
||||||
#[Validate(['boolean'])]
|
#[Validate(['boolean'])]
|
||||||
public bool $statusChangeEmailNotifications = false;
|
public bool $statusChangeEmailNotifications = false;
|
||||||
|
|
||||||
|
#[Validate(['boolean'])]
|
||||||
|
public bool $restartLimitReachedEmailNotifications = true;
|
||||||
|
|
||||||
#[Validate(['boolean'])]
|
#[Validate(['boolean'])]
|
||||||
public bool $backupSuccessEmailNotifications = false;
|
public bool $backupSuccessEmailNotifications = false;
|
||||||
|
|
||||||
|
|
@ -155,6 +158,7 @@ public function syncData(bool $toModel = false)
|
||||||
$this->settings->deployment_success_email_notifications = $this->deploymentSuccessEmailNotifications;
|
$this->settings->deployment_success_email_notifications = $this->deploymentSuccessEmailNotifications;
|
||||||
$this->settings->deployment_failure_email_notifications = $this->deploymentFailureEmailNotifications;
|
$this->settings->deployment_failure_email_notifications = $this->deploymentFailureEmailNotifications;
|
||||||
$this->settings->status_change_email_notifications = $this->statusChangeEmailNotifications;
|
$this->settings->status_change_email_notifications = $this->statusChangeEmailNotifications;
|
||||||
|
$this->settings->restart_limit_reached_email_notifications = $this->restartLimitReachedEmailNotifications;
|
||||||
$this->settings->backup_success_email_notifications = $this->backupSuccessEmailNotifications;
|
$this->settings->backup_success_email_notifications = $this->backupSuccessEmailNotifications;
|
||||||
$this->settings->backup_failure_email_notifications = $this->backupFailureEmailNotifications;
|
$this->settings->backup_failure_email_notifications = $this->backupFailureEmailNotifications;
|
||||||
$this->settings->scheduled_task_success_email_notifications = $this->scheduledTaskSuccessEmailNotifications;
|
$this->settings->scheduled_task_success_email_notifications = $this->scheduledTaskSuccessEmailNotifications;
|
||||||
|
|
@ -193,6 +197,7 @@ public function syncData(bool $toModel = false)
|
||||||
$this->deploymentSuccessEmailNotifications = $this->settings->deployment_success_email_notifications;
|
$this->deploymentSuccessEmailNotifications = $this->settings->deployment_success_email_notifications;
|
||||||
$this->deploymentFailureEmailNotifications = $this->settings->deployment_failure_email_notifications;
|
$this->deploymentFailureEmailNotifications = $this->settings->deployment_failure_email_notifications;
|
||||||
$this->statusChangeEmailNotifications = $this->settings->status_change_email_notifications;
|
$this->statusChangeEmailNotifications = $this->settings->status_change_email_notifications;
|
||||||
|
$this->restartLimitReachedEmailNotifications = $this->settings->restart_limit_reached_email_notifications;
|
||||||
$this->backupSuccessEmailNotifications = $this->settings->backup_success_email_notifications;
|
$this->backupSuccessEmailNotifications = $this->settings->backup_success_email_notifications;
|
||||||
$this->backupFailureEmailNotifications = $this->settings->backup_failure_email_notifications;
|
$this->backupFailureEmailNotifications = $this->settings->backup_failure_email_notifications;
|
||||||
$this->scheduledTaskSuccessEmailNotifications = $this->settings->scheduled_task_success_email_notifications;
|
$this->scheduledTaskSuccessEmailNotifications = $this->settings->scheduled_task_success_email_notifications;
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,9 @@ class Pushover extends Component
|
||||||
#[Validate(['boolean'])]
|
#[Validate(['boolean'])]
|
||||||
public bool $statusChangePushoverNotifications = false;
|
public bool $statusChangePushoverNotifications = false;
|
||||||
|
|
||||||
|
#[Validate(['boolean'])]
|
||||||
|
public bool $restartLimitReachedPushoverNotifications = true;
|
||||||
|
|
||||||
#[Validate(['boolean'])]
|
#[Validate(['boolean'])]
|
||||||
public bool $backupSuccessPushoverNotifications = false;
|
public bool $backupSuccessPushoverNotifications = false;
|
||||||
|
|
||||||
|
|
@ -98,6 +101,7 @@ public function syncData(bool $toModel = false)
|
||||||
$this->settings->deployment_success_pushover_notifications = $this->deploymentSuccessPushoverNotifications;
|
$this->settings->deployment_success_pushover_notifications = $this->deploymentSuccessPushoverNotifications;
|
||||||
$this->settings->deployment_failure_pushover_notifications = $this->deploymentFailurePushoverNotifications;
|
$this->settings->deployment_failure_pushover_notifications = $this->deploymentFailurePushoverNotifications;
|
||||||
$this->settings->status_change_pushover_notifications = $this->statusChangePushoverNotifications;
|
$this->settings->status_change_pushover_notifications = $this->statusChangePushoverNotifications;
|
||||||
|
$this->settings->restart_limit_reached_pushover_notifications = $this->restartLimitReachedPushoverNotifications;
|
||||||
$this->settings->backup_success_pushover_notifications = $this->backupSuccessPushoverNotifications;
|
$this->settings->backup_success_pushover_notifications = $this->backupSuccessPushoverNotifications;
|
||||||
$this->settings->backup_failure_pushover_notifications = $this->backupFailurePushoverNotifications;
|
$this->settings->backup_failure_pushover_notifications = $this->backupFailurePushoverNotifications;
|
||||||
$this->settings->scheduled_task_success_pushover_notifications = $this->scheduledTaskSuccessPushoverNotifications;
|
$this->settings->scheduled_task_success_pushover_notifications = $this->scheduledTaskSuccessPushoverNotifications;
|
||||||
|
|
@ -125,6 +129,7 @@ public function syncData(bool $toModel = false)
|
||||||
$this->deploymentSuccessPushoverNotifications = $this->settings->deployment_success_pushover_notifications;
|
$this->deploymentSuccessPushoverNotifications = $this->settings->deployment_success_pushover_notifications;
|
||||||
$this->deploymentFailurePushoverNotifications = $this->settings->deployment_failure_pushover_notifications;
|
$this->deploymentFailurePushoverNotifications = $this->settings->deployment_failure_pushover_notifications;
|
||||||
$this->statusChangePushoverNotifications = $this->settings->status_change_pushover_notifications;
|
$this->statusChangePushoverNotifications = $this->settings->status_change_pushover_notifications;
|
||||||
|
$this->restartLimitReachedPushoverNotifications = $this->settings->restart_limit_reached_pushover_notifications;
|
||||||
$this->backupSuccessPushoverNotifications = $this->settings->backup_success_pushover_notifications;
|
$this->backupSuccessPushoverNotifications = $this->settings->backup_success_pushover_notifications;
|
||||||
$this->backupFailurePushoverNotifications = $this->settings->backup_failure_pushover_notifications;
|
$this->backupFailurePushoverNotifications = $this->settings->backup_failure_pushover_notifications;
|
||||||
$this->scheduledTaskSuccessPushoverNotifications = $this->settings->scheduled_task_success_pushover_notifications;
|
$this->scheduledTaskSuccessPushoverNotifications = $this->settings->scheduled_task_success_pushover_notifications;
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,9 @@ class Slack extends Component
|
||||||
#[Validate(['boolean'])]
|
#[Validate(['boolean'])]
|
||||||
public bool $statusChangeSlackNotifications = false;
|
public bool $statusChangeSlackNotifications = false;
|
||||||
|
|
||||||
|
#[Validate(['boolean'])]
|
||||||
|
public bool $restartLimitReachedSlackNotifications = true;
|
||||||
|
|
||||||
#[Validate(['boolean'])]
|
#[Validate(['boolean'])]
|
||||||
public bool $backupSuccessSlackNotifications = false;
|
public bool $backupSuccessSlackNotifications = false;
|
||||||
|
|
||||||
|
|
@ -95,6 +98,7 @@ public function syncData(bool $toModel = false)
|
||||||
$this->settings->deployment_success_slack_notifications = $this->deploymentSuccessSlackNotifications;
|
$this->settings->deployment_success_slack_notifications = $this->deploymentSuccessSlackNotifications;
|
||||||
$this->settings->deployment_failure_slack_notifications = $this->deploymentFailureSlackNotifications;
|
$this->settings->deployment_failure_slack_notifications = $this->deploymentFailureSlackNotifications;
|
||||||
$this->settings->status_change_slack_notifications = $this->statusChangeSlackNotifications;
|
$this->settings->status_change_slack_notifications = $this->statusChangeSlackNotifications;
|
||||||
|
$this->settings->restart_limit_reached_slack_notifications = $this->restartLimitReachedSlackNotifications;
|
||||||
$this->settings->backup_success_slack_notifications = $this->backupSuccessSlackNotifications;
|
$this->settings->backup_success_slack_notifications = $this->backupSuccessSlackNotifications;
|
||||||
$this->settings->backup_failure_slack_notifications = $this->backupFailureSlackNotifications;
|
$this->settings->backup_failure_slack_notifications = $this->backupFailureSlackNotifications;
|
||||||
$this->settings->scheduled_task_success_slack_notifications = $this->scheduledTaskSuccessSlackNotifications;
|
$this->settings->scheduled_task_success_slack_notifications = $this->scheduledTaskSuccessSlackNotifications;
|
||||||
|
|
@ -118,6 +122,7 @@ public function syncData(bool $toModel = false)
|
||||||
$this->deploymentSuccessSlackNotifications = $this->settings->deployment_success_slack_notifications;
|
$this->deploymentSuccessSlackNotifications = $this->settings->deployment_success_slack_notifications;
|
||||||
$this->deploymentFailureSlackNotifications = $this->settings->deployment_failure_slack_notifications;
|
$this->deploymentFailureSlackNotifications = $this->settings->deployment_failure_slack_notifications;
|
||||||
$this->statusChangeSlackNotifications = $this->settings->status_change_slack_notifications;
|
$this->statusChangeSlackNotifications = $this->settings->status_change_slack_notifications;
|
||||||
|
$this->restartLimitReachedSlackNotifications = $this->settings->restart_limit_reached_slack_notifications;
|
||||||
$this->backupSuccessSlackNotifications = $this->settings->backup_success_slack_notifications;
|
$this->backupSuccessSlackNotifications = $this->settings->backup_success_slack_notifications;
|
||||||
$this->backupFailureSlackNotifications = $this->settings->backup_failure_slack_notifications;
|
$this->backupFailureSlackNotifications = $this->settings->backup_failure_slack_notifications;
|
||||||
$this->scheduledTaskSuccessSlackNotifications = $this->settings->scheduled_task_success_slack_notifications;
|
$this->scheduledTaskSuccessSlackNotifications = $this->settings->scheduled_task_success_slack_notifications;
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,9 @@ class Telegram extends Component
|
||||||
#[Validate(['boolean'])]
|
#[Validate(['boolean'])]
|
||||||
public bool $statusChangeTelegramNotifications = false;
|
public bool $statusChangeTelegramNotifications = false;
|
||||||
|
|
||||||
|
#[Validate(['boolean'])]
|
||||||
|
public bool $restartLimitReachedTelegramNotifications = true;
|
||||||
|
|
||||||
#[Validate(['boolean'])]
|
#[Validate(['boolean'])]
|
||||||
public bool $backupSuccessTelegramNotifications = false;
|
public bool $backupSuccessTelegramNotifications = false;
|
||||||
|
|
||||||
|
|
@ -83,6 +86,9 @@ class Telegram extends Component
|
||||||
#[Validate(['nullable', 'string'])]
|
#[Validate(['nullable', 'string'])]
|
||||||
public ?string $telegramNotificationsStatusChangeThreadId = null;
|
public ?string $telegramNotificationsStatusChangeThreadId = null;
|
||||||
|
|
||||||
|
#[Validate(['nullable', 'string', 'max:255'])]
|
||||||
|
public ?string $telegramNotificationsRestartLimitReachedThreadId = null;
|
||||||
|
|
||||||
#[Validate(['nullable', 'string'])]
|
#[Validate(['nullable', 'string'])]
|
||||||
public ?string $telegramNotificationsBackupSuccessThreadId = null;
|
public ?string $telegramNotificationsBackupSuccessThreadId = null;
|
||||||
|
|
||||||
|
|
@ -140,6 +146,7 @@ public function syncData(bool $toModel = false)
|
||||||
$this->settings->deployment_success_telegram_notifications = $this->deploymentSuccessTelegramNotifications;
|
$this->settings->deployment_success_telegram_notifications = $this->deploymentSuccessTelegramNotifications;
|
||||||
$this->settings->deployment_failure_telegram_notifications = $this->deploymentFailureTelegramNotifications;
|
$this->settings->deployment_failure_telegram_notifications = $this->deploymentFailureTelegramNotifications;
|
||||||
$this->settings->status_change_telegram_notifications = $this->statusChangeTelegramNotifications;
|
$this->settings->status_change_telegram_notifications = $this->statusChangeTelegramNotifications;
|
||||||
|
$this->settings->restart_limit_reached_telegram_notifications = $this->restartLimitReachedTelegramNotifications;
|
||||||
$this->settings->backup_success_telegram_notifications = $this->backupSuccessTelegramNotifications;
|
$this->settings->backup_success_telegram_notifications = $this->backupSuccessTelegramNotifications;
|
||||||
$this->settings->backup_failure_telegram_notifications = $this->backupFailureTelegramNotifications;
|
$this->settings->backup_failure_telegram_notifications = $this->backupFailureTelegramNotifications;
|
||||||
$this->settings->scheduled_task_success_telegram_notifications = $this->scheduledTaskSuccessTelegramNotifications;
|
$this->settings->scheduled_task_success_telegram_notifications = $this->scheduledTaskSuccessTelegramNotifications;
|
||||||
|
|
@ -155,6 +162,7 @@ public function syncData(bool $toModel = false)
|
||||||
$this->settings->telegram_notifications_deployment_success_thread_id = $this->telegramNotificationsDeploymentSuccessThreadId;
|
$this->settings->telegram_notifications_deployment_success_thread_id = $this->telegramNotificationsDeploymentSuccessThreadId;
|
||||||
$this->settings->telegram_notifications_deployment_failure_thread_id = $this->telegramNotificationsDeploymentFailureThreadId;
|
$this->settings->telegram_notifications_deployment_failure_thread_id = $this->telegramNotificationsDeploymentFailureThreadId;
|
||||||
$this->settings->telegram_notifications_status_change_thread_id = $this->telegramNotificationsStatusChangeThreadId;
|
$this->settings->telegram_notifications_status_change_thread_id = $this->telegramNotificationsStatusChangeThreadId;
|
||||||
|
$this->settings->telegram_notifications_restart_limit_reached_thread_id = $this->telegramNotificationsRestartLimitReachedThreadId;
|
||||||
$this->settings->telegram_notifications_backup_success_thread_id = $this->telegramNotificationsBackupSuccessThreadId;
|
$this->settings->telegram_notifications_backup_success_thread_id = $this->telegramNotificationsBackupSuccessThreadId;
|
||||||
$this->settings->telegram_notifications_backup_failure_thread_id = $this->telegramNotificationsBackupFailureThreadId;
|
$this->settings->telegram_notifications_backup_failure_thread_id = $this->telegramNotificationsBackupFailureThreadId;
|
||||||
$this->settings->telegram_notifications_scheduled_task_success_thread_id = $this->telegramNotificationsScheduledTaskSuccessThreadId;
|
$this->settings->telegram_notifications_scheduled_task_success_thread_id = $this->telegramNotificationsScheduledTaskSuccessThreadId;
|
||||||
|
|
@ -173,6 +181,21 @@ public function syncData(bool $toModel = false)
|
||||||
if (auth()->user()->can('update', $this->settings)) {
|
if (auth()->user()->can('update', $this->settings)) {
|
||||||
$this->telegramToken = $this->settings->telegram_token;
|
$this->telegramToken = $this->settings->telegram_token;
|
||||||
$this->telegramChatId = $this->settings->telegram_chat_id;
|
$this->telegramChatId = $this->settings->telegram_chat_id;
|
||||||
|
$this->telegramNotificationsDeploymentSuccessThreadId = $this->settings->telegram_notifications_deployment_success_thread_id;
|
||||||
|
$this->telegramNotificationsDeploymentFailureThreadId = $this->settings->telegram_notifications_deployment_failure_thread_id;
|
||||||
|
$this->telegramNotificationsStatusChangeThreadId = $this->settings->telegram_notifications_status_change_thread_id;
|
||||||
|
$this->telegramNotificationsRestartLimitReachedThreadId = $this->settings->telegram_notifications_restart_limit_reached_thread_id;
|
||||||
|
$this->telegramNotificationsBackupSuccessThreadId = $this->settings->telegram_notifications_backup_success_thread_id;
|
||||||
|
$this->telegramNotificationsBackupFailureThreadId = $this->settings->telegram_notifications_backup_failure_thread_id;
|
||||||
|
$this->telegramNotificationsScheduledTaskSuccessThreadId = $this->settings->telegram_notifications_scheduled_task_success_thread_id;
|
||||||
|
$this->telegramNotificationsScheduledTaskFailureThreadId = $this->settings->telegram_notifications_scheduled_task_failure_thread_id;
|
||||||
|
$this->telegramNotificationsDockerCleanupSuccessThreadId = $this->settings->telegram_notifications_docker_cleanup_success_thread_id;
|
||||||
|
$this->telegramNotificationsDockerCleanupFailureThreadId = $this->settings->telegram_notifications_docker_cleanup_failure_thread_id;
|
||||||
|
$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;
|
||||||
|
$this->telegramNotificationsTraefikOutdatedThreadId = $this->settings->telegram_notifications_traefik_outdated_thread_id;
|
||||||
} else {
|
} else {
|
||||||
$this->telegramToken = null;
|
$this->telegramToken = null;
|
||||||
$this->telegramChatId = null;
|
$this->telegramChatId = null;
|
||||||
|
|
@ -181,6 +204,7 @@ public function syncData(bool $toModel = false)
|
||||||
$this->deploymentSuccessTelegramNotifications = $this->settings->deployment_success_telegram_notifications;
|
$this->deploymentSuccessTelegramNotifications = $this->settings->deployment_success_telegram_notifications;
|
||||||
$this->deploymentFailureTelegramNotifications = $this->settings->deployment_failure_telegram_notifications;
|
$this->deploymentFailureTelegramNotifications = $this->settings->deployment_failure_telegram_notifications;
|
||||||
$this->statusChangeTelegramNotifications = $this->settings->status_change_telegram_notifications;
|
$this->statusChangeTelegramNotifications = $this->settings->status_change_telegram_notifications;
|
||||||
|
$this->restartLimitReachedTelegramNotifications = $this->settings->restart_limit_reached_telegram_notifications;
|
||||||
$this->backupSuccessTelegramNotifications = $this->settings->backup_success_telegram_notifications;
|
$this->backupSuccessTelegramNotifications = $this->settings->backup_success_telegram_notifications;
|
||||||
$this->backupFailureTelegramNotifications = $this->settings->backup_failure_telegram_notifications;
|
$this->backupFailureTelegramNotifications = $this->settings->backup_failure_telegram_notifications;
|
||||||
$this->scheduledTaskSuccessTelegramNotifications = $this->settings->scheduled_task_success_telegram_notifications;
|
$this->scheduledTaskSuccessTelegramNotifications = $this->settings->scheduled_task_success_telegram_notifications;
|
||||||
|
|
@ -193,20 +217,6 @@ public function syncData(bool $toModel = false)
|
||||||
$this->serverPatchTelegramNotifications = $this->settings->server_patch_telegram_notifications;
|
$this->serverPatchTelegramNotifications = $this->settings->server_patch_telegram_notifications;
|
||||||
$this->traefikOutdatedTelegramNotifications = $this->settings->traefik_outdated_telegram_notifications;
|
$this->traefikOutdatedTelegramNotifications = $this->settings->traefik_outdated_telegram_notifications;
|
||||||
|
|
||||||
$this->telegramNotificationsDeploymentSuccessThreadId = $this->settings->telegram_notifications_deployment_success_thread_id;
|
|
||||||
$this->telegramNotificationsDeploymentFailureThreadId = $this->settings->telegram_notifications_deployment_failure_thread_id;
|
|
||||||
$this->telegramNotificationsStatusChangeThreadId = $this->settings->telegram_notifications_status_change_thread_id;
|
|
||||||
$this->telegramNotificationsBackupSuccessThreadId = $this->settings->telegram_notifications_backup_success_thread_id;
|
|
||||||
$this->telegramNotificationsBackupFailureThreadId = $this->settings->telegram_notifications_backup_failure_thread_id;
|
|
||||||
$this->telegramNotificationsScheduledTaskSuccessThreadId = $this->settings->telegram_notifications_scheduled_task_success_thread_id;
|
|
||||||
$this->telegramNotificationsScheduledTaskFailureThreadId = $this->settings->telegram_notifications_scheduled_task_failure_thread_id;
|
|
||||||
$this->telegramNotificationsDockerCleanupSuccessThreadId = $this->settings->telegram_notifications_docker_cleanup_success_thread_id;
|
|
||||||
$this->telegramNotificationsDockerCleanupFailureThreadId = $this->settings->telegram_notifications_docker_cleanup_failure_thread_id;
|
|
||||||
$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;
|
|
||||||
$this->telegramNotificationsTraefikOutdatedThreadId = $this->settings->telegram_notifications_traefik_outdated_thread_id;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,9 @@ class Webhook extends Component
|
||||||
#[Validate(['boolean'])]
|
#[Validate(['boolean'])]
|
||||||
public bool $statusChangeWebhookNotifications = false;
|
public bool $statusChangeWebhookNotifications = false;
|
||||||
|
|
||||||
|
#[Validate(['boolean'])]
|
||||||
|
public bool $restartLimitReachedWebhookNotifications = true;
|
||||||
|
|
||||||
#[Validate(['boolean'])]
|
#[Validate(['boolean'])]
|
||||||
public bool $backupSuccessWebhookNotifications = false;
|
public bool $backupSuccessWebhookNotifications = false;
|
||||||
|
|
||||||
|
|
@ -90,6 +93,7 @@ public function syncData(bool $toModel = false)
|
||||||
$this->settings->deployment_success_webhook_notifications = $this->deploymentSuccessWebhookNotifications;
|
$this->settings->deployment_success_webhook_notifications = $this->deploymentSuccessWebhookNotifications;
|
||||||
$this->settings->deployment_failure_webhook_notifications = $this->deploymentFailureWebhookNotifications;
|
$this->settings->deployment_failure_webhook_notifications = $this->deploymentFailureWebhookNotifications;
|
||||||
$this->settings->status_change_webhook_notifications = $this->statusChangeWebhookNotifications;
|
$this->settings->status_change_webhook_notifications = $this->statusChangeWebhookNotifications;
|
||||||
|
$this->settings->restart_limit_reached_webhook_notifications = $this->restartLimitReachedWebhookNotifications;
|
||||||
$this->settings->backup_success_webhook_notifications = $this->backupSuccessWebhookNotifications;
|
$this->settings->backup_success_webhook_notifications = $this->backupSuccessWebhookNotifications;
|
||||||
$this->settings->backup_failure_webhook_notifications = $this->backupFailureWebhookNotifications;
|
$this->settings->backup_failure_webhook_notifications = $this->backupFailureWebhookNotifications;
|
||||||
$this->settings->scheduled_task_success_webhook_notifications = $this->scheduledTaskSuccessWebhookNotifications;
|
$this->settings->scheduled_task_success_webhook_notifications = $this->scheduledTaskSuccessWebhookNotifications;
|
||||||
|
|
@ -113,6 +117,7 @@ public function syncData(bool $toModel = false)
|
||||||
$this->deploymentSuccessWebhookNotifications = $this->settings->deployment_success_webhook_notifications;
|
$this->deploymentSuccessWebhookNotifications = $this->settings->deployment_success_webhook_notifications;
|
||||||
$this->deploymentFailureWebhookNotifications = $this->settings->deployment_failure_webhook_notifications;
|
$this->deploymentFailureWebhookNotifications = $this->settings->deployment_failure_webhook_notifications;
|
||||||
$this->statusChangeWebhookNotifications = $this->settings->status_change_webhook_notifications;
|
$this->statusChangeWebhookNotifications = $this->settings->status_change_webhook_notifications;
|
||||||
|
$this->restartLimitReachedWebhookNotifications = $this->settings->restart_limit_reached_webhook_notifications;
|
||||||
$this->backupSuccessWebhookNotifications = $this->settings->backup_success_webhook_notifications;
|
$this->backupSuccessWebhookNotifications = $this->settings->backup_success_webhook_notifications;
|
||||||
$this->backupFailureWebhookNotifications = $this->settings->backup_failure_webhook_notifications;
|
$this->backupFailureWebhookNotifications = $this->settings->backup_failure_webhook_notifications;
|
||||||
$this->scheduledTaskSuccessWebhookNotifications = $this->settings->scheduled_task_success_webhook_notifications;
|
$this->scheduledTaskSuccessWebhookNotifications = $this->settings->scheduled_task_success_webhook_notifications;
|
||||||
|
|
|
||||||
|
|
@ -187,6 +187,11 @@ private function toSearchableArray(Collection $items, string $type, string $type
|
||||||
'fqdn' => $item->fqdn ?? null,
|
'fqdn' => $item->fqdn ?? null,
|
||||||
'description' => $item->description ?? null,
|
'description' => $item->description ?? null,
|
||||||
'status' => $item->status ?? '',
|
'status' => $item->status ?? '',
|
||||||
|
'restartLimitReached' => method_exists($item, 'stoppedAfterRestartLimit') && $item->stoppedAfterRestartLimit(),
|
||||||
|
'restartCount' => method_exists($item, 'stoppedAfterRestartLimit') && $item->stoppedAfterRestartLimit()
|
||||||
|
? max($item->restart_count ?? 0, $item->max_restart_count ?? 0)
|
||||||
|
: ($item->restart_count ?? 0),
|
||||||
|
'maxRestartCount' => $item->max_restart_count ?? 0,
|
||||||
'server_status' => $item->server_status ?? null,
|
'server_status' => $item->server_status ?? null,
|
||||||
'hrefLink' => $item->hrefLink ?? '',
|
'hrefLink' => $item->hrefLink ?? '',
|
||||||
'destination' => [
|
'destination' => [
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,11 @@
|
||||||
use App\Actions\Docker\GetContainersStatus;
|
use App\Actions\Docker\GetContainersStatus;
|
||||||
use App\Actions\Service\StartService;
|
use App\Actions\Service\StartService;
|
||||||
use App\Actions\Service\StopService;
|
use App\Actions\Service\StopService;
|
||||||
|
use App\Actions\Service\StopServiceApplication;
|
||||||
use App\Enums\ProcessStatus;
|
use App\Enums\ProcessStatus;
|
||||||
use App\Models\Service;
|
use App\Models\Service;
|
||||||
|
use App\Models\ServiceApplication;
|
||||||
|
use App\Models\ServiceDatabase;
|
||||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
@ -169,6 +172,29 @@ public function restart()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function removeSelectedResourceContainer(): void
|
||||||
|
{
|
||||||
|
$resource = $this->selectedResource();
|
||||||
|
if (! $resource) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->authorize('update', $resource);
|
||||||
|
StopServiceApplication::run($resource, true, true);
|
||||||
|
$this->dispatch('success', 'Container removed.');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function selectedResource(): ServiceApplication|ServiceDatabase|null
|
||||||
|
{
|
||||||
|
$uuid = data_get($this->parameters, 'stack_service_uuid');
|
||||||
|
if (! $uuid) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->service->applications()->whereUuid($uuid)->first()
|
||||||
|
?? $this->service->databases()->whereUuid($uuid)->first();
|
||||||
|
}
|
||||||
|
|
||||||
public function pullAndRestartEvent()
|
public function pullAndRestartEvent()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,13 @@ class Status extends Component
|
||||||
{
|
{
|
||||||
public Service $service;
|
public Service $service;
|
||||||
|
|
||||||
|
public ?string $selectedResourceUuid = null;
|
||||||
|
|
||||||
|
public function mount(): void
|
||||||
|
{
|
||||||
|
$this->selectedResourceUuid = request()->route('stack_service_uuid');
|
||||||
|
}
|
||||||
|
|
||||||
public function getListeners(): array
|
public function getListeners(): array
|
||||||
{
|
{
|
||||||
$teamId = auth()->user()->currentTeam()->id;
|
$teamId = auth()->user()->currentTeam()->id;
|
||||||
|
|
@ -27,6 +34,11 @@ public function refreshStatus(): void
|
||||||
|
|
||||||
public function render(): View
|
public function render(): View
|
||||||
{
|
{
|
||||||
return view('livewire.project.service.status');
|
$selectedResource = $this->selectedResourceUuid
|
||||||
|
? $this->service->applications->firstWhere('uuid', $this->selectedResourceUuid)
|
||||||
|
?? $this->service->databases->firstWhere('uuid', $this->selectedResourceUuid)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return view('livewire.project.service.status', compact('selectedResource'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -213,6 +213,8 @@ class Application extends BaseModel
|
||||||
'last_online_at',
|
'last_online_at',
|
||||||
'restart_count',
|
'restart_count',
|
||||||
'max_restart_count',
|
'max_restart_count',
|
||||||
|
'restart_limit_reached',
|
||||||
|
'container_present',
|
||||||
'last_restart_at',
|
'last_restart_at',
|
||||||
'last_restart_type',
|
'last_restart_type',
|
||||||
'uuid',
|
'uuid',
|
||||||
|
|
@ -258,6 +260,8 @@ protected function casts(): array
|
||||||
'domain_dns_statuses' => 'array',
|
'domain_dns_statuses' => 'array',
|
||||||
'restart_count' => 'integer',
|
'restart_count' => 'integer',
|
||||||
'max_restart_count' => 'integer',
|
'max_restart_count' => 'integer',
|
||||||
|
'restart_limit_reached' => 'boolean',
|
||||||
|
'container_present' => 'boolean',
|
||||||
'last_restart_at' => 'datetime',
|
'last_restart_at' => 'datetime',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
@ -605,10 +609,8 @@ public function link()
|
||||||
public function stoppedAfterRestartLimit(): bool
|
public function stoppedAfterRestartLimit(): bool
|
||||||
{
|
{
|
||||||
return str($this->status)->startsWith('exited')
|
return str($this->status)->startsWith('exited')
|
||||||
&& ($this->restart_count ?? 0) > 0
|
&& $this->container_present === true
|
||||||
&& ($this->max_restart_count ?? 0) > 0
|
&& $this->restart_limit_reached === true;
|
||||||
&& $this->restart_count >= $this->max_restart_count
|
|
||||||
&& $this->last_restart_type === 'crash';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function taskLink($task_uuid)
|
public function taskLink($task_uuid)
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,14 @@
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Support\ValidationPatterns;
|
use App\Support\ValidationPatterns;
|
||||||
|
use App\Traits\HasRestartLimit;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
use Spatie\Url\Url;
|
use Spatie\Url\Url;
|
||||||
|
|
||||||
class ApplicationPreview extends BaseModel
|
class ApplicationPreview extends BaseModel
|
||||||
{
|
{
|
||||||
use SoftDeletes;
|
use HasRestartLimit, SoftDeletes;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'uuid',
|
'uuid',
|
||||||
|
|
@ -102,6 +103,11 @@ public function application()
|
||||||
return $this->belongsTo(Application::class);
|
return $this->belongsTo(Application::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function restartLimitMaximum(): int
|
||||||
|
{
|
||||||
|
return $this->application->max_restart_count ?? $this->max_restart_count ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
public function persistentStorages()
|
public function persistentStorages()
|
||||||
{
|
{
|
||||||
return $this->morphMany(LocalPersistentVolume::class, 'resource');
|
return $this->morphMany(LocalPersistentVolume::class, 'resource');
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ class DiscordNotificationSettings extends Model
|
||||||
'deployment_success_discord_notifications',
|
'deployment_success_discord_notifications',
|
||||||
'deployment_failure_discord_notifications',
|
'deployment_failure_discord_notifications',
|
||||||
'status_change_discord_notifications',
|
'status_change_discord_notifications',
|
||||||
|
'restart_limit_reached_discord_notifications',
|
||||||
'backup_success_discord_notifications',
|
'backup_success_discord_notifications',
|
||||||
'backup_failure_discord_notifications',
|
'backup_failure_discord_notifications',
|
||||||
'scheduled_task_success_discord_notifications',
|
'scheduled_task_success_discord_notifications',
|
||||||
|
|
@ -45,6 +46,7 @@ class DiscordNotificationSettings extends Model
|
||||||
'deployment_success_discord_notifications' => 'boolean',
|
'deployment_success_discord_notifications' => 'boolean',
|
||||||
'deployment_failure_discord_notifications' => 'boolean',
|
'deployment_failure_discord_notifications' => 'boolean',
|
||||||
'status_change_discord_notifications' => 'boolean',
|
'status_change_discord_notifications' => 'boolean',
|
||||||
|
'restart_limit_reached_discord_notifications' => 'boolean',
|
||||||
'backup_success_discord_notifications' => 'boolean',
|
'backup_success_discord_notifications' => 'boolean',
|
||||||
'backup_failure_discord_notifications' => 'boolean',
|
'backup_failure_discord_notifications' => 'boolean',
|
||||||
'scheduled_task_success_discord_notifications' => 'boolean',
|
'scheduled_task_success_discord_notifications' => 'boolean',
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ class EmailNotificationSettings extends Model
|
||||||
'deployment_success_email_notifications',
|
'deployment_success_email_notifications',
|
||||||
'deployment_failure_email_notifications',
|
'deployment_failure_email_notifications',
|
||||||
'status_change_email_notifications',
|
'status_change_email_notifications',
|
||||||
|
'restart_limit_reached_email_notifications',
|
||||||
'backup_success_email_notifications',
|
'backup_success_email_notifications',
|
||||||
'backup_failure_email_notifications',
|
'backup_failure_email_notifications',
|
||||||
'scheduled_task_success_email_notifications',
|
'scheduled_task_success_email_notifications',
|
||||||
|
|
@ -73,6 +74,7 @@ class EmailNotificationSettings extends Model
|
||||||
'deployment_success_email_notifications' => 'boolean',
|
'deployment_success_email_notifications' => 'boolean',
|
||||||
'deployment_failure_email_notifications' => 'boolean',
|
'deployment_failure_email_notifications' => 'boolean',
|
||||||
'status_change_email_notifications' => 'boolean',
|
'status_change_email_notifications' => 'boolean',
|
||||||
|
'restart_limit_reached_email_notifications' => 'boolean',
|
||||||
'backup_success_email_notifications' => 'boolean',
|
'backup_success_email_notifications' => 'boolean',
|
||||||
'backup_failure_email_notifications' => 'boolean',
|
'backup_failure_email_notifications' => 'boolean',
|
||||||
'scheduled_task_success_email_notifications' => 'boolean',
|
'scheduled_task_success_email_notifications' => 'boolean',
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ class PushoverNotificationSettings extends Model
|
||||||
'deployment_success_pushover_notifications',
|
'deployment_success_pushover_notifications',
|
||||||
'deployment_failure_pushover_notifications',
|
'deployment_failure_pushover_notifications',
|
||||||
'status_change_pushover_notifications',
|
'status_change_pushover_notifications',
|
||||||
|
'restart_limit_reached_pushover_notifications',
|
||||||
'backup_success_pushover_notifications',
|
'backup_success_pushover_notifications',
|
||||||
'backup_failure_pushover_notifications',
|
'backup_failure_pushover_notifications',
|
||||||
'scheduled_task_success_pushover_notifications',
|
'scheduled_task_success_pushover_notifications',
|
||||||
|
|
@ -47,6 +48,7 @@ class PushoverNotificationSettings extends Model
|
||||||
'deployment_success_pushover_notifications' => 'boolean',
|
'deployment_success_pushover_notifications' => 'boolean',
|
||||||
'deployment_failure_pushover_notifications' => 'boolean',
|
'deployment_failure_pushover_notifications' => 'boolean',
|
||||||
'status_change_pushover_notifications' => 'boolean',
|
'status_change_pushover_notifications' => 'boolean',
|
||||||
|
'restart_limit_reached_pushover_notifications' => 'boolean',
|
||||||
'backup_success_pushover_notifications' => 'boolean',
|
'backup_success_pushover_notifications' => 'boolean',
|
||||||
'backup_failure_pushover_notifications' => 'boolean',
|
'backup_failure_pushover_notifications' => 'boolean',
|
||||||
'scheduled_task_success_pushover_notifications' => 'boolean',
|
'scheduled_task_success_pushover_notifications' => 'boolean',
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Traits\HasNoindexDomains;
|
use App\Traits\HasNoindexDomains;
|
||||||
|
use App\Traits\HasRestartLimit;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
|
@ -10,7 +11,7 @@
|
||||||
|
|
||||||
class ServiceApplication extends BaseModel
|
class ServiceApplication extends BaseModel
|
||||||
{
|
{
|
||||||
use HasFactory, HasNoindexDomains, SoftDeletes;
|
use HasFactory, HasNoindexDomains, HasRestartLimit, SoftDeletes;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'service_id',
|
'service_id',
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,13 @@
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Traits\HasRestartLimit;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
|
||||||
class ServiceDatabase extends BaseModel
|
class ServiceDatabase extends BaseModel
|
||||||
{
|
{
|
||||||
use HasFactory, SoftDeletes;
|
use HasFactory, HasRestartLimit, SoftDeletes;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'service_id',
|
'service_id',
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ class SlackNotificationSettings extends Model
|
||||||
'deployment_success_slack_notifications',
|
'deployment_success_slack_notifications',
|
||||||
'deployment_failure_slack_notifications',
|
'deployment_failure_slack_notifications',
|
||||||
'status_change_slack_notifications',
|
'status_change_slack_notifications',
|
||||||
|
'restart_limit_reached_slack_notifications',
|
||||||
'backup_success_slack_notifications',
|
'backup_success_slack_notifications',
|
||||||
'backup_failure_slack_notifications',
|
'backup_failure_slack_notifications',
|
||||||
'scheduled_task_success_slack_notifications',
|
'scheduled_task_success_slack_notifications',
|
||||||
|
|
@ -44,6 +45,7 @@ class SlackNotificationSettings extends Model
|
||||||
'deployment_success_slack_notifications' => 'boolean',
|
'deployment_success_slack_notifications' => 'boolean',
|
||||||
'deployment_failure_slack_notifications' => 'boolean',
|
'deployment_failure_slack_notifications' => 'boolean',
|
||||||
'status_change_slack_notifications' => 'boolean',
|
'status_change_slack_notifications' => 'boolean',
|
||||||
|
'restart_limit_reached_slack_notifications' => 'boolean',
|
||||||
'backup_success_slack_notifications' => 'boolean',
|
'backup_success_slack_notifications' => 'boolean',
|
||||||
'backup_failure_slack_notifications' => 'boolean',
|
'backup_failure_slack_notifications' => 'boolean',
|
||||||
'scheduled_task_success_slack_notifications' => 'boolean',
|
'scheduled_task_success_slack_notifications' => 'boolean',
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
use App\Traits\ClearsGlobalSearchCache;
|
use App\Traits\ClearsGlobalSearchCache;
|
||||||
use App\Traits\HasDatabaseHealthCheck;
|
use App\Traits\HasDatabaseHealthCheck;
|
||||||
use App\Traits\HasMetrics;
|
use App\Traits\HasMetrics;
|
||||||
|
use App\Traits\HasRestartLimit;
|
||||||
use App\Traits\HasSafeStringAttribute;
|
use App\Traits\HasSafeStringAttribute;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
|
@ -12,7 +13,7 @@
|
||||||
|
|
||||||
class StandaloneClickhouse extends BaseModel
|
class StandaloneClickhouse extends BaseModel
|
||||||
{
|
{
|
||||||
use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
|
use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasRestartLimit, HasSafeStringAttribute, SoftDeletes;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'uuid',
|
'uuid',
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
use App\Traits\ClearsGlobalSearchCache;
|
use App\Traits\ClearsGlobalSearchCache;
|
||||||
use App\Traits\HasDatabaseHealthCheck;
|
use App\Traits\HasDatabaseHealthCheck;
|
||||||
use App\Traits\HasMetrics;
|
use App\Traits\HasMetrics;
|
||||||
|
use App\Traits\HasRestartLimit;
|
||||||
use App\Traits\HasSafeStringAttribute;
|
use App\Traits\HasSafeStringAttribute;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
|
@ -12,7 +13,7 @@
|
||||||
|
|
||||||
class StandaloneDragonfly extends BaseModel
|
class StandaloneDragonfly extends BaseModel
|
||||||
{
|
{
|
||||||
use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
|
use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasRestartLimit, HasSafeStringAttribute, SoftDeletes;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'uuid',
|
'uuid',
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
use App\Traits\ClearsGlobalSearchCache;
|
use App\Traits\ClearsGlobalSearchCache;
|
||||||
use App\Traits\HasDatabaseHealthCheck;
|
use App\Traits\HasDatabaseHealthCheck;
|
||||||
use App\Traits\HasMetrics;
|
use App\Traits\HasMetrics;
|
||||||
|
use App\Traits\HasRestartLimit;
|
||||||
use App\Traits\HasSafeStringAttribute;
|
use App\Traits\HasSafeStringAttribute;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
|
@ -12,7 +13,7 @@
|
||||||
|
|
||||||
class StandaloneKeydb extends BaseModel
|
class StandaloneKeydb extends BaseModel
|
||||||
{
|
{
|
||||||
use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
|
use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasRestartLimit, HasSafeStringAttribute, SoftDeletes;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'uuid',
|
'uuid',
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
use App\Traits\ClearsGlobalSearchCache;
|
use App\Traits\ClearsGlobalSearchCache;
|
||||||
use App\Traits\HasDatabaseHealthCheck;
|
use App\Traits\HasDatabaseHealthCheck;
|
||||||
use App\Traits\HasMetrics;
|
use App\Traits\HasMetrics;
|
||||||
|
use App\Traits\HasRestartLimit;
|
||||||
use App\Traits\HasSafeStringAttribute;
|
use App\Traits\HasSafeStringAttribute;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
|
@ -13,7 +14,7 @@
|
||||||
|
|
||||||
class StandaloneMariadb extends BaseModel
|
class StandaloneMariadb extends BaseModel
|
||||||
{
|
{
|
||||||
use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
|
use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasRestartLimit, HasSafeStringAttribute, SoftDeletes;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'uuid',
|
'uuid',
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
use App\Traits\ClearsGlobalSearchCache;
|
use App\Traits\ClearsGlobalSearchCache;
|
||||||
use App\Traits\HasDatabaseHealthCheck;
|
use App\Traits\HasDatabaseHealthCheck;
|
||||||
use App\Traits\HasMetrics;
|
use App\Traits\HasMetrics;
|
||||||
|
use App\Traits\HasRestartLimit;
|
||||||
use App\Traits\HasSafeStringAttribute;
|
use App\Traits\HasSafeStringAttribute;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
|
@ -12,7 +13,7 @@
|
||||||
|
|
||||||
class StandaloneMongodb extends BaseModel
|
class StandaloneMongodb extends BaseModel
|
||||||
{
|
{
|
||||||
use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
|
use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasRestartLimit, HasSafeStringAttribute, SoftDeletes;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'uuid',
|
'uuid',
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
use App\Traits\ClearsGlobalSearchCache;
|
use App\Traits\ClearsGlobalSearchCache;
|
||||||
use App\Traits\HasDatabaseHealthCheck;
|
use App\Traits\HasDatabaseHealthCheck;
|
||||||
use App\Traits\HasMetrics;
|
use App\Traits\HasMetrics;
|
||||||
|
use App\Traits\HasRestartLimit;
|
||||||
use App\Traits\HasSafeStringAttribute;
|
use App\Traits\HasSafeStringAttribute;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
|
@ -12,7 +13,7 @@
|
||||||
|
|
||||||
class StandaloneMysql extends BaseModel
|
class StandaloneMysql extends BaseModel
|
||||||
{
|
{
|
||||||
use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
|
use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasRestartLimit, HasSafeStringAttribute, SoftDeletes;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'uuid',
|
'uuid',
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
use App\Traits\ClearsGlobalSearchCache;
|
use App\Traits\ClearsGlobalSearchCache;
|
||||||
use App\Traits\HasDatabaseHealthCheck;
|
use App\Traits\HasDatabaseHealthCheck;
|
||||||
use App\Traits\HasMetrics;
|
use App\Traits\HasMetrics;
|
||||||
|
use App\Traits\HasRestartLimit;
|
||||||
use App\Traits\HasSafeStringAttribute;
|
use App\Traits\HasSafeStringAttribute;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
|
@ -12,7 +13,7 @@
|
||||||
|
|
||||||
class StandalonePostgresql extends BaseModel
|
class StandalonePostgresql extends BaseModel
|
||||||
{
|
{
|
||||||
use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
|
use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasRestartLimit, HasSafeStringAttribute, SoftDeletes;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'uuid',
|
'uuid',
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
use App\Traits\ClearsGlobalSearchCache;
|
use App\Traits\ClearsGlobalSearchCache;
|
||||||
use App\Traits\HasDatabaseHealthCheck;
|
use App\Traits\HasDatabaseHealthCheck;
|
||||||
use App\Traits\HasMetrics;
|
use App\Traits\HasMetrics;
|
||||||
|
use App\Traits\HasRestartLimit;
|
||||||
use App\Traits\HasSafeStringAttribute;
|
use App\Traits\HasSafeStringAttribute;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
|
@ -12,7 +13,7 @@
|
||||||
|
|
||||||
class StandaloneRedis extends BaseModel
|
class StandaloneRedis extends BaseModel
|
||||||
{
|
{
|
||||||
use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
|
use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasRestartLimit, HasSafeStringAttribute, SoftDeletes;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'uuid',
|
'uuid',
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ class TelegramNotificationSettings extends Model
|
||||||
'deployment_success_telegram_notifications',
|
'deployment_success_telegram_notifications',
|
||||||
'deployment_failure_telegram_notifications',
|
'deployment_failure_telegram_notifications',
|
||||||
'status_change_telegram_notifications',
|
'status_change_telegram_notifications',
|
||||||
|
'restart_limit_reached_telegram_notifications',
|
||||||
'backup_success_telegram_notifications',
|
'backup_success_telegram_notifications',
|
||||||
'backup_failure_telegram_notifications',
|
'backup_failure_telegram_notifications',
|
||||||
'scheduled_task_success_telegram_notifications',
|
'scheduled_task_success_telegram_notifications',
|
||||||
|
|
@ -36,6 +37,7 @@ class TelegramNotificationSettings extends Model
|
||||||
'telegram_notifications_deployment_success_thread_id',
|
'telegram_notifications_deployment_success_thread_id',
|
||||||
'telegram_notifications_deployment_failure_thread_id',
|
'telegram_notifications_deployment_failure_thread_id',
|
||||||
'telegram_notifications_status_change_thread_id',
|
'telegram_notifications_status_change_thread_id',
|
||||||
|
'telegram_notifications_restart_limit_reached_thread_id',
|
||||||
'telegram_notifications_backup_success_thread_id',
|
'telegram_notifications_backup_success_thread_id',
|
||||||
'telegram_notifications_backup_failure_thread_id',
|
'telegram_notifications_backup_failure_thread_id',
|
||||||
'telegram_notifications_scheduled_task_success_thread_id',
|
'telegram_notifications_scheduled_task_success_thread_id',
|
||||||
|
|
@ -55,6 +57,7 @@ class TelegramNotificationSettings extends Model
|
||||||
'telegram_notifications_deployment_success_thread_id',
|
'telegram_notifications_deployment_success_thread_id',
|
||||||
'telegram_notifications_deployment_failure_thread_id',
|
'telegram_notifications_deployment_failure_thread_id',
|
||||||
'telegram_notifications_status_change_thread_id',
|
'telegram_notifications_status_change_thread_id',
|
||||||
|
'telegram_notifications_restart_limit_reached_thread_id',
|
||||||
'telegram_notifications_backup_success_thread_id',
|
'telegram_notifications_backup_success_thread_id',
|
||||||
'telegram_notifications_backup_failure_thread_id',
|
'telegram_notifications_backup_failure_thread_id',
|
||||||
'telegram_notifications_scheduled_task_success_thread_id',
|
'telegram_notifications_scheduled_task_success_thread_id',
|
||||||
|
|
@ -76,6 +79,7 @@ class TelegramNotificationSettings extends Model
|
||||||
'deployment_success_telegram_notifications' => 'boolean',
|
'deployment_success_telegram_notifications' => 'boolean',
|
||||||
'deployment_failure_telegram_notifications' => 'boolean',
|
'deployment_failure_telegram_notifications' => 'boolean',
|
||||||
'status_change_telegram_notifications' => 'boolean',
|
'status_change_telegram_notifications' => 'boolean',
|
||||||
|
'restart_limit_reached_telegram_notifications' => 'boolean',
|
||||||
'backup_success_telegram_notifications' => 'boolean',
|
'backup_success_telegram_notifications' => 'boolean',
|
||||||
'backup_failure_telegram_notifications' => 'boolean',
|
'backup_failure_telegram_notifications' => 'boolean',
|
||||||
'scheduled_task_success_telegram_notifications' => 'boolean',
|
'scheduled_task_success_telegram_notifications' => 'boolean',
|
||||||
|
|
@ -90,6 +94,7 @@ class TelegramNotificationSettings extends Model
|
||||||
'telegram_notifications_deployment_success_thread_id' => 'encrypted',
|
'telegram_notifications_deployment_success_thread_id' => 'encrypted',
|
||||||
'telegram_notifications_deployment_failure_thread_id' => 'encrypted',
|
'telegram_notifications_deployment_failure_thread_id' => 'encrypted',
|
||||||
'telegram_notifications_status_change_thread_id' => 'encrypted',
|
'telegram_notifications_status_change_thread_id' => 'encrypted',
|
||||||
|
'telegram_notifications_restart_limit_reached_thread_id' => 'encrypted',
|
||||||
'telegram_notifications_backup_success_thread_id' => 'encrypted',
|
'telegram_notifications_backup_success_thread_id' => 'encrypted',
|
||||||
'telegram_notifications_backup_failure_thread_id' => 'encrypted',
|
'telegram_notifications_backup_failure_thread_id' => 'encrypted',
|
||||||
'telegram_notifications_scheduled_task_success_thread_id' => 'encrypted',
|
'telegram_notifications_scheduled_task_success_thread_id' => 'encrypted',
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ class WebhookNotificationSettings extends Model
|
||||||
'deployment_success_webhook_notifications',
|
'deployment_success_webhook_notifications',
|
||||||
'deployment_failure_webhook_notifications',
|
'deployment_failure_webhook_notifications',
|
||||||
'status_change_webhook_notifications',
|
'status_change_webhook_notifications',
|
||||||
|
'restart_limit_reached_webhook_notifications',
|
||||||
'backup_success_webhook_notifications',
|
'backup_success_webhook_notifications',
|
||||||
'backup_failure_webhook_notifications',
|
'backup_failure_webhook_notifications',
|
||||||
'scheduled_task_success_webhook_notifications',
|
'scheduled_task_success_webhook_notifications',
|
||||||
|
|
@ -46,6 +47,7 @@ protected function casts(): array
|
||||||
'deployment_success_webhook_notifications' => 'boolean',
|
'deployment_success_webhook_notifications' => 'boolean',
|
||||||
'deployment_failure_webhook_notifications' => 'boolean',
|
'deployment_failure_webhook_notifications' => 'boolean',
|
||||||
'status_change_webhook_notifications' => 'boolean',
|
'status_change_webhook_notifications' => 'boolean',
|
||||||
|
'restart_limit_reached_webhook_notifications' => 'boolean',
|
||||||
'backup_success_webhook_notifications' => 'boolean',
|
'backup_success_webhook_notifications' => 'boolean',
|
||||||
'backup_failure_webhook_notifications' => 'boolean',
|
'backup_failure_webhook_notifications' => 'boolean',
|
||||||
'scheduled_task_success_webhook_notifications' => 'boolean',
|
'scheduled_task_success_webhook_notifications' => 'boolean',
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@
|
||||||
|
|
||||||
namespace App\Notifications\Application;
|
namespace App\Notifications\Application;
|
||||||
|
|
||||||
use App\Models\Application;
|
use App\Models\ApplicationPreview;
|
||||||
|
use App\Models\BaseModel;
|
||||||
use App\Notifications\CustomEmailNotification;
|
use App\Notifications\CustomEmailNotification;
|
||||||
use App\Notifications\Dto\DiscordMessage;
|
use App\Notifications\Dto\DiscordMessage;
|
||||||
use App\Notifications\Dto\PushoverMessage;
|
use App\Notifications\Dto\PushoverMessage;
|
||||||
|
|
@ -27,26 +28,40 @@ class RestartLimitReached extends CustomEmailNotification
|
||||||
|
|
||||||
public int $max_restart_count;
|
public int $max_restart_count;
|
||||||
|
|
||||||
public function __construct(public Application $resource)
|
public function __construct(public BaseModel $resource)
|
||||||
{
|
{
|
||||||
$this->onQueue('high');
|
$this->onQueue('high');
|
||||||
$this->afterCommit();
|
$this->afterCommit();
|
||||||
$this->resource_name = data_get($resource, 'name');
|
$environment = data_get($resource, 'environment')
|
||||||
$this->project_uuid = data_get($resource, 'environment.project.uuid');
|
?? data_get($resource, 'application.environment')
|
||||||
$this->environment_uuid = data_get($resource, 'environment.uuid');
|
?? data_get($resource, 'service.environment');
|
||||||
$this->environment_name = data_get($resource, 'environment.name');
|
$this->resource_name = $resource instanceof ApplicationPreview
|
||||||
|
? data_get($resource, 'application.name').' PR #'.$resource->pull_request_id
|
||||||
|
: data_get($resource, 'name');
|
||||||
|
$this->project_uuid = data_get($environment, 'project.uuid');
|
||||||
|
$this->environment_uuid = data_get($environment, 'uuid');
|
||||||
|
$this->environment_name = data_get($environment, 'name');
|
||||||
$this->fqdn = data_get($resource, 'fqdn', null);
|
$this->fqdn = data_get($resource, 'fqdn', null);
|
||||||
$this->restart_count = $resource->restart_count;
|
$this->restart_count = $resource->restart_count;
|
||||||
$this->max_restart_count = $resource->max_restart_count;
|
$this->max_restart_count = method_exists($resource, 'restartLimitMaximum')
|
||||||
|
? $resource->restartLimitMaximum()
|
||||||
|
: $resource->max_restart_count;
|
||||||
if (str($this->fqdn)->explode(',')->count() > 1) {
|
if (str($this->fqdn)->explode(',')->count() > 1) {
|
||||||
$this->fqdn = str($this->fqdn)->explode(',')->first();
|
$this->fqdn = str($this->fqdn)->explode(',')->first();
|
||||||
}
|
}
|
||||||
$this->resource_url = $this->resource->link() ?? base_url()."/project/{$this->project_uuid}/environment/{$this->environment_uuid}/application/{$this->resource->uuid}";
|
$service = data_get($resource, 'service');
|
||||||
|
$this->resource_url = match (true) {
|
||||||
|
method_exists($this->resource, 'link') => $this->resource->link(),
|
||||||
|
$resource instanceof ApplicationPreview => $resource->application->link(),
|
||||||
|
is_object($service) && method_exists($service, 'link') => $service->link(),
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
$this->resource_url ??= base_url()."/project/{$this->project_uuid}/environment/{$this->environment_uuid}";
|
||||||
}
|
}
|
||||||
|
|
||||||
public function via(object $notifiable): array
|
public function via(object $notifiable): array
|
||||||
{
|
{
|
||||||
return $notifiable->getEnabledChannels('status_change');
|
return $notifiable->getEnabledChannels('restart_limit_reached');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function toMail(): MailMessage
|
public function toMail(): MailMessage
|
||||||
|
|
@ -68,7 +83,7 @@ public function toDiscord(): DiscordMessage
|
||||||
{
|
{
|
||||||
return new DiscordMessage(
|
return new DiscordMessage(
|
||||||
title: ':warning: Restart limit reached',
|
title: ':warning: Restart limit reached',
|
||||||
description: "{$this->resource_name} has been stopped after {$this->restart_count} restarts (limit: {$this->max_restart_count}).\n\n[Open Application in Coolify]({$this->resource_url})",
|
description: "{$this->resource_name} has been stopped after {$this->restart_count} restarts (limit: {$this->max_restart_count}).\n\n[Open Resource in Coolify]({$this->resource_url})",
|
||||||
color: DiscordMessage::errorColor(),
|
color: DiscordMessage::errorColor(),
|
||||||
isCritical: true,
|
isCritical: true,
|
||||||
);
|
);
|
||||||
|
|
@ -82,7 +97,7 @@ public function toTelegram(): array
|
||||||
'message' => $message,
|
'message' => $message,
|
||||||
'buttons' => [
|
'buttons' => [
|
||||||
[
|
[
|
||||||
'text' => 'Open Application in Coolify',
|
'text' => 'Open Resource in Coolify',
|
||||||
'url' => $this->resource_url,
|
'url' => $this->resource_url,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
@ -99,7 +114,7 @@ public function toPushover(): PushoverMessage
|
||||||
message: $message,
|
message: $message,
|
||||||
buttons: [
|
buttons: [
|
||||||
[
|
[
|
||||||
'text' => 'Open Application in Coolify',
|
'text' => 'Open Resource in Coolify',
|
||||||
'url' => $this->resource_url,
|
'url' => $this->resource_url,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
@ -110,10 +125,13 @@ public function toSlack(): SlackMessage
|
||||||
{
|
{
|
||||||
$title = 'Restart limit reached';
|
$title = 'Restart limit reached';
|
||||||
$description = "{$this->resource_name} has been stopped after {$this->restart_count} restarts (limit: {$this->max_restart_count})";
|
$description = "{$this->resource_name} has been stopped after {$this->restart_count} restarts (limit: {$this->max_restart_count})";
|
||||||
|
$environment = data_get($this->resource, 'environment')
|
||||||
|
?? data_get($this->resource, 'application.environment')
|
||||||
|
?? data_get($this->resource, 'service.environment');
|
||||||
|
|
||||||
$description .= "\n\n*Project:* ".data_get($this->resource, 'environment.project.name');
|
$description .= "\n\n*Project:* ".data_get($environment, 'project.name');
|
||||||
$description .= "\n*Environment:* {$this->environment_name}";
|
$description .= "\n*Environment:* {$this->environment_name}";
|
||||||
$description .= "\n*Application URL:* {$this->resource_url}";
|
$description .= "\n*Resource URL:* {$this->resource_url}";
|
||||||
|
|
||||||
return new SlackMessage(
|
return new SlackMessage(
|
||||||
title: $title,
|
title: $title,
|
||||||
|
|
@ -130,6 +148,8 @@ public function toWebhook(): array
|
||||||
'event' => 'restart_limit_reached',
|
'event' => 'restart_limit_reached',
|
||||||
'application_name' => $this->resource_name,
|
'application_name' => $this->resource_name,
|
||||||
'application_uuid' => $this->resource->uuid,
|
'application_uuid' => $this->resource->uuid,
|
||||||
|
'resource_name' => $this->resource_name,
|
||||||
|
'resource_uuid' => $this->resource->uuid,
|
||||||
'restart_count' => $this->restart_count,
|
'restart_count' => $this->restart_count,
|
||||||
'max_restart_count' => $this->max_restart_count,
|
'max_restart_count' => $this->max_restart_count,
|
||||||
'url' => $this->resource_url,
|
'url' => $this->resource_url,
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,21 @@
|
||||||
namespace App\Notifications\Channels;
|
namespace App\Notifications\Channels;
|
||||||
|
|
||||||
use App\Jobs\SendMessageToTelegramJob;
|
use App\Jobs\SendMessageToTelegramJob;
|
||||||
|
use App\Notifications\Application\DeploymentFailed;
|
||||||
|
use App\Notifications\Application\DeploymentSuccess;
|
||||||
|
use App\Notifications\Application\RestartLimitReached;
|
||||||
|
use App\Notifications\Application\StatusChanged;
|
||||||
|
use App\Notifications\Container\ContainerRestarted;
|
||||||
|
use App\Notifications\Database\BackupFailed;
|
||||||
|
use App\Notifications\Database\BackupSuccess;
|
||||||
|
use App\Notifications\ScheduledTask\TaskFailed;
|
||||||
|
use App\Notifications\ScheduledTask\TaskSuccess;
|
||||||
|
use App\Notifications\Server\DockerCleanupFailed;
|
||||||
|
use App\Notifications\Server\DockerCleanupSuccess;
|
||||||
|
use App\Notifications\Server\HighDiskUsage;
|
||||||
|
use App\Notifications\Server\Reachable;
|
||||||
|
use App\Notifications\Server\ServerPatchCheck;
|
||||||
|
use App\Notifications\Server\Unreachable;
|
||||||
|
|
||||||
class TelegramChannel
|
class TelegramChannel
|
||||||
{
|
{
|
||||||
|
|
@ -17,24 +32,24 @@ public function send($notifiable, $notification): void
|
||||||
$chatId = $settings->telegram_chat_id;
|
$chatId = $settings->telegram_chat_id;
|
||||||
|
|
||||||
$threadId = match (get_class($notification)) {
|
$threadId = match (get_class($notification)) {
|
||||||
\App\Notifications\Application\DeploymentSuccess::class => $settings->telegram_notifications_deployment_success_thread_id,
|
DeploymentSuccess::class => $settings->telegram_notifications_deployment_success_thread_id,
|
||||||
\App\Notifications\Application\DeploymentFailed::class => $settings->telegram_notifications_deployment_failure_thread_id,
|
DeploymentFailed::class => $settings->telegram_notifications_deployment_failure_thread_id,
|
||||||
\App\Notifications\Application\StatusChanged::class,
|
StatusChanged::class,
|
||||||
\App\Notifications\Container\ContainerRestarted::class,
|
ContainerRestarted::class => $settings->telegram_notifications_status_change_thread_id,
|
||||||
\App\Notifications\Container\ContainerStopped::class => $settings->telegram_notifications_status_change_thread_id,
|
RestartLimitReached::class => $settings->telegram_notifications_restart_limit_reached_thread_id,
|
||||||
|
|
||||||
\App\Notifications\Database\BackupSuccess::class => $settings->telegram_notifications_backup_success_thread_id,
|
BackupSuccess::class => $settings->telegram_notifications_backup_success_thread_id,
|
||||||
\App\Notifications\Database\BackupFailed::class => $settings->telegram_notifications_backup_failure_thread_id,
|
BackupFailed::class => $settings->telegram_notifications_backup_failure_thread_id,
|
||||||
|
|
||||||
\App\Notifications\ScheduledTask\TaskSuccess::class => $settings->telegram_notifications_scheduled_task_success_thread_id,
|
TaskSuccess::class => $settings->telegram_notifications_scheduled_task_success_thread_id,
|
||||||
\App\Notifications\ScheduledTask\TaskFailed::class => $settings->telegram_notifications_scheduled_task_failure_thread_id,
|
TaskFailed::class => $settings->telegram_notifications_scheduled_task_failure_thread_id,
|
||||||
|
|
||||||
\App\Notifications\Server\DockerCleanupSuccess::class => $settings->telegram_notifications_docker_cleanup_success_thread_id,
|
DockerCleanupSuccess::class => $settings->telegram_notifications_docker_cleanup_success_thread_id,
|
||||||
\App\Notifications\Server\DockerCleanupFailed::class => $settings->telegram_notifications_docker_cleanup_failure_thread_id,
|
DockerCleanupFailed::class => $settings->telegram_notifications_docker_cleanup_failure_thread_id,
|
||||||
\App\Notifications\Server\HighDiskUsage::class => $settings->telegram_notifications_server_disk_usage_thread_id,
|
HighDiskUsage::class => $settings->telegram_notifications_server_disk_usage_thread_id,
|
||||||
\App\Notifications\Server\Unreachable::class => $settings->telegram_notifications_server_unreachable_thread_id,
|
Unreachable::class => $settings->telegram_notifications_server_unreachable_thread_id,
|
||||||
\App\Notifications\Server\Reachable::class => $settings->telegram_notifications_server_reachable_thread_id,
|
Reachable::class => $settings->telegram_notifications_server_reachable_thread_id,
|
||||||
\App\Notifications\Server\ServerPatchCheck::class => $settings->telegram_notifications_server_patch_thread_id,
|
ServerPatchCheck::class => $settings->telegram_notifications_server_patch_thread_id,
|
||||||
|
|
||||||
default => null,
|
default => null,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,123 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Notifications\Container;
|
|
||||||
|
|
||||||
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 ContainerStopped extends CustomEmailNotification
|
|
||||||
{
|
|
||||||
public function __construct(public string $name, public Server $server, public ?string $url = null)
|
|
||||||
{
|
|
||||||
$this->onQueue('high');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function via(object $notifiable): array
|
|
||||||
{
|
|
||||||
return $notifiable->getEnabledChannels('status_change');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function toMail(): MailMessage
|
|
||||||
{
|
|
||||||
$mail = new MailMessage;
|
|
||||||
$mail->subject("Coolify: A resource has been stopped unexpectedly on {$this->server->name}");
|
|
||||||
$mail->view('emails.container-stopped', [
|
|
||||||
'containerName' => $this->name,
|
|
||||||
'serverName' => $this->server->name,
|
|
||||||
'url' => $this->url,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return $mail;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function toDiscord(): DiscordMessage
|
|
||||||
{
|
|
||||||
$message = new DiscordMessage(
|
|
||||||
title: ':cross_mark: Resource stopped',
|
|
||||||
description: "{$this->name} has been stopped unexpectedly on {$this->server->name}.",
|
|
||||||
color: DiscordMessage::errorColor(),
|
|
||||||
);
|
|
||||||
|
|
||||||
if ($this->url) {
|
|
||||||
$message->addField('Resource', '[Link]('.$this->url.')');
|
|
||||||
}
|
|
||||||
|
|
||||||
return $message;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function toTelegram(): array
|
|
||||||
{
|
|
||||||
$message = "Coolify: A resource ($this->name) has been stopped unexpectedly on {$this->server->name}";
|
|
||||||
$payload = [
|
|
||||||
'message' => $message,
|
|
||||||
];
|
|
||||||
if ($this->url) {
|
|
||||||
$payload['buttons'] = [
|
|
||||||
[
|
|
||||||
[
|
|
||||||
'text' => 'Open Application in Coolify',
|
|
||||||
'url' => $this->url,
|
|
||||||
],
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $payload;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function toPushover(): PushoverMessage
|
|
||||||
{
|
|
||||||
$buttons = [];
|
|
||||||
if ($this->url) {
|
|
||||||
$buttons[] = [
|
|
||||||
'text' => 'Open Application in Coolify',
|
|
||||||
'url' => $this->url,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return new PushoverMessage(
|
|
||||||
title: 'Resource stopped',
|
|
||||||
level: 'error',
|
|
||||||
message: "A resource ({$this->name}) has been stopped unexpectedly on {$this->server->name}",
|
|
||||||
buttons: $buttons,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function toSlack(): SlackMessage
|
|
||||||
{
|
|
||||||
$title = 'Resource stopped';
|
|
||||||
$description = "A resource ({$this->name}) has been stopped unexpectedly on {$this->server->name}";
|
|
||||||
|
|
||||||
if ($this->url) {
|
|
||||||
$description .= "\n*Resource URL:* {$this->url}";
|
|
||||||
}
|
|
||||||
|
|
||||||
return new SlackMessage(
|
|
||||||
title: $title,
|
|
||||||
description: $description,
|
|
||||||
color: SlackMessage::errorColor()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function toWebhook(): array
|
|
||||||
{
|
|
||||||
$data = [
|
|
||||||
'success' => false,
|
|
||||||
'message' => 'Resource stopped unexpectedly',
|
|
||||||
'event' => 'container_stopped',
|
|
||||||
'container_name' => $this->name,
|
|
||||||
'server_name' => $this->server->name,
|
|
||||||
'server_uuid' => $this->server->uuid,
|
|
||||||
];
|
|
||||||
|
|
||||||
if ($this->url) {
|
|
||||||
$data['url'] = $this->url;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -18,14 +18,13 @@
|
||||||
* State Priority (highest to lowest):
|
* State Priority (highest to lowest):
|
||||||
* 1. Degraded (from sub-resources) → degraded:unhealthy
|
* 1. Degraded (from sub-resources) → degraded:unhealthy
|
||||||
* 2. Restarting → degraded:unhealthy (or restarting:unknown if preserveRestarting=true)
|
* 2. Restarting → degraded:unhealthy (or restarting:unknown if preserveRestarting=true)
|
||||||
* 3. Crash Loop (exited with restarts) → degraded:unhealthy
|
* 3. Mixed (running + exited) → degraded:unhealthy
|
||||||
* 4. Mixed (running + exited) → degraded:unhealthy
|
* 4. Mixed (running + starting) → starting:unknown
|
||||||
* 5. Mixed (running + starting) → starting:unknown
|
* 5. Running → running:healthy/unhealthy/unknown
|
||||||
* 6. Running → running:healthy/unhealthy/unknown
|
* 6. Dead/Removing → degraded:unhealthy
|
||||||
* 7. Dead/Removing → degraded:unhealthy
|
* 7. Paused → paused:unknown
|
||||||
* 8. Paused → paused:unknown
|
* 8. Starting/Created → starting:unknown
|
||||||
* 9. Starting/Created → starting:unknown
|
* 9. Exited → exited
|
||||||
* 10. Exited → exited
|
|
||||||
*
|
*
|
||||||
* The $preserveRestarting parameter controls whether "restarting" containers should be
|
* The $preserveRestarting parameter controls whether "restarting" containers should be
|
||||||
* reported as "restarting:unknown" (true) or "degraded:unhealthy" (false, default).
|
* reported as "restarting:unknown" (true) or "degraded:unhealthy" (false, default).
|
||||||
|
|
@ -228,23 +227,18 @@ private function resolveStatus(
|
||||||
return $preserveRestarting ? 'restarting:unknown' : 'degraded:unhealthy';
|
return $preserveRestarting ? 'restarting:unknown' : 'degraded:unhealthy';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Priority 3: Crash loop detection (exited with restart count > 0)
|
// Priority 3: Mixed state (some running, some exited = degraded)
|
||||||
if ($hasExited && $maxRestartCount > 0) {
|
|
||||||
return 'degraded:unhealthy';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Priority 4: Mixed state (some running, some exited = degraded)
|
|
||||||
if ($hasRunning && $hasExited) {
|
if ($hasRunning && $hasExited) {
|
||||||
return 'degraded:unhealthy';
|
return 'degraded:unhealthy';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Priority 5: Mixed state (some running, some starting = still starting)
|
// Priority 4: Mixed state (some running, some starting = still starting)
|
||||||
// If any component is still starting, the entire service stack is not fully ready
|
// If any component is still starting, the entire service stack is not fully ready
|
||||||
if ($hasRunning && $hasStarting) {
|
if ($hasRunning && $hasStarting) {
|
||||||
return 'starting:unknown';
|
return 'starting:unknown';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Priority 6: Running containers (check health status)
|
// Priority 5: Running containers (check health status)
|
||||||
if ($hasRunning) {
|
if ($hasRunning) {
|
||||||
if ($hasUnhealthy) {
|
if ($hasUnhealthy) {
|
||||||
return 'running:unhealthy';
|
return 'running:unhealthy';
|
||||||
|
|
@ -255,22 +249,22 @@ private function resolveStatus(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Priority 7: Dead or removing containers
|
// Priority 6: Dead or removing containers
|
||||||
if ($hasDead) {
|
if ($hasDead) {
|
||||||
return 'degraded:unhealthy';
|
return 'degraded:unhealthy';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Priority 8: Paused containers
|
// Priority 7: Paused containers
|
||||||
if ($hasPaused) {
|
if ($hasPaused) {
|
||||||
return 'paused:unknown';
|
return 'paused:unknown';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Priority 9: Starting/created containers
|
// Priority 8: Starting/created containers
|
||||||
if ($hasStarting) {
|
if ($hasStarting) {
|
||||||
return 'starting:unknown';
|
return 'starting:unknown';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Priority 10: All containers exited (no restart count = truly stopped)
|
// Priority 9: All containers exited
|
||||||
return 'exited';
|
return 'exited';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
31
app/Services/RestartCountTracker.php
Normal file
31
app/Services/RestartCountTracker.php
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
class RestartCountTracker
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return array{restart_count: int, restart_count_changed: bool, restart_limit_reached: bool, new_generation: bool}
|
||||||
|
*/
|
||||||
|
public function evaluate(
|
||||||
|
int $previousRestartCount,
|
||||||
|
int $observedRestartCount,
|
||||||
|
int $maxRestartCount,
|
||||||
|
bool $newGenerationConfirmed = false,
|
||||||
|
): array {
|
||||||
|
$newGeneration = $newGenerationConfirmed
|
||||||
|
&& $observedRestartCount < $previousRestartCount;
|
||||||
|
$restartCountIncreased = $observedRestartCount > $previousRestartCount;
|
||||||
|
$restartCountChanged = $newGeneration || $restartCountIncreased;
|
||||||
|
|
||||||
|
$restartLimitReached = $maxRestartCount > 0
|
||||||
|
&& $observedRestartCount >= $maxRestartCount;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'restart_count' => $restartCountChanged ? $observedRestartCount : $previousRestartCount,
|
||||||
|
'restart_count_changed' => $restartCountChanged,
|
||||||
|
'restart_limit_reached' => $restartLimitReached,
|
||||||
|
'new_generation' => $newGeneration,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
73
app/Traits/HasRestartLimit.php
Normal file
73
app/Traits/HasRestartLimit.php
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Traits;
|
||||||
|
|
||||||
|
use App\Services\RestartCountTracker;
|
||||||
|
|
||||||
|
trait HasRestartLimit
|
||||||
|
{
|
||||||
|
public function initializeHasRestartLimit(): void
|
||||||
|
{
|
||||||
|
$this->mergeFillable(['restart_count', 'max_restart_count', 'restart_limit_reached', 'last_restart_at', 'last_restart_type']);
|
||||||
|
$this->mergeCasts([
|
||||||
|
'restart_count' => 'integer',
|
||||||
|
'max_restart_count' => 'integer',
|
||||||
|
'restart_limit_reached' => 'boolean',
|
||||||
|
'last_restart_at' => 'datetime',
|
||||||
|
'last_restart_type' => 'string',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stoppedAfterRestartLimit(): bool
|
||||||
|
{
|
||||||
|
return str($this->status)->startsWith('exited') && $this->restart_limit_reached === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function trackRestartCount(int $observedRestartCount): bool
|
||||||
|
{
|
||||||
|
$state = (new RestartCountTracker)->evaluate(
|
||||||
|
previousRestartCount: $this->restart_count ?? 0,
|
||||||
|
observedRestartCount: $observedRestartCount,
|
||||||
|
maxRestartCount: $this->restartLimitMaximum(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($state['restart_count_changed']) {
|
||||||
|
$hasCrashRestarts = $state['restart_count'] > 0;
|
||||||
|
$this->update([
|
||||||
|
'restart_count' => $state['restart_count'],
|
||||||
|
'last_restart_at' => $hasCrashRestarts ? now() : null,
|
||||||
|
'last_restart_type' => $hasCrashRestarts ? 'crash' : null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $state['restart_limit_reached']) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$claimed = $this->newQuery()
|
||||||
|
->whereKey($this->getKey())
|
||||||
|
->where('restart_limit_reached', false)
|
||||||
|
->update(['restart_limit_reached' => true]) === 1;
|
||||||
|
|
||||||
|
if ($claimed) {
|
||||||
|
$this->restart_limit_reached = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $claimed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resetRestartLimit(): void
|
||||||
|
{
|
||||||
|
$this->update([
|
||||||
|
'restart_count' => 0,
|
||||||
|
'restart_limit_reached' => false,
|
||||||
|
'last_restart_at' => null,
|
||||||
|
'last_restart_type' => null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function restartLimitMaximum(): int
|
||||||
|
{
|
||||||
|
return $this->max_restart_count ?? 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?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
|
||||||
|
{
|
||||||
|
Schema::table('applications', function (Blueprint $table) {
|
||||||
|
$table->boolean('container_present')->nullable()->after('status');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('applications', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('container_present');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('applications', function (Blueprint $table) {
|
||||||
|
$table->boolean('restart_limit_reached')->default(false)->after('max_restart_count');
|
||||||
|
});
|
||||||
|
|
||||||
|
DB::table('applications')
|
||||||
|
->where('status', 'like', 'exited%')
|
||||||
|
->where('restart_count', '>', 0)
|
||||||
|
->where('max_restart_count', '>', 0)
|
||||||
|
->whereColumn('restart_count', '>=', 'max_restart_count')
|
||||||
|
->where('last_restart_type', 'crash')
|
||||||
|
->update(['restart_limit_reached' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('applications', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('restart_limit_reached');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('application_previews', 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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('application_previews', function (Blueprint $table) {
|
||||||
|
$table->dropColumn([
|
||||||
|
'restart_count',
|
||||||
|
'max_restart_count',
|
||||||
|
'restart_limit_reached',
|
||||||
|
'last_restart_at',
|
||||||
|
'last_restart_type',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('service_applications', 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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('service_applications', function (Blueprint $table) {
|
||||||
|
$table->dropColumn([
|
||||||
|
'restart_count',
|
||||||
|
'max_restart_count',
|
||||||
|
'restart_limit_reached',
|
||||||
|
'last_restart_at',
|
||||||
|
'last_restart_type',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('service_databases', function (Blueprint $table) {
|
||||||
|
$table->dropColumn([
|
||||||
|
'restart_count',
|
||||||
|
'max_restart_count',
|
||||||
|
'restart_limit_reached',
|
||||||
|
'last_restart_at',
|
||||||
|
'last_restart_type',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('standalone_postgresqls', function (Blueprint $table) {
|
||||||
|
$table->integer('max_restart_count')->default(10);
|
||||||
|
$table->boolean('restart_limit_reached')->default(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('standalone_postgresqls', function (Blueprint $table) {
|
||||||
|
$table->dropColumn(['max_restart_count', 'restart_limit_reached']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('standalone_redis', function (Blueprint $table) {
|
||||||
|
$table->integer('max_restart_count')->default(10);
|
||||||
|
$table->boolean('restart_limit_reached')->default(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('standalone_redis', function (Blueprint $table) {
|
||||||
|
$table->dropColumn(['max_restart_count', 'restart_limit_reached']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('standalone_mongodbs', function (Blueprint $table) {
|
||||||
|
$table->integer('max_restart_count')->default(10);
|
||||||
|
$table->boolean('restart_limit_reached')->default(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('standalone_mongodbs', function (Blueprint $table) {
|
||||||
|
$table->dropColumn(['max_restart_count', 'restart_limit_reached']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('standalone_mysqls', function (Blueprint $table) {
|
||||||
|
$table->integer('max_restart_count')->default(10);
|
||||||
|
$table->boolean('restart_limit_reached')->default(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('standalone_mysqls', function (Blueprint $table) {
|
||||||
|
$table->dropColumn(['max_restart_count', 'restart_limit_reached']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('standalone_mariadbs', function (Blueprint $table) {
|
||||||
|
$table->integer('max_restart_count')->default(10);
|
||||||
|
$table->boolean('restart_limit_reached')->default(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('standalone_mariadbs', function (Blueprint $table) {
|
||||||
|
$table->dropColumn(['max_restart_count', 'restart_limit_reached']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('standalone_keydbs', function (Blueprint $table) {
|
||||||
|
$table->integer('max_restart_count')->default(10);
|
||||||
|
$table->boolean('restart_limit_reached')->default(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('standalone_keydbs', function (Blueprint $table) {
|
||||||
|
$table->dropColumn(['max_restart_count', 'restart_limit_reached']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('standalone_dragonflies', function (Blueprint $table) {
|
||||||
|
$table->integer('max_restart_count')->default(10);
|
||||||
|
$table->boolean('restart_limit_reached')->default(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('standalone_dragonflies', function (Blueprint $table) {
|
||||||
|
$table->dropColumn(['max_restart_count', 'restart_limit_reached']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('standalone_clickhouses', function (Blueprint $table) {
|
||||||
|
$table->integer('max_restart_count')->default(10);
|
||||||
|
$table->boolean('restart_limit_reached')->default(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('standalone_clickhouses', function (Blueprint $table) {
|
||||||
|
$table->dropColumn(['max_restart_count', 'restart_limit_reached']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?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
|
||||||
|
{
|
||||||
|
Schema::table('email_notification_settings', function (Blueprint $table) {
|
||||||
|
$table->boolean('restart_limit_reached_email_notifications')->default(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('email_notification_settings', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('restart_limit_reached_email_notifications');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?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
|
||||||
|
{
|
||||||
|
Schema::table('discord_notification_settings', function (Blueprint $table) {
|
||||||
|
$table->boolean('restart_limit_reached_discord_notifications')->default(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('discord_notification_settings', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('restart_limit_reached_discord_notifications');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?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
|
||||||
|
{
|
||||||
|
Schema::table('telegram_notification_settings', function (Blueprint $table) {
|
||||||
|
$table->boolean('restart_limit_reached_telegram_notifications')->default(true);
|
||||||
|
$table->text('telegram_notifications_restart_limit_reached_thread_id')->nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('telegram_notification_settings', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('restart_limit_reached_telegram_notifications');
|
||||||
|
$table->dropColumn('telegram_notifications_restart_limit_reached_thread_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?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
|
||||||
|
{
|
||||||
|
Schema::table('slack_notification_settings', function (Blueprint $table) {
|
||||||
|
$table->boolean('restart_limit_reached_slack_notifications')->default(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('slack_notification_settings', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('restart_limit_reached_slack_notifications');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?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
|
||||||
|
{
|
||||||
|
Schema::table('pushover_notification_settings', function (Blueprint $table) {
|
||||||
|
$table->boolean('restart_limit_reached_pushover_notifications')->default(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('pushover_notification_settings', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('restart_limit_reached_pushover_notifications');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?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
|
||||||
|
{
|
||||||
|
Schema::table('webhook_notification_settings', function (Blueprint $table) {
|
||||||
|
$table->boolean('restart_limit_reached_webhook_notifications')->default(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('webhook_notification_settings', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('restart_limit_reached_webhook_notifications');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
@props(['application'])
|
||||||
|
|
||||||
|
@if ($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
|
||||||
|
status="Restart limit reached"
|
||||||
|
type="warning"
|
||||||
|
title="Container has crashed and Coolify stopped it after {{ $displayRestartCount }} restart attempts." />
|
||||||
|
@endif
|
||||||
|
|
@ -9,10 +9,17 @@
|
||||||
'Deployments' => [
|
'Deployments' => [
|
||||||
['key' => 'deploymentSuccess', 'label' => 'Deployment success'],
|
['key' => 'deploymentSuccess', 'label' => 'Deployment success'],
|
||||||
['key' => 'deploymentFailure', 'label' => 'Deployment failure'],
|
['key' => 'deploymentFailure', 'label' => 'Deployment failure'],
|
||||||
|
],
|
||||||
|
'Resources' => [
|
||||||
[
|
[
|
||||||
'key' => 'statusChange',
|
'key' => 'statusChange',
|
||||||
'label' => 'Container status changes',
|
'label' => 'Resource status changes',
|
||||||
'helper' => 'Notify when a container stops or restarts.',
|
'helper' => 'Notify when a resource stops or Coolify automatically restarts it.',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'key' => 'restartLimitReached',
|
||||||
|
'label' => 'Restart limit reached',
|
||||||
|
'helper' => 'Notify when a resource is stopped after reaching its restart limit.',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
'Backups' => [
|
'Backups' => [
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
|
|
||||||
$containerType = match (true) {
|
$containerType = match (true) {
|
||||||
str($containerStatus)->startsWith('running') => 'success',
|
str($containerStatus)->startsWith('running') => 'success',
|
||||||
str($containerStatus)->startsWith(['starting', 'restarting']) => 'warning',
|
str($containerStatus)->startsWith(['starting', 'restarting', 'degraded']) => 'warning',
|
||||||
default => 'error',
|
default => 'error',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -36,6 +36,7 @@
|
||||||
|
|
||||||
[$summaryLabel, $summaryType] = match (true) {
|
[$summaryLabel, $summaryType] = match (true) {
|
||||||
$containerType === 'error' => [$containerLabel, 'error'],
|
$containerType === 'error' => [$containerLabel, 'error'],
|
||||||
|
str($containerStatus)->startsWith('degraded') => ['Degraded', 'warning'],
|
||||||
$healthType === 'error' => ['Degraded', 'error'],
|
$healthType === 'error' => ['Degraded', 'error'],
|
||||||
$containerType === 'warning' => [$containerLabel, 'warning'],
|
$containerType === 'warning' => [$containerLabel, 'warning'],
|
||||||
$monitoringExcluded => ["{$containerLabel} (monitoring disabled)", 'warning'],
|
$monitoringExcluded => ["{$containerLabel} (monitoring disabled)", 'warning'],
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,7 @@
|
||||||
title="Container has restarted {{ $resource->restart_count }} time{{ $resource->restart_count > 1 ? 's' : '' }}. Last restart: {{ $resource->last_restart_at?->diffForHumans() }}" />
|
title="Container has restarted {{ $resource->restart_count }} time{{ $resource->restart_count > 1 ? 's' : '' }}. Last restart: {{ $resource->last_restart_at?->diffForHumans() }}" />
|
||||||
@endif
|
@endif
|
||||||
@if ($stoppedAfterRestartLimit)
|
@if ($stoppedAfterRestartLimit)
|
||||||
<x-status-badge status="Stopped after reaching restart limit ({{ $resource->restart_count }}/{{ $resource->max_restart_count }})."
|
<x-application.restart-limit-warning :application="$resource" />
|
||||||
type="warning"
|
|
||||||
title="Container has crashed and Coolify stopped it after {{ $resource->restart_count }} restart attempts." />
|
|
||||||
@endif
|
@endif
|
||||||
@if (!str($resource->status)->contains('exited') && $showRefreshButton)
|
@if (!str($resource->status)->contains('exited') && $showRefreshButton)
|
||||||
<x-status-badge as="button" wire:target="manualCheckStatus" wire:loading.attr="disabled"
|
<x-status-badge as="button" wire:target="manualCheckStatus" wire:loading.attr="disabled"
|
||||||
|
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
<x-emails.layout>
|
|
||||||
A resource ({{ $containerName }}) has been stopped unexpectedly on {{ $serverName }}.
|
|
||||||
|
|
||||||
@if ($url)
|
|
||||||
Please check what is going on [here]({{ $url }}).
|
|
||||||
@endif
|
|
||||||
</x-emails.layout>
|
|
||||||
|
|
@ -149,7 +149,11 @@ class="button button-highlighted">
|
||||||
:events="[
|
:events="[
|
||||||
['property' => 'deploymentSuccessEmailNotifications', 'label' => 'Deployment success', 'enabled' => $deploymentSuccessEmailNotifications],
|
['property' => 'deploymentSuccessEmailNotifications', 'label' => 'Deployment success', 'enabled' => $deploymentSuccessEmailNotifications],
|
||||||
['property' => 'deploymentFailureEmailNotifications', 'label' => 'Deployment failure', 'enabled' => $deploymentFailureEmailNotifications],
|
['property' => 'deploymentFailureEmailNotifications', 'label' => 'Deployment failure', 'enabled' => $deploymentFailureEmailNotifications],
|
||||||
['property' => 'statusChangeEmailNotifications', 'label' => 'Container status changes', 'enabled' => $statusChangeEmailNotifications],
|
]" />
|
||||||
|
<x-notification.event-multiselect :settings="$settings" id="resource-email-events" label="Resources"
|
||||||
|
:events="[
|
||||||
|
['property' => 'statusChangeEmailNotifications', 'label' => 'Resource status changes', 'enabled' => $statusChangeEmailNotifications],
|
||||||
|
['property' => 'restartLimitReachedEmailNotifications', 'label' => 'Restart limit reached', 'enabled' => $restartLimitReachedEmailNotifications],
|
||||||
]" />
|
]" />
|
||||||
<x-notification.event-multiselect :settings="$settings" id="backup-email-events" label="Backups"
|
<x-notification.event-multiselect :settings="$settings" id="backup-email-events" label="Backups"
|
||||||
:events="[
|
:events="[
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,9 @@
|
||||||
<x-status-summary :status="$application->status" />
|
<x-status-summary :status="$application->status" />
|
||||||
<x-applications.links :application="$application" compact />
|
<x-applications.links :application="$application" compact />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex w-full flex-wrap gap-1">
|
||||||
|
<x-application.restart-limit-warning :application="$application" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -103,32 +106,18 @@ class="listbox-panel top-full! left-0! right-0! mt-1! w-full! min-w-0!" role="me
|
||||||
@endcan
|
@endcan
|
||||||
@endif
|
@endif
|
||||||
@endif
|
@endif
|
||||||
@can('deploy', $application)
|
|
||||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
|
||||||
@click="open = false; document.getElementById('application-mobile-stop-trigger')?.click()"
|
|
||||||
role="menuitem">
|
|
||||||
<x-reicon name="stop-circle" class="size-3.5 text-error" />
|
|
||||||
Stop
|
|
||||||
</button>
|
|
||||||
@else
|
|
||||||
<button type="button" class="listbox-option justify-start! gap-2.5!" disabled
|
|
||||||
role="menuitem">
|
|
||||||
<x-reicon name="stop-circle" class="size-3.5 opacity-70" />
|
|
||||||
Stop
|
|
||||||
</button>
|
|
||||||
@endcan
|
|
||||||
@else
|
@else
|
||||||
@can('deploy', $application)
|
@can('deploy', $application)
|
||||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||||
wire:click="deploy" @click="open = false" role="menuitem">
|
wire:click="deploy" @click="open = false" role="menuitem">
|
||||||
<x-reicon name="play-circle" class="size-3.5 opacity-70" />
|
<x-reicon name="play-circle" class="size-3.5 opacity-70" />
|
||||||
Deploy
|
{{ $application->stoppedAfterRestartLimit() ? 'Retry deployment' : 'Deploy' }}
|
||||||
</button>
|
</button>
|
||||||
@else
|
@else
|
||||||
<button type="button" class="listbox-option justify-start! gap-2.5!" disabled
|
<button type="button" class="listbox-option justify-start! gap-2.5!" disabled
|
||||||
role="menuitem">
|
role="menuitem">
|
||||||
<x-reicon name="play-circle" class="size-3.5 opacity-70" />
|
<x-reicon name="play-circle" class="size-3.5 opacity-70" />
|
||||||
Deploy
|
{{ $application->stoppedAfterRestartLimit() ? 'Retry deployment' : 'Deploy' }}
|
||||||
</button>
|
</button>
|
||||||
@endcan
|
@endcan
|
||||||
@endif
|
@endif
|
||||||
|
|
@ -138,25 +127,36 @@ class="listbox-panel top-full! left-0! right-0! mt-1! w-full! min-w-0!" role="me
|
||||||
wire:click="{{ $application->status === 'running' ? 'force_deploy_without_cache' : 'deploy(true)' }}"
|
wire:click="{{ $application->status === 'running' ? 'force_deploy_without_cache' : 'deploy(true)' }}"
|
||||||
@click="open = false" role="menuitem">
|
@click="open = false" role="menuitem">
|
||||||
<x-reicon name="refresh" class="size-3.5 opacity-70" />
|
<x-reicon name="refresh" class="size-3.5 opacity-70" />
|
||||||
Deploy (without cache)
|
{{ $application->stoppedAfterRestartLimit() ? 'Retry deployment (without cache)' : 'Deploy (without cache)' }}
|
||||||
</button>
|
</button>
|
||||||
@else
|
@else
|
||||||
<button type="button" class="listbox-option justify-start! gap-2.5!" disabled
|
<button type="button" class="listbox-option justify-start! gap-2.5!" disabled
|
||||||
role="menuitem">
|
role="menuitem">
|
||||||
<x-reicon name="refresh" class="size-3.5 opacity-70" />
|
<x-reicon name="refresh" class="size-3.5 opacity-70" />
|
||||||
Deploy (without cache)
|
{{ $application->stoppedAfterRestartLimit() ? 'Retry deployment (without cache)' : 'Deploy (without cache)' }}
|
||||||
</button>
|
</button>
|
||||||
@endcan
|
@endcan
|
||||||
@endif
|
@endif
|
||||||
|
@if (!str($application->status)->startsWith('exited') || $application->container_present !== false)
|
||||||
|
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||||
|
@click="open = false; document.getElementById('application-mobile-stop-trigger')?.click()"
|
||||||
|
role="menuitem">
|
||||||
|
<x-reicon name="stop-circle" class="size-3.5 text-error" />
|
||||||
|
{{ str($application->status)->startsWith('exited') ? 'Remove container' : 'Stop' }}
|
||||||
|
</button>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endcan
|
@endcan
|
||||||
@endif
|
@endif
|
||||||
<div class="hidden" aria-hidden="true">
|
<div class="hidden" aria-hidden="true">
|
||||||
<x-modal-confirmation title="Confirm Application Stopping?" buttonTitle="Stop"
|
<x-modal-confirmation
|
||||||
|
canGate="deploy" :canResource="$application"
|
||||||
|
title="{{ str($application->status)->startsWith('exited') ? 'Confirm Container Removal?' : 'Confirm Application Stopping?' }}"
|
||||||
|
buttonTitle="{{ str($application->status)->startsWith('exited') ? 'Remove container' : 'Stop' }}"
|
||||||
submitAction="stop" :checkboxes="$checkboxes" :actions="[
|
submitAction="stop" :checkboxes="$checkboxes" :actions="[
|
||||||
'This application will be stopped.',
|
str($application->status)->startsWith('exited') ? 'The exited application container will be removed.' : 'This application will be stopped.',
|
||||||
'All non-persistent data of this application will be deleted.',
|
str($application->status)->startsWith('exited') ? 'Anonymous volumes may become eligible for Docker cleanup.' : 'All non-persistent data of this application will be deleted.',
|
||||||
]" :confirmWithText="false" :confirmWithPassword="false"
|
]" :confirmWithText="false" :confirmWithPassword="false"
|
||||||
step1ButtonText="Continue" step2ButtonText="Confirm">
|
step1ButtonText="Continue" step2ButtonText="Confirm">
|
||||||
<x-slot:trigger>
|
<x-slot:trigger>
|
||||||
|
|
@ -204,14 +204,22 @@ class="listbox-panel top-full! right-0! left-auto! mt-1! w-60! min-w-0!" role="m
|
||||||
@disabled(!auth()->user()->can('deploy', $application))
|
@disabled(!auth()->user()->can('deploy', $application))
|
||||||
wire:click="deploy" @click="open = false" role="menuitem">
|
wire:click="deploy" @click="open = false" role="menuitem">
|
||||||
<x-reicon name="play-circle" class="size-3.5 opacity-70" />
|
<x-reicon name="play-circle" class="size-3.5 opacity-70" />
|
||||||
Deploy
|
{{ $application->stoppedAfterRestartLimit() ? 'Retry deployment' : 'Deploy' }}
|
||||||
</button>
|
</button>
|
||||||
@if (!$application->destination->server->isSwarm())
|
@if (!$application->destination->server->isSwarm())
|
||||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||||
@disabled(!auth()->user()->can('deploy', $application))
|
@disabled(!auth()->user()->can('deploy', $application))
|
||||||
wire:click="deploy(true)" @click="open = false" role="menuitem">
|
wire:click="deploy(true)" @click="open = false" role="menuitem">
|
||||||
<x-reicon name="refresh" class="size-3.5 opacity-70" />
|
<x-reicon name="refresh" class="size-3.5 opacity-70" />
|
||||||
Deploy (without cache)
|
{{ $application->stoppedAfterRestartLimit() ? 'Retry deployment (without cache)' : 'Deploy (without cache)' }}
|
||||||
|
</button>
|
||||||
|
@endif
|
||||||
|
@if ($application->container_present !== false)
|
||||||
|
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||||
|
@click="open = false; document.getElementById('application-mobile-stop-trigger')?.click()"
|
||||||
|
role="menuitem">
|
||||||
|
<x-reicon name="stop-circle" class="size-3.5 text-error" />
|
||||||
|
Remove container
|
||||||
</button>
|
</button>
|
||||||
@endif
|
@endif
|
||||||
@else
|
@else
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,7 @@ class="flex size-9 shrink-0 items-center justify-center rounded-lg bg-neutral-10
|
||||||
Preview #{{ data_get($preview, 'pull_request_id') }}
|
Preview #{{ data_get($preview, 'pull_request_id') }}
|
||||||
</h4>
|
</h4>
|
||||||
<x-status-summary :status="data_get($preview, 'status')" title="Preview status" />
|
<x-status-summary :status="data_get($preview, 'status')" title="Preview status" />
|
||||||
|
<x-application.restart-limit-warning :application="$preview" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
<div wire:poll.10000ms="refreshStatus">
|
<div wire:poll.10000ms="refreshStatus">
|
||||||
<x-status-summary :status="$application->status" />
|
<x-status-summary :status="$application->status" />
|
||||||
|
<x-application.restart-limit-warning :application="$application" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,12 @@
|
||||||
<h1 class="min-w-0 max-w-full truncate text-[24px]! leading-7! font-semibold! tracking-tight! text-black dark:text-fg">
|
<h1 class="min-w-0 max-w-full truncate text-[24px]! leading-7! font-semibold! tracking-tight! text-black dark:text-fg">
|
||||||
{{ $database->name }}
|
{{ $database->name }}
|
||||||
</h1>
|
</h1>
|
||||||
<x-status-summary :status="$database->status" title="Database status" />
|
<div class="relative flex w-full min-w-0 items-center gap-2">
|
||||||
|
<x-status-summary :status="$database->status" title="Database status" />
|
||||||
|
</div>
|
||||||
|
<div class="flex w-full flex-wrap gap-1">
|
||||||
|
<x-application.restart-limit-warning :application="$database" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
<div wire:poll.10000ms="refreshStatus">
|
<div wire:poll.10000ms="refreshStatus">
|
||||||
<x-status-summary :status="$database->status" title="Database status" />
|
<x-status-summary :status="$database->status" title="Database status" />
|
||||||
|
<x-application.restart-limit-warning :application="$database" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -220,7 +220,7 @@ class="relative z-10 block truncate text-[11px] text-neutral-500 hover:underline
|
||||||
x-text="item.typeLabel"></div>
|
x-text="item.typeLabel"></div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<x-status-badge dynamic>
|
<x-status-badge dynamic x-bind:title="statusTitle(item)">
|
||||||
<span class="size-1.5 shrink-0 rounded-full"
|
<span class="size-1.5 shrink-0 rounded-full"
|
||||||
x-bind:class="statusDotClass(item)"></span>
|
x-bind:class="statusDotClass(item)"></span>
|
||||||
<span class="truncate" x-text="statusLabel(item)"></span>
|
<span class="truncate" x-text="statusLabel(item)"></span>
|
||||||
|
|
@ -299,7 +299,7 @@ class="truncate text-[13px]! leading-4! font-semibold! text-black dark:text-fg"
|
||||||
<p class="mt-0.5 text-[11px] text-neutral-500 dark:text-fg-faint"
|
<p class="mt-0.5 text-[11px] text-neutral-500 dark:text-fg-faint"
|
||||||
x-text="item.typeLabel"></p>
|
x-text="item.typeLabel"></p>
|
||||||
</div>
|
</div>
|
||||||
<x-status-badge dynamic>
|
<x-status-badge dynamic x-bind:title="statusTitle(item)">
|
||||||
<span class="size-1.5 shrink-0 rounded-full"
|
<span class="size-1.5 shrink-0 rounded-full"
|
||||||
x-bind:class="statusDotClass(item)"></span>
|
x-bind:class="statusDotClass(item)"></span>
|
||||||
<span class="truncate" x-text="statusLabel(item)"></span>
|
<span class="truncate" x-text="statusLabel(item)"></span>
|
||||||
|
|
@ -518,13 +518,32 @@ function resourceIndex() {
|
||||||
localStorage.setItem('environment-resource-view', mode);
|
localStorage.setItem('environment-resource-view', mode);
|
||||||
},
|
},
|
||||||
statusState(item) {
|
statusState(item) {
|
||||||
|
if (item.restartLimitReached) {
|
||||||
|
return 'restart-limit';
|
||||||
|
}
|
||||||
|
|
||||||
return String(item.status || 'unknown').split(':')[0].toLowerCase();
|
return String(item.status || 'unknown').split(':')[0].toLowerCase();
|
||||||
},
|
},
|
||||||
statusLabel(item) {
|
statusLabel(item) {
|
||||||
|
if (item.restartLimitReached) {
|
||||||
|
return 'Restart limit reached';
|
||||||
|
}
|
||||||
|
|
||||||
const state = this.statusState(item);
|
const state = this.statusState(item);
|
||||||
return state.charAt(0).toUpperCase() + state.slice(1);
|
return state.charAt(0).toUpperCase() + state.slice(1);
|
||||||
},
|
},
|
||||||
|
statusTitle(item) {
|
||||||
|
if (item.restartLimitReached) {
|
||||||
|
return `${item.restartCount}/${item.maxRestartCount} restarts. Container preserved.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.statusLabel(item);
|
||||||
|
},
|
||||||
statusTone(item) {
|
statusTone(item) {
|
||||||
|
if (item.restartLimitReached) {
|
||||||
|
return 'warning';
|
||||||
|
}
|
||||||
|
|
||||||
const state = this.statusState(item);
|
const state = this.statusState(item);
|
||||||
if (state === 'running') {
|
if (state === 'running') {
|
||||||
return 'success';
|
return 'success';
|
||||||
|
|
@ -539,6 +558,10 @@ function resourceIndex() {
|
||||||
return 'neutral';
|
return 'neutral';
|
||||||
},
|
},
|
||||||
statusDotClass(item) {
|
statusDotClass(item) {
|
||||||
|
if (item.restartLimitReached) {
|
||||||
|
return 'bg-warning';
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: 'bg-emerald-500',
|
success: 'bg-emerald-500',
|
||||||
warning: 'bg-warning',
|
warning: 'bg-warning',
|
||||||
|
|
|
||||||
|
|
@ -143,12 +143,12 @@ class="flex size-7.5 items-center justify-center rounded-md transition-colors"
|
||||||
|
|
||||||
<div :class="viewMode === 'grid'
|
<div :class="viewMode === 'grid'
|
||||||
? 'grid grid-cols-1 gap-3 sm:grid-cols-2'
|
? 'grid grid-cols-1 gap-3 sm:grid-cols-2'
|
||||||
: 'overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]'">
|
: 'overflow-x-auto rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]'">
|
||||||
@if ($applications->isNotEmpty() || $databases->isNotEmpty())
|
@if ($applications->isNotEmpty() || $databases->isNotEmpty())
|
||||||
<div x-cloak x-show="viewMode === 'table'"
|
<div x-cloak x-show="viewMode === 'table'"
|
||||||
class="grid grid-cols-[minmax(0,1fr)_auto] gap-3 border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 sm:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_8rem_5rem] dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
|
class="grid min-w-[48rem] grid-cols-[minmax(14rem,1fr)_minmax(12rem,1fr)_12rem_5rem] gap-3 border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
|
||||||
<div>Resource</div>
|
<div>Resource</div>
|
||||||
<div class="hidden sm:block">Image</div>
|
<div>Image</div>
|
||||||
<div class="justify-self-start">Status</div>
|
<div class="justify-self-start">Status</div>
|
||||||
<div></div>
|
<div></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,13 @@
|
||||||
));
|
));
|
||||||
|
|
||||||
$serviceStatus = str($service->status ?? 'exited');
|
$serviceStatus = str($service->status ?? 'exited');
|
||||||
|
$selectedResourceUuid = data_get($parameters, 'stack_service_uuid');
|
||||||
|
$selectedResource = $selectedResourceUuid
|
||||||
|
? $service->applications->firstWhere('uuid', $selectedResourceUuid)
|
||||||
|
?? $service->databases->firstWhere('uuid', $selectedResourceUuid)
|
||||||
|
: null;
|
||||||
|
$displayStatus = $selectedResource?->status ?? $service->status;
|
||||||
|
$selectedResourceStatus = str($selectedResource?->status ?? '');
|
||||||
$environmentVariablesUrl = route('project.service.environment-variables', [
|
$environmentVariablesUrl = route('project.service.environment-variables', [
|
||||||
'project_uuid' => $service->environment->project->uuid,
|
'project_uuid' => $service->environment->project->uuid,
|
||||||
'environment_uuid' => $service->environment->uuid,
|
'environment_uuid' => $service->environment->uuid,
|
||||||
|
|
@ -65,9 +72,15 @@
|
||||||
{{ $service->name }}
|
{{ $service->name }}
|
||||||
</h1>
|
</h1>
|
||||||
<div class="relative flex w-full min-w-0 items-center gap-2">
|
<div class="relative flex w-full min-w-0 items-center gap-2">
|
||||||
<x-status-summary :status="$service->status" title="Service status" container-name="Containers" />
|
<x-status-summary :status="$displayStatus" :title="$selectedResource ? 'Resource status' : 'Service status'"
|
||||||
|
:container-name="$selectedResource ? 'Container' : 'Containers'" />
|
||||||
<x-services.links :service="$service" compact />
|
<x-services.links :service="$service" compact />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex w-full flex-wrap gap-1">
|
||||||
|
@if ($selectedResource)
|
||||||
|
<x-application.restart-limit-warning :application="$selectedResource" />
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -91,7 +104,14 @@
|
||||||
|
|
||||||
<div x-cloak x-show="open" x-transition.origin.top.left
|
<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">
|
class="listbox-panel top-full! left-0! right-0! mt-1! w-full! min-w-0!" role="menu">
|
||||||
@if ($serviceStatus->contains('running') || $serviceStatus->contains('degraded'))
|
@if ($selectedResource && $selectedResource->container_present !== false && $selectedResourceStatus->startsWith('exited'))
|
||||||
|
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||||
|
@click="open = false; document.getElementById('selected-resource-remove-trigger')?.click()"
|
||||||
|
role="menuitem">
|
||||||
|
<x-reicon name="trash" class="size-3.5 text-error" />
|
||||||
|
Remove container
|
||||||
|
</button>
|
||||||
|
@elseif ($serviceStatus->contains('running') || $serviceStatus->contains('degraded'))
|
||||||
@can('deploy', $service)
|
@can('deploy', $service)
|
||||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||||
@click="open = false; document.getElementById('service-restart-trigger')?.click()"
|
@click="open = false; document.getElementById('service-restart-trigger')?.click()"
|
||||||
|
|
@ -195,6 +215,14 @@ class="resource-heading-navbar application-heading-actions flex w-auto min-w-0 i
|
||||||
</button>
|
</button>
|
||||||
<div x-cloak x-show="open" x-transition.origin.top.right
|
<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">
|
class="listbox-panel top-full! right-0! left-auto! mt-1! w-64! min-w-0!" role="menu">
|
||||||
|
@if ($selectedResource && $selectedResource->container_present !== false && $selectedResourceStatus->startsWith('exited'))
|
||||||
|
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||||
|
@click="open = false; document.getElementById('selected-resource-remove-trigger')?.click()"
|
||||||
|
role="menuitem">
|
||||||
|
<x-reicon name="trash" class="size-3.5 text-error" />
|
||||||
|
Remove container
|
||||||
|
</button>
|
||||||
|
@else
|
||||||
@if ($serviceStatus->contains('running') || $serviceStatus->contains('degraded'))
|
@if ($serviceStatus->contains('running') || $serviceStatus->contains('degraded'))
|
||||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||||
@disabled(!auth()->user()->can('deploy', $service))
|
@disabled(!auth()->user()->can('deploy', $service))
|
||||||
|
|
@ -248,6 +276,7 @@ class="listbox-panel top-full! right-0! left-auto! mt-1! w-64! min-w-0!" role="m
|
||||||
Force Cleanup Containers
|
Force Cleanup Containers
|
||||||
</button>
|
</button>
|
||||||
@endif
|
@endif
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endcan
|
@endcan
|
||||||
|
|
@ -281,6 +310,17 @@ class="listbox-panel top-full! right-0! left-auto! mt-1! w-64! min-w-0!" role="m
|
||||||
<button id="service-stop-trigger" type="button">Stop</button>
|
<button id="service-stop-trigger" type="button">Stop</button>
|
||||||
</x-slot:trigger>
|
</x-slot:trigger>
|
||||||
</x-modal-confirmation>
|
</x-modal-confirmation>
|
||||||
|
@if ($selectedResource)
|
||||||
|
<x-modal-confirmation title="Confirm Container Removal?" buttonTitle="Remove container"
|
||||||
|
canGate="deploy" :canResource="$service" submitAction="removeSelectedResourceContainer"
|
||||||
|
:actions="['The exited service resource container will be removed.', __('resource.non_persistent')]"
|
||||||
|
:confirmWithText="false" :confirmWithPassword="false" step1ButtonText="Continue"
|
||||||
|
step2ButtonText="Confirm">
|
||||||
|
<x-slot:trigger>
|
||||||
|
<button id="selected-resource-remove-trigger" type="button">Remove container</button>
|
||||||
|
</x-slot:trigger>
|
||||||
|
</x-modal-confirmation>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,12 @@
|
||||||
<div>
|
<div x-data="{
|
||||||
|
settingsUrl: @js(route('project.service.index', [...$parameters, 'stack_service_uuid' => $resource->uuid])),
|
||||||
|
openSettings(event) {
|
||||||
|
if (event.target.closest('a, button')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Livewire.navigate(this.settingsUrl);
|
||||||
|
}
|
||||||
|
}">
|
||||||
@php
|
@php
|
||||||
[$statusType, $statusLabel] = match (true) {
|
[$statusType, $statusLabel] = match (true) {
|
||||||
str($resource->status)->contains('running') => ['success', formatContainerStatus($resource->status)],
|
str($resource->status)->contains('running') => ['success', formatContainerStatus($resource->status)],
|
||||||
|
|
@ -26,6 +34,7 @@ class="flex size-9 shrink-0 items-center justify-center rounded-lg bg-neutral-10
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<x-status-badge :status="$statusLabel" :type="$statusType" />
|
<x-status-badge :status="$statusLabel" :type="$statusType" />
|
||||||
|
<x-application.restart-limit-warning :application="$resource" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if ($resource->configuration_required)
|
@if ($resource->configuration_required)
|
||||||
|
|
@ -76,7 +85,9 @@ class="flex items-center justify-end gap-1 border-t border-neutral-200 bg-neutra
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div x-cloak x-show="viewMode === 'table'"
|
<div x-cloak x-show="viewMode === 'table'"
|
||||||
class="grid min-h-14 grid-cols-[minmax(0,1fr)_auto] items-center gap-3 border-b border-neutral-200 px-4 py-2.5 last:border-b-0 hover:bg-neutral-50 sm:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_8rem_5rem] dark:border-white/[0.07] dark:hover:bg-white/[0.025]">
|
x-on:click="openSettings($event)" x-on:keydown.enter="openSettings($event)"
|
||||||
|
role="link" tabindex="0" aria-label="Open {{ $resourceName }} settings"
|
||||||
|
class="grid min-h-14 min-w-[48rem] cursor-pointer grid-cols-[minmax(14rem,1fr)_minmax(12rem,1fr)_12rem_5rem] items-center gap-3 border-b border-neutral-200 px-4 py-2.5 last:border-b-0 hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-primary dark:border-white/[0.07] dark:hover:bg-white/[0.025]">
|
||||||
<div class="flex min-w-0 items-center gap-3">
|
<div class="flex min-w-0 items-center gap-3">
|
||||||
<div
|
<div
|
||||||
class="flex size-8 shrink-0 items-center justify-center rounded-lg bg-neutral-100 text-neutral-500 dark:bg-white/[0.06] dark:text-fg-dim">
|
class="flex size-8 shrink-0 items-center justify-center rounded-lg bg-neutral-100 text-neutral-500 dark:bg-white/[0.06] dark:text-fg-dim">
|
||||||
|
|
@ -86,14 +97,14 @@ class="flex size-8 shrink-0 items-center justify-center rounded-lg bg-neutral-10
|
||||||
<div class="truncate text-[13px] font-semibold text-black dark:text-fg">{{ $resourceName }}</div>
|
<div class="truncate text-[13px] font-semibold text-black dark:text-fg">{{ $resourceName }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="hidden truncate font-mono text-xs text-neutral-500 sm:block dark:text-fg-faint">
|
<div class="truncate font-mono text-xs text-neutral-500 dark:text-fg-faint">
|
||||||
{{ $resource->image }}
|
{{ $resource->image }}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap items-center justify-end gap-1 sm:contents">
|
<div class="flex flex-wrap items-center gap-1">
|
||||||
<div class="justify-self-start">
|
<x-status-badge :status="$statusLabel" :type="$statusType" />
|
||||||
<x-status-badge :status="$statusLabel" :type="$statusType" />
|
<x-application.restart-limit-warning :application="$resource" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-end gap-1">
|
<div class="flex items-center justify-end gap-1">
|
||||||
@if ($isDatabase && ($resource->isBackupSolutionAvailable() || $resource->is_migrated))
|
@if ($isDatabase && ($resource->isBackupSolutionAvailable() || $resource->is_migrated))
|
||||||
<a class="icon-button" title="Service backups" aria-label="Service backups" {{ wireNavigate() }}
|
<a class="icon-button" title="Service backups" aria-label="Service backups" {{ wireNavigate() }}
|
||||||
href="{{ route('project.service.volume-backups.index', $parameters) }}">
|
href="{{ route('project.service.volume-backups.index', $parameters) }}">
|
||||||
|
|
@ -109,10 +120,9 @@ class="flex size-8 shrink-0 items-center justify-center rounded-lg bg-neutral-10
|
||||||
@endcan
|
@endcan
|
||||||
@endif
|
@endif
|
||||||
<a class="icon-button" title="Resource settings" aria-label="Resource settings" {{ wireNavigate() }}
|
<a class="icon-button" title="Resource settings" aria-label="Resource settings" {{ wireNavigate() }}
|
||||||
href="{{ route('project.service.index', [...$parameters, 'stack_service_uuid' => $resource->uuid]) }}">
|
href="{{ route('project.service.index', [...$parameters, 'stack_service_uuid' => $resource->uuid]) }}">
|
||||||
<x-reicon name="settings" class="size-4" />
|
<x-reicon name="settings" class="size-4" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,8 @@
|
||||||
<div wire:poll.10000ms="refreshStatus">
|
<div wire:poll.10000ms="refreshStatus">
|
||||||
<x-status-summary :status="$service->status" title="Service status" container-name="Containers" />
|
@php($displayStatus = $selectedResource?->status ?? $service->status)
|
||||||
|
<x-status-summary :status="$displayStatus" :title="$selectedResource ? 'Resource status' : 'Service status'"
|
||||||
|
:container-name="$selectedResource ? 'Container' : 'Containers'" />
|
||||||
|
@if ($selectedResource)
|
||||||
|
<x-application.restart-limit-warning :application="$selectedResource" />
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ class="rounded bg-neutral-100 px-1.5 py-0.5 font-mono text-xs text-neutral-700 d
|
||||||
<div class="flex flex-wrap items-center gap-2 sm:justify-end">
|
<div class="flex flex-wrap items-center gap-2 sm:justify-end">
|
||||||
<a href="{{ route('server.show', ['server_uuid' => data_get($resource, 'destination.server.uuid')]) }}"
|
<a href="{{ route('server.show', ['server_uuid' => data_get($resource, 'destination.server.uuid')]) }}"
|
||||||
{{ wireNavigate() }} class="button">Open server</a>
|
{{ wireNavigate() }} class="button">Open server</a>
|
||||||
|
<x-application.restart-limit-warning :application="$resource" />
|
||||||
<x-status-summary :status="$resource->status" />
|
<x-status-summary :status="$resource->status" />
|
||||||
@if ($hasAdditionalDestinations)
|
@if ($hasAdditionalDestinations)
|
||||||
<x-forms.button canGate="deploy" :canResource="$resource"
|
<x-forms.button canGate="deploy" :canResource="$resource"
|
||||||
|
|
@ -217,6 +218,9 @@ class="rounded bg-neutral-100 px-1.5 py-0.5 font-mono text-xs text-neutral-700 d
|
||||||
<div class="flex flex-wrap items-center gap-2 sm:justify-end">
|
<div class="flex flex-wrap items-center gap-2 sm:justify-end">
|
||||||
<a href="{{ route('server.show', ['server_uuid' => data_get($resource, 'destination.server.uuid')]) }}"
|
<a href="{{ route('server.show', ['server_uuid' => data_get($resource, 'destination.server.uuid')]) }}"
|
||||||
{{ wireNavigate() }} class="button">Open server</a>
|
{{ wireNavigate() }} class="button">Open server</a>
|
||||||
|
@if (method_exists($resource, 'stoppedAfterRestartLimit'))
|
||||||
|
<x-application.restart-limit-warning :application="$resource" />
|
||||||
|
@endif
|
||||||
@if ($primaryStatus->startsWith('running'))
|
@if ($primaryStatus->startsWith('running'))
|
||||||
<x-status.running :status="$primaryStatus->value()" />
|
<x-status.running :status="$primaryStatus->value()" />
|
||||||
@elseif ($primaryStatus->startsWith(['starting', 'restarting']))
|
@elseif ($primaryStatus->startsWith(['starting', 'restarting']))
|
||||||
|
|
|
||||||
|
|
@ -61,10 +61,14 @@ class="data-table-row server-resources-managed-table-grid border-b border-neutra
|
||||||
{{ str($resource->type())->headline() }}
|
{{ str($resource->type())->headline() }}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<x-status-badge :status="str($resourceStatus)->headline()"
|
@if (method_exists($resource, 'stoppedAfterRestartLimit') && $resource->stoppedAfterRestartLimit())
|
||||||
:type="str($resourceStatus)->contains('running')
|
<x-application.restart-limit-warning :application="$resource" />
|
||||||
? 'success'
|
@else
|
||||||
: (str($resourceStatus)->contains(['failed', 'exited']) ? 'error' : 'neutral')" />
|
<x-status-badge :status="str($resourceStatus)->headline()"
|
||||||
|
:type="str($resourceStatus)->contains('running')
|
||||||
|
? 'success'
|
||||||
|
: (str($resourceStatus)->contains(['failed', 'exited']) ? 'error' : 'neutral')" />
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endforeach
|
@endforeach
|
||||||
|
|
|
||||||
223
tests/Feature/AllResourceRestartLimitsTest.php
Normal file
223
tests/Feature/AllResourceRestartLimitsTest.php
Normal file
|
|
@ -0,0 +1,223 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\ApplicationPreview;
|
||||||
|
use App\Models\ServiceApplication;
|
||||||
|
use App\Models\ServiceDatabase;
|
||||||
|
use App\Models\StandaloneClickhouse;
|
||||||
|
use App\Models\StandaloneDragonfly;
|
||||||
|
use App\Models\StandaloneKeydb;
|
||||||
|
use App\Models\StandaloneMariadb;
|
||||||
|
use App\Models\StandaloneMongodb;
|
||||||
|
use App\Models\StandaloneMysql;
|
||||||
|
use App\Models\StandalonePostgresql;
|
||||||
|
use App\Models\StandaloneRedis;
|
||||||
|
use App\Traits\HasRestartLimit;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
it('gives every independently runnable non-application resource restart limit state', function (string $modelClass) {
|
||||||
|
expect(class_uses_recursive($modelClass))->toContain(HasRestartLimit::class);
|
||||||
|
|
||||||
|
$resource = new $modelClass;
|
||||||
|
|
||||||
|
expect($resource->getFillable())->toContain(
|
||||||
|
'restart_count',
|
||||||
|
'max_restart_count',
|
||||||
|
'restart_limit_reached',
|
||||||
|
'last_restart_at',
|
||||||
|
'last_restart_type',
|
||||||
|
)->and($resource->getCasts())->toMatchArray([
|
||||||
|
'restart_count' => 'integer',
|
||||||
|
'max_restart_count' => 'integer',
|
||||||
|
'restart_limit_reached' => 'boolean',
|
||||||
|
'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,
|
||||||
|
]);
|
||||||
|
|
||||||
|
it('collects restart counts for preview and service containers from both status sources', function () {
|
||||||
|
$dockerStatus = file_get_contents(app_path('Actions/Docker/GetContainersStatus.php'));
|
||||||
|
$sentinelStatus = file_get_contents(app_path('Jobs/PushServerUpdateJob.php'));
|
||||||
|
|
||||||
|
expect($dockerStatus)
|
||||||
|
->toContain('previewContainerRestartCounts')
|
||||||
|
->toContain('serviceContainerRestartCounts')
|
||||||
|
->and($sentinelStatus)
|
||||||
|
->toContain('previewContainerRestartCounts')
|
||||||
|
->toContain('serviceContainerRestartCounts');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores docker compose one-off job containers in both status sources', function () {
|
||||||
|
$dockerStatus = file_get_contents(app_path('Actions/Docker/GetContainersStatus.php'));
|
||||||
|
$sentinelStatus = file_get_contents(app_path('Jobs/PushServerUpdateJob.php'));
|
||||||
|
|
||||||
|
expect($dockerStatus)
|
||||||
|
->toContain("filter_var(data_get(\$labels, 'com.docker.compose.oneoff'), FILTER_VALIDATE_BOOLEAN)")
|
||||||
|
->and($sentinelStatus)
|
||||||
|
->toContain("filter_var(\$labels->get('com.docker.compose.oneoff'), FILTER_VALIDATE_BOOLEAN)");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows restart limit warnings for every resource family', function () {
|
||||||
|
$previews = file_get_contents(resource_path('views/livewire/project/application/previews.blade.php'));
|
||||||
|
$serviceCard = file_get_contents(resource_path('views/livewire/project/service/resource-card.blade.php'));
|
||||||
|
$applicationStatus = file_get_contents(resource_path('views/livewire/project/application/status.blade.php'));
|
||||||
|
$databaseStatus = file_get_contents(resource_path('views/livewire/project/database/status.blade.php'));
|
||||||
|
|
||||||
|
expect($previews)->toContain('<x-application.restart-limit-warning :application="$preview" />')
|
||||||
|
->and($serviceCard)->toContain('<x-application.restart-limit-warning :application="$resource" />')
|
||||||
|
->and($applicationStatus)->toContain('<x-application.restart-limit-warning :application="$application" />')
|
||||||
|
->and($databaseStatus)->toContain('<x-application.restart-limit-warning :application="$database" />');
|
||||||
|
|
||||||
|
$serviceStatus = file_get_contents(resource_path('views/livewire/project/service/status.blade.php'));
|
||||||
|
$serviceHeading = file_get_contents(resource_path('views/livewire/project/service/heading.blade.php'));
|
||||||
|
expect($serviceStatus)->toContain('<x-application.restart-limit-warning :application="$selectedResource" />')
|
||||||
|
->and($serviceStatus)->toContain('$selectedResource?->status ?? $service->status')
|
||||||
|
->and($serviceHeading)->toContain('<x-application.restart-limit-warning :application="$selectedResource" />')
|
||||||
|
->and($serviceHeading)->toContain('$selectedResource?->status ?? $service->status');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('matches application restart badge layout on mobile resource headings', function () {
|
||||||
|
$applicationHeading = file_get_contents(resource_path('views/livewire/project/application/heading.blade.php'));
|
||||||
|
$databaseHeading = file_get_contents(resource_path('views/livewire/project/database/heading.blade.php'));
|
||||||
|
$serviceHeading = file_get_contents(resource_path('views/livewire/project/service/heading.blade.php'));
|
||||||
|
|
||||||
|
foreach ([$applicationHeading, $databaseHeading, $serviceHeading] as $heading) {
|
||||||
|
expect($heading)
|
||||||
|
->toContain('class="relative flex w-full min-w-0 items-center gap-2"')
|
||||||
|
->toContain('class="flex w-full flex-wrap gap-1"');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows database restart limits in shared resource listings', function () {
|
||||||
|
$resourceIndex = file_get_contents(app_path('Livewire/Project/Resource/Index.php'));
|
||||||
|
$serverResources = file_get_contents(resource_path('views/livewire/server/resources.blade.php'));
|
||||||
|
$destination = file_get_contents(resource_path('views/livewire/project/shared/destination.blade.php'));
|
||||||
|
|
||||||
|
expect($resourceIndex)
|
||||||
|
->toContain("method_exists(\$item, 'stoppedAfterRestartLimit')")
|
||||||
|
->not->toContain("\$type === 'application' && \$item->stoppedAfterRestartLimit()")
|
||||||
|
->and($serverResources)
|
||||||
|
->toContain('<x-application.restart-limit-warning :application="$resource" />')
|
||||||
|
->and($destination)
|
||||||
|
->toContain('<x-application.restart-limit-warning :application="$resource" />');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps the service resource table readable with horizontal scrolling on mobile', function () {
|
||||||
|
$configuration = file_get_contents(resource_path('views/livewire/project/service/configuration.blade.php'));
|
||||||
|
$resourceCard = file_get_contents(resource_path('views/livewire/project/service/resource-card.blade.php'));
|
||||||
|
|
||||||
|
expect($configuration)
|
||||||
|
->toContain("'overflow-x-auto rounded-xl")
|
||||||
|
->toContain('min-w-[48rem]')
|
||||||
|
->and($resourceCard)
|
||||||
|
->toContain('min-w-[48rem]')
|
||||||
|
->not->toContain('<div class="hidden truncate font-mono');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('opens service resource settings when a table row is clicked', function () {
|
||||||
|
$resourceCard = file_get_contents(resource_path('views/livewire/project/service/resource-card.blade.php'));
|
||||||
|
|
||||||
|
expect($resourceCard)
|
||||||
|
->toContain('x-on:click="openSettings($event)"')
|
||||||
|
->toContain('x-on:keydown.enter="openSettings($event)"')
|
||||||
|
->toContain("closest('a, button')")
|
||||||
|
->toContain('role="link"')
|
||||||
|
->toContain('tabindex="0"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses selected service resource actions instead of parent complex status actions', function () {
|
||||||
|
$heading = file_get_contents(resource_path('views/livewire/project/service/heading.blade.php'));
|
||||||
|
$headingClass = file_get_contents(app_path('Livewire/Project/Service/Heading.php'));
|
||||||
|
|
||||||
|
expect(substr_count($heading, "\$selectedResource && \$selectedResource->container_present !== false && \$selectedResourceStatus->startsWith('exited')"))->toBe(2)
|
||||||
|
->and($heading)
|
||||||
|
->toContain('Remove container')
|
||||||
|
->toContain('removeSelectedResourceContainer')
|
||||||
|
->and($headingClass)
|
||||||
|
->toContain('public function removeSelectedResourceContainer(): void');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('imports the application model used when claiming a restart limit', function () {
|
||||||
|
$statusAction = file_get_contents(app_path('Actions/Docker/GetContainersStatus.php'));
|
||||||
|
|
||||||
|
expect($statusAction)
|
||||||
|
->toContain('use App\\Models\\Application;')
|
||||||
|
->toContain('Application::query()');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds restart limit columns to previews services and standalone databases', function () {
|
||||||
|
$migrations = collect(glob(database_path('migrations/*.php')))
|
||||||
|
->map(fn (string $path): string => file_get_contents($path))
|
||||||
|
->implode("\n");
|
||||||
|
|
||||||
|
expect($migrations)
|
||||||
|
->toContain("'application_previews'")
|
||||||
|
->toContain("'service_applications'")
|
||||||
|
->toContain("'service_databases'")
|
||||||
|
->toContain("'max_restart_count'")
|
||||||
|
->toContain("'restart_limit_reached'");
|
||||||
|
|
||||||
|
$restartLimitMigrations = collect(glob(database_path('migrations/*_add_restart_limit_to_*.php')));
|
||||||
|
|
||||||
|
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 (');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('atomically claims a resource restart limit once and can reset it', function () {
|
||||||
|
Schema::create('restart_limit_test_resources', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->string('status')->default('running');
|
||||||
|
$table->integer('restart_count')->default(0);
|
||||||
|
$table->integer('max_restart_count')->default(2);
|
||||||
|
$table->boolean('restart_limit_reached')->default(false);
|
||||||
|
$table->timestamp('last_restart_at')->nullable();
|
||||||
|
$table->string('last_restart_type')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
|
||||||
|
$resource = new class extends Model
|
||||||
|
{
|
||||||
|
use HasRestartLimit;
|
||||||
|
|
||||||
|
protected $table = 'restart_limit_test_resources';
|
||||||
|
};
|
||||||
|
$resource->save();
|
||||||
|
$resource->refresh();
|
||||||
|
|
||||||
|
expect($resource->trackRestartCount(2))->toBeTrue()
|
||||||
|
->and($resource->fresh()->restart_limit_reached)->toBeTrue()
|
||||||
|
->and($resource->trackRestartCount(2))->toBeFalse();
|
||||||
|
|
||||||
|
$resource->resetRestartLimit();
|
||||||
|
|
||||||
|
expect($resource->fresh()->restart_count)->toBe(0)
|
||||||
|
->and($resource->restart_limit_reached)->toBeFalse();
|
||||||
|
|
||||||
|
$resourceWithExistingRestarts = $resource->newInstance();
|
||||||
|
$resourceWithExistingRestarts->max_restart_count = 0;
|
||||||
|
$resourceWithExistingRestarts->save();
|
||||||
|
expect($resourceWithExistingRestarts->trackRestartCount(17))->toBeFalse();
|
||||||
|
|
||||||
|
$resourceWithExistingRestarts->update(['max_restart_count' => 10]);
|
||||||
|
expect($resourceWithExistingRestarts->trackRestartCount(17))->toBeTrue()
|
||||||
|
->and($resourceWithExistingRestarts->fresh()->restart_limit_reached)->toBeTrue();
|
||||||
|
|
||||||
|
Schema::drop('restart_limit_test_resources');
|
||||||
|
});
|
||||||
56
tests/Feature/ApplicationContainerPresenceTest.php
Normal file
56
tests/Feature/ApplicationContainerPresenceTest.php
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Application;
|
||||||
|
|
||||||
|
it('stores nullable application container presence as a boolean', function () {
|
||||||
|
$application = new Application;
|
||||||
|
$application->forceFill(['container_present' => 1]);
|
||||||
|
$migration = file_get_contents(base_path('database/migrations/2026_08_30_193506_add_container_present_to_applications_table.php'));
|
||||||
|
|
||||||
|
expect($application->container_present)->toBeTrue()
|
||||||
|
->and($migration)->toContain("boolean('container_present')->nullable()");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates container presence at application lifecycle boundaries', function () {
|
||||||
|
$stopAction = file_get_contents(app_path('Actions/Application/StopApplication.php'));
|
||||||
|
$dockerStatus = file_get_contents(app_path('Actions/Docker/GetContainersStatus.php'));
|
||||||
|
$sentinelStatus = file_get_contents(app_path('Jobs/PushServerUpdateJob.php'));
|
||||||
|
|
||||||
|
expect($stopAction)->toContain('$containerPresent = ! $removeContainers;')
|
||||||
|
->and($stopAction)->toMatch('/if \(\$server->isSwarm\(\)\).*?\$containerPresent = false;.*?docker stack rm/s')
|
||||||
|
->and($stopAction)->toContain("'container_present' => \$containerPresent")
|
||||||
|
->and($dockerStatus)->toContain("'container_present' => true")
|
||||||
|
->and($dockerStatus)->toContain("'container_present' => false")
|
||||||
|
->and($sentinelStatus)->toContain("'container_present' => true")
|
||||||
|
->and($sentinelStatus)->toContain("'container_present' => false");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows accurate destructive actions and the restart warning on mobile', function () {
|
||||||
|
$heading = file_get_contents(resource_path('views/livewire/project/application/heading.blade.php'));
|
||||||
|
$mobileActions = str($heading)
|
||||||
|
->after('id="application-mobile-actions"')
|
||||||
|
->before('<div class="hidden" aria-hidden="true">')
|
||||||
|
->toString();
|
||||||
|
|
||||||
|
expect($heading)->toContain('<x-application.restart-limit-warning :application="$application" />')
|
||||||
|
->and(substr_count($heading, '$application->container_present !== false'))->toBe(2)
|
||||||
|
->and($heading)->toContain("\$application->stoppedAfterRestartLimit() ? 'Retry deployment' : 'Deploy'")
|
||||||
|
->and($heading)->toContain("\$application->stoppedAfterRestartLimit() ? 'Retry deployment (without cache)' : 'Deploy (without cache)'")
|
||||||
|
->and($heading)->toContain('Remove container')
|
||||||
|
->and($mobileActions)->toContain('Deploy (without cache)')
|
||||||
|
->and($mobileActions)->toContain('Remove container')
|
||||||
|
->and(strrpos($mobileActions, 'Remove container'))->toBeGreaterThan(strrpos($mobileActions, 'Deploy (without cache)'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows restart limit reached as a yellow state in environment resource lists', function () {
|
||||||
|
$indexClass = file_get_contents(app_path('Livewire/Project/Resource/Index.php'));
|
||||||
|
$indexView = file_get_contents(resource_path('views/livewire/project/resource/index.blade.php'));
|
||||||
|
|
||||||
|
expect($indexClass)->toContain("'restartLimitReached' => \$type === 'application' && \$item->stoppedAfterRestartLimit()")
|
||||||
|
->and($indexClass)->toContain('? max($item->restart_count ?? 0, $item->max_restart_count ?? 0)')
|
||||||
|
->and($indexClass)->toContain("'maxRestartCount' => \$item->max_restart_count ?? 0")
|
||||||
|
->and($indexView)->toContain("if (item.restartLimitReached) {\n return 'restart-limit';")
|
||||||
|
->and($indexView)->toContain("return 'Restart limit reached';")
|
||||||
|
->and($indexView)->toContain("if (item.restartLimitReached) {\n return 'bg-warning';")
|
||||||
|
->and($indexView)->toContain('x-bind:title="statusTitle(item)"');
|
||||||
|
});
|
||||||
|
|
@ -1,16 +1,24 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Actions\Application\StopApplication;
|
use App\Actions\Application\StopApplication;
|
||||||
|
use App\Actions\Docker\GetContainersStatus;
|
||||||
|
use App\Jobs\ApplicationDeploymentJob;
|
||||||
use App\Models\Application;
|
use App\Models\Application;
|
||||||
|
use App\Models\ApplicationPreview;
|
||||||
|
use App\Models\BaseModel;
|
||||||
|
use App\Models\Server;
|
||||||
use App\Notifications\Application\RestartLimitReached;
|
use App\Notifications\Application\RestartLimitReached;
|
||||||
|
use Mockery\MockInterface;
|
||||||
|
|
||||||
function applicationWithRestartState(array $attributes = []): Application
|
function applicationWithRestartState(array $attributes = []): Application
|
||||||
{
|
{
|
||||||
$application = new Application;
|
$application = new Application;
|
||||||
$application->forceFill(array_merge([
|
$application->forceFill(array_merge([
|
||||||
'status' => 'exited:unhealthy',
|
'status' => 'exited:unhealthy',
|
||||||
|
'container_present' => true,
|
||||||
'restart_count' => 2,
|
'restart_count' => 2,
|
||||||
'max_restart_count' => 2,
|
'max_restart_count' => 2,
|
||||||
|
'restart_limit_reached' => true,
|
||||||
'last_restart_type' => 'crash',
|
'last_restart_type' => 'crash',
|
||||||
'last_restart_at' => now(),
|
'last_restart_at' => now(),
|
||||||
], $attributes));
|
], $attributes));
|
||||||
|
|
@ -21,9 +29,51 @@ function applicationWithRestartState(array $attributes = []): Application
|
||||||
it('detects applications stopped after reaching the crash restart limit', function () {
|
it('detects applications stopped after reaching the crash restart limit', function () {
|
||||||
expect(applicationWithRestartState()->stoppedAfterRestartLimit())->toBeTrue()
|
expect(applicationWithRestartState()->stoppedAfterRestartLimit())->toBeTrue()
|
||||||
->and(applicationWithRestartState(['status' => 'running:unhealthy'])->stoppedAfterRestartLimit())->toBeFalse()
|
->and(applicationWithRestartState(['status' => 'running:unhealthy'])->stoppedAfterRestartLimit())->toBeFalse()
|
||||||
->and(applicationWithRestartState(['restart_count' => 1])->stoppedAfterRestartLimit())->toBeFalse()
|
->and(applicationWithRestartState(['restart_limit_reached' => false])->stoppedAfterRestartLimit())->toBeFalse();
|
||||||
->and(applicationWithRestartState(['max_restart_count' => 0])->stoppedAfterRestartLimit())->toBeFalse()
|
});
|
||||||
->and(applicationWithRestartState(['last_restart_type' => null])->stoppedAfterRestartLimit())->toBeFalse();
|
|
||||||
|
it('keeps the restart limit state after Docker resets its counter', function () {
|
||||||
|
expect(applicationWithRestartState([
|
||||||
|
'restart_count' => 0,
|
||||||
|
'last_restart_type' => null,
|
||||||
|
'last_restart_at' => null,
|
||||||
|
])->stoppedAfterRestartLimit())->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves exited application state when the container snapshot is empty', function () {
|
||||||
|
$application = Mockery::mock(Application::class)->makePartial();
|
||||||
|
$application->setRelation('additional_servers', collect());
|
||||||
|
$application->forceFill([
|
||||||
|
'id' => 1,
|
||||||
|
'status' => 'exited:unhealthy',
|
||||||
|
'container_present' => true,
|
||||||
|
'restart_limit_reached' => true,
|
||||||
|
]);
|
||||||
|
$application->shouldNotReceive('update');
|
||||||
|
|
||||||
|
$services = Mockery::mock();
|
||||||
|
$services->shouldReceive('get')->once()->andReturn(collect());
|
||||||
|
|
||||||
|
$server = Mockery::mock(Server::class, function (MockInterface $mock) use ($application, $services) {
|
||||||
|
$mock->shouldReceive('isFunctional')->once()->andReturnTrue();
|
||||||
|
$mock->shouldReceive('applications')->once()->andReturn(collect([$application]));
|
||||||
|
$mock->shouldReceive('databases')->once()->andReturn(collect());
|
||||||
|
$mock->shouldReceive('services')->once()->andReturn($services);
|
||||||
|
$mock->shouldReceive('previews')->once()->andReturn(collect());
|
||||||
|
})->makePartial();
|
||||||
|
$server->setRelation('team', (object) ['id' => 1]);
|
||||||
|
|
||||||
|
GetContainersStatus::run($server, collect(), collect());
|
||||||
|
|
||||||
|
expect($application->container_present)->toBeTrue()
|
||||||
|
->and($application->restart_limit_reached)->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not infer the restart limit from an exited existing container', function () {
|
||||||
|
expect(applicationWithRestartState([
|
||||||
|
'container_present' => true,
|
||||||
|
'restart_limit_reached' => false,
|
||||||
|
])->stoppedAfterRestartLimit())->toBeFalse();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows a stopped after restart limit warning in the status badge', function () {
|
it('shows a stopped after restart limit warning in the status badge', function () {
|
||||||
|
|
@ -32,7 +82,8 @@ function applicationWithRestartState(array $attributes = []): Application
|
||||||
'showRefreshButton' => false,
|
'showRefreshButton' => false,
|
||||||
])->render();
|
])->render();
|
||||||
|
|
||||||
expect($html)->toContain('Stopped after reaching restart limit (2/2).')
|
expect($html)->toContain('Restart limit reached')
|
||||||
|
->not->toContain('Stopped after reaching restart limit (2/2).')
|
||||||
->and($html)->toContain('Container has crashed and Coolify stopped it after 2 restart attempts.');
|
->and($html)->toContain('Container has crashed and Coolify stopped it after 2 restart attempts.');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -41,11 +92,12 @@ function applicationWithRestartState(array $attributes = []): Application
|
||||||
'resource' => applicationWithRestartState([
|
'resource' => applicationWithRestartState([
|
||||||
'restart_count' => 0,
|
'restart_count' => 0,
|
||||||
'last_restart_type' => null,
|
'last_restart_type' => null,
|
||||||
|
'restart_limit_reached' => false,
|
||||||
]),
|
]),
|
||||||
'showRefreshButton' => false,
|
'showRefreshButton' => false,
|
||||||
])->render();
|
])->render();
|
||||||
|
|
||||||
expect($html)->not->toContain('Stopped after reaching restart limit');
|
expect($html)->not->toContain('Restart limit reached');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps restart tracking configurable when stopping an application', function () {
|
it('keeps restart tracking configurable when stopping an application', function () {
|
||||||
|
|
@ -56,6 +108,59 @@ function applicationWithRestartState(array $attributes = []): Application
|
||||||
->and($resetRestartCount->getDefaultValue())->toBeTrue();
|
->and($resetRestartCount->getDefaultValue())->toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('can stop an application without removing its containers', function () {
|
||||||
|
$method = new ReflectionMethod(StopApplication::class, 'handle');
|
||||||
|
$removeContainers = collect($method->getParameters())->firstWhere('name', 'removeContainers');
|
||||||
|
$action = file_get_contents(app_path('Actions/Application/StopApplication.php'));
|
||||||
|
|
||||||
|
expect($removeContainers)->not->toBeNull()
|
||||||
|
->and($removeContainers->getDefaultValue())->toBeTrue()
|
||||||
|
->and($action)->toContain('docker update --restart=no')
|
||||||
|
->and($action)->toContain('if ($removeContainers)');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves containers and skips cleanup when the restart limit is reached', function () {
|
||||||
|
$statusAction = file_get_contents(app_path('Actions/Docker/GetContainersStatus.php'));
|
||||||
|
$sentinelJob = file_get_contents(app_path('Jobs/PushServerUpdateJob.php'));
|
||||||
|
|
||||||
|
expect($statusAction)->toContain('dockerCleanup: false')
|
||||||
|
->and($statusAction)->toContain('resetRestartCount: false')
|
||||||
|
->and($statusAction)->toContain('removeContainers: false')
|
||||||
|
->and($statusAction)->toContain("['restart_limit_reached' => true]")
|
||||||
|
->and($sentinelJob)->toContain("['restart_limit_reached' => true]");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('atomically claims the restart limit transition before stopping and notifying', function () {
|
||||||
|
$statusAction = file_get_contents(app_path('Actions/Docker/GetContainersStatus.php'));
|
||||||
|
$sentinelJob = file_get_contents(app_path('Jobs/PushServerUpdateJob.php'));
|
||||||
|
|
||||||
|
foreach ([$statusAction, $sentinelJob] as $detector) {
|
||||||
|
expect($detector)
|
||||||
|
->toContain("->where('restart_limit_reached', false)")
|
||||||
|
->toContain("->update(['restart_limit_reached' => true]) === 1");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clears the explicit restart limit state only after a successful main deployment', function () {
|
||||||
|
$method = new ReflectionMethod(ApplicationDeploymentJob::class, 'handleSuccessfulDeployment');
|
||||||
|
$source = file($method->getFileName());
|
||||||
|
$deploymentJob = implode(array_slice($source, $method->getStartLine() - 1, $method->getEndLine() - $method->getStartLine() + 1));
|
||||||
|
|
||||||
|
expect(substr_count($deploymentJob, "'restart_limit_reached'] = false"))->toBe(1)
|
||||||
|
->and($deploymentJob)->toContain("if (\$this->pull_request_id === 0) {\n \$restartState['restart_limit_reached'] = false;\n }")
|
||||||
|
->and($deploymentJob)->toContain('$this->application->update($restartState);')
|
||||||
|
->and(substr_count($deploymentJob, '$this->application->update('))->toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves restart-limit applications only while their exited container exists', function () {
|
||||||
|
$statusAction = file_get_contents(app_path('Actions/Docker/GetContainersStatus.php'));
|
||||||
|
$sentinelJob = file_get_contents(app_path('Jobs/PushServerUpdateJob.php'));
|
||||||
|
|
||||||
|
expect($statusAction)->toContain("'container_present' => false")
|
||||||
|
->and($statusAction)->toContain("'restart_limit_reached' => false")
|
||||||
|
->and($sentinelJob)->toContain('if ($application->stoppedAfterRestartLimit() && $containerStatuses->every(');
|
||||||
|
});
|
||||||
|
|
||||||
it('uses the application link for restart limit notifications', function () {
|
it('uses the application link for restart limit notifications', function () {
|
||||||
$application = new class extends Application
|
$application = new class extends Application
|
||||||
{
|
{
|
||||||
|
|
@ -80,3 +185,55 @@ public function link()
|
||||||
|
|
||||||
expect($notification->resource_url)->toBe('https://coolify.test/project/link-from-model');
|
expect($notification->resource_url)->toBe('https://coolify.test/project/link-from-model');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('uses the resolved environment project name in Slack restart limit notifications', function () {
|
||||||
|
$environment = (object) [
|
||||||
|
'uuid' => 'environment-uuid',
|
||||||
|
'name' => 'production',
|
||||||
|
'project' => (object) [
|
||||||
|
'uuid' => 'project-uuid',
|
||||||
|
'name' => 'Coolify',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$application = new class extends Application
|
||||||
|
{
|
||||||
|
public function link(): string
|
||||||
|
{
|
||||||
|
return 'https://coolify.test/application';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
$application->forceFill(['name' => 'app']);
|
||||||
|
$application->setRelation('environment', $environment);
|
||||||
|
|
||||||
|
$preview = new ApplicationPreview;
|
||||||
|
$preview->forceFill([
|
||||||
|
'uuid' => 'preview-uuid',
|
||||||
|
'pull_request_id' => 42,
|
||||||
|
'restart_count' => 2,
|
||||||
|
'max_restart_count' => 2,
|
||||||
|
]);
|
||||||
|
$preview->setRelation('application', $application);
|
||||||
|
|
||||||
|
$serviceResource = new class extends BaseModel {};
|
||||||
|
$serviceResource->forceFill([
|
||||||
|
'name' => 'database',
|
||||||
|
'uuid' => 'service-resource-uuid',
|
||||||
|
'restart_count' => 2,
|
||||||
|
'max_restart_count' => 2,
|
||||||
|
]);
|
||||||
|
$serviceResource->setRelation('service', new class($environment)
|
||||||
|
{
|
||||||
|
public function __construct(public object $environment) {}
|
||||||
|
|
||||||
|
public function link(): string
|
||||||
|
{
|
||||||
|
return 'https://coolify.test/service';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect((new RestartLimitReached($preview))->toSlack()->description)
|
||||||
|
->toContain('*Project:* Coolify')
|
||||||
|
->and((new RestartLimitReached($serviceResource))->toSlack()->description)
|
||||||
|
->toContain('*Project:* Coolify');
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -132,6 +132,80 @@
|
||||||
expect($this->admin->can('update', $settings))->toBeTrue();
|
expect($this->admin->can('update', $settings))->toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('telegram restart limit thread id accepts 255 characters', function () {
|
||||||
|
$this->actingAs($this->admin);
|
||||||
|
session(['currentTeam' => $this->team]);
|
||||||
|
|
||||||
|
Livewire::test(TelegramNotification::class)
|
||||||
|
->set('telegramNotificationsRestartLimitReachedThreadId', str_repeat('a', 255))
|
||||||
|
->call('syncData', true)
|
||||||
|
->assertHasNoErrors(['telegramNotificationsRestartLimitReachedThreadId']);
|
||||||
|
|
||||||
|
expect($this->team->telegramNotificationSettings->fresh()->telegram_notifications_restart_limit_reached_thread_id)
|
||||||
|
->toBe(str_repeat('a', 255));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('telegram restart limit thread id rejects 256 characters', function () {
|
||||||
|
$this->actingAs($this->admin);
|
||||||
|
session(['currentTeam' => $this->team]);
|
||||||
|
|
||||||
|
Livewire::test(TelegramNotification::class)
|
||||||
|
->set('telegramNotificationsRestartLimitReachedThreadId', str_repeat('a', 256))
|
||||||
|
->call('syncData', true)
|
||||||
|
->assertHasErrors(['telegramNotificationsRestartLimitReachedThreadId' => 'max']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('member cannot view telegram thread ids', function () {
|
||||||
|
$threadIds = [
|
||||||
|
'telegram_notifications_deployment_success_thread_id' => 'deployment-success-thread',
|
||||||
|
'telegram_notifications_deployment_failure_thread_id' => 'deployment-failure-thread',
|
||||||
|
'telegram_notifications_status_change_thread_id' => 'status-change-thread',
|
||||||
|
'telegram_notifications_restart_limit_reached_thread_id' => 'restart-limit-thread',
|
||||||
|
'telegram_notifications_backup_success_thread_id' => 'backup-success-thread',
|
||||||
|
'telegram_notifications_backup_failure_thread_id' => 'backup-failure-thread',
|
||||||
|
'telegram_notifications_scheduled_task_success_thread_id' => 'scheduled-task-success-thread',
|
||||||
|
'telegram_notifications_scheduled_task_failure_thread_id' => 'scheduled-task-failure-thread',
|
||||||
|
'telegram_notifications_docker_cleanup_success_thread_id' => 'docker-cleanup-success-thread',
|
||||||
|
'telegram_notifications_docker_cleanup_failure_thread_id' => 'docker-cleanup-failure-thread',
|
||||||
|
'telegram_notifications_server_disk_usage_thread_id' => 'server-disk-usage-thread',
|
||||||
|
'telegram_notifications_server_reachable_thread_id' => 'server-reachable-thread',
|
||||||
|
'telegram_notifications_server_unreachable_thread_id' => 'server-unreachable-thread',
|
||||||
|
'telegram_notifications_server_patch_thread_id' => 'server-patch-thread',
|
||||||
|
'telegram_notifications_traefik_outdated_thread_id' => 'traefik-outdated-thread',
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->team->telegramNotificationSettings->update($threadIds);
|
||||||
|
|
||||||
|
$this->actingAs($this->member);
|
||||||
|
session(['currentTeam' => $this->team]);
|
||||||
|
|
||||||
|
$component = Livewire::test(TelegramNotification::class);
|
||||||
|
|
||||||
|
foreach ($threadIds as $column => $threadId) {
|
||||||
|
$component
|
||||||
|
->assertSet(str($column)->camel()->toString(), null)
|
||||||
|
->assertDontSee($threadId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('admin can view telegram thread ids', function () {
|
||||||
|
$threadIds = [
|
||||||
|
'telegram_notifications_deployment_success_thread_id' => 'deployment-success-thread',
|
||||||
|
'telegram_notifications_restart_limit_reached_thread_id' => 'restart-limit-thread',
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->team->telegramNotificationSettings->update($threadIds);
|
||||||
|
|
||||||
|
$this->actingAs($this->admin);
|
||||||
|
session(['currentTeam' => $this->team]);
|
||||||
|
|
||||||
|
$component = Livewire::test(TelegramNotification::class);
|
||||||
|
|
||||||
|
foreach ($threadIds as $column => $threadId) {
|
||||||
|
$component->assertSet(str($column)->camel()->toString(), $threadId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// --- Email ---
|
// --- Email ---
|
||||||
|
|
||||||
test('member cannot send test email notification', function () {
|
test('member cannot send test email notification', function () {
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,22 @@
|
||||||
'server actions' => ['views/livewire/server/navbar.blade.php', 'manageProxy', 'server', 'server'],
|
'server actions' => ['views/livewire/server/navbar.blade.php', 'manageProxy', 'server', 'server'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
it('declares deploy authorization on the application stop confirmation', function () {
|
||||||
|
$source = file_get_contents(resource_path('views/livewire/project/application/heading.blade.php'));
|
||||||
|
|
||||||
|
expect($source)->toMatch(
|
||||||
|
'/<x-modal-confirmation\s+canGate="deploy" :canResource="\$application"/'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('declares deploy authorization on the service container removal confirmation', function () {
|
||||||
|
$source = file_get_contents(resource_path('views/livewire/project/service/heading.blade.php'));
|
||||||
|
|
||||||
|
expect($source)->toMatch(
|
||||||
|
'/<x-modal-confirmation(?=[^>]*title="Confirm Container Removal\?")(?=[^>]*canGate="deploy")(?=[^>]*:canResource="\$service")[^>]*>/'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('keeps mutable Livewire components behind authorization checks', function (string $path, array $requiredNeedles) {
|
it('keeps mutable Livewire components behind authorization checks', function (string $path, array $requiredNeedles) {
|
||||||
$source = file_get_contents(base_path($path));
|
$source = file_get_contents(base_path($path));
|
||||||
|
|
||||||
|
|
|
||||||
48
tests/Feature/NotificationRestartLimitSettingTest.php
Normal file
48
tests/Feature/NotificationRestartLimitSettingTest.php
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
it('uses a dedicated notification event for restart limits', function () {
|
||||||
|
$notification = file_get_contents(app_path('Notifications/Application/RestartLimitReached.php'));
|
||||||
|
$telegramChannel = file_get_contents(app_path('Notifications/Channels/TelegramChannel.php'));
|
||||||
|
$eventGrid = file_get_contents(resource_path('views/components/notification/event-grid.blade.php'));
|
||||||
|
|
||||||
|
expect($notification)->toContain("getEnabledChannels('restart_limit_reached')")
|
||||||
|
->and($eventGrid)
|
||||||
|
->toContain("'Resources' => [")
|
||||||
|
->toContain("'key' => 'statusChange'")
|
||||||
|
->toContain("'helper' => 'Notify when a resource stops or Coolify automatically restarts it.'")
|
||||||
|
->toContain("'key' => 'restartLimitReached'")
|
||||||
|
->toContain("'label' => 'Restart limit reached'")
|
||||||
|
->and($telegramChannel)
|
||||||
|
->toContain('RestartLimitReached::class => $settings->telegram_notifications_restart_limit_reached_thread_id');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('persists a restart limit notification preference for every channel', function (string $channel) {
|
||||||
|
$studly = Str::studly($channel);
|
||||||
|
$component = file_get_contents(app_path("Livewire/Notifications/{$studly}.php"));
|
||||||
|
$model = file_get_contents(app_path('Models/'.$studly.'NotificationSettings.php'));
|
||||||
|
$column = "restart_limit_reached_{$channel}_notifications";
|
||||||
|
$property = "restartLimitReached{$studly}Notifications";
|
||||||
|
|
||||||
|
expect($component)
|
||||||
|
->toContain("public bool \${$property} = true;")
|
||||||
|
->toContain("\$this->settings->{$column} = \$this->{$property};")
|
||||||
|
->toContain("\$this->{$property} = \$this->settings->{$column};")
|
||||||
|
->and($model)
|
||||||
|
->toContain("'{$column}'");
|
||||||
|
})->with(['email', 'discord', 'telegram', 'slack', 'pushover', 'webhook']);
|
||||||
|
|
||||||
|
it('enables restart limit notifications by default in every channel migration', function () {
|
||||||
|
$migrations = collect(glob(database_path('migrations/*_add_restart_limit_reached_notifications_to_*')));
|
||||||
|
|
||||||
|
expect($migrations)->toHaveCount(6);
|
||||||
|
$migrations->each(fn (string $migration) => expect(file_get_contents($migration))->toContain('->default(true)'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses the inline validator facade for notification API updates', function () {
|
||||||
|
$controller = file_get_contents(app_path('Http/Controllers/Api/NotificationsController.php'));
|
||||||
|
|
||||||
|
expect($controller)
|
||||||
|
->toContain('use Illuminate\\Support\\Facades\\Validator;')
|
||||||
|
->toContain("Validator::make(\$body, \$config['rules'])")
|
||||||
|
->not->toContain("customApiValidator(\$body, \$config['rules'])");
|
||||||
|
});
|
||||||
|
|
@ -19,6 +19,16 @@
|
||||||
->toContain('w-[min(16rem,calc(100vw-1.5rem))]!');
|
->toContain('w-[min(16rem,calc(100vw-1.5rem))]!');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows degraded aggregate service status as a warning', function () {
|
||||||
|
$html = Blade::render('<x-status-summary status="degraded:unhealthy" title="Service status" container-name="Containers" />');
|
||||||
|
$summaryButton = str($html)->between('<button', '</button>')->toString();
|
||||||
|
|
||||||
|
expect($summaryButton)
|
||||||
|
->toContain('Degraded')
|
||||||
|
->toContain('bg-warning')
|
||||||
|
->not->toContain('bg-error');
|
||||||
|
});
|
||||||
|
|
||||||
it('uses the aggregated preview status in the previews list', function () {
|
it('uses the aggregated preview status in the previews list', function () {
|
||||||
$view = file_get_contents(resource_path('views/livewire/project/application/previews.blade.php'));
|
$view = file_get_contents(resource_path('views/livewire/project/application/previews.blade.php'));
|
||||||
|
|
||||||
|
|
@ -42,7 +52,7 @@
|
||||||
expect($databaseStatus)
|
expect($databaseStatus)
|
||||||
->toContain('<x-status-summary :status="$database->status" title="Database status" />')
|
->toContain('<x-status-summary :status="$database->status" title="Database status" />')
|
||||||
->and($serviceStatus)
|
->and($serviceStatus)
|
||||||
->toContain('<x-status-summary :status="$service->status" title="Service status" container-name="Containers" />');
|
->toContain('<x-status-summary :status="$displayStatus" :title="$selectedResource ? \'Resource status\' : \'Service status\'"');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('groups preview deployment actions in a dropdown', function () {
|
it('groups preview deployment actions in a dropdown', function () {
|
||||||
|
|
|
||||||
|
|
@ -255,6 +255,25 @@
|
||||||
->not->toContain('Force deploy without cache');
|
->not->toContain('Force deploy without cache');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows stop in application action menus when the application is exited', function () {
|
||||||
|
$heading = file_get_contents(resource_path('views/livewire/project/application/heading.blade.php'));
|
||||||
|
$desktopExitedActions = str($heading)
|
||||||
|
->after("@if (str(\$application->status)->startsWith('exited'))")
|
||||||
|
->before('@else')
|
||||||
|
->toString();
|
||||||
|
|
||||||
|
$mobileActions = str($heading)
|
||||||
|
->after('id="application-mobile-actions"')
|
||||||
|
->before('<div class="hidden" aria-hidden="true">')
|
||||||
|
->toString();
|
||||||
|
|
||||||
|
expect($mobileActions)->toContain('application-mobile-stop-trigger')
|
||||||
|
->and($desktopExitedActions)->toContain('application-mobile-stop-trigger')
|
||||||
|
->and($mobileActions)->toContain('Deploy (without cache)')
|
||||||
|
->and(strrpos($mobileActions, 'Deploy (without cache)'))
|
||||||
|
->toBeLessThan(strrpos($mobileActions, 'application-mobile-stop-trigger'));
|
||||||
|
});
|
||||||
|
|
||||||
it('places the state-aware no-cache action immediately after deploy or redeploy', function () {
|
it('places the state-aware no-cache action immediately after deploy or redeploy', function () {
|
||||||
$heading = file_get_contents(resource_path('views/livewire/project/application/heading.blade.php'));
|
$heading = file_get_contents(resource_path('views/livewire/project/application/heading.blade.php'));
|
||||||
$actions = str($heading)->after('id="application-desktop-actions"')->before('@endteleport')->toString();
|
$actions = str($heading)->after('id="application-desktop-actions"')->before('@endteleport')->toString();
|
||||||
|
|
|
||||||
|
|
@ -147,6 +147,16 @@ function sentinelPayload(array $containers, ?float $diskPercentage = 42.0): arra
|
||||||
Queue::assertPushed(PushServerUpdateJob::class, 2);
|
Queue::assertPushed(PushServerUpdateJob::class, 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('dispatches the job when only the container restart count changes', function () {
|
||||||
|
$beforeRestart = [['name' => 'app-1', 'state' => 'running', 'restart_count' => 0]];
|
||||||
|
$afterRestart = [['name' => 'app-1', 'state' => 'running', 'restart_count' => 1]];
|
||||||
|
|
||||||
|
pushSentinel($this->token, sentinelPayload($beforeRestart))->assertOk();
|
||||||
|
pushSentinel($this->token, sentinelPayload($afterRestart))->assertOk();
|
||||||
|
|
||||||
|
Queue::assertPushed(PushServerUpdateJob::class, 2);
|
||||||
|
});
|
||||||
|
|
||||||
it('ignores health status changes while container lifecycle state is unchanged', function () {
|
it('ignores health status changes while container lifecycle state is unchanged', function () {
|
||||||
$healthy = [['name' => 'app-1', 'state' => 'running', 'health_status' => 'healthy']];
|
$healthy = [['name' => 'app-1', 'state' => 'running', 'health_status' => 'healthy']];
|
||||||
$unhealthy = [['name' => 'app-1', 'state' => 'running', 'health_status' => 'unhealthy']];
|
$unhealthy = [['name' => 'app-1', 'state' => 'running', 'health_status' => 'unhealthy']];
|
||||||
|
|
|
||||||
|
|
@ -70,12 +70,12 @@
|
||||||
expect($result)->toBe('running:unknown');
|
expect($result)->toBe('running:unknown');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('returns degraded:unhealthy for crash loop (exited with restart count)', function () {
|
test('returns exited for an exited container with a restart count', function () {
|
||||||
$statuses = collect(['exited']);
|
$statuses = collect(['exited']);
|
||||||
|
|
||||||
$result = $this->aggregator->aggregateFromStrings($statuses, maxRestartCount: 5);
|
$result = $this->aggregator->aggregateFromStrings($statuses, maxRestartCount: 5);
|
||||||
|
|
||||||
expect($result)->toBe('degraded:unhealthy');
|
expect($result)->toBe('exited');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('returns exited for exited containers without restart count', function () {
|
test('returns exited for exited containers without restart count', function () {
|
||||||
|
|
@ -214,12 +214,12 @@
|
||||||
expect($result)->toBe('degraded:unhealthy');
|
expect($result)->toBe('degraded:unhealthy');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('prioritizes crash loop over running containers', function () {
|
test('returns exited when all containers are exited with restart counts', function () {
|
||||||
$statuses = collect(['exited', 'exited']);
|
$statuses = collect(['exited', 'exited']);
|
||||||
|
|
||||||
$result = $this->aggregator->aggregateFromStrings($statuses, maxRestartCount: 3);
|
$result = $this->aggregator->aggregateFromStrings($statuses, maxRestartCount: 3);
|
||||||
|
|
||||||
expect($result)->toBe('degraded:unhealthy');
|
expect($result)->toBe('exited');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('prioritizes mixed state over healthy running', function () {
|
test('prioritizes mixed state over healthy running', function () {
|
||||||
|
|
@ -238,12 +238,12 @@
|
||||||
expect($result)->toBe('starting:unknown');
|
expect($result)->toBe('starting:unknown');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('prioritizes running over paused/exited when no starting', function () {
|
test('returns degraded for mixed running and exited containers', function () {
|
||||||
$statuses = collect(['running:healthy', 'paused', 'exited']);
|
$statuses = collect(['running:healthy', 'paused', 'exited']);
|
||||||
|
|
||||||
$result = $this->aggregator->aggregateFromStrings($statuses);
|
$result = $this->aggregator->aggregateFromStrings($statuses);
|
||||||
|
|
||||||
expect($result)->toBe('running:healthy');
|
expect($result)->toBe('degraded:unhealthy');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('prioritizes dead over paused/starting/exited', function () {
|
test('prioritizes dead over paused/starting/exited', function () {
|
||||||
|
|
@ -357,7 +357,7 @@
|
||||||
expect($result)->toBe('degraded:unhealthy');
|
expect($result)->toBe('degraded:unhealthy');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('returns degraded:unhealthy for crash loop (exited with restart count)', function () {
|
test('returns exited for an exited container object with a restart count', function () {
|
||||||
$containers = collect([
|
$containers = collect([
|
||||||
(object) [
|
(object) [
|
||||||
'State' => (object) [
|
'State' => (object) [
|
||||||
|
|
@ -368,7 +368,7 @@
|
||||||
|
|
||||||
$result = $this->aggregator->aggregateFromContainers($containers, maxRestartCount: 5);
|
$result = $this->aggregator->aggregateFromContainers($containers, maxRestartCount: 5);
|
||||||
|
|
||||||
expect($result)->toBe('degraded:unhealthy');
|
expect($result)->toBe('exited');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('returns exited for exited containers without restart count', function () {
|
test('returns exited for exited containers without restart count', function () {
|
||||||
|
|
@ -501,7 +501,7 @@
|
||||||
expect($result)->toBe('degraded:unhealthy');
|
expect($result)->toBe('degraded:unhealthy');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('crash loop has third highest priority', function () {
|
test('mixed running and exited containers are degraded before paused or starting states', function () {
|
||||||
$statuses = collect([
|
$statuses = collect([
|
||||||
'exited',
|
'exited',
|
||||||
'running:healthy',
|
'running:healthy',
|
||||||
|
|
@ -602,31 +602,29 @@
|
||||||
|
|
||||||
$result = $this->aggregator->aggregateFromStrings($statuses, maxRestartCount: 0);
|
$result = $this->aggregator->aggregateFromStrings($statuses, maxRestartCount: 0);
|
||||||
|
|
||||||
// Zero is valid default - no crash loop detection
|
|
||||||
expect($result)->toBe('exited');
|
expect($result)->toBe('exited');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('positive maxRestartCount works correctly', function () {
|
test('positive maxRestartCount does not override an exited state', function () {
|
||||||
$statuses = collect(['exited']);
|
$statuses = collect(['exited']);
|
||||||
|
|
||||||
$result = $this->aggregator->aggregateFromStrings($statuses, maxRestartCount: 5);
|
$result = $this->aggregator->aggregateFromStrings($statuses, maxRestartCount: 5);
|
||||||
|
|
||||||
// Positive value enables crash loop detection
|
expect($result)->toBe('exited');
|
||||||
expect($result)->toBe('degraded:unhealthy');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('crash loop detection still functions after validation', function () {
|
test('exited state is preserved for any positive restart count', function () {
|
||||||
$statuses = collect(['exited']);
|
$statuses = collect(['exited']);
|
||||||
|
|
||||||
// Test with various positive restart counts
|
// Test with various positive restart counts
|
||||||
expect($this->aggregator->aggregateFromStrings($statuses, maxRestartCount: 1))
|
expect($this->aggregator->aggregateFromStrings($statuses, maxRestartCount: 1))
|
||||||
->toBe('degraded:unhealthy');
|
->toBe('exited');
|
||||||
|
|
||||||
expect($this->aggregator->aggregateFromStrings($statuses, maxRestartCount: 100))
|
expect($this->aggregator->aggregateFromStrings($statuses, maxRestartCount: 100))
|
||||||
->toBe('degraded:unhealthy');
|
->toBe('exited');
|
||||||
|
|
||||||
expect($this->aggregator->aggregateFromStrings($statuses, maxRestartCount: 999))
|
expect($this->aggregator->aggregateFromStrings($statuses, maxRestartCount: 999))
|
||||||
->toBe('degraded:unhealthy');
|
->toBe('exited');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('default maxRestartCount parameter works', function () {
|
test('default maxRestartCount parameter works', function () {
|
||||||
|
|
|
||||||
9
tests/Unit/PushServerUpdateJobReturnTypesTest.php
Normal file
9
tests/Unit/PushServerUpdateJobReturnTypesTest.php
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Jobs\PushServerUpdateJob;
|
||||||
|
|
||||||
|
test('database status update declares a void return type', function () {
|
||||||
|
$method = new ReflectionMethod(PushServerUpdateJob::class, 'updateDatabaseStatus');
|
||||||
|
|
||||||
|
expect($method->getReturnType()?->getName())->toBe('void');
|
||||||
|
});
|
||||||
11
tests/Unit/RemovedContainerStoppedNotificationTest.php
Normal file
11
tests/Unit/RemovedContainerStoppedNotificationTest.php
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
it('does not keep the unreliable generic stopped container notification path', function () {
|
||||||
|
$statusAction = file_get_contents(__DIR__.'/../../app/Actions/Docker/GetContainersStatus.php');
|
||||||
|
$telegramChannel = file_get_contents(__DIR__.'/../../app/Notifications/Channels/TelegramChannel.php');
|
||||||
|
|
||||||
|
expect($statusAction)->not->toContain('ContainerStopped')
|
||||||
|
->and($telegramChannel)->not->toContain('ContainerStopped')
|
||||||
|
->and(file_exists(__DIR__.'/../../app/Notifications/Container/ContainerStopped.php'))->toBeFalse()
|
||||||
|
->and(file_exists(__DIR__.'/../../resources/views/emails/container-stopped.blade.php'))->toBeFalse();
|
||||||
|
});
|
||||||
95
tests/Unit/RestartCountTrackerTest.php
Normal file
95
tests/Unit/RestartCountTrackerTest.php
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Services\RestartCountTracker;
|
||||||
|
|
||||||
|
it('starts a new generation when an active container restart count drops', function () {
|
||||||
|
$result = (new RestartCountTracker)->evaluate(
|
||||||
|
previousRestartCount: 11,
|
||||||
|
observedRestartCount: 0,
|
||||||
|
maxRestartCount: 2,
|
||||||
|
newGenerationConfirmed: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect($result)->toMatchArray([
|
||||||
|
'restart_count' => 0,
|
||||||
|
'restart_count_changed' => true,
|
||||||
|
'restart_limit_reached' => false,
|
||||||
|
'new_generation' => true,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('evaluates the restart limit immediately in a new generation', function () {
|
||||||
|
$result = (new RestartCountTracker)->evaluate(
|
||||||
|
previousRestartCount: 11,
|
||||||
|
observedRestartCount: 3,
|
||||||
|
maxRestartCount: 2,
|
||||||
|
newGenerationConfirmed: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect($result)->toMatchArray([
|
||||||
|
'restart_count' => 3,
|
||||||
|
'restart_count_changed' => true,
|
||||||
|
'restart_limit_reached' => true,
|
||||||
|
'new_generation' => true,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not reset the generation without explicit confirmation', function () {
|
||||||
|
$result = (new RestartCountTracker)->evaluate(
|
||||||
|
previousRestartCount: 11,
|
||||||
|
observedRestartCount: 0,
|
||||||
|
maxRestartCount: 2,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect($result)->toMatchArray([
|
||||||
|
'restart_count' => 11,
|
||||||
|
'restart_count_changed' => false,
|
||||||
|
'restart_limit_reached' => false,
|
||||||
|
'new_generation' => false,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves the previous count when an active payload omits the container with the previous maximum', function () {
|
||||||
|
$result = (new RestartCountTracker)->evaluate(
|
||||||
|
previousRestartCount: 11,
|
||||||
|
observedRestartCount: 3,
|
||||||
|
maxRestartCount: 20,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect($result)->toMatchArray([
|
||||||
|
'restart_count' => 11,
|
||||||
|
'restart_count_changed' => false,
|
||||||
|
'restart_limit_reached' => false,
|
||||||
|
'new_generation' => false,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('detects a normal threshold crossing', function () {
|
||||||
|
$result = (new RestartCountTracker)->evaluate(
|
||||||
|
previousRestartCount: 1,
|
||||||
|
observedRestartCount: 2,
|
||||||
|
maxRestartCount: 2,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect($result)->toMatchArray([
|
||||||
|
'restart_count' => 2,
|
||||||
|
'restart_count_changed' => true,
|
||||||
|
'restart_limit_reached' => true,
|
||||||
|
'new_generation' => false,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('detects a limit that is enabled below the current observed restart count', function () {
|
||||||
|
$result = (new RestartCountTracker)->evaluate(
|
||||||
|
previousRestartCount: 17,
|
||||||
|
observedRestartCount: 17,
|
||||||
|
maxRestartCount: 10,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect($result)->toMatchArray([
|
||||||
|
'restart_count' => 17,
|
||||||
|
'restart_count_changed' => false,
|
||||||
|
'restart_limit_reached' => true,
|
||||||
|
'new_generation' => false,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
@ -10,8 +10,7 @@
|
||||||
$action = file_get_contents(__DIR__.'/../../app/Actions/Application/StopApplication.php');
|
$action = file_get_contents(__DIR__.'/../../app/Actions/Application/StopApplication.php');
|
||||||
|
|
||||||
expect($action)
|
expect($action)
|
||||||
->toContain("\$status = ['status' => 'exited'];")
|
->toMatch('/\$status\s*=\s*\[\s*\'status\'\s*=>\s*\'exited\',.*?\];.*?\$application->update\(\$status\);/s')
|
||||||
->toContain('$application->update($status);')
|
|
||||||
->not->toMatch('/docker stack rm .*?return;/s');
|
->not->toMatch('/docker stack rm .*?return;/s');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -19,8 +18,10 @@
|
||||||
$action = file_get_contents(__DIR__.'/../../app/Actions/Service/StopService.php');
|
$action = file_get_contents(__DIR__.'/../../app/Actions/Service/StopService.php');
|
||||||
|
|
||||||
expect($action)
|
expect($action)
|
||||||
->toContain("\$applications->each->update(['status' => 'exited']);")
|
->toContain("\$application->update(['status' => 'exited']);")
|
||||||
->toContain("\$dbs->each->update(['status' => 'exited']);");
|
->toContain('$application->resetRestartLimit();')
|
||||||
|
->toContain("\$database->update(['status' => 'exited']);")
|
||||||
|
->toContain('$database->resetRestartLimit();');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('persists exited status when stopping an individual service resource', function () {
|
it('persists exited status when stopping an individual service resource', function () {
|
||||||
|
|
@ -28,6 +29,8 @@
|
||||||
|
|
||||||
expect($action)
|
expect($action)
|
||||||
->toContain("\$serviceApplication->update(['status' => 'exited']);")
|
->toContain("\$serviceApplication->update(['status' => 'exited']);")
|
||||||
|
->toContain('$commands = ["docker rm -f {$containerName}"];')
|
||||||
|
->toContain('throwError: ! $removeContainer')
|
||||||
->toContain('ServiceStatusChanged::dispatch($service->environment->project->team->id);');
|
->toContain('ServiceStatusChanged::dispatch($service->environment->project->team->id);');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
12
tests/Unit/StopDatabaseTypesTest.php
Normal file
12
tests/Unit/StopDatabaseTypesTest.php
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Actions\Database\StopDatabase;
|
||||||
|
use App\Models\BaseModel;
|
||||||
|
|
||||||
|
it('declares strict method types', function () {
|
||||||
|
$handle = new ReflectionMethod(StopDatabase::class, 'handle');
|
||||||
|
$stopContainer = new ReflectionMethod(StopDatabase::class, 'stopContainer');
|
||||||
|
|
||||||
|
expect($handle->getReturnType()?->getName())->toBe('string')
|
||||||
|
->and($stopContainer->getParameters()[0]->getType()?->getName())->toBe(BaseModel::class);
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue