fix: force enable/disable server in case ultimate package quantity decreases

This commit is contained in:
Andras Bacsai 2024-02-26 10:25:21 +01:00
parent 453956172b
commit 678647f39a
25 changed files with 172 additions and 68 deletions

View File

@ -14,7 +14,7 @@
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Visus\Cuid2\Cuid2; use Visus\Cuid2\Cuid2;
class Deploy extends Controller class APIDeploy extends Controller
{ {
public function deploy(Request $request) public function deploy(Request $request)
{ {

View File

@ -6,7 +6,7 @@
use App\Models\Project as ModelsProject; use App\Models\Project as ModelsProject;
use Illuminate\Http\Request; use Illuminate\Http\Request;
class Project extends Controller class APIProject extends Controller
{ {
public function projects(Request $request) public function projects(Request $request)
{ {

View File

@ -6,7 +6,7 @@
use App\Models\Server as ModelsServer; use App\Models\Server as ModelsServer;
use Illuminate\Http\Request; use Illuminate\Http\Request;
class Server extends Controller class APIServer extends Controller
{ {
public function servers(Request $request) public function servers(Request $request)
{ {

View File

@ -1675,7 +1675,6 @@ public function failed(Throwable $exception): void
); );
} }
} }
$this->next(ApplicationDeploymentStatus::FAILED->value); $this->next(ApplicationDeploymentStatus::FAILED->value);
} }
} }

View File

@ -3,7 +3,8 @@
namespace App\Jobs; namespace App\Jobs;
use App\Models\Team; use App\Models\Team;
use App\Notifications\Server\DisabledDueToOverflow; use App\Notifications\Server\ForceDisabled;
use App\Notifications\Server\ForceEnabled;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
@ -12,7 +13,7 @@
use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
class ServerOverflowJob implements ShouldQueue, ShouldBeEncrypted class ServerLimitCheckJob implements ShouldQueue, ShouldBeEncrypted
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
@ -37,26 +38,31 @@ public function uniqueId(): int
public function handle() public function handle()
{ {
try { try {
ray('ServerOverflowJob');
$servers = $this->team->servers; $servers = $this->team->servers;
$servers_count = $servers->count(); $servers_count = $servers->count();
$limit = $this->team->limits['serverLimit']; $limit = $this->team->limits['serverLimit'];
$number_of_servers_to_disable = $servers_count - $limit; $number_of_servers_to_disable = $servers_count - $limit;
ray($number_of_servers_to_disable, $servers_count, $limit); ray('ServerLimitCheckJob', $this->team->uuid, $servers_count, $limit, $number_of_servers_to_disable);
if ($number_of_servers_to_disable > 0) { if ($number_of_servers_to_disable > 0) {
ray('Disabling servers'); ray('Disabling servers');
$servers = $servers->sortBy('created_at'); $servers = $servers->sortbyDesc('created_at');
$servers_to_disable = $servers->take($number_of_servers_to_disable); $servers_to_disable = $servers->take($number_of_servers_to_disable);
$servers_to_disable->each(function ($server) { $servers_to_disable->each(function ($server) {
$server->disableServerDueToOverflow(); $server->forceDisableServer();
$this->team->notify(new DisabledDueToOverflow($server)); $this->team->notify(new ForceDisabled($server));
});
} else if ($number_of_servers_to_disable === 0) {
$servers->each(function ($server) {
if ($server->isForceDisabled()) {
$server->forceEnableServer();
$this->team->notify(new ForceEnabled($server));
}
}); });
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
send_internal_notification('ServerOverflowJob failed with: ' . $e->getMessage()); send_internal_notification('ServerLimitCheckJob failed with: ' . $e->getMessage());
ray($e->getMessage()); ray($e->getMessage());
return handleError($e); return handleError($e);
} }
} }
} }

View File

@ -3,6 +3,7 @@
namespace App\Livewire\Admin; namespace App\Livewire\Admin;
use App\Models\User; use App\Models\User;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\Crypt;
use Livewire\Component; use Livewire\Component;
@ -27,6 +28,7 @@ public function switchUser(int $user_id)
auth()->login($user); auth()->login($user);
if ($user_id === 0) { if ($user_id === 0) {
Cache::forget('team:0');
session()->forget('adminToken'); session()->forget('adminToken');
} else { } else {
$token_payload = [ $token_payload = [
@ -35,6 +37,7 @@ public function switchUser(int $user_id)
$token = Crypt::encrypt($token_payload); $token = Crypt::encrypt($token_payload);
session(['adminToken' => $token]); session(['adminToken' => $token]);
} }
session()->regenerate();
return refreshSession(); return refreshSession();
} }
public function render() public function render()

View File

@ -2,7 +2,7 @@
namespace App\Livewire\Tags; namespace App\Livewire\Tags;
use App\Http\Controllers\Api\Deploy; use App\Http\Controllers\Api\APIDeploy as Deploy;
use App\Models\ApplicationDeploymentQueue; use App\Models\ApplicationDeploymentQueue;
use App\Models\Tag; use App\Models\Tag;
use Livewire\Component; use Livewire\Component;

View File

@ -10,6 +10,7 @@
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Spatie\SchemalessAttributes\Casts\SchemalessAttributes; use Spatie\SchemalessAttributes\Casts\SchemalessAttributes;
use Spatie\SchemalessAttributes\SchemalessAttributesTrait; use Spatie\SchemalessAttributes\SchemalessAttributesTrait;
use Illuminate\Support\Str; use Illuminate\Support\Str;
@ -69,7 +70,7 @@ static public function ownedByCurrentTeam(array $select = ['*'])
static public function isUsable() static public function isUsable()
{ {
return Server::ownedByCurrentTeam()->whereRelation('settings', 'is_reachable', true)->whereRelation('settings', 'is_usable', true)->whereRelation('settings', 'is_swarm_worker', false)->whereRelation('settings', 'is_build_server', false); return Server::ownedByCurrentTeam()->whereRelation('settings', 'is_reachable', true)->whereRelation('settings', 'is_usable', true)->whereRelation('settings', 'is_swarm_worker', false)->whereRelation('settings', 'is_build_server', false)->whereRelation('settings', 'force_disabled', false);
} }
static public function destinationsByServer(string $server_id) static public function destinationsByServer(string $server_id)
@ -149,13 +150,31 @@ public function skipServer()
ray('skipping 1.2.3.4'); ray('skipping 1.2.3.4');
return true; return true;
} }
if ($this->settings->force_disabled === true) {
ray('force_disabled');
return true;
}
return false; return false;
} }
public function disableServerDueToOverflow() { public function isForceDisabled()
{
return $this->settings->force_disabled;
}
public function forceEnableServer()
{
$this->settings->update([ $this->settings->update([
'disabled_by_overflow' => true, 'force_disabled' => false,
]); ]);
} }
public function forceDisableServer()
{
$this->settings->update([
'force_disabled' => true,
]);
$sshKeyFileLocation = "id.root@{$this->uuid}";
Storage::disk('ssh-keys')->delete($sshKeyFileLocation);
Storage::disk('ssh-mux')->delete($this->muxFilename());
}
public function isServerReady(int $tries = 3) public function isServerReady(int $tries = 3)
{ {
if ($this->skipServer()) { if ($this->skipServer()) {
@ -379,7 +398,7 @@ public function isProxyShouldRun()
} }
public function isFunctional() public function isFunctional()
{ {
return $this->settings->is_reachable && $this->settings->is_usable; return $this->settings->is_reachable && $this->settings->is_usable && !$this->settings->force_disabled;
} }
public function isLogDrainEnabled() public function isLogDrainEnabled()
{ {

View File

@ -11,7 +11,7 @@
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification; use Illuminate\Notifications\Notification;
class DisabledDueToOverflow extends Notification implements ShouldQueue class ForceDisabled extends Notification implements ShouldQueue
{ {
use Queueable; use Queueable;
@ -43,7 +43,7 @@ public function toMail(): MailMessage
{ {
$mail = new MailMessage(); $mail = new MailMessage();
$mail->subject("Coolify: Server ({$this->server->name}) disabled because it is not paid!"); $mail->subject("Coolify: Server ({$this->server->name}) disabled because it is not paid!");
$mail->view('emails.server-disabled-due-to-overflow', [ $mail->view('emails.server-force-disabled', [
'name' => $this->server->name, 'name' => $this->server->name,
]); ]);
return $mail; return $mail;

View File

@ -0,0 +1,63 @@
<?php
namespace App\Notifications\Server;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\TelegramChannel;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class ForceEnabled extends Notification implements ShouldQueue
{
use Queueable;
public $tries = 1;
public function __construct(public Server $server)
{
}
public function via(object $notifiable): array
{
$channels = [];
$isEmailEnabled = isEmailEnabled($notifiable);
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isTelegramEnabled = data_get($notifiable, 'telegram_enabled');
if ($isDiscordEnabled) {
$channels[] = DiscordChannel::class;
}
if ($isEmailEnabled) {
$channels[] = EmailChannel::class;
}
if ($isTelegramEnabled) {
$channels[] = TelegramChannel::class;
}
return $channels;
}
public function toMail(): MailMessage
{
$mail = new MailMessage();
$mail->subject("Coolify: Server ({$this->server->name}) enabled again!");
$mail->view('emails.server-force-enabled', [
'name' => $this->server->name,
]);
return $mail;
}
public function toDiscord(): string
{
$message = "Coolify: Server ({$this->server->name}) enabled again!";
return $message;
}
public function toTelegram(): array
{
return [
"message" => "Coolify: Server ({$this->server->name}) enabled again!"
];
}
}

View File

@ -24,6 +24,12 @@ public function execute_remote_command(...$commands)
if ($this->server instanceof Server === false) { if ($this->server instanceof Server === false) {
throw new \RuntimeException('Server is not set or is not an instance of Server model'); throw new \RuntimeException('Server is not set or is not an instance of Server model');
} }
if ($this->server->settings->force_disabled) {
$this->application_deployment_queue->update([
'status' => ApplicationDeploymentStatus::FAILED->value,
]);
throw new \RuntimeException('Server is disabled');
}
$commandsText->each(function ($single_command) { $commandsText->each(function ($single_command) {
$command = data_get($single_command, 'command') ?? $single_command[0] ?? null; $command = data_get($single_command, 'command') ?? $single_command[0] ?? null;
if ($command === null) { if ($command === null) {

View File

@ -12,7 +12,7 @@
public function up(): void public function up(): void
{ {
Schema::table('server_settings', function (Blueprint $table) { Schema::table('server_settings', function (Blueprint $table) {
$table->boolean('disabled_by_overflow')->default(false); $table->boolean('force_disabled')->default(false);
}); });
} }
@ -22,7 +22,7 @@ public function up(): void
public function down(): void public function down(): void
{ {
Schema::table('server_settings', function (Blueprint $table) { Schema::table('server_settings', function (Blueprint $table) {
$table->dropColumn('disabled_by_overflow'); $table->dropColumn('force_disabled');
}); });
} }
}; };

View File

@ -1,5 +1,3 @@
<div>
@if ($server->isFunctional())
<div class="flex h-full pr-4"> <div class="flex h-full pr-4">
<div class="flex flex-col w-48 gap-4 min-w-fit"> <div class="flex flex-col w-48 gap-4 min-w-fit">
<a class="{{ request()->routeIs('server.proxy') ? 'text-white' : '' }}" <a class="{{ request()->routeIs('server.proxy') ? 'text-white' : '' }}"
@ -18,7 +16,3 @@
@endif @endif
</div> </div>
</div> </div>
@else
<div>Server is not validated. Validate first.</div>
@endif
</div>

View File

@ -0,0 +1,3 @@
<x-emails.layout>
Your server ({{ $name }}) is enabled again!
</x-emails.layout>

View File

@ -13,7 +13,11 @@ class="underline text-warning">Please
@endif @endif
@if (currentTeam()->serverOverflow()) @if (currentTeam()->serverOverflow())
<x-banner :closable=false> <x-banner :closable=false>
<div><span class="font-bold text-red-500">WARNING:</span> The number of active servers exceeds the limit covered by your payment. If not resolved, some of your servers <span class="font-bold text-red-500">will be deactivated</span> in the next billing cycle. Visit <a href="{{route('subscription.show')}}" class="text-white underline">/subscription</a> to update your subscription.</div> <div><span class="font-bold text-red-500">WARNING:</span> The number of active servers exceeds the limit
covered by your payment. If not resolved, some of your servers <span class="font-bold text-red-500">will
be deactivated</span>. Visit <a href="{{ route('subscription.show') }}"
class="text-white underline">/subscription</a> to update your subscription or remove some servers.
</div>
</x-banner> </x-banner>
@endif @endif
</div> </div>

View File

@ -7,10 +7,10 @@
back! back!
</div> </div>
@if ($server->definedResources()->count() > 0) @if ($server->definedResources()->count() > 0)
<div class="pb-2 text-red-500">You need to delete all resources before deleting this server.</div>
<x-new-modal disabled isErrorButton buttonTitle="Delete"> <x-new-modal disabled isErrorButton buttonTitle="Delete">
This server will be deleted. It is not reversible. <br>Please think again. This server will be deleted. It is not reversible. <br>Please think again.
</x-new-modal> </x-new-modal>
<div>You need to delete all resources before deleting this server.</div>
@else @else
<x-new-modal isErrorButton buttonTitle="Delete"> <x-new-modal isErrorButton buttonTitle="Delete">
This server will be deleted. It is not reversible. <br>Please think again. This server will be deleted. It is not reversible. <br>Please think again.

View File

@ -47,6 +47,10 @@ class="w-full mt-8 mb-4 font-bold box-without-bg bg-coollabs hover:bg-coollabs-1
Validate Server Validate Server
</x-forms.button> </x-forms.button>
@endif @endif
@if ($server->isForceDisabled() && isCloud())
<div class="pt-4 font-bold text-red-500">The system has disabled the server because you have exceeded the
number of servers for which you have paid.</div>
@endif
<div class="flex flex-col gap-2 pt-4"> <div class="flex flex-col gap-2 pt-4">
<div class="flex flex-col w-full gap-2 lg:flex-row"> <div class="flex flex-col w-full gap-2 lg:flex-row">
<x-forms.input id="server.name" label="Name" required /> <x-forms.input id="server.name" label="Name" required />

View File

@ -11,8 +11,8 @@
<a href="{{ route('server.show', ['server_uuid' => data_get($server, 'uuid')]) }}" <a href="{{ route('server.show', ['server_uuid' => data_get($server, 'uuid')]) }}"
@class([ @class([
'gap-2 border cursor-pointer box group', 'gap-2 border cursor-pointer box group',
'border-transparent' => $server->settings->is_reachable, 'border-transparent' => $server->settings->is_reachable && $server->settings->is_usable && !$server->settings->force_disabled,
'border-red-500' => !$server->settings->is_reachable, 'border-red-500' => !$server->settings->is_reachable || $server->settings->force_disabled,
])> ])>
<div class="flex flex-col mx-6"> <div class="flex flex-col mx-6">
<div class="font-bold text-white"> <div class="font-bold text-white">
@ -30,6 +30,9 @@
@if (!$server->settings->is_usable) @if (!$server->settings->is_usable)
<span>Not usable by Coolify</span> <span>Not usable by Coolify</span>
@endif @endif
@if ($server->settings->force_disabled)
<span>Disabled by the system</span>
@endif
</div> </div>
</div> </div>
<div class="flex-1"></div> <div class="flex-1"></div>

View File

@ -1,7 +1,7 @@
<div> <div>
<x-server.navbar :server="$server" :parameters="$parameters" /> <x-server.navbar :server="$server" :parameters="$parameters" />
<div class="flex gap-2">
<x-server.sidebar :server="$server" :parameters="$parameters" /> <x-server.sidebar :server="$server" :parameters="$parameters" />
<div class="flex gap-2">
<div class="w-full"> <div class="w-full">
@if ($server->isFunctional()) @if ($server->isFunctional())
<div class="flex gap-2"> <div class="flex gap-2">
@ -48,7 +48,6 @@ class="font-normal text-white normal-case border-none rounded btn btn-primary bt
<div wire:loading.remove> No dynamic configurations found.</div> <div wire:loading.remove> No dynamic configurations found.</div>
@endif @endif
</div> </div>
@endif @endif
</div> </div>
</div> </div>

View File

@ -1,11 +1,13 @@
<div> <div>
<x-server.navbar :server="$server" :parameters="$parameters" /> <x-server.navbar :server="$server" :parameters="$parameters" />
@if ($server->isFunctional())
<div class="flex gap-2"> <div class="flex gap-2">
<x-server.sidebar :server="$server" :parameters="$parameters" /> <x-server.sidebar :server="$server" :parameters="$parameters" />
<div class="w-full"> <div class="w-full">
@if ($server->isFunctional())
<livewire:server.proxy :server="$server" /> <livewire:server.proxy :server="$server" />
</div>
</div>
@else
<div>Server is not validated. Validate first.</div>
@endif @endif
</div> </div>
</div>
</div>

View File

@ -17,7 +17,7 @@ class="text-warning">{{ data_get(currentTeam(), 'subscription')->type() }}</stro
<div class="py-4"><span class="font-bold text-red-500">WARNING:</span> You must delete <div class="py-4"><span class="font-bold text-red-500">WARNING:</span> You must delete
{{ currentTeam()->servers->count() - $server_limits }} servers, {{ currentTeam()->servers->count() - $server_limits }} servers,
or upgrade your subscription. {{ currentTeam()->servers->count() - $server_limits }} servers will be or upgrade your subscription. {{ currentTeam()->servers->count() - $server_limits }} servers will be
deactivated in the next billing cycle.</div> deactivated.</div>
@endif @endif
<h2 class="pt-4">Manage your subscription</h2> <h2 class="pt-4">Manage your subscription</h2>
<div class="pb-4">Cancel, upgrade or downgrade your subscription.</div> <div class="pb-4">Cancel, upgrade or downgrade your subscription.</div>

View File

@ -1,8 +1,8 @@
<?php <?php
use App\Http\Controllers\Api\Deploy; use App\Http\Controllers\Api\APIDeploy as Deploy;
use App\Http\Controllers\Api\Project; use App\Http\Controllers\Api\APIProject as Project;
use App\Http\Controllers\Api\Server; use App\Http\Controllers\Api\APIServer as Server;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;

View File

@ -1,6 +1,5 @@
<?php <?php
use App\Http\Controllers\Api\Server as ApiServer;
use App\Models\GitlabApp; use App\Models\GitlabApp;
use App\Models\PrivateKey; use App\Models\PrivateKey;
use App\Models\Server; use App\Models\Server;

View File

@ -3,7 +3,7 @@
use App\Enums\ProcessStatus; use App\Enums\ProcessStatus;
use App\Jobs\ApplicationPullRequestUpdateJob; use App\Jobs\ApplicationPullRequestUpdateJob;
use App\Jobs\GithubAppPermissionJob; use App\Jobs\GithubAppPermissionJob;
use App\Jobs\ServerOverflowJob; use App\Jobs\ServerLimitCheckJob;
use App\Jobs\SubscriptionInvoiceFailedJob; use App\Jobs\SubscriptionInvoiceFailedJob;
use App\Jobs\SubscriptionTrialEndedJob; use App\Jobs\SubscriptionTrialEndedJob;
use App\Jobs\SubscriptionTrialEndsSoonJob; use App\Jobs\SubscriptionTrialEndsSoonJob;
@ -883,7 +883,7 @@
$team->update([ $team->update([
'custom_server_limit' => $quantity, 'custom_server_limit' => $quantity,
]); ]);
ServerOverflowJob::dispatch($team); ServerLimitCheckJob::dispatch($team);
} }
$subscription->update([ $subscription->update([
'stripe_feedback' => $feedback, 'stripe_feedback' => $feedback,