Merge pull request #1295 from coollabsio/next

v4.0.0-beta.70
This commit is contained in:
Andras Bacsai 2023-10-09 11:23:32 +02:00 committed by GitHub
commit 0de042dbac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 536 additions and 182 deletions

View File

@ -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

View File

@ -15,11 +15,12 @@ ## 1) Setup your development environment
## 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

View File

@ -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('{

View File

@ -0,0 +1,33 @@
<?php
namespace App\Console\Commands;
use App\Models\Server;
use Illuminate\Console\Command;
class Cloud extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'cloud:unused-servers';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Get Unused Servers from Cloud';
/**
* Execute the console command.
*/
public function handle()
{
Server::all()->whereNotNull('team.subscription')->where('team.subscription.stripe_trial_already_ended',true)->each(function($server){
$this->info($server->name);
});
}
}

View File

@ -48,7 +48,11 @@ private function cleanup_servers($schedule)
}
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();
}

View File

@ -1,32 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Models\PrivateKey;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Validation\ValidatesRequests;
class ServerController extends Controller
{
use AuthorizesRequests, ValidatesRequests;
public function new_server()
{
$privateKeys = PrivateKey::ownedByCurrentTeam()->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,
]);
}
}

View File

@ -220,7 +220,7 @@ public function validateServer()
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()

View File

@ -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;

View File

@ -144,7 +144,7 @@ public function submit()
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 @@ public function submit()
$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', [

View File

@ -0,0 +1,29 @@
<?php
namespace App\Http\Livewire\Server;
use App\Models\PrivateKey;
use Livewire\Component;
class Create extends Component
{
public $private_keys = [];
public bool $limit_reached = false;
public function mount()
{
$this->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');
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Http\Livewire\Server\Destination;
use App\Models\Server;
use Livewire\Component;
class Show extends Component
{
public ?Server $server = null;
public $parameters = [];
public function mount()
{
$this->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');
}
}

View File

@ -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 @@ public function mount()
$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.<br> 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');
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Http\Livewire\Server\PrivateKey;
use App\Models\PrivateKey;
use App\Models\Server;
use Livewire\Component;
class Show extends Component
{
public ?Server $server = null;
public $privateKeys = [];
public $parameters = [];
public function mount()
{
$this->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');
}
}

View File

@ -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();

View File

@ -0,0 +1,28 @@
<?php
namespace App\Http\Livewire\Server\Proxy;
use App\Models\Server;
use Livewire\Component;
class Show extends Component
{
public ?Server $server = null;
public $parameters = [];
public function mount()
{
$this->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');
}
}

View File

@ -26,7 +26,9 @@ public function getProxyStatus()
}
public function getProxyStatusWithNoti()
{
$this->emit('success', 'Refreshed proxy status.');
$this->getProxyStatus();
if ($this->server->isFunctional()) {
$this->emit('success', 'Refreshed proxy status.');
$this->getProxyStatus();
}
}
}

View File

@ -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 @@ public function mount()
return handleError($e, $this);
}
}
public function submit()
{
$this->emit('serverRefresh');
}
public function render()
{
return view('livewire.server.show');

View File

@ -32,36 +32,34 @@ public function setPrivateKey($newPrivateKeyId)
}
}
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.<br> 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');
}
}

View File

@ -254,7 +254,7 @@ private function deploy()
);
$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 @@ private function deploy_pull_request()
]);
$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 @@ private function prepare_builder_image()
);
}
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 @@ private function importing_git_repository()
}
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"),

View File

@ -7,6 +7,7 @@
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 @@ public function uniqueId(): string
{
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 @@ public function handle(): void
} catch (\Throwable $e) {
send_internal_notification('ContainerStatusJob failed with: ' . $e->getMessage());
ray($e->getMessage());
throw $e;
return handleError($e);
}
}
}

View File

@ -5,6 +5,7 @@
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,

View File

@ -8,6 +8,7 @@
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 @@ public function isFunctional()
{
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);
}
}

View File

@ -0,0 +1,66 @@
<?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 Revived extends Notification implements ShouldQueue
{
use Queueable;
public $tries = 1;
public function __construct(public Server $server)
{
if ($this->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!"
];
}
}

View File

@ -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 @@ public function __construct(public Server $server)
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 @@ public function toMail(): MailMessage
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."
];
}
}

View File

@ -7,6 +7,8 @@
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();
}
}
}

View File

@ -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());
}

View File

@ -7,7 +7,7 @@
// 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'),

View File

@ -1,3 +1,3 @@
<?php
return '4.0.0-beta.69';
return '4.0.0-beta.70';

View File

@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('servers', function (Blueprint $table) {
$table->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);
});
}
};

View File

@ -56,7 +56,7 @@ .box {
@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 {

View File

@ -8,25 +8,25 @@
<nav class="navbar-main">
<a class="{{ request()->routeIs('server.show') ? 'text-white' : '' }}"
href="{{ route('server.show', [
'server_uuid' => Route::current()->parameters()['server_uuid'],
'server_uuid' => data_get($parameters, 'server_uuid'),
]) }}">
<button>General</button>
</a>
<a class="{{ request()->routeIs('server.private-key') ? 'text-white' : '' }}"
href="{{ route('server.private-key', [
'server_uuid' => Route::current()->parameters()['server_uuid'],
'server_uuid' => data_get($parameters, 'server_uuid'),
]) }}">
<button>Private Key</button>
</a>
<a class="{{ request()->routeIs('server.proxy') ? 'text-white' : '' }}"
href="{{ route('server.proxy', [
'server_uuid' => Route::current()->parameters()['server_uuid'],
'server_uuid' => data_get($parameters, 'server_uuid'),
]) }}">
<button>Proxy</button>
</a>
<a class="{{ request()->routeIs('server.destinations') ? 'text-white' : '' }}"
href="{{ route('server.destinations', [
'server_uuid' => Route::current()->parameters()['server_uuid'],
'server_uuid' => data_get($parameters, 'server_uuid'),
]) }}">
<button>Destinations</button>
</a>

View File

@ -5,10 +5,6 @@
@if ($containerName === 'coolify-proxy')
Coolify Proxy should run on your server as you have FQDNs set up in one of your resources.
Note: The proxy should not stop unexpectedly, so please check what is going on your server.
If you don't want to use Coolify Proxy, please remove FQDN from your resources or set Proxy type to Custom(None).
@endif

View File

@ -4,7 +4,7 @@
All automations & integrations are turned off!
IMPORTANT: You have to validate your server again after you fix the issue.
IMPORTANT: We automatically try to revive your server. If your server is back online, we will automatically turn on all automations & integrations.
If you have any questions, please contact us.

View File

@ -0,0 +1,6 @@
<x-emails.layout>
Your server ({{$name}}) was offline for a while, but it is back online now. All automations & integrations are turned on again.
</x-emails.layout>

View File

@ -41,25 +41,28 @@
</div>
@endif
<h3>Build</h3>
@if ($application->could_set_build_commands())
<h3>Build</h3>
<div class="flex flex-col gap-2 xl:flex-row">
<x-forms.input placeholder="pnpm install" id="application.install_command"
label="Install Command" />
<x-forms.input placeholder="pnpm build" id="application.build_command" label="Build Command" />
<x-forms.input placeholder="pnpm start" id="application.start_command" label="Start Command" />
</div>
<div class="flex flex-col gap-2 xl:flex-row">
<x-forms.input placeholder="/" id="application.base_directory" label="Base Directory"
helper="Directory to use as root. Useful for monorepos." />
@endif
<div class="flex flex-col gap-2 xl:flex-row">
<x-forms.input placeholder="/" id="application.base_directory" label="Base Directory"
helper="Directory to use as root. Useful for monorepos." />
@if ($application->could_set_build_commands())
@if ($application->settings->is_static)
<x-forms.input placeholder="/dist" id="application.publish_directory" label="Publish Directory"
required />
@else
<x-forms.input placeholder="/" id="application.publish_directory" label="Publish Directory" />
@endif
</div>
@endif
@endif
</div>
@if ($application->dockerfile)
<x-forms.textarea label="Dockerfile" id="application.dockerfile" rows="6"> </x-forms.textarea>
@endif

View File

@ -130,7 +130,7 @@
<li class="step step-secondary">Select a Server</li>
<li class="step">Select a Destination</li>
</ul>
<div class="flex flex-col justify-center gap-2 text-left xl:flex-row">
<div class="flex flex-col justify-center gap-2 text-left xl:flex-row xl:flex-wrap">
@forelse($servers as $server)
<div class="box group" wire:click="setServer({{ $server }})">
<div class="flex flex-col mx-6">
@ -158,7 +158,7 @@
<li class="step step-secondary">Select a Server</li>
<li class="step step-secondary">Select a Destination</li>
</ul>
<div class="flex flex-col justify-center gap-2 text-left xl:flex-row">
<div class="flex flex-col justify-center gap-2 text-left xl:flex-row xl:flex-wrap">
@foreach ($standaloneDockers as $standaloneDocker)
<div class="box group" wire:click="setDestination('{{ $standaloneDocker->uuid }}')">
<div class="flex flex-col mx-6">

View File

@ -0,0 +1,9 @@
<div>
@if ($private_keys->count() === 0)
<h1>Create Private Key</h1>
<div class="subtitle">You need to create a private key before you can create a server.</div>
<livewire:private-key.create from="server" />
@else
<livewire:server.new.by-ip :private_keys="$private_keys" :limit_reached="$limit_reached" />
@endif
</div>

View File

@ -0,0 +1,4 @@
<div>
<x-server.navbar :server="$server" :parameters="$parameters" />
<livewire:destination.show :server="$server" />
</div>

View File

@ -1,4 +1,4 @@
<div>
<div x-init="$wire.validateServer(false)">
<x-modal yesOrNo modalId="deleteServer" modalTitle="Delete Server">
<x-slot:modalBody>
<p>This server will be deleted. It is not reversible. <br>Please think again..</p>
@ -25,6 +25,11 @@
@else
Server is reachable and validated.
@endif
@if ((!$server->settings->is_reachable || !$server->settings->is_usable) && $server->id !== 0)
<x-forms.button class="mt-8 mb-4 font-bold box-without-bg bg-coollabs hover:bg-coollabs-100" wire:click.prevent='validateServer' isHighlighted>
Validate Server & Install Docker Engine
</x-forms.button>
@endif
<div class="flex flex-col gap-2 pt-4">
<div class="flex flex-col w-full gap-2 lg:flex-row">
<x-forms.input id="server.name" label="Name" required />
@ -42,27 +47,12 @@
</div>
</div>
<div class="w-64">
<x-forms.checkbox instantSave helper="If you are using Cloudflare Tunnels, enable this. It will proxy all ssh requests to your server through Cloudflare.<span class='text-warning'>Coolify does not install/setup Cloudflare (cloudflared) on your server.</span>"
<x-forms.checkbox instantSave
helper="If you are using Cloudflare Tunnels, enable this. It will proxy all ssh requests to your server through Cloudflare.<span class='text-warning'>Coolify does not install/setup Cloudflare (cloudflared) on your server.</span>"
id="server.settings.is_cloudflare_tunnel" label="Cloudflare Tunnel" />
</div>
</div>
@if (!$server->settings->is_reachable)
<x-forms.button class="mt-8 mb-4 box" wire:click.prevent='validateServer'>
Validate Server
</x-forms.button>
@endif
@if ($server->settings->is_reachable && !$server->settings->is_usable && $server->id !== 0)
@if ($dockerInstallationStarted)
<x-forms.button class="mt-8 mb-4 box" wire:click.prevent='validateServer'>
Validate Server
</x-forms.button>
@else
<x-forms.button class="mt-8 mb-4 box" onclick="installDocker.showModal()"
wire:click.prevent='installDocker' isHighlighted>
Install Docker Engine 24.0
</x-forms.button>
@endif
@endif
@if ($server->isFunctional())
<h3 class="py-4">Settings</h3>
<x-forms.input id="cleanup_after_percentage" label="Disk Cleanup threshold (%)" required
@ -80,4 +70,11 @@
Delete
</x-forms.button>
@endif
<script>
Livewire.on('installDocker', () => {
console.log('asd');
installDocker.showModal();
})
</script>
</div>

View File

@ -1,4 +1,4 @@
<x-layout>
<x-server.navbar :server="$server" />
<div>
<x-server.navbar :server="$server" :parameters="$parameters" />
<livewire:server.show-private-key :server="$server" :privateKeys="$privateKeys" />
</x-layout>
</div>

View File

@ -0,0 +1,4 @@
<div>
<x-server.navbar :server="$server" :parameters="$parameters" />
<livewire:server.proxy :server="$server" />
</div>

View File

@ -1,7 +1,7 @@
<div>
<x-modal noSubmit modalId="installDocker">
<x-modal modalId="installDocker">
<x-slot:modalBody>
<livewire:activity-monitor header="Installation Logs" />
<livewire:activity-monitor header="Docker Installation Logs" />
</x-slot:modalBody>
<x-slot:modalSubmit>
<x-forms.button onclick="installDocker.close()" type="submit">
@ -9,6 +9,6 @@
</x-forms.button>
</x-slot:modalSubmit>
</x-modal>
<x-server.navbar :server="$server" />
<x-server.navbar :server="$server" :parameters="$parameters" />
<livewire:server.form :server="$server" />
</div>

View File

@ -1,3 +0,0 @@
<x-layout>
</x-layout>

View File

@ -1,4 +0,0 @@
<x-layout>
<x-server.navbar :server="$server" />
<livewire:destination.show :server="$server" />
</x-layout>

View File

@ -1,4 +0,0 @@
<x-layout>
<x-server.navbar :server="$server" />
<livewire:server.proxy :server="$server" />
</x-layout>

View File

@ -1,4 +0,0 @@
<x-layout>
<x-server.navbar :server="$server" />
<livewire:server.form :server="$server" />
</x-layout>

View File

@ -13,6 +13,10 @@
use App\Http\Livewire\Dashboard;
use App\Http\Livewire\Project\Shared\Logs;
use App\Http\Livewire\Server\All;
use App\Http\Livewire\Server\Create;
use App\Http\Livewire\Server\Destination\Show as DestinationShow;
use App\Http\Livewire\Server\PrivateKey\Show as PrivateKeyShow;
use App\Http\Livewire\Server\Proxy\Show as ProxyShow;
use App\Http\Livewire\Server\Show;
use App\Http\Livewire\Waitlist\Index as WaitlistIndex;
use App\Models\GithubApp;
@ -102,18 +106,11 @@
Route::middleware(['auth'])->group(function () {
Route::get('/servers', All::class)->name('server.all');
Route::get('/server/new', [ServerController::class, 'new_server'])->name('server.create');
Route::get('/server/new', Create::class)->name('server.create');
Route::get('/server/{server_uuid}', Show::class)->name('server.show');
Route::get('/server/{server_uuid}/proxy', fn () => view('server.proxy', [
'server' => Server::ownedByCurrentTeam(['name', 'proxy'])->whereUuid(request()->server_uuid)->firstOrFail(),
]))->name('server.proxy');
Route::get('/server/{server_uuid}/private-key', fn () => view('server.private-key', [
'server' => Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->firstOrFail(),
'privateKeys' => PrivateKey::ownedByCurrentTeam()->get()->where('is_git_related', false),
]))->name('server.private-key');
Route::get('/server/{server_uuid}/destinations', fn () => view('server.destinations', [
'server' => Server::ownedByCurrentTeam(['name', 'proxy'])->whereUuid(request()->server_uuid)->firstOrFail()
]))->name('server.destinations');
Route::get('/server/{server_uuid}/proxy', ProxyShow::class)->name('server.proxy');
Route::get('/server/{server_uuid}/private-key', PrivateKeyShow::class)->name('server.private-key');
Route::get('/server/{server_uuid}/destinations', DestinationShow::class)->name('server.destinations');
});

View File

@ -16,7 +16,7 @@
},
"fider": {
"documentation": "https://fider.io/docs",
"slogan": "A platform to collect and oragnize customer feedback.",
"slogan": "A platform to collect and organize customer feedback.",
"compose": "c2VydmljZXM6CiAgZmlkZXI6CiAgICBpbWFnZTogZ2V0ZmlkZXIvZmlkZXI6c3RhYmxlCiAgICBlbnZpcm9ubWVudDoKICAgICAgQkFTRV9VUkw6ICRTRVJWSUNFX0ZRRE5fRklERVIKICAgICAgREFUQUJBU0VfVVJMOiBwb3N0Z3JlczovLyRTRVJWSUNFX1VTRVJfTVlTUUw6JFNFUlZJQ0VfUEFTU1dPUkRfTVlTUUxAZGF0YWJhc2U6NTQzMi9maWRlcj9zc2xtb2RlPWRpc2FibGUKICAgICAgSldUX1NFQ1JFVDogJFNFUlZJQ0VfUEFTU1dPUkRfNjRfRklERVIKICAgICAgRU1BSUxfTk9SRVBMWTogJHtFTUFJTF9OT1JFUExZOi1ub3JlcGx5QGV4YW1wbGUuY29tfQogICAgICBFTUFJTF9NQUlMR1VOX0FQSTogJEVNQUlMX01BSUxHVU5fQVBJCiAgICAgIEVNQUlMX01BSUxHVU5fRE9NQUlOOiAkRU1BSUxfTUFJTEdVTl9ET01BSU4KICAgICAgRU1BSUxfTUFJTEdVTl9SRUdJT046ICRFTUFJTF9NQUlMR1VOX1JFR0lPTgogICAgICBFTUFJTF9TTVRQX0hPU1Q6ICR7RU1BSUxfU01UUF9IT1NUOi1zbXRwLm1haWxndW4uY29tfQogICAgICBFTUFJTF9TTVRQX1BPUlQ6ICR7RU1BSUxfU01UUF9QT1JUOi01ODd9CiAgICAgIEVNQUlMX1NNVFBfVVNFUk5BTUU6ICR7RU1BSUxfU01UUF9VU0VSTkFNRTotcG9zdG1hc3RlckBtYWlsZ3VuLmNvbX0KICAgICAgRU1BSUxfU01UUF9QQVNTV09SRDogJEVNQUlMX1NNVFBfUEFTU1dPUkQKICAgICAgRU1BSUxfU01UUF9FTkFCTEVfU1RBUlRUTFM6ICRFTUFJTF9TTVRQX0VOQUJMRV9TVEFSVFRMUwogICAgICBFTUFJTF9BV1NTRVNfUkVHSU9OOiAkRU1BSUxfQVdTU0VTX1JFR0lPTgogICAgICBFTUFJTF9BV1NTRVNfQUNDRVNTX0tFWV9JRDogJEVNQUlMX0FXU1NFU19BQ0NFU1NfS0VZX0lECiAgICAgIEVNQUlMX0FXU1NFU19TRUNSRVRfQUNDRVNTX0tFWTogJEVNQUlMX0FXU1NFU19TRUNSRVRfQUNDRVNTX0tFWQogIGRhdGFiYXNlOgogICAgaW1hZ2U6IHBvc3RncmVzOjEyCiAgICB2b2x1bWVzOgogICAgICAtIHBnX2RhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhCiAgICBlbnZpcm9ubWVudDoKICAgICAgUE9TVEdSRVNfVVNFUjogJFNFUlZJQ0VfVVNFUl9NWVNRTAogICAgICBQT1NUR1JFU19QQVNTV09SRDogJFNFUlZJQ0VfUEFTU1dPUkRfTVlTUUwKICAgICAgUE9TVEdSRVNfREI6ICR7UE9TVEdSRVNfREI6LWZpZGVyfQo="
},
"ghost": {

View File

@ -4,7 +4,7 @@
"version": "3.12.36"
},
"v4": {
"version": "4.0.0-beta.69"
"version": "4.0.0-beta.70"
}
}
}