Merge branch 'next' into feat/disable-default-redirect

This commit is contained in:
Kael
2024-10-28 19:20:52 +11:00
committed by GitHub
63 changed files with 628 additions and 872 deletions

View File

@@ -209,6 +209,8 @@ Files:
];
$command = array_merge($command, $add_envs_command, $restart_command);
StopLogDrain::run($server);
return instant_remote_process($command, $server);
} catch (\Throwable $e) {
return handleError($e);

View File

@@ -9,25 +9,21 @@ class StartSentinel
{
use AsAction;
public function handle(Server $server, bool $restart = false, bool $is_dev = false)
public function handle(Server $server, bool $restart = false, ?string $latestVersion = null)
{
// TODO: Sentinel is not available in this version (soon).
if (! $is_dev) {
return;
}
$version = get_latest_sentinel_version();
if ($server->isSwarm() || $server->isBuildServer()) {
return;
}
if ($restart) {
StopSentinel::run($server);
}
$metrics_history = data_get($server, 'settings.sentinel_metrics_history_days');
$refresh_rate = data_get($server, 'settings.sentinel_metrics_refresh_rate_seconds');
$push_interval = data_get($server, 'settings.sentinel_push_interval_seconds');
$version = $latestVersion ?? get_latest_sentinel_version();
$metricsHistory = data_get($server, 'settings.sentinel_metrics_history_days');
$refreshRate = data_get($server, 'settings.sentinel_metrics_refresh_rate_seconds');
$pushInterval = data_get($server, 'settings.sentinel_push_interval_seconds');
$token = data_get($server, 'settings.sentinel_token');
$endpoint = data_get($server, 'settings.sentinel_custom_url');
$mount_dir = '/data/coolify/sentinel';
$mountDir = '/data/coolify/sentinel';
$image = "ghcr.io/coollabsio/sentinel:$version";
if (! $endpoint) {
throw new \Exception('You should set FQDN in Instance Settings.');
@@ -35,26 +31,29 @@ class StartSentinel
$environments = [
'TOKEN' => $token,
'PUSH_ENDPOINT' => $endpoint,
'PUSH_INTERVAL_SECONDS' => $push_interval,
'PUSH_INTERVAL_SECONDS' => $pushInterval,
'COLLECTOR_ENABLED' => $server->isMetricsEnabled() ? 'true' : 'false',
'COLLECTOR_REFRESH_RATE_SECONDS' => $refresh_rate,
'COLLECTOR_RETENTION_PERIOD_DAYS' => $metrics_history,
'COLLECTOR_REFRESH_RATE_SECONDS' => $refreshRate,
'COLLECTOR_RETENTION_PERIOD_DAYS' => $metricsHistory,
];
$labels = [
'coolify.managed' => 'true',
];
if (isDev()) {
// data_set($environments, 'DEBUG', 'true');
$mount_dir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/sentinel';
// $image = 'sentinel';
$mountDir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/sentinel';
}
$docker_environments = '-e "'.implode('" -e "', array_map(fn ($key, $value) => "$key=$value", array_keys($environments), $environments)).'"';
$docker_command = "docker run -d $docker_environments --name coolify-sentinel -v /var/run/docker.sock:/var/run/docker.sock -v $mount_dir:/app/db --pid host --health-cmd \"curl --fail http://127.0.0.1:8888/api/health || exit 1\" --health-interval 10s --health-retries 3 --add-host=host.docker.internal:host-gateway $image";
$dockerEnvironments = '-e "'.implode('" -e "', array_map(fn ($key, $value) => "$key=$value", array_keys($environments), $environments)).'"';
$dockerLabels = implode(' ', array_map(fn ($key, $value) => "$key=$value", array_keys($labels), $labels));
$dockerCommand = "docker run -d $dockerEnvironments --name coolify-sentinel -v /var/run/docker.sock:/var/run/docker.sock -v $mountDir:/app/db --pid host --health-cmd \"curl --fail http://127.0.0.1:8888/api/health || exit 1\" --health-interval 10s --health-retries 3 --add-host=host.docker.internal:host-gateway --label $dockerLabels $image";
instant_remote_process([
'docker rm -f coolify-sentinel || true',
"mkdir -p $mount_dir",
$docker_command,
"chown -R 9999:root $mount_dir",
"chmod -R 700 $mount_dir",
"mkdir -p $mountDir",
$dockerCommand,
"chown -R 9999:root $mountDir",
"chmod -R 700 $mountDir",
], $server);
$server->settings->is_sentinel_enabled = true;

View File

@@ -2,13 +2,13 @@
namespace App\Console;
use App\Jobs\CheckAndStartSentinelJob;
use App\Jobs\CheckForUpdatesJob;
use App\Jobs\CheckHelperImageJob;
use App\Jobs\CleanupInstanceStuffsJob;
use App\Jobs\CleanupStaleMultiplexedConnections;
use App\Jobs\DatabaseBackupJob;
use App\Jobs\DockerCleanupJob;
use App\Jobs\PullSentinelImageJob;
use App\Jobs\PullTemplatesFromCDN;
use App\Jobs\ScheduledTaskJob;
use App\Jobs\ServerCheckJob;
@@ -23,11 +23,11 @@ use Illuminate\Support\Carbon;
class Kernel extends ConsoleKernel
{
private $all_servers;
private $allServers;
protected function schedule(Schedule $schedule): void
{
$this->all_servers = Server::all();
$this->allServers = Server::all();
$settings = instanceSettings();
$schedule->job(new CleanupStaleMultiplexedConnections)->hourly();
@@ -37,9 +37,9 @@ class Kernel extends ConsoleKernel
$schedule->command('horizon:snapshot')->everyMinute();
$schedule->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer();
// Server Jobs
$this->check_scheduled_backups($schedule);
$this->check_resources($schedule);
$this->check_scheduled_tasks($schedule);
$this->checkScheduledBackups($schedule);
$this->checkResources($schedule);
$this->checkScheduledTasks($schedule);
$schedule->command('uploads:clear')->everyTwoMinutes();
$schedule->command('telescope:prune')->daily();
@@ -51,32 +51,27 @@ class Kernel extends ConsoleKernel
$schedule->command('cleanup:unreachable-servers')->daily()->onOneServer();
$schedule->job(new PullTemplatesFromCDN)->cron($settings->update_check_frequency)->timezone($settings->instance_timezone)->onOneServer();
$schedule->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer();
$this->schedule_updates($schedule);
$this->scheduleUpdates($schedule);
// Server Jobs
$this->check_scheduled_backups($schedule);
$this->check_resources($schedule);
$this->pull_images($schedule);
$this->check_scheduled_tasks($schedule);
$this->checkScheduledBackups($schedule);
$this->checkResources($schedule);
$this->pullImages($schedule);
$this->checkScheduledTasks($schedule);
$schedule->command('cleanup:database --yes')->daily();
$schedule->command('uploads:clear')->everyTwoMinutes();
}
}
private function pull_images($schedule)
private function pullImages($schedule): void
{
$settings = instanceSettings();
$servers = $this->all_servers->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4');
$servers = $this->allServers->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4');
foreach ($servers as $server) {
if ($server->isSentinelEnabled()) {
$schedule->job(function () use ($server) {
$sentinel_found = instant_remote_process(['docker inspect coolify-sentinel'], $server, false);
$sentinel_found = json_decode($sentinel_found, true);
$status = data_get($sentinel_found, '0.State.Status', 'exited');
if ($status !== 'running') {
PullSentinelImageJob::dispatch($server);
}
CheckAndStartSentinelJob::dispatch($server);
})->cron($settings->update_check_frequency)->timezone($settings->instance_timezone)->onOneServer();
}
}
@@ -86,7 +81,7 @@ class Kernel extends ConsoleKernel
->onOneServer();
}
private function schedule_updates($schedule)
private function scheduleUpdates($schedule): void
{
$settings = instanceSettings();
@@ -105,21 +100,21 @@ class Kernel extends ConsoleKernel
}
}
private function check_resources($schedule)
private function checkResources($schedule): void
{
if (isCloud()) {
$servers = $this->all_servers->whereNotNull('team.subscription')->where('team.subscription.stripe_trial_already_ended', false)->where('ip', '!=', '1.2.3.4');
$servers = $this->allServers->whereNotNull('team.subscription')->where('team.subscription.stripe_trial_already_ended', false)->where('ip', '!=', '1.2.3.4');
$own = Team::find(0)->servers;
$servers = $servers->merge($own);
} else {
$servers = $this->all_servers->where('ip', '!=', '1.2.3.4');
$servers = $this->allServers->where('ip', '!=', '1.2.3.4');
}
foreach ($servers as $server) {
$last_sentinel_update = $server->sentinel_updated_at;
if (Carbon::parse($last_sentinel_update)->isBefore(now()->subSeconds($server->waitBeforeDoingSshCheck()))) {
$lastSentinelUpdate = $server->sentinel_updated_at;
$serverTimezone = $server->settings->server_timezone;
if (Carbon::parse($lastSentinelUpdate)->isBefore(now()->subSeconds($server->waitBeforeDoingSshCheck()))) {
$schedule->job(new ServerCheckJob($server))->everyMinute()->onOneServer();
}
$serverTimezone = $server->settings->server_timezone;
if ($server->settings->force_docker_cleanup) {
$schedule->job(new DockerCleanupJob($server))->cron($server->settings->docker_cleanup_frequency)->timezone($serverTimezone)->onOneServer();
} else {
@@ -128,7 +123,7 @@ class Kernel extends ConsoleKernel
}
}
private function check_scheduled_backups($schedule)
private function checkScheduledBackups($schedule): void
{
$scheduled_backups = ScheduledDatabaseBackup::all();
if ($scheduled_backups->isEmpty()) {
@@ -161,7 +156,7 @@ class Kernel extends ConsoleKernel
}
}
private function check_scheduled_tasks($schedule)
private function checkScheduledTasks($schedule): void
{
$scheduled_tasks = ScheduledTask::all();
if ($scheduled_tasks->isEmpty()) {

View File

@@ -0,0 +1,56 @@
<?php
namespace App\Jobs;
use App\Actions\Server\StartSentinel;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class CheckAndStartSentinelJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $timeout = 120;
public function __construct(public Server $server) {}
public function handle(): void
{
try {
$latestVersion = get_latest_sentinel_version();
// Check if sentinel is running
$sentinelFound = instant_remote_process(['docker inspect coolify-sentinel'], $this->server, false);
$sentinelFoundJson = json_decode($sentinelFound, true);
$sentinelStatus = data_get($sentinelFoundJson, '0.State.Status', 'exited');
if ($sentinelStatus !== 'running') {
StartSentinel::run(server: $this->server, restart: true, latestVersion: $latestVersion);
return;
}
// If sentinel is running, check if it needs an update
$runningVersion = instant_remote_process(['docker exec coolify-sentinel sh -c "curl http://127.0.0.1:8888/api/version"'], $this->server, false);
if (empty($runningVersion)) {
$runningVersion = '0.0.0';
}
if ($latestVersion === '0.0.0' && $runningVersion === '0.0.0') {
StartSentinel::run(server: $this->server, restart: true, latestVersion: 'latest');
return;
} else {
if (version_compare($runningVersion, $latestVersion, '<')) {
StartSentinel::run(server: $this->server, restart: true, latestVersion: $latestVersion);
return;
}
}
} catch (\Throwable $e) {
throw $e;
}
}
}

View File

@@ -1,47 +0,0 @@
<?php
namespace App\Jobs;
use App\Actions\Server\StartSentinel;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class PullSentinelImageJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $timeout = 1000;
public function __construct(public Server $server) {}
public function handle(): void
{
try {
$version = get_latest_sentinel_version();
if (! $version) {
ray('Failed to get latest Sentinel version');
return;
}
$local_version = instant_remote_process(['docker exec coolify-sentinel sh -c "curl http://127.0.0.1:8888/api/version"'], $this->server, false);
if (empty($local_version)) {
$local_version = '0.0.0';
}
if (version_compare($local_version, $version, '<')) {
StartSentinel::run($this->server, true);
return;
}
ray('Sentinel image is up to date');
} catch (\Throwable $e) {
// send_internal_notification('PullSentinelImageJob failed with: '.$e->getMessage());
ray($e->getMessage());
throw $e;
}
}
}

View File

@@ -97,8 +97,6 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
}
$data = collect($this->data);
$this->serverStatus();
$this->server->sentinelHeartbeat();
$this->containers = collect(data_get($data, 'containers'));
@@ -212,16 +210,6 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
}
private function serverStatus()
{
if ($this->server->isFunctional() === false) {
throw new \Exception('Server is not ready.');
}
if ($this->server->status() === false) {
throw new \Exception('Server is not reachable.');
}
}
private function updateApplicationStatus(string $applicationId, string $containerStatus)
{
$application = $this->applications->where('id', $applicationId)->first();

View File

@@ -43,22 +43,15 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
public function handle()
{
try {
if ($this->server->serverStatus() === false) {
return 'Server is not reachable or not ready.';
}
$this->applications = $this->server->applications();
$this->databases = $this->server->databases();
$this->services = $this->server->services()->get();
$this->previews = $this->server->previews();
$up = $this->serverStatus();
if (! $up) {
ray('Server is not reachable.');
return 'Server is not reachable.';
}
if (! $this->server->isFunctional()) {
ray('Server is not ready.');
return 'Server is not ready.';
}
if (! $this->server->isSwarmWorker() && ! $this->server->isBuildServer()) {
['containers' => $this->containers, 'containerReplicates' => $containerReplicates] = $this->server->getContainers();
if (is_null($this->containers)) {
@@ -67,9 +60,14 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
ServerStorageCheckJob::dispatch($this->server);
GetContainersStatus::run($this->server, $this->containers, $containerReplicates);
if ($this->server->isSentinelEnabled()) {
CheckAndStartSentinelJob::dispatch($this->server);
}
if ($this->server->isLogDrainEnabled()) {
$this->checkLogDrainContainer();
}
if ($this->server->proxySet() && ! $this->server->proxy->force_stop) {
$this->server->proxyType();
$foundProxyContainer = $this->containers->filter(function ($value, $key) {
@@ -106,39 +104,6 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
}
private function serverStatus()
{
['uptime' => $uptime] = $this->server->validateConnection(false);
if ($uptime) {
if ($this->server->unreachable_notification_sent === true) {
$this->server->update(['unreachable_notification_sent' => false]);
}
} else {
// $this->server->team?->notify(new Unreachable($this->server));
foreach ($this->applications as $application) {
$application->update(['status' => 'exited']);
}
foreach ($this->databases as $database) {
$database->update(['status' => 'exited']);
}
foreach ($this->services as $service) {
$apps = $service->applications()->get();
$dbs = $service->databases()->get();
foreach ($apps as $app) {
$app->update(['status' => 'exited']);
}
foreach ($dbs as $db) {
$db->update(['status' => 'exited']);
}
}
return false;
}
return true;
}
private function checkLogDrainContainer()
{
$foundLogDrainContainer = $this->containers->filter(function ($value, $key) {

View File

@@ -1,60 +0,0 @@
<?php
namespace App\Jobs;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ServerStatusJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int|string|null $disk_usage = null;
public $tries = 3;
public function backoff(): int
{
return isDev() ? 1 : 3;
}
public function __construct(public Server $server) {}
public function handle()
{
if (! $this->server->isServerReady($this->tries)) {
throw new \RuntimeException('Server is not ready.');
}
try {
if ($this->server->isFunctional()) {
$this->remove_unnecessary_coolify_yaml();
if ($this->server->isSentinelEnabled()) {
$this->server->checkSentinel();
}
}
} catch (\Throwable $e) {
// send_internal_notification('ServerStatusJob failed with: '.$e->getMessage());
ray($e->getMessage());
return handleError($e);
}
}
private function remove_unnecessary_coolify_yaml()
{
// This will remote the coolify.yaml file from the server as it is not needed on cloud servers
if (isCloud() && $this->server->id !== 0) {
$file = $this->server->proxyPath().'/dynamic/coolify.yaml';
return instant_remote_process([
"rm -f $file",
], $this->server, false);
}
}
}

View File

@@ -10,7 +10,6 @@ use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\RateLimiter;
class ServerStorageCheckJob implements ShouldBeEncrypted, ShouldQueue
@@ -39,7 +38,7 @@ class ServerStorageCheckJob implements ShouldBeEncrypted, ShouldQueue
if (is_null($this->percentage)) {
$this->percentage = $this->server->storageCheck();
Log::info('Server storage check percentage: '.$this->percentage);
loggy('Server storage check percentage: '.$this->percentage);
}
if (! $this->percentage) {
return 'No percentage could be retrieved.';

View File

@@ -14,6 +14,18 @@ class Index extends Component
public $search = '';
public function mount()
{
if (! isCloud()) {
return redirect()->route('dashboard');
}
if (auth()->user()->id !== 0) {
return redirect()->route('dashboard');
}
$this->getSubscribers();
}
public function submitSearch()
{
if ($this->search !== '') {
@@ -38,17 +50,6 @@ class Index extends Component
}
}
public function mount()
{
if (! isCloud()) {
return redirect()->route('dashboard');
}
if (auth()->user()->id !== 0) {
return redirect()->route('dashboard');
}
$this->getSubscribers();
}
public function getSubscribers()
{
$this->inactive_subscribers = User::whereDoesntHave('teams', function ($query) {

View File

@@ -19,7 +19,7 @@ class NavbarDeleteTeam extends Component
public function delete($password)
{
if (! InstanceSettings::get('disable_two_step_confirmation')) {
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');

View File

@@ -59,7 +59,7 @@ class BackupEdit extends Component
public function delete($password)
{
if (! InstanceSettings::get('disable_two_step_confirmation')) {
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');

View File

@@ -42,7 +42,7 @@ class BackupExecutions extends Component
public function deleteBackup($executionId, $password)
{
if (! InstanceSettings::get('disable_two_step_confirmation')) {
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');

View File

@@ -88,7 +88,7 @@ class FileStorage extends Component
public function delete($password)
{
if (! InstanceSettings::get('disable_two_step_confirmation')) {
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');

View File

@@ -50,7 +50,7 @@ class ServiceApplicationView extends Component
public function delete($password)
{
if (! InstanceSettings::get('disable_two_step_confirmation')) {
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');

View File

@@ -92,7 +92,7 @@ class Danger extends Component
public function delete($password)
{
if (! InstanceSettings::get('disable_two_step_confirmation')) {
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');

View File

@@ -119,7 +119,7 @@ class Destination extends Component
public function removeServer(int $network_id, int $server_id, $password)
{
if (! InstanceSettings::get('disable_two_step_confirmation')) {
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');

View File

@@ -39,7 +39,7 @@ class GetLogs extends Component
public ?bool $showTimeStamps = true;
public int $numberOfLines = 100;
public ?int $numberOfLines = 100;
public function mount()
{
@@ -98,7 +98,7 @@ class GetLogs extends Component
if (! $refresh && ($this->resource?->getMorphClass() === 'App\Models\Service' || str($this->container)->contains('-pr-'))) {
return;
}
if ($this->numberOfLines <= 0) {
if ($this->numberOfLines <= 0 || is_null($this->numberOfLines)) {
$this->numberOfLines = 1000;
}
if ($this->container) {

View File

@@ -41,7 +41,7 @@ class Show extends Component
public function delete($password)
{
if (! InstanceSettings::get('disable_two_step_confirmation')) {
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');

View File

@@ -17,7 +17,7 @@ class Delete extends Component
public function delete($password)
{
if (! InstanceSettings::get('disable_two_step_confirmation')) {
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');

View File

@@ -174,7 +174,21 @@ class Form extends Component
$this->server->settings->refresh();
return handleError($e, $this);
} finally {}
} finally {
}
}
public function saveSentinel()
{
try {
$this->validate();
$this->server->settings->save();
$this->dispatch('success', 'Sentinel updated.');
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
$this->checkSyncStatus();
}
}
public function restartSentinel($notification = true)
@@ -184,7 +198,8 @@ class Form extends Component
$this->validate([
'server.settings.sentinel_custom_url' => 'required|url',
]);
$this->server->restartSentinel();
$this->server->settings->save();
$this->server->restartSentinel(async: false);
if ($notification) {
$this->dispatch('success', 'Sentinel restarted.');
}

View File

@@ -5,6 +5,8 @@ namespace App\Livewire\Settings;
use App\Jobs\CheckForUpdatesJob;
use App\Models\InstanceSettings;
use App\Models\Server;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Component;
class Index extends Component
@@ -185,8 +187,14 @@ class Index extends Component
return view('livewire.settings.index');
}
public function toggleTwoStepConfirmation()
public function toggleTwoStepConfirmation($password)
{
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
$this->settings->disable_two_step_confirmation = true;
$this->settings->save();
$this->disable_two_step_confirmation = true;

View File

@@ -28,6 +28,9 @@ class License extends Component
if (! isCloud()) {
abort(404);
}
if (! isInstanceAdmin()) {
return redirect()->route('home');
}
$this->instance_id = config('app.id');
$this->settings = instanceSettings();
}

View File

@@ -24,6 +24,9 @@ class SettingsOauth extends Component
public function mount()
{
if (! isInstanceAdmin()) {
return redirect()->route('home');
}
$this->oauth_settings_map = OauthSetting::all()->sortBy('provider')->reduce(function ($carry, $setting) {
$carry[$setting->provider] = $setting;

View File

@@ -7,19 +7,19 @@ use Livewire\Component;
class Deployments extends Component
{
public $deployments_per_tag_per_server = [];
public $deploymentsPerTagPerServer = [];
public $resource_ids = [];
public $resourceIds = [];
public function render()
{
return view('livewire.tags.deployments');
}
public function get_deployments()
public function getDeployments()
{
try {
$this->deployments_per_tag_per_server = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('application_id', $this->resource_ids)->get([
$this->deploymentsPerTagPerServer = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('application_id', $this->resourceIds)->get([
'id',
'application_id',
'application_name',
@@ -29,7 +29,7 @@ class Deployments extends Component
'server_id',
'status',
])->sortBy('id')->groupBy('server_name')->toArray();
$this->dispatch('deployments', $this->deployments_per_tag_per_server);
$this->dispatch('deployments', $this->deploymentsPerTagPerServer);
} catch (\Exception $e) {
return handleError($e, $this);
}

View File

@@ -5,9 +5,11 @@ namespace App\Livewire\Tags;
use App\Http\Controllers\Api\DeployController;
use App\Models\Tag;
use Illuminate\Support\Collection;
use Livewire\Attributes\Title;
use Livewire\Attributes\Url;
use Livewire\Component;
#[Title('Tags | Coolify')]
class Index extends Component
{
#[Url()]
@@ -21,33 +23,47 @@ class Index extends Component
public $webhook = null;
public $deployments_per_tag_per_server = [];
public $deploymentsPerTagPerServer = [];
protected $listeners = ['deployments' => 'update_deployments'];
protected $listeners = ['deployments' => 'updateDeployments'];
public function update_deployments($deployments)
public function render()
{
$this->deployments_per_tag_per_server = $deployments;
return view('livewire.tags.index');
}
public function tag_updated()
public function mount()
{
$this->tags = Tag::ownedByCurrentTeam()->get()->unique('name')->sortBy('name');
if ($this->tag) {
$this->tagUpdated();
}
}
public function updateDeployments($deployments)
{
$this->deploymentsPerTagPerServer = $deployments;
}
public function tagUpdated()
{
if ($this->tag == '') {
return;
}
$tag = $this->tags->where('name', $this->tag)->first();
$sanitizedTag = htmlspecialchars($this->tag, ENT_QUOTES, 'UTF-8');
$tag = $this->tags->where('name', $sanitizedTag)->first();
if (! $tag) {
$this->dispatch('error', "Tag ({$this->tag}) not found.");
$this->dispatch('error', 'Tag ('.e($sanitizedTag).') not found.');
$this->tag = '';
return;
}
$this->webhook = generatTagDeployWebhook($tag->name);
$this->webhook = generateTagDeployWebhook($tag->name);
$this->applications = $tag->applications()->get();
$this->services = $tag->services()->get();
}
public function redeploy_all()
public function redeployAll()
{
try {
$this->applications->each(function ($resource) {
@@ -63,17 +79,4 @@ class Index extends Component
return handleError($e, $this);
}
}
public function mount()
{
$this->tags = Tag::ownedByCurrentTeam()->get()->unique('name')->sortBy('name');
if ($this->tag) {
$this->tag_updated();
}
}
public function render()
{
return view('livewire.tags.index');
}
}

View File

@@ -5,8 +5,10 @@ namespace App\Livewire\Tags;
use App\Http\Controllers\Api\DeployController;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Tag;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Title('Tags | Coolify')]
class Show extends Component
{
public $tags;
@@ -28,7 +30,7 @@ class Show extends Component
if (! $tag) {
return redirect()->route('tags.index');
}
$this->webhook = generatTagDeployWebhook($tag->name);
$this->webhook = generateTagDeployWebhook($tag->name);
$this->applications = $tag->applications()->get();
$this->services = $tag->services()->get();
$this->tag = $tag;

View File

@@ -78,7 +78,7 @@ class AdminView extends Component
public function delete($id, $password)
{
if (! InstanceSettings::get('disable_two_step_confirmation')) {
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');

View File

@@ -41,6 +41,9 @@ class InviteLink extends Component
{
try {
$this->validate();
if (auth()->user()->role() === 'admin' && $this->role === 'owner') {
throw new \Exception('Admins cannot invite owners.');
}
$member_emails = currentTeam()->members()->get()->pluck('email');
if ($member_emails->contains($this->email)) {
return handleError(livewire: $this, customErrorMessage: "$this->email is already a member of ".currentTeam()->name.'.');

View File

@@ -4,10 +4,12 @@ namespace App\Livewire\Team;
use App\Models\User;
use Illuminate\Support\Facades\Cache;
use Livewire\Attributes\Locked;
use Livewire\Component;
class Member extends Component
{
#[Locked]
public User $member;
public function makeAdmin()

View File

@@ -5,7 +5,9 @@ namespace App\Models;
use App\Actions\Server\InstallDocker;
use App\Actions\Server\StartSentinel;
use App\Enums\ProxyTypes;
use App\Jobs\PullSentinelImageJob;
use App\Jobs\CheckAndStartSentinelJob;
use App\Notifications\Server\Reachable;
use App\Notifications\Server\Unreachable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\SoftDeletes;
@@ -61,6 +63,7 @@ class Server extends BaseModel
$payload['ip'] = str($server->ip)->trim();
}
$server->forceFill($payload);
});
static::created(function ($server) {
ServerSetting::create([
@@ -115,12 +118,15 @@ class Server extends BaseModel
});
}
public $casts = [
protected $casts = [
'proxy' => SchemalessAttributes::class,
'logdrain_axiom_api_key' => 'encrypted',
'logdrain_newrelic_license_key' => 'encrypted',
'delete_unused_volumes' => 'boolean',
'delete_unused_networks' => 'boolean',
'unreachable_notification_sent' => 'boolean',
'is_build_server' => 'boolean',
'force_disabled' => 'boolean',
];
protected $schemalessAttributes = [
@@ -139,11 +145,11 @@ class Server extends BaseModel
protected $guarded = [];
public function type()
{
return 'server';
}
public static function isReachable()
{
return Server::ownedByCurrentTeam()->whereRelation('settings', 'is_reachable', true);
@@ -514,16 +520,14 @@ $schema://$host {
public function forceEnableServer()
{
$this->settings->update([
'force_disabled' => false,
]);
$this->settings->force_disabled = false;
$this->settings->save();
}
public function forceDisableServer()
{
$this->settings->update([
'force_disabled' => true,
]);
$this->settings->force_disabled = true;
$this->settings->save();
$sshKeyFileLocation = "id.root@{$this->uuid}";
Storage::disk('ssh-keys')->delete($sshKeyFileLocation);
Storage::disk('ssh-mux')->delete($this->muxFilename());
@@ -570,21 +574,9 @@ $schema://$host {
return $this->settings->is_sentinel_enabled;
}
public function checkSentinel()
{
// ray("Checking sentinel on server: {$this->name}");
if ($this->isSentinelEnabled()) {
$sentinel_found = instant_remote_process(['docker inspect coolify-sentinel'], $this, false);
$sentinel_found = json_decode($sentinel_found, true);
$status = data_get($sentinel_found, '0.State.Status', 'exited');
if ($status !== 'running') {
// ray('Sentinel is not running, starting it...');
PullSentinelImageJob::dispatch($this);
} else {
// ray('Sentinel is running');
}
}
CheckAndStartSentinelJob::dispatch($this);
}
public function getCpuMetrics(int $mins = 5)
@@ -631,72 +623,6 @@ $schema://$host {
}
}
public function isServerReady(int $tries = 3)
{
if ($this->skipServer()) {
return false;
}
$serverUptimeCheckNumber = $this->unreachable_count;
if ($this->unreachable_count < $tries) {
$serverUptimeCheckNumber = $this->unreachable_count + 1;
}
if ($this->unreachable_count > $tries) {
$serverUptimeCheckNumber = $tries;
}
$serverUptimeCheckNumberMax = $tries;
// ray('server: ' . $this->name);
// ray('serverUptimeCheckNumber: ' . $serverUptimeCheckNumber);
// ray('serverUptimeCheckNumberMax: ' . $serverUptimeCheckNumberMax);
['uptime' => $uptime] = $this->validateConnection();
if ($uptime) {
if ($this->unreachable_notification_sent === true) {
$this->update(['unreachable_notification_sent' => false]);
}
return true;
} else {
if ($serverUptimeCheckNumber >= $serverUptimeCheckNumberMax) {
// Reached max number of retries
if ($this->unreachable_notification_sent === false) {
ray('Server unreachable, sending notification...');
// $this->team?->notify(new Unreachable($this));
$this->update(['unreachable_notification_sent' => true]);
}
if ($this->settings->is_reachable === true) {
$this->settings()->update([
'is_reachable' => false,
]);
}
foreach ($this->applications() as $application) {
$application->update(['status' => 'exited']);
}
foreach ($this->databases() as $database) {
$database->update(['status' => 'exited']);
}
foreach ($this->services()->get() as $service) {
$apps = $service->applications()->get();
$dbs = $service->databases()->get();
foreach ($apps as $app) {
$app->update(['status' => 'exited']);
}
foreach ($dbs as $db) {
$db->update(['status' => 'exited']);
}
}
} else {
$this->update([
'unreachable_count' => $this->unreachable_count + 1,
]);
}
return false;
}
}
public function getDiskUsage(): ?string
{
return instant_remote_process(['df / --output=pcent | tr -cd 0-9'], $this, false);
@@ -1045,29 +971,43 @@ $schema://$host {
return data_get($this, 'settings.is_swarm_worker');
}
public function serverStatus(): bool
{
if ($this->status() === false) {
return false;
}
if ($this->isFunctional() === false) {
return false;
}
return true;
}
public function status(): bool
{
if ($this->skipServer()) {
return false;
}
['uptime' => $uptime] = $this->validateConnection(false);
if ($uptime) {
if ($this->unreachable_notification_sent === true) {
$this->update(['unreachable_notification_sent' => false]);
if ($uptime === false) {
foreach ($this->applications() as $application) {
$application->status = 'exited';
$application->save();
}
} else {
// $this->server->team?->notify(new Unreachable($this->server));
foreach ($this->applications as $application) {
$application->update(['status' => 'exited']);
foreach ($this->databases() as $database) {
$database->status = 'exited';
$database->save();
}
foreach ($this->databases as $database) {
$database->update(['status' => 'exited']);
}
foreach ($this->services as $service) {
foreach ($this->services() as $service) {
$apps = $service->applications()->get();
$dbs = $service->databases()->get();
foreach ($apps as $app) {
$app->update(['status' => 'exited']);
$app->status = 'exited';
$app->save();
}
foreach ($dbs as $db) {
$db->update(['status' => 'exited']);
$db->status = 'exited';
$db->save();
}
}
@@ -1077,39 +1017,65 @@ $schema://$host {
return true;
}
public function isReachableChanged()
{
$this->refresh();
$unreachableNotificationSent = (bool) $this->unreachable_notification_sent;
$isReachable = (bool) $this->settings->is_reachable;
loggy('Server setting is_reachable changed to '.$isReachable.' for server '.$this->id.'. Unreachable notification sent: '.$unreachableNotificationSent);
// If the server is reachable, send the reachable notification if it was sent before
if ($isReachable === true) {
if ($unreachableNotificationSent === true) {
$this->sendReachableNotification();
}
} else {
// If the server is unreachable, send the unreachable notification if it was not sent before
if ($unreachableNotificationSent === false) {
$this->sendUnreachableNotification();
}
}
}
public function sendReachableNotification()
{
$this->unreachable_notification_sent = false;
$this->save();
$this->refresh();
$this->team->notify(new Reachable($this));
}
public function sendUnreachableNotification()
{
$this->unreachable_notification_sent = true;
$this->save();
$this->refresh();
$this->team->notify(new Unreachable($this));
}
public function validateConnection($isManualCheck = true)
{
config()->set('constants.ssh.mux_enabled', ! $isManualCheck);
// ray('Manual Check: ' . ($isManualCheck ? 'true' : 'false'));
$server = Server::find($this->id);
if (! $server) {
return ['uptime' => false, 'error' => 'Server not found.'];
}
if ($server->skipServer()) {
if ($this->skipServer()) {
return ['uptime' => false, 'error' => 'Server skipped.'];
}
try {
// Make sure the private key is stored
if ($server->privateKey) {
$server->privateKey->storeInFileSystem();
if ($this->privateKey) {
$this->privateKey->storeInFileSystem();
}
instant_remote_process(['ls /'], $server);
$server->settings()->update([
'is_reachable' => true,
]);
$server->update([
'unreachable_count' => 0,
]);
if (data_get($server, 'unreachable_notification_sent') === true) {
$server->update(['unreachable_notification_sent' => false]);
instant_remote_process(['ls /'], $this);
if ($this->settings->is_reachable === false) {
$this->settings->is_reachable = true;
$this->settings->save();
}
return ['uptime' => true, 'error' => null];
} catch (\Throwable $e) {
$server->settings()->update([
'is_reachable' => false,
]);
if ($this->settings->is_reachable === true) {
$this->settings->is_reachable = false;
$this->settings->save();
}
return ['uptime' => false, 'error' => $e->getMessage()];
}
@@ -1265,14 +1231,19 @@ $schema://$host {
return str($this->ip)->contains(':');
}
public function restartSentinel()
public function restartSentinel(bool $async = true): void
{
try {
StartSentinel::dispatch($this,true);
if ($async) {
StartSentinel::dispatch($this, true);
} else {
StartSentinel::run($this, true);
}
} catch (\Throwable $e) {
loggy('Error restarting Sentinel: '.$e->getMessage());
}
}
public function url()
{
return base_url().'/server/'.$this->uuid;

View File

@@ -54,6 +54,8 @@ class ServerSetting extends Model
'force_docker_cleanup' => 'boolean',
'docker_cleanup_threshold' => 'integer',
'sentinel_token' => 'encrypted',
'is_reachable' => 'boolean',
'is_usable' => 'boolean',
];
protected static function booted()
@@ -70,15 +72,18 @@ class ServerSetting extends Model
loggy('Error creating server setting: '.$e->getMessage());
}
});
static::updated(function ($setting) {
static::updated(function ($settings) {
if (
$setting->isDirty('sentinel_token') ||
$setting->isDirty('sentinel_custom_url') ||
$setting->isDirty('sentinel_metrics_refresh_rate_seconds') ||
$setting->isDirty('sentinel_metrics_history_days') ||
$setting->isDirty('sentinel_push_interval_seconds')
$settings->isDirty('sentinel_token') ||
$settings->isDirty('sentinel_custom_url') ||
$settings->isDirty('sentinel_metrics_refresh_rate_seconds') ||
$settings->isDirty('sentinel_metrics_history_days') ||
$settings->isDirty('sentinel_push_interval_seconds')
) {
$setting->server->restartSentinel();
$settings->server->restartSentinel();
}
if ($settings->isDirty('is_reachable')) {
$settings->server->isReachableChanged();
}
});
}
@@ -106,12 +111,13 @@ class ServerSetting extends Model
$domain = 'http://host.docker.internal:8000';
} elseif ($settings->fqdn) {
$domain = $settings->fqdn;
} elseif ($settings->ipv4) {
$domain = $settings->ipv4.':8000';
} elseif ($settings->ipv6) {
$domain = $settings->ipv6.':8000';
} elseif ($settings->public_ipv4) {
$domain = 'http://'.$settings->public_ipv4.':8000';
} elseif ($settings->public_ipv6) {
$domain = 'http://'.$settings->public_ipv6.':8000';
}
$this->sentinel_custom_url = $domain;
loggy('Sentinel URL: '.$domain);
if ($save) {
$this->save();
}

View File

@@ -3,9 +3,6 @@
namespace App\Notifications\Server;
use App\Models\Server;
use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\TelegramChannel;
use App\Notifications\Dto\DiscordMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;

View File

@@ -2,8 +2,6 @@
namespace App\Notifications\Server;
use App\Actions\Docker\GetContainersStatus;
use App\Jobs\ContainerStatusJob;
use App\Models\Server;
use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel;
@@ -13,25 +11,28 @@ use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use Illuminate\Support\Facades\RateLimiter;
class Revived extends Notification implements ShouldQueue
class Reachable extends Notification implements ShouldQueue
{
use Queueable;
public $tries = 1;
protected bool $isRateLimited = false;
public function __construct(public Server $server)
{
if ($this->server->unreachable_notification_sent === false) {
return;
}
GetContainersStatus::dispatch($server)->onQueue('high');
// dispatch(new ContainerStatusJob($server));
$this->isRateLimited = isEmailRateLimited(
limiterKey: 'server-reachable:'.$this->server->id,
);
}
public function via(object $notifiable): array
{
if ($this->isRateLimited) {
return [];
}
$channels = [];
$isEmailEnabled = isEmailEnabled($notifiable);
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
@@ -46,20 +47,8 @@ class Revived extends Notification implements ShouldQueue
if ($isTelegramEnabled) {
$channels[] = TelegramChannel::class;
}
$executed = RateLimiter::attempt(
'notification-server-revived-'.$this->server->uuid,
1,
function () use ($channels) {
return $channels;
},
7200,
);
if (! $executed) {
return [];
}
return $executed;
return $channels;
}
public function toMail(): MailMessage

View File

@@ -11,7 +11,6 @@ use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use Illuminate\Support\Facades\RateLimiter;
class Unreachable extends Notification implements ShouldQueue
{
@@ -19,10 +18,21 @@ class Unreachable extends Notification implements ShouldQueue
public $tries = 1;
public function __construct(public Server $server) {}
protected bool $isRateLimited = false;
public function __construct(public Server $server)
{
$this->isRateLimited = isEmailRateLimited(
limiterKey: 'server-unreachable:'.$this->server->id,
);
}
public function via(object $notifiable): array
{
if ($this->isRateLimited) {
return [];
}
$channels = [];
$isEmailEnabled = isEmailEnabled($notifiable);
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
@@ -37,23 +47,11 @@ class Unreachable extends Notification implements ShouldQueue
if ($isTelegramEnabled) {
$channels[] = TelegramChannel::class;
}
$executed = RateLimiter::attempt(
'notification-server-unreachable-'.$this->server->uuid,
1,
function () use ($channels) {
return $channels;
},
7200,
);
if (! $executed) {
return [];
}
return $executed;
return $channels;
}
public function toMail(): MailMessage
public function toMail(): ?MailMessage
{
$mail = new MailMessage;
$mail->subject("Coolify: Your server ({$this->server->name}) is unreachable.");
@@ -64,7 +62,7 @@ class Unreachable extends Notification implements ShouldQueue
return $mail;
}
public function toDiscord(): DiscordMessage
public function toDiscord(): ?DiscordMessage
{
$message = new DiscordMessage(
title: ':cross_mark: Server unreachable',
@@ -77,7 +75,7 @@ class Unreachable extends Notification implements ShouldQueue
return $message;
}
public function toTelegram(): array
public function toTelegram(): ?array
{
return [
'message' => "Coolify: Your server '{$this->server->name}' is unreachable. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server and turn on all automations & integrations.",