diff --git a/.env.development.example b/.env.development.example index c956daafd..920c32d92 100644 --- a/.env.development.example +++ b/.env.development.example @@ -1,11 +1,3 @@ -############################################################################################################ -# Development Environment - -# User and group id for the user that will run the application inside the container -# Run in your terminal: `id -u` and `id -g` and that's the results -USERID= -GROUPID= -############################################################################################################ APP_NAME=Coolify-localhost APP_ID=development APP_ENV=local @@ -13,6 +5,7 @@ APP_KEY= APP_DEBUG=true APP_URL=http://localhost APP_PORT=8000 +MUX_ENABLED=false DUSK_DRIVER_URL=http://selenium:4444 diff --git a/CONTRIBUTION.md b/CONTRIBUTION.md index ef67baba1..0952e1bd8 100644 --- a/CONTRIBUTION.md +++ b/CONTRIBUTION.md @@ -15,11 +15,12 @@ You can ask for guidance anytime on our ## 2) Set your environment variables - Copy [.env.development.example](./.env.development.example) to .env. -- If necessary, set `USERID` & `GROUPID` accordingly (read in .env file). ## 3) Start & setup Coolify - Run `spin up` - You can notice that errors will be thrown. Don't worry. + - If you see weird permission errors, especially on Mac, run `sudo spin up` instead. + - Run `./scripts/run setup:dev` - This will generate a secret key for you, delete any existing database layouts, migrate database to the new layout, and seed your database. ## 4) Start development diff --git a/app/Actions/Server/InstallDocker.php b/app/Actions/Server/InstallDocker.php index 2fb12d34c..e99e8a11b 100644 --- a/app/Actions/Server/InstallDocker.php +++ b/app/Actions/Server/InstallDocker.php @@ -2,12 +2,14 @@ namespace App\Actions\Server; +use Lorisleiva\Actions\Concerns\AsAction; use App\Models\Server; use App\Models\StandaloneDocker; class InstallDocker { - public function __invoke(Server $server) + use AsAction; + public function handle(Server $server) { $dockerVersion = '24.0'; $config = base64_encode('{ diff --git a/app/Console/Commands/Cloud.php b/app/Console/Commands/Cloud.php new file mode 100644 index 000000000..1386b296c --- /dev/null +++ b/app/Console/Commands/Cloud.php @@ -0,0 +1,33 @@ +whereNotNull('team.subscription')->where('team.subscription.stripe_trial_already_ended',true)->each(function($server){ + $this->info($server->name); + }); + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index ee9f67ad7..d6ea360ae 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -48,7 +48,11 @@ class Kernel extends ConsoleKernel } private function check_resources($schedule) { - $servers = Server::all()->where('settings.is_usable', true)->where('settings.is_reachable', true); + if (isCloud()) { + $servers = Server::all()->whereNotNull('team.subscription')->where('team.subscription.stripe_trial_already_ended', false); + } else { + $servers = Server::all(); + } foreach ($servers as $server) { $schedule->job(new ContainerStatusJob($server))->everyMinute()->onOneServer(); } diff --git a/app/Http/Controllers/ServerController.php b/app/Http/Controllers/ServerController.php deleted file mode 100644 index 21ca2ba77..000000000 --- a/app/Http/Controllers/ServerController.php +++ /dev/null @@ -1,32 +0,0 @@ -get(); - if (!isCloud()) { - return view('server.create', [ - 'limit_reached' => false, - 'private_keys' => $privateKeys, - ]); - } - $team = currentTeam(); - $servers = $team->servers->count(); - ['serverLimit' => $serverLimit] = $team->limits; - $limit_reached = $servers >= $serverLimit; - - return view('server.create', [ - 'limit_reached' => $limit_reached, - 'private_keys' => $privateKeys, - ]); - } -} diff --git a/app/Http/Livewire/Boarding/Index.php b/app/Http/Livewire/Boarding/Index.php index a0c5038e0..c8a53d26e 100644 --- a/app/Http/Livewire/Boarding/Index.php +++ b/app/Http/Livewire/Boarding/Index.php @@ -220,7 +220,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== public function installDocker() { $this->dockerInstallationStarted = true; - $activity = resolve(InstallDocker::class)($this->createdServer); + $activity = InstallDocker::run($this->createdServer); $this->emit('newMonitorActivity', $activity->id); } public function dockerInstalledOrSkipped() diff --git a/app/Http/Livewire/Project/Application/General.php b/app/Http/Livewire/Project/Application/General.php index b9d65f504..52212dc9e 100644 --- a/app/Http/Livewire/Project/Application/General.php +++ b/app/Http/Livewire/Project/Application/General.php @@ -17,10 +17,10 @@ class General extends Component public Application $application; public Collection $services; public string $name; - public string|null $fqdn; + public ?string $fqdn = null; public string $git_repository; public string $git_branch; - public string|null $git_commit_sha; + public ?string $git_commit_sha = null; public string $build_pack; public bool $is_static; diff --git a/app/Http/Livewire/Project/New/PublicGitRepository.php b/app/Http/Livewire/Project/New/PublicGitRepository.php index 7a206fae4..ebcbb8253 100644 --- a/app/Http/Livewire/Project/New/PublicGitRepository.php +++ b/app/Http/Livewire/Project/New/PublicGitRepository.php @@ -144,7 +144,7 @@ class PublicGitRepository extends Component if ($this->git_source === 'other') { $application_init = [ - 'name' => generate_application_name($this->git_repository, $this->git_branch), + 'name' => generate_random_name(), 'git_repository' => $this->git_repository, 'git_branch' => $this->git_branch, 'build_pack' => 'nixpacks', @@ -178,7 +178,6 @@ class PublicGitRepository extends Component $fqdn = generateFqdn($destination->server, $application->uuid); $application->fqdn = $fqdn; - $application->name = generate_application_name($this->git_repository, $this->git_branch, $application->uuid); $application->save(); return redirect()->route('project.application.configuration', [ diff --git a/app/Http/Livewire/Server/Create.php b/app/Http/Livewire/Server/Create.php new file mode 100644 index 000000000..3d1953513 --- /dev/null +++ b/app/Http/Livewire/Server/Create.php @@ -0,0 +1,29 @@ +private_keys = PrivateKey::ownedByCurrentTeam()->get(); + if (!isCloud()) { + $this->limit_reached = false; + return; + } + $team = currentTeam(); + $servers = $team->servers->count(); + ['serverLimit' => $serverLimit] = $team->limits; + + $this->limit_reached = $servers >= $serverLimit; + } + public function render() + { + return view('livewire.server.create'); + } +} diff --git a/app/Http/Livewire/Server/Destination/Show.php b/app/Http/Livewire/Server/Destination/Show.php new file mode 100644 index 000000000..e021f9605 --- /dev/null +++ b/app/Http/Livewire/Server/Destination/Show.php @@ -0,0 +1,28 @@ +parameters = get_route_parameters(); + try { + $this->server = Server::ownedByCurrentTeam(['name', 'proxy'])->whereUuid(request()->server_uuid)->first(); + if (is_null($this->server)) { + return redirect()->route('server.all'); + } + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + public function render() + { + return view('livewire.server.destination.show'); + } +} diff --git a/app/Http/Livewire/Server/Form.php b/app/Http/Livewire/Server/Form.php index 9f6cb594f..18fb1dbec 100644 --- a/app/Http/Livewire/Server/Form.php +++ b/app/Http/Livewire/Server/Form.php @@ -11,11 +11,12 @@ class Form extends Component { use AuthorizesRequests; public Server $server; - public $uptime; - public $dockerVersion; - public string|null $wildcard_domain = null; + public bool $isValidConnection = false; + public bool $isValidDocker = false; + public ?string $wildcard_domain = null; public int $cleanup_after_percentage; public bool $dockerInstallationStarted = false; + protected $listeners = ['serverRefresh']; protected $rules = [ 'server.name' => 'required|min:6', @@ -44,37 +45,49 @@ class Form extends Component $this->wildcard_domain = $this->server->settings->wildcard_domain; $this->cleanup_after_percentage = $this->server->settings->cleanup_after_percentage; } - public function instantSave() { + public function serverRefresh() { + $this->validateServer(); + } + public function instantSave() + { refresh_server_connection($this->server->privateKey); $this->validateServer(); $this->server->settings->save(); } public function installDocker() { + $this->emit('installDocker'); $this->dockerInstallationStarted = true; - $activity = resolve(InstallDocker::class)($this->server); + $activity = InstallDocker::run($this->server); $this->emit('newMonitorActivity', $activity->id); } - public function validateServer() + public function validateServer($install = true) { try { - ['uptime' => $uptime, 'dockerVersion' => $dockerVersion] = validateServer($this->server, true); + $uptime = $this->server->validateConnection(); if ($uptime) { - $this->uptime = $uptime; - $this->emit('success', 'Server is reachable.'); + $install && $this->emit('success', 'Server is reachable.'); } else { - $this->emit('error', 'Server is not reachable.'); + $install &&$this->emit('error', 'Server is not reachable. Please check your connection and private key configuration.'); return; } - if ($dockerVersion) { - $this->dockerVersion = $dockerVersion; - $this->emit('success', 'Docker Engine 23+ is installed!'); + $dockerInstalled = $this->server->validateDockerEngine(); + if ($dockerInstalled) { + $install && $this->emit('success', 'Docker Engine is installed.
Checking version.'); } else { - $this->emit('error', 'No Docker Engine or older than 23 version installed.'); + $install && $this->installDocker(); + return; + } + $dockerVersion = $this->server->validateDockerEngineVersion(); + if ($dockerVersion) { + $install && $this->emit('success', 'Docker Engine version is 23+.'); + } else { + $install && $this->installDocker(); + return; } } catch (\Throwable $e) { - return handleError($e, $this, customErrorMessage: "Server is not reachable: "); + return handleError($e, $this); } finally { $this->emit('proxyStatusUpdated'); } diff --git a/app/Http/Livewire/Server/PrivateKey/Show.php b/app/Http/Livewire/Server/PrivateKey/Show.php new file mode 100644 index 000000000..51c91350e --- /dev/null +++ b/app/Http/Livewire/Server/PrivateKey/Show.php @@ -0,0 +1,31 @@ +parameters = get_route_parameters(); + try { + $this->server = Server::ownedByCurrentTeam(['name', 'proxy'])->whereUuid(request()->server_uuid)->first(); + if (is_null($this->server)) { + return redirect()->route('server.all'); + } + $this->privateKeys = PrivateKey::ownedByCurrentTeam()->get()->where('is_git_related', false); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + public function render() + { + return view('livewire.server.private-key.show'); + } +} diff --git a/app/Http/Livewire/Server/Proxy/Deploy.php b/app/Http/Livewire/Server/Proxy/Deploy.php index 85899d7b7..ad5884e18 100644 --- a/app/Http/Livewire/Server/Proxy/Deploy.php +++ b/app/Http/Livewire/Server/Proxy/Deploy.php @@ -11,7 +11,7 @@ class Deploy extends Component public Server $server; public bool $traefikDashboardAvailable = false; public ?string $currentRoute = null; - protected $listeners = ['proxyStatusUpdated', 'traefikDashboardAvailable']; + protected $listeners = ['proxyStatusUpdated', 'traefikDashboardAvailable', 'serverRefresh' => 'proxyStatusUpdated']; public function mount() { $this->currentRoute = request()->route()->getName(); diff --git a/app/Http/Livewire/Server/Proxy/Show.php b/app/Http/Livewire/Server/Proxy/Show.php new file mode 100644 index 000000000..daadf0ede --- /dev/null +++ b/app/Http/Livewire/Server/Proxy/Show.php @@ -0,0 +1,28 @@ +parameters = get_route_parameters(); + try { + $this->server = Server::ownedByCurrentTeam(['name', 'proxy'])->whereUuid(request()->server_uuid)->first(); + if (is_null($this->server)) { + return redirect()->route('server.all'); + } + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + public function render() + { + return view('livewire.server.proxy.show'); + } +} diff --git a/app/Http/Livewire/Server/Proxy/Status.php b/app/Http/Livewire/Server/Proxy/Status.php index 6eb198a4d..5cfd22082 100644 --- a/app/Http/Livewire/Server/Proxy/Status.php +++ b/app/Http/Livewire/Server/Proxy/Status.php @@ -26,7 +26,9 @@ class Status extends Component } public function getProxyStatusWithNoti() { - $this->emit('success', 'Refreshed proxy status.'); - $this->getProxyStatus(); + if ($this->server->isFunctional()) { + $this->emit('success', 'Refreshed proxy status.'); + $this->getProxyStatus(); + } } } diff --git a/app/Http/Livewire/Server/Show.php b/app/Http/Livewire/Server/Show.php index 79d39bb19..77ae447d7 100644 --- a/app/Http/Livewire/Server/Show.php +++ b/app/Http/Livewire/Server/Show.php @@ -10,8 +10,10 @@ class Show extends Component { use AuthorizesRequests; public ?Server $server = null; + public $parameters = []; public function mount() { + $this->parameters = get_route_parameters(); try { $this->server = Server::ownedByCurrentTeam(['name', 'description', 'ip', 'port', 'user', 'proxy'])->whereUuid(request()->server_uuid)->first(); if (is_null($this->server)) { @@ -21,6 +23,10 @@ class Show extends Component return handleError($e, $this); } } + public function submit() + { + $this->emit('serverRefresh'); + } public function render() { return view('livewire.server.show'); diff --git a/app/Http/Livewire/Server/ShowPrivateKey.php b/app/Http/Livewire/Server/ShowPrivateKey.php index c9c11becb..1974226fc 100644 --- a/app/Http/Livewire/Server/ShowPrivateKey.php +++ b/app/Http/Livewire/Server/ShowPrivateKey.php @@ -32,36 +32,34 @@ class ShowPrivateKey extends Component } } - public function checkConnection() + public function checkConnection($install = false) { try { - ['uptime' => $uptime, 'dockerVersion' => $dockerVersion] = validateServer($this->server, true); + $uptime = $this->server->validateConnection(); if ($uptime) { - $this->server->settings->update([ - 'is_reachable' => true - ]); - $this->emit('success', 'Server is reachable with this private key.'); + $install && $this->emit('success', 'Server is reachable.'); } else { - $this->server->settings->update([ - 'is_reachable' => false, - 'is_usable' => false - ]); - $this->emit('error', 'Server is not reachable with this private key.'); + $install && $this->emit('error', 'Server is not reachable. Please check your connection and private key configuration.'); return; } - if ($dockerVersion) { - $this->server->settings->update([ - 'is_usable' => true - ]); - $this->emit('success', 'Server is usable for Coolify.'); + $dockerInstalled = $this->server->validateDockerEngine(); + if ($dockerInstalled) { + $install && $this->emit('success', 'Docker Engine is installed.
Checking version.'); } else { - $this->server->settings->update([ - 'is_usable' => false - ]); - $this->emit('error', 'Old (lower than 23) or no Docker version detected. Install Docker Engine on the General tab.'); + $install && $this->installDocker(); + return; + } + $dockerVersion = $this->server->validateDockerEngineVersion(); + if ($dockerVersion) { + $install && $this->emit('success', 'Docker Engine version is 23+.'); + } else { + $install && $this->installDocker(); + return; } } catch (\Throwable $e) { return handleError($e, $this); + } finally { + $this->emit('proxyStatusUpdated'); } } diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index fc0387357..cfccb6912 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -254,7 +254,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted ); $this->prepare_builder_image(); $this->clone_repository(); - + $this->set_base_dir(); $tag = Str::of("{$this->commit}-{$this->application->id}-{$this->pull_request_id}"); if (strlen($tag) > 128) { $tag = $tag->substr(0, 128); @@ -364,6 +364,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted ]); $this->prepare_builder_image(); $this->clone_repository(); + $this->set_base_dir(); $this->cleanup_git(); if ($this->application->build_pack === 'nixpacks') { $this->generate_nixpacks_confs(); @@ -400,7 +401,13 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted ); } - + private function set_base_dir() { + $this->execute_remote_command( + [ + "echo -n 'Setting base directory to {$this->workdir}.'" + ], + ); + } private function clone_repository() { @@ -452,7 +459,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted } if ($this->application->deploymentType() === 'deploy_key') { $private_key = base64_encode($this->application->private_key->private_key); - $git_clone_command = "GIT_SSH_COMMAND=\"ssh -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$git_clone_command} {$this->application->git_full_url} {$this->workdir}"; + $git_clone_command = "GIT_SSH_COMMAND=\"ssh -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$git_clone_command} {$this->application->git_full_url} {$this->basedir}"; $git_clone_command = $this->set_git_import_settings($git_clone_command); $commands = collect([ executeInDocker($this->deployment_uuid, "mkdir -p /root/.ssh"), diff --git a/app/Jobs/ContainerStatusJob.php b/app/Jobs/ContainerStatusJob.php index f2abae3ca..6d09644dc 100644 --- a/app/Jobs/ContainerStatusJob.php +++ b/app/Jobs/ContainerStatusJob.php @@ -7,6 +7,7 @@ use App\Models\ApplicationPreview; use App\Models\Server; use App\Notifications\Container\ContainerRestarted; use App\Notifications\Container\ContainerStopped; +use App\Notifications\Server\Revived; use App\Notifications\Server\Unreachable; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldBeEncrypted; @@ -40,33 +41,49 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted { return $this->server->uuid; } - - private function checkServerConnection() - { - $uptime = instant_remote_process(['uptime'], $this->server, false); - if (!is_null($uptime)) { - return true; - } - } - public function handle(): void + public function handle() { try { + ray("checking server status for {$this->server->name}"); // ray()->clearAll(); $serverUptimeCheckNumber = 0; $serverUptimeCheckNumberMax = 3; while (true) { + ray('checking # ' . $serverUptimeCheckNumber); if ($serverUptimeCheckNumber >= $serverUptimeCheckNumberMax) { - $this->server->settings()->update(['is_reachable' => false]); - $this->server->team->notify(new Unreachable($this->server)); + send_internal_notification('Server unreachable: ' . $this->server->name); + if ($this->server->unreachable_email_sent === false) { + ray('Server unreachable, sending notification...'); + $this->server->team->notify(new Unreachable($this->server)); + } + $this->server->settings()->update([ + 'is_reachable' => false, + ]); + $this->server->update(['unreachable_email_sent' => true]); return; } - $result = $this->checkServerConnection(); + $result = $this->server->validateConnection(); if ($result) { break; } $serverUptimeCheckNumber++; sleep(5); } + if (data_get($this->server, 'unreachable_email_sent') === true) { + ray('Server is reachable again, sending notification...'); + $this->server->team->notify(new Revived($this->server)); + $this->server->update(['unreachable_email_sent' => false]); + } + if ( + data_get($this->server, 'settings.is_reachable') === false || + data_get($this->server, 'settings.is_usable') === false + ) { + $this->server->settings()->update([ + 'is_reachable' => true, + 'is_usable' => true + ]); + } + $this->server->validateDockerEngine(true); $containers = instant_remote_process(["docker container ls -q"], $this->server); if (!$containers) { return; @@ -266,7 +283,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted } catch (\Throwable $e) { send_internal_notification('ContainerStatusJob failed with: ' . $e->getMessage()); ray($e->getMessage()); - throw $e; + return handleError($e); } } } diff --git a/app/Models/Application.php b/app/Models/Application.php index 3f5eb54d5..69f4abe22 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -5,6 +5,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Relations\HasMany; use Spatie\Activitylog\Models\Activity; +use Illuminate\Support\Str; class Application extends BaseModel { @@ -12,6 +13,19 @@ class Application extends BaseModel protected static function booted() { + static::saving(function ($application) { + if ($application->fqdn == '') { + $application->fqdn = null; + } + $application->forceFill([ + 'fqdn' => $application->fqdn, + 'install_command' => Str::of($application->install_command)->trim(), + 'build_command' => Str::of($application->build_command)->trim(), + 'start_command' => Str::of($application->start_command)->trim(), + 'base_directory' => Str::of($application->base_directory)->trim(), + 'publish_directory' => Str::of($application->publish_directory)->trim(), + ]); + }); static::created(function ($application) { ApplicationSetting::create([ 'application_id' => $application->id, diff --git a/app/Models/Server.php b/app/Models/Server.php index f174222c0..5cbd6deb5 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; use Spatie\SchemalessAttributes\Casts\SchemalessAttributes; use Spatie\SchemalessAttributes\SchemalessAttributesTrait; +use Illuminate\Support\Str; class Server extends BaseModel { @@ -15,6 +16,13 @@ class Server extends BaseModel protected static function booted() { + static::saving(function ($server) { + $server->forceFill([ + 'ip' => Str::of($server->ip)->trim(), + 'user' => Str::of($server->user)->trim(), + ]); + }); + static::created(function ($server) { ServerSetting::create([ 'server_id' => $server->id, @@ -199,4 +207,48 @@ class Server extends BaseModel { return $this->settings->is_reachable && $this->settings->is_usable; } + public function validateConnection() + { + $uptime = instant_remote_process(['uptime'], $this, false); + if (!$uptime) { + $this->settings->is_reachable = false; + $this->settings->save(); + return false; + } + $this->settings->is_reachable = true; + $this->settings->save(); + return true; + } + public function validateDockerEngine($throwError = false) + { + $dockerBinary = instant_remote_process(["command -v docker"], $this, false); + if (is_null($dockerBinary)) { + $this->settings->is_usable = false; + $this->settings->save(); + if ($throwError) { + throw new \Exception('Server is not usable.'); + } + return false; + } + $this->settings->is_usable = true; + $this->settings->save(); + $this->validateCoolifyNetwork(); + return true; + } + public function validateDockerEngineVersion() + { + $dockerVersion = instant_remote_process(["docker version|head -2|grep -i version| awk '{print $2}'"], $this, false); + $dockerVersion = checkMinimumDockerEngineVersion($dockerVersion); + if (is_null($dockerVersion)) { + $this->settings->is_usable = false; + $this->settings->save(); + return false; + } + $this->settings->is_usable = true; + $this->settings->save(); + return true; + } + public function validateCoolifyNetwork() { + return instant_remote_process(["docker network create coolify --attachable >/dev/null 2>&1 || true"], $this, false); + } } diff --git a/app/Notifications/Server/Revived.php b/app/Notifications/Server/Revived.php new file mode 100644 index 000000000..07771a287 --- /dev/null +++ b/app/Notifications/Server/Revived.php @@ -0,0 +1,66 @@ +server->unreachable_email_sent === false) { + return; + } + } + + 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("✅ Server ({$this->server->name}) revived."); + $mail->view('emails.server-revived', [ + 'name' => $this->server->name, + ]); + return $mail; + } + + public function toDiscord(): string + { + $message = "✅ Server '{$this->server->name}' revived. All automations & integrations are turned on again!"; + return $message; + } + public function toTelegram(): array + { + return [ + "message" => "✅ Server '{$this->server->name}' revived. All automations & integrations are turned on again!" + ]; + } +} diff --git a/app/Notifications/Server/Unreachable.php b/app/Notifications/Server/Unreachable.php index ec9c11d11..705988e31 100644 --- a/app/Notifications/Server/Unreachable.php +++ b/app/Notifications/Server/Unreachable.php @@ -3,6 +3,9 @@ namespace App\Notifications\Server; use App\Models\Server; +use App\Notifications\Channels\DiscordChannel; +use App\Notifications\Channels\EmailChannel; +use App\Notifications\Channels\TelegramChannel; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; @@ -20,7 +23,21 @@ class Unreachable extends Notification implements ShouldQueue public function via(object $notifiable): array { - return setNotificationChannels($notifiable, 'status_changes'); + $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 @@ -35,13 +52,13 @@ class Unreachable extends Notification implements ShouldQueue public function toDiscord(): string { - $message = "⛔ Server '{$this->server->name}' is unreachable after trying to connect to it 5 times. All automations & integrations are turned off! Please check your server! IMPORTANT: You have to validate your server again after you fix the issue."; + $message = "⛔ Server '{$this->server->name}' is unreachable after trying to connect to it 5 times. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server. If your server is back online, we will automatically turn on all automations & integrations."; return $message; } public function toTelegram(): array { return [ - "message" => "⛔ Server '{$this->server->name}' is unreachable after trying to connect to it 5 times. All automations & integrations are turned off! Please check your server! IMPORTANT: You have to validate your server again after you fix the issue." + "message" => "⛔ Server '{$this->server->name}' is unreachable after trying to connect to it 5 times. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server. If your server is back online, we will automatically turn on all automations & integrations." ]; } } diff --git a/bootstrap/helpers/remoteProcess.php b/bootstrap/helpers/remoteProcess.php index e5a2a5863..8aa22d6f2 100644 --- a/bootstrap/helpers/remoteProcess.php +++ b/bootstrap/helpers/remoteProcess.php @@ -7,6 +7,8 @@ use App\Models\Application; use App\Models\ApplicationDeploymentQueue; use App\Models\PrivateKey; use App\Models\Server; +use App\Notifications\Server\Revived; +use App\Notifications\Server\Unreachable; use Carbon\Carbon; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Collection; @@ -85,7 +87,7 @@ function generateSshCommand(Server $server, string $command, bool $isMux = true) if ($isMux && config('coolify.mux_enabled')) { $ssh_command .= '-o ControlMaster=auto -o ControlPersist=1m -o ControlPath=/var/www/html/storage/app/ssh/mux/%h_%p_%r '; } - if (data_get($server,'settings.is_cloudflare_tunnel')) { + if (data_get($server, 'settings.is_cloudflare_tunnel')) { $ssh_command .= '-o ProxyCommand="/usr/local/bin/cloudflared access ssh --hostname %h" '; } $command = "PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/host/usr/local/sbin:/host/usr/local/bin:/host/usr/sbin:/host/usr/bin:/host/sbin:/host/bin && $command"; @@ -122,13 +124,14 @@ function instant_remote_process(Collection|array $command, Server $server, $thro } return $output; } -function excludeCertainErrors(string $errorOutput, ?int $exitCode = null) { +function excludeCertainErrors(string $errorOutput, ?int $exitCode = null) +{ $ignoredErrors = collect([ 'Permission denied (publickey', 'Could not resolve hostname', ]); $ignored = false; - foreach ($ignoredErrors as $ignoredError) { + foreach ($ignoredErrors as $ignoredError) { if (Str::contains($errorOutput, $ignoredError)) { $ignored = true; break; @@ -183,6 +186,9 @@ function validateServer(Server $server, bool $throwError = false) $uptime = instant_remote_process(['uptime'], $server, $throwError); if (!$uptime) { $server->settings->is_reachable = false; + $server->team->notify(new Unreachable($server)); + $server->unreachable_email_sent = true; + $server->save(); return [ "uptime" => null, "dockerVersion" => null, @@ -203,6 +209,11 @@ function validateServer(Server $server, bool $throwError = false) $server->settings->is_usable = false; } else { $server->settings->is_usable = true; + if (data_get($server, 'unreachable_email_sent') === true) { + $server->team->notify(new Revived($server)); + $server->unreachable_email_sent = false; + $server->save(); + } } return [ "uptime" => $uptime, @@ -213,7 +224,9 @@ function validateServer(Server $server, bool $throwError = false) $server->settings->is_usable = false; throw $e; } finally { - if (data_get($server, 'settings')) $server->settings->save(); + if (data_get($server, 'settings')) { + $server->settings->save(); + } } } diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index a12d6b02d..25237abab 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -310,6 +310,7 @@ function send_internal_notification(string $message): void $baseUrl = config('app.name'); $team = Team::find(0); $team->notify(new GeneralNotification("👀 {$baseUrl}: " . $message)); + ray("👀 {$baseUrl}: " . $message); } catch (\Throwable $e) { ray($e->getMessage()); } diff --git a/config/sentry.php b/config/sentry.php index 337e904e1..ab05d1fa5 100644 --- a/config/sentry.php +++ b/config/sentry.php @@ -7,7 +7,7 @@ return [ // The release version of your application // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) - 'release' => '4.0.0-beta.69', + 'release' => '4.0.0-beta.70', // When left empty or `null` the Laravel environment will be used 'environment' => config('app.env'), diff --git a/config/version.php b/config/version.php index 0dafd3ed9..c58fd74b3 100644 --- a/config/version.php +++ b/config/version.php @@ -1,3 +1,3 @@ boolean('unreachable_email_sent')->default(false); + $table->dropColumn('unreachable_count'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('servers', function (Blueprint $table) { + $table->dropColumn('unreachable_email_sent'); + $table->integer('unreachable_count')->default(0); + }); + + } +}; diff --git a/resources/css/app.css b/resources/css/app.css index ea7109d3d..c1e8f3259 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -56,7 +56,7 @@ a { @apply flex items-center p-2 transition-colors cursor-pointer min-h-16 bg-coolgray-200 hover:bg-coollabs-100 hover:text-white hover:no-underline min-w-[24rem]; } .box-without-bg { - @apply flex items-center p-2 transition-colors min-h-16 hover:text-white hover:no-underline min-w-[24rem]; + @apply flex items-center p-2 transition-colors min-h-16 hover:text-white hover:no-underline min-w-[24rem]; } .lds-heart { diff --git a/resources/views/components/server/navbar.blade.php b/resources/views/components/server/navbar.blade.php index d36a35b8f..8230c3a7c 100644 --- a/resources/views/components/server/navbar.blade.php +++ b/resources/views/components/server/navbar.blade.php @@ -8,25 +8,25 @@